Merge pull request #2314 from wpaulino/chainnotifier-subserver

chainntnfs+lnrpc/chainrpc: add ChainNotifier RPC subserver
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2019-01-21 15:19:12 -08:00
commit 375be936ce
29 files changed
+4587 -1420

No files matched your search

+249 -141
View File
@@ -1,6 +1,8 @@
package bitcoindnotify package bitcoindnotify
import ( import (
"bytes"
"encoding/hex"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
@@ -8,6 +10,7 @@ import (
"sync/atomic" "sync/atomic"
"github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
@@ -23,11 +26,6 @@ const (
) )
var ( 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.")
// ErrTransactionNotFound is an error returned when we attempt to find a // ErrTransactionNotFound is an error returned when we attempt to find a
// transaction by manually scanning the chain within a specific range // transaction by manually scanning the chain within a specific range
// but it is not found. // but it is not found.
@@ -58,7 +56,8 @@ type BitcoindNotifier struct {
started int32 // To be used atomically. started int32 // To be used atomically.
stopped int32 // To be used atomically. stopped int32 // To be used atomically.
chainConn *chain.BitcoindClient chainConn *chain.BitcoindClient
chainParams *chaincfg.Params
notificationCancels chan interface{} notificationCancels chan interface{}
notificationRegistry chan interface{} notificationRegistry chan interface{}
@@ -88,12 +87,15 @@ type BitcoindNotifier struct {
var _ chainntnfs.ChainNotifier = (*BitcoindNotifier)(nil) var _ chainntnfs.ChainNotifier = (*BitcoindNotifier)(nil)
// New returns a new BitcoindNotifier instance. This function assumes the // New returns a new BitcoindNotifier instance. This function assumes the
// bitcoind node detailed in the passed configuration is already running, and // bitcoind node detailed in the passed configuration is already running, and
// willing to accept RPC requests and new zmq clients. // willing to accept RPC requests and new zmq clients.
func New(chainConn *chain.BitcoindConn, spendHintCache chainntnfs.SpendHintCache, func New(chainConn *chain.BitcoindConn, chainParams *chaincfg.Params,
spendHintCache chainntnfs.SpendHintCache,
confirmHintCache chainntnfs.ConfirmHintCache) *BitcoindNotifier { confirmHintCache chainntnfs.ConfirmHintCache) *BitcoindNotifier {
notifier := &BitcoindNotifier{ notifier := &BitcoindNotifier{
chainParams: chainParams,
notificationCancels: make(chan interface{}), notificationCancels: make(chan interface{}),
notificationRegistry: make(chan interface{}), notificationRegistry: make(chan interface{}),
@@ -229,7 +231,8 @@ out:
defer b.wg.Done() defer b.wg.Done()
confDetails, _, err := b.historicalConfDetails( confDetails, _, err := b.historicalConfDetails(
msg.TxID, msg.StartHeight, msg.EndHeight, msg.ConfRequest,
msg.StartHeight, msg.EndHeight,
) )
if err != nil { if err != nil {
chainntnfs.Log.Error(err) chainntnfs.Log.Error(err)
@@ -244,7 +247,7 @@ out:
// cache at tip, since any pending // cache at tip, since any pending
// rescans have now completed. // rescans have now completed.
err = b.txNotifier.UpdateConfDetails( err = b.txNotifier.UpdateConfDetails(
*msg.TxID, confDetails, msg.ConfRequest, confDetails,
) )
if err != nil { if err != nil {
chainntnfs.Log.Error(err) chainntnfs.Log.Error(err)
@@ -265,29 +268,50 @@ out:
if err != nil { if err != nil {
chainntnfs.Log.Errorf("Rescan to "+ chainntnfs.Log.Errorf("Rescan to "+
"determine the spend "+ "determine the spend "+
"details of %v failed: %v", "details of %v within "+
msg.OutPoint, err) "range %d-%d failed: %v",
msg.SpendRequest,
msg.StartHeight,
msg.EndHeight, err)
} }
}() }()
case *blockEpochRegistration: case *blockEpochRegistration:
chainntnfs.Log.Infof("New block epoch subscription") chainntnfs.Log.Infof("New block epoch subscription")
b.blockEpochClients[msg.epochID] = msg b.blockEpochClients[msg.epochID] = msg
if msg.bestBlock != nil {
missedBlocks, err := // If the client did not provide their best
chainntnfs.GetClientMissedBlocks( // known block, then we'll immediately dispatch
b.chainConn, msg.bestBlock, // a notification for the current tip.
b.bestBlock.Height, true, if msg.bestBlock == nil {
) b.notifyBlockEpochClient(
if err != nil { msg, b.bestBlock.Height,
msg.errorChan <- err b.bestBlock.Hash,
continue )
}
for _, block := range missedBlocks { msg.errorChan <- nil
b.notifyBlockEpochClient(msg, continue
block.Height, block.Hash)
}
} }
// Otherwise, we'll attempt to deliver the
// backlog of notifications from their best
// known block.
missedBlocks, err := chainntnfs.GetClientMissedBlocks(
b.chainConn, msg.bestBlock,
b.bestBlock.Height, true,
)
if err != nil {
msg.errorChan <- err
continue
}
for _, block := range missedBlocks {
b.notifyBlockEpochClient(
msg, block.Height, block.Hash,
)
}
msg.errorChan <- nil msg.errorChan <- nil
} }
@@ -372,14 +396,14 @@ out:
continue continue
} }
tx := &item.TxRecord.MsgTx tx := btcutil.NewTx(&item.TxRecord.MsgTx)
err := b.txNotifier.ProcessRelevantSpendTx( err := b.txNotifier.ProcessRelevantSpendTx(
tx, item.Block.Height, tx, uint32(item.Block.Height),
) )
if err != nil { if err != nil {
chainntnfs.Log.Errorf("Unable to "+ chainntnfs.Log.Errorf("Unable to "+
"process transaction %v: %v", "process transaction %v: %v",
tx.TxHash(), err) tx.Hash(), err)
} }
} }
@@ -390,15 +414,28 @@ out:
b.wg.Done() b.wg.Done()
} }
// historicalConfDetails looks up whether a transaction is already included in a // historicalConfDetails looks up whether a confirmation request (txid/output
// block in the active chain and, if so, returns details about the confirmation. // script) has already been included in a block in the active chain and, if so,
func (b *BitcoindNotifier) historicalConfDetails(txid *chainhash.Hash, // returns details about said block.
func (b *BitcoindNotifier) historicalConfDetails(confRequest chainntnfs.ConfRequest,
startHeight, endHeight uint32) (*chainntnfs.TxConfirmation, startHeight, endHeight uint32) (*chainntnfs.TxConfirmation,
chainntnfs.TxConfStatus, error) { chainntnfs.TxConfStatus, error) {
// If a txid was not provided, then we should dispatch upon seeing the
// script on-chain, so we'll short-circuit straight to scanning manually
// as there doesn't exist a script index to query.
if confRequest.TxID == chainntnfs.ZeroHash {
return b.confDetailsManually(
confRequest, startHeight, endHeight,
)
}
// Otherwise, we'll dispatch upon seeing a transaction on-chain with the
// given hash.
//
// We'll first attempt to retrieve the transaction using the node's // We'll first attempt to retrieve the transaction using the node's
// txindex. // txindex.
txConf, txStatus, err := b.confDetailsFromTxIndex(txid) txConf, txStatus, err := b.confDetailsFromTxIndex(&confRequest.TxID)
// We'll then check the status of the transaction lookup returned to // We'll then check the status of the transaction lookup returned to
// determine whether we should proceed with any fallback methods. // determine whether we should proceed with any fallback methods.
@@ -409,7 +446,7 @@ func (b *BitcoindNotifier) historicalConfDetails(txid *chainhash.Hash,
case err != nil: case err != nil:
chainntnfs.Log.Debugf("Failed getting conf details from "+ chainntnfs.Log.Debugf("Failed getting conf details from "+
"index (%v), scanning manually", err) "index (%v), scanning manually", err)
return b.confDetailsManually(txid, startHeight, endHeight) return b.confDetailsManually(confRequest, startHeight, endHeight)
// The transaction was found within the node's mempool. // The transaction was found within the node's mempool.
case txStatus == chainntnfs.TxFoundMempool: case txStatus == chainntnfs.TxFoundMempool:
@@ -440,7 +477,7 @@ func (b *BitcoindNotifier) confDetailsFromTxIndex(txid *chainhash.Hash,
// If the transaction has some or all of its confirmations required, // If the transaction has some or all of its confirmations required,
// then we may be able to dispatch it immediately. // then we may be able to dispatch it immediately.
tx, err := b.chainConn.GetRawTransactionVerbose(txid) rawTxRes, err := b.chainConn.GetRawTransactionVerbose(txid)
if err != nil { if err != nil {
// If the transaction lookup was successful, but it wasn't found // If the transaction lookup was successful, but it wasn't found
// within the index itself, then we can exit early. We'll also // within the index itself, then we can exit early. We'll also
@@ -461,20 +498,19 @@ func (b *BitcoindNotifier) confDetailsFromTxIndex(txid *chainhash.Hash,
// Make sure we actually retrieved a transaction that is included in a // Make sure we actually retrieved a transaction that is included in a
// block. If not, the transaction must be unconfirmed (in the mempool), // block. If not, the transaction must be unconfirmed (in the mempool),
// and we'll return TxFoundMempool together with a nil TxConfirmation. // and we'll return TxFoundMempool together with a nil TxConfirmation.
if tx.BlockHash == "" { if rawTxRes.BlockHash == "" {
return nil, chainntnfs.TxFoundMempool, nil return nil, chainntnfs.TxFoundMempool, nil
} }
// As we need to fully populate the returned TxConfirmation struct, // As we need to fully populate the returned TxConfirmation struct,
// grab the block in which the transaction was confirmed so we can // grab the block in which the transaction was confirmed so we can
// locate its exact index within the block. // locate its exact index within the block.
blockHash, err := chainhash.NewHashFromStr(tx.BlockHash) blockHash, err := chainhash.NewHashFromStr(rawTxRes.BlockHash)
if err != nil { if err != nil {
return nil, chainntnfs.TxNotFoundIndex, return nil, chainntnfs.TxNotFoundIndex,
fmt.Errorf("unable to get block hash %v for "+ fmt.Errorf("unable to get block hash %v for "+
"historical dispatch: %v", tx.BlockHash, err) "historical dispatch: %v", rawTxRes.BlockHash, err)
} }
block, err := b.chainConn.GetBlockVerbose(blockHash) block, err := b.chainConn.GetBlockVerbose(blockHash)
if err != nil { if err != nil {
return nil, chainntnfs.TxNotFoundIndex, return nil, chainntnfs.TxNotFoundIndex,
@@ -484,36 +520,49 @@ func (b *BitcoindNotifier) confDetailsFromTxIndex(txid *chainhash.Hash,
// If the block was obtained, locate the transaction's index within the // If the block was obtained, locate the transaction's index within the
// block so we can give the subscriber full confirmation details. // block so we can give the subscriber full confirmation details.
targetTxidStr := txid.String() txidStr := txid.String()
for txIndex, txHash := range block.Tx { for txIndex, txHash := range block.Tx {
if txHash == targetTxidStr { if txHash != txidStr {
details := &chainntnfs.TxConfirmation{ continue
BlockHash: blockHash,
BlockHeight: uint32(block.Height),
TxIndex: uint32(txIndex),
}
return details, chainntnfs.TxFoundIndex, nil
} }
// Deserialize the hex-encoded transaction to include it in the
// confirmation details.
rawTx, err := hex.DecodeString(rawTxRes.Hex)
if err != nil {
return nil, chainntnfs.TxFoundIndex,
fmt.Errorf("unable to deserialize tx %v: %v",
txHash, err)
}
var tx wire.MsgTx
if err := tx.Deserialize(bytes.NewReader(rawTx)); err != nil {
return nil, chainntnfs.TxFoundIndex,
fmt.Errorf("unable to deserialize tx %v: %v",
txHash, err)
}
return &chainntnfs.TxConfirmation{
Tx: &tx,
BlockHash: blockHash,
BlockHeight: uint32(block.Height),
TxIndex: uint32(txIndex),
}, chainntnfs.TxFoundIndex, nil
} }
// We return an error because we should have found the transaction // We return an error because we should have found the transaction
// within the block, but didn't. // within the block, but didn't.
return nil, chainntnfs.TxNotFoundIndex, return nil, chainntnfs.TxNotFoundIndex, fmt.Errorf("unable to locate "+
fmt.Errorf("unable to locate tx %v in block %v", txid, "tx %v in block %v", txid, blockHash)
blockHash)
} }
// confDetailsManually looks up whether a transaction is already included in a // confDetailsManually looks up whether a transaction/output script has already
// block in the active chain by scanning the chain's blocks, starting from the // been included in a block in the active chain by scanning the chain's blocks
// earliest height the transaction could have been included in, to the current // within the given range. If the transaction/output script is found, its
// height in the chain. If the transaction is found, its confirmation details // confirmation details are returned. Otherwise, nil is returned.
// are returned. Otherwise, nil is returned. func (b *BitcoindNotifier) confDetailsManually(confRequest chainntnfs.ConfRequest,
func (b *BitcoindNotifier) confDetailsManually(txid *chainhash.Hash,
heightHint, currentHeight uint32) (*chainntnfs.TxConfirmation, heightHint, currentHeight uint32) (*chainntnfs.TxConfirmation,
chainntnfs.TxConfStatus, error) { chainntnfs.TxConfStatus, error) {
targetTxidStr := txid.String()
// Begin scanning blocks at every height to determine where the // Begin scanning blocks at every height to determine where the
// transaction was included in. // transaction was included in.
for height := currentHeight; height >= heightHint && height > 0; height-- { for height := currentHeight; height >= heightHint && height > 0; height-- {
@@ -522,7 +571,7 @@ func (b *BitcoindNotifier) confDetailsManually(txid *chainhash.Hash,
select { select {
case <-b.quit: case <-b.quit:
return nil, chainntnfs.TxNotFoundManually, return nil, chainntnfs.TxNotFoundManually,
ErrChainNotifierShuttingDown chainntnfs.ErrChainNotifierShuttingDown
default: default:
} }
@@ -533,24 +582,27 @@ func (b *BitcoindNotifier) confDetailsManually(txid *chainhash.Hash,
"with height %d", height) "with height %d", height)
} }
block, err := b.chainConn.GetBlockVerbose(blockHash) block, err := b.chainConn.GetBlock(blockHash)
if err != nil { if err != nil {
return nil, chainntnfs.TxNotFoundManually, return nil, chainntnfs.TxNotFoundManually,
fmt.Errorf("unable to get block with hash "+ fmt.Errorf("unable to get block with hash "+
"%v: %v", blockHash, err) "%v: %v", blockHash, err)
} }
for txIndex, txHash := range block.Tx { // For every transaction in the block, check which one matches
// If we're able to find the transaction in this block, // our request. If we find one that does, we can dispatch its
// return its confirmation details. // confirmation details.
if txHash == targetTxidStr { for txIndex, tx := range block.Transactions {
details := &chainntnfs.TxConfirmation{ if !confRequest.MatchesTx(tx) {
BlockHash: blockHash, continue
BlockHeight: height,
TxIndex: uint32(txIndex),
}
return details, chainntnfs.TxFoundManually, nil
} }
return &chainntnfs.TxConfirmation{
Tx: tx,
BlockHash: blockHash,
BlockHeight: height,
TxIndex: uint32(txIndex),
}, chainntnfs.TxFoundManually, nil
} }
} }
@@ -620,33 +672,57 @@ func (b *BitcoindNotifier) notifyBlockEpochClient(epochClient *blockEpochRegistr
} }
// RegisterSpendNtfn registers an intent to be notified once the target // 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/output script has been spent by a transaction on-chain. When
// outpoint has been detected, the details of the spending event will be sent // intending to be notified of the spend of an output script, a nil outpoint
// across the 'Spend' channel. The heightHint should represent the earliest // must be used. The heightHint should represent the earliest height in the
// height in the chain where the transaction could have been spent in. // chain of the transaction that spent the outpoint/output script.
//
// Once a spend of has been detected, the details of the spending event will be
// sent across the 'Spend' channel.
func (b *BitcoindNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, func (b *BitcoindNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) { pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) {
// First, we'll construct a spend notification request and hand it off // First, we'll construct a spend notification request and hand it off
// to the txNotifier. // to the txNotifier.
spendID := atomic.AddUint64(&b.spendClientCounter, 1) spendID := atomic.AddUint64(&b.spendClientCounter, 1)
cancel := func() { spendRequest, err := chainntnfs.NewSpendRequest(outpoint, pkScript)
b.txNotifier.CancelSpend(*outpoint, spendID) if err != nil {
return nil, err
} }
ntfn := &chainntnfs.SpendNtfn{ ntfn := &chainntnfs.SpendNtfn{
SpendID: spendID, SpendID: spendID,
OutPoint: *outpoint, SpendRequest: spendRequest,
PkScript: pkScript, Event: chainntnfs.NewSpendEvent(func() {
Event: chainntnfs.NewSpendEvent(cancel), b.txNotifier.CancelSpend(spendRequest, spendID)
}),
HeightHint: heightHint, HeightHint: heightHint,
} }
historicalDispatch, err := b.txNotifier.RegisterSpend(ntfn) historicalDispatch, _, err := b.txNotifier.RegisterSpend(ntfn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// We'll then request the backend to notify us when it has detected the
// outpoint/output script as spent.
//
// TODO(wilmer): use LoadFilter API instead.
if spendRequest.OutPoint == chainntnfs.ZeroOutPoint {
addr, err := spendRequest.PkScript.Address(b.chainParams)
if err != nil {
return nil, err
}
addrs := []btcutil.Address{addr}
if err := b.chainConn.NotifyReceived(addrs); err != nil {
return nil, err
}
} else {
ops := []*wire.OutPoint{&spendRequest.OutPoint}
if err := b.chainConn.NotifySpent(ops); err != nil {
return nil, err
}
}
// If the txNotifier didn't return any details to perform a historical // If the txNotifier didn't return any details to perform a historical
// scan of the chain, then we can return early as there's nothing left // scan of the chain, then we can return early as there's nothing left
// for us to do. // for us to do.
@@ -654,23 +730,39 @@ func (b *BitcoindNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
return ntfn.Event, nil return ntfn.Event, nil
} }
// We'll then request the backend to notify us when it has detected the // Otherwise, we'll need to dispatch a historical rescan to determine if
// outpoint as spent. // the outpoint was already spent at a previous height.
if err := b.chainConn.NotifySpent([]*wire.OutPoint{outpoint}); err != nil { //
return nil, err // We'll short-circuit the path when dispatching the spend of a script,
// rather than an outpoint, as there aren't any additional checks we can
// make for scripts.
if spendRequest.OutPoint == chainntnfs.ZeroOutPoint {
select {
case b.notificationRegistry <- historicalDispatch:
case <-b.quit:
return nil, chainntnfs.ErrChainNotifierShuttingDown
}
return ntfn.Event, nil
} }
// In addition to the check above, we'll also check the backend's UTXO // When dispatching spends of outpoints, there are a number of checks we
// set to determine whether the outpoint has been spent. If it hasn't, // can make to start our rescan from a better height or completely avoid
// we can return to the caller as well. // it.
txOut, err := b.chainConn.GetTxOut(&outpoint.Hash, outpoint.Index, true) //
// We'll start by checking the backend's UTXO set to determine whether
// the outpoint has been spent. If it hasn't, we can return to the
// caller as well.
txOut, err := b.chainConn.GetTxOut(
&spendRequest.OutPoint.Hash, spendRequest.OutPoint.Index, true,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if txOut != nil { if txOut != nil {
// We'll let the txNotifier know the outpoint is still unspent // We'll let the txNotifier know the outpoint is still unspent
// in order to begin updating its spend hint. // in order to begin updating its spend hint.
err := b.txNotifier.UpdateSpendDetails(*outpoint, nil) err := b.txNotifier.UpdateSpendDetails(spendRequest, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -678,22 +770,21 @@ func (b *BitcoindNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
return ntfn.Event, nil return ntfn.Event, nil
} }
// Otherwise, we'll determine when the output was spent by scanning the // Since the outpoint was spent, as it no longer exists within the UTXO
// chain. We'll begin by determining where to start our historical // set, we'll determine when it happened by scanning the chain.
// rescan.
// //
// As a minimal optimization, we'll query the backend's transaction // As a minimal optimization, we'll query the backend's transaction
// index (if enabled) to determine if we have a better rescan starting // index (if enabled) to determine if we have a better rescan starting
// height. We can do this as the GetRawTransaction call will return the // height. We can do this as the GetRawTransaction call will return the
// hash of the block it was included in within the chain. // hash of the block it was included in within the chain.
tx, err := b.chainConn.GetRawTransactionVerbose(&outpoint.Hash) tx, err := b.chainConn.GetRawTransactionVerbose(&spendRequest.OutPoint.Hash)
if err != nil { if err != nil {
// Avoid returning an error if the transaction was not found to // Avoid returning an error if the transaction was not found to
// proceed with fallback methods. // proceed with fallback methods.
jsonErr, ok := err.(*btcjson.RPCError) jsonErr, ok := err.(*btcjson.RPCError)
if !ok || jsonErr.Code != btcjson.ErrRPCNoTxInfo { if !ok || jsonErr.Code != btcjson.ErrRPCNoTxInfo {
return nil, fmt.Errorf("unable to query for "+ return nil, fmt.Errorf("unable to query for txid %v: %v",
"txid %v: %v", outpoint.Hash, err) spendRequest.OutPoint.Hash, err)
} }
} }
@@ -722,23 +813,24 @@ func (b *BitcoindNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
} }
// Now that we've determined the starting point of our rescan, we can // Now that we've determined the starting point of our rescan, we can
// dispatch it. // dispatch it and return.
select { select {
case b.notificationRegistry <- historicalDispatch: case b.notificationRegistry <- historicalDispatch:
return ntfn.Event, nil
case <-b.quit: case <-b.quit:
return nil, ErrChainNotifierShuttingDown return nil, chainntnfs.ErrChainNotifierShuttingDown
} }
return ntfn.Event, nil
} }
// disaptchSpendDetailsManually attempts to manually scan the chain within the // disaptchSpendDetailsManually attempts to manually scan the chain within the
// given height range for a transaction that spends the given outpoint. If one // given height range for a transaction that spends the given outpoint/output
// is found, it's spending details are sent to the notifier dispatcher, which // script. If one is found, it's spending details are sent to the TxNotifier,
// will then dispatch the notification to all of its clients. // which will then dispatch the notification to all of its clients.
func (b *BitcoindNotifier) dispatchSpendDetailsManually( func (b *BitcoindNotifier) dispatchSpendDetailsManually(
historicalDispatchDetails *chainntnfs.HistoricalSpendDispatch) error { historicalDispatchDetails *chainntnfs.HistoricalSpendDispatch) error {
op := historicalDispatchDetails.OutPoint spendRequest := historicalDispatchDetails.SpendRequest
startHeight := historicalDispatchDetails.StartHeight startHeight := historicalDispatchDetails.StartHeight
endHeight := historicalDispatchDetails.EndHeight endHeight := historicalDispatchDetails.EndHeight
@@ -749,7 +841,7 @@ func (b *BitcoindNotifier) dispatchSpendDetailsManually(
// processing the next height. // processing the next height.
select { select {
case <-b.quit: case <-b.quit:
return ErrChainNotifierShuttingDown return chainntnfs.ErrChainNotifierShuttingDown
default: default:
} }
@@ -765,61 +857,75 @@ func (b *BitcoindNotifier) dispatchSpendDetailsManually(
"%v: %v", blockHash, err) "%v: %v", blockHash, err)
} }
// Then, we'll manually go over every transaction in it and // Then, we'll manually go over every input in every transaction
// determine whether it spends the outpoint in question. // in it and determine whether it spends the request in
// question. If we find one, we'll dispatch the spend details.
for _, tx := range block.Transactions { for _, tx := range block.Transactions {
for i, txIn := range tx.TxIn { matches, inputIdx, err := spendRequest.MatchesTx(tx)
if txIn.PreviousOutPoint != op { if err != nil {
continue return err
}
// If it does, we'll construct its spend details
// and hand them over to the TxNotifier so that
// it can properly notify its registered
// clients.
txHash := tx.TxHash()
details := &chainntnfs.SpendDetail{
SpentOutPoint: &op,
SpenderTxHash: &txHash,
SpendingTx: tx,
SpenderInputIndex: uint32(i),
SpendingHeight: int32(height),
}
return b.txNotifier.UpdateSpendDetails(
op, details,
)
} }
if !matches {
continue
}
txHash := tx.TxHash()
details := &chainntnfs.SpendDetail{
SpentOutPoint: &tx.TxIn[inputIdx].PreviousOutPoint,
SpenderTxHash: &txHash,
SpendingTx: tx,
SpenderInputIndex: inputIdx,
SpendingHeight: int32(height),
}
return b.txNotifier.UpdateSpendDetails(
historicalDispatchDetails.SpendRequest,
details,
)
} }
} }
return ErrTransactionNotFound return ErrTransactionNotFound
} }
// RegisterConfirmationsNtfn registers a notification with BitcoindNotifier // RegisterConfirmationsNtfn registers an intent to be notified once the target
// which will be triggered once the txid reaches numConfs number of // txid/output script has reached numConfs confirmations on-chain. When
// confirmations. // intending to be notified of the confirmation of an output script, a nil txid
// must be used. The heightHint should represent the earliest height at which
// the txid/output script could have been included in the chain.
//
// Progress on the number of confirmations left can be read from the 'Updates'
// channel. Once it has reached all of its confirmations, a notification will be
// sent across the 'Confirmed' channel.
func (b *BitcoindNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, func (b *BitcoindNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash,
_ []byte, numConfs, heightHint uint32) (*chainntnfs.ConfirmationEvent, error) { pkScript []byte,
numConfs, heightHint uint32) (*chainntnfs.ConfirmationEvent, error) {
// Construct a notification request for the transaction and send it to // Construct a notification request for the transaction and send it to
// the main event loop. // the main event loop.
confID := atomic.AddUint64(&b.confClientCounter, 1)
confRequest, err := chainntnfs.NewConfRequest(txid, pkScript)
if err != nil {
return nil, err
}
ntfn := &chainntnfs.ConfNtfn{ ntfn := &chainntnfs.ConfNtfn{
ConfID: atomic.AddUint64(&b.confClientCounter, 1), ConfID: confID,
TxID: txid, ConfRequest: confRequest,
NumConfirmations: numConfs, NumConfirmations: numConfs,
Event: chainntnfs.NewConfirmationEvent(numConfs), Event: chainntnfs.NewConfirmationEvent(numConfs, func() {
HeightHint: heightHint, b.txNotifier.CancelConf(confRequest, confID)
}),
HeightHint: heightHint,
} }
chainntnfs.Log.Infof("New confirmation subscription: "+ chainntnfs.Log.Infof("New confirmation subscription: %v, num_confs=%v",
"txid=%v, numconfs=%v", txid, numConfs) confRequest, numConfs)
// Register the conf notification with the TxNotifier. A non-nil value // Register the conf notification with the TxNotifier. A non-nil value
// for `dispatch` will be returned if we are required to perform a // for `dispatch` will be returned if we are required to perform a
// manual scan for the confirmation. Otherwise the notifier will begin // manual scan for the confirmation. Otherwise the notifier will begin
// watching at tip for the transaction to confirm. // watching at tip for the transaction to confirm.
dispatch, err := b.txNotifier.RegisterConf(ntfn) dispatch, _, err := b.txNotifier.RegisterConf(ntfn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -832,7 +938,7 @@ func (b *BitcoindNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash,
case b.notificationRegistry <- dispatch: case b.notificationRegistry <- dispatch:
return ntfn.Event, nil return ntfn.Event, nil
case <-b.quit: case <-b.quit:
return nil, ErrChainNotifierShuttingDown return nil, chainntnfs.ErrChainNotifierShuttingDown
} }
} }
@@ -863,7 +969,9 @@ type epochCancel struct {
// RegisterBlockEpochNtfn returns a BlockEpochEvent which subscribes the // RegisterBlockEpochNtfn returns a BlockEpochEvent which subscribes the
// caller to receive notifications, of each new block connected to the main // caller to receive notifications, of each new block connected to the main
// chain. Clients have the option of passing in their best known block, which // chain. Clients have the option of passing in their best known block, which
// the notifier uses to check if they are behind on blocks and catch them up. // the notifier uses to check if they are behind on blocks and catch them up. If
// they do not provide one, then a notification will be dispatched immediately
// for the current tip of the chain upon a successful registration.
func (b *BitcoindNotifier) RegisterBlockEpochNtfn( func (b *BitcoindNotifier) RegisterBlockEpochNtfn(
bestBlock *chainntnfs.BlockEpoch) (*chainntnfs.BlockEpochEvent, error) { bestBlock *chainntnfs.BlockEpoch) (*chainntnfs.BlockEpochEvent, error) {
+47 -11
View File
@@ -3,6 +3,7 @@
package bitcoindnotify package bitcoindnotify
import ( import (
"bytes"
"io/ioutil" "io/ioutil"
"testing" "testing"
"time" "time"
@@ -14,6 +15,20 @@ import (
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
) )
var (
testScript = []byte{
// OP_HASH160
0xA9,
// OP_DATA_20
0x14,
// <20-byte hash>
0xec, 0x6f, 0x7a, 0x5a, 0xa8, 0xf2, 0xb1, 0x0c, 0xa5, 0x15,
0x04, 0x52, 0x3a, 0x60, 0xd4, 0x03, 0x06, 0xf6, 0x96, 0xcd,
// OP_EQUAL
0x87,
}
)
func initHintCache(t *testing.T) *chainntnfs.HeightHintCache { func initHintCache(t *testing.T) *chainntnfs.HeightHintCache {
t.Helper() t.Helper()
@@ -41,7 +56,10 @@ func setUpNotifier(t *testing.T, bitcoindConn *chain.BitcoindConn,
t.Helper() t.Helper()
notifier := New(bitcoindConn, spendHintCache, confirmHintCache) notifier := New(
bitcoindConn, chainntnfs.NetParams, spendHintCache,
confirmHintCache,
)
if err := notifier.Start(); err != nil { if err := notifier.Start(); err != nil {
t.Fatalf("unable to start notifier: %v", err) t.Fatalf("unable to start notifier: %v", err)
} }
@@ -104,8 +122,13 @@ func TestHistoricalConfDetailsTxIndex(t *testing.T) {
// A transaction unknown to the node should not be found within the // A transaction unknown to the node should not be found within the
// txindex even if it is enabled, so we should not proceed with any // txindex even if it is enabled, so we should not proceed with any
// fallback methods. // fallback methods.
var zeroHash chainhash.Hash var unknownHash chainhash.Hash
_, txStatus, err := notifier.historicalConfDetails(&zeroHash, 0, 0) copy(unknownHash[:], bytes.Repeat([]byte{0x10}, 32))
unknownConfReq, err := chainntnfs.NewConfRequest(&unknownHash, testScript)
if err != nil {
t.Fatalf("unable to create conf request: %v", err)
}
_, txStatus, err := notifier.historicalConfDetails(unknownConfReq, 0, 0)
if err != nil { if err != nil {
t.Fatalf("unable to retrieve historical conf details: %v", err) t.Fatalf("unable to retrieve historical conf details: %v", err)
} }
@@ -120,16 +143,20 @@ func TestHistoricalConfDetailsTxIndex(t *testing.T) {
// Now, we'll create a test transaction, confirm it, and attempt to // Now, we'll create a test transaction, confirm it, and attempt to
// retrieve its confirmation details. // retrieve its confirmation details.
txid, _, err := chainntnfs.GetTestTxidAndScript(miner) txid, pkScript, err := chainntnfs.GetTestTxidAndScript(miner)
if err != nil { if err != nil {
t.Fatalf("unable to create tx: %v", err) t.Fatalf("unable to create tx: %v", err)
} }
if err := chainntnfs.WaitForMempoolTx(miner, txid); err != nil { if err := chainntnfs.WaitForMempoolTx(miner, txid); err != nil {
t.Fatal(err) t.Fatal(err)
} }
confReq, err := chainntnfs.NewConfRequest(txid, pkScript)
if err != nil {
t.Fatalf("unable to create conf request: %v", err)
}
// The transaction should be found in the mempool at this point. // The transaction should be found in the mempool at this point.
_, txStatus, err = notifier.historicalConfDetails(txid, 0, 0) _, txStatus, err = notifier.historicalConfDetails(confReq, 0, 0)
if err != nil { if err != nil {
t.Fatalf("unable to retrieve historical conf details: %v", err) t.Fatalf("unable to retrieve historical conf details: %v", err)
} }
@@ -151,7 +178,7 @@ func TestHistoricalConfDetailsTxIndex(t *testing.T) {
// the txindex includes the transaction just mined. // the txindex includes the transaction just mined.
syncNotifierWithMiner(t, notifier, miner) syncNotifierWithMiner(t, notifier, miner)
_, txStatus, err = notifier.historicalConfDetails(txid, 0, 0) _, txStatus, err = notifier.historicalConfDetails(confReq, 0, 0)
if err != nil { if err != nil {
t.Fatalf("unable to retrieve historical conf details: %v", err) t.Fatalf("unable to retrieve historical conf details: %v", err)
} }
@@ -186,10 +213,15 @@ func TestHistoricalConfDetailsNoTxIndex(t *testing.T) {
// Since the node has its txindex disabled, we fall back to scanning the // Since the node has its txindex disabled, we fall back to scanning the
// chain manually. A transaction unknown to the network should not be // chain manually. A transaction unknown to the network should not be
// found. // found.
var zeroHash chainhash.Hash var unknownHash chainhash.Hash
copy(unknownHash[:], bytes.Repeat([]byte{0x10}, 32))
unknownConfReq, err := chainntnfs.NewConfRequest(&unknownHash, testScript)
if err != nil {
t.Fatalf("unable to create conf request: %v", err)
}
broadcastHeight := syncNotifierWithMiner(t, notifier, miner) broadcastHeight := syncNotifierWithMiner(t, notifier, miner)
_, txStatus, err := notifier.historicalConfDetails( _, txStatus, err := notifier.historicalConfDetails(
&zeroHash, uint32(broadcastHeight), uint32(broadcastHeight), unknownConfReq, uint32(broadcastHeight), uint32(broadcastHeight),
) )
if err != nil { if err != nil {
t.Fatalf("unable to retrieve historical conf details: %v", err) t.Fatalf("unable to retrieve historical conf details: %v", err)
@@ -210,8 +242,8 @@ func TestHistoricalConfDetailsNoTxIndex(t *testing.T) {
// one output, which we will manually spend. The backend node's // one output, which we will manually spend. The backend node's
// transaction index should also be disabled, which we've already // transaction index should also be disabled, which we've already
// ensured above. // ensured above.
output, pkScript := chainntnfs.CreateSpendableOutput(t, miner) outpoint, output, privKey := chainntnfs.CreateSpendableOutput(t, miner)
spendTx := chainntnfs.CreateSpendTx(t, output, pkScript) spendTx := chainntnfs.CreateSpendTx(t, outpoint, output, privKey)
spendTxHash, err := miner.Node.SendRawTransaction(spendTx, true) spendTxHash, err := miner.Node.SendRawTransaction(spendTx, true)
if err != nil { if err != nil {
t.Fatalf("unable to broadcast tx: %v", err) t.Fatalf("unable to broadcast tx: %v", err)
@@ -225,9 +257,13 @@ func TestHistoricalConfDetailsNoTxIndex(t *testing.T) {
// Ensure the notifier and miner are synced to the same height to ensure // Ensure the notifier and miner are synced to the same height to ensure
// we can find the transaction when manually scanning the chain. // we can find the transaction when manually scanning the chain.
confReq, err := chainntnfs.NewConfRequest(&outpoint.Hash, output.PkScript)
if err != nil {
t.Fatalf("unable to create conf request: %v", err)
}
currentHeight := syncNotifierWithMiner(t, notifier, miner) currentHeight := syncNotifierWithMiner(t, notifier, miner)
_, txStatus, err = notifier.historicalConfDetails( _, txStatus, err = notifier.historicalConfDetails(
&output.Hash, uint32(broadcastHeight), uint32(currentHeight), confReq, uint32(broadcastHeight), uint32(currentHeight),
) )
if err != nil { if err != nil {
t.Fatalf("unable to retrieve historical conf details: %v", err) t.Fatalf("unable to retrieve historical conf details: %v", err)
+13 -6
View File
@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcwallet/chain" "github.com/btcsuite/btcwallet/chain"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
) )
@@ -11,9 +12,9 @@ import (
// createNewNotifier creates a new instance of the ChainNotifier interface // createNewNotifier creates a new instance of the ChainNotifier interface
// implemented by BitcoindNotifier. // implemented by BitcoindNotifier.
func createNewNotifier(args ...interface{}) (chainntnfs.ChainNotifier, error) { func createNewNotifier(args ...interface{}) (chainntnfs.ChainNotifier, error) {
if len(args) != 3 { if len(args) != 4 {
return nil, fmt.Errorf("incorrect number of arguments to "+ return nil, fmt.Errorf("incorrect number of arguments to "+
".New(...), expected 2, instead passed %v", len(args)) ".New(...), expected 4, instead passed %v", len(args))
} }
chainConn, ok := args[0].(*chain.BitcoindConn) chainConn, ok := args[0].(*chain.BitcoindConn)
@@ -22,19 +23,25 @@ func createNewNotifier(args ...interface{}) (chainntnfs.ChainNotifier, error) {
"is incorrect, expected a *chain.BitcoindConn") "is incorrect, expected a *chain.BitcoindConn")
} }
spendHintCache, ok := args[1].(chainntnfs.SpendHintCache) chainParams, ok := args[1].(*chaincfg.Params)
if !ok { if !ok {
return nil, errors.New("second argument to bitcoindnotify.New " + return nil, errors.New("second argument to bitcoindnotify.New " +
"is incorrect, expected a *chaincfg.Params")
}
spendHintCache, ok := args[2].(chainntnfs.SpendHintCache)
if !ok {
return nil, errors.New("third argument to bitcoindnotify.New " +
"is incorrect, expected a chainntnfs.SpendHintCache") "is incorrect, expected a chainntnfs.SpendHintCache")
} }
confirmHintCache, ok := args[2].(chainntnfs.ConfirmHintCache) confirmHintCache, ok := args[3].(chainntnfs.ConfirmHintCache)
if !ok { if !ok {
return nil, errors.New("third argument to bitcoindnotify.New " + return nil, errors.New("fourth argument to bitcoindnotify.New " +
"is incorrect, expected a chainntnfs.ConfirmHintCache") "is incorrect, expected a chainntnfs.ConfirmHintCache")
} }
return New(chainConn, spendHintCache, confirmHintCache), nil return New(chainConn, chainParams, spendHintCache, confirmHintCache), nil
} }
// init registers a driver for the BtcdNotifier concrete implementation of the // init registers a driver for the BtcdNotifier concrete implementation of the
+248 -119
View File
@@ -1,6 +1,8 @@
package btcdnotify package btcdnotify
import ( import (
"bytes"
"encoding/hex"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
@@ -9,6 +11,7 @@ import (
"time" "time"
"github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
@@ -23,13 +26,6 @@ const (
notifierType = "btcd" notifierType = "btcd"
) )
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 // 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 // used as an element within an unbounded queue in order to avoid blocking the
// main rpc dispatch rule. // main rpc dispatch rule.
@@ -64,7 +60,8 @@ type BtcdNotifier struct {
started int32 // To be used atomically. started int32 // To be used atomically.
stopped int32 // To be used atomically. stopped int32 // To be used atomically.
chainConn *rpcclient.Client chainConn *rpcclient.Client
chainParams *chaincfg.Params
notificationCancels chan interface{} notificationCancels chan interface{}
notificationRegistry chan interface{} notificationRegistry chan interface{}
@@ -98,10 +95,13 @@ var _ chainntnfs.ChainNotifier = (*BtcdNotifier)(nil)
// New returns a new BtcdNotifier instance. This function assumes the btcd node // New returns a new BtcdNotifier instance. This function assumes the btcd node
// detailed in the passed configuration is already running, and willing to // detailed in the passed configuration is already running, and willing to
// accept new websockets clients. // accept new websockets clients.
func New(config *rpcclient.ConnConfig, spendHintCache chainntnfs.SpendHintCache, func New(config *rpcclient.ConnConfig, chainParams *chaincfg.Params,
spendHintCache chainntnfs.SpendHintCache,
confirmHintCache chainntnfs.ConfirmHintCache) (*BtcdNotifier, error) { confirmHintCache chainntnfs.ConfirmHintCache) (*BtcdNotifier, error) {
notifier := &BtcdNotifier{ notifier := &BtcdNotifier{
chainParams: chainParams,
notificationCancels: make(chan interface{}), notificationCancels: make(chan interface{}),
notificationRegistry: make(chan interface{}), notificationRegistry: make(chan interface{}),
@@ -289,10 +289,10 @@ out:
case registerMsg := <-b.notificationRegistry: case registerMsg := <-b.notificationRegistry:
switch msg := registerMsg.(type) { switch msg := registerMsg.(type) {
case *chainntnfs.HistoricalConfDispatch: case *chainntnfs.HistoricalConfDispatch:
// Look up whether the transaction is already // Look up whether the transaction/output script
// included in the active chain. We'll do this // has already confirmed in the active chain.
// in a goroutine to prevent blocking // We'll do this in a goroutine to prevent
// potentially long rescans. // blocking potentially long rescans.
// //
// TODO(wilmer): add retry logic if rescan fails? // TODO(wilmer): add retry logic if rescan fails?
b.wg.Add(1) b.wg.Add(1)
@@ -300,7 +300,8 @@ out:
defer b.wg.Done() defer b.wg.Done()
confDetails, _, err := b.historicalConfDetails( confDetails, _, err := b.historicalConfDetails(
msg.TxID, msg.StartHeight, msg.EndHeight, msg.ConfRequest,
msg.StartHeight, msg.EndHeight,
) )
if err != nil { if err != nil {
chainntnfs.Log.Error(err) chainntnfs.Log.Error(err)
@@ -315,7 +316,7 @@ out:
// cache at tip, since any pending // cache at tip, since any pending
// rescans have now completed. // rescans have now completed.
err = b.txNotifier.UpdateConfDetails( err = b.txNotifier.UpdateConfDetails(
*msg.TxID, confDetails, msg.ConfRequest, confDetails,
) )
if err != nil { if err != nil {
chainntnfs.Log.Error(err) chainntnfs.Log.Error(err)
@@ -324,23 +325,40 @@ out:
case *blockEpochRegistration: case *blockEpochRegistration:
chainntnfs.Log.Infof("New block epoch subscription") chainntnfs.Log.Infof("New block epoch subscription")
b.blockEpochClients[msg.epochID] = msg
if msg.bestBlock != nil {
missedBlocks, err :=
chainntnfs.GetClientMissedBlocks(
b.chainConn, msg.bestBlock,
b.bestBlock.Height, true,
)
if err != nil {
msg.errorChan <- err
continue
}
for _, block := range missedBlocks {
b.notifyBlockEpochClient(msg,
block.Height, block.Hash)
}
b.blockEpochClients[msg.epochID] = msg
// If the client did not provide their best
// known block, then we'll immediately dispatch
// a notification for the current tip.
if msg.bestBlock == nil {
b.notifyBlockEpochClient(
msg, b.bestBlock.Height,
b.bestBlock.Hash,
)
msg.errorChan <- nil
continue
} }
// Otherwise, we'll attempt to deliver the
// backlog of notifications from their best
// known block.
missedBlocks, err := chainntnfs.GetClientMissedBlocks(
b.chainConn, msg.bestBlock,
b.bestBlock.Height, true,
)
if err != nil {
msg.errorChan <- err
continue
}
for _, block := range missedBlocks {
b.notifyBlockEpochClient(
msg, block.Height, block.Hash,
)
}
msg.errorChan <- nil msg.errorChan <- nil
} }
@@ -425,13 +443,13 @@ out:
continue continue
} }
tx := newSpend.tx.MsgTx()
err := b.txNotifier.ProcessRelevantSpendTx( err := b.txNotifier.ProcessRelevantSpendTx(
tx, newSpend.details.Height, newSpend.tx, uint32(newSpend.details.Height),
) )
if err != nil { if err != nil {
chainntnfs.Log.Errorf("Unable to process "+ chainntnfs.Log.Errorf("Unable to process "+
"transaction %v: %v", tx.TxHash(), err) "transaction %v: %v",
newSpend.tx.Hash(), err)
} }
case <-b.quit: case <-b.quit:
@@ -441,15 +459,28 @@ out:
b.wg.Done() b.wg.Done()
} }
// historicalConfDetails looks up whether a transaction is already included in a // historicalConfDetails looks up whether a confirmation request (txid/output
// block in the active chain and, if so, returns details about the confirmation. // script) has already been included in a block in the active chain and, if so,
func (b *BtcdNotifier) historicalConfDetails(txid *chainhash.Hash, // returns details about said block.
func (b *BtcdNotifier) historicalConfDetails(confRequest chainntnfs.ConfRequest,
startHeight, endHeight uint32) (*chainntnfs.TxConfirmation, startHeight, endHeight uint32) (*chainntnfs.TxConfirmation,
chainntnfs.TxConfStatus, error) { chainntnfs.TxConfStatus, error) {
// If a txid was not provided, then we should dispatch upon seeing the
// script on-chain, so we'll short-circuit straight to scanning manually
// as there doesn't exist a script index to query.
if confRequest.TxID == chainntnfs.ZeroHash {
return b.confDetailsManually(
confRequest, startHeight, endHeight,
)
}
// Otherwise, we'll dispatch upon seeing a transaction on-chain with the
// given hash.
//
// We'll first attempt to retrieve the transaction using the node's // We'll first attempt to retrieve the transaction using the node's
// txindex. // txindex.
txConf, txStatus, err := b.confDetailsFromTxIndex(txid) txConf, txStatus, err := b.confDetailsFromTxIndex(&confRequest.TxID)
// We'll then check the status of the transaction lookup returned to // We'll then check the status of the transaction lookup returned to
// determine whether we should proceed with any fallback methods. // determine whether we should proceed with any fallback methods.
@@ -458,9 +489,13 @@ func (b *BtcdNotifier) historicalConfDetails(txid *chainhash.Hash,
// We failed querying the index for the transaction, fall back to // We failed querying the index for the transaction, fall back to
// scanning manually. // scanning manually.
case err != nil: case err != nil:
chainntnfs.Log.Debugf("Failed getting conf details from "+ chainntnfs.Log.Debugf("Unable to determine confirmation of %v "+
"index (%v), scanning manually", err) "through the backend's txindex (%v), scanning manually",
return b.confDetailsManually(txid, startHeight, endHeight) confRequest.TxID, err)
return b.confDetailsManually(
confRequest, startHeight, endHeight,
)
// The transaction was found within the node's mempool. // The transaction was found within the node's mempool.
case txStatus == chainntnfs.TxFoundMempool: case txStatus == chainntnfs.TxFoundMempool:
@@ -491,7 +526,7 @@ func (b *BtcdNotifier) confDetailsFromTxIndex(txid *chainhash.Hash,
// If the transaction has some or all of its confirmations required, // If the transaction has some or all of its confirmations required,
// then we may be able to dispatch it immediately. // then we may be able to dispatch it immediately.
tx, err := b.chainConn.GetRawTransactionVerbose(txid) rawTxRes, err := b.chainConn.GetRawTransactionVerbose(txid)
if err != nil { if err != nil {
// If the transaction lookup was successful, but it wasn't found // If the transaction lookup was successful, but it wasn't found
// within the index itself, then we can exit early. We'll also // within the index itself, then we can exit early. We'll also
@@ -512,20 +547,19 @@ func (b *BtcdNotifier) confDetailsFromTxIndex(txid *chainhash.Hash,
// Make sure we actually retrieved a transaction that is included in a // Make sure we actually retrieved a transaction that is included in a
// block. If not, the transaction must be unconfirmed (in the mempool), // block. If not, the transaction must be unconfirmed (in the mempool),
// and we'll return TxFoundMempool together with a nil TxConfirmation. // and we'll return TxFoundMempool together with a nil TxConfirmation.
if tx.BlockHash == "" { if rawTxRes.BlockHash == "" {
return nil, chainntnfs.TxFoundMempool, nil return nil, chainntnfs.TxFoundMempool, nil
} }
// As we need to fully populate the returned TxConfirmation struct, // As we need to fully populate the returned TxConfirmation struct,
// grab the block in which the transaction was confirmed so we can // grab the block in which the transaction was confirmed so we can
// locate its exact index within the block. // locate its exact index within the block.
blockHash, err := chainhash.NewHashFromStr(tx.BlockHash) blockHash, err := chainhash.NewHashFromStr(rawTxRes.BlockHash)
if err != nil { if err != nil {
return nil, chainntnfs.TxNotFoundIndex, return nil, chainntnfs.TxNotFoundIndex,
fmt.Errorf("unable to get block hash %v for "+ fmt.Errorf("unable to get block hash %v for "+
"historical dispatch: %v", tx.BlockHash, err) "historical dispatch: %v", rawTxRes.BlockHash, err)
} }
block, err := b.chainConn.GetBlockVerbose(blockHash) block, err := b.chainConn.GetBlockVerbose(blockHash)
if err != nil { if err != nil {
return nil, chainntnfs.TxNotFoundIndex, return nil, chainntnfs.TxNotFoundIndex,
@@ -535,36 +569,49 @@ func (b *BtcdNotifier) confDetailsFromTxIndex(txid *chainhash.Hash,
// If the block was obtained, locate the transaction's index within the // If the block was obtained, locate the transaction's index within the
// block so we can give the subscriber full confirmation details. // block so we can give the subscriber full confirmation details.
targetTxidStr := txid.String() txidStr := txid.String()
for txIndex, txHash := range block.Tx { for txIndex, txHash := range block.Tx {
if txHash == targetTxidStr { if txHash != txidStr {
details := &chainntnfs.TxConfirmation{ continue
BlockHash: blockHash,
BlockHeight: uint32(block.Height),
TxIndex: uint32(txIndex),
}
return details, chainntnfs.TxFoundIndex, nil
} }
// Deserialize the hex-encoded transaction to include it in the
// confirmation details.
rawTx, err := hex.DecodeString(rawTxRes.Hex)
if err != nil {
return nil, chainntnfs.TxFoundIndex,
fmt.Errorf("unable to deserialize tx %v: %v",
txHash, err)
}
var tx wire.MsgTx
if err := tx.Deserialize(bytes.NewReader(rawTx)); err != nil {
return nil, chainntnfs.TxFoundIndex,
fmt.Errorf("unable to deserialize tx %v: %v",
txHash, err)
}
return &chainntnfs.TxConfirmation{
Tx: &tx,
BlockHash: blockHash,
BlockHeight: uint32(block.Height),
TxIndex: uint32(txIndex),
}, chainntnfs.TxFoundIndex, nil
} }
// We return an error because we should have found the transaction // We return an error because we should have found the transaction
// within the block, but didn't. // within the block, but didn't.
return nil, chainntnfs.TxNotFoundIndex, return nil, chainntnfs.TxNotFoundIndex, fmt.Errorf("unable to locate "+
fmt.Errorf("unable to locate tx %v in block %v", txid, "tx %v in block %v", txid, blockHash)
blockHash)
} }
// confDetailsManually looks up whether a transaction is already included in a // confDetailsManually looks up whether a transaction/output script has already
// block in the active chain by scanning the chain's blocks, starting from the // been included in a block in the active chain by scanning the chain's blocks
// earliest height the transaction could have been included in, to the current // within the given range. If the transaction/output script is found, its
// height in the chain. If the transaction is found, its confirmation details // confirmation details are returned. Otherwise, nil is returned.
// are returned. Otherwise, nil is returned. func (b *BtcdNotifier) confDetailsManually(confRequest chainntnfs.ConfRequest,
func (b *BtcdNotifier) confDetailsManually(txid *chainhash.Hash, startHeight, startHeight, endHeight uint32) (*chainntnfs.TxConfirmation,
endHeight uint32) (*chainntnfs.TxConfirmation,
chainntnfs.TxConfStatus, error) { chainntnfs.TxConfStatus, error) {
targetTxidStr := txid.String()
// Begin scanning blocks at every height to determine where the // Begin scanning blocks at every height to determine where the
// transaction was included in. // transaction was included in.
for height := endHeight; height >= startHeight && height > 0; height-- { for height := endHeight; height >= startHeight && height > 0; height-- {
@@ -573,7 +620,7 @@ func (b *BtcdNotifier) confDetailsManually(txid *chainhash.Hash, startHeight,
select { select {
case <-b.quit: case <-b.quit:
return nil, chainntnfs.TxNotFoundManually, return nil, chainntnfs.TxNotFoundManually,
ErrChainNotifierShuttingDown chainntnfs.ErrChainNotifierShuttingDown
default: default:
} }
@@ -585,24 +632,27 @@ func (b *BtcdNotifier) confDetailsManually(txid *chainhash.Hash, startHeight,
} }
// TODO: fetch the neutrino filters instead. // TODO: fetch the neutrino filters instead.
block, err := b.chainConn.GetBlockVerbose(blockHash) block, err := b.chainConn.GetBlock(blockHash)
if err != nil { if err != nil {
return nil, chainntnfs.TxNotFoundManually, return nil, chainntnfs.TxNotFoundManually,
fmt.Errorf("unable to get block with hash "+ fmt.Errorf("unable to get block with hash "+
"%v: %v", blockHash, err) "%v: %v", blockHash, err)
} }
for txIndex, txHash := range block.Tx { // For every transaction in the block, check which one matches
// If we're able to find the transaction in this block, // our request. If we find one that does, we can dispatch its
// return its confirmation details. // confirmation details.
if txHash == targetTxidStr { for txIndex, tx := range block.Transactions {
details := &chainntnfs.TxConfirmation{ if !confRequest.MatchesTx(tx) {
BlockHash: blockHash, continue
BlockHeight: height,
TxIndex: uint32(txIndex),
}
return details, chainntnfs.TxFoundManually, nil
} }
return &chainntnfs.TxConfirmation{
Tx: tx,
BlockHash: blockHash,
BlockHeight: height,
TxIndex: uint32(txIndex),
}, chainntnfs.TxFoundManually, nil
} }
} }
@@ -681,32 +731,57 @@ func (b *BtcdNotifier) notifyBlockEpochClient(epochClient *blockEpochRegistratio
} }
// RegisterSpendNtfn registers an intent to be notified once the target // 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/output script has been spent by a transaction on-chain. When
// outpoint has been detected, the details of the spending event will be sent // intending to be notified of the spend of an output script, a nil outpoint
// across the 'Spend' channel. The heightHint should represent the earliest // must be used. The heightHint should represent the earliest height in the
// height in the chain where the transaction could have been spent in. // chain of the transaction that spent the outpoint/output script.
//
// Once a spend of has been detected, the details of the spending event will be
// sent across the 'Spend' channel.
func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) { pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) {
// First, we'll construct a spend notification request and hand it off // First, we'll construct a spend notification request and hand it off
// to the txNotifier. // to the txNotifier.
spendID := atomic.AddUint64(&b.spendClientCounter, 1) spendID := atomic.AddUint64(&b.spendClientCounter, 1)
cancel := func() { spendRequest, err := chainntnfs.NewSpendRequest(outpoint, pkScript)
b.txNotifier.CancelSpend(*outpoint, spendID) if err != nil {
return nil, err
} }
ntfn := &chainntnfs.SpendNtfn{ ntfn := &chainntnfs.SpendNtfn{
SpendID: spendID, SpendID: spendID,
OutPoint: *outpoint, SpendRequest: spendRequest,
PkScript: pkScript, Event: chainntnfs.NewSpendEvent(func() {
Event: chainntnfs.NewSpendEvent(cancel), b.txNotifier.CancelSpend(spendRequest, spendID)
}),
HeightHint: heightHint, HeightHint: heightHint,
} }
historicalDispatch, err := b.txNotifier.RegisterSpend(ntfn) historicalDispatch, _, err := b.txNotifier.RegisterSpend(ntfn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// We'll then request the backend to notify us when it has detected the
// outpoint/output script as spent.
//
// TODO(wilmer): use LoadFilter API instead.
if spendRequest.OutPoint == chainntnfs.ZeroOutPoint {
addr, err := spendRequest.PkScript.Address(b.chainParams)
if err != nil {
return nil, err
}
addrs := []btcutil.Address{addr}
if err := b.chainConn.NotifyReceived(addrs); err != nil {
return nil, err
}
} else {
ops := []*wire.OutPoint{&spendRequest.OutPoint}
if err := b.chainConn.NotifySpent(ops); err != nil {
return nil, err
}
}
// If the txNotifier didn't return any details to perform a historical // If the txNotifier didn't return any details to perform a historical
// scan of the chain, then we can return early as there's nothing left // scan of the chain, then we can return early as there's nothing left
// for us to do. // for us to do.
@@ -714,24 +789,55 @@ func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
return ntfn.Event, nil return ntfn.Event, nil
} }
// We'll then request the backend to notify us when it has detected the // Otherwise, we'll need to dispatch a historical rescan to determine if
// outpoint as spent. // the outpoint was already spent at a previous height.
ops := []*wire.OutPoint{outpoint} //
if err := b.chainConn.NotifySpent(ops); err != nil { // We'll short-circuit the path when dispatching the spend of a script,
return nil, err // rather than an outpoint, as there aren't any additional checks we can
// make for scripts.
if spendRequest.OutPoint == chainntnfs.ZeroOutPoint {
startHash, err := b.chainConn.GetBlockHash(
int64(historicalDispatch.StartHeight),
)
if err != nil {
return nil, err
}
// TODO(wilmer): add retry logic if rescan fails?
addr, err := spendRequest.PkScript.Address(b.chainParams)
if err != nil {
return nil, err
}
addrs := []btcutil.Address{addr}
asyncResult := b.chainConn.RescanAsync(startHash, addrs, nil)
go func() {
if rescanErr := asyncResult.Receive(); rescanErr != nil {
chainntnfs.Log.Errorf("Rescan to determine "+
"the spend details of %v failed: %v",
spendRequest, rescanErr)
}
}()
return ntfn.Event, nil
} }
// In addition to the check above, we'll also check the backend's UTXO // When dispatching spends of outpoints, there are a number of checks we
// set to determine whether the outpoint has been spent. If it hasn't, // can make to start our rescan from a better height or completely avoid
// we can return to the caller as well. // it.
txOut, err := b.chainConn.GetTxOut(&outpoint.Hash, outpoint.Index, true) //
// We'll start by checking the backend's UTXO set to determine whether
// the outpoint has been spent. If it hasn't, we can return to the
// caller as well.
txOut, err := b.chainConn.GetTxOut(
&spendRequest.OutPoint.Hash, spendRequest.OutPoint.Index, true,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if txOut != nil { if txOut != nil {
// We'll let the txNotifier know the outpoint is still unspent // We'll let the txNotifier know the outpoint is still unspent
// in order to begin updating its spend hint. // in order to begin updating its spend hint.
err := b.txNotifier.UpdateSpendDetails(*outpoint, nil) err := b.txNotifier.UpdateSpendDetails(spendRequest, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -739,9 +845,9 @@ func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
return ntfn.Event, nil return ntfn.Event, nil
} }
// Otherwise, we'll determine when the output was spent by scanning the // Since the outpoint was spent, as it no longer exists within the UTXO
// chain. We'll begin by determining where to start our historical // set, we'll determine when it happened by scanning the chain. We'll
// rescan. // begin by fetching the block hash of our starting height.
startHash, err := b.chainConn.GetBlockHash( startHash, err := b.chainConn.GetBlockHash(
int64(historicalDispatch.StartHeight), int64(historicalDispatch.StartHeight),
) )
@@ -754,14 +860,14 @@ func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
// index (if enabled) to determine if we have a better rescan starting // index (if enabled) to determine if we have a better rescan starting
// height. We can do this as the GetRawTransaction call will return the // height. We can do this as the GetRawTransaction call will return the
// hash of the block it was included in within the chain. // hash of the block it was included in within the chain.
tx, err := b.chainConn.GetRawTransactionVerbose(&outpoint.Hash) tx, err := b.chainConn.GetRawTransactionVerbose(&spendRequest.OutPoint.Hash)
if err != nil { if err != nil {
// Avoid returning an error if the transaction was not found to // Avoid returning an error if the transaction was not found to
// proceed with fallback methods. // proceed with fallback methods.
jsonErr, ok := err.(*btcjson.RPCError) jsonErr, ok := err.(*btcjson.RPCError)
if !ok || jsonErr.Code != btcjson.ErrRPCNoTxInfo { if !ok || jsonErr.Code != btcjson.ErrRPCNoTxInfo {
return nil, fmt.Errorf("unable to query for "+ return nil, fmt.Errorf("unable to query for txid %v: %v",
"txid %v: %v", outpoint.Hash, err) spendRequest.OutPoint.Hash, err)
} }
} }
@@ -797,6 +903,9 @@ func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
} }
} }
// Now that we've determined the best starting point for our rescan,
// we can go ahead and dispatch it.
//
// In order to ensure that we don't block the caller on what may be a // In order to ensure that we don't block the caller on what may be a
// long rescan, we'll launch a new goroutine to handle the async result // long rescan, we'll launch a new goroutine to handle the async result
// of the rescan. We purposefully prevent from adding this goroutine to // of the rescan. We purposefully prevent from adding this goroutine to
@@ -804,41 +913,58 @@ func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
// asyncResult channel not being exposed. // asyncResult channel not being exposed.
// //
// TODO(wilmer): add retry logic if rescan fails? // TODO(wilmer): add retry logic if rescan fails?
asyncResult := b.chainConn.RescanAsync(startHash, nil, ops) asyncResult := b.chainConn.RescanAsync(
startHash, nil, []*wire.OutPoint{&spendRequest.OutPoint},
)
go func() { go func() {
if rescanErr := asyncResult.Receive(); rescanErr != nil { if rescanErr := asyncResult.Receive(); rescanErr != nil {
chainntnfs.Log.Errorf("Rescan to determine the spend "+ chainntnfs.Log.Errorf("Rescan to determine the spend "+
"details of %v failed: %v", outpoint, rescanErr) "details of %v failed: %v", spendRequest,
rescanErr)
} }
}() }()
return ntfn.Event, nil return ntfn.Event, nil
} }
// RegisterConfirmationsNtfn registers a notification with BtcdNotifier // RegisterConfirmationsNtfn registers an intent to be notified once the target
// which will be triggered once the txid reaches numConfs number of // txid/output script has reached numConfs confirmations on-chain. When
// confirmations. // intending to be notified of the confirmation of an output script, a nil txid
func (b *BtcdNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, _ []byte, // must be used. The heightHint should represent the earliest height at which
// the txid/output script could have been included in the chain.
//
// Progress on the number of confirmations left can be read from the 'Updates'
// channel. Once it has reached all of its confirmations, a notification will be
// sent across the 'Confirmed' channel.
func (b *BtcdNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash,
pkScript []byte,
numConfs, heightHint uint32) (*chainntnfs.ConfirmationEvent, error) { numConfs, heightHint uint32) (*chainntnfs.ConfirmationEvent, error) {
// Construct a notification request for the transaction and send it to // Construct a notification request for the transaction and send it to
// the main event loop. // the main event loop.
confID := atomic.AddUint64(&b.confClientCounter, 1)
confRequest, err := chainntnfs.NewConfRequest(txid, pkScript)
if err != nil {
return nil, err
}
ntfn := &chainntnfs.ConfNtfn{ ntfn := &chainntnfs.ConfNtfn{
ConfID: atomic.AddUint64(&b.confClientCounter, 1), ConfID: confID,
TxID: txid, ConfRequest: confRequest,
NumConfirmations: numConfs, NumConfirmations: numConfs,
Event: chainntnfs.NewConfirmationEvent(numConfs), Event: chainntnfs.NewConfirmationEvent(numConfs, func() {
HeightHint: heightHint, b.txNotifier.CancelConf(confRequest, confID)
}),
HeightHint: heightHint,
} }
chainntnfs.Log.Infof("New confirmation subscription: "+ chainntnfs.Log.Infof("New confirmation subscription: %v, num_confs=%v ",
"txid=%v, numconfs=%v", txid, numConfs) confRequest, numConfs)
// Register the conf notification with the TxNotifier. A non-nil value // Register the conf notification with the TxNotifier. A non-nil value
// for `dispatch` will be returned if we are required to perform a // for `dispatch` will be returned if we are required to perform a
// manual scan for the confirmation. Otherwise the notifier will begin // manual scan for the confirmation. Otherwise the notifier will begin
// watching at tip for the transaction to confirm. // watching at tip for the transaction to confirm.
dispatch, err := b.txNotifier.RegisterConf(ntfn) dispatch, _, err := b.txNotifier.RegisterConf(ntfn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -851,7 +977,7 @@ func (b *BtcdNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, _ []byte,
case b.notificationRegistry <- dispatch: case b.notificationRegistry <- dispatch:
return ntfn.Event, nil return ntfn.Event, nil
case <-b.quit: case <-b.quit:
return nil, ErrChainNotifierShuttingDown return nil, chainntnfs.ErrChainNotifierShuttingDown
} }
} }
@@ -882,7 +1008,9 @@ type epochCancel struct {
// RegisterBlockEpochNtfn returns a BlockEpochEvent which subscribes the // RegisterBlockEpochNtfn returns a BlockEpochEvent which subscribes the
// caller to receive notifications, of each new block connected to the main // caller to receive notifications, of each new block connected to the main
// chain. Clients have the option of passing in their best known block, which // chain. Clients have the option of passing in their best known block, which
// the notifier uses to check if they are behind on blocks and catch them up. // the notifier uses to check if they are behind on blocks and catch them up. If
// they do not provide one, then a notification will be dispatched immediately
// for the current tip of the chain upon a successful registration.
func (b *BtcdNotifier) RegisterBlockEpochNtfn( func (b *BtcdNotifier) RegisterBlockEpochNtfn(
bestBlock *chainntnfs.BlockEpoch) (*chainntnfs.BlockEpochEvent, error) { bestBlock *chainntnfs.BlockEpoch) (*chainntnfs.BlockEpochEvent, error) {
@@ -894,6 +1022,7 @@ func (b *BtcdNotifier) RegisterBlockEpochNtfn(
bestBlock: bestBlock, bestBlock: bestBlock,
errorChan: make(chan error, 1), errorChan: make(chan error, 1),
} }
reg.epochQueue.Start() reg.epochQueue.Start()
// Before we send the request to the main goroutine, we'll launch a new // Before we send the request to the main goroutine, we'll launch a new
+44 -11
View File
@@ -3,6 +3,7 @@
package btcdnotify package btcdnotify
import ( import (
"bytes"
"io/ioutil" "io/ioutil"
"testing" "testing"
@@ -12,6 +13,20 @@ import (
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
) )
var (
testScript = []byte{
// OP_HASH160
0xA9,
// OP_DATA_20
0x14,
// <20-byte hash>
0xec, 0x6f, 0x7a, 0x5a, 0xa8, 0xf2, 0xb1, 0x0c, 0xa5, 0x15,
0x04, 0x52, 0x3a, 0x60, 0xd4, 0x03, 0x06, 0xf6, 0x96, 0xcd,
// OP_EQUAL
0x87,
}
)
func initHintCache(t *testing.T) *chainntnfs.HeightHintCache { func initHintCache(t *testing.T) *chainntnfs.HeightHintCache {
t.Helper() t.Helper()
@@ -37,7 +52,7 @@ func setUpNotifier(t *testing.T, h *rpctest.Harness) *BtcdNotifier {
hintCache := initHintCache(t) hintCache := initHintCache(t)
rpcCfg := h.RPCConfig() rpcCfg := h.RPCConfig()
notifier, err := New(&rpcCfg, hintCache, hintCache) notifier, err := New(&rpcCfg, chainntnfs.NetParams, hintCache, hintCache)
if err != nil { if err != nil {
t.Fatalf("unable to create notifier: %v", err) t.Fatalf("unable to create notifier: %v", err)
} }
@@ -64,8 +79,13 @@ func TestHistoricalConfDetailsTxIndex(t *testing.T) {
// A transaction unknown to the node should not be found within the // A transaction unknown to the node should not be found within the
// txindex even if it is enabled, so we should not proceed with any // txindex even if it is enabled, so we should not proceed with any
// fallback methods. // fallback methods.
var zeroHash chainhash.Hash var unknownHash chainhash.Hash
_, txStatus, err := notifier.historicalConfDetails(&zeroHash, 0, 0) copy(unknownHash[:], bytes.Repeat([]byte{0x10}, 32))
unknownConfReq, err := chainntnfs.NewConfRequest(&unknownHash, testScript)
if err != nil {
t.Fatalf("unable to create conf request: %v", err)
}
_, txStatus, err := notifier.historicalConfDetails(unknownConfReq, 0, 0)
if err != nil { if err != nil {
t.Fatalf("unable to retrieve historical conf details: %v", err) t.Fatalf("unable to retrieve historical conf details: %v", err)
} }
@@ -80,16 +100,20 @@ func TestHistoricalConfDetailsTxIndex(t *testing.T) {
// Now, we'll create a test transaction and attempt to retrieve its // Now, we'll create a test transaction and attempt to retrieve its
// confirmation details. // confirmation details.
txid, _, err := chainntnfs.GetTestTxidAndScript(harness) txid, pkScript, err := chainntnfs.GetTestTxidAndScript(harness)
if err != nil { if err != nil {
t.Fatalf("unable to create tx: %v", err) t.Fatalf("unable to create tx: %v", err)
} }
if err := chainntnfs.WaitForMempoolTx(harness, txid); err != nil { if err := chainntnfs.WaitForMempoolTx(harness, txid); err != nil {
t.Fatalf("unable to find tx in the mempool: %v", err) t.Fatalf("unable to find tx in the mempool: %v", err)
} }
confReq, err := chainntnfs.NewConfRequest(txid, pkScript)
if err != nil {
t.Fatalf("unable to create conf request: %v", err)
}
// The transaction should be found in the mempool at this point. // The transaction should be found in the mempool at this point.
_, txStatus, err = notifier.historicalConfDetails(txid, 0, 0) _, txStatus, err = notifier.historicalConfDetails(confReq, 0, 0)
if err != nil { if err != nil {
t.Fatalf("unable to retrieve historical conf details: %v", err) t.Fatalf("unable to retrieve historical conf details: %v", err)
} }
@@ -109,7 +133,7 @@ func TestHistoricalConfDetailsTxIndex(t *testing.T) {
t.Fatalf("unable to generate block: %v", err) t.Fatalf("unable to generate block: %v", err)
} }
_, txStatus, err = notifier.historicalConfDetails(txid, 0, 0) _, txStatus, err = notifier.historicalConfDetails(confReq, 0, 0)
if err != nil { if err != nil {
t.Fatalf("unable to retrieve historical conf details: %v", err) t.Fatalf("unable to retrieve historical conf details: %v", err)
} }
@@ -139,8 +163,13 @@ func TestHistoricalConfDetailsNoTxIndex(t *testing.T) {
// Since the node has its txindex disabled, we fall back to scanning the // Since the node has its txindex disabled, we fall back to scanning the
// chain manually. A transaction unknown to the network should not be // chain manually. A transaction unknown to the network should not be
// found. // found.
var zeroHash chainhash.Hash var unknownHash chainhash.Hash
_, txStatus, err := notifier.historicalConfDetails(&zeroHash, 0, 0) copy(unknownHash[:], bytes.Repeat([]byte{0x10}, 32))
unknownConfReq, err := chainntnfs.NewConfRequest(&unknownHash, testScript)
if err != nil {
t.Fatalf("unable to create conf request: %v", err)
}
_, txStatus, err := notifier.historicalConfDetails(unknownConfReq, 0, 0)
if err != nil { if err != nil {
t.Fatalf("unable to retrieve historical conf details: %v", err) t.Fatalf("unable to retrieve historical conf details: %v", err)
} }
@@ -161,15 +190,19 @@ func TestHistoricalConfDetailsNoTxIndex(t *testing.T) {
t.Fatalf("unable to retrieve current height: %v", err) t.Fatalf("unable to retrieve current height: %v", err)
} }
txid, _, err := chainntnfs.GetTestTxidAndScript(harness) txid, pkScript, err := chainntnfs.GetTestTxidAndScript(harness)
if err != nil { if err != nil {
t.Fatalf("unable to create tx: %v", err) t.Fatalf("unable to create tx: %v", err)
} }
if err := chainntnfs.WaitForMempoolTx(harness, txid); err != nil { if err := chainntnfs.WaitForMempoolTx(harness, txid); err != nil {
t.Fatalf("unable to find tx in the mempool: %v", err) t.Fatalf("unable to find tx in the mempool: %v", err)
} }
confReq, err := chainntnfs.NewConfRequest(txid, pkScript)
if err != nil {
t.Fatalf("unable to create conf request: %v", err)
}
_, txStatus, err = notifier.historicalConfDetails(txid, 0, 0) _, txStatus, err = notifier.historicalConfDetails(confReq, 0, 0)
if err != nil { if err != nil {
t.Fatalf("unable to retrieve historical conf details: %v", err) t.Fatalf("unable to retrieve historical conf details: %v", err)
} }
@@ -188,7 +221,7 @@ func TestHistoricalConfDetailsNoTxIndex(t *testing.T) {
} }
_, txStatus, err = notifier.historicalConfDetails( _, txStatus, err = notifier.historicalConfDetails(
txid, uint32(currentHeight), uint32(currentHeight)+1, confReq, uint32(currentHeight), uint32(currentHeight)+1,
) )
if err != nil { if err != nil {
t.Fatalf("unable to retrieve historical conf details: %v", err) t.Fatalf("unable to retrieve historical conf details: %v", err)
+15 -8
View File
@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/rpcclient"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
) )
@@ -11,30 +12,36 @@ import (
// createNewNotifier creates a new instance of the ChainNotifier interface // createNewNotifier creates a new instance of the ChainNotifier interface
// implemented by BtcdNotifier. // implemented by BtcdNotifier.
func createNewNotifier(args ...interface{}) (chainntnfs.ChainNotifier, error) { func createNewNotifier(args ...interface{}) (chainntnfs.ChainNotifier, error) {
if len(args) != 3 { if len(args) != 4 {
return nil, fmt.Errorf("incorrect number of arguments to "+ return nil, fmt.Errorf("incorrect number of arguments to "+
".New(...), expected 2, instead passed %v", len(args)) ".New(...), expected 4, instead passed %v", len(args))
} }
config, ok := args[0].(*rpcclient.ConnConfig) config, ok := args[0].(*rpcclient.ConnConfig)
if !ok { if !ok {
return nil, errors.New("first argument to btcdnotifier.New " + return nil, errors.New("first argument to btcdnotify.New " +
"is incorrect, expected a *rpcclient.ConnConfig") "is incorrect, expected a *rpcclient.ConnConfig")
} }
spendHintCache, ok := args[1].(chainntnfs.SpendHintCache) chainParams, ok := args[1].(*chaincfg.Params)
if !ok { if !ok {
return nil, errors.New("second argument to btcdnotifier.New " + return nil, errors.New("second argument to btcdnotify.New " +
"is incorrect, expected a *chaincfg.Params")
}
spendHintCache, ok := args[2].(chainntnfs.SpendHintCache)
if !ok {
return nil, errors.New("third argument to btcdnotify.New " +
"is incorrect, expected a chainntnfs.SpendHintCache") "is incorrect, expected a chainntnfs.SpendHintCache")
} }
confirmHintCache, ok := args[2].(chainntnfs.ConfirmHintCache) confirmHintCache, ok := args[3].(chainntnfs.ConfirmHintCache)
if !ok { if !ok {
return nil, errors.New("third argument to btcdnotifier.New " + return nil, errors.New("fourth argument to btcdnotify.New " +
"is incorrect, expected a chainntnfs.ConfirmHintCache") "is incorrect, expected a chainntnfs.ConfirmHintCache")
} }
return New(config, spendHintCache, confirmHintCache) return New(config, chainParams, spendHintCache, confirmHintCache)
} }
// init registers a driver for the BtcdNotifier concrete implementation of the // init registers a driver for the BtcdNotifier concrete implementation of the
+44 -52
View File
@@ -4,8 +4,6 @@ import (
"bytes" "bytes"
"errors" "errors"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
bolt "github.com/coreos/bbolt" bolt "github.com/coreos/bbolt"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
) )
@@ -51,16 +49,16 @@ var (
// which an outpoint could have been spent within. // which an outpoint could have been spent within.
type SpendHintCache interface { type SpendHintCache interface {
// CommitSpendHint commits a spend hint for the outpoints to the cache. // CommitSpendHint commits a spend hint for the outpoints to the cache.
CommitSpendHint(height uint32, ops ...wire.OutPoint) error CommitSpendHint(height uint32, spendRequests ...SpendRequest) error
// QuerySpendHint returns the latest spend hint for an outpoint. // QuerySpendHint returns the latest spend hint for an outpoint.
// ErrSpendHintNotFound is returned if a spend hint does not exist // ErrSpendHintNotFound is returned if a spend hint does not exist
// within the cache for the outpoint. // within the cache for the outpoint.
QuerySpendHint(op wire.OutPoint) (uint32, error) QuerySpendHint(spendRequest SpendRequest) (uint32, error)
// PurgeSpendHint removes the spend hint for the outpoints from the // PurgeSpendHint removes the spend hint for the outpoints from the
// cache. // cache.
PurgeSpendHint(ops ...wire.OutPoint) error PurgeSpendHint(spendRequests ...SpendRequest) error
} }
// ConfirmHintCache is an interface whose duty is to cache confirm hints for // ConfirmHintCache is an interface whose duty is to cache confirm hints for
@@ -69,16 +67,16 @@ type SpendHintCache interface {
type ConfirmHintCache interface { type ConfirmHintCache interface {
// CommitConfirmHint commits a confirm hint for the transactions to the // CommitConfirmHint commits a confirm hint for the transactions to the
// cache. // cache.
CommitConfirmHint(height uint32, txids ...chainhash.Hash) error CommitConfirmHint(height uint32, confRequests ...ConfRequest) error
// QueryConfirmHint returns the latest confirm hint for a transaction // QueryConfirmHint returns the latest confirm hint for a transaction
// hash. ErrConfirmHintNotFound is returned if a confirm hint does not // hash. ErrConfirmHintNotFound is returned if a confirm hint does not
// exist within the cache for the transaction hash. // exist within the cache for the transaction hash.
QueryConfirmHint(txid chainhash.Hash) (uint32, error) QueryConfirmHint(confRequest ConfRequest) (uint32, error)
// PurgeConfirmHint removes the confirm hint for the transactions from // PurgeConfirmHint removes the confirm hint for the transactions from
// the cache. // the cache.
PurgeConfirmHint(txids ...chainhash.Hash) error PurgeConfirmHint(confRequests ...ConfRequest) error
} }
// HeightHintCache is an implementation of the SpendHintCache and // HeightHintCache is an implementation of the SpendHintCache and
@@ -118,12 +116,15 @@ func (c *HeightHintCache) initBuckets() error {
} }
// CommitSpendHint commits a spend hint for the outpoints to the cache. // CommitSpendHint commits a spend hint for the outpoints to the cache.
func (c *HeightHintCache) CommitSpendHint(height uint32, ops ...wire.OutPoint) error { func (c *HeightHintCache) CommitSpendHint(height uint32,
if len(ops) == 0 { spendRequests ...SpendRequest) error {
if len(spendRequests) == 0 {
return nil return nil
} }
Log.Tracef("Updating spend hint to height %d for %v", height, ops) Log.Tracef("Updating spend hint to height %d for %v", height,
spendRequests)
return c.db.Batch(func(tx *bolt.Tx) error { return c.db.Batch(func(tx *bolt.Tx) error {
spendHints := tx.Bucket(spendHintBucket) spendHints := tx.Bucket(spendHintBucket)
@@ -136,14 +137,12 @@ func (c *HeightHintCache) CommitSpendHint(height uint32, ops ...wire.OutPoint) e
return err return err
} }
for _, op := range ops { for _, spendRequest := range spendRequests {
var outpoint bytes.Buffer spendHintKey, err := spendRequest.SpendHintKey()
err := channeldb.WriteElement(&outpoint, op)
if err != nil { if err != nil {
return err return err
} }
err = spendHints.Put(spendHintKey, hint.Bytes())
err = spendHints.Put(outpoint.Bytes(), hint.Bytes())
if err != nil { if err != nil {
return err return err
} }
@@ -156,7 +155,7 @@ func (c *HeightHintCache) CommitSpendHint(height uint32, ops ...wire.OutPoint) e
// QuerySpendHint returns the latest spend hint for an outpoint. // QuerySpendHint returns the latest spend hint for an outpoint.
// ErrSpendHintNotFound is returned if a spend hint does not exist within the // ErrSpendHintNotFound is returned if a spend hint does not exist within the
// cache for the outpoint. // cache for the outpoint.
func (c *HeightHintCache) QuerySpendHint(op wire.OutPoint) (uint32, error) { func (c *HeightHintCache) QuerySpendHint(spendRequest SpendRequest) (uint32, error) {
var hint uint32 var hint uint32
err := c.db.View(func(tx *bolt.Tx) error { err := c.db.View(func(tx *bolt.Tx) error {
spendHints := tx.Bucket(spendHintBucket) spendHints := tx.Bucket(spendHintBucket)
@@ -164,12 +163,11 @@ func (c *HeightHintCache) QuerySpendHint(op wire.OutPoint) (uint32, error) {
return ErrCorruptedHeightHintCache return ErrCorruptedHeightHintCache
} }
var outpoint bytes.Buffer spendHintKey, err := spendRequest.SpendHintKey()
if err := channeldb.WriteElement(&outpoint, op); err != nil { if err != nil {
return err return err
} }
spendHint := spendHints.Get(spendHintKey)
spendHint := spendHints.Get(outpoint.Bytes())
if spendHint == nil { if spendHint == nil {
return ErrSpendHintNotFound return ErrSpendHintNotFound
} }
@@ -184,12 +182,12 @@ func (c *HeightHintCache) QuerySpendHint(op wire.OutPoint) (uint32, error) {
} }
// PurgeSpendHint removes the spend hint for the outpoints from the cache. // PurgeSpendHint removes the spend hint for the outpoints from the cache.
func (c *HeightHintCache) PurgeSpendHint(ops ...wire.OutPoint) error { func (c *HeightHintCache) PurgeSpendHint(spendRequests ...SpendRequest) error {
if len(ops) == 0 { if len(spendRequests) == 0 {
return nil return nil
} }
Log.Tracef("Removing spend hints for %v", ops) Log.Tracef("Removing spend hints for %v", spendRequests)
return c.db.Batch(func(tx *bolt.Tx) error { return c.db.Batch(func(tx *bolt.Tx) error {
spendHints := tx.Bucket(spendHintBucket) spendHints := tx.Bucket(spendHintBucket)
@@ -197,15 +195,12 @@ func (c *HeightHintCache) PurgeSpendHint(ops ...wire.OutPoint) error {
return ErrCorruptedHeightHintCache return ErrCorruptedHeightHintCache
} }
for _, op := range ops { for _, spendRequest := range spendRequests {
var outpoint bytes.Buffer spendHintKey, err := spendRequest.SpendHintKey()
err := channeldb.WriteElement(&outpoint, op)
if err != nil { if err != nil {
return err return err
} }
if err := spendHints.Delete(spendHintKey); err != nil {
err = spendHints.Delete(outpoint.Bytes())
if err != nil {
return err return err
} }
} }
@@ -215,12 +210,15 @@ func (c *HeightHintCache) PurgeSpendHint(ops ...wire.OutPoint) error {
} }
// CommitConfirmHint commits a confirm hint for the transactions to the cache. // CommitConfirmHint commits a confirm hint for the transactions to the cache.
func (c *HeightHintCache) CommitConfirmHint(height uint32, txids ...chainhash.Hash) error { func (c *HeightHintCache) CommitConfirmHint(height uint32,
if len(txids) == 0 { confRequests ...ConfRequest) error {
if len(confRequests) == 0 {
return nil return nil
} }
Log.Tracef("Updating confirm hints to height %d for %v", height, txids) Log.Tracef("Updating confirm hints to height %d for %v", height,
confRequests)
return c.db.Batch(func(tx *bolt.Tx) error { return c.db.Batch(func(tx *bolt.Tx) error {
confirmHints := tx.Bucket(confirmHintBucket) confirmHints := tx.Bucket(confirmHintBucket)
@@ -233,14 +231,12 @@ func (c *HeightHintCache) CommitConfirmHint(height uint32, txids ...chainhash.Ha
return err return err
} }
for _, txid := range txids { for _, confRequest := range confRequests {
var txHash bytes.Buffer confHintKey, err := confRequest.ConfHintKey()
err := channeldb.WriteElement(&txHash, txid)
if err != nil { if err != nil {
return err return err
} }
err = confirmHints.Put(confHintKey, hint.Bytes())
err = confirmHints.Put(txHash.Bytes(), hint.Bytes())
if err != nil { if err != nil {
return err return err
} }
@@ -253,7 +249,7 @@ func (c *HeightHintCache) CommitConfirmHint(height uint32, txids ...chainhash.Ha
// QueryConfirmHint returns the latest confirm hint for a transaction hash. // QueryConfirmHint returns the latest confirm hint for a transaction hash.
// ErrConfirmHintNotFound is returned if a confirm hint does not exist within // ErrConfirmHintNotFound is returned if a confirm hint does not exist within
// the cache for the transaction hash. // the cache for the transaction hash.
func (c *HeightHintCache) QueryConfirmHint(txid chainhash.Hash) (uint32, error) { func (c *HeightHintCache) QueryConfirmHint(confRequest ConfRequest) (uint32, error) {
var hint uint32 var hint uint32
err := c.db.View(func(tx *bolt.Tx) error { err := c.db.View(func(tx *bolt.Tx) error {
confirmHints := tx.Bucket(confirmHintBucket) confirmHints := tx.Bucket(confirmHintBucket)
@@ -261,12 +257,11 @@ func (c *HeightHintCache) QueryConfirmHint(txid chainhash.Hash) (uint32, error)
return ErrCorruptedHeightHintCache return ErrCorruptedHeightHintCache
} }
var txHash bytes.Buffer confHintKey, err := confRequest.ConfHintKey()
if err := channeldb.WriteElement(&txHash, txid); err != nil { if err != nil {
return err return err
} }
confirmHint := confirmHints.Get(confHintKey)
confirmHint := confirmHints.Get(txHash.Bytes())
if confirmHint == nil { if confirmHint == nil {
return ErrConfirmHintNotFound return ErrConfirmHintNotFound
} }
@@ -282,12 +277,12 @@ func (c *HeightHintCache) QueryConfirmHint(txid chainhash.Hash) (uint32, error)
// PurgeConfirmHint removes the confirm hint for the transactions from the // PurgeConfirmHint removes the confirm hint for the transactions from the
// cache. // cache.
func (c *HeightHintCache) PurgeConfirmHint(txids ...chainhash.Hash) error { func (c *HeightHintCache) PurgeConfirmHint(confRequests ...ConfRequest) error {
if len(txids) == 0 { if len(confRequests) == 0 {
return nil return nil
} }
Log.Tracef("Removing confirm hints for %v", txids) Log.Tracef("Removing confirm hints for %v", confRequests)
return c.db.Batch(func(tx *bolt.Tx) error { return c.db.Batch(func(tx *bolt.Tx) error {
confirmHints := tx.Bucket(confirmHintBucket) confirmHints := tx.Bucket(confirmHintBucket)
@@ -295,15 +290,12 @@ func (c *HeightHintCache) PurgeConfirmHint(txids ...chainhash.Hash) error {
return ErrCorruptedHeightHintCache return ErrCorruptedHeightHintCache
} }
for _, txid := range txids { for _, confRequest := range confRequests {
var txHash bytes.Buffer confHintKey, err := confRequest.ConfHintKey()
err := channeldb.WriteElement(&txHash, txid)
if err != nil { if err != nil {
return err return err
} }
if err := confirmHints.Delete(confHintKey); err != nil {
err = confirmHints.Delete(txHash.Bytes())
if err != nil {
return err return err
} }
} }
+29 -24
View File
@@ -39,7 +39,9 @@ func TestHeightHintCacheConfirms(t *testing.T) {
// Querying for a transaction hash not found within the cache should // Querying for a transaction hash not found within the cache should
// return an error indication so. // return an error indication so.
var unknownHash chainhash.Hash var unknownHash chainhash.Hash
_, err := hintCache.QueryConfirmHint(unknownHash) copy(unknownHash[:], bytes.Repeat([]byte{0x01}, 32))
unknownConfRequest := ConfRequest{TxID: unknownHash}
_, err := hintCache.QueryConfirmHint(unknownConfRequest)
if err != ErrConfirmHintNotFound { if err != ErrConfirmHintNotFound {
t.Fatalf("expected ErrConfirmHintNotFound, got: %v", err) t.Fatalf("expected ErrConfirmHintNotFound, got: %v", err)
} }
@@ -48,23 +50,24 @@ func TestHeightHintCacheConfirms(t *testing.T) {
// cache with the same confirm hint. // cache with the same confirm hint.
const height = 100 const height = 100
const numHashes = 5 const numHashes = 5
txHashes := make([]chainhash.Hash, numHashes) confRequests := make([]ConfRequest, numHashes)
for i := 0; i < numHashes; i++ { for i := 0; i < numHashes; i++ {
var txHash chainhash.Hash var txHash chainhash.Hash
copy(txHash[:], bytes.Repeat([]byte{byte(i)}, 32)) copy(txHash[:], bytes.Repeat([]byte{byte(i + 1)}, 32))
txHashes[i] = txHash confRequests[i] = ConfRequest{TxID: txHash}
} }
if err := hintCache.CommitConfirmHint(height, txHashes...); err != nil { err = hintCache.CommitConfirmHint(height, confRequests...)
if err != nil {
t.Fatalf("unable to add entries to cache: %v", err) t.Fatalf("unable to add entries to cache: %v", err)
} }
// With the hashes committed, we'll now query the cache to ensure that // With the hashes committed, we'll now query the cache to ensure that
// we're able to properly retrieve the confirm hints. // we're able to properly retrieve the confirm hints.
for _, txHash := range txHashes { for _, confRequest := range confRequests {
confirmHint, err := hintCache.QueryConfirmHint(txHash) confirmHint, err := hintCache.QueryConfirmHint(confRequest)
if err != nil { if err != nil {
t.Fatalf("unable to query for hint: %v", err) t.Fatalf("unable to query for hint of %v: %v", confRequest, err)
} }
if confirmHint != height { if confirmHint != height {
t.Fatalf("expected confirm hint %d, got %d", height, t.Fatalf("expected confirm hint %d, got %d", height,
@@ -74,14 +77,14 @@ func TestHeightHintCacheConfirms(t *testing.T) {
// We'll also attempt to purge all of them in a single database // We'll also attempt to purge all of them in a single database
// transaction. // transaction.
if err := hintCache.PurgeConfirmHint(txHashes...); err != nil { if err := hintCache.PurgeConfirmHint(confRequests...); err != nil {
t.Fatalf("unable to remove confirm hints: %v", err) t.Fatalf("unable to remove confirm hints: %v", err)
} }
// Finally, we'll attempt to query for each hash. We should expect not // Finally, we'll attempt to query for each hash. We should expect not
// to find a hint for any of them. // to find a hint for any of them.
for _, txHash := range txHashes { for _, confRequest := range confRequests {
_, err := hintCache.QueryConfirmHint(txHash) _, err := hintCache.QueryConfirmHint(confRequest)
if err != ErrConfirmHintNotFound { if err != ErrConfirmHintNotFound {
t.Fatalf("expected ErrConfirmHintNotFound, got :%v", err) t.Fatalf("expected ErrConfirmHintNotFound, got :%v", err)
} }
@@ -97,8 +100,9 @@ func TestHeightHintCacheSpends(t *testing.T) {
// Querying for an outpoint not found within the cache should return an // Querying for an outpoint not found within the cache should return an
// error indication so. // error indication so.
var unknownOutPoint wire.OutPoint unknownOutPoint := wire.OutPoint{Index: 1}
_, err := hintCache.QuerySpendHint(unknownOutPoint) unknownSpendRequest := SpendRequest{OutPoint: unknownOutPoint}
_, err := hintCache.QuerySpendHint(unknownSpendRequest)
if err != ErrSpendHintNotFound { if err != ErrSpendHintNotFound {
t.Fatalf("expected ErrSpendHintNotFound, got: %v", err) t.Fatalf("expected ErrSpendHintNotFound, got: %v", err)
} }
@@ -107,21 +111,22 @@ func TestHeightHintCacheSpends(t *testing.T) {
// the same spend hint. // the same spend hint.
const height = 100 const height = 100
const numOutpoints = 5 const numOutpoints = 5
var txHash chainhash.Hash spendRequests := make([]SpendRequest, numOutpoints)
copy(txHash[:], bytes.Repeat([]byte{0xFF}, 32))
outpoints := make([]wire.OutPoint, numOutpoints)
for i := uint32(0); i < numOutpoints; i++ { for i := uint32(0); i < numOutpoints; i++ {
outpoints[i] = wire.OutPoint{Hash: txHash, Index: i} spendRequests[i] = SpendRequest{
OutPoint: wire.OutPoint{Index: i + 1},
}
} }
if err := hintCache.CommitSpendHint(height, outpoints...); err != nil { err = hintCache.CommitSpendHint(height, spendRequests...)
t.Fatalf("unable to add entry to cache: %v", err) if err != nil {
t.Fatalf("unable to add entries to cache: %v", err)
} }
// With the outpoints committed, we'll now query the cache to ensure // With the outpoints committed, we'll now query the cache to ensure
// that we're able to properly retrieve the confirm hints. // that we're able to properly retrieve the confirm hints.
for _, op := range outpoints { for _, spendRequest := range spendRequests {
spendHint, err := hintCache.QuerySpendHint(op) spendHint, err := hintCache.QuerySpendHint(spendRequest)
if err != nil { if err != nil {
t.Fatalf("unable to query for hint: %v", err) t.Fatalf("unable to query for hint: %v", err)
} }
@@ -133,14 +138,14 @@ func TestHeightHintCacheSpends(t *testing.T) {
// We'll also attempt to purge all of them in a single database // We'll also attempt to purge all of them in a single database
// transaction. // transaction.
if err := hintCache.PurgeSpendHint(outpoints...); err != nil { if err := hintCache.PurgeSpendHint(spendRequests...); err != nil {
t.Fatalf("unable to remove spend hint: %v", err) t.Fatalf("unable to remove spend hint: %v", err)
} }
// Finally, we'll attempt to query for each outpoint. We should expect // Finally, we'll attempt to query for each outpoint. We should expect
// not to find a hint for any of them. // not to find a hint for any of them.
for _, op := range outpoints { for _, spendRequest := range spendRequests {
_, err = hintCache.QuerySpendHint(op) _, err = hintCache.QuerySpendHint(spendRequest)
if err != ErrSpendHintNotFound { if err != ErrSpendHintNotFound {
t.Fatalf("expected ErrSpendHintNotFound, got: %v", err) t.Fatalf("expected ErrSpendHintNotFound, got: %v", err)
} }
+69 -31
View File
@@ -1,6 +1,7 @@
package chainntnfs package chainntnfs
import ( import (
"errors"
"fmt" "fmt"
"sync" "sync"
@@ -9,6 +10,12 @@ import (
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
) )
var (
// ErrChainNotifierShuttingDown is used when we are trying to
// measure a spend notification when notifier is already stopped.
ErrChainNotifierShuttingDown = errors.New("chain notifier shutting down")
)
// TxConfStatus denotes the status of a transaction's lookup. // TxConfStatus denotes the status of a transaction's lookup.
type TxConfStatus uint8 type TxConfStatus uint8
@@ -65,36 +72,44 @@ func (t TxConfStatus) String() string {
// //
// Concrete implementations of ChainNotifier should be able to support multiple // Concrete implementations of ChainNotifier should be able to support multiple
// concurrent client requests, as well as multiple concurrent notification events. // concurrent client requests, as well as multiple concurrent notification events.
// TODO(roasbeef): all events should have a Cancel() method to free up the
// resource
type ChainNotifier interface { type ChainNotifier interface {
// RegisterConfirmationsNtfn registers an intent to be notified once // RegisterConfirmationsNtfn registers an intent to be notified once
// txid reaches numConfs confirmations. We also pass in the pkScript as // txid reaches numConfs confirmations. We also pass in the pkScript as
// the default light client instead needs to match on scripts created // the default light client instead needs to match on scripts created in
// in the block. The returned ConfirmationEvent should properly notify // the block. If a nil txid is passed in, then not only should we match
// the client once the specified number of confirmations has been // on the script, but we should also dispatch once the transaction
// reached for the txid, as well as if the original tx gets re-org'd // containing the script reaches numConfs confirmations. This can be
// out of the mainchain. The heightHint parameter is provided as a // useful in instances where we only know the script in advance, but not
// convenience to light clients. The heightHint denotes the earliest // the transaction containing it.
// height in the blockchain in which the target txid _could_ have been //
// included in the chain. This can be used to bound the search space // The returned ConfirmationEvent should properly notify the client once
// when checking to see if a notification can immediately be dispatched // the specified number of confirmations has been reached for the txid,
// due to historical data. // as well as if the original tx gets re-org'd out of the mainchain. The
// heightHint parameter is provided as a convenience to light clients.
// It heightHint denotes the earliest height in the blockchain in which
// the target txid _could_ have been included in the chain. This can be
// used to bound the search space when checking to see if a notification
// can immediately be dispatched due to historical data.
// //
// NOTE: Dispatching notifications to multiple clients subscribed to // NOTE: Dispatching notifications to multiple clients subscribed to
// the same (txid, numConfs) tuple MUST be supported. // the same (txid, numConfs) tuple MUST be supported.
RegisterConfirmationsNtfn(txid *chainhash.Hash, pkScript []byte, numConfs, RegisterConfirmationsNtfn(txid *chainhash.Hash, pkScript []byte,
heightHint uint32) (*ConfirmationEvent, error) numConfs, heightHint uint32) (*ConfirmationEvent, error)
// RegisterSpendNtfn registers an intent to be notified once the target // RegisterSpendNtfn registers an intent to be notified once the target
// outpoint is successfully spent within a transaction. The script that // outpoint is successfully spent within a transaction. The script that
// the outpoint creates must also be specified. This allows this // the outpoint creates must also be specified. This allows this
// interface to be implemented by BIP 158-like filtering. The returned // interface to be implemented by BIP 158-like filtering. If a nil
// SpendEvent will receive a send on the 'Spend' transaction once a // outpoint is passed in, then not only should we match on the script,
// transaction spending the input is detected on the blockchain. The // but we should also dispatch once a transaction spends the output
// heightHint parameter is provided as a convenience to light clients. // containing said script. This can be useful in instances where we only
// The heightHint denotes the earliest height in the blockchain in // know the script in advance, but not the outpoint itself.
// which the target output could have been created. //
// The returned SpendEvent will receive a send on the 'Spend'
// transaction once a transaction spending the input is detected on the
// blockchain. The heightHint parameter is provided as a convenience to
// light clients. It denotes the earliest height in the blockchain in
// which the target output could have been spent.
// //
// NOTE: The notification should only be triggered when the spending // NOTE: The notification should only be triggered when the spending
// transaction receives a single confirmation. // transaction receives a single confirmation.
@@ -112,7 +127,9 @@ type ChainNotifier interface {
// Clients have the option of passing in their best known block. // Clients have the option of passing in their best known block.
// If they specify a block, the ChainNotifier checks whether the client // If they specify a block, the ChainNotifier checks whether the client
// is behind on blocks. If they are, the ChainNotifier sends a backlog // is behind on blocks. If they are, the ChainNotifier sends a backlog
// of block notifications for the missed blocks. // of block notifications for the missed blocks. If they do not provide
// one, then a notification will be dispatched immediately for the
// current tip of the chain upon a successful registration.
RegisterBlockEpochNtfn(*BlockEpoch) (*BlockEpochEvent, error) RegisterBlockEpochNtfn(*BlockEpoch) (*BlockEpochEvent, error)
// Start the ChainNotifier. Once started, the implementation should be // Start the ChainNotifier. Once started, the implementation should be
@@ -140,6 +157,9 @@ type TxConfirmation struct {
// TxIndex is the index within the block of the ultimate confirmed // TxIndex is the index within the block of the ultimate confirmed
// transaction. // transaction.
TxIndex uint32 TxIndex uint32
// Tx is the transaction for which the notification was requested for.
Tx *wire.MsgTx
} }
// ConfirmationEvent encapsulates a confirmation notification. With this struct, // ConfirmationEvent encapsulates a confirmation notification. With this struct,
@@ -155,6 +175,9 @@ type TxConfirmation struct {
// If the event that the original transaction becomes re-org'd out of the main // If the event that the original transaction becomes re-org'd out of the main
// chain, the 'NegativeConf' will be sent upon with a value representing the // chain, the 'NegativeConf' will be sent upon with a value representing the
// depth of the re-org. // depth of the re-org.
//
// NOTE: If the caller wishes to cancel their registered spend notification,
// the Cancel closure MUST be called.
type ConfirmationEvent struct { type ConfirmationEvent struct {
// Confirmed is a channel that will be sent upon once the transaction // Confirmed is a channel that will be sent upon once the transaction
// has been fully confirmed. The struct sent will contain all the // has been fully confirmed. The struct sent will contain all the
@@ -171,26 +194,34 @@ type ConfirmationEvent struct {
// confirmations. // confirmations.
Updates chan uint32 Updates chan uint32
// TODO(roasbeef): all goroutines on ln channel updates should also
// have a struct chan that's closed if funding gets re-org out. Need
// to sync, to request another confirmation event ntfn, then re-open
// channel after confs.
// NegativeConf is a channel that will be sent upon if the transaction // NegativeConf is a channel that will be sent upon if the transaction
// confirms, but is later reorged out of the chain. The integer sent // confirms, but is later reorged out of the chain. The integer sent
// through the channel represents the reorg depth. // through the channel represents the reorg depth.
// //
// NOTE: This channel must be buffered. // NOTE: This channel must be buffered.
NegativeConf chan int32 NegativeConf chan int32
// Done is a channel that gets sent upon once the confirmation request
// is no longer under the risk of being reorged out of the chain.
//
// NOTE: This channel must be buffered.
Done chan struct{}
// Cancel is a closure that should be executed by the caller in the case
// that they wish to prematurely abandon their registered confirmation
// notification.
Cancel func()
} }
// NewConfirmationEvent constructs a new ConfirmationEvent with newly opened // NewConfirmationEvent constructs a new ConfirmationEvent with newly opened
// channels. // channels.
func NewConfirmationEvent(numConfs uint32) *ConfirmationEvent { func NewConfirmationEvent(numConfs uint32, cancel func()) *ConfirmationEvent {
return &ConfirmationEvent{ return &ConfirmationEvent{
Confirmed: make(chan *TxConfirmation, 1), Confirmed: make(chan *TxConfirmation, 1),
Updates: make(chan uint32, numConfs), Updates: make(chan uint32, numConfs),
NegativeConf: make(chan int32, 1), NegativeConf: make(chan int32, 1),
Done: make(chan struct{}, 1),
Cancel: cancel,
} }
} }
@@ -227,8 +258,14 @@ type SpendEvent struct {
// NOTE: This channel must be buffered. // NOTE: This channel must be buffered.
Reorg chan struct{} Reorg chan struct{}
// Cancel is a closure that should be executed by the caller in the // Done is a channel that gets sent upon once the confirmation request
// case that they wish to prematurely abandon their registered spend // is no longer under the risk of being reorged out of the chain.
//
// NOTE: This channel must be buffered.
Done chan struct{}
// Cancel is a closure that should be executed by the caller in the case
// that they wish to prematurely abandon their registered spend
// notification. // notification.
Cancel func() Cancel func()
} }
@@ -238,6 +275,7 @@ func NewSpendEvent(cancel func()) *SpendEvent {
return &SpendEvent{ return &SpendEvent{
Spend: make(chan *SpendDetail, 1), Spend: make(chan *SpendDetail, 1),
Reorg: make(chan struct{}, 1), Reorg: make(chan struct{}, 1),
Done: make(chan struct{}, 1),
Cancel: cancel, Cancel: cancel,
} }
} }
@@ -267,8 +305,8 @@ type BlockEpochEvent struct {
// NOTE: This channel must be buffered. // NOTE: This channel must be buffered.
Epochs <-chan *BlockEpoch Epochs <-chan *BlockEpoch
// Cancel is a closure that should be executed by the caller in the // Cancel is a closure that should be executed by the caller in the case
// case that they wish to abandon their registered spend notification. // that they wish to abandon their registered block epochs notification.
Cancel func() Cancel func()
} }
+285 -132
View File
@@ -16,29 +16,18 @@ import (
"github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/chain"
_ "github.com/btcsuite/btcwallet/walletdb/bdb" // Required to auto-register the boltdb walletdb implementation.
"github.com/lightninglabs/neutrino" "github.com/lightninglabs/neutrino"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
// Required to auto-register the bitcoind backed ChainNotifier
// implementation.
"github.com/lightningnetwork/lnd/chainntnfs/bitcoindnotify" "github.com/lightningnetwork/lnd/chainntnfs/bitcoindnotify"
// Required to auto-register the btcd backed ChainNotifier
// implementation.
"github.com/lightningnetwork/lnd/chainntnfs/btcdnotify" "github.com/lightningnetwork/lnd/chainntnfs/btcdnotify"
// Required to auto-register the neutrino backed ChainNotifier
// implementation.
"github.com/lightningnetwork/lnd/chainntnfs/neutrinonotify" "github.com/lightningnetwork/lnd/chainntnfs/neutrinonotify"
"github.com/lightningnetwork/lnd/channeldb"
// Required to register the boltdb walletdb implementation.
"github.com/btcsuite/btcwallet/chain"
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
) )
func testSingleConfirmationNotification(miner *rpctest.Harness, func testSingleConfirmationNotification(miner *rpctest.Harness,
notifier chainntnfs.TestChainNotifier, t *testing.T) { notifier chainntnfs.TestChainNotifier, scriptDispatch bool, t *testing.T) {
// We'd like to test the case of being notified once a txid reaches // We'd like to test the case of being notified once a txid reaches
// a *single* confirmation. // a *single* confirmation.
@@ -62,9 +51,16 @@ func testSingleConfirmationNotification(miner *rpctest.Harness,
// Now that we have a txid, register a confirmation notification with // Now that we have a txid, register a confirmation notification with
// the chainntfn source. // the chainntfn source.
numConfs := uint32(1) numConfs := uint32(1)
confIntent, err := notifier.RegisterConfirmationsNtfn( var confIntent *chainntnfs.ConfirmationEvent
txid, pkScript, numConfs, uint32(currentHeight), if scriptDispatch {
) confIntent, err = notifier.RegisterConfirmationsNtfn(
nil, pkScript, numConfs, uint32(currentHeight),
)
} else {
confIntent, err = notifier.RegisterConfirmationsNtfn(
txid, pkScript, numConfs, uint32(currentHeight),
)
}
if err != nil { if err != nil {
t.Fatalf("unable to register ntfn: %v", err) t.Fatalf("unable to register ntfn: %v", err)
} }
@@ -106,7 +102,7 @@ func testSingleConfirmationNotification(miner *rpctest.Harness,
} }
func testMultiConfirmationNotification(miner *rpctest.Harness, func testMultiConfirmationNotification(miner *rpctest.Harness,
notifier chainntnfs.TestChainNotifier, t *testing.T) { notifier chainntnfs.TestChainNotifier, scriptDispatch bool, t *testing.T) {
// We'd like to test the case of being notified once a txid reaches // We'd like to test the case of being notified once a txid reaches
// N confirmations, where N > 1. // N confirmations, where N > 1.
@@ -127,9 +123,16 @@ func testMultiConfirmationNotification(miner *rpctest.Harness,
} }
numConfs := uint32(6) numConfs := uint32(6)
confIntent, err := notifier.RegisterConfirmationsNtfn( var confIntent *chainntnfs.ConfirmationEvent
txid, pkScript, numConfs, uint32(currentHeight), if scriptDispatch {
) confIntent, err = notifier.RegisterConfirmationsNtfn(
nil, pkScript, numConfs, uint32(currentHeight),
)
} else {
confIntent, err = notifier.RegisterConfirmationsNtfn(
txid, pkScript, numConfs, uint32(currentHeight),
)
}
if err != nil { if err != nil {
t.Fatalf("unable to register ntfn: %v", err) t.Fatalf("unable to register ntfn: %v", err)
} }
@@ -152,7 +155,7 @@ func testMultiConfirmationNotification(miner *rpctest.Harness,
} }
func testBatchConfirmationNotification(miner *rpctest.Harness, func testBatchConfirmationNotification(miner *rpctest.Harness,
notifier chainntnfs.TestChainNotifier, t *testing.T) { notifier chainntnfs.TestChainNotifier, scriptDispatch bool, t *testing.T) {
// We'd like to test a case of serving notifications to multiple // We'd like to test a case of serving notifications to multiple
// clients, each requesting to be notified once a txid receives // clients, each requesting to be notified once a txid receives
@@ -174,9 +177,16 @@ func testBatchConfirmationNotification(miner *rpctest.Harness,
if err != nil { if err != nil {
t.Fatalf("unable to create test addr: %v", err) t.Fatalf("unable to create test addr: %v", err)
} }
confIntent, err := notifier.RegisterConfirmationsNtfn( var confIntent *chainntnfs.ConfirmationEvent
txid, pkScript, numConfs, uint32(currentHeight), if scriptDispatch {
) confIntent, err = notifier.RegisterConfirmationsNtfn(
nil, pkScript, numConfs, uint32(currentHeight),
)
} else {
confIntent, err = notifier.RegisterConfirmationsNtfn(
txid, pkScript, numConfs, uint32(currentHeight),
)
}
if err != nil { if err != nil {
t.Fatalf("unable to register ntfn: %v", err) t.Fatalf("unable to register ntfn: %v", err)
} }
@@ -257,13 +267,13 @@ func checkNotificationFields(ntfn *chainntnfs.SpendDetail,
} }
func testSpendNotification(miner *rpctest.Harness, func testSpendNotification(miner *rpctest.Harness,
notifier chainntnfs.TestChainNotifier, t *testing.T) { notifier chainntnfs.TestChainNotifier, scriptDispatch bool, t *testing.T) {
// We'd like to test the spend notifications for all ChainNotifier // We'd like to test the spend notifications for all ChainNotifier
// concrete implementations. // concrete implementations.
// //
// To do so, we first create a new output to our test target address. // To do so, we first create a new output to our test target address.
outpoint, pkScript := chainntnfs.CreateSpendableOutput(t, miner) outpoint, output, privKey := chainntnfs.CreateSpendableOutput(t, miner)
_, currentHeight, err := miner.Node.GetBestBlock() _, currentHeight, err := miner.Node.GetBestBlock()
if err != nil { if err != nil {
@@ -277,9 +287,16 @@ func testSpendNotification(miner *rpctest.Harness,
const numClients = 5 const numClients = 5
spendClients := make([]*chainntnfs.SpendEvent, numClients) spendClients := make([]*chainntnfs.SpendEvent, numClients)
for i := 0; i < numClients; i++ { for i := 0; i < numClients; i++ {
spentIntent, err := notifier.RegisterSpendNtfn( var spentIntent *chainntnfs.SpendEvent
outpoint, pkScript, uint32(currentHeight), if scriptDispatch {
) spentIntent, err = notifier.RegisterSpendNtfn(
nil, output.PkScript, uint32(currentHeight),
)
} else {
spentIntent, err = notifier.RegisterSpendNtfn(
outpoint, output.PkScript, uint32(currentHeight),
)
}
if err != nil { if err != nil {
t.Fatalf("unable to register for spend ntfn: %v", err) t.Fatalf("unable to register for spend ntfn: %v", err)
} }
@@ -288,7 +305,7 @@ func testSpendNotification(miner *rpctest.Harness,
} }
// Next, create a new transaction spending that output. // Next, create a new transaction spending that output.
spendingTx := chainntnfs.CreateSpendTx(t, outpoint, pkScript) spendingTx := chainntnfs.CreateSpendTx(t, outpoint, output, privKey)
// Broadcast our spending transaction. // Broadcast our spending transaction.
spenderSha, err := miner.Node.SendRawTransaction(spendingTx, true) spenderSha, err := miner.Node.SendRawTransaction(spendingTx, true)
@@ -328,9 +345,16 @@ func testSpendNotification(miner *rpctest.Harness,
// Make sure registering a client after the tx is in the mempool still // Make sure registering a client after the tx is in the mempool still
// doesn't trigger a notification. // doesn't trigger a notification.
spentIntent, err := notifier.RegisterSpendNtfn( var spentIntent *chainntnfs.SpendEvent
outpoint, pkScript, uint32(currentHeight), if scriptDispatch {
) spentIntent, err = notifier.RegisterSpendNtfn(
nil, output.PkScript, uint32(currentHeight),
)
} else {
spentIntent, err = notifier.RegisterSpendNtfn(
outpoint, output.PkScript, uint32(currentHeight),
)
}
if err != nil { if err != nil {
t.Fatalf("unable to register for spend ntfn: %v", err) t.Fatalf("unable to register for spend ntfn: %v", err)
} }
@@ -374,22 +398,23 @@ func testBlockEpochNotification(miner *rpctest.Harness,
// block epoch notifications. // block epoch notifications.
const numBlocks = 10 const numBlocks = 10
const numNtfns = numBlocks + 1
const numClients = 5 const numClients = 5
var wg sync.WaitGroup var wg sync.WaitGroup
// Create numClients clients which will listen for block notifications. We // Create numClients clients which will listen for block notifications. We
// expect each client to receive 10 notifications for each of the ten // expect each client to receive 11 notifications, one for the current
// blocks we generate below. So we'll use a WaitGroup to synchronize the // tip of the chain, and one for each of the ten blocks we generate
// test. // below. So we'll use a WaitGroup to synchronize the test.
for i := 0; i < numClients; i++ { for i := 0; i < numClients; i++ {
epochClient, err := notifier.RegisterBlockEpochNtfn(nil) epochClient, err := notifier.RegisterBlockEpochNtfn(nil)
if err != nil { if err != nil {
t.Fatalf("unable to register for epoch notification") t.Fatalf("unable to register for epoch notification")
} }
wg.Add(numBlocks) wg.Add(numNtfns)
go func() { go func() {
for i := 0; i < numBlocks; i++ { for i := 0; i < numNtfns; i++ {
<-epochClient.Epochs <-epochClient.Epochs
wg.Done() wg.Done()
} }
@@ -416,7 +441,7 @@ func testBlockEpochNotification(miner *rpctest.Harness,
} }
func testMultiClientConfirmationNotification(miner *rpctest.Harness, func testMultiClientConfirmationNotification(miner *rpctest.Harness,
notifier chainntnfs.TestChainNotifier, t *testing.T) { notifier chainntnfs.TestChainNotifier, scriptDispatch bool, t *testing.T) {
// We'd like to test the case of a multiple clients registered to // We'd like to test the case of a multiple clients registered to
// receive a confirmation notification for the same transaction. // receive a confirmation notification for the same transaction.
@@ -442,9 +467,16 @@ func testMultiClientConfirmationNotification(miner *rpctest.Harness,
// Register for a conf notification for the above generated txid with // Register for a conf notification for the above generated txid with
// numConfsClients distinct clients. // numConfsClients distinct clients.
for i := 0; i < numConfsClients; i++ { for i := 0; i < numConfsClients; i++ {
confClient, err := notifier.RegisterConfirmationsNtfn( var confClient *chainntnfs.ConfirmationEvent
txid, pkScript, numConfs, uint32(currentHeight), if scriptDispatch {
) confClient, err = notifier.RegisterConfirmationsNtfn(
nil, pkScript, numConfs, uint32(currentHeight),
)
} else {
confClient, err = notifier.RegisterConfirmationsNtfn(
txid, pkScript, numConfs, uint32(currentHeight),
)
}
if err != nil { if err != nil {
t.Fatalf("unable to register for confirmation: %v", err) t.Fatalf("unable to register for confirmation: %v", err)
} }
@@ -479,7 +511,7 @@ func testMultiClientConfirmationNotification(miner *rpctest.Harness,
// transaction that has already been included in a block. In this case, the // transaction that has already been included in a block. In this case, the
// confirmation notification should be dispatched immediately. // confirmation notification should be dispatched immediately.
func testTxConfirmedBeforeNtfnRegistration(miner *rpctest.Harness, func testTxConfirmedBeforeNtfnRegistration(miner *rpctest.Harness,
notifier chainntnfs.TestChainNotifier, t *testing.T) { notifier chainntnfs.TestChainNotifier, scriptDispatch bool, t *testing.T) {
// First, let's send some coins to "ourself", obtaining a txid. We're // First, let's send some coins to "ourself", obtaining a txid. We're
// spending from a coinbase output here, so we use the dedicated // spending from a coinbase output here, so we use the dedicated
@@ -533,9 +565,16 @@ func testTxConfirmedBeforeNtfnRegistration(miner *rpctest.Harness,
// which is included in the last block. The height hint is the height before // which is included in the last block. The height hint is the height before
// the block is included. This notification should fire immediately since // the block is included. This notification should fire immediately since
// only 1 confirmation is required. // only 1 confirmation is required.
ntfn1, err := notifier.RegisterConfirmationsNtfn( var ntfn1 *chainntnfs.ConfirmationEvent
txid1, pkScript1, 1, uint32(currentHeight), if scriptDispatch {
) ntfn1, err = notifier.RegisterConfirmationsNtfn(
nil, pkScript1, 1, uint32(currentHeight),
)
} else {
ntfn1, err = notifier.RegisterConfirmationsNtfn(
txid1, pkScript1, 1, uint32(currentHeight),
)
}
if err != nil { if err != nil {
t.Fatalf("unable to register ntfn: %v", err) t.Fatalf("unable to register ntfn: %v", err)
} }
@@ -572,9 +611,16 @@ func testTxConfirmedBeforeNtfnRegistration(miner *rpctest.Harness,
// Register a confirmation notification for tx2, requiring 3 confirmations. // Register a confirmation notification for tx2, requiring 3 confirmations.
// This transaction is only partially confirmed, so the notification should // This transaction is only partially confirmed, so the notification should
// not fire yet. // not fire yet.
ntfn2, err := notifier.RegisterConfirmationsNtfn( var ntfn2 *chainntnfs.ConfirmationEvent
txid2, pkScript2, 3, uint32(currentHeight), if scriptDispatch {
) ntfn2, err = notifier.RegisterConfirmationsNtfn(
nil, pkScript2, 3, uint32(currentHeight),
)
} else {
ntfn2, err = notifier.RegisterConfirmationsNtfn(
txid2, pkScript2, 3, uint32(currentHeight),
)
}
if err != nil { if err != nil {
t.Fatalf("unable to register ntfn: %v", err) t.Fatalf("unable to register ntfn: %v", err)
} }
@@ -600,9 +646,16 @@ func testTxConfirmedBeforeNtfnRegistration(miner *rpctest.Harness,
// Finally register a confirmation notification for tx3, requiring 1 // Finally register a confirmation notification for tx3, requiring 1
// confirmation. Ensure that conf notifications do not refire on txs // confirmation. Ensure that conf notifications do not refire on txs
// 1 or 2. // 1 or 2.
ntfn3, err := notifier.RegisterConfirmationsNtfn( var ntfn3 *chainntnfs.ConfirmationEvent
txid3, pkScript3, 1, uint32(currentHeight-1), if scriptDispatch {
) ntfn3, err = notifier.RegisterConfirmationsNtfn(
nil, pkScript3, 1, uint32(currentHeight-1),
)
} else {
ntfn3, err = notifier.RegisterConfirmationsNtfn(
txid3, pkScript3, 1, uint32(currentHeight-1),
)
}
if err != nil { if err != nil {
t.Fatalf("unable to register ntfn: %v", err) t.Fatalf("unable to register ntfn: %v", err)
} }
@@ -632,7 +685,7 @@ func testTxConfirmedBeforeNtfnRegistration(miner *rpctest.Harness,
// checking for a confirmation. This should not cause the notifier to stop // checking for a confirmation. This should not cause the notifier to stop
// working // working
func testLazyNtfnConsumer(miner *rpctest.Harness, func testLazyNtfnConsumer(miner *rpctest.Harness,
notifier chainntnfs.TestChainNotifier, t *testing.T) { notifier chainntnfs.TestChainNotifier, scriptDispatch bool, t *testing.T) {
// Create a transaction to be notified about. We'll register for // Create a transaction to be notified about. We'll register for
// notifications on this transaction but won't be prompt in checking them // notifications on this transaction but won't be prompt in checking them
@@ -657,9 +710,16 @@ func testLazyNtfnConsumer(miner *rpctest.Harness,
t.Fatalf("unable to generate blocks: %v", err) t.Fatalf("unable to generate blocks: %v", err)
} }
firstConfIntent, err := notifier.RegisterConfirmationsNtfn( var firstConfIntent *chainntnfs.ConfirmationEvent
txid, pkScript, numConfs, uint32(currentHeight), if scriptDispatch {
) firstConfIntent, err = notifier.RegisterConfirmationsNtfn(
nil, pkScript, numConfs, uint32(currentHeight),
)
} else {
firstConfIntent, err = notifier.RegisterConfirmationsNtfn(
txid, pkScript, numConfs, uint32(currentHeight),
)
}
if err != nil { if err != nil {
t.Fatalf("unable to register ntfn: %v", err) t.Fatalf("unable to register ntfn: %v", err)
} }
@@ -686,10 +746,16 @@ func testLazyNtfnConsumer(miner *rpctest.Harness,
} }
numConfs = 1 numConfs = 1
var secondConfIntent *chainntnfs.ConfirmationEvent
secondConfIntent, err := notifier.RegisterConfirmationsNtfn( if scriptDispatch {
txid, pkScript, numConfs, uint32(currentHeight), secondConfIntent, err = notifier.RegisterConfirmationsNtfn(
) nil, pkScript, numConfs, uint32(currentHeight),
)
} else {
secondConfIntent, err = notifier.RegisterConfirmationsNtfn(
txid, pkScript, numConfs, uint32(currentHeight),
)
}
if err != nil { if err != nil {
t.Fatalf("unable to register ntfn: %v", err) t.Fatalf("unable to register ntfn: %v", err)
} }
@@ -719,16 +785,21 @@ func testLazyNtfnConsumer(miner *rpctest.Harness,
// has already been included in a block. In this case, the spend notification // has already been included in a block. In this case, the spend notification
// should be dispatched immediately. // should be dispatched immediately.
func testSpendBeforeNtfnRegistration(miner *rpctest.Harness, func testSpendBeforeNtfnRegistration(miner *rpctest.Harness,
notifier chainntnfs.TestChainNotifier, t *testing.T) { notifier chainntnfs.TestChainNotifier, scriptDispatch bool, t *testing.T) {
// We'd like to test the spend notifications for all ChainNotifier // We'd like to test the spend notifications for all ChainNotifier
// concrete implementations. // concrete implementations.
// //
// To do so, we first create a new output to our test target address. // To do so, we first create a new output to our test target address.
outpoint, pkScript := chainntnfs.CreateSpendableOutput(t, miner) outpoint, output, privKey := chainntnfs.CreateSpendableOutput(t, miner)
_, heightHint, err := miner.Node.GetBestBlock()
if err != nil {
t.Fatalf("unable to get current height: %v", err)
}
// We'll then spend this output and broadcast the spend transaction. // We'll then spend this output and broadcast the spend transaction.
spendingTx := chainntnfs.CreateSpendTx(t, outpoint, pkScript) spendingTx := chainntnfs.CreateSpendTx(t, outpoint, output, privKey)
spenderSha, err := miner.Node.SendRawTransaction(spendingTx, true) spenderSha, err := miner.Node.SendRawTransaction(spendingTx, true)
if err != nil { if err != nil {
t.Fatalf("unable to broadcast tx: %v", err) t.Fatalf("unable to broadcast tx: %v", err)
@@ -748,8 +819,7 @@ func testSpendBeforeNtfnRegistration(miner *rpctest.Harness,
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)
} }
_, spendHeight, 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)
} }
@@ -763,9 +833,17 @@ func testSpendBeforeNtfnRegistration(miner *rpctest.Harness,
const numClients = 2 const numClients = 2
spendClients := make([]*chainntnfs.SpendEvent, numClients) spendClients := make([]*chainntnfs.SpendEvent, numClients)
for i := 0; i < numClients; i++ { for i := 0; i < numClients; i++ {
spentIntent, err := notifier.RegisterSpendNtfn( var spentIntent *chainntnfs.SpendEvent
outpoint, pkScript, uint32(currentHeight), if scriptDispatch {
) spentIntent, err = notifier.RegisterSpendNtfn(
nil, output.PkScript, uint32(heightHint),
)
} else {
spentIntent, err = notifier.RegisterSpendNtfn(
outpoint, output.PkScript,
uint32(heightHint),
)
}
if err != nil { if err != nil {
t.Fatalf("unable to register for spend ntfn: %v", t.Fatalf("unable to register for spend ntfn: %v",
err) err)
@@ -779,8 +857,9 @@ func testSpendBeforeNtfnRegistration(miner *rpctest.Harness,
case ntfn := <-client.Spend: case ntfn := <-client.Spend:
// We've received the spend nftn. So now verify // We've received the spend nftn. So now verify
// all the fields have been set properly. // all the fields have been set properly.
checkNotificationFields(ntfn, outpoint, spenderSha, checkNotificationFields(
currentHeight, t) ntfn, outpoint, spenderSha, spendHeight, t,
)
case <-time.After(30 * time.Second): case <-time.After(30 * time.Second):
t.Fatalf("spend ntfn never received") t.Fatalf("spend ntfn never received")
} }
@@ -824,14 +903,14 @@ func testSpendBeforeNtfnRegistration(miner *rpctest.Harness,
} }
func testCancelSpendNtfn(node *rpctest.Harness, func testCancelSpendNtfn(node *rpctest.Harness,
notifier chainntnfs.TestChainNotifier, t *testing.T) { notifier chainntnfs.TestChainNotifier, scriptDispatch bool, t *testing.T) {
// We'd like to test that once a spend notification is registered, it // We'd like to test that once a spend notification is registered, it
// can be cancelled before the notification is dispatched. // can be cancelled before the notification is dispatched.
// First, we'll start by creating a new output that we can spend // First, we'll start by creating a new output that we can spend
// ourselves. // ourselves.
outpoint, pkScript := chainntnfs.CreateSpendableOutput(t, node) outpoint, output, privKey := chainntnfs.CreateSpendableOutput(t, node)
_, currentHeight, err := node.Node.GetBestBlock() _, currentHeight, err := node.Node.GetBestBlock()
if err != nil { if err != nil {
@@ -844,9 +923,16 @@ func testCancelSpendNtfn(node *rpctest.Harness,
const numClients = 2 const numClients = 2
spendClients := make([]*chainntnfs.SpendEvent, numClients) spendClients := make([]*chainntnfs.SpendEvent, numClients)
for i := 0; i < numClients; i++ { for i := 0; i < numClients; i++ {
spentIntent, err := notifier.RegisterSpendNtfn( var spentIntent *chainntnfs.SpendEvent
outpoint, pkScript, uint32(currentHeight), if scriptDispatch {
) spentIntent, err = notifier.RegisterSpendNtfn(
nil, output.PkScript, uint32(currentHeight),
)
} else {
spentIntent, err = notifier.RegisterSpendNtfn(
outpoint, output.PkScript, uint32(currentHeight),
)
}
if err != nil { if err != nil {
t.Fatalf("unable to register for spend ntfn: %v", err) t.Fatalf("unable to register for spend ntfn: %v", err)
} }
@@ -855,7 +941,7 @@ func testCancelSpendNtfn(node *rpctest.Harness,
} }
// Next, create a new transaction spending that output. // Next, create a new transaction spending that output.
spendingTx := chainntnfs.CreateSpendTx(t, outpoint, pkScript) spendingTx := chainntnfs.CreateSpendTx(t, outpoint, output, privKey)
// Before we broadcast the spending transaction, we'll cancel the // Before we broadcast the spending transaction, we'll cancel the
// notification of the first client. // notification of the first client.
@@ -877,8 +963,8 @@ func testCancelSpendNtfn(node *rpctest.Harness,
t.Fatalf("unable to generate single block: %v", err) t.Fatalf("unable to generate single block: %v", err)
} }
// However, the spend notification for the first client should have // The spend notification for the first client should have been
// been dispatched. // dispatched.
select { select {
case ntfn := <-spendClients[0].Spend: case ntfn := <-spendClients[0].Spend:
// We've received the spend nftn. So now verify all the // We've received the spend nftn. So now verify all the
@@ -902,7 +988,7 @@ func testCancelSpendNtfn(node *rpctest.Harness,
t.Fatalf("spend ntfn never received") t.Fatalf("spend ntfn never received")
} }
// However, The spend notification of the second client should NOT have // However, the spend notification of the second client should NOT have
// been dispatched. // been dispatched.
select { select {
case _, ok := <-spendClients[1].Spend: case _, ok := <-spendClients[1].Spend:
@@ -914,8 +1000,8 @@ func testCancelSpendNtfn(node *rpctest.Harness,
} }
} }
func testCancelEpochNtfn(node *rpctest.Harness, notifier chainntnfs.TestChainNotifier, func testCancelEpochNtfn(node *rpctest.Harness,
t *testing.T) { notifier chainntnfs.TestChainNotifier, t *testing.T) {
// We'd like to ensure that once a client cancels their block epoch // We'd like to ensure that once a client cancels their block epoch
// notifications, no further notifications are sent over the channel // notifications, no further notifications are sent over the channel
@@ -964,8 +1050,8 @@ func testCancelEpochNtfn(node *rpctest.Harness, notifier chainntnfs.TestChainNot
} }
} }
func testReorgConf(miner *rpctest.Harness, notifier chainntnfs.TestChainNotifier, func testReorgConf(miner *rpctest.Harness,
t *testing.T) { notifier chainntnfs.TestChainNotifier, scriptDispatch bool, t *testing.T) {
// Set up a new miner that we can use to cause a reorg. // Set up a new miner that we can use to cause a reorg.
miner2, err := rpctest.New(chainntnfs.NetParams, nil, []string{"--txindex"}) miner2, err := rpctest.New(chainntnfs.NetParams, nil, []string{"--txindex"})
@@ -1026,9 +1112,16 @@ func testReorgConf(miner *rpctest.Harness, notifier chainntnfs.TestChainNotifier
// Now that we have a txid, register a confirmation notification with // Now that we have a txid, register a confirmation notification with
// the chainntfn source. // the chainntfn source.
numConfs := uint32(2) numConfs := uint32(2)
confIntent, err := notifier.RegisterConfirmationsNtfn( var confIntent *chainntnfs.ConfirmationEvent
txid, pkScript, numConfs, uint32(currentHeight), if scriptDispatch {
) confIntent, err = notifier.RegisterConfirmationsNtfn(
nil, pkScript, numConfs, uint32(currentHeight),
)
} else {
confIntent, err = notifier.RegisterConfirmationsNtfn(
txid, pkScript, numConfs, uint32(currentHeight),
)
}
if err != nil { if err != nil {
t.Fatalf("unable to register ntfn: %v", err) t.Fatalf("unable to register ntfn: %v", err)
} }
@@ -1116,18 +1209,26 @@ func testReorgConf(miner *rpctest.Harness, notifier chainntnfs.TestChainNotifier
// correctly handle outpoints whose spending transaction has been reorged out of // correctly handle outpoints whose spending transaction has been reorged out of
// the chain. // the chain.
func testReorgSpend(miner *rpctest.Harness, func testReorgSpend(miner *rpctest.Harness,
notifier chainntnfs.TestChainNotifier, t *testing.T) { notifier chainntnfs.TestChainNotifier, scriptDispatch bool, t *testing.T) {
// We'll start by creating an output and registering a spend // We'll start by creating an output and registering a spend
// notification for it. // notification for it.
outpoint, pkScript := chainntnfs.CreateSpendableOutput(t, miner) outpoint, output, privKey := chainntnfs.CreateSpendableOutput(t, miner)
_, currentHeight, err := miner.Node.GetBestBlock() _, heightHint, err := miner.Node.GetBestBlock()
if err != nil { if err != nil {
t.Fatalf("unable to retrieve current height: %v", err) t.Fatalf("unable to retrieve current height: %v", err)
} }
spendIntent, err := notifier.RegisterSpendNtfn(
outpoint, pkScript, uint32(currentHeight), var spendIntent *chainntnfs.SpendEvent
) if scriptDispatch {
spendIntent, err = notifier.RegisterSpendNtfn(
nil, output.PkScript, uint32(heightHint),
)
} else {
spendIntent, err = notifier.RegisterSpendNtfn(
outpoint, output.PkScript, uint32(heightHint),
)
}
if err != nil { if err != nil {
t.Fatalf("unable to register for spend: %v", err) t.Fatalf("unable to register for spend: %v", err)
} }
@@ -1174,7 +1275,7 @@ func testReorgSpend(miner *rpctest.Harness,
// Craft the spending transaction for the outpoint created above and // Craft the spending transaction for the outpoint created above and
// confirm it under the chain of the original miner. // confirm it under the chain of the original miner.
spendTx := chainntnfs.CreateSpendTx(t, outpoint, pkScript) spendTx := chainntnfs.CreateSpendTx(t, outpoint, output, privKey)
spendTxHash, err := miner.Node.SendRawTransaction(spendTx, true) spendTxHash, err := miner.Node.SendRawTransaction(spendTx, true)
if err != nil { if err != nil {
t.Fatalf("unable to broadcast spend tx: %v", err) t.Fatalf("unable to broadcast spend tx: %v", err)
@@ -1186,14 +1287,17 @@ func testReorgSpend(miner *rpctest.Harness,
if _, err := miner.Node.Generate(numBlocks); err != nil { if _, err := miner.Node.Generate(numBlocks); err != nil {
t.Fatalf("unable to generate blocks: %v", err) t.Fatalf("unable to generate blocks: %v", err)
} }
_, spendHeight, err := miner.Node.GetBestBlock()
if err != nil {
t.Fatalf("unable to get spend height: %v", err)
}
// We should see a spend notification dispatched with the correct spend // We should see a spend notification dispatched with the correct spend
// details. // details.
select { select {
case spendDetails := <-spendIntent.Spend: case spendDetails := <-spendIntent.Spend:
checkNotificationFields( checkNotificationFields(
spendDetails, outpoint, spendTxHash, spendDetails, outpoint, spendTxHash, spendHeight, t,
currentHeight+numBlocks, t,
) )
case <-time.After(5 * time.Second): case <-time.After(5 * time.Second):
t.Fatal("expected spend notification to be dispatched") t.Fatal("expected spend notification to be dispatched")
@@ -1243,19 +1347,18 @@ func testReorgSpend(miner *rpctest.Harness,
if err := chainntnfs.WaitForMempoolTx(miner, spendTxHash); err != nil { if err := chainntnfs.WaitForMempoolTx(miner, spendTxHash); err != nil {
t.Fatalf("tx not relayed to miner: %v", err) t.Fatalf("tx not relayed to miner: %v", err)
} }
_, currentHeight, err = miner.Node.GetBestBlock()
if err != nil {
t.Fatalf("unable to retrieve current height: %v", err)
}
if _, err := miner.Node.Generate(numBlocks); err != nil { if _, err := miner.Node.Generate(numBlocks); err != nil {
t.Fatalf("unable to generate single block: %v", err) t.Fatalf("unable to generate single block: %v", err)
} }
_, spendHeight, err = miner.Node.GetBestBlock()
if err != nil {
t.Fatalf("unable to retrieve current height: %v", err)
}
select { select {
case spendDetails := <-spendIntent.Spend: case spendDetails := <-spendIntent.Spend:
checkNotificationFields( checkNotificationFields(
spendDetails, outpoint, spendTxHash, spendDetails, outpoint, spendTxHash, spendHeight, t,
currentHeight+numBlocks, t,
) )
case <-time.After(5 * time.Second): case <-time.After(5 * time.Second):
t.Fatal("expected spend notification to be dispatched") t.Fatal("expected spend notification to be dispatched")
@@ -1392,6 +1495,16 @@ func testCatchUpOnMissedBlocks(miner *rpctest.Harness,
if err != nil { if err != nil {
t.Fatalf("unable to register for epoch notification: %v", err) t.Fatalf("unable to register for epoch notification: %v", err)
} }
// Drain the notification dispatched upon registration as we're
// not interested in it.
select {
case <-epochClient.Epochs:
case <-time.After(5 * time.Second):
t.Fatal("expected to receive epoch for current block " +
"upon registration")
}
clients = append(clients, epochClient) clients = append(clients, epochClient)
} }
@@ -1567,6 +1680,16 @@ func testCatchUpOnMissedBlocksWithReorg(miner1 *rpctest.Harness,
if err != nil { if err != nil {
t.Fatalf("unable to register for epoch notification: %v", err) t.Fatalf("unable to register for epoch notification: %v", err)
} }
// Drain the notification dispatched upon registration as we're
// not interested in it.
select {
case <-epochClient.Epochs:
case <-time.After(5 * time.Second):
t.Fatal("expected to receive epoch for current block " +
"upon registration")
}
clients = append(clients, epochClient) clients = append(clients, epochClient)
} }
@@ -1642,7 +1765,13 @@ func testCatchUpOnMissedBlocksWithReorg(miner1 *rpctest.Harness,
} }
} }
type testCase struct { type txNtfnTestCase struct {
name string
test func(node *rpctest.Harness, notifier chainntnfs.TestChainNotifier,
scriptDispatch bool, t *testing.T)
}
type blockNtfnTestCase struct {
name string name string
test func(node *rpctest.Harness, notifier chainntnfs.TestChainNotifier, test func(node *rpctest.Harness, notifier chainntnfs.TestChainNotifier,
t *testing.T) t *testing.T)
@@ -1654,7 +1783,7 @@ type blockCatchupTestCase struct {
t *testing.T) t *testing.T)
} }
var ntfnTests = []testCase{ var txNtfnTests = []txNtfnTestCase{
{ {
name: "single conf ntfn", name: "single conf ntfn",
test: testSingleConfirmationNotification, test: testSingleConfirmationNotification,
@@ -1672,41 +1801,44 @@ var ntfnTests = []testCase{
test: testMultiClientConfirmationNotification, test: testMultiClientConfirmationNotification,
}, },
{ {
name: "spend ntfn", name: "lazy ntfn consumer",
test: testSpendNotification, test: testLazyNtfnConsumer,
},
{
name: "block epoch",
test: testBlockEpochNotification,
}, },
{ {
name: "historical conf dispatch", name: "historical conf dispatch",
test: testTxConfirmedBeforeNtfnRegistration, test: testTxConfirmedBeforeNtfnRegistration,
}, },
{
name: "reorg conf",
test: testReorgConf,
},
{
name: "spend ntfn",
test: testSpendNotification,
},
{ {
name: "historical spend dispatch", name: "historical spend dispatch",
test: testSpendBeforeNtfnRegistration, test: testSpendBeforeNtfnRegistration,
}, },
{
name: "reorg spend",
test: testReorgSpend,
},
{ {
name: "cancel spend ntfn", name: "cancel spend ntfn",
test: testCancelSpendNtfn, test: testCancelSpendNtfn,
}, },
}
var blockNtfnTests = []blockNtfnTestCase{
{
name: "block epoch",
test: testBlockEpochNotification,
},
{ {
name: "cancel epoch ntfn", name: "cancel epoch ntfn",
test: testCancelEpochNtfn, test: testCancelEpochNtfn,
}, },
{
name: "lazy ntfn consumer",
test: testLazyNtfnConsumer,
},
{
name: "reorg conf",
test: testReorgConf,
},
{
name: "reorg spend",
test: testReorgSpend,
},
} }
var blockCatchupTests = []blockCatchupTestCase{ var blockCatchupTests = []blockCatchupTestCase{
@@ -1746,7 +1878,8 @@ func TestInterfaces(t *testing.T) {
rpcConfig := miner.RPCConfig() rpcConfig := miner.RPCConfig()
p2pAddr := miner.P2PAddress() p2pAddr := miner.P2PAddress()
log.Printf("Running %v ChainNotifier interface tests", len(ntfnTests)) log.Printf("Running %v ChainNotifier interface tests",
2*len(txNtfnTests)+len(blockNtfnTests)+len(blockCatchupTests))
for _, notifierDriver := range chainntnfs.RegisteredNotifiers() { for _, notifierDriver := range chainntnfs.RegisteredNotifiers() {
// Initialize a height hint cache for each notifier. // Initialize a height hint cache for each notifier.
@@ -1777,14 +1910,16 @@ func TestInterfaces(t *testing.T) {
) )
newNotifier = func() (chainntnfs.TestChainNotifier, error) { newNotifier = func() (chainntnfs.TestChainNotifier, error) {
return bitcoindnotify.New( return bitcoindnotify.New(
bitcoindConn, hintCache, hintCache, bitcoindConn, chainntnfs.NetParams,
hintCache, hintCache,
), nil ), nil
} }
case "btcd": case "btcd":
newNotifier = func() (chainntnfs.TestChainNotifier, error) { newNotifier = func() (chainntnfs.TestChainNotifier, error) {
return btcdnotify.New( return btcdnotify.New(
&rpcConfig, hintCache, hintCache, &rpcConfig, chainntnfs.NetParams,
hintCache, hintCache,
) )
} }
@@ -1796,7 +1931,7 @@ func TestInterfaces(t *testing.T) {
newNotifier = func() (chainntnfs.TestChainNotifier, error) { newNotifier = func() (chainntnfs.TestChainNotifier, error) {
return neutrinonotify.New( return neutrinonotify.New(
spvNode, hintCache, hintCache, spvNode, hintCache, hintCache,
) ), nil
} }
} }
@@ -1813,12 +1948,30 @@ func TestInterfaces(t *testing.T) {
notifierType, err) notifierType, err)
} }
for _, ntfnTest := range ntfnTests { for _, txNtfnTest := range txNtfnTests {
testName := fmt.Sprintf("%v: %v", notifierType, for _, scriptDispatch := range []bool{false, true} {
ntfnTest.name) testName := fmt.Sprintf("%v %v", notifierType,
txNtfnTest.name)
if scriptDispatch {
testName += " with script dispatch"
}
success := t.Run(testName, func(t *testing.T) {
txNtfnTest.test(
miner, notifier, scriptDispatch,
t,
)
})
if !success {
break
}
}
}
for _, blockNtfnTest := range blockNtfnTests {
testName := fmt.Sprintf("%v %v", notifierType,
blockNtfnTest.name)
success := t.Run(testName, func(t *testing.T) { success := t.Run(testName, func(t *testing.T) {
ntfnTest.test(miner, notifier, t) blockNtfnTest.test(miner, notifier, t)
}) })
if !success { if !success {
break break
@@ -1836,7 +1989,7 @@ func TestInterfaces(t *testing.T) {
notifierType, err) notifierType, err)
} }
testName := fmt.Sprintf("%v: %v", notifierType, testName := fmt.Sprintf("%v %v", notifierType,
blockCatchupTest.name) blockCatchupTest.name)
success := t.Run(testName, func(t *testing.T) { success := t.Run(testName, func(t *testing.T) {
+3 -3
View File
@@ -11,9 +11,9 @@ import (
// createNewNotifier creates a new instance of the ChainNotifier interface // createNewNotifier creates a new instance of the ChainNotifier interface
// implemented by NeutrinoNotifier. // implemented by NeutrinoNotifier.
func createNewNotifier(args ...interface{}) (chainntnfs.ChainNotifier, error) { func createNewNotifier(args ...interface{}) (chainntnfs.ChainNotifier, error) {
if len(args) != 2 { if len(args) != 3 {
return nil, fmt.Errorf("incorrect number of arguments to "+ return nil, fmt.Errorf("incorrect number of arguments to "+
".New(...), expected 2, instead passed %v", len(args)) ".New(...), expected 3, instead passed %v", len(args))
} }
config, ok := args[0].(*neutrino.ChainService) config, ok := args[0].(*neutrino.ChainService)
@@ -34,7 +34,7 @@ func createNewNotifier(args ...interface{}) (chainntnfs.ChainNotifier, error) {
"is incorrect, expected a chainntfs.ConfirmHintCache") "is incorrect, expected a chainntfs.ConfirmHintCache")
} }
return New(config, spendHintCache, confirmHintCache) return New(config, spendHintCache, confirmHintCache), nil
} }
// init registers a driver for the NeutrinoNotify concrete implementation of // init registers a driver for the NeutrinoNotify concrete implementation of
+230 -149
View File
@@ -27,13 +27,6 @@ const (
notifierType = "neutrino" notifierType = "neutrino"
) )
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.")
)
// NeutrinoNotifier is a version of ChainNotifier that's backed by the neutrino // NeutrinoNotifier is a version of ChainNotifier that's backed by the neutrino
// Bitcoin light client. Unlike other implementations, this implementation // Bitcoin light client. Unlike other implementations, this implementation
// speaks directly to the p2p network. As a result, this implementation of the // speaks directly to the p2p network. As a result, this implementation of the
@@ -51,8 +44,8 @@ type NeutrinoNotifier struct {
started int32 // To be used atomically. started int32 // To be used atomically.
stopped int32 // To be used atomically. stopped int32 // To be used atomically.
heightMtx sync.RWMutex bestBlockMtx sync.RWMutex
bestHeight uint32 bestBlock chainntnfs.BlockEpoch
p2pNode *neutrino.ChainService p2pNode *neutrino.ChainService
chainView *neutrino.Rescan chainView *neutrino.Rescan
@@ -69,6 +62,7 @@ type NeutrinoNotifier struct {
rescanErr <-chan error rescanErr <-chan error
chainUpdates *queue.ConcurrentQueue chainUpdates *queue.ConcurrentQueue
txUpdates *queue.ConcurrentQueue
// spendHintCache is a cache used to query and update the latest height // spendHintCache is a cache used to query and update the latest height
// hints for an outpoint. Each height hint represents the earliest // hints for an outpoint. Each height hint represents the earliest
@@ -93,27 +87,27 @@ var _ chainntnfs.ChainNotifier = (*NeutrinoNotifier)(nil)
// NOTE: The passed neutrino node should already be running and active before // NOTE: The passed neutrino node should already be running and active before
// being passed into this function. // being passed into this function.
func New(node *neutrino.ChainService, spendHintCache chainntnfs.SpendHintCache, func New(node *neutrino.ChainService, spendHintCache chainntnfs.SpendHintCache,
confirmHintCache chainntnfs.ConfirmHintCache) (*NeutrinoNotifier, error) { confirmHintCache chainntnfs.ConfirmHintCache) *NeutrinoNotifier {
notifier := &NeutrinoNotifier{ return &NeutrinoNotifier{
notificationCancels: make(chan interface{}), notificationCancels: make(chan interface{}),
notificationRegistry: make(chan interface{}), notificationRegistry: make(chan interface{}),
blockEpochClients: make(map[uint64]*blockEpochRegistration), blockEpochClients: make(map[uint64]*blockEpochRegistration),
p2pNode: node, p2pNode: node,
chainConn: &NeutrinoChainConn{node},
rescanErr: make(chan error), rescanErr: make(chan error),
chainUpdates: queue.NewConcurrentQueue(10), chainUpdates: queue.NewConcurrentQueue(10),
txUpdates: queue.NewConcurrentQueue(10),
spendHintCache: spendHintCache, spendHintCache: spendHintCache,
confirmHintCache: confirmHintCache, confirmHintCache: confirmHintCache,
quit: make(chan struct{}), quit: make(chan struct{}),
} }
return notifier, nil
} }
// Start contacts the running neutrino light client and kicks off an initial // Start contacts the running neutrino light client and kicks off an initial
@@ -132,8 +126,13 @@ func (n *NeutrinoNotifier) Start() error {
if err != nil { if err != nil {
return err return err
} }
n.bestBlock.Hash = &startingPoint.Hash
n.bestBlock.Height = startingPoint.Height
n.bestHeight = uint32(startingPoint.Height) n.txNotifier = chainntnfs.NewTxNotifier(
uint32(n.bestBlock.Height), chainntnfs.ReorgSafetyLimit,
n.confirmHintCache, n.spendHintCache,
)
// Next, we'll create our set of rescan options. Currently it's // Next, we'll create our set of rescan options. Currently it's
// required that a user MUST set an addr/outpoint/txid when creating a // required that a user MUST set an addr/outpoint/txid when creating a
@@ -147,24 +146,19 @@ func (n *NeutrinoNotifier) Start() error {
rpcclient.NotificationHandlers{ rpcclient.NotificationHandlers{
OnFilteredBlockConnected: n.onFilteredBlockConnected, OnFilteredBlockConnected: n.onFilteredBlockConnected,
OnFilteredBlockDisconnected: n.onFilteredBlockDisconnected, OnFilteredBlockDisconnected: n.onFilteredBlockDisconnected,
OnRedeemingTx: n.onRelevantTx,
}, },
), ),
neutrino.WatchInputs(zeroInput), neutrino.WatchInputs(zeroInput),
} }
n.txNotifier = chainntnfs.NewTxNotifier(
n.bestHeight, chainntnfs.ReorgSafetyLimit, n.confirmHintCache,
n.spendHintCache,
)
n.chainConn = &NeutrinoChainConn{n.p2pNode}
// Finally, we'll create our rescan struct, start it, and launch all // Finally, we'll create our rescan struct, start it, and launch all
// the goroutines we need to operate this ChainNotifier instance. // the goroutines we need to operate this ChainNotifier instance.
n.chainView = n.p2pNode.NewRescan(rescanOptions...) n.chainView = n.p2pNode.NewRescan(rescanOptions...)
n.rescanErr = n.chainView.Start() n.rescanErr = n.chainView.Start()
n.chainUpdates.Start() n.chainUpdates.Start()
n.txUpdates.Start()
n.wg.Add(1) n.wg.Add(1)
go n.notificationDispatcher() go n.notificationDispatcher()
@@ -183,6 +177,7 @@ func (n *NeutrinoNotifier) Stop() error {
n.wg.Wait() n.wg.Wait()
n.chainUpdates.Stop() n.chainUpdates.Stop()
n.txUpdates.Stop()
// Notify all pending clients of our shutdown by closing the related // Notify all pending clients of our shutdown by closing the related
// notification channels. // notification channels.
@@ -226,11 +221,14 @@ func (n *NeutrinoNotifier) onFilteredBlockConnected(height int32,
// Append this new chain update to the end of the queue of new chain // Append this new chain update to the end of the queue of new chain
// updates. // updates.
n.chainUpdates.ChanIn() <- &filteredBlock{ select {
case n.chainUpdates.ChanIn() <- &filteredBlock{
hash: header.BlockHash(), hash: header.BlockHash(),
height: uint32(height), height: uint32(height),
txns: txns, txns: txns,
connect: true, connect: true,
}:
case <-n.quit:
} }
} }
@@ -241,10 +239,29 @@ func (n *NeutrinoNotifier) onFilteredBlockDisconnected(height int32,
// Append this new chain update to the end of the queue of new chain // Append this new chain update to the end of the queue of new chain
// disconnects. // disconnects.
n.chainUpdates.ChanIn() <- &filteredBlock{ select {
case n.chainUpdates.ChanIn() <- &filteredBlock{
hash: header.BlockHash(), hash: header.BlockHash(),
height: uint32(height), height: uint32(height),
connect: false, connect: false,
}:
case <-n.quit:
}
}
// relevantTx represents a relevant transaction to the notifier that fulfills
// any outstanding spend requests.
type relevantTx struct {
tx *btcutil.Tx
details *btcjson.BlockDetails
}
// onRelevantTx is a callback that proxies relevant transaction notifications
// from the backend to the notifier's main event handler.
func (n *NeutrinoNotifier) onRelevantTx(tx *btcutil.Tx, details *btcjson.BlockDetails) {
select {
case n.txUpdates.ChanIn() <- &relevantTx{tx, details}:
case <-n.quit:
} }
} }
@@ -293,7 +310,7 @@ out:
defer n.wg.Done() defer n.wg.Done()
confDetails, err := n.historicalConfDetails( confDetails, err := n.historicalConfDetails(
msg.TxID, msg.PkScript, msg.ConfRequest,
msg.StartHeight, msg.EndHeight, msg.StartHeight, msg.EndHeight,
) )
if err != nil { if err != nil {
@@ -308,7 +325,7 @@ out:
// cache at tip, since any pending // cache at tip, since any pending
// rescans have now completed. // rescans have now completed.
err = n.txNotifier.UpdateConfDetails( err = n.txNotifier.UpdateConfDetails(
*msg.TxID, confDetails, msg.ConfRequest, confDetails,
) )
if err != nil { if err != nil {
chainntnfs.Log.Error(err) chainntnfs.Log.Error(err)
@@ -317,25 +334,44 @@ out:
case *blockEpochRegistration: case *blockEpochRegistration:
chainntnfs.Log.Infof("New block epoch subscription") chainntnfs.Log.Infof("New block epoch subscription")
n.blockEpochClients[msg.epochID] = msg n.blockEpochClients[msg.epochID] = msg
if msg.bestBlock != nil {
n.heightMtx.Lock() // If the client did not provide their best
bestHeight := int32(n.bestHeight) // known block, then we'll immediately dispatch
n.heightMtx.Unlock() // a notification for the current tip.
missedBlocks, err := if msg.bestBlock == nil {
chainntnfs.GetClientMissedBlocks( n.notifyBlockEpochClient(
n.chainConn, msg.bestBlock, msg, n.bestBlock.Height,
bestHeight, false, n.bestBlock.Hash,
) )
if err != nil {
msg.errorChan <- err msg.errorChan <- nil
continue continue
}
for _, block := range missedBlocks {
n.notifyBlockEpochClient(msg,
block.Height, block.Hash)
}
} }
// Otherwise, we'll attempt to deliver the
// backlog of notifications from their best
// known block.
n.bestBlockMtx.Lock()
bestHeight := n.bestBlock.Height
n.bestBlockMtx.Unlock()
missedBlocks, err := chainntnfs.GetClientMissedBlocks(
n.chainConn, msg.bestBlock, bestHeight,
false,
)
if err != nil {
msg.errorChan <- err
continue
}
for _, block := range missedBlocks {
n.notifyBlockEpochClient(
msg, block.Height, block.Hash,
)
}
msg.errorChan <- nil msg.errorChan <- nil
case *rescanFilterUpdate: case *rescanFilterUpdate:
@@ -350,7 +386,7 @@ out:
case item := <-n.chainUpdates.ChanOut(): case item := <-n.chainUpdates.ChanOut():
update := item.(*filteredBlock) update := item.(*filteredBlock)
if update.connect { if update.connect {
n.heightMtx.Lock() n.bestBlockMtx.Lock()
// Since neutrino has no way of knowing what // Since neutrino has no way of knowing what
// height to rewind to in the case of a reorged // height to rewind to in the case of a reorged
// best known height, there is no point in // best known height, there is no point in
@@ -359,27 +395,24 @@ out:
// the other notifiers do when they receive // the other notifiers do when they receive
// a new connected block. Therefore, we just // a new connected block. Therefore, we just
// compare the heights. // compare the heights.
if update.height != n.bestHeight+1 { if update.height != uint32(n.bestBlock.Height+1) {
// Handle the case where the notifier // Handle the case where the notifier
// missed some blocks from its chain // missed some blocks from its chain
// backend // backend
chainntnfs.Log.Infof("Missed blocks, " + chainntnfs.Log.Infof("Missed blocks, " +
"attempting to catch up") "attempting to catch up")
bestBlock := chainntnfs.BlockEpoch{
Height: int32(n.bestHeight),
Hash: nil,
}
_, missedBlocks, err := _, missedBlocks, err :=
chainntnfs.HandleMissedBlocks( chainntnfs.HandleMissedBlocks(
n.chainConn, n.chainConn,
n.txNotifier, n.txNotifier,
bestBlock, n.bestBlock,
int32(update.height), int32(update.height),
false, false,
) )
if err != nil { if err != nil {
chainntnfs.Log.Error(err) chainntnfs.Log.Error(err)
n.heightMtx.Unlock() n.bestBlockMtx.Unlock()
continue continue
} }
@@ -388,13 +421,13 @@ out:
n.getFilteredBlock(block) n.getFilteredBlock(block)
if err != nil { if err != nil {
chainntnfs.Log.Error(err) chainntnfs.Log.Error(err)
n.heightMtx.Unlock() n.bestBlockMtx.Unlock()
continue out continue out
} }
err = n.handleBlockConnected(filteredBlock) err = n.handleBlockConnected(filteredBlock)
if err != nil { if err != nil {
chainntnfs.Log.Error(err) chainntnfs.Log.Error(err)
n.heightMtx.Unlock() n.bestBlockMtx.Unlock()
continue out continue out
} }
} }
@@ -405,42 +438,46 @@ out:
if err != nil { if err != nil {
chainntnfs.Log.Error(err) chainntnfs.Log.Error(err)
} }
n.heightMtx.Unlock()
n.bestBlockMtx.Unlock()
continue continue
} }
n.heightMtx.Lock() n.bestBlockMtx.Lock()
if update.height != uint32(n.bestHeight) { if update.height != uint32(n.bestBlock.Height) {
chainntnfs.Log.Infof("Missed disconnected " + chainntnfs.Log.Infof("Missed disconnected " +
"blocks, attempting to catch up") "blocks, attempting to catch up")
} }
hash, err := n.p2pNode.GetBlockHash(int64(n.bestHeight))
if err != nil {
chainntnfs.Log.Errorf("Unable to fetch block hash "+
"for height %d: %v", n.bestHeight, err)
n.heightMtx.Unlock()
continue
}
notifierBestBlock := chainntnfs.BlockEpoch{
Height: int32(n.bestHeight),
Hash: hash,
}
newBestBlock, err := chainntnfs.RewindChain( newBestBlock, err := chainntnfs.RewindChain(
n.chainConn, n.txNotifier, notifierBestBlock, n.chainConn, n.txNotifier, n.bestBlock,
int32(update.height-1), int32(update.height-1),
) )
if err != nil { if err != nil {
chainntnfs.Log.Errorf("Unable to rewind chain "+ chainntnfs.Log.Errorf("Unable to rewind chain "+
"from height %d to height %d: %v", "from height %d to height %d: %v",
n.bestHeight, update.height-1, err) n.bestBlock.Height, update.height-1, err)
} }
// Set the bestHeight here in case a chain rewind // Set the bestHeight here in case a chain rewind
// partially completed. // partially completed.
n.bestHeight = uint32(newBestBlock.Height) n.bestBlock = newBestBlock
n.heightMtx.Unlock() n.bestBlockMtx.Unlock()
case txUpdate := <-n.txUpdates.ChanOut():
// A new relevant transaction notification has been
// received from the backend. We'll attempt to process
// it to determine if it fulfills any outstanding
// confirmation and/or spend requests and dispatch
// notifications for them.
update := txUpdate.(*relevantTx)
err := n.txNotifier.ProcessRelevantSpendTx(
update.tx, uint32(update.details.Height),
)
if err != nil {
chainntnfs.Log.Errorf("Unable to process "+
"transaction %v: %v", update.tx.Hash(),
err)
}
case err := <-n.rescanErr: case err := <-n.rescanErr:
chainntnfs.Log.Errorf("Error during rescan: %v", err) chainntnfs.Log.Errorf("Error during rescan: %v", err)
@@ -452,21 +489,20 @@ out:
} }
} }
// historicalConfDetails looks up whether a transaction is already included in // historicalConfDetails looks up whether a confirmation request (txid/output
// a block in the active chain and, if so, returns details about the // script) has already been included in a block in the active chain and, if so,
// confirmation. // returns details about said block.
func (n *NeutrinoNotifier) historicalConfDetails(targetHash *chainhash.Hash, func (n *NeutrinoNotifier) historicalConfDetails(confRequest chainntnfs.ConfRequest,
pkScript []byte,
startHeight, endHeight uint32) (*chainntnfs.TxConfirmation, error) { startHeight, endHeight uint32) (*chainntnfs.TxConfirmation, error) {
// Starting from the height hint, we'll walk forwards in the chain to // Starting from the height hint, we'll walk forwards in the chain to
// see if this transaction has already been confirmed. // see if this transaction/output script has already been confirmed.
for scanHeight := endHeight; scanHeight >= startHeight && scanHeight > 0; scanHeight-- { for scanHeight := endHeight; scanHeight >= startHeight && scanHeight > 0; scanHeight-- {
// Ensure we haven't been requested to shut down before // Ensure we haven't been requested to shut down before
// processing the next height. // processing the next height.
select { select {
case <-n.quit: case <-n.quit:
return nil, ErrChainNotifierShuttingDown return nil, chainntnfs.ErrChainNotifierShuttingDown
default: default:
} }
@@ -498,7 +534,7 @@ func (n *NeutrinoNotifier) historicalConfDetails(targetHash *chainhash.Hash,
// In the case that the filter exists, we'll attempt to see if // In the case that the filter exists, we'll attempt to see if
// any element in it matches our target public key script. // any element in it matches our target public key script.
key := builder.DeriveKey(blockHash) key := builder.DeriveKey(blockHash)
match, err := regFilter.Match(key, pkScript) match, err := regFilter.Match(key, confRequest.PkScript.Script())
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to query filter: %v", err) return nil, fmt.Errorf("unable to query filter: %v", err)
} }
@@ -516,16 +552,21 @@ func (n *NeutrinoNotifier) historicalConfDetails(targetHash *chainhash.Hash,
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to get block from network: %v", err) return nil, fmt.Errorf("unable to get block from network: %v", err)
} }
for j, tx := range block.Transactions() {
txHash := tx.Hash() // For every transaction in the block, check which one matches
if txHash.IsEqual(targetHash) { // our request. If we find one that does, we can dispatch its
confDetails := chainntnfs.TxConfirmation{ // confirmation details.
BlockHash: blockHash, for i, tx := range block.Transactions() {
BlockHeight: scanHeight, if !confRequest.MatchesTx(tx.MsgTx()) {
TxIndex: uint32(j), continue
}
return &confDetails, nil
} }
return &chainntnfs.TxConfirmation{
Tx: tx.MsgTx(),
BlockHash: blockHash,
BlockHeight: scanHeight,
TxIndex: uint32(i),
}, nil
} }
} }
@@ -535,6 +576,8 @@ func (n *NeutrinoNotifier) historicalConfDetails(targetHash *chainhash.Hash,
// handleBlockConnected applies a chain update for a new block. Any watched // handleBlockConnected applies a chain update for a new block. Any watched
// transactions included this block will processed to either send notifications // transactions included this block will processed to either send notifications
// now or after numConfirmations confs. // now or after numConfirmations confs.
//
// NOTE: This method must be called with the bestBlockMtx lock held.
func (n *NeutrinoNotifier) handleBlockConnected(newBlock *filteredBlock) error { func (n *NeutrinoNotifier) handleBlockConnected(newBlock *filteredBlock) error {
// We'll extend the txNotifier's height with the information of this new // We'll extend the txNotifier's height with the information of this new
// block, which will handle all of the notification logic for us. // block, which will handle all of the notification logic for us.
@@ -553,7 +596,8 @@ func (n *NeutrinoNotifier) handleBlockConnected(newBlock *filteredBlock) error {
// registered clients whom have had notifications fulfilled. Before // registered clients whom have had notifications fulfilled. Before
// doing so, we'll make sure update our in memory state in order to // doing so, we'll make sure update our in memory state in order to
// satisfy any client requests based upon the new block. // satisfy any client requests based upon the new block.
n.bestHeight = newBlock.height n.bestBlock.Hash = &newBlock.hash
n.bestBlock.Height = int32(newBlock.height)
n.notifyBlockEpochs(int32(newBlock.height), &newBlock.hash) n.notifyBlockEpochs(int32(newBlock.height), &newBlock.hash)
return n.txNotifier.NotifyHeight(newBlock.height) return n.txNotifier.NotifyHeight(newBlock.height)
@@ -603,8 +647,12 @@ func (n *NeutrinoNotifier) notifyBlockEpochClient(epochClient *blockEpochRegistr
} }
// RegisterSpendNtfn registers an intent to be notified once the target // RegisterSpendNtfn registers an intent to be notified once the target
// outpoint has been spent by a transaction on-chain. Once a spend of the // outpoint/output script has been spent by a transaction on-chain. When
// target outpoint has been detected, the details of the spending event will be // intending to be notified of the spend of an output script, a nil outpoint
// must be used. The heightHint should represent the earliest height in the
// chain of the transaction that spent the outpoint/output script.
//
// Once a spend of has been detected, the details of the spending event will be
// sent across the 'Spend' channel. // sent across the 'Spend' channel.
func (n *NeutrinoNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, func (n *NeutrinoNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) { pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) {
@@ -612,26 +660,22 @@ func (n *NeutrinoNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
// First, we'll construct a spend notification request and hand it off // First, we'll construct a spend notification request and hand it off
// to the txNotifier. // to the txNotifier.
spendID := atomic.AddUint64(&n.spendClientCounter, 1) spendID := atomic.AddUint64(&n.spendClientCounter, 1)
cancel := func() { spendRequest, err := chainntnfs.NewSpendRequest(outpoint, pkScript)
n.txNotifier.CancelSpend(*outpoint, spendID)
}
ntfn := &chainntnfs.SpendNtfn{
SpendID: spendID,
OutPoint: *outpoint,
Event: chainntnfs.NewSpendEvent(cancel),
HeightHint: heightHint,
}
historicalDispatch, err := n.txNotifier.RegisterSpend(ntfn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ntfn := &chainntnfs.SpendNtfn{
SpendID: spendID,
SpendRequest: spendRequest,
Event: chainntnfs.NewSpendEvent(func() {
n.txNotifier.CancelSpend(spendRequest, spendID)
}),
HeightHint: heightHint,
}
// If the txNotifier didn't return any details to perform a historical historicalDispatch, txNotifierTip, err := n.txNotifier.RegisterSpend(ntfn)
// scan of the chain, then we can return early as there's nothing left if err != nil {
// for us to do. return nil, err
if historicalDispatch == nil {
return ntfn.Event, nil
} }
// To determine whether this outpoint has been spent on-chain, we'll // To determine whether this outpoint has been spent on-chain, we'll
@@ -640,45 +684,64 @@ func (n *NeutrinoNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
// past. // past.
// //
// We'll update our filter first to ensure we can immediately detect the // We'll update our filter first to ensure we can immediately detect the
// spend at tip. To do so, we'll map the script into an address // spend at tip.
// type so we can instruct neutrino to match if the transaction
// containing the script is found in a block.
inputToWatch := neutrino.InputWithScript{ inputToWatch := neutrino.InputWithScript{
OutPoint: *outpoint, OutPoint: spendRequest.OutPoint,
PkScript: pkScript, PkScript: spendRequest.PkScript.Script(),
} }
updateOptions := []neutrino.UpdateOption{
neutrino.AddInputs(inputToWatch),
neutrino.DisableDisconnectedNtfns(true),
}
// We'll use the txNotifier's tip as the starting point of our filter
// update. In the case of an output script spend request, we'll check if
// we should perform a historical rescan and start from there, as we
// cannot do so with GetUtxo since it matches outpoints.
rewindHeight := txNotifierTip
if historicalDispatch != nil &&
spendRequest.OutPoint == chainntnfs.ZeroOutPoint {
rewindHeight = historicalDispatch.StartHeight
}
updateOptions = append(updateOptions, neutrino.Rewind(rewindHeight))
errChan := make(chan error, 1) errChan := make(chan error, 1)
select { select {
case n.notificationRegistry <- &rescanFilterUpdate{ case n.notificationRegistry <- &rescanFilterUpdate{
updateOptions: []neutrino.UpdateOption{ updateOptions: updateOptions,
neutrino.AddInputs(inputToWatch), errChan: errChan,
neutrino.Rewind(historicalDispatch.EndHeight),
neutrino.DisableDisconnectedNtfns(true),
},
errChan: errChan,
}: }:
case <-n.quit: case <-n.quit:
return nil, ErrChainNotifierShuttingDown return nil, chainntnfs.ErrChainNotifierShuttingDown
} }
select { select {
case err = <-errChan: case err = <-errChan:
case <-n.quit: case <-n.quit:
return nil, ErrChainNotifierShuttingDown return nil, chainntnfs.ErrChainNotifierShuttingDown
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to update filter: %v", err) return nil, fmt.Errorf("unable to update filter: %v", err)
} }
// If the txNotifier didn't return any details to perform a historical
// scan of the chain, or if we already performed one like in the case of
// output script spend requests, then we can return early as there's
// nothing left for us to do.
if historicalDispatch == nil ||
spendRequest.OutPoint == chainntnfs.ZeroOutPoint {
return ntfn.Event, nil
}
// With the filter updated, we'll dispatch our historical rescan to // With the filter updated, we'll dispatch our historical rescan to
// ensure we detect the spend if it happened in the past. We'll ensure // ensure we detect the spend if it happened in the past. We'll ensure
// that neutrino is caught up to the starting height before we attempt // that neutrino is caught up to the starting height before we attempt
// to fetch the UTXO from the chain. If we're behind, then we may miss a // to fetch the UTXO from the chain. If we're behind, then we may miss a
// notification dispatch. // notification dispatch.
for { for {
n.heightMtx.RLock() n.bestBlockMtx.RLock()
currentHeight := n.bestHeight currentHeight := uint32(n.bestBlock.Height)
n.heightMtx.RUnlock() n.bestBlockMtx.RUnlock()
if currentHeight >= historicalDispatch.StartHeight { if currentHeight >= historicalDispatch.StartHeight {
break break
@@ -706,7 +769,7 @@ func (n *NeutrinoNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
if spendReport != nil && spendReport.SpendingTx != nil { if spendReport != nil && spendReport.SpendingTx != nil {
spendingTxHash := spendReport.SpendingTx.TxHash() spendingTxHash := spendReport.SpendingTx.TxHash()
spendDetails = &chainntnfs.SpendDetail{ spendDetails = &chainntnfs.SpendDetail{
SpentOutPoint: outpoint, SpentOutPoint: &spendRequest.OutPoint,
SpenderTxHash: &spendingTxHash, SpenderTxHash: &spendingTxHash,
SpendingTx: spendReport.SpendingTx, SpendingTx: spendReport.SpendingTx,
SpenderInputIndex: spendReport.SpendingInputIndex, SpenderInputIndex: spendReport.SpendingInputIndex,
@@ -718,7 +781,7 @@ func (n *NeutrinoNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
// not, we'll mark our historical rescan as complete to ensure the // not, we'll mark our historical rescan as complete to ensure the
// outpoint's spend hint gets updated upon connected/disconnected // outpoint's spend hint gets updated upon connected/disconnected
// blocks. // blocks.
err = n.txNotifier.UpdateSpendDetails(*outpoint, spendDetails) err = n.txNotifier.UpdateSpendDetails(spendRequest, spendDetails)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -726,40 +789,48 @@ func (n *NeutrinoNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
return ntfn.Event, nil return ntfn.Event, nil
} }
// RegisterConfirmationsNtfn registers a notification with NeutrinoNotifier // RegisterConfirmationsNtfn registers an intent to be notified once the target
// which will be triggered once the txid reaches numConfs number of // txid/output script has reached numConfs confirmations on-chain. When
// confirmations. // intending to be notified of the confirmation of an output script, a nil txid
// must be used. The heightHint should represent the earliest height at which
// the txid/output script could have been included in the chain.
//
// Progress on the number of confirmations left can be read from the 'Updates'
// channel. Once it has reached all of its confirmations, a notification will be
// sent across the 'Confirmed' channel.
func (n *NeutrinoNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, func (n *NeutrinoNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash,
pkScript []byte, pkScript []byte,
numConfs, heightHint uint32) (*chainntnfs.ConfirmationEvent, error) { numConfs, heightHint uint32) (*chainntnfs.ConfirmationEvent, error) {
// Construct a notification request for the transaction and send it to // Construct a notification request for the transaction and send it to
// the main event loop. // the main event loop.
confID := atomic.AddUint64(&n.confClientCounter, 1)
confRequest, err := chainntnfs.NewConfRequest(txid, pkScript)
if err != nil {
return nil, err
}
ntfn := &chainntnfs.ConfNtfn{ ntfn := &chainntnfs.ConfNtfn{
ConfID: atomic.AddUint64(&n.confClientCounter, 1), ConfID: confID,
TxID: txid, ConfRequest: confRequest,
PkScript: pkScript,
NumConfirmations: numConfs, NumConfirmations: numConfs,
Event: chainntnfs.NewConfirmationEvent(numConfs), Event: chainntnfs.NewConfirmationEvent(numConfs, func() {
HeightHint: heightHint, n.txNotifier.CancelConf(confRequest, confID)
}),
HeightHint: heightHint,
} }
chainntnfs.Log.Infof("New confirmation subscription: "+ chainntnfs.Log.Infof("New confirmation subscription: %v, num_confs=%v",
"txid=%v, numconfs=%v", txid, numConfs) confRequest, numConfs)
// Register the conf notification with the TxNotifier. A non-nil value // Register the conf notification with the TxNotifier. A non-nil value
// for `dispatch` will be returned if we are required to perform a // for `dispatch` will be returned if we are required to perform a
// manual scan for the confirmation. Otherwise the notifier will begin // manual scan for the confirmation. Otherwise the notifier will begin
// watching at tip for the transaction to confirm. // watching at tip for the transaction to confirm.
dispatch, err := n.txNotifier.RegisterConf(ntfn) dispatch, txNotifierTip, err := n.txNotifier.RegisterConf(ntfn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if dispatch == nil {
return ntfn.Event, nil
}
// To determine whether this transaction has confirmed on-chain, we'll // To determine whether this transaction has confirmed on-chain, we'll
// update our filter to watch for the transaction at tip and we'll also // update our filter to watch for the transaction at tip and we'll also
// dispatch a historical rescan to determine if it has confirmed in the // dispatch a historical rescan to determine if it has confirmed in the
@@ -770,7 +841,9 @@ func (n *NeutrinoNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash,
// type so we can instruct neutrino to match if the transaction // type so we can instruct neutrino to match if the transaction
// containing the script is found in a block. // containing the script is found in a block.
params := n.p2pNode.ChainParams() params := n.p2pNode.ChainParams()
_, addrs, _, err := txscript.ExtractPkScriptAddrs(pkScript, &params) _, addrs, _, err := txscript.ExtractPkScriptAddrs(
confRequest.PkScript.Script(), &params,
)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to extract script: %v", err) return nil, fmt.Errorf("unable to extract script: %v", err)
} }
@@ -782,30 +855,36 @@ func (n *NeutrinoNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash,
case n.notificationRegistry <- &rescanFilterUpdate{ case n.notificationRegistry <- &rescanFilterUpdate{
updateOptions: []neutrino.UpdateOption{ updateOptions: []neutrino.UpdateOption{
neutrino.AddAddrs(addrs...), neutrino.AddAddrs(addrs...),
neutrino.Rewind(dispatch.EndHeight), neutrino.Rewind(txNotifierTip),
neutrino.DisableDisconnectedNtfns(true), neutrino.DisableDisconnectedNtfns(true),
}, },
errChan: errChan, errChan: errChan,
}: }:
case <-n.quit: case <-n.quit:
return nil, ErrChainNotifierShuttingDown return nil, chainntnfs.ErrChainNotifierShuttingDown
} }
select { select {
case err = <-errChan: case err = <-errChan:
case <-n.quit: case <-n.quit:
return nil, ErrChainNotifierShuttingDown return nil, chainntnfs.ErrChainNotifierShuttingDown
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to update filter: %v", err) return nil, fmt.Errorf("unable to update filter: %v", err)
} }
// Finally, with the filter updates, we can dispatch the historical // If a historical rescan was not requested by the txNotifier, then we
// can return to the caller.
if dispatch == nil {
return ntfn.Event, nil
}
// Finally, with the filter updated, we can dispatch the historical
// rescan to ensure we can detect if the event happened in the past. // rescan to ensure we can detect if the event happened in the past.
select { select {
case n.notificationRegistry <- dispatch: case n.notificationRegistry <- dispatch:
case <-n.quit: case <-n.quit:
return nil, ErrChainNotifierShuttingDown return nil, chainntnfs.ErrChainNotifierShuttingDown
} }
return ntfn.Event, nil return ntfn.Event, nil
@@ -838,7 +917,9 @@ type epochCancel struct {
// RegisterBlockEpochNtfn returns a BlockEpochEvent which subscribes the // RegisterBlockEpochNtfn returns a BlockEpochEvent which subscribes the
// caller to receive notifications, of each new block connected to the main // caller to receive notifications, of each new block connected to the main
// chain. Clients have the option of passing in their best known block, which // chain. Clients have the option of passing in their best known block, which
// the notifier uses to check if they are behind on blocks and catch them up. // the notifier uses to check if they are behind on blocks and catch them up. If
// they do not provide one, then a notification will be dispatched immediately
// for the current tip of the chain upon a successful registration.
func (n *NeutrinoNotifier) RegisterBlockEpochNtfn( func (n *NeutrinoNotifier) RegisterBlockEpochNtfn(
bestBlock *chainntnfs.BlockEpoch) (*chainntnfs.BlockEpochEvent, error) { bestBlock *chainntnfs.BlockEpoch) (*chainntnfs.BlockEpochEvent, error) {
+3 -3
View File
@@ -53,14 +53,13 @@ func (n *NeutrinoNotifier) UnsafeStart(bestHeight int32,
n.confirmHintCache, n.spendHintCache, n.confirmHintCache, n.spendHintCache,
) )
n.chainConn = &NeutrinoChainConn{n.p2pNode}
// Finally, we'll create our rescan struct, start it, and launch all // Finally, we'll create our rescan struct, start it, and launch all
// the goroutines we need to operate this ChainNotifier instance. // the goroutines we need to operate this ChainNotifier instance.
n.chainView = n.p2pNode.NewRescan(rescanOptions...) n.chainView = n.p2pNode.NewRescan(rescanOptions...)
n.rescanErr = n.chainView.Start() n.rescanErr = n.chainView.Start()
n.chainUpdates.Start() n.chainUpdates.Start()
n.txUpdates.Start()
if generateBlocks != nil { if generateBlocks != nil {
// Ensure no block notifications are pending when we start the // Ensure no block notifications are pending when we start the
@@ -90,7 +89,8 @@ func (n *NeutrinoNotifier) UnsafeStart(bestHeight int32,
// Run notificationDispatcher after setting the notifier's best height // Run notificationDispatcher after setting the notifier's best height
// to avoid a race condition. // to avoid a race condition.
n.bestHeight = uint32(bestHeight) n.bestBlock.Hash = bestHash
n.bestBlock.Height = bestHeight
n.wg.Add(1) n.wg.Add(1)
go n.notificationDispatcher() go n.notificationDispatcher()
+41 -27
View File
@@ -35,35 +35,44 @@ var (
var ( var (
NetParams = &chaincfg.RegressionNetParams NetParams = &chaincfg.RegressionNetParams
testPrivKey = []byte{
0x81, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda,
0x63, 0x59, 0xe6, 0x96, 0x31, 0x13, 0xa1, 0x17,
0xd, 0xe7, 0x95, 0xe4, 0xb7, 0x25, 0xb8, 0x4d,
0x1e, 0xb, 0x4c, 0xfd, 0x9e, 0xc5, 0x8c, 0xe9,
}
privKey, pubKey = btcec.PrivKeyFromBytes(btcec.S256(), testPrivKey)
addrPk, _ = btcutil.NewAddressPubKey(
pubKey.SerializeCompressed(), NetParams,
)
testAddr = addrPk.AddressPubKeyHash()
) )
// GetTestTxidAndScript generate a new test transaction and returns its txid and // randPubKeyHashScript generates a P2PKH script that pays to the public key of
// the script of the output being generated. // a randomly-generated private key.
func GetTestTxidAndScript(h *rpctest.Harness) (*chainhash.Hash, []byte, error) { func randPubKeyHashScript() ([]byte, *btcec.PrivateKey, error) {
script, err := txscript.PayToAddrScript(testAddr) privKey, err := btcec.NewPrivateKey(btcec.S256())
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
output := &wire.TxOut{Value: 2e8, PkScript: script} pubKeyHash := btcutil.Hash160(privKey.PubKey().SerializeCompressed())
addrScript, err := btcutil.NewAddressPubKeyHash(pubKeyHash, NetParams)
if err != nil {
return nil, nil, err
}
pkScript, err := txscript.PayToAddrScript(addrScript)
if err != nil {
return nil, nil, err
}
return pkScript, privKey, nil
}
// GetTestTxidAndScript generate a new test transaction and returns its txid and
// the script of the output being generated.
func GetTestTxidAndScript(h *rpctest.Harness) (*chainhash.Hash, []byte, error) {
pkScript, _, err := randPubKeyHashScript()
if err != nil {
return nil, nil, fmt.Errorf("unable to generate pkScript: %v", err)
}
output := &wire.TxOut{Value: 2e8, PkScript: pkScript}
txid, err := h.SendOutputs([]*wire.TxOut{output}, 10) txid, err := h.SendOutputs([]*wire.TxOut{output}, 10)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
return txid, script, nil return txid, pkScript, nil
} }
// WaitForMempoolTx waits for the txid to be seen in the miner's mempool. // WaitForMempoolTx waits for the txid to be seen in the miner's mempool.
@@ -107,16 +116,18 @@ func WaitForMempoolTx(miner *rpctest.Harness, txid *chainhash.Hash) error {
// CreateSpendableOutput creates and returns an output that can be spent later // CreateSpendableOutput creates and returns an output that can be spent later
// on. // on.
func CreateSpendableOutput(t *testing.T, miner *rpctest.Harness) (*wire.OutPoint, []byte) { func CreateSpendableOutput(t *testing.T,
miner *rpctest.Harness) (*wire.OutPoint, *wire.TxOut, *btcec.PrivateKey) {
t.Helper() t.Helper()
// Create a transaction that only has one output, the one destined for // Create a transaction that only has one output, the one destined for
// the recipient. // the recipient.
script, err := txscript.PayToAddrScript(testAddr) pkScript, privKey, err := randPubKeyHashScript()
if err != nil { if err != nil {
t.Fatalf("unable to create p2pkh script: %v", err) t.Fatalf("unable to generate pkScript: %v", err)
} }
output := &wire.TxOut{Value: 2e8, PkScript: script} output := &wire.TxOut{Value: 2e8, PkScript: pkScript}
txid, err := miner.SendOutputsWithoutChange([]*wire.TxOut{output}, 10) txid, err := miner.SendOutputsWithoutChange([]*wire.TxOut{output}, 10)
if err != nil { if err != nil {
t.Fatalf("unable to create tx: %v", err) t.Fatalf("unable to create tx: %v", err)
@@ -130,19 +141,22 @@ func CreateSpendableOutput(t *testing.T, miner *rpctest.Harness) (*wire.OutPoint
t.Fatalf("unable to generate single block: %v", err) t.Fatalf("unable to generate single block: %v", err)
} }
return wire.NewOutPoint(txid, 0), script return wire.NewOutPoint(txid, 0), output, privKey
} }
// CreateSpendTx creates a transaction spending the specified output. // CreateSpendTx creates a transaction spending the specified output.
func CreateSpendTx(t *testing.T, outpoint *wire.OutPoint, pkScript []byte) *wire.MsgTx { func CreateSpendTx(t *testing.T, prevOutPoint *wire.OutPoint,
prevOutput *wire.TxOut, privKey *btcec.PrivateKey) *wire.MsgTx {
t.Helper() t.Helper()
spendingTx := wire.NewMsgTx(1) spendingTx := wire.NewMsgTx(1)
spendingTx.AddTxIn(&wire.TxIn{PreviousOutPoint: *outpoint}) spendingTx.AddTxIn(&wire.TxIn{PreviousOutPoint: *prevOutPoint})
spendingTx.AddTxOut(&wire.TxOut{Value: 1e8, PkScript: pkScript}) spendingTx.AddTxOut(&wire.TxOut{Value: 1e8, PkScript: prevOutput.PkScript})
sigScript, err := txscript.SignatureScript( sigScript, err := txscript.SignatureScript(
spendingTx, 0, pkScript, txscript.SigHashAll, privKey, true, spendingTx, 0, prevOutput.PkScript, txscript.SigHashAll,
privKey, true,
) )
if err != nil { if err != nil {
t.Fatalf("unable to sign tx: %v", err) t.Fatalf("unable to sign tx: %v", err)
+813 -465
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+3 -8
View File
@@ -256,12 +256,7 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
// Next we'll create the instances of the ChainNotifier and // Next we'll create the instances of the ChainNotifier and
// FilteredChainView interface which is backed by the neutrino // FilteredChainView interface which is backed by the neutrino
// light client. // light client.
cc.chainNotifier, err = neutrinonotify.New( cc.chainNotifier = neutrinonotify.New(svc, hintCache, hintCache)
svc, hintCache, hintCache,
)
if err != nil {
return nil, nil, err
}
cc.chainView, err = chainview.NewCfFilteredChainView(svc) cc.chainView, err = chainview.NewCfFilteredChainView(svc)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -336,7 +331,7 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
} }
cc.chainNotifier = bitcoindnotify.New( cc.chainNotifier = bitcoindnotify.New(
bitcoindConn, hintCache, hintCache, bitcoindConn, activeNetParams.Params, hintCache, hintCache,
) )
cc.chainView = chainview.NewBitcoindFilteredChainView(bitcoindConn) cc.chainView = chainview.NewBitcoindFilteredChainView(bitcoindConn)
walletConfig.ChainSource = bitcoindConn.NewBitcoindClient() walletConfig.ChainSource = bitcoindConn.NewBitcoindClient()
@@ -446,7 +441,7 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
DisableAutoReconnect: false, DisableAutoReconnect: false,
} }
cc.chainNotifier, err = btcdnotify.New( cc.chainNotifier, err = btcdnotify.New(
rpcConfig, hintCache, hintCache, rpcConfig, activeNetParams.Params, hintCache, hintCache,
) )
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
+4 -8
View File
@@ -7,19 +7,17 @@ require (
github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2 github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/boltdb/bolt v1.3.1 // indirect github.com/boltdb/bolt v1.3.1 // indirect
github.com/btcsuite/btcd v0.0.0-20180824064422-7d2daa5bfef28c5e282571bc06416516936115ee github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60ae4834a1f57511e20c17b5f78be4b github.com/btcsuite/btcutil v0.0.0-20190112041146-bf1e1be93589
github.com/btcsuite/btcwallet v0.0.0-20181130221647-e59e51f8e13c github.com/btcsuite/btcwallet v0.0.0-20190115024521-9ad115360b37
github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941 github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941
github.com/btcsuite/goleveldb v1.0.0 // indirect github.com/btcsuite/goleveldb v1.0.0 // indirect
github.com/coreos/bbolt v0.0.0-20180223184059-7ee3ded59d4835e10f3e7d0f7603c42aa5e83820 github.com/coreos/bbolt v0.0.0-20180223184059-7ee3ded59d4835e10f3e7d0f7603c42aa5e83820
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/fsnotify/fsnotify v1.4.7 // indirect
github.com/go-errors/errors v1.0.1 github.com/go-errors/errors v1.0.1
github.com/golang/protobuf v1.2.0 github.com/golang/protobuf v1.2.0
github.com/grpc-ecosystem/grpc-gateway v0.0.0-20170724004829-f2862b476edc github.com/grpc-ecosystem/grpc-gateway v0.0.0-20170724004829-f2862b476edc
github.com/hpcloud/tail v1.0.0 // indirect
github.com/jackpal/gateway v1.0.4 github.com/jackpal/gateway v1.0.4
github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad
github.com/jessevdk/go-flags v0.0.0-20170926144705-f88afde2fa19 github.com/jessevdk/go-flags v0.0.0-20170926144705-f88afde2fa19
@@ -32,7 +30,7 @@ require (
github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d // indirect github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d // indirect
github.com/juju/version v0.0.0-20180108022336-b64dbd566305 // indirect github.com/juju/version v0.0.0-20180108022336-b64dbd566305 // indirect
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec
github.com/lightninglabs/neutrino v0.0.0-20181130220745-8d09312ac266 github.com/lightninglabs/neutrino v0.0.0-20190115022559-351f5f06c6af
github.com/lightningnetwork/lightning-onion v0.0.0-20180605012408-ac4d9da8f1d6 github.com/lightningnetwork/lightning-onion v0.0.0-20180605012408-ac4d9da8f1d6
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796
github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8
@@ -47,9 +45,7 @@ require (
google.golang.org/genproto v0.0.0-20181127195345-31ac5d88444a google.golang.org/genproto v0.0.0-20181127195345-31ac5d88444a
google.golang.org/grpc v1.16.0 google.golang.org/grpc v1.16.0
gopkg.in/errgo.v1 v1.0.0 // indirect gopkg.in/errgo.v1 v1.0.0 // indirect
gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/macaroon-bakery.v2 v2.0.1 gopkg.in/macaroon-bakery.v2 v2.0.1
gopkg.in/macaroon.v2 v2.0.0 gopkg.in/macaroon.v2 v2.0.0
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
) )
+16 -9
View File
@@ -14,16 +14,17 @@ github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBA
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/btcsuite/btcd v0.0.0-20180823030728-d81d8877b8f3/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ= github.com/btcsuite/btcd v0.0.0-20180823030728-d81d8877b8f3/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ=
github.com/btcsuite/btcd v0.0.0-20180824064422-7d2daa5bfef28c5e282571bc06416516936115ee h1:YSiTy2Hn6a5TaPnmcyk0+OTc13E1rbmCCgo/J0LDxtA= github.com/btcsuite/btcd v0.0.0-20180824064422-ed77733ec07dfc8a513741138419b8d9d3de9d2d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0=
github.com/btcsuite/btcd v0.0.0-20180824064422-7d2daa5bfef28c5e282571bc06416516936115ee/go.mod h1:Jr9bmNVGZ7TH2Ux1QuP0ec+yGgh0gE9FIlkzQiI5bR0= github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d h1:xG8Pj6Y6J760xwETNmMzmlt38QSwz0BLp1cZ09g27uw=
github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60ae4834a1f57511e20c17b5f78be4b h1:sa+k743hEDlacUmuzqYlpxx4gp61C9Cf531bPaOneWo= github.com/btcsuite/btcutil v0.0.0-20190112041146-bf1e1be93589 h1:9A5pe5iQS+ll6R1EVLFv/y92IjrymihwITCU81aCIBQ=
github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60ae4834a1f57511e20c17b5f78be4b/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcutil v0.0.0-20190112041146-bf1e1be93589/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcwallet v0.0.0-20180904010540-284e2e0e696e33d5be388f7f3d9a26db703e0c06/go.mod h1:/d7QHZsfUAruXuBhyPITqoYOmJ+nq35qPsJjz/aSpCg= github.com/btcsuite/btcwallet v0.0.0-20180904010540-284e2e0e696e33d5be388f7f3d9a26db703e0c06/go.mod h1:/d7QHZsfUAruXuBhyPITqoYOmJ+nq35qPsJjz/aSpCg=
github.com/btcsuite/btcwallet v0.0.0-20181130221647-e59e51f8e13c h1:k39UlSgW6cR0cIEleF9Or3S4uSP7NgvRLPd/WNYScbU= github.com/btcsuite/btcwallet v0.0.0-20190115024521-9ad115360b37 h1:f8RY/x01F18bYrOvekIUDLqm9oGZFrim+wx5Z0ts/Kg=
github.com/btcsuite/btcwallet v0.0.0-20181130221647-e59e51f8e13c/go.mod h1:mHSlZHtkxbCzvqKCzkQOSDb7Lc5iqoD8+pj6qc0yDBU= github.com/btcsuite/btcwallet v0.0.0-20190115024521-9ad115360b37/go.mod h1:+u1ftn+QOb9qHKwsLf7rBOr0PHCo9CGA7U1WFq7VLA4=
github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941 h1:kij1x2aL7VE6gtx8KMIt8PGPgI5GV9LgtHFG5KaEMPY= github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941 h1:kij1x2aL7VE6gtx8KMIt8PGPgI5GV9LgtHFG5KaEMPY=
github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941/go.mod h1:QcFA8DZHtuIAdYKCq/BzELOaznRsCvwf4zTPmaYwaig= github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941/go.mod h1:QcFA8DZHtuIAdYKCq/BzELOaznRsCvwf4zTPmaYwaig=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
@@ -95,9 +96,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lightninglabs/gozmq v0.0.0-20180324010646-462a8a753885 h1:fTLuPUkaKIIV0+gA1IxiBDvDxtF8tzpSF6N6NfFGmsU= github.com/lightninglabs/gozmq v0.0.0-20180324010646-462a8a753885 h1:fTLuPUkaKIIV0+gA1IxiBDvDxtF8tzpSF6N6NfFGmsU=
github.com/lightninglabs/gozmq v0.0.0-20180324010646-462a8a753885/go.mod h1:KUh15naRlx/TmUMFS/p4JJrCrE6F7RGF7rsnvuu45E4= github.com/lightninglabs/gozmq v0.0.0-20180324010646-462a8a753885/go.mod h1:KUh15naRlx/TmUMFS/p4JJrCrE6F7RGF7rsnvuu45E4=
github.com/lightninglabs/neutrino v0.0.0-20181017011010-4d6069299130/go.mod h1:KJq43Fu9ceitbJsSXMILcT4mGDNI/crKmPIkDOZXFyM= github.com/lightninglabs/neutrino v0.0.0-20181017011010-4d6069299130/go.mod h1:KJq43Fu9ceitbJsSXMILcT4mGDNI/crKmPIkDOZXFyM=
github.com/lightninglabs/neutrino v0.0.0-20181017011010-8d09312ac266916a00d367abeaf05fbd8bccf5d8/go.mod h1:/ie0+o5CLo6NDM52EpoMAJsMPq25DqpK1APv6MzZj7U= github.com/lightninglabs/neutrino v0.0.0-20190115022559-351f5f06c6af h1:JzoYbWqwPb+PARU4LTtlohetdNa6/ocyQ0xidZQw4Hg=
github.com/lightninglabs/neutrino v0.0.0-20181130220745-8d09312ac266 h1:2WHiUpriTMc4cLWvS4syzNHLS9+hPVLJjNeZuhq5HR4= github.com/lightninglabs/neutrino v0.0.0-20190115022559-351f5f06c6af/go.mod h1:aR+E6cs+FTaIwIa/WLyvNsB8FZg8TiP3r0Led+4Q4gI=
github.com/lightninglabs/neutrino v0.0.0-20181130220745-8d09312ac266/go.mod h1:/ie0+o5CLo6NDM52EpoMAJsMPq25DqpK1APv6MzZj7U=
github.com/lightningnetwork/lightning-onion v0.0.0-20180605012408-ac4d9da8f1d6 h1:ONLGrYJVQdbtP6CE/ff1KNWZtygRGEh12RzonTiCzPs= github.com/lightningnetwork/lightning-onion v0.0.0-20180605012408-ac4d9da8f1d6 h1:ONLGrYJVQdbtP6CE/ff1KNWZtygRGEh12RzonTiCzPs=
github.com/lightningnetwork/lightning-onion v0.0.0-20180605012408-ac4d9da8f1d6/go.mod h1:8EgEt4a/NUOVQd+3kk6n9aZCJ1Ssj96Pb6lCrci+6oc= github.com/lightningnetwork/lightning-onion v0.0.0-20180605012408-ac4d9da8f1d6/go.mod h1:8EgEt4a/NUOVQd+3kk6n9aZCJ1Ssj96Pb6lCrci+6oc=
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw= github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw=
@@ -108,8 +108,12 @@ github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 h1:PRMAcldsl4mXKJeRNB/KV
github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U= github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af h1:gu+uRPtBe88sKxUCEXRoeCvVG90TJmwhiqRpvdhQFng= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af h1:gu+uRPtBe88sKxUCEXRoeCvVG90TJmwhiqRpvdhQFng=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E= github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E=
@@ -126,6 +130,7 @@ golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20180821023952-922f4815f713/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180821023952-922f4815f713/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953 h1:LuZIitY8waaxUfNIdtajyE/YzA/zyf0YxXG27VpLrkg= golang.org/x/net v0.0.0-20181106065722-10aee1819953 h1:LuZIitY8waaxUfNIdtajyE/YzA/zyf0YxXG27VpLrkg=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -135,6 +140,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTm
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180821140842-3b58ed4ad339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180821140842-3b58ed4ad339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU= golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+459
View File
@@ -0,0 +1,459 @@
// +build chainrpc
package chainrpc
import (
"bytes"
"context"
"errors"
"io/ioutil"
"os"
"path/filepath"
"sync/atomic"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/lnrpc"
"google.golang.org/grpc"
"gopkg.in/macaroon-bakery.v2/bakery"
)
const (
// subServerName is the name of the RPC sub-server. We'll use this name
// to register ourselves, and we also require that the main
// SubServerConfigDispatcher instance recognize this as the name of the
// config file that we need.
subServerName = "ChainRPC"
)
var (
// macaroonOps are the set of capabilities that our minted macaroon (if
// it doesn't already exist) will have.
macaroonOps = []bakery.Op{
{
Entity: "onchain",
Action: "read",
},
}
// macPermissions maps RPC calls to the permissions they require.
macPermissions = map[string][]bakery.Op{
"/chainrpc.ChainNotifier/RegisterConfirmationsNtfn": {{
Entity: "onchain",
Action: "read",
}},
"/chainrpc.ChainNotifier/RegisterSpendNtfn": {{
Entity: "onchain",
Action: "read",
}},
"/chainrpc.ChainNotifier/RegisterBlockEpochNtfn": {{
Entity: "onchain",
Action: "read",
}},
}
// DefaultChainNotifierMacFilename is the default name of the chain
// notifier macaroon that we expect to find via a file handle within the
// main configuration file in this package.
DefaultChainNotifierMacFilename = "chainnotifier.macaroon"
// ErrChainNotifierServerShuttingDown is an error returned when we are
// waiting for a notification to arrive but the chain notifier server
// has been shut down.
ErrChainNotifierServerShuttingDown = errors.New("chain notifier RPC " +
"subserver shutting down")
)
// fileExists reports whether the named file or directory exists.
func fileExists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
// Server is a sub-server of the main RPC server: the chain notifier RPC. This
// RPC sub-server allows external callers to access the full chain notifier
// capabilities of lnd. This allows callers to create custom protocols, external
// to lnd, even backed by multiple distinct lnd across independent failure
// domains.
type Server struct {
started uint32
stopped uint32
cfg Config
quit chan struct{}
}
// New returns a new instance of the chainrpc ChainNotifier sub-server. We also
// return the set of permissions for the macaroons that we may create within
// this method. If the macaroons we need aren't found in the filepath, then
// we'll create them on start up. If we're unable to locate, or create the
// macaroons we need, then we'll return with an error.
func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) {
// If the path of the chain notifier macaroon wasn't generated, then
// we'll assume that it's found at the default network directory.
if cfg.ChainNotifierMacPath == "" {
cfg.ChainNotifierMacPath = filepath.Join(
cfg.NetworkDir, DefaultChainNotifierMacFilename,
)
}
// Now that we know the full path of the chain notifier macaroon, we can
// check to see if we need to create it or not.
macFilePath := cfg.ChainNotifierMacPath
if cfg.MacService != nil && !fileExists(macFilePath) {
log.Infof("Baking macaroons for ChainNotifier RPC Server at: %v",
macFilePath)
// At this point, we know that the chain notifier macaroon
// doesn't yet, exist, so we need to create it with the help of
// the main macaroon service.
chainNotifierMac, err := cfg.MacService.Oven.NewMacaroon(
context.Background(), bakery.LatestVersion, nil,
macaroonOps...,
)
if err != nil {
return nil, nil, err
}
chainNotifierMacBytes, err := chainNotifierMac.M().MarshalBinary()
if err != nil {
return nil, nil, err
}
err = ioutil.WriteFile(macFilePath, chainNotifierMacBytes, 0644)
if err != nil {
os.Remove(macFilePath)
return nil, nil, err
}
}
return &Server{
cfg: *cfg,
quit: make(chan struct{}),
}, macPermissions, nil
}
// Compile-time checks to ensure that Server fully implements the
// ChainNotifierServer gRPC service and lnrpc.SubServer interface.
var _ ChainNotifierServer = (*Server)(nil)
var _ lnrpc.SubServer = (*Server)(nil)
// Start launches any helper goroutines required for the server to function.
//
// NOTE: This is part of the lnrpc.SubServer interface.
func (s *Server) Start() error {
if !atomic.CompareAndSwapUint32(&s.started, 0, 1) {
return nil
}
return nil
}
// Stop signals any active goroutines for a graceful closure.
//
// NOTE: This is part of the lnrpc.SubServer interface.
func (s *Server) Stop() error {
if !atomic.CompareAndSwapUint32(&s.stopped, 0, 1) {
return nil
}
close(s.quit)
return nil
}
// Name returns a unique string representation of the sub-server. This can be
// used to identify the sub-server and also de-duplicate them.
//
// NOTE: This is part of the lnrpc.SubServer interface.
func (s *Server) Name() string {
return subServerName
}
// RegisterWithRootServer will be called by the root gRPC server to direct a RPC
// sub-server to register itself with the main gRPC root server. Until this is
// called, each sub-server won't be able to have requests routed towards it.
//
// NOTE: This is part of the lnrpc.SubServer interface.
func (s *Server) RegisterWithRootServer(grpcServer *grpc.Server) error {
// We make sure that we register it with the main gRPC server to ensure
// all our methods are routed properly.
RegisterChainNotifierServer(grpcServer, s)
log.Debug("ChainNotifier RPC server successfully register with root " +
"gRPC server")
return nil
}
// RegisterConfirmationsNtfn is a synchronous response-streaming RPC that
// registers an intent for a client to be notified once a confirmation request
// has reached its required number of confirmations on-chain.
//
// A client can specify whether the confirmation request should be for a
// particular transaction by its hash or for an output script by specifying a
// zero hash.
//
// NOTE: This is part of the chainrpc.ChainNotifierService interface.
func (s *Server) RegisterConfirmationsNtfn(in *ConfRequest,
confStream ChainNotifier_RegisterConfirmationsNtfnServer) error {
// We'll start by reconstructing the RPC request into what the
// underlying ChainNotifier expects.
var txid chainhash.Hash
copy(txid[:], in.Txid)
// We'll then register for the spend notification of the request.
confEvent, err := s.cfg.ChainNotifier.RegisterConfirmationsNtfn(
&txid, in.Script, in.NumConfs, in.HeightHint,
)
if err != nil {
return err
}
defer confEvent.Cancel()
// With the request registered, we'll wait for its spend notification to
// be dispatched.
for {
select {
// The transaction satisfying the request has confirmed on-chain
// and reached its required number of confirmations. We'll
// dispatch an event to the caller indicating so.
case details, ok := <-confEvent.Confirmed:
if !ok {
return chainntnfs.ErrChainNotifierShuttingDown
}
var rawTxBuf bytes.Buffer
err := details.Tx.Serialize(&rawTxBuf)
if err != nil {
return err
}
rpcConfDetails := &ConfDetails{
RawTx: rawTxBuf.Bytes(),
BlockHash: details.BlockHash[:],
BlockHeight: details.BlockHeight,
TxIndex: details.TxIndex,
}
conf := &ConfEvent{
Event: &ConfEvent_Conf{
Conf: rpcConfDetails,
},
}
if err := confStream.Send(conf); err != nil {
return err
}
// The transaction satisfying the request has been reorged out
// of the chain, so we'll send an event describing so.
case _, ok := <-confEvent.NegativeConf:
if !ok {
return chainntnfs.ErrChainNotifierShuttingDown
}
reorg := &ConfEvent{
Event: &ConfEvent_Reorg{Reorg: &Reorg{}},
}
if err := confStream.Send(reorg); err != nil {
return err
}
// The transaction satisfying the request has confirmed and is
// no longer under the risk of being reorged out of the chain,
// so we can safely exit.
case _, ok := <-confEvent.Done:
if !ok {
return chainntnfs.ErrChainNotifierShuttingDown
}
return nil
// The response stream's context for whatever reason has been
// closed. We'll return the error indicated by the context
// itself to the caller.
case <-confStream.Context().Done():
return confStream.Context().Err()
// The server has been requested to shut down.
case <-s.quit:
return ErrChainNotifierServerShuttingDown
}
}
}
// RegisterSpendNtfn is a synchronous response-streaming RPC that registers an
// intent for a client to be notification once a spend request has been spent by
// a transaction that has confirmed on-chain.
//
// A client can specify whether the spend request should be for a particular
// outpoint or for an output script by specifying a zero outpoint.
//
// NOTE: This is part of the chainrpc.ChainNotifierService interface.
func (s *Server) RegisterSpendNtfn(in *SpendRequest,
spendStream ChainNotifier_RegisterSpendNtfnServer) error {
// We'll start by reconstructing the RPC request into what the
// underlying ChainNotifier expects.
var op *wire.OutPoint
if in.Outpoint != nil {
var txid chainhash.Hash
copy(txid[:], in.Outpoint.Hash)
op = &wire.OutPoint{Hash: txid, Index: in.Outpoint.Index}
}
// We'll then register for the spend notification of the request.
spendEvent, err := s.cfg.ChainNotifier.RegisterSpendNtfn(
op, in.Script, in.HeightHint,
)
if err != nil {
return err
}
defer spendEvent.Cancel()
// With the request registered, we'll wait for its spend notification to
// be dispatched.
for {
select {
// A transaction that spends the given has confirmed on-chain.
// We'll return an event to the caller indicating so that
// includes the details of the spending transaction.
case details, ok := <-spendEvent.Spend:
if !ok {
return chainntnfs.ErrChainNotifierShuttingDown
}
var rawSpendingTxBuf bytes.Buffer
err := details.SpendingTx.Serialize(&rawSpendingTxBuf)
if err != nil {
return err
}
rpcSpendDetails := &SpendDetails{
SpendingOutpoint: &Outpoint{
Hash: details.SpentOutPoint.Hash[:],
Index: details.SpentOutPoint.Index,
},
RawSpendingTx: rawSpendingTxBuf.Bytes(),
SpendingTxHash: details.SpenderTxHash[:],
SpendingInputIndex: details.SpenderInputIndex,
SpendingHeight: uint32(details.SpendingHeight),
}
spend := &SpendEvent{
Event: &SpendEvent_Spend{
Spend: rpcSpendDetails,
},
}
if err := spendStream.Send(spend); err != nil {
return err
}
// The spending transaction of the request has been reorged of
// the chain. We'll return an event to the caller indicating so.
case _, ok := <-spendEvent.Reorg:
if !ok {
return chainntnfs.ErrChainNotifierShuttingDown
}
reorg := &SpendEvent{
Event: &SpendEvent_Reorg{Reorg: &Reorg{}},
}
if err := spendStream.Send(reorg); err != nil {
return err
}
// The spending transaction of the requests has confirmed
// on-chain and is no longer under the risk of being reorged out
// of the chain, so we can safely exit.
case _, ok := <-spendEvent.Done:
if !ok {
return chainntnfs.ErrChainNotifierShuttingDown
}
return nil
// The response stream's context for whatever reason has been
// closed. We'll return the error indicated by the context
// itself to the caller.
case <-spendStream.Context().Done():
return spendStream.Context().Err()
// The server has been requested to shut down.
case <-s.quit:
return ErrChainNotifierServerShuttingDown
}
}
}
// RegisterBlockEpochNtfn is a synchronous response-streaming RPC that registers
// an intent for a client to be notified of blocks in the chain. The stream will
// return a hash and height tuple of a block for each new/stale block in the
// chain. It is the client's responsibility to determine whether the tuple
// returned is for a new or stale block in the chain.
//
// A client can also request a historical backlog of blocks from a particular
// point. This allows clients to be idempotent by ensuring that they do not
// missing processing a single block within the chain.
//
// NOTE: This is part of the chainrpc.ChainNotifierService interface.
func (s *Server) RegisterBlockEpochNtfn(in *BlockEpoch,
epochStream ChainNotifier_RegisterBlockEpochNtfnServer) error {
// We'll start by reconstructing the RPC request into what the
// underlying ChainNotifier expects.
var hash chainhash.Hash
copy(hash[:], in.Hash)
// If the request isn't for a zero hash and a zero height, then we
// should deliver a backlog of notifications from the given block
// (hash/height tuple) until tip, and continue delivering epochs for
// new blocks.
var blockEpoch *chainntnfs.BlockEpoch
if hash != chainntnfs.ZeroHash && in.Height != 0 {
blockEpoch = &chainntnfs.BlockEpoch{
Hash: &hash,
Height: int32(in.Height),
}
}
epochEvent, err := s.cfg.ChainNotifier.RegisterBlockEpochNtfn(blockEpoch)
if err != nil {
return err
}
defer epochEvent.Cancel()
for {
select {
// A notification for a block has been received. This block can
// either be a new block or stale.
case blockEpoch, ok := <-epochEvent.Epochs:
if !ok {
return chainntnfs.ErrChainNotifierShuttingDown
}
epoch := &BlockEpoch{
Hash: blockEpoch.Hash[:],
Height: uint32(blockEpoch.Height),
}
if err := epochStream.Send(epoch); err != nil {
return err
}
// The response stream's context for whatever reason has been
// closed. We'll return the error indicated by the context
// itself to the caller.
case <-epochStream.Context().Done():
return epochStream.Context().Err()
// The server has been requested to shut down.
case <-s.quit:
return ErrChainNotifierServerShuttingDown
}
}
}
File diff suppressed because it is too large. Load diff
+177
View File
@@ -0,0 +1,177 @@
syntax = "proto3";
package chainrpc;
message ConfRequest {
/*
The transaction hash for which we should request a confirmation notification
for. If set to a hash of all zeros, then the confirmation notification will
be requested for the script instead.
*/
bytes txid = 1;
/*
An output script within a transaction with the hash above which will be used
by light clients to match block filters. If the transaction hash is set to a
hash of all zeros, then a confirmation notification will be requested for
this script instead.
*/
bytes script = 2;
/*
The number of desired confirmations the transaction/output script should
reach before dispatching a confirmation notification.
*/
uint32 num_confs = 3;
/*
The earliest height in the chain for which the transaction/output script
could have been included in a block. This should in most cases be set to the
broadcast height of the transaction/output script.
*/
uint32 height_hint = 4;
}
message ConfDetails {
// The raw bytes of the confirmed transaction.
bytes raw_tx = 1;
// The hash of the block in which the confirmed transaction was included in.
bytes block_hash = 2;
// The height of the block in which the confirmed transaction was included in.
uint32 block_height = 3;
// The index of the confirmed transaction within the transaction.
uint32 tx_index = 4;
}
message Reorg {
// TODO(wilmer): need to know how the client will use this first.
}
message ConfEvent {
oneof event {
/*
An event that includes the confirmation details of the request
(txid/ouput script).
*/
ConfDetails conf = 1;
/*
An event send when the transaction of the request is reorged out of the
chain.
*/
Reorg reorg = 2;
}
}
message Outpoint {
// The hash of the transaction.
bytes hash = 1;
// The index of the output within the transaction.
uint32 index = 2;
}
message SpendRequest {
/*
The outpoint for which we should request a spend notification for. If set to
a zero outpoint, then the spend notification will be requested for the
script instead.
*/
Outpoint outpoint = 1;
/*
The output script for the outpoint above. This will be used by light clients
to match block filters. If the outpoint is set to a zero outpoint, then a
spend notification will be requested for this script instead.
*/
bytes script = 2;
/*
The earliest height in the chain for which the outpoint/output script could
have been spent. This should in most cases be set to the broadcast height of
the outpoint/output script.
*/
uint32 height_hint = 3;
// TODO(wilmer): extend to support num confs on spending tx.
}
message SpendDetails {
// The outpoint was that spent.
Outpoint spending_outpoint = 1;
// The raw bytes of the spending transaction.
bytes raw_spending_tx = 2;
// The hash of the spending transaction.
bytes spending_tx_hash = 3;
// The input of the spending transaction that fulfilled the spend request.
uint32 spending_input_index = 4;
// The height at which the spending transaction was included in a block.
uint32 spending_height = 5;
}
message SpendEvent {
oneof event {
/*
An event that includes the details of the spending transaction of the
request (outpoint/output script).
*/
SpendDetails spend = 1;
/*
An event sent when the spending transaction of the request was
reorged out of the chain.
*/
Reorg reorg = 2;
}
}
message BlockEpoch {
// The hash of the block.
bytes hash = 1;
// The height of the block.
uint32 height = 2;
}
service ChainNotifier {
/*
RegisterConfirmationsNtfn is a synchronous response-streaming RPC that
registers an intent for a client to be notified once a confirmation request
has reached its required number of confirmations on-chain.
A client can specify whether the confirmation request should be for a
particular transaction by its hash or for an output script by specifying a
zero hash.
*/
rpc RegisterConfirmationsNtfn(ConfRequest) returns (stream ConfEvent);
/*
RegisterSpendNtfn is a synchronous response-streaming RPC that registers an
intent for a client to be notification once a spend request has been spent
by a transaction that has confirmed on-chain.
A client can specify whether the spend request should be for a particular
outpoint or for an output script by specifying a zero outpoint.
*/
rpc RegisterSpendNtfn(SpendRequest) returns (stream SpendEvent);
/*
RegisterBlockEpochNtfn is a synchronous response-streaming RPC that
registers an intent for a client to be notified of blocks in the chain. The
stream will return a hash and height tuple of a block for each new/stale
block in the chain. It is the client's responsibility to determine whether
the tuple returned is for a new or stale block in the chain.
A client can also request a historical backlog of blocks from a particular
point. This allows clients to be idempotent by ensuring that they do not
missing processing a single block within the chain.
*/
rpc RegisterBlockEpochNtfn(BlockEpoch) returns (stream BlockEpoch);
}
+34
View File
@@ -0,0 +1,34 @@
// +build chainrpc
package chainrpc
import (
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/macaroons"
)
// Config is the primary configuration struct for the chain notifier RPC server.
// It contains all the items required for the server to carry out its duties.
// The fields with struct tags are meant to be parsed as normal configuration
// options, while if able to be populated, the latter fields MUST also be
// specified.
type Config struct {
// ChainNotifierMacPath is the path for the chain notifier macaroon. If
// unspecified then we assume that the macaroon will be found under the
// network directory, named DefaultChainNotifierMacFilename.
ChainNotifierMacPath string `long:"notifiermacaroonpath" description:"Path to the chain notifier macaroon"`
// NetworkDir is the main network directory wherein the chain notifier
// RPC server will find the macaroon named
// DefaultChainNotifierMacFilename.
NetworkDir string
// MacService is the main macaroon service that we'll use to handle
// authentication for the chain notifier RPC server.
MacService *macaroons.Service
// ChainNotifier is the chain notifier instance that backs the chain
// notifier RPC server. The job of the chain notifier RPC server is
// simply to proxy valid requests to the active chain notifier instance.
ChainNotifier chainntnfs.ChainNotifier
}
+6
View File
@@ -0,0 +1,6 @@
// +build !chainrpc
package chainrpc
// Config is empty for non-chainrpc builds.
type Config struct{}
+71
View File
@@ -0,0 +1,71 @@
// +build chainrpc
package chainrpc
import (
"fmt"
"github.com/lightningnetwork/lnd/lnrpc"
)
// createNewSubServer is a helper method that will create the new chain notifier
// sub server given the main config dispatcher method. If we're unable to find
// the config that is meant for us in the config dispatcher, then we'll exit
// with an error.
func createNewSubServer(configRegistry lnrpc.SubServerConfigDispatcher) (
lnrpc.SubServer, lnrpc.MacaroonPerms, error) {
// We'll attempt to look up the config that we expect, according to our
// subServerName name. If we can't find this, then we'll exit with an
// error, as we're unable to properly initialize ourselves without this
// config.
chainNotifierServerConf, ok := configRegistry.FetchConfig(subServerName)
if !ok {
return nil, nil, fmt.Errorf("unable to find config for "+
"subserver type %s", subServerName)
}
// Now that we've found an object mapping to our service name, we'll
// ensure that it's the type we need.
config, ok := chainNotifierServerConf.(*Config)
if !ok {
return nil, nil, fmt.Errorf("wrong type of config for "+
"subserver %s, expected %T got %T", subServerName,
&Config{}, chainNotifierServerConf)
}
// Before we try to make the new chain notifier service instance, we'll
// perform some sanity checks on the arguments to ensure that they're
// usable.
switch {
// If the macaroon service is set (we should use macaroons), then
// ensure that we know where to look for them, or create them if not
// found.
case config.MacService != nil && config.NetworkDir == "":
return nil, nil, fmt.Errorf("NetworkDir must be set to create " +
"chainrpc")
case config.ChainNotifier == nil:
return nil, nil, fmt.Errorf("ChainNotifier must be set to " +
"create chainrpc")
}
return New(config)
}
func init() {
subServer := &lnrpc.SubServerDriver{
SubServerName: subServerName,
New: func(c lnrpc.SubServerConfigDispatcher) (
lnrpc.SubServer, lnrpc.MacaroonPerms, error) {
return createNewSubServer(c)
},
}
// If the build tag is active, then we'll register ourselves as a
// sub-RPC server within the global lnrpc package namespace.
if err := lnrpc.RegisterSubServer(subServer); err != nil {
panic(fmt.Sprintf("failed to register subserver driver %s: %v",
subServerName, err))
}
}
+45
View File
@@ -0,0 +1,45 @@
package chainrpc
import (
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/build"
)
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var log btclog.Logger
// The default amount of logging is none.
func init() {
UseLogger(build.NewSubLogger("NTFR", nil))
}
// DisableLog disables all library log output. Logging output is disabled
// by default until UseLogger is called.
func DisableLog() {
UseLogger(btclog.Disabled)
}
// UseLogger uses a specified Logger to output package logging info.
// This should be used in preference to SetLogWriter if the caller is also
// using btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}
// logClosure is used to provide a closure over expensive logging operations so
// don't have to be performed when the logging level doesn't warrant it.
type logClosure func() string
// String invokes the underlying function and returns the result.
func (c logClosure) String() string {
return c()
}
// newLogClosure returns a new closure over a function that returns a string
// which itself provides a Stringer interface so that it can be used with the
// logging system.
func newLogClosure(c func() string) logClosure {
return logClosure(c)
}
+3 -1
View File
@@ -2362,7 +2362,9 @@ func TestLightningWallet(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("unable to create height hint cache: %v", err) t.Fatalf("unable to create height hint cache: %v", err)
} }
chainNotifier, err := btcdnotify.New(&rpcConfig, hintCache, hintCache) chainNotifier, err := btcdnotify.New(
&rpcConfig, netParams, hintCache, hintCache,
)
if err != nil { if err != nil {
t.Fatalf("unable to create notifier: %v", err) t.Fatalf("unable to create notifier: %v", err)
} }
+4 -1
View File
@@ -10,7 +10,6 @@ import (
"github.com/btcsuite/btclog" "github.com/btcsuite/btclog"
"github.com/jrick/logrotate/rotator" "github.com/jrick/logrotate/rotator"
"github.com/lightninglabs/neutrino" "github.com/lightninglabs/neutrino"
"github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/autopilot" "github.com/lightningnetwork/lnd/autopilot"
"github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/build"
@@ -21,6 +20,7 @@ import (
"github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc" "github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
"github.com/lightningnetwork/lnd/lnrpc/signrpc" "github.com/lightningnetwork/lnd/lnrpc/signrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
@@ -77,6 +77,7 @@ var (
invcLog = build.NewSubLogger("INVC", backendLog.Logger) invcLog = build.NewSubLogger("INVC", backendLog.Logger)
nannLog = build.NewSubLogger("NANN", backendLog.Logger) nannLog = build.NewSubLogger("NANN", backendLog.Logger)
wtwrLog = build.NewSubLogger("WTWR", backendLog.Logger) wtwrLog = build.NewSubLogger("WTWR", backendLog.Logger)
ntfrLog = build.NewSubLogger("NTFR", backendLog.Logger)
) )
// Initialize package-global logger variables. // Initialize package-global logger variables.
@@ -100,6 +101,7 @@ func init() {
invoices.UseLogger(invcLog) invoices.UseLogger(invcLog)
netann.UseLogger(nannLog) netann.UseLogger(nannLog)
watchtower.UseLogger(wtwrLog) watchtower.UseLogger(wtwrLog)
chainrpc.UseLogger(ntfrLog)
} }
// subsystemLoggers maps each subsystem identifier to its associated logger. // subsystemLoggers maps each subsystem identifier to its associated logger.
@@ -129,6 +131,7 @@ var subsystemLoggers = map[string]btclog.Logger{
"INVC": invcLog, "INVC": invcLog,
"NANN": nannLog, "NANN": nannLog,
"WTWR": wtwrLog, "WTWR": wtwrLog,
"NTFR": ntfnLog,
} }
// initLogRotator initializes the logging rotator to write logs to logFile and // initLogRotator initializes the logging rotator to write logs to logFile and
+19
View File
@@ -6,6 +6,7 @@ import (
"github.com/lightningnetwork/lnd/autopilot" "github.com/lightningnetwork/lnd/autopilot"
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc" "github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
"github.com/lightningnetwork/lnd/lnrpc/signrpc" "github.com/lightningnetwork/lnd/lnrpc/signrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/macaroons" "github.com/lightningnetwork/lnd/macaroons"
@@ -31,6 +32,11 @@ type subRPCServerConfigs struct {
// AutopilotRPC is a sub-RPC server that exposes methods on the running // AutopilotRPC is a sub-RPC server that exposes methods on the running
// autopilot as a gRPC service. // autopilot as a gRPC service.
AutopilotRPC *autopilotrpc.Config `group:"autopilotrpc" namespace:"autopilotrpc"` AutopilotRPC *autopilotrpc.Config `group:"autopilotrpc" namespace:"autopilotrpc"`
// ChainRPC is a sub-RPC server that exposes functionality allowing a
// client to be notified of certain on-chain events (new blocks,
// confirmations, spends).
ChainRPC *chainrpc.Config `group:"chainrpc" namespace:"chainrpc"`
} }
// PopulateDependencies attempts to iterate through all the sub-server configs // PopulateDependencies attempts to iterate through all the sub-server configs
@@ -106,6 +112,19 @@ func (s *subRPCServerConfigs) PopulateDependencies(cc *chainControl,
reflect.ValueOf(atpl), reflect.ValueOf(atpl),
) )
case *chainrpc.Config:
subCfgValue := extractReflectValue(cfg)
subCfgValue.FieldByName("NetworkDir").Set(
reflect.ValueOf(networkDir),
)
subCfgValue.FieldByName("MacService").Set(
reflect.ValueOf(macService),
)
subCfgValue.FieldByName("ChainNotifier").Set(
reflect.ValueOf(cc.chainNotifier),
)
default: default:
return fmt.Errorf("unknown field: %v, %T", fieldName, return fmt.Errorf("unknown field: %v, %T", fieldName,
cfg) cfg)