Merge pull request #2004 from wpaulino/spend-tx-notifier
chainntnfs: handle spend notifications within TxConfNotifier
This commit is contained in:
commit
9f0b008773
@ -12,7 +12,6 @@ import (
|
|||||||
"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/chain"
|
||||||
"github.com/btcsuite/btcwallet/wtxmgr"
|
|
||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/queue"
|
"github.com/lightningnetwork/lnd/queue"
|
||||||
)
|
)
|
||||||
@ -69,9 +68,7 @@ type BitcoindNotifier struct {
|
|||||||
notificationCancels chan interface{}
|
notificationCancels chan interface{}
|
||||||
notificationRegistry chan interface{}
|
notificationRegistry chan interface{}
|
||||||
|
|
||||||
spendNotifications map[wire.OutPoint]map[uint64]*spendNotification
|
txNotifier *chainntnfs.TxNotifier
|
||||||
|
|
||||||
txConfNotifier *chainntnfs.TxConfNotifier
|
|
||||||
|
|
||||||
blockEpochClients map[uint64]*blockEpochRegistration
|
blockEpochClients map[uint64]*blockEpochRegistration
|
||||||
|
|
||||||
@ -107,8 +104,6 @@ func New(chainConn *chain.BitcoindConn, spendHintCache chainntnfs.SpendHintCache
|
|||||||
|
|
||||||
blockEpochClients: make(map[uint64]*blockEpochRegistration),
|
blockEpochClients: make(map[uint64]*blockEpochRegistration),
|
||||||
|
|
||||||
spendNotifications: make(map[wire.OutPoint]map[uint64]*spendNotification),
|
|
||||||
|
|
||||||
spendHintCache: spendHintCache,
|
spendHintCache: spendHintCache,
|
||||||
confirmHintCache: confirmHintCache,
|
confirmHintCache: confirmHintCache,
|
||||||
|
|
||||||
@ -142,8 +137,9 @@ func (b *BitcoindNotifier) Start() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.txConfNotifier = chainntnfs.NewTxConfNotifier(
|
b.txNotifier = chainntnfs.NewTxNotifier(
|
||||||
uint32(currentHeight), reorgSafetyLimit, b.confirmHintCache,
|
uint32(currentHeight), reorgSafetyLimit, b.confirmHintCache,
|
||||||
|
b.spendHintCache,
|
||||||
)
|
)
|
||||||
|
|
||||||
b.bestBlock = chainntnfs.BlockEpoch{
|
b.bestBlock = chainntnfs.BlockEpoch{
|
||||||
@ -173,18 +169,13 @@ func (b *BitcoindNotifier) Stop() error {
|
|||||||
|
|
||||||
// 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.
|
||||||
for _, spendClients := range b.spendNotifications {
|
|
||||||
for _, spendClient := range spendClients {
|
|
||||||
close(spendClient.spendChan)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, epochClient := range b.blockEpochClients {
|
for _, epochClient := range b.blockEpochClients {
|
||||||
close(epochClient.cancelChan)
|
close(epochClient.cancelChan)
|
||||||
epochClient.wg.Wait()
|
epochClient.wg.Wait()
|
||||||
|
|
||||||
close(epochClient.epochChan)
|
close(epochClient.epochChan)
|
||||||
}
|
}
|
||||||
b.txConfNotifier.TearDown()
|
b.txNotifier.TearDown()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -204,19 +195,6 @@ out:
|
|||||||
select {
|
select {
|
||||||
case cancelMsg := <-b.notificationCancels:
|
case cancelMsg := <-b.notificationCancels:
|
||||||
switch msg := cancelMsg.(type) {
|
switch msg := cancelMsg.(type) {
|
||||||
case *spendCancel:
|
|
||||||
chainntnfs.Log.Infof("Cancelling spend "+
|
|
||||||
"notification for out_point=%v, "+
|
|
||||||
"spend_id=%v", msg.op, msg.spendID)
|
|
||||||
|
|
||||||
// Before we attempt to close the spendChan,
|
|
||||||
// ensure that the notification hasn't already
|
|
||||||
// yet been dispatched.
|
|
||||||
if outPointClients, ok := b.spendNotifications[msg.op]; ok {
|
|
||||||
close(outPointClients[msg.spendID].spendChan)
|
|
||||||
delete(b.spendNotifications[msg.op], msg.spendID)
|
|
||||||
}
|
|
||||||
|
|
||||||
case *epochCancel:
|
case *epochCancel:
|
||||||
chainntnfs.Log.Infof("Cancelling epoch "+
|
chainntnfs.Log.Infof("Cancelling epoch "+
|
||||||
"notification, epoch_id=%v", msg.epochID)
|
"notification, epoch_id=%v", msg.epochID)
|
||||||
@ -244,21 +222,13 @@ out:
|
|||||||
}
|
}
|
||||||
case registerMsg := <-b.notificationRegistry:
|
case registerMsg := <-b.notificationRegistry:
|
||||||
switch msg := registerMsg.(type) {
|
switch msg := registerMsg.(type) {
|
||||||
case *spendNotification:
|
|
||||||
chainntnfs.Log.Infof("New spend subscription: "+
|
|
||||||
"utxo=%v", msg.targetOutpoint)
|
|
||||||
op := *msg.targetOutpoint
|
|
||||||
|
|
||||||
if _, ok := b.spendNotifications[op]; !ok {
|
|
||||||
b.spendNotifications[op] = make(map[uint64]*spendNotification)
|
|
||||||
}
|
|
||||||
b.spendNotifications[op][msg.spendID] = msg
|
|
||||||
|
|
||||||
case *chainntnfs.HistoricalConfDispatch:
|
case *chainntnfs.HistoricalConfDispatch:
|
||||||
// Look up whether the transaction is already
|
// Look up whether the transaction is already
|
||||||
// included in the active chain. We'll do this
|
// included in the active chain. We'll do this
|
||||||
// in a goroutine to prevent blocking
|
// in a goroutine to prevent blocking
|
||||||
// potentially long rescans.
|
// potentially long rescans.
|
||||||
|
//
|
||||||
|
// TODO(wilmer): add retry logic if rescan fails?
|
||||||
b.wg.Add(1)
|
b.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer b.wg.Done()
|
defer b.wg.Done()
|
||||||
@ -278,7 +248,7 @@ out:
|
|||||||
// begin safely updating the height hint
|
// begin safely updating the height hint
|
||||||
// cache at tip, since any pending
|
// cache at tip, since any pending
|
||||||
// rescans have now completed.
|
// rescans have now completed.
|
||||||
err = b.txConfNotifier.UpdateConfDetails(
|
err = b.txNotifier.UpdateConfDetails(
|
||||||
*msg.TxID, confDetails,
|
*msg.TxID, confDetails,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -286,6 +256,25 @@ out:
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
case *chainntnfs.HistoricalSpendDispatch:
|
||||||
|
// In order to ensure we don't block the caller
|
||||||
|
// on what may be a long rescan, we'll launch a
|
||||||
|
// goroutine to do so in the background.
|
||||||
|
//
|
||||||
|
// TODO(wilmer): add retry logic if rescan fails?
|
||||||
|
b.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer b.wg.Done()
|
||||||
|
|
||||||
|
err := b.dispatchSpendDetailsManually(msg)
|
||||||
|
if err != nil {
|
||||||
|
chainntnfs.Log.Errorf("Rescan to "+
|
||||||
|
"determine the spend "+
|
||||||
|
"details of %v failed: %v",
|
||||||
|
msg.OutPoint, 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
|
||||||
@ -305,9 +294,6 @@ out:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
msg.errorChan <- nil
|
msg.errorChan <- nil
|
||||||
|
|
||||||
case chain.RelevantTx:
|
|
||||||
b.handleRelevantTx(msg, b.bestBlock.Height)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case ntfn := <-b.chainConn.Notifications():
|
case ntfn := <-b.chainConn.Notifications():
|
||||||
@ -330,7 +316,7 @@ out:
|
|||||||
newBestBlock, missedBlocks, err :=
|
newBestBlock, missedBlocks, err :=
|
||||||
chainntnfs.HandleMissedBlocks(
|
chainntnfs.HandleMissedBlocks(
|
||||||
b.chainConn,
|
b.chainConn,
|
||||||
b.txConfNotifier,
|
b.txNotifier,
|
||||||
b.bestBlock, item.Height,
|
b.bestBlock, item.Height,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
@ -369,7 +355,7 @@ out:
|
|||||||
}
|
}
|
||||||
|
|
||||||
newBestBlock, err := chainntnfs.RewindChain(
|
newBestBlock, err := chainntnfs.RewindChain(
|
||||||
b.chainConn, b.txConfNotifier,
|
b.chainConn, b.txNotifier,
|
||||||
b.bestBlock, item.Height-1,
|
b.bestBlock, item.Height-1,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -383,7 +369,23 @@ out:
|
|||||||
b.bestBlock = newBestBlock
|
b.bestBlock = newBestBlock
|
||||||
|
|
||||||
case chain.RelevantTx:
|
case chain.RelevantTx:
|
||||||
b.handleRelevantTx(item, b.bestBlock.Height)
|
// We only care about notifying on confirmed
|
||||||
|
// spends, so if this is a mempool spend, we can
|
||||||
|
// ignore it and wait for the spend to appear in
|
||||||
|
// on-chain.
|
||||||
|
if item.Block == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := &item.TxRecord.MsgTx
|
||||||
|
err := b.txNotifier.ProcessRelevantSpendTx(
|
||||||
|
tx, item.Block.Height,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
chainntnfs.Log.Errorf("Unable to "+
|
||||||
|
"process transaction %v: %v",
|
||||||
|
tx.TxHash(), err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-b.quit:
|
case <-b.quit:
|
||||||
@ -393,55 +395,6 @@ out:
|
|||||||
b.wg.Done()
|
b.wg.Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleRelevantTx notifies any clients of a relevant transaction.
|
|
||||||
func (b *BitcoindNotifier) handleRelevantTx(tx chain.RelevantTx, bestHeight int32) {
|
|
||||||
msgTx := tx.TxRecord.MsgTx
|
|
||||||
|
|
||||||
// We only care about notifying on confirmed spends, so in case this is
|
|
||||||
// a mempool spend, we can continue, and wait for the spend to appear
|
|
||||||
// in chain.
|
|
||||||
if tx.Block == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, check if this transaction spends an output
|
|
||||||
// that has an existing spend notification for it.
|
|
||||||
for i, txIn := range msgTx.TxIn {
|
|
||||||
prevOut := txIn.PreviousOutPoint
|
|
||||||
|
|
||||||
// If this transaction indeed does spend an
|
|
||||||
// output which we have a registered
|
|
||||||
// notification for, then create a spend
|
|
||||||
// summary, finally sending off the details to
|
|
||||||
// the notification subscriber.
|
|
||||||
if clients, ok := b.spendNotifications[prevOut]; ok {
|
|
||||||
spenderSha := msgTx.TxHash()
|
|
||||||
spendDetails := &chainntnfs.SpendDetail{
|
|
||||||
SpentOutPoint: &prevOut,
|
|
||||||
SpenderTxHash: &spenderSha,
|
|
||||||
SpendingTx: &msgTx,
|
|
||||||
SpenderInputIndex: uint32(i),
|
|
||||||
}
|
|
||||||
spendDetails.SpendingHeight = tx.Block.Height
|
|
||||||
|
|
||||||
for _, ntfn := range clients {
|
|
||||||
chainntnfs.Log.Infof("Dispatching confirmed "+
|
|
||||||
"spend notification for outpoint=%v "+
|
|
||||||
"at height %v", ntfn.targetOutpoint,
|
|
||||||
spendDetails.SpendingHeight)
|
|
||||||
ntfn.spendChan <- spendDetails
|
|
||||||
|
|
||||||
// Close spendChan to ensure that any calls to
|
|
||||||
// Cancel will not block. This is safe to do
|
|
||||||
// since the channel is buffered, and the
|
|
||||||
// message can still be read by the receiver.
|
|
||||||
close(ntfn.spendChan)
|
|
||||||
}
|
|
||||||
delete(b.spendNotifications, prevOut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// historicalConfDetails looks up whether a transaction is already included in a
|
// historicalConfDetails looks up whether a transaction is already included in a
|
||||||
// block in the active chain and, if so, returns details about the confirmation.
|
// block in the active chain and, if so, returns details about the confirmation.
|
||||||
func (b *BitcoindNotifier) historicalConfDetails(txid *chainhash.Hash,
|
func (b *BitcoindNotifier) historicalConfDetails(txid *chainhash.Hash,
|
||||||
@ -621,7 +574,7 @@ func (b *BitcoindNotifier) handleBlockConnected(block chainntnfs.BlockEpoch) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
txns := btcutil.NewBlock(rawBlock).Transactions()
|
txns := btcutil.NewBlock(rawBlock).Transactions()
|
||||||
err = b.txConfNotifier.ConnectTip(
|
err = b.txNotifier.ConnectTip(
|
||||||
block.Hash, uint32(block.Height), txns)
|
block.Hash, uint32(block.Height), txns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to connect tip: %v", err)
|
return fmt.Errorf("unable to connect tip: %v", err)
|
||||||
@ -630,27 +583,6 @@ func (b *BitcoindNotifier) handleBlockConnected(block chainntnfs.BlockEpoch) err
|
|||||||
chainntnfs.Log.Infof("New block: height=%v, sha=%v", block.Height,
|
chainntnfs.Log.Infof("New block: height=%v, sha=%v", block.Height,
|
||||||
block.Hash)
|
block.Hash)
|
||||||
|
|
||||||
// Finally, we'll update the spend height hint for all of our watched
|
|
||||||
// outpoints that have not been spent yet. This is safe to do as we do
|
|
||||||
// not watch already spent outpoints for spend notifications.
|
|
||||||
ops := make([]wire.OutPoint, 0, len(b.spendNotifications))
|
|
||||||
for op := range b.spendNotifications {
|
|
||||||
ops = append(ops, op)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ops) > 0 {
|
|
||||||
err := b.spendHintCache.CommitSpendHint(
|
|
||||||
uint32(block.Height), ops...,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
// The error is not fatal since we are connecting a
|
|
||||||
// block, and advancing the spend hint is an optimistic
|
|
||||||
// optimization.
|
|
||||||
chainntnfs.Log.Errorf("Unable to update spend hint to "+
|
|
||||||
"%d for %v: %v", block.Height, ops, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to set the best block before dispatching notifications so
|
// We want to set the best block before dispatching notifications so
|
||||||
// if any subscribers make queries based on their received block epoch,
|
// if any subscribers make queries based on their received block epoch,
|
||||||
// our state is fully updated in time.
|
// our state is fully updated in time.
|
||||||
@ -687,28 +619,6 @@ func (b *BitcoindNotifier) notifyBlockEpochClient(epochClient *blockEpochRegistr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// spendNotification couples a target outpoint along with the channel used for
|
|
||||||
// notifications once a spend of the outpoint has been detected.
|
|
||||||
type spendNotification struct {
|
|
||||||
targetOutpoint *wire.OutPoint
|
|
||||||
|
|
||||||
spendChan chan *chainntnfs.SpendDetail
|
|
||||||
|
|
||||||
spendID uint64
|
|
||||||
|
|
||||||
heightHint uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// spendCancel is a message sent to the BitcoindNotifier when a client wishes
|
|
||||||
// to cancel an outstanding spend notification that has yet to be dispatched.
|
|
||||||
type spendCancel struct {
|
|
||||||
// op is the target outpoint of the notification to be cancelled.
|
|
||||||
op wire.OutPoint
|
|
||||||
|
|
||||||
// spendID the ID of the notification to cancel.
|
|
||||||
spendID uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterSpendNtfn registers an intent to be notified once the target
|
// RegisterSpendNtfn registers an intent to be notified once the target
|
||||||
// outpoint has been spent by a transaction on-chain. Once a spend of the target
|
// outpoint has been spent by a transaction on-chain. Once a spend of the target
|
||||||
// outpoint has been detected, the details of the spending event will be sent
|
// outpoint has been detected, the details of the spending event will be sent
|
||||||
@ -717,167 +627,120 @@ type spendCancel struct {
|
|||||||
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) {
|
||||||
|
|
||||||
// Before proceeding to register the notification, we'll query our
|
// First, we'll construct a spend notification request and hand it off
|
||||||
// height hint cache to determine whether a better one exists.
|
// to the txNotifier.
|
||||||
if hint, err := b.spendHintCache.QuerySpendHint(*outpoint); err == nil {
|
spendID := atomic.AddUint64(&b.spendClientCounter, 1)
|
||||||
if hint > heightHint {
|
cancel := func() {
|
||||||
chainntnfs.Log.Debugf("Using height hint %d retrieved "+
|
b.txNotifier.CancelSpend(*outpoint, spendID)
|
||||||
"from cache for %v", hint, outpoint)
|
|
||||||
heightHint = hint
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct a notification request for the outpoint and send it to the
|
ntfn := &chainntnfs.SpendNtfn{
|
||||||
// main event loop.
|
SpendID: spendID,
|
||||||
ntfn := &spendNotification{
|
OutPoint: *outpoint,
|
||||||
targetOutpoint: outpoint,
|
PkScript: pkScript,
|
||||||
spendChan: make(chan *chainntnfs.SpendDetail, 1),
|
Event: chainntnfs.NewSpendEvent(cancel),
|
||||||
spendID: atomic.AddUint64(&b.spendClientCounter, 1),
|
HeightHint: heightHint,
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
historicalDispatch, err := b.txNotifier.RegisterSpend(ntfn)
|
||||||
case <-b.quit:
|
|
||||||
return nil, ErrChainNotifierShuttingDown
|
|
||||||
case b.notificationRegistry <- ntfn:
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.chainConn.NotifySpent([]*wire.OutPoint{outpoint}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following conditional checks to ensure that when a spend
|
|
||||||
// notification is registered, the output hasn't already been spent. If
|
|
||||||
// the output is no longer in the UTXO set, the chain will be rescanned
|
|
||||||
// from the point where the output was added. The rescan will dispatch
|
|
||||||
// the notification.
|
|
||||||
txOut, err := b.chainConn.GetTxOut(&outpoint.Hash, outpoint.Index, true)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the output is unspent, then we'll write it to the cache with the
|
// If the txNotifier didn't return any details to perform a historical
|
||||||
// given height hint. This allows us to increase the height hint as the
|
// scan of the chain, then we can return early as there's nothing left
|
||||||
// chain extends and the output remains unspent.
|
// for us to do.
|
||||||
|
if historicalDispatch == nil {
|
||||||
|
return ntfn.Event, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll then request the backend to notify us when it has detected the
|
||||||
|
// outpoint as spent.
|
||||||
|
if err := b.chainConn.NotifySpent([]*wire.OutPoint{outpoint}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// In addition to the check above, we'll also check 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(&outpoint.Hash, outpoint.Index, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if txOut != nil {
|
if txOut != nil {
|
||||||
err := b.spendHintCache.CommitSpendHint(heightHint, *outpoint)
|
// We'll let the txNotifier know the outpoint is still unspent
|
||||||
|
// in order to begin updating its spend hint.
|
||||||
|
err := b.txNotifier.UpdateSpendDetails(*outpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// The error is not fatal, so we should not return an
|
return nil, err
|
||||||
// error to the caller.
|
|
||||||
chainntnfs.Log.Error("Unable to update spend hint to "+
|
|
||||||
"%d for %v: %v", heightHint, *outpoint, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Otherwise, we'll determine when the output was spent.
|
|
||||||
//
|
|
||||||
// First, we'll attempt to retrieve the transaction's block hash
|
|
||||||
// using the backend's transaction index.
|
|
||||||
tx, err := b.chainConn.GetRawTransactionVerbose(&outpoint.Hash)
|
|
||||||
if err != nil {
|
|
||||||
// Avoid returning an error if the transaction was not
|
|
||||||
// found to proceed with fallback methods.
|
|
||||||
jsonErr, ok := err.(*btcjson.RPCError)
|
|
||||||
if !ok || jsonErr.Code != btcjson.ErrRPCNoTxInfo {
|
|
||||||
return nil, fmt.Errorf("unable to query for "+
|
|
||||||
"txid %v: %v", outpoint.Hash, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var blockHash *chainhash.Hash
|
return ntfn.Event, nil
|
||||||
if tx != nil && tx.BlockHash != "" {
|
}
|
||||||
// If we're able to retrieve a valid block hash from the
|
|
||||||
// transaction, then we'll use it as our rescan starting
|
|
||||||
// point.
|
|
||||||
blockHash, err = chainhash.NewHashFromStr(tx.BlockHash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Otherwise, we'll attempt to retrieve the hash for the
|
|
||||||
// block at the heightHint.
|
|
||||||
blockHash, err = b.chainConn.GetBlockHash(
|
|
||||||
int64(heightHint),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to retrieve "+
|
|
||||||
"hash for block with height %d: %v",
|
|
||||||
heightHint, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll only scan old blocks if the transaction has actually
|
// Otherwise, we'll determine when the output was spent by scanning the
|
||||||
// been included within a block. Otherwise, we'll encounter an
|
// chain. We'll begin by determining where to start our historical
|
||||||
// error when scanning for blocks. This can happens in the case
|
// rescan.
|
||||||
// of a race condition, wherein the output itself is unspent,
|
//
|
||||||
// and only arrives in the mempool after the getxout call.
|
// As a minimal optimization, we'll query the backend's transaction
|
||||||
if blockHash != nil {
|
// index (if enabled) to determine if we have a better rescan starting
|
||||||
// Rescan all the blocks until the current one.
|
// height. We can do this as the GetRawTransaction call will return the
|
||||||
startHeight, err := b.chainConn.GetBlockHeight(
|
// hash of the block it was included in within the chain.
|
||||||
blockHash,
|
tx, err := b.chainConn.GetRawTransactionVerbose(&outpoint.Hash)
|
||||||
)
|
if err != nil {
|
||||||
if err != nil {
|
// Avoid returning an error if the transaction was not found to
|
||||||
return nil, err
|
// proceed with fallback methods.
|
||||||
}
|
jsonErr, ok := err.(*btcjson.RPCError)
|
||||||
|
if !ok || jsonErr.Code != btcjson.ErrRPCNoTxInfo {
|
||||||
_, endHeight, err := b.chainConn.GetBestBlock()
|
return nil, fmt.Errorf("unable to query for "+
|
||||||
if err != nil {
|
"txid %v: %v", outpoint.Hash, err)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// In order to ensure we don't block the caller on what
|
|
||||||
// may be a long rescan, we'll launch a goroutine to do
|
|
||||||
// so in the background.
|
|
||||||
b.wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer b.wg.Done()
|
|
||||||
|
|
||||||
err := b.dispatchSpendDetailsManually(
|
|
||||||
*outpoint, startHeight, endHeight,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
chainntnfs.Log.Errorf("Rescan for spend "+
|
|
||||||
"notification txout(%x) "+
|
|
||||||
"failed: %v", outpoint, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &chainntnfs.SpendEvent{
|
// If the transaction index was enabled, we'll use the block's hash to
|
||||||
Spend: ntfn.spendChan,
|
// retrieve its height and check whether it provides a better starting
|
||||||
Cancel: func() {
|
// point for our rescan.
|
||||||
cancel := &spendCancel{
|
if tx != nil {
|
||||||
op: *outpoint,
|
// If the transaction containing the outpoint hasn't confirmed
|
||||||
spendID: ntfn.spendID,
|
// on-chain, then there's no need to perform a rescan.
|
||||||
}
|
if tx.BlockHash == "" {
|
||||||
|
return ntfn.Event, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Submit spend cancellation to notification dispatcher.
|
blockHash, err := chainhash.NewHashFromStr(tx.BlockHash)
|
||||||
select {
|
if err != nil {
|
||||||
case b.notificationCancels <- cancel:
|
return nil, err
|
||||||
// Cancellation is being handled, drain the
|
}
|
||||||
// spend chan until it is closed before yielding
|
blockHeight, err := b.chainConn.GetBlockHeight(blockHash)
|
||||||
// to the caller.
|
if err != nil {
|
||||||
for {
|
return nil, err
|
||||||
select {
|
}
|
||||||
case _, ok := <-ntfn.spendChan:
|
|
||||||
if !ok {
|
if uint32(blockHeight) > historicalDispatch.StartHeight {
|
||||||
return
|
historicalDispatch.StartHeight = uint32(blockHeight)
|
||||||
}
|
}
|
||||||
case <-b.quit:
|
}
|
||||||
return
|
|
||||||
}
|
// Now that we've determined the starting point of our rescan, we can
|
||||||
}
|
// dispatch it.
|
||||||
case <-b.quit:
|
select {
|
||||||
}
|
case b.notificationRegistry <- historicalDispatch:
|
||||||
},
|
return ntfn.Event, nil
|
||||||
}, nil
|
case <-b.quit:
|
||||||
|
return nil, ErrChainNotifierShuttingDown
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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. If one
|
||||||
// is found, it's spending details are sent to the notifier dispatcher, which
|
// is found, it's spending details are sent to the notifier dispatcher, which
|
||||||
// will then dispatch the notification to all of its clients.
|
// will then dispatch the notification to all of its clients.
|
||||||
func (b *BitcoindNotifier) dispatchSpendDetailsManually(op wire.OutPoint,
|
func (b *BitcoindNotifier) dispatchSpendDetailsManually(
|
||||||
startHeight, endHeight int32) error {
|
historicalDispatchDetails *chainntnfs.HistoricalSpendDispatch) error {
|
||||||
|
|
||||||
|
op := historicalDispatchDetails.OutPoint
|
||||||
|
startHeight := historicalDispatchDetails.StartHeight
|
||||||
|
endHeight := historicalDispatchDetails.EndHeight
|
||||||
|
|
||||||
// Begin scanning blocks at every height to determine if the outpoint
|
// Begin scanning blocks at every height to determine if the outpoint
|
||||||
// was spent.
|
// was spent.
|
||||||
@ -890,6 +753,7 @@ func (b *BitcoindNotifier) dispatchSpendDetailsManually(op wire.OutPoint,
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First, we'll fetch the block for the current height.
|
||||||
blockHash, err := b.chainConn.GetBlockHash(int64(height))
|
blockHash, err := b.chainConn.GetBlockHash(int64(height))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to retrieve hash for block "+
|
return fmt.Errorf("unable to retrieve hash for block "+
|
||||||
@ -901,38 +765,30 @@ func (b *BitcoindNotifier) dispatchSpendDetailsManually(op wire.OutPoint,
|
|||||||
"%v: %v", blockHash, err)
|
"%v: %v", blockHash, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Then, we'll manually go over every transaction in it and
|
||||||
|
// determine whether it spends the outpoint in question.
|
||||||
for _, tx := range block.Transactions {
|
for _, tx := range block.Transactions {
|
||||||
for _, in := range tx.TxIn {
|
for i, txIn := range tx.TxIn {
|
||||||
if in.PreviousOutPoint != op {
|
if txIn.PreviousOutPoint != op {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this transaction input spends the
|
// If it does, we'll construct its spend details
|
||||||
// outpoint, we'll gather the details of the
|
// and hand them over to the TxNotifier so that
|
||||||
// spending transaction and dispatch a spend
|
// it can properly notify its registered
|
||||||
// notification to our clients.
|
// clients.
|
||||||
relTx := chain.RelevantTx{
|
txHash := tx.TxHash()
|
||||||
TxRecord: &wtxmgr.TxRecord{
|
details := &chainntnfs.SpendDetail{
|
||||||
MsgTx: *tx,
|
SpentOutPoint: &op,
|
||||||
Hash: tx.TxHash(),
|
SpenderTxHash: &txHash,
|
||||||
Received: block.Header.Timestamp,
|
SpendingTx: tx,
|
||||||
},
|
SpenderInputIndex: uint32(i),
|
||||||
Block: &wtxmgr.BlockMeta{
|
SpendingHeight: int32(height),
|
||||||
Block: wtxmgr.Block{
|
|
||||||
Hash: *blockHash,
|
|
||||||
Height: height,
|
|
||||||
},
|
|
||||||
Time: block.Header.Timestamp,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
return b.txNotifier.UpdateSpendDetails(
|
||||||
case b.notificationRegistry <- relTx:
|
op, details,
|
||||||
case <-b.quit:
|
)
|
||||||
return ErrChainNotifierShuttingDown
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -959,11 +815,11 @@ func (b *BitcoindNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash,
|
|||||||
chainntnfs.Log.Infof("New confirmation subscription: "+
|
chainntnfs.Log.Infof("New confirmation subscription: "+
|
||||||
"txid=%v, numconfs=%v", txid, numConfs)
|
"txid=%v, numconfs=%v", txid, numConfs)
|
||||||
|
|
||||||
// Register the conf notification with txconfnotifier. 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.txConfNotifier.Register(ntfn)
|
dispatch, err := b.txNotifier.RegisterConf(ntfn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// UnsafeStart starts the notifier with a specified best height and optional
|
// UnsafeStart starts the notifier with a specified best height and optional
|
||||||
// best hash. Its bestBlock and txConfNotifier are initialized with
|
// best hash. Its bestBlock and txNotifier are initialized with bestHeight and
|
||||||
// bestHeight and optionally bestHash. The parameter generateBlocks is
|
// optionally bestHash. The parameter generateBlocks is necessary for the
|
||||||
// necessary for the bitcoind notifier to ensure we drain all notifications up
|
// bitcoind notifier to ensure we drain all notifications up to syncHeight,
|
||||||
// to syncHeight, since if they are generated ahead of UnsafeStart the chainConn
|
// since if they are generated ahead of UnsafeStart the chainConn may start up
|
||||||
// may start up with an outdated best block and miss sending ntfns. Used for
|
// with an outdated best block and miss sending ntfns. Used for testing.
|
||||||
// testing.
|
|
||||||
func (b *BitcoindNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Hash,
|
func (b *BitcoindNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Hash,
|
||||||
syncHeight int32, generateBlocks func() error) error {
|
syncHeight int32, generateBlocks func() error) error {
|
||||||
|
|
||||||
@ -30,8 +29,9 @@ func (b *BitcoindNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Has
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.txConfNotifier = chainntnfs.NewTxConfNotifier(
|
b.txNotifier = chainntnfs.NewTxNotifier(
|
||||||
uint32(bestHeight), reorgSafetyLimit, b.confirmHintCache,
|
uint32(bestHeight), reorgSafetyLimit, b.confirmHintCache,
|
||||||
|
b.spendHintCache,
|
||||||
)
|
)
|
||||||
|
|
||||||
if generateBlocks != nil {
|
if generateBlocks != nil {
|
||||||
|
@ -74,9 +74,7 @@ type BtcdNotifier struct {
|
|||||||
notificationCancels chan interface{}
|
notificationCancels chan interface{}
|
||||||
notificationRegistry chan interface{}
|
notificationRegistry chan interface{}
|
||||||
|
|
||||||
spendNotifications map[wire.OutPoint]map[uint64]*spendNotification
|
txNotifier *chainntnfs.TxNotifier
|
||||||
|
|
||||||
txConfNotifier *chainntnfs.TxConfNotifier
|
|
||||||
|
|
||||||
blockEpochClients map[uint64]*blockEpochRegistration
|
blockEpochClients map[uint64]*blockEpochRegistration
|
||||||
|
|
||||||
@ -114,8 +112,6 @@ func New(config *rpcclient.ConnConfig, spendHintCache chainntnfs.SpendHintCache,
|
|||||||
|
|
||||||
blockEpochClients: make(map[uint64]*blockEpochRegistration),
|
blockEpochClients: make(map[uint64]*blockEpochRegistration),
|
||||||
|
|
||||||
spendNotifications: make(map[wire.OutPoint]map[uint64]*spendNotification),
|
|
||||||
|
|
||||||
chainUpdates: queue.NewConcurrentQueue(10),
|
chainUpdates: queue.NewConcurrentQueue(10),
|
||||||
txUpdates: queue.NewConcurrentQueue(10),
|
txUpdates: queue.NewConcurrentQueue(10),
|
||||||
|
|
||||||
@ -166,8 +162,9 @@ func (b *BtcdNotifier) Start() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.txConfNotifier = chainntnfs.NewTxConfNotifier(
|
b.txNotifier = chainntnfs.NewTxNotifier(
|
||||||
uint32(currentHeight), reorgSafetyLimit, b.confirmHintCache,
|
uint32(currentHeight), reorgSafetyLimit, b.confirmHintCache,
|
||||||
|
b.spendHintCache,
|
||||||
)
|
)
|
||||||
|
|
||||||
b.bestBlock = chainntnfs.BlockEpoch{
|
b.bestBlock = chainntnfs.BlockEpoch{
|
||||||
@ -203,18 +200,13 @@ func (b *BtcdNotifier) Stop() error {
|
|||||||
|
|
||||||
// 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.
|
||||||
for _, spendClients := range b.spendNotifications {
|
|
||||||
for _, spendClient := range spendClients {
|
|
||||||
close(spendClient.spendChan)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, epochClient := range b.blockEpochClients {
|
for _, epochClient := range b.blockEpochClients {
|
||||||
close(epochClient.cancelChan)
|
close(epochClient.cancelChan)
|
||||||
epochClient.wg.Wait()
|
epochClient.wg.Wait()
|
||||||
|
|
||||||
close(epochClient.epochChan)
|
close(epochClient.epochChan)
|
||||||
}
|
}
|
||||||
b.txConfNotifier.TearDown()
|
b.txNotifier.TearDown()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -275,19 +267,6 @@ out:
|
|||||||
select {
|
select {
|
||||||
case cancelMsg := <-b.notificationCancels:
|
case cancelMsg := <-b.notificationCancels:
|
||||||
switch msg := cancelMsg.(type) {
|
switch msg := cancelMsg.(type) {
|
||||||
case *spendCancel:
|
|
||||||
chainntnfs.Log.Infof("Cancelling spend "+
|
|
||||||
"notification for out_point=%v, "+
|
|
||||||
"spend_id=%v", msg.op, msg.spendID)
|
|
||||||
|
|
||||||
// Before we attempt to close the spendChan,
|
|
||||||
// ensure that the notification hasn't already
|
|
||||||
// yet been dispatched.
|
|
||||||
if outPointClients, ok := b.spendNotifications[msg.op]; ok {
|
|
||||||
close(outPointClients[msg.spendID].spendChan)
|
|
||||||
delete(b.spendNotifications[msg.op], msg.spendID)
|
|
||||||
}
|
|
||||||
|
|
||||||
case *epochCancel:
|
case *epochCancel:
|
||||||
chainntnfs.Log.Infof("Cancelling epoch "+
|
chainntnfs.Log.Infof("Cancelling epoch "+
|
||||||
"notification, epoch_id=%v", msg.epochID)
|
"notification, epoch_id=%v", msg.epochID)
|
||||||
@ -314,21 +293,13 @@ out:
|
|||||||
}
|
}
|
||||||
case registerMsg := <-b.notificationRegistry:
|
case registerMsg := <-b.notificationRegistry:
|
||||||
switch msg := registerMsg.(type) {
|
switch msg := registerMsg.(type) {
|
||||||
case *spendNotification:
|
|
||||||
chainntnfs.Log.Infof("New spend subscription: "+
|
|
||||||
"utxo=%v", msg.targetOutpoint)
|
|
||||||
op := *msg.targetOutpoint
|
|
||||||
|
|
||||||
if _, ok := b.spendNotifications[op]; !ok {
|
|
||||||
b.spendNotifications[op] = make(map[uint64]*spendNotification)
|
|
||||||
}
|
|
||||||
b.spendNotifications[op][msg.spendID] = msg
|
|
||||||
|
|
||||||
case *chainntnfs.HistoricalConfDispatch:
|
case *chainntnfs.HistoricalConfDispatch:
|
||||||
// Look up whether the transaction is already
|
// Look up whether the transaction is already
|
||||||
// included in the active chain. We'll do this
|
// included in the active chain. We'll do this
|
||||||
// in a goroutine to prevent blocking
|
// in a goroutine to prevent blocking
|
||||||
// potentially long rescans.
|
// potentially long rescans.
|
||||||
|
//
|
||||||
|
// TODO(wilmer): add retry logic if rescan fails?
|
||||||
b.wg.Add(1)
|
b.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer b.wg.Done()
|
defer b.wg.Done()
|
||||||
@ -348,7 +319,7 @@ out:
|
|||||||
// begin safely updating the height hint
|
// begin safely updating the height hint
|
||||||
// cache at tip, since any pending
|
// cache at tip, since any pending
|
||||||
// rescans have now completed.
|
// rescans have now completed.
|
||||||
err = b.txConfNotifier.UpdateConfDetails(
|
err = b.txNotifier.UpdateConfDetails(
|
||||||
*msg.TxID, confDetails,
|
*msg.TxID, confDetails,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -398,7 +369,7 @@ out:
|
|||||||
newBestBlock, missedBlocks, err :=
|
newBestBlock, missedBlocks, err :=
|
||||||
chainntnfs.HandleMissedBlocks(
|
chainntnfs.HandleMissedBlocks(
|
||||||
b.chainConn,
|
b.chainConn,
|
||||||
b.txConfNotifier,
|
b.txNotifier,
|
||||||
b.bestBlock,
|
b.bestBlock,
|
||||||
update.blockHeight,
|
update.blockHeight,
|
||||||
true,
|
true,
|
||||||
@ -436,7 +407,7 @@ out:
|
|||||||
}
|
}
|
||||||
|
|
||||||
newBestBlock, err := chainntnfs.RewindChain(
|
newBestBlock, err := chainntnfs.RewindChain(
|
||||||
b.chainConn, b.txConfNotifier, b.bestBlock,
|
b.chainConn, b.txNotifier, b.bestBlock,
|
||||||
update.blockHeight-1,
|
update.blockHeight-1,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -449,60 +420,23 @@ out:
|
|||||||
// partially completed.
|
// partially completed.
|
||||||
b.bestBlock = newBestBlock
|
b.bestBlock = newBestBlock
|
||||||
|
|
||||||
// NOTE: we currently only use txUpdates for mempool spends and
|
|
||||||
// rescan spends. It might get removed entirely in the future.
|
|
||||||
case item := <-b.txUpdates.ChanOut():
|
case item := <-b.txUpdates.ChanOut():
|
||||||
newSpend := item.(*txUpdate)
|
newSpend := item.(*txUpdate)
|
||||||
|
|
||||||
// We only care about notifying on confirmed spends, so
|
// We only care about notifying on confirmed spends, so
|
||||||
// in case this is a mempool spend, we can continue,
|
// if this is a mempool spend, we can ignore it and wait
|
||||||
// and wait for the spend to appear in chain.
|
// for the spend to appear in on-chain.
|
||||||
if newSpend.details == nil {
|
if newSpend.details == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
spendingTx := newSpend.tx
|
tx := newSpend.tx.MsgTx()
|
||||||
|
err := b.txNotifier.ProcessRelevantSpendTx(
|
||||||
// First, check if this transaction spends an output
|
tx, newSpend.details.Height,
|
||||||
// that has an existing spend notification for it.
|
)
|
||||||
for i, txIn := range spendingTx.MsgTx().TxIn {
|
if err != nil {
|
||||||
prevOut := txIn.PreviousOutPoint
|
chainntnfs.Log.Errorf("Unable to process "+
|
||||||
|
"transaction %v: %v", tx.TxHash(), err)
|
||||||
// If this transaction indeed does spend an
|
|
||||||
// output which we have a registered
|
|
||||||
// notification for, then create a spend
|
|
||||||
// summary, finally sending off the details to
|
|
||||||
// the notification subscriber.
|
|
||||||
if clients, ok := b.spendNotifications[prevOut]; ok {
|
|
||||||
spenderSha := newSpend.tx.Hash()
|
|
||||||
spendDetails := &chainntnfs.SpendDetail{
|
|
||||||
SpentOutPoint: &prevOut,
|
|
||||||
SpenderTxHash: spenderSha,
|
|
||||||
SpendingTx: spendingTx.MsgTx(),
|
|
||||||
SpenderInputIndex: uint32(i),
|
|
||||||
}
|
|
||||||
spendDetails.SpendingHeight = newSpend.details.Height
|
|
||||||
|
|
||||||
for _, ntfn := range clients {
|
|
||||||
chainntnfs.Log.Infof("Dispatching "+
|
|
||||||
"confirmed spend "+
|
|
||||||
"notification for "+
|
|
||||||
"outpoint=%v at height %v",
|
|
||||||
ntfn.targetOutpoint,
|
|
||||||
spendDetails.SpendingHeight)
|
|
||||||
ntfn.spendChan <- spendDetails
|
|
||||||
|
|
||||||
// Close spendChan to ensure
|
|
||||||
// that any calls to Cancel
|
|
||||||
// will not block. This is safe
|
|
||||||
// to do since the channel is
|
|
||||||
// buffered, and the message
|
|
||||||
// can still be read by the
|
|
||||||
// receiver.
|
|
||||||
close(ntfn.spendChan)
|
|
||||||
}
|
|
||||||
delete(b.spendNotifications, prevOut)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-b.quit:
|
case <-b.quit:
|
||||||
@ -703,7 +637,7 @@ func (b *BtcdNotifier) handleBlockConnected(epoch chainntnfs.BlockEpoch) error {
|
|||||||
connect: true,
|
connect: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = b.txConfNotifier.ConnectTip(
|
err = b.txNotifier.ConnectTip(
|
||||||
&newBlock.hash, newBlock.height, newBlock.txns,
|
&newBlock.hash, newBlock.height, newBlock.txns,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -713,94 +647,12 @@ func (b *BtcdNotifier) handleBlockConnected(epoch chainntnfs.BlockEpoch) error {
|
|||||||
chainntnfs.Log.Infof("New block: height=%v, sha=%v", epoch.Height,
|
chainntnfs.Log.Infof("New block: height=%v, sha=%v", epoch.Height,
|
||||||
epoch.Hash)
|
epoch.Hash)
|
||||||
|
|
||||||
// Define a helper struct for coalescing the spend notifications we will
|
// We want to set the best block before dispatching notifications so if
|
||||||
// dispatch after trying to commit the spend hints.
|
// any subscribers make queries based on their received block epoch, our
|
||||||
type spendNtfnBatch struct {
|
// state is fully updated in time.
|
||||||
details *chainntnfs.SpendDetail
|
|
||||||
clients map[uint64]*spendNotification
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan over the list of relevant transactions and possibly dispatch
|
|
||||||
// notifications for spends.
|
|
||||||
spendBatches := make(map[wire.OutPoint]spendNtfnBatch)
|
|
||||||
for _, tx := range newBlock.txns {
|
|
||||||
mtx := tx.MsgTx()
|
|
||||||
txSha := mtx.TxHash()
|
|
||||||
|
|
||||||
for i, txIn := range mtx.TxIn {
|
|
||||||
prevOut := txIn.PreviousOutPoint
|
|
||||||
|
|
||||||
// If this transaction indeed does spend an output which
|
|
||||||
// we have a registered notification for, then create a
|
|
||||||
// spend summary, finally sending off the details to the
|
|
||||||
// notification subscriber.
|
|
||||||
clients, ok := b.spendNotifications[prevOut]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
delete(b.spendNotifications, prevOut)
|
|
||||||
|
|
||||||
spendDetails := &chainntnfs.SpendDetail{
|
|
||||||
SpentOutPoint: &prevOut,
|
|
||||||
SpenderTxHash: &txSha,
|
|
||||||
SpendingTx: mtx,
|
|
||||||
SpenderInputIndex: uint32(i),
|
|
||||||
SpendingHeight: int32(newBlock.height),
|
|
||||||
}
|
|
||||||
|
|
||||||
spendBatches[prevOut] = spendNtfnBatch{
|
|
||||||
details: spendDetails,
|
|
||||||
clients: clients,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, we'll update the spend height hint for all of our watched
|
|
||||||
// outpoints that have not been spent yet. This is safe to do as we do
|
|
||||||
// not watch already spent outpoints for spend notifications.
|
|
||||||
ops := make([]wire.OutPoint, 0, len(b.spendNotifications))
|
|
||||||
for op := range b.spendNotifications {
|
|
||||||
ops = append(ops, op)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ops) > 0 {
|
|
||||||
err := b.spendHintCache.CommitSpendHint(
|
|
||||||
uint32(epoch.Height), ops...,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
// The error is not fatal since we are connecting a
|
|
||||||
// block, and advancing the spend hint is an optimistic
|
|
||||||
// optimization.
|
|
||||||
chainntnfs.Log.Errorf("Unable to update spend hint to "+
|
|
||||||
"%d for %v: %v", epoch.Height, ops, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to set the best block before dispatching notifications
|
|
||||||
// so if any subscribers make queries based on their received
|
|
||||||
// block epoch, our state is fully updated in time.
|
|
||||||
b.bestBlock = epoch
|
b.bestBlock = epoch
|
||||||
|
|
||||||
// Next we'll notify any subscribed clients of the block.
|
|
||||||
b.notifyBlockEpochs(int32(newBlock.height), &newBlock.hash)
|
b.notifyBlockEpochs(int32(newBlock.height), &newBlock.hash)
|
||||||
|
|
||||||
// Finally, send off the spend details to the notification subscribers.
|
|
||||||
for _, batch := range spendBatches {
|
|
||||||
for _, ntfn := range batch.clients {
|
|
||||||
chainntnfs.Log.Infof("Dispatching spend "+
|
|
||||||
"notification for outpoint=%v",
|
|
||||||
ntfn.targetOutpoint)
|
|
||||||
|
|
||||||
ntfn.spendChan <- batch.details
|
|
||||||
|
|
||||||
// Close spendChan to ensure that any calls to
|
|
||||||
// Cancel will not block. This is safe to do
|
|
||||||
// since the channel is buffered, and the
|
|
||||||
// message can still be read by the receiver.
|
|
||||||
close(ntfn.spendChan)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -829,28 +681,6 @@ func (b *BtcdNotifier) notifyBlockEpochClient(epochClient *blockEpochRegistratio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// spendNotification couples a target outpoint along with the channel used for
|
|
||||||
// notifications once a spend of the outpoint has been detected.
|
|
||||||
type spendNotification struct {
|
|
||||||
targetOutpoint *wire.OutPoint
|
|
||||||
|
|
||||||
spendChan chan *chainntnfs.SpendDetail
|
|
||||||
|
|
||||||
spendID uint64
|
|
||||||
|
|
||||||
heightHint uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// spendCancel is a message sent to the BtcdNotifier when a client wishes to
|
|
||||||
// cancel an outstanding spend notification that has yet to be dispatched.
|
|
||||||
type spendCancel struct {
|
|
||||||
// op is the target outpoint of the notification to be cancelled.
|
|
||||||
op wire.OutPoint
|
|
||||||
|
|
||||||
// spendID the ID of the notification to cancel.
|
|
||||||
spendID uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterSpendNtfn registers an intent to be notified once the target
|
// RegisterSpendNtfn registers an intent to be notified once the target
|
||||||
// outpoint has been spent by a transaction on-chain. Once a spend of the target
|
// outpoint has been spent by a transaction on-chain. Once a spend of the target
|
||||||
// outpoint has been detected, the details of the spending event will be sent
|
// outpoint has been detected, the details of the spending event will be sent
|
||||||
@ -859,145 +689,131 @@ type spendCancel struct {
|
|||||||
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) {
|
||||||
|
|
||||||
// Before proceeding to register the notification, we'll query our
|
// First, we'll construct a spend notification request and hand it off
|
||||||
// height hint cache to determine whether a better one exists.
|
// to the txNotifier.
|
||||||
if hint, err := b.spendHintCache.QuerySpendHint(*outpoint); err == nil {
|
spendID := atomic.AddUint64(&b.spendClientCounter, 1)
|
||||||
if hint > heightHint {
|
cancel := func() {
|
||||||
chainntnfs.Log.Debugf("Using height hint %d retrieved "+
|
b.txNotifier.CancelSpend(*outpoint, spendID)
|
||||||
"from cache for %v", hint, outpoint)
|
}
|
||||||
heightHint = hint
|
ntfn := &chainntnfs.SpendNtfn{
|
||||||
}
|
SpendID: spendID,
|
||||||
|
OutPoint: *outpoint,
|
||||||
|
PkScript: pkScript,
|
||||||
|
Event: chainntnfs.NewSpendEvent(cancel),
|
||||||
|
HeightHint: heightHint,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct a notification request for the outpoint and send it to the
|
historicalDispatch, err := b.txNotifier.RegisterSpend(ntfn)
|
||||||
// main event loop.
|
|
||||||
ntfn := &spendNotification{
|
|
||||||
targetOutpoint: outpoint,
|
|
||||||
spendChan: make(chan *chainntnfs.SpendDetail, 1),
|
|
||||||
spendID: atomic.AddUint64(&b.spendClientCounter, 1),
|
|
||||||
heightHint: heightHint,
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-b.quit:
|
|
||||||
return nil, ErrChainNotifierShuttingDown
|
|
||||||
case b.notificationRegistry <- ntfn:
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(roasbeef): update btcd rescan logic to also use both
|
|
||||||
if err := b.chainConn.NotifySpent([]*wire.OutPoint{outpoint}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following conditional checks to ensure that when a spend
|
|
||||||
// notification is registered, the output hasn't already been spent. If
|
|
||||||
// the output is no longer in the UTXO set, the chain will be rescanned
|
|
||||||
// from the point where the output was added. The rescan will dispatch
|
|
||||||
// the notification.
|
|
||||||
txOut, err := b.chainConn.GetTxOut(&outpoint.Hash, outpoint.Index, true)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the output is unspent, then we'll write it to the cache with the
|
// If the txNotifier didn't return any details to perform a historical
|
||||||
// given height hint. This allows us to increase the height hint as the
|
// scan of the chain, then we can return early as there's nothing left
|
||||||
// chain extends and the output remains unspent.
|
// for us to do.
|
||||||
|
if historicalDispatch == nil {
|
||||||
|
return ntfn.Event, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll then request the backend to notify us when it has detected the
|
||||||
|
// outpoint as spent.
|
||||||
|
ops := []*wire.OutPoint{outpoint}
|
||||||
|
if err := b.chainConn.NotifySpent(ops); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// In addition to the check above, we'll also check 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(&outpoint.Hash, outpoint.Index, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if txOut != nil {
|
if txOut != nil {
|
||||||
err := b.spendHintCache.CommitSpendHint(heightHint, *outpoint)
|
// We'll let the txNotifier know the outpoint is still unspent
|
||||||
|
// in order to begin updating its spend hint.
|
||||||
|
err := b.txNotifier.UpdateSpendDetails(*outpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// The error is not fatal, so we should not return an
|
return nil, err
|
||||||
// error to the caller.
|
|
||||||
chainntnfs.Log.Error("Unable to update spend hint to "+
|
|
||||||
"%d for %v: %v", heightHint, *outpoint, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Otherwise, we'll determine when the output was spent.
|
|
||||||
//
|
|
||||||
// First, we'll attempt to retrieve the transaction's block hash
|
|
||||||
// using the backend's transaction index.
|
|
||||||
tx, err := b.chainConn.GetRawTransactionVerbose(&outpoint.Hash)
|
|
||||||
if err != nil {
|
|
||||||
// Avoid returning an error if the transaction was not
|
|
||||||
// found to proceed with fallback methods.
|
|
||||||
jsonErr, ok := err.(*btcjson.RPCError)
|
|
||||||
if !ok || jsonErr.Code != btcjson.ErrRPCNoTxInfo {
|
|
||||||
return nil, fmt.Errorf("unable to query for "+
|
|
||||||
"txid %v: %v", outpoint.Hash, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var blockHash *chainhash.Hash
|
return ntfn.Event, nil
|
||||||
if tx != nil && tx.BlockHash != "" {
|
}
|
||||||
// If we're able to retrieve a valid block hash from the
|
|
||||||
// transaction, then we'll use it as our rescan starting
|
|
||||||
// point.
|
|
||||||
blockHash, err = chainhash.NewHashFromStr(tx.BlockHash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Otherwise, we'll attempt to retrieve the hash for the
|
|
||||||
// block at the heightHint.
|
|
||||||
blockHash, err = b.chainConn.GetBlockHash(
|
|
||||||
int64(heightHint),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll only request a rescan if the transaction has actually
|
// Otherwise, we'll determine when the output was spent by scanning the
|
||||||
// been included within a block. Otherwise, we'll encounter an
|
// chain. We'll begin by determining where to start our historical
|
||||||
// error when scanning for blocks. This can happen in the case
|
// rescan.
|
||||||
// of a race condition, wherein the output itself is unspent,
|
startHash, err := b.chainConn.GetBlockHash(
|
||||||
// and only arrives in the mempool after the getxout call.
|
int64(historicalDispatch.StartHeight),
|
||||||
if blockHash != nil {
|
)
|
||||||
ops := []*wire.OutPoint{outpoint}
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get block hash for height "+
|
||||||
|
"%d: %v", historicalDispatch.StartHeight, err)
|
||||||
|
}
|
||||||
|
|
||||||
// In order to ensure that we don't block the caller on
|
// As a minimal optimization, we'll query the backend's transaction
|
||||||
// what may be a long rescan, we'll launch a new
|
// index (if enabled) to determine if we have a better rescan starting
|
||||||
// goroutine to handle the async result of the rescan.
|
// height. We can do this as the GetRawTransaction call will return the
|
||||||
asyncResult := b.chainConn.RescanAsync(
|
// hash of the block it was included in within the chain.
|
||||||
blockHash, nil, ops,
|
tx, err := b.chainConn.GetRawTransactionVerbose(&outpoint.Hash)
|
||||||
)
|
if err != nil {
|
||||||
go func() {
|
// Avoid returning an error if the transaction was not found to
|
||||||
rescanErr := asyncResult.Receive()
|
// proceed with fallback methods.
|
||||||
if rescanErr != nil {
|
jsonErr, ok := err.(*btcjson.RPCError)
|
||||||
chainntnfs.Log.Errorf("Rescan for spend "+
|
if !ok || jsonErr.Code != btcjson.ErrRPCNoTxInfo {
|
||||||
"notification txout(%x) "+
|
return nil, fmt.Errorf("unable to query for "+
|
||||||
"failed: %v", outpoint, rescanErr)
|
"txid %v: %v", outpoint.Hash, err)
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &chainntnfs.SpendEvent{
|
// If the transaction index was enabled, we'll use the block's hash to
|
||||||
Spend: ntfn.spendChan,
|
// retrieve its height and check whether it provides a better starting
|
||||||
Cancel: func() {
|
// point for our rescan.
|
||||||
cancel := &spendCancel{
|
if tx != nil {
|
||||||
op: *outpoint,
|
// If the transaction containing the outpoint hasn't confirmed
|
||||||
spendID: ntfn.spendID,
|
// on-chain, then there's no need to perform a rescan.
|
||||||
}
|
if tx.BlockHash == "" {
|
||||||
|
return ntfn.Event, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Submit spend cancellation to notification dispatcher.
|
blockHash, err := chainhash.NewHashFromStr(tx.BlockHash)
|
||||||
select {
|
if err != nil {
|
||||||
case b.notificationCancels <- cancel:
|
return nil, err
|
||||||
// Cancellation is being handled, drain the spend chan until it is
|
}
|
||||||
// closed before yielding to the caller.
|
blockHeader, err := b.chainConn.GetBlockHeaderVerbose(blockHash)
|
||||||
for {
|
if err != nil {
|
||||||
select {
|
return nil, fmt.Errorf("unable to get header for "+
|
||||||
case _, ok := <-ntfn.spendChan:
|
"block %v: %v", blockHash, err)
|
||||||
if !ok {
|
}
|
||||||
return
|
|
||||||
}
|
if uint32(blockHeader.Height) > historicalDispatch.StartHeight {
|
||||||
case <-b.quit:
|
startHash, err = b.chainConn.GetBlockHash(
|
||||||
return
|
int64(blockHeader.Height),
|
||||||
}
|
)
|
||||||
}
|
if err != nil {
|
||||||
case <-b.quit:
|
return nil, fmt.Errorf("unable to get block "+
|
||||||
|
"hash for height %d: %v",
|
||||||
|
blockHeader.Height, err)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// of the rescan. We purposefully prevent from adding this goroutine to
|
||||||
|
// the WaitGroup as we cannnot wait for a quit signal due to the
|
||||||
|
// asyncResult channel not being exposed.
|
||||||
|
//
|
||||||
|
// TODO(wilmer): add retry logic if rescan fails?
|
||||||
|
asyncResult := b.chainConn.RescanAsync(startHash, nil, ops)
|
||||||
|
go func() {
|
||||||
|
if rescanErr := asyncResult.Receive(); rescanErr != nil {
|
||||||
|
chainntnfs.Log.Errorf("Rescan to determine the spend "+
|
||||||
|
"details of %v failed: %v", outpoint, rescanErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ntfn.Event, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterConfirmationsNtfn registers a notification with BtcdNotifier
|
// RegisterConfirmationsNtfn registers a notification with BtcdNotifier
|
||||||
@ -1019,11 +835,11 @@ func (b *BtcdNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, _ []byte,
|
|||||||
chainntnfs.Log.Infof("New confirmation subscription: "+
|
chainntnfs.Log.Infof("New confirmation subscription: "+
|
||||||
"txid=%v, numconfs=%v", txid, numConfs)
|
"txid=%v, numconfs=%v", txid, numConfs)
|
||||||
|
|
||||||
// Register the conf notification with txconfnotifier. 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.txConfNotifier.Register(ntfn)
|
dispatch, err := b.txNotifier.RegisterConf(ntfn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -11,12 +11,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// UnsafeStart starts the notifier with a specified best height and optional
|
// UnsafeStart starts the notifier with a specified best height and optional
|
||||||
// best hash. Its bestBlock and txConfNotifier are initialized with
|
// best hash. Its bestBlock and txNotifier are initialized with bestHeight and
|
||||||
// bestHeight and optionally bestHash. The parameter generateBlocks is
|
// optionally bestHash. The parameter generateBlocks is necessary for the
|
||||||
// necessary for the bitcoind notifier to ensure we drain all notifications up
|
// bitcoind notifier to ensure we drain all notifications up to syncHeight,
|
||||||
// to syncHeight, since if they are generated ahead of UnsafeStart the chainConn
|
// since if they are generated ahead of UnsafeStart the chainConn may start up
|
||||||
// may start up with an outdated best block and miss sending ntfns. Used for
|
// with an outdated best block and miss sending ntfns. Used for testing.
|
||||||
// testing.
|
|
||||||
func (b *BtcdNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Hash,
|
func (b *BtcdNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Hash,
|
||||||
syncHeight int32, generateBlocks func() error) error {
|
syncHeight int32, generateBlocks func() error) error {
|
||||||
|
|
||||||
@ -29,8 +28,9 @@ func (b *BtcdNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Hash,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.txConfNotifier = chainntnfs.NewTxConfNotifier(
|
b.txNotifier = chainntnfs.NewTxNotifier(
|
||||||
uint32(bestHeight), reorgSafetyLimit, b.confirmHintCache,
|
uint32(bestHeight), reorgSafetyLimit, b.confirmHintCache,
|
||||||
|
b.spendHintCache,
|
||||||
)
|
)
|
||||||
|
|
||||||
b.chainUpdates.Start()
|
b.chainUpdates.Start()
|
||||||
|
@ -127,6 +127,10 @@ func (c *HeightHintCache) CommitSpendHint(height uint32, ops ...wire.OutPoint) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(ops) == 0 {
|
||||||
|
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, ops)
|
||||||
|
|
||||||
return c.db.Batch(func(tx *bolt.Tx) error {
|
return c.db.Batch(func(tx *bolt.Tx) error {
|
||||||
@ -197,6 +201,10 @@ func (c *HeightHintCache) PurgeSpendHint(ops ...wire.OutPoint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(ops) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
Log.Tracef("Removing spend hints for %v", ops)
|
Log.Tracef("Removing spend hints for %v", ops)
|
||||||
|
|
||||||
return c.db.Batch(func(tx *bolt.Tx) error {
|
return c.db.Batch(func(tx *bolt.Tx) error {
|
||||||
@ -228,6 +236,10 @@ func (c *HeightHintCache) CommitConfirmHint(height uint32, txids ...chainhash.Ha
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(txids) == 0 {
|
||||||
|
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, txids)
|
||||||
|
|
||||||
return c.db.Batch(func(tx *bolt.Tx) error {
|
return c.db.Batch(func(tx *bolt.Tx) error {
|
||||||
@ -299,6 +311,10 @@ func (c *HeightHintCache) PurgeConfirmHint(txids ...chainhash.Hash) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(txids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
Log.Tracef("Removing confirm hints for %v", txids)
|
Log.Tracef("Removing confirm hints for %v", txids)
|
||||||
|
|
||||||
return c.db.Batch(func(tx *bolt.Tx) error {
|
return c.db.Batch(func(tx *bolt.Tx) error {
|
||||||
|
@ -159,19 +159,39 @@ 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
|
||||||
// details of the channel's confirmation.
|
// details of the channel's confirmation.
|
||||||
Confirmed chan *TxConfirmation // MUST be buffered.
|
//
|
||||||
|
// NOTE: This channel must be buffered.
|
||||||
|
Confirmed chan *TxConfirmation
|
||||||
|
|
||||||
// Updates is a channel that will sent upon, at every incremental
|
// Updates is a channel that will sent upon, at every incremental
|
||||||
// confirmation, how many confirmations are left to declare the
|
// confirmation, how many confirmations are left to declare the
|
||||||
// transaction as fully confirmed.
|
// transaction as fully confirmed.
|
||||||
Updates chan uint32 // MUST be buffered.
|
//
|
||||||
|
// NOTE: This channel must be buffered with the number of required
|
||||||
|
// confirmations.
|
||||||
|
Updates chan uint32
|
||||||
|
|
||||||
// TODO(roasbeef): all goroutines on ln channel updates should also
|
// TODO(roasbeef): all goroutines on ln channel updates should also
|
||||||
// have a struct chan that's closed if funding gets re-org out. Need
|
// 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
|
// to sync, to request another confirmation event ntfn, then re-open
|
||||||
// channel after confs.
|
// channel after confs.
|
||||||
|
|
||||||
NegativeConf chan int32 // MUST be buffered.
|
// NegativeConf is a channel that will be sent upon if the transaction
|
||||||
|
// confirms, but is later reorged out of the chain. The integer sent
|
||||||
|
// through the channel represents the reorg depth.
|
||||||
|
//
|
||||||
|
// NOTE: This channel must be buffered.
|
||||||
|
NegativeConf chan int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfirmationEvent constructs a new ConfirmationEvent with newly opened
|
||||||
|
// channels.
|
||||||
|
func NewConfirmationEvent(numConfs uint32) *ConfirmationEvent {
|
||||||
|
return &ConfirmationEvent{
|
||||||
|
Confirmed: make(chan *TxConfirmation, 1),
|
||||||
|
Updates: make(chan uint32, numConfs),
|
||||||
|
NegativeConf: make(chan int32, 1),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SpendDetail contains details pertaining to a spent output. This struct itself
|
// SpendDetail contains details pertaining to a spent output. This struct itself
|
||||||
@ -196,7 +216,16 @@ type SpendDetail struct {
|
|||||||
type SpendEvent struct {
|
type SpendEvent struct {
|
||||||
// Spend is a receive only channel which will be sent upon once the
|
// Spend is a receive only channel which will be sent upon once the
|
||||||
// target outpoint has been spent.
|
// target outpoint has been spent.
|
||||||
Spend <-chan *SpendDetail // MUST be buffered.
|
//
|
||||||
|
// NOTE: This channel must be buffered.
|
||||||
|
Spend chan *SpendDetail
|
||||||
|
|
||||||
|
// Reorg is a channel that will be sent upon once we detect the spending
|
||||||
|
// transaction of the outpoint in question has been reorged out of the
|
||||||
|
// chain.
|
||||||
|
//
|
||||||
|
// NOTE: This channel must be buffered.
|
||||||
|
Reorg chan struct{}
|
||||||
|
|
||||||
// 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 that they wish to prematurely abandon their registered spend
|
// case that they wish to prematurely abandon their registered spend
|
||||||
@ -204,6 +233,15 @@ type SpendEvent struct {
|
|||||||
Cancel func()
|
Cancel func()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSpendEvent constructs a new SpendEvent with newly opened channels.
|
||||||
|
func NewSpendEvent(cancel func()) *SpendEvent {
|
||||||
|
return &SpendEvent{
|
||||||
|
Spend: make(chan *SpendDetail, 1),
|
||||||
|
Reorg: make(chan struct{}, 1),
|
||||||
|
Cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// BlockEpoch represents metadata concerning each new block connected to the
|
// BlockEpoch represents metadata concerning each new block connected to the
|
||||||
// main chain.
|
// main chain.
|
||||||
type BlockEpoch struct {
|
type BlockEpoch struct {
|
||||||
@ -225,7 +263,9 @@ type BlockEpoch struct {
|
|||||||
type BlockEpochEvent struct {
|
type BlockEpochEvent struct {
|
||||||
// Epochs is a receive only channel that will be sent upon each time a
|
// Epochs is a receive only channel that will be sent upon each time a
|
||||||
// new block is connected to the end of the main chain.
|
// new block is connected to the end of the main chain.
|
||||||
Epochs <-chan *BlockEpoch // MUST be buffered.
|
//
|
||||||
|
// NOTE: This channel must be buffered.
|
||||||
|
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 that they wish to abandon their registered spend notification.
|
// case that they wish to abandon their registered spend notification.
|
||||||
@ -392,10 +432,10 @@ func GetClientMissedBlocks(chainConn ChainConn, clientBestBlock *BlockEpoch,
|
|||||||
return missedBlocks, nil
|
return missedBlocks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RewindChain handles internal state updates for the notifier's TxConfNotifier
|
// RewindChain handles internal state updates for the notifier's TxNotifier It
|
||||||
// It has no effect if given a height greater than or equal to our current best
|
// has no effect if given a height greater than or equal to our current best
|
||||||
// known height. It returns the new best block for the notifier.
|
// known height. It returns the new best block for the notifier.
|
||||||
func RewindChain(chainConn ChainConn, txConfNotifier *TxConfNotifier,
|
func RewindChain(chainConn ChainConn, txNotifier *TxNotifier,
|
||||||
currBestBlock BlockEpoch, targetHeight int32) (BlockEpoch, error) {
|
currBestBlock BlockEpoch, targetHeight int32) (BlockEpoch, error) {
|
||||||
|
|
||||||
newBestBlock := BlockEpoch{
|
newBestBlock := BlockEpoch{
|
||||||
@ -414,7 +454,7 @@ func RewindChain(chainConn ChainConn, txConfNotifier *TxConfNotifier,
|
|||||||
Log.Infof("Block disconnected from main chain: "+
|
Log.Infof("Block disconnected from main chain: "+
|
||||||
"height=%v, sha=%v", height, newBestBlock.Hash)
|
"height=%v, sha=%v", height, newBestBlock.Hash)
|
||||||
|
|
||||||
err = txConfNotifier.DisconnectTip(uint32(height))
|
err = txNotifier.DisconnectTip(uint32(height))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return newBestBlock, fmt.Errorf("unable to "+
|
return newBestBlock, fmt.Errorf("unable to "+
|
||||||
" disconnect tip for height=%d: %v",
|
" disconnect tip for height=%d: %v",
|
||||||
@ -436,7 +476,7 @@ func RewindChain(chainConn ChainConn, txConfNotifier *TxConfNotifier,
|
|||||||
// returned in case a chain rewind occurs and partially completes before
|
// returned in case a chain rewind occurs and partially completes before
|
||||||
// erroring. In the case where there is no rewind, the notifier's
|
// erroring. In the case where there is no rewind, the notifier's
|
||||||
// current best block is returned.
|
// current best block is returned.
|
||||||
func HandleMissedBlocks(chainConn ChainConn, txConfNotifier *TxConfNotifier,
|
func HandleMissedBlocks(chainConn ChainConn, txNotifier *TxNotifier,
|
||||||
currBestBlock BlockEpoch, newHeight int32,
|
currBestBlock BlockEpoch, newHeight int32,
|
||||||
backendStoresReorgs bool) (BlockEpoch, []BlockEpoch, error) {
|
backendStoresReorgs bool) (BlockEpoch, []BlockEpoch, error) {
|
||||||
|
|
||||||
@ -462,7 +502,7 @@ func HandleMissedBlocks(chainConn ChainConn, txConfNotifier *TxConfNotifier,
|
|||||||
"common ancestor: %v", err)
|
"common ancestor: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
currBestBlock, err = RewindChain(chainConn, txConfNotifier,
|
currBestBlock, err = RewindChain(chainConn, txNotifier,
|
||||||
currBestBlock, startingHeight)
|
currBestBlock, startingHeight)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return currBestBlock, nil, fmt.Errorf("unable to "+
|
return currBestBlock, nil, fmt.Errorf("unable to "+
|
||||||
|
@ -232,6 +232,8 @@ func checkNotificationFields(ntfn *chainntnfs.SpendDetail,
|
|||||||
outpoint *wire.OutPoint, spenderSha *chainhash.Hash,
|
outpoint *wire.OutPoint, spenderSha *chainhash.Hash,
|
||||||
height int32, t *testing.T) {
|
height int32, t *testing.T) {
|
||||||
|
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
if *ntfn.SpentOutPoint != *outpoint {
|
if *ntfn.SpentOutPoint != *outpoint {
|
||||||
t.Fatalf("ntfn includes wrong output, reports "+
|
t.Fatalf("ntfn includes wrong output, reports "+
|
||||||
"%v instead of %v",
|
"%v instead of %v",
|
||||||
@ -756,6 +758,8 @@ func testSpendBeforeNtfnRegistration(miner *rpctest.Harness,
|
|||||||
// already happened. The notifier should dispatch a spend notification
|
// already happened. The notifier should dispatch a spend notification
|
||||||
// immediately.
|
// immediately.
|
||||||
checkSpends := func() {
|
checkSpends := func() {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
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++ {
|
||||||
@ -1108,6 +1112,156 @@ func testReorgConf(miner *rpctest.Harness, notifier chainntnfs.TestChainNotifier
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testReorgSpend ensures that the different ChainNotifier implementations
|
||||||
|
// correctly handle outpoints whose spending transaction has been reorged out of
|
||||||
|
// the chain.
|
||||||
|
func testReorgSpend(miner *rpctest.Harness,
|
||||||
|
notifier chainntnfs.TestChainNotifier, t *testing.T) {
|
||||||
|
|
||||||
|
// We'll start by creating an output and registering a spend
|
||||||
|
// notification for it.
|
||||||
|
outpoint, pkScript := chainntnfs.CreateSpendableOutput(t, miner)
|
||||||
|
_, currentHeight, err := miner.Node.GetBestBlock()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to retrieve current height: %v", err)
|
||||||
|
}
|
||||||
|
spendIntent, err := notifier.RegisterSpendNtfn(
|
||||||
|
outpoint, pkScript, uint32(currentHeight),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to register for spend: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up a new miner that we can use to cause a reorg.
|
||||||
|
miner2, err := rpctest.New(chainntnfs.NetParams, nil, []string{"--txindex"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create mining node: %v", err)
|
||||||
|
}
|
||||||
|
if err := miner2.SetUp(false, 0); err != nil {
|
||||||
|
t.Fatalf("unable to set up mining node: %v", err)
|
||||||
|
}
|
||||||
|
defer miner2.TearDown()
|
||||||
|
|
||||||
|
// We start by connecting the new miner to our original miner, in order
|
||||||
|
// to have a consistent view of the chain from both miners. They should
|
||||||
|
// be on the same block height.
|
||||||
|
if err := rpctest.ConnectNode(miner, miner2); err != nil {
|
||||||
|
t.Fatalf("unable to connect miners: %v", err)
|
||||||
|
}
|
||||||
|
nodeSlice := []*rpctest.Harness{miner, miner2}
|
||||||
|
if err := rpctest.JoinNodes(nodeSlice, rpctest.Blocks); err != nil {
|
||||||
|
t.Fatalf("unable to sync miners: %v", err)
|
||||||
|
}
|
||||||
|
_, minerHeight1, err := miner.Node.GetBestBlock()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get miner1's current height: %v", err)
|
||||||
|
}
|
||||||
|
_, minerHeight2, err := miner2.Node.GetBestBlock()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get miner2's current height: %v", err)
|
||||||
|
}
|
||||||
|
if minerHeight1 != minerHeight2 {
|
||||||
|
t.Fatalf("expected both miners to be on the same height: "+
|
||||||
|
"%v vs %v", minerHeight1, minerHeight2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We disconnect the two nodes, such that we can start mining on them
|
||||||
|
// individually without the other one learning about the new blocks.
|
||||||
|
err = miner.Node.AddNode(miner2.P2PAddress(), rpcclient.ANRemove)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to disconnect miners: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Craft the spending transaction for the outpoint created above and
|
||||||
|
// confirm it under the chain of the original miner.
|
||||||
|
spendTx := chainntnfs.CreateSpendTx(t, outpoint, pkScript)
|
||||||
|
spendTxHash, err := miner.Node.SendRawTransaction(spendTx, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to broadcast spend tx: %v", err)
|
||||||
|
}
|
||||||
|
if err := chainntnfs.WaitForMempoolTx(miner, spendTxHash); err != nil {
|
||||||
|
t.Fatalf("spend tx not relayed to miner: %v", err)
|
||||||
|
}
|
||||||
|
const numBlocks = 1
|
||||||
|
if _, err := miner.Node.Generate(numBlocks); err != nil {
|
||||||
|
t.Fatalf("unable to generate blocks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should see a spend notification dispatched with the correct spend
|
||||||
|
// details.
|
||||||
|
select {
|
||||||
|
case spendDetails := <-spendIntent.Spend:
|
||||||
|
checkNotificationFields(
|
||||||
|
spendDetails, outpoint, spendTxHash,
|
||||||
|
currentHeight+numBlocks, t,
|
||||||
|
)
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("expected spend notification to be dispatched")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, with the other miner, we'll generate one more block than the
|
||||||
|
// other miner and connect them to cause a reorg.
|
||||||
|
if _, err := miner2.Node.Generate(numBlocks + 1); err != nil {
|
||||||
|
t.Fatalf("unable to generate blocks: %v", err)
|
||||||
|
}
|
||||||
|
if err := rpctest.ConnectNode(miner, miner2); err != nil {
|
||||||
|
t.Fatalf("unable to connect miners: %v", err)
|
||||||
|
}
|
||||||
|
nodeSlice = []*rpctest.Harness{miner2, miner}
|
||||||
|
if err := rpctest.JoinNodes(nodeSlice, rpctest.Blocks); err != nil {
|
||||||
|
t.Fatalf("unable to sync miners: %v", err)
|
||||||
|
}
|
||||||
|
_, minerHeight1, err = miner.Node.GetBestBlock()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get miner1's current height: %v", err)
|
||||||
|
}
|
||||||
|
_, minerHeight2, err = miner2.Node.GetBestBlock()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get miner2's current height: %v", err)
|
||||||
|
}
|
||||||
|
if minerHeight1 != minerHeight2 {
|
||||||
|
t.Fatalf("expected both miners to be on the same height: "+
|
||||||
|
"%v vs %v", minerHeight1, minerHeight2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should receive a reorg notification.
|
||||||
|
select {
|
||||||
|
case _, ok := <-spendIntent.Reorg:
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("unexpected reorg channel closed")
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("expected to receive reorg notification")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that both miners are on the same chain, we'll confirm the
|
||||||
|
// spending transaction of the outpoint and receive a notification for
|
||||||
|
// it.
|
||||||
|
if _, err = miner2.Node.SendRawTransaction(spendTx, true); err != nil {
|
||||||
|
t.Fatalf("unable to broadcast spend tx: %v", err)
|
||||||
|
}
|
||||||
|
if err := chainntnfs.WaitForMempoolTx(miner, spendTxHash); err != nil {
|
||||||
|
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 {
|
||||||
|
t.Fatalf("unable to generate single block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case spendDetails := <-spendIntent.Spend:
|
||||||
|
checkNotificationFields(
|
||||||
|
spendDetails, outpoint, spendTxHash,
|
||||||
|
currentHeight+numBlocks, t,
|
||||||
|
)
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("expected spend notification to be dispatched")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// testCatchUpClientOnMissedBlocks tests the case of multiple registered client
|
// testCatchUpClientOnMissedBlocks tests the case of multiple registered client
|
||||||
// receiving historical block epoch notifications due to their best known block
|
// receiving historical block epoch notifications due to their best known block
|
||||||
// being out of date.
|
// being out of date.
|
||||||
@ -1134,11 +1288,11 @@ func testCatchUpClientOnMissedBlocks(miner *rpctest.Harness,
|
|||||||
// the notifier's best block is at the tip of the chain. If it isn't, the
|
// the notifier's best block is at the tip of the chain. If it isn't, the
|
||||||
// client may not receive all historical notifications.
|
// client may not receive all historical notifications.
|
||||||
bestHeight := outdatedHeight + numBlocks
|
bestHeight := outdatedHeight + numBlocks
|
||||||
if err := notifier.UnsafeStart(
|
err = notifier.UnsafeStart(bestHeight, nil, bestHeight, generateBlocks)
|
||||||
bestHeight, nil, bestHeight, generateBlocks); err != nil {
|
if err != nil {
|
||||||
|
t.Fatalf("unable to unsafe start the notifier: %v", err)
|
||||||
t.Fatalf("Unable to unsafe start the notifier: %v", err)
|
|
||||||
}
|
}
|
||||||
|
defer notifier.Stop()
|
||||||
|
|
||||||
// Create numClients clients whose best known block is 10 blocks behind
|
// Create numClients clients whose best known block is 10 blocks behind
|
||||||
// the tip of the chain. We expect each client to receive numBlocks
|
// the tip of the chain. We expect each client to receive numBlocks
|
||||||
@ -1223,11 +1377,13 @@ func testCatchUpOnMissedBlocks(miner *rpctest.Harness,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Next, start the notifier with outdated best block information.
|
// Next, start the notifier with outdated best block information.
|
||||||
if err := notifier.UnsafeStart(bestHeight,
|
err = notifier.UnsafeStart(
|
||||||
nil, bestHeight+numBlocks, generateBlocks); err != nil {
|
bestHeight, nil, bestHeight+numBlocks, generateBlocks,
|
||||||
|
)
|
||||||
t.Fatalf("Unable to unsafe start the notifier: %v", err)
|
if err != nil {
|
||||||
|
t.Fatalf("unable to unsafe start the notifier: %v", err)
|
||||||
}
|
}
|
||||||
|
defer notifier.Stop()
|
||||||
|
|
||||||
// Create numClients clients who will listen for block notifications.
|
// Create numClients clients who will listen for block notifications.
|
||||||
clients := make([]*chainntnfs.BlockEpochEvent, 0, numClients)
|
clients := make([]*chainntnfs.BlockEpochEvent, 0, numClients)
|
||||||
@ -1396,11 +1552,13 @@ func testCatchUpOnMissedBlocksWithReorg(miner1 *rpctest.Harness,
|
|||||||
// shorter chain, to test that the notifier correctly rewinds to
|
// shorter chain, to test that the notifier correctly rewinds to
|
||||||
// the common ancestor between the two chains.
|
// the common ancestor between the two chains.
|
||||||
syncHeight := nodeHeight1 + numBlocks + 1
|
syncHeight := nodeHeight1 + numBlocks + 1
|
||||||
if err := notifier.UnsafeStart(nodeHeight1+numBlocks,
|
err = notifier.UnsafeStart(
|
||||||
blocks[numBlocks-1], syncHeight, nil); err != nil {
|
nodeHeight1+numBlocks, blocks[numBlocks-1], syncHeight, nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
t.Fatalf("Unable to unsafe start the notifier: %v", err)
|
t.Fatalf("Unable to unsafe start the notifier: %v", err)
|
||||||
}
|
}
|
||||||
|
defer notifier.Stop()
|
||||||
|
|
||||||
// Create numClients clients who will listen for block notifications.
|
// Create numClients clients who will listen for block notifications.
|
||||||
clients := make([]*chainntnfs.BlockEpochEvent, 0, numClients)
|
clients := make([]*chainntnfs.BlockEpochEvent, 0, numClients)
|
||||||
@ -1545,6 +1703,10 @@ var ntfnTests = []testCase{
|
|||||||
name: "reorg conf",
|
name: "reorg conf",
|
||||||
test: testReorgConf,
|
test: testReorgConf,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "reorg spend",
|
||||||
|
test: testReorgSpend,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var blockCatchupTests = []blockCatchupTestCase{
|
var blockCatchupTests = []blockCatchupTestCase{
|
||||||
@ -1680,9 +1842,6 @@ func TestInterfaces(t *testing.T) {
|
|||||||
success := t.Run(testName, func(t *testing.T) {
|
success := t.Run(testName, func(t *testing.T) {
|
||||||
blockCatchupTest.test(miner, notifier, t)
|
blockCatchupTest.test(miner, notifier, t)
|
||||||
})
|
})
|
||||||
|
|
||||||
notifier.Stop()
|
|
||||||
|
|
||||||
if !success {
|
if !success {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -68,9 +68,7 @@ type NeutrinoNotifier struct {
|
|||||||
notificationCancels chan interface{}
|
notificationCancels chan interface{}
|
||||||
notificationRegistry chan interface{}
|
notificationRegistry chan interface{}
|
||||||
|
|
||||||
spendNotifications map[wire.OutPoint]map[uint64]*spendNotification
|
txNotifier *chainntnfs.TxNotifier
|
||||||
|
|
||||||
txConfNotifier *chainntnfs.TxConfNotifier
|
|
||||||
|
|
||||||
blockEpochClients map[uint64]*blockEpochRegistration
|
blockEpochClients map[uint64]*blockEpochRegistration
|
||||||
|
|
||||||
@ -109,8 +107,6 @@ func New(node *neutrino.ChainService, spendHintCache chainntnfs.SpendHintCache,
|
|||||||
|
|
||||||
blockEpochClients: make(map[uint64]*blockEpochRegistration),
|
blockEpochClients: make(map[uint64]*blockEpochRegistration),
|
||||||
|
|
||||||
spendNotifications: make(map[wire.OutPoint]map[uint64]*spendNotification),
|
|
||||||
|
|
||||||
p2pNode: node,
|
p2pNode: node,
|
||||||
|
|
||||||
rescanErr: make(chan error),
|
rescanErr: make(chan error),
|
||||||
@ -162,8 +158,9 @@ func (n *NeutrinoNotifier) Start() error {
|
|||||||
neutrino.WatchInputs(zeroInput),
|
neutrino.WatchInputs(zeroInput),
|
||||||
}
|
}
|
||||||
|
|
||||||
n.txConfNotifier = chainntnfs.NewTxConfNotifier(
|
n.txNotifier = chainntnfs.NewTxNotifier(
|
||||||
n.bestHeight, reorgSafetyLimit, n.confirmHintCache,
|
n.bestHeight, reorgSafetyLimit, n.confirmHintCache,
|
||||||
|
n.spendHintCache,
|
||||||
)
|
)
|
||||||
|
|
||||||
n.chainConn = &NeutrinoChainConn{n.p2pNode}
|
n.chainConn = &NeutrinoChainConn{n.p2pNode}
|
||||||
@ -195,18 +192,13 @@ func (n *NeutrinoNotifier) Stop() error {
|
|||||||
|
|
||||||
// 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.
|
||||||
for _, spendClients := range n.spendNotifications {
|
|
||||||
for _, spendClient := range spendClients {
|
|
||||||
close(spendClient.spendChan)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, epochClient := range n.blockEpochClients {
|
for _, epochClient := range n.blockEpochClients {
|
||||||
close(epochClient.cancelChan)
|
close(epochClient.cancelChan)
|
||||||
epochClient.wg.Wait()
|
epochClient.wg.Wait()
|
||||||
|
|
||||||
close(epochClient.epochChan)
|
close(epochClient.epochChan)
|
||||||
}
|
}
|
||||||
n.txConfNotifier.TearDown()
|
n.txNotifier.TearDown()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -225,6 +217,14 @@ type filteredBlock struct {
|
|||||||
connect bool
|
connect bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rescanFilterUpdate represents a request that will be sent to the
|
||||||
|
// notificaionRegistry in order to prevent race conditions between the filter
|
||||||
|
// update and new block notifications.
|
||||||
|
type rescanFilterUpdate struct {
|
||||||
|
updateOptions []neutrino.UpdateOption
|
||||||
|
errChan chan error
|
||||||
|
}
|
||||||
|
|
||||||
// onFilteredBlockConnected is a callback which is executed each a new block is
|
// onFilteredBlockConnected is a callback which is executed each a new block is
|
||||||
// connected to the end of the main chain.
|
// connected to the end of the main chain.
|
||||||
func (n *NeutrinoNotifier) onFilteredBlockConnected(height int32,
|
func (n *NeutrinoNotifier) onFilteredBlockConnected(height int32,
|
||||||
@ -263,19 +263,6 @@ out:
|
|||||||
select {
|
select {
|
||||||
case cancelMsg := <-n.notificationCancels:
|
case cancelMsg := <-n.notificationCancels:
|
||||||
switch msg := cancelMsg.(type) {
|
switch msg := cancelMsg.(type) {
|
||||||
case *spendCancel:
|
|
||||||
chainntnfs.Log.Infof("Cancelling spend "+
|
|
||||||
"notification for out_point=%v, "+
|
|
||||||
"spend_id=%v", msg.op, msg.spendID)
|
|
||||||
|
|
||||||
// Before we attempt to close the spendChan,
|
|
||||||
// ensure that the notification hasn't already
|
|
||||||
// yet been dispatched.
|
|
||||||
if outPointClients, ok := n.spendNotifications[msg.op]; ok {
|
|
||||||
close(outPointClients[msg.spendID].spendChan)
|
|
||||||
delete(n.spendNotifications[msg.op], msg.spendID)
|
|
||||||
}
|
|
||||||
|
|
||||||
case *epochCancel:
|
case *epochCancel:
|
||||||
chainntnfs.Log.Infof("Cancelling epoch "+
|
chainntnfs.Log.Infof("Cancelling epoch "+
|
||||||
"notification, epoch_id=%v", msg.epochID)
|
"notification, epoch_id=%v", msg.epochID)
|
||||||
@ -303,21 +290,9 @@ out:
|
|||||||
|
|
||||||
case registerMsg := <-n.notificationRegistry:
|
case registerMsg := <-n.notificationRegistry:
|
||||||
switch msg := registerMsg.(type) {
|
switch msg := registerMsg.(type) {
|
||||||
case *spendNotification:
|
|
||||||
chainntnfs.Log.Infof("New spend subscription: "+
|
|
||||||
"utxo=%v, height_hint=%v",
|
|
||||||
msg.targetOutpoint, msg.heightHint)
|
|
||||||
op := *msg.targetOutpoint
|
|
||||||
|
|
||||||
if _, ok := n.spendNotifications[op]; !ok {
|
|
||||||
n.spendNotifications[op] = make(map[uint64]*spendNotification)
|
|
||||||
}
|
|
||||||
n.spendNotifications[op][msg.spendID] = msg
|
|
||||||
|
|
||||||
case *chainntnfs.HistoricalConfDispatch:
|
case *chainntnfs.HistoricalConfDispatch:
|
||||||
// Look up whether the transaction is already
|
// We'll start a historical rescan chain of the
|
||||||
// included in the active chain. We'll do this
|
// chain asynchronously to prevent blocking
|
||||||
// in a goroutine to prevent blocking
|
|
||||||
// potentially long rescans.
|
// potentially long rescans.
|
||||||
n.wg.Add(1)
|
n.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
@ -331,18 +306,6 @@ out:
|
|||||||
chainntnfs.Log.Error(err)
|
chainntnfs.Log.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We'll map the script into an address
|
|
||||||
// type so we can instruct neutrino to
|
|
||||||
// match if the transaction containing
|
|
||||||
// the script is found in a block.
|
|
||||||
params := n.p2pNode.ChainParams()
|
|
||||||
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
|
|
||||||
msg.PkScript, ¶ms,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
chainntnfs.Log.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the historical dispatch finished
|
// If the historical dispatch finished
|
||||||
// without error, we will invoke
|
// without error, we will invoke
|
||||||
// UpdateConfDetails even if none were
|
// UpdateConfDetails even if none were
|
||||||
@ -350,31 +313,12 @@ out:
|
|||||||
// begin safely updating the height hint
|
// begin safely updating the height hint
|
||||||
// cache at tip, since any pending
|
// cache at tip, since any pending
|
||||||
// rescans have now completed.
|
// rescans have now completed.
|
||||||
err = n.txConfNotifier.UpdateConfDetails(
|
err = n.txNotifier.UpdateConfDetails(
|
||||||
*msg.TxID, confDetails,
|
*msg.TxID, confDetails,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
chainntnfs.Log.Error(err)
|
chainntnfs.Log.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if confDetails != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we can't fully dispatch
|
|
||||||
// confirmation, then we'll update our
|
|
||||||
// filter so we can be notified of its
|
|
||||||
// future initial confirmation.
|
|
||||||
rescanUpdate := []neutrino.UpdateOption{
|
|
||||||
neutrino.AddAddrs(addrs...),
|
|
||||||
neutrino.Rewind(msg.EndHeight),
|
|
||||||
neutrino.DisableDisconnectedNtfns(true),
|
|
||||||
}
|
|
||||||
err = n.chainView.Update(rescanUpdate...)
|
|
||||||
if err != nil {
|
|
||||||
chainntnfs.Log.Errorf("Unable to update rescan: %v",
|
|
||||||
err)
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
case *blockEpochRegistration:
|
case *blockEpochRegistration:
|
||||||
@ -399,6 +343,14 @@ out:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
msg.errorChan <- nil
|
msg.errorChan <- nil
|
||||||
|
|
||||||
|
case *rescanFilterUpdate:
|
||||||
|
err := n.chainView.Update(msg.updateOptions...)
|
||||||
|
if err != nil {
|
||||||
|
chainntnfs.Log.Errorf("Unable to "+
|
||||||
|
"update rescan filter: %v", err)
|
||||||
|
}
|
||||||
|
msg.errChan <- err
|
||||||
}
|
}
|
||||||
|
|
||||||
case item := <-n.chainUpdates.ChanOut():
|
case item := <-n.chainUpdates.ChanOut():
|
||||||
@ -426,7 +378,7 @@ out:
|
|||||||
_, missedBlocks, err :=
|
_, missedBlocks, err :=
|
||||||
chainntnfs.HandleMissedBlocks(
|
chainntnfs.HandleMissedBlocks(
|
||||||
n.chainConn,
|
n.chainConn,
|
||||||
n.txConfNotifier,
|
n.txNotifier,
|
||||||
bestBlock,
|
bestBlock,
|
||||||
int32(update.height),
|
int32(update.height),
|
||||||
false,
|
false,
|
||||||
@ -482,7 +434,7 @@ out:
|
|||||||
Hash: hash,
|
Hash: hash,
|
||||||
}
|
}
|
||||||
newBestBlock, err := chainntnfs.RewindChain(
|
newBestBlock, err := chainntnfs.RewindChain(
|
||||||
n.chainConn, n.txConfNotifier, notifierBestBlock,
|
n.chainConn, n.txNotifier, notifierBestBlock,
|
||||||
int32(update.height-1),
|
int32(update.height-1),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -593,7 +545,7 @@ func (n *NeutrinoNotifier) handleBlockConnected(newBlock *filteredBlock) error {
|
|||||||
// First process the block for our internal state. A new block has
|
// First process the block for our internal state. A new block has
|
||||||
// been connected to the main chain. Send out any N confirmation
|
// been connected to the main chain. Send out any N confirmation
|
||||||
// notifications which may have been triggered by this new block.
|
// notifications which may have been triggered by this new block.
|
||||||
err := n.txConfNotifier.ConnectTip(
|
err := n.txNotifier.ConnectTip(
|
||||||
&newBlock.hash, newBlock.height, newBlock.txns,
|
&newBlock.hash, newBlock.height, newBlock.txns,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -603,68 +555,6 @@ func (n *NeutrinoNotifier) handleBlockConnected(newBlock *filteredBlock) error {
|
|||||||
chainntnfs.Log.Infof("New block: height=%v, sha=%v", newBlock.height,
|
chainntnfs.Log.Infof("New block: height=%v, sha=%v", newBlock.height,
|
||||||
newBlock.hash)
|
newBlock.hash)
|
||||||
|
|
||||||
// Create a helper struct for coalescing spend notifications triggered
|
|
||||||
// by this block.
|
|
||||||
type spendNtfnBatch struct {
|
|
||||||
details *chainntnfs.SpendDetail
|
|
||||||
clients map[uint64]*spendNotification
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan over the list of relevant transactions and assemble the
|
|
||||||
// possible spend notifications we need to dispatch.
|
|
||||||
spendBatches := make(map[wire.OutPoint]spendNtfnBatch)
|
|
||||||
for _, tx := range newBlock.txns {
|
|
||||||
mtx := tx.MsgTx()
|
|
||||||
txSha := mtx.TxHash()
|
|
||||||
|
|
||||||
for i, txIn := range mtx.TxIn {
|
|
||||||
prevOut := txIn.PreviousOutPoint
|
|
||||||
|
|
||||||
// If this transaction indeed does spend an output which
|
|
||||||
// we have a registered notification for, then create a
|
|
||||||
// spend summary and add it to our batch of spend
|
|
||||||
// notifications to be delivered.
|
|
||||||
clients, ok := n.spendNotifications[prevOut]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
delete(n.spendNotifications, prevOut)
|
|
||||||
|
|
||||||
spendDetails := &chainntnfs.SpendDetail{
|
|
||||||
SpentOutPoint: &prevOut,
|
|
||||||
SpenderTxHash: &txSha,
|
|
||||||
SpendingTx: mtx,
|
|
||||||
SpenderInputIndex: uint32(i),
|
|
||||||
SpendingHeight: int32(newBlock.height),
|
|
||||||
}
|
|
||||||
|
|
||||||
spendBatches[prevOut] = spendNtfnBatch{
|
|
||||||
details: spendDetails,
|
|
||||||
clients: clients,
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now, we'll update the spend height hint for all of our watched
|
|
||||||
// outpoints that have not been spent yet. This is safe to do as we do
|
|
||||||
// not watch already spent outpoints for spend notifications.
|
|
||||||
ops := make([]wire.OutPoint, 0, len(n.spendNotifications))
|
|
||||||
for op := range n.spendNotifications {
|
|
||||||
ops = append(ops, op)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ops) > 0 {
|
|
||||||
err := n.spendHintCache.CommitSpendHint(newBlock.height, ops...)
|
|
||||||
if err != nil {
|
|
||||||
// The error is not fatal since we are connecting a
|
|
||||||
// block, and advancing the spend hint is an optimistic
|
|
||||||
// optimization.
|
|
||||||
chainntnfs.Log.Errorf("Unable to update spend hint to "+
|
|
||||||
"%d for %v: %v", newBlock.height, ops, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to set the best block before dispatching notifications
|
// We want to set the best block before dispatching notifications
|
||||||
// so if any subscribers make queries based on their received
|
// so if any subscribers make queries based on their received
|
||||||
// block epoch, our state is fully updated in time.
|
// block epoch, our state is fully updated in time.
|
||||||
@ -674,23 +564,6 @@ func (n *NeutrinoNotifier) handleBlockConnected(newBlock *filteredBlock) error {
|
|||||||
// of the block.
|
// of the block.
|
||||||
n.notifyBlockEpochs(int32(newBlock.height), &newBlock.hash)
|
n.notifyBlockEpochs(int32(newBlock.height), &newBlock.hash)
|
||||||
|
|
||||||
// Finally, send off the spend details to the notification subscribers.
|
|
||||||
for _, batch := range spendBatches {
|
|
||||||
for _, ntfn := range batch.clients {
|
|
||||||
chainntnfs.Log.Infof("Dispatching spend "+
|
|
||||||
"notification for outpoint=%v",
|
|
||||||
ntfn.targetOutpoint)
|
|
||||||
|
|
||||||
ntfn.spendChan <- batch.details
|
|
||||||
|
|
||||||
// Close spendChan to ensure that any calls to
|
|
||||||
// Cancel will not block. This is safe to do
|
|
||||||
// since the channel is buffered, and the
|
|
||||||
// message can still be read by the receiver.
|
|
||||||
close(ntfn.spendChan)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -737,28 +610,6 @@ func (n *NeutrinoNotifier) notifyBlockEpochClient(epochClient *blockEpochRegistr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// spendNotification couples a target outpoint along with the channel used for
|
|
||||||
// notifications once a spend of the outpoint has been detected.
|
|
||||||
type spendNotification struct {
|
|
||||||
targetOutpoint *wire.OutPoint
|
|
||||||
|
|
||||||
spendChan chan *chainntnfs.SpendDetail
|
|
||||||
|
|
||||||
spendID uint64
|
|
||||||
|
|
||||||
heightHint uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// spendCancel is a message sent to the NeutrinoNotifier when a client wishes
|
|
||||||
// to cancel an outstanding spend notification that has yet to be dispatched.
|
|
||||||
type spendCancel struct {
|
|
||||||
// op is the target outpoint of the notification to be cancelled.
|
|
||||||
op wire.OutPoint
|
|
||||||
|
|
||||||
// spendID the ID of the notification to cancel.
|
|
||||||
spendID uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterSpendNtfn registers an intent to be notified once the target
|
// 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 has been spent by a transaction on-chain. Once a spend of the
|
||||||
// target outpoint has been detected, the details of the spending event will be
|
// target outpoint has been detected, the details of the spending event will be
|
||||||
@ -766,86 +617,91 @@ type spendCancel struct {
|
|||||||
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) {
|
||||||
|
|
||||||
n.heightMtx.RLock()
|
// First, we'll construct a spend notification request and hand it off
|
||||||
currentHeight := n.bestHeight
|
// to the txNotifier.
|
||||||
n.heightMtx.RUnlock()
|
spendID := atomic.AddUint64(&n.spendClientCounter, 1)
|
||||||
|
cancel := func() {
|
||||||
// Before proceeding to register the notification, we'll query our
|
n.txNotifier.CancelSpend(*outpoint, spendID)
|
||||||
// height hint cache to determine whether a better one exists.
|
}
|
||||||
if hint, err := n.spendHintCache.QuerySpendHint(*outpoint); err == nil {
|
ntfn := &chainntnfs.SpendNtfn{
|
||||||
if hint > heightHint {
|
SpendID: spendID,
|
||||||
chainntnfs.Log.Debugf("Using height hint %d retrieved "+
|
OutPoint: *outpoint,
|
||||||
"from cache for %v", hint, outpoint)
|
Event: chainntnfs.NewSpendEvent(cancel),
|
||||||
heightHint = hint
|
HeightHint: heightHint,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct a notification request for the outpoint. We'll defer
|
historicalDispatch, err := n.txNotifier.RegisterSpend(ntfn)
|
||||||
// sending it to the main event loop until after we've guaranteed that
|
if err != nil {
|
||||||
// the outpoint has not been spent.
|
return nil, err
|
||||||
ntfn := &spendNotification{
|
|
||||||
targetOutpoint: outpoint,
|
|
||||||
spendChan: make(chan *chainntnfs.SpendDetail, 1),
|
|
||||||
spendID: atomic.AddUint64(&n.spendClientCounter, 1),
|
|
||||||
heightHint: heightHint,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
spendEvent := &chainntnfs.SpendEvent{
|
// If the txNotifier didn't return any details to perform a historical
|
||||||
Spend: ntfn.spendChan,
|
// scan of the chain, then we can return early as there's nothing left
|
||||||
Cancel: func() {
|
// for us to do.
|
||||||
cancel := &spendCancel{
|
if historicalDispatch == nil {
|
||||||
op: *outpoint,
|
return ntfn.Event, nil
|
||||||
spendID: ntfn.spendID,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit spend cancellation to notification dispatcher.
|
|
||||||
select {
|
|
||||||
case n.notificationCancels <- cancel:
|
|
||||||
// Cancellation is being handled, drain the
|
|
||||||
// spend chan until it is closed before yielding
|
|
||||||
// to the caller.
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case _, ok := <-ntfn.spendChan:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-n.quit:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case <-n.quit:
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that neutrino is caught up to the height hint before we
|
|
||||||
// attempt to fetch the utxo from the chain. If we're behind, then we
|
|
||||||
// may miss a notification dispatch.
|
|
||||||
for {
|
|
||||||
n.heightMtx.RLock()
|
|
||||||
currentHeight = n.bestHeight
|
|
||||||
n.heightMtx.RUnlock()
|
|
||||||
|
|
||||||
if currentHeight < heightHint {
|
|
||||||
time.Sleep(time.Millisecond * 200)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To determine whether this outpoint has been spent on-chain, we'll
|
||||||
|
// update our filter to watch for the transaction at tip and we'll also
|
||||||
|
// dispatch a historical rescan to determine if it has been spent in the
|
||||||
|
// past.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// 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: *outpoint,
|
||||||
PkScript: pkScript,
|
PkScript: pkScript,
|
||||||
}
|
}
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
select {
|
||||||
|
case n.notificationRegistry <- &rescanFilterUpdate{
|
||||||
|
updateOptions: []neutrino.UpdateOption{
|
||||||
|
neutrino.AddInputs(inputToWatch),
|
||||||
|
neutrino.Rewind(historicalDispatch.EndHeight),
|
||||||
|
neutrino.DisableDisconnectedNtfns(true),
|
||||||
|
},
|
||||||
|
errChan: errChan,
|
||||||
|
}:
|
||||||
|
case <-n.quit:
|
||||||
|
return nil, ErrChainNotifierShuttingDown
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err = <-errChan:
|
||||||
|
case <-n.quit:
|
||||||
|
return nil, ErrChainNotifierShuttingDown
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to update filter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// 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
|
||||||
|
// notification dispatch.
|
||||||
|
for {
|
||||||
|
n.heightMtx.RLock()
|
||||||
|
currentHeight := n.bestHeight
|
||||||
|
n.heightMtx.RUnlock()
|
||||||
|
|
||||||
|
if currentHeight >= historicalDispatch.StartHeight {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond * 200)
|
||||||
|
}
|
||||||
|
|
||||||
// Before sending off the notification request, we'll attempt to see if
|
|
||||||
// this output is still spent or not at this point in the chain.
|
|
||||||
spendReport, err := n.p2pNode.GetUtxo(
|
spendReport, err := n.p2pNode.GetUtxo(
|
||||||
neutrino.WatchInputs(inputToWatch),
|
neutrino.WatchInputs(inputToWatch),
|
||||||
neutrino.StartBlock(&waddrmgr.BlockStamp{
|
neutrino.StartBlock(&waddrmgr.BlockStamp{
|
||||||
Height: int32(heightHint),
|
Height: int32(historicalDispatch.StartHeight),
|
||||||
|
}),
|
||||||
|
neutrino.EndBlock(&waddrmgr.BlockStamp{
|
||||||
|
Height: int32(historicalDispatch.EndHeight),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if err != nil && !strings.Contains(err.Error(), "not found") {
|
if err != nil && !strings.Contains(err.Error(), "not found") {
|
||||||
@ -854,57 +710,28 @@ func (n *NeutrinoNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
|
|||||||
|
|
||||||
// If a spend report was returned, and the transaction is present, then
|
// If a spend report was returned, and the transaction is present, then
|
||||||
// this means that the output is already spent.
|
// this means that the output is already spent.
|
||||||
|
var spendDetails *chainntnfs.SpendDetail
|
||||||
if spendReport != nil && spendReport.SpendingTx != nil {
|
if spendReport != nil && spendReport.SpendingTx != nil {
|
||||||
// As a result, we'll launch a goroutine to immediately
|
spendingTxHash := spendReport.SpendingTx.TxHash()
|
||||||
// dispatch the notification with a normal response.
|
spendDetails = &chainntnfs.SpendDetail{
|
||||||
go func() {
|
SpentOutPoint: outpoint,
|
||||||
txSha := spendReport.SpendingTx.TxHash()
|
SpenderTxHash: &spendingTxHash,
|
||||||
select {
|
SpendingTx: spendReport.SpendingTx,
|
||||||
case ntfn.spendChan <- &chainntnfs.SpendDetail{
|
SpenderInputIndex: spendReport.SpendingInputIndex,
|
||||||
SpentOutPoint: outpoint,
|
SpendingHeight: int32(spendReport.SpendingTxHeight),
|
||||||
SpenderTxHash: &txSha,
|
}
|
||||||
SpendingTx: spendReport.SpendingTx,
|
|
||||||
SpenderInputIndex: spendReport.SpendingInputIndex,
|
|
||||||
SpendingHeight: int32(spendReport.SpendingTxHeight),
|
|
||||||
}:
|
|
||||||
case <-n.quit:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}()
|
|
||||||
|
|
||||||
return spendEvent, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the output is still unspent, then we'll update our rescan's
|
// Finally, no matter whether the rescan found a spend in the past or
|
||||||
// filter, and send the request to the dispatcher goroutine.
|
// not, we'll mark our historical rescan as complete to ensure the
|
||||||
rescanUpdate := []neutrino.UpdateOption{
|
// outpoint's spend hint gets updated upon connected/disconnected
|
||||||
neutrino.AddInputs(inputToWatch),
|
// blocks.
|
||||||
neutrino.Rewind(currentHeight),
|
err = n.txNotifier.UpdateSpendDetails(*outpoint, spendDetails)
|
||||||
neutrino.DisableDisconnectedNtfns(true),
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
if err := n.chainView.Update(rescanUpdate...); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
return ntfn.Event, nil
|
||||||
case n.notificationRegistry <- ntfn:
|
|
||||||
case <-n.quit:
|
|
||||||
return nil, ErrChainNotifierShuttingDown
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, we'll add a spent hint with the current height to the cache
|
|
||||||
// in order to better keep track of when this outpoint is spent.
|
|
||||||
err = n.spendHintCache.CommitSpendHint(currentHeight, *outpoint)
|
|
||||||
if err != nil {
|
|
||||||
// The error is not fatal, so we should not return an error to
|
|
||||||
// the caller.
|
|
||||||
chainntnfs.Log.Errorf("Unable to update spend hint to %d for "+
|
|
||||||
"%v: %v", currentHeight, outpoint, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return spendEvent, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterConfirmationsNtfn registers a notification with NeutrinoNotifier
|
// RegisterConfirmationsNtfn registers a notification with NeutrinoNotifier
|
||||||
@ -928,11 +755,11 @@ func (n *NeutrinoNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash,
|
|||||||
chainntnfs.Log.Infof("New confirmation subscription: "+
|
chainntnfs.Log.Infof("New confirmation subscription: "+
|
||||||
"txid=%v, numconfs=%v", txid, numConfs)
|
"txid=%v, numconfs=%v", txid, numConfs)
|
||||||
|
|
||||||
// Register the conf notification with txconfnotifier. 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.txConfNotifier.Register(ntfn)
|
dispatch, err := n.txNotifier.RegisterConf(ntfn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -941,12 +768,55 @@ func (n *NeutrinoNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash,
|
|||||||
return ntfn.Event, nil
|
return ntfn.Event, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// dispatch a historical rescan to determine if it has confirmed in the
|
||||||
|
// past.
|
||||||
|
//
|
||||||
|
// We'll update our filter first to ensure we can immediately detect the
|
||||||
|
// confirmation at tip. To do so, we'll map the script into an address
|
||||||
|
// type so we can instruct neutrino to match if the transaction
|
||||||
|
// containing the script is found in a block.
|
||||||
|
params := n.p2pNode.ChainParams()
|
||||||
|
_, addrs, _, err := txscript.ExtractPkScriptAddrs(pkScript, ¶ms)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to extract script: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll send the filter update request to the notifier's main event
|
||||||
|
// handler and wait for its response.
|
||||||
|
errChan := make(chan error, 1)
|
||||||
select {
|
select {
|
||||||
case n.notificationRegistry <- dispatch:
|
case n.notificationRegistry <- &rescanFilterUpdate{
|
||||||
return ntfn.Event, nil
|
updateOptions: []neutrino.UpdateOption{
|
||||||
|
neutrino.AddAddrs(addrs...),
|
||||||
|
neutrino.Rewind(dispatch.EndHeight),
|
||||||
|
neutrino.DisableDisconnectedNtfns(true),
|
||||||
|
},
|
||||||
|
errChan: errChan,
|
||||||
|
}:
|
||||||
case <-n.quit:
|
case <-n.quit:
|
||||||
return nil, ErrChainNotifierShuttingDown
|
return nil, ErrChainNotifierShuttingDown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err = <-errChan:
|
||||||
|
case <-n.quit:
|
||||||
|
return nil, ErrChainNotifierShuttingDown
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to update filter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, with the filter updates, we can dispatch the historical
|
||||||
|
// rescan to ensure we can detect if the event happened in the past.
|
||||||
|
select {
|
||||||
|
case n.notificationRegistry <- dispatch:
|
||||||
|
case <-n.quit:
|
||||||
|
return nil, ErrChainNotifierShuttingDown
|
||||||
|
}
|
||||||
|
|
||||||
|
return ntfn.Event, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// blockEpochRegistration represents a client's intent to receive a
|
// blockEpochRegistration represents a client's intent to receive a
|
||||||
|
@ -13,13 +13,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// UnsafeStart starts the notifier with a specified best height and optional
|
// UnsafeStart starts the notifier with a specified best height and optional
|
||||||
// best hash. Its bestHeight, txConfNotifier and neutrino node are initialized
|
// best hash. Its bestHeight, txNotifier and neutrino node are initialized with
|
||||||
// with bestHeight. The parameter generateBlocks is necessary for the
|
// bestHeight. The parameter generateBlocks is necessary for the bitcoind
|
||||||
// bitcoind notifier to ensure we drain all notifications up to syncHeight,
|
// notifier to ensure we drain all notifications up to syncHeight, since if they
|
||||||
// since if they are generated ahead of UnsafeStart the chainConn may start
|
// are generated ahead of UnsafeStart the chainConn may start up with an
|
||||||
// up with an outdated best block and miss sending ntfns. Used for testing.
|
// outdated best block and miss sending ntfns. Used for testing.
|
||||||
func (n *NeutrinoNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Hash,
|
func (n *NeutrinoNotifier) UnsafeStart(bestHeight int32,
|
||||||
syncHeight int32, generateBlocks func() error) error {
|
bestHash *chainhash.Hash, syncHeight int32,
|
||||||
|
generateBlocks func() error) error {
|
||||||
|
|
||||||
// We'll obtain the latest block height of the p2p node. We'll
|
// We'll obtain the latest block height of the p2p node. We'll
|
||||||
// start the auto-rescan from this point. Once a caller actually wishes
|
// start the auto-rescan from this point. Once a caller actually wishes
|
||||||
@ -47,8 +48,9 @@ func (n *NeutrinoNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Has
|
|||||||
neutrino.WatchInputs(zeroInput),
|
neutrino.WatchInputs(zeroInput),
|
||||||
}
|
}
|
||||||
|
|
||||||
n.txConfNotifier = chainntnfs.NewTxConfNotifier(
|
n.txNotifier = chainntnfs.NewTxNotifier(
|
||||||
uint32(bestHeight), reorgSafetyLimit, n.confirmHintCache,
|
uint32(bestHeight), reorgSafetyLimit, n.confirmHintCache,
|
||||||
|
n.spendHintCache,
|
||||||
)
|
)
|
||||||
|
|
||||||
n.chainConn = &NeutrinoChainConn{n.p2pNode}
|
n.chainConn = &NeutrinoChainConn{n.p2pNode}
|
||||||
|
@ -1,806 +0,0 @@
|
|||||||
package chainntnfs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
||||||
"github.com/btcsuite/btcutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrTxConfNotifierExiting is an error returned when attempting to
|
|
||||||
// interact with the TxConfNotifier but it been shut down.
|
|
||||||
ErrTxConfNotifierExiting = errors.New("TxConfNotifier is exiting")
|
|
||||||
|
|
||||||
// ErrTxMaxConfs signals that the user requested a number of
|
|
||||||
// confirmations beyond the reorg safety limit.
|
|
||||||
ErrTxMaxConfs = errors.New("too many confirmations requested")
|
|
||||||
)
|
|
||||||
|
|
||||||
// ConfNtfn represents a notifier client's request to receive a notification
|
|
||||||
// once the target transaction gets sufficient confirmations. The client is
|
|
||||||
// asynchronously notified via the ConfirmationEvent channels.
|
|
||||||
type ConfNtfn struct {
|
|
||||||
// ConfID uniquely identifies the confirmation notification request for
|
|
||||||
// the specified transaction.
|
|
||||||
ConfID uint64
|
|
||||||
|
|
||||||
// TxID is the hash of the transaction for which confirmation notifications
|
|
||||||
// are requested.
|
|
||||||
TxID *chainhash.Hash
|
|
||||||
|
|
||||||
// PkScript is the public key script of an outpoint created in this
|
|
||||||
// transaction.
|
|
||||||
//
|
|
||||||
// NOTE: This value MUST be set when the dispatch is to be performed
|
|
||||||
// using compact filters.
|
|
||||||
PkScript []byte
|
|
||||||
|
|
||||||
// NumConfirmations is the number of confirmations after which the
|
|
||||||
// notification is to be sent.
|
|
||||||
NumConfirmations uint32
|
|
||||||
|
|
||||||
// Event contains references to the channels that the notifications are to
|
|
||||||
// be sent over.
|
|
||||||
Event *ConfirmationEvent
|
|
||||||
|
|
||||||
// HeightHint is the minimum height in the chain that we expect to find
|
|
||||||
// this txid.
|
|
||||||
HeightHint uint32
|
|
||||||
|
|
||||||
// dispatched is false if the confirmed notification has not been sent yet.
|
|
||||||
dispatched bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// HistoricalConfDispatch parameterizes a manual rescan for a particular
|
|
||||||
// transaction identifier. The parameters include the start and end block
|
|
||||||
// heights specifying the range of blocks to scan.
|
|
||||||
type HistoricalConfDispatch struct {
|
|
||||||
// TxID is the transaction ID to search for in the historical dispatch.
|
|
||||||
TxID *chainhash.Hash
|
|
||||||
|
|
||||||
// PkScript is a public key script from an output created by this
|
|
||||||
// transaction.
|
|
||||||
//
|
|
||||||
// NOTE: This value MUST be set when the dispatch is to be performed
|
|
||||||
// using compact filters.
|
|
||||||
PkScript []byte
|
|
||||||
|
|
||||||
// StartHeight specifies the block height at which to being the
|
|
||||||
// historical rescan.
|
|
||||||
StartHeight uint32
|
|
||||||
|
|
||||||
// EndHeight specifies the last block height (inclusive) that the
|
|
||||||
// historical scan should consider.
|
|
||||||
EndHeight uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewConfirmationEvent constructs a new ConfirmationEvent with newly opened
|
|
||||||
// channels.
|
|
||||||
func NewConfirmationEvent(numConfs uint32) *ConfirmationEvent {
|
|
||||||
return &ConfirmationEvent{
|
|
||||||
Confirmed: make(chan *TxConfirmation, 1),
|
|
||||||
Updates: make(chan uint32, numConfs),
|
|
||||||
NegativeConf: make(chan int32, 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TxConfNotifier is used to register transaction confirmation notifications and
|
|
||||||
// dispatch them as the transactions confirm. A client can request to be
|
|
||||||
// notified when a particular transaction has sufficient on-chain confirmations
|
|
||||||
// (or be notified immediately if the tx already does), and the TxConfNotifier
|
|
||||||
// will watch changes to the blockchain in order to satisfy these requests.
|
|
||||||
type TxConfNotifier struct {
|
|
||||||
// currentHeight is the height of the tracked blockchain. It is used to
|
|
||||||
// determine the number of confirmations a tx has and ensure blocks are
|
|
||||||
// connected and disconnected in order.
|
|
||||||
currentHeight uint32
|
|
||||||
|
|
||||||
// reorgSafetyLimit is the chain depth beyond which it is assumed a block
|
|
||||||
// will not be reorganized out of the chain. This is used to determine when
|
|
||||||
// to prune old confirmation requests so that reorgs are handled correctly.
|
|
||||||
// The coinbase maturity period is a reasonable value to use.
|
|
||||||
reorgSafetyLimit uint32
|
|
||||||
|
|
||||||
// reorgDepth is the depth of a chain organization that this system is being
|
|
||||||
// informed of. This is incremented as long as a sequence of blocks are
|
|
||||||
// disconnected without being interrupted by a new block.
|
|
||||||
reorgDepth uint32
|
|
||||||
|
|
||||||
// confNotifications is an index of notification requests by transaction
|
|
||||||
// hash.
|
|
||||||
confNotifications map[chainhash.Hash]*confNtfnSet
|
|
||||||
|
|
||||||
// txsByInitialHeight is an index of watched transactions by the height
|
|
||||||
// that they are included at in the blockchain. This is tracked so that
|
|
||||||
// incorrect notifications are not sent if a transaction is reorganized
|
|
||||||
// out of the chain and so that negative confirmations can be recognized.
|
|
||||||
txsByInitialHeight map[uint32]map[chainhash.Hash]struct{}
|
|
||||||
|
|
||||||
// ntfnsByConfirmHeight is an index of notification requests by the height
|
|
||||||
// at which the transaction will have sufficient confirmations.
|
|
||||||
ntfnsByConfirmHeight map[uint32]map[*ConfNtfn]struct{}
|
|
||||||
|
|
||||||
// hintCache is a cache used to maintain the latest height hints for
|
|
||||||
// transactions. Each height hint represents the earliest height at
|
|
||||||
// which the transactions could have been confirmed within the chain.
|
|
||||||
hintCache ConfirmHintCache
|
|
||||||
|
|
||||||
// quit is closed in order to signal that the notifier is gracefully
|
|
||||||
// exiting.
|
|
||||||
quit chan struct{}
|
|
||||||
|
|
||||||
sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// rescanState indicates the progression of a registration before the notifier
|
|
||||||
// can begin dispatching confirmations at tip.
|
|
||||||
type rescanState uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
// rescanNotStarted is the initial state, denoting that a historical
|
|
||||||
// dispatch may be required.
|
|
||||||
rescanNotStarted rescanState = iota
|
|
||||||
|
|
||||||
// rescanPending indicates that a dispatch has already been made, and we
|
|
||||||
// are waiting for its completion. No other rescans should be dispatched
|
|
||||||
// while in this state.
|
|
||||||
rescanPending
|
|
||||||
|
|
||||||
// rescanComplete signals either that a rescan was dispatched and has
|
|
||||||
// completed, or that we began watching at tip immediately. In either
|
|
||||||
// case, the notifier can only dispatch notifications from tip when in
|
|
||||||
// this state.
|
|
||||||
rescanComplete
|
|
||||||
)
|
|
||||||
|
|
||||||
// confNtfnSet holds all known, registered confirmation notifications for a
|
|
||||||
// single txid. If duplicates notifications are requested, only one historical
|
|
||||||
// dispatch will be spawned to ensure redundant scans are not permitted. A
|
|
||||||
// single conf detail will be constructed and dispatched to all interested
|
|
||||||
// clients.
|
|
||||||
type confNtfnSet struct {
|
|
||||||
ntfns map[uint64]*ConfNtfn
|
|
||||||
rescanStatus rescanState
|
|
||||||
details *TxConfirmation
|
|
||||||
}
|
|
||||||
|
|
||||||
// newConfNtfnSet constructs a fresh confNtfnSet for a group of clients
|
|
||||||
// interested in a notification for a particular txid.
|
|
||||||
func newConfNtfnSet() *confNtfnSet {
|
|
||||||
return &confNtfnSet{
|
|
||||||
ntfns: make(map[uint64]*ConfNtfn),
|
|
||||||
rescanStatus: rescanNotStarted,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTxConfNotifier creates a TxConfNotifier. The current height of the
|
|
||||||
// blockchain is accepted as a parameter.
|
|
||||||
func NewTxConfNotifier(startHeight uint32, reorgSafetyLimit uint32,
|
|
||||||
hintCache ConfirmHintCache) *TxConfNotifier {
|
|
||||||
|
|
||||||
return &TxConfNotifier{
|
|
||||||
currentHeight: startHeight,
|
|
||||||
reorgSafetyLimit: reorgSafetyLimit,
|
|
||||||
confNotifications: make(map[chainhash.Hash]*confNtfnSet),
|
|
||||||
txsByInitialHeight: make(map[uint32]map[chainhash.Hash]struct{}),
|
|
||||||
ntfnsByConfirmHeight: make(map[uint32]map[*ConfNtfn]struct{}),
|
|
||||||
hintCache: hintCache,
|
|
||||||
quit: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register handles a new notification request. The client will be notified when
|
|
||||||
// the transaction gets a sufficient number of confirmations on the blockchain.
|
|
||||||
// The registration succeeds if no error is returned. If the returned
|
|
||||||
// HistoricalConfDispatch is non-nil, the caller is responsible for attempting
|
|
||||||
// to manually rescan blocks for the txid between the start and end heights.
|
|
||||||
//
|
|
||||||
// NOTE: If the transaction has already been included in a block on the chain,
|
|
||||||
// the confirmation details must be provided with the UpdateConfDetails method,
|
|
||||||
// otherwise we will wait for the transaction to confirm even though it already
|
|
||||||
// has.
|
|
||||||
func (tcn *TxConfNotifier) Register(
|
|
||||||
ntfn *ConfNtfn) (*HistoricalConfDispatch, error) {
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-tcn.quit:
|
|
||||||
return nil, ErrTxConfNotifierExiting
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce that we will not dispatch confirmations beyond the reorg
|
|
||||||
// safety limit.
|
|
||||||
if ntfn.NumConfirmations > tcn.reorgSafetyLimit {
|
|
||||||
return nil, ErrTxMaxConfs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Before proceeding to register the notification, we'll query our
|
|
||||||
// height hint cache to determine whether a better one exists.
|
|
||||||
//
|
|
||||||
// TODO(conner): verify that all submitted height hints are identical.
|
|
||||||
startHeight := ntfn.HeightHint
|
|
||||||
hint, err := tcn.hintCache.QueryConfirmHint(*ntfn.TxID)
|
|
||||||
if err == nil {
|
|
||||||
if hint > startHeight {
|
|
||||||
Log.Debugf("Using height hint %d retrieved "+
|
|
||||||
"from cache for %v", hint, *ntfn.TxID)
|
|
||||||
startHeight = hint
|
|
||||||
}
|
|
||||||
} else if err != ErrConfirmHintNotFound {
|
|
||||||
Log.Errorf("Unable to query confirm hint for %v: %v",
|
|
||||||
*ntfn.TxID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tcn.Lock()
|
|
||||||
defer tcn.Unlock()
|
|
||||||
|
|
||||||
confSet, ok := tcn.confNotifications[*ntfn.TxID]
|
|
||||||
if !ok {
|
|
||||||
// If this is the first registration for this txid, construct a
|
|
||||||
// confSet to coalesce all notifications for the same txid.
|
|
||||||
confSet = newConfNtfnSet()
|
|
||||||
tcn.confNotifications[*ntfn.TxID] = confSet
|
|
||||||
}
|
|
||||||
|
|
||||||
confSet.ntfns[ntfn.ConfID] = ntfn
|
|
||||||
|
|
||||||
switch confSet.rescanStatus {
|
|
||||||
|
|
||||||
// A prior rescan has already completed and we are actively watching at
|
|
||||||
// tip for this txid.
|
|
||||||
case rescanComplete:
|
|
||||||
// If conf details for this set of notifications has already
|
|
||||||
// been found, we'll attempt to deliver them immediately to this
|
|
||||||
// client.
|
|
||||||
Log.Debugf("Attempting to dispatch conf for txid=%v "+
|
|
||||||
"on registration since rescan has finished", ntfn.TxID)
|
|
||||||
return nil, tcn.dispatchConfDetails(ntfn, confSet.details)
|
|
||||||
|
|
||||||
// A rescan is already in progress, return here to prevent dispatching
|
|
||||||
// another. When the scan returns, this notifications details will be
|
|
||||||
// updated as well.
|
|
||||||
case rescanPending:
|
|
||||||
Log.Debugf("Waiting for pending rescan to finish before "+
|
|
||||||
"notifying txid=%v at tip", ntfn.TxID)
|
|
||||||
return nil, nil
|
|
||||||
|
|
||||||
// If no rescan has been dispatched, attempt to do so now.
|
|
||||||
case rescanNotStarted:
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the provided or cached height hint indicates that the transaction
|
|
||||||
// is to be confirmed at a height greater than the conf notifier's
|
|
||||||
// current height, we'll refrain from spawning a historical dispatch.
|
|
||||||
if startHeight > tcn.currentHeight {
|
|
||||||
Log.Debugf("Height hint is above current height, not dispatching "+
|
|
||||||
"historical rescan for txid=%v ", ntfn.TxID)
|
|
||||||
// Set the rescan status to complete, which will allow the conf
|
|
||||||
// notifier to start delivering messages for this set
|
|
||||||
// immediately.
|
|
||||||
confSet.rescanStatus = rescanComplete
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Debugf("Dispatching historical rescan for txid=%v ", ntfn.TxID)
|
|
||||||
|
|
||||||
// Construct the parameters for historical dispatch, scanning the range
|
|
||||||
// of blocks between our best known height hint and the notifier's
|
|
||||||
// current height. The notifier will begin also watching for
|
|
||||||
// confirmations at tip starting with the next block.
|
|
||||||
dispatch := &HistoricalConfDispatch{
|
|
||||||
TxID: ntfn.TxID,
|
|
||||||
PkScript: ntfn.PkScript,
|
|
||||||
StartHeight: startHeight,
|
|
||||||
EndHeight: tcn.currentHeight,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set this confSet's status to pending, ensuring subsequent
|
|
||||||
// registrations don't also attempt a dispatch.
|
|
||||||
confSet.rescanStatus = rescanPending
|
|
||||||
|
|
||||||
return dispatch, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateConfDetails attempts to update the confirmation details for an active
|
|
||||||
// notification within the notifier. This should only be used in the case of a
|
|
||||||
// transaction that has confirmed before the notifier's current height.
|
|
||||||
//
|
|
||||||
// NOTE: The notification should be registered first to ensure notifications are
|
|
||||||
// dispatched correctly.
|
|
||||||
func (tcn *TxConfNotifier) UpdateConfDetails(txid chainhash.Hash,
|
|
||||||
details *TxConfirmation) error {
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-tcn.quit:
|
|
||||||
return ErrTxConfNotifierExiting
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we hold the lock throughout handling the notification to
|
|
||||||
// prevent the notifier from advancing its height underneath us.
|
|
||||||
tcn.Lock()
|
|
||||||
defer tcn.Unlock()
|
|
||||||
|
|
||||||
// First, we'll determine whether we have an active notification for
|
|
||||||
// this transaction with the given ID.
|
|
||||||
confSet, ok := tcn.confNotifications[txid]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("no notification found with TxID %v", txid)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the conf details were already found at tip, all existing
|
|
||||||
// notifications will have been dispatched or queued for dispatch. We
|
|
||||||
// can exit early to avoid sending too many notifications on the
|
|
||||||
// buffered channels.
|
|
||||||
if confSet.details != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// The historical dispatch has been completed for this confSet. We'll
|
|
||||||
// update the rescan status and cache any details that were found. If
|
|
||||||
// the details are nil, that implies we did not find them and will
|
|
||||||
// continue to watch for them at tip.
|
|
||||||
confSet.rescanStatus = rescanComplete
|
|
||||||
|
|
||||||
// The notifier has yet to reach the height at which the transaction was
|
|
||||||
// included in a block, so we should defer until handling it then within
|
|
||||||
// ConnectTip.
|
|
||||||
if details == nil {
|
|
||||||
Log.Debugf("Conf details for txid=%v not found during "+
|
|
||||||
"historical dispatch, waiting to dispatch at tip", txid)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if details.BlockHeight > tcn.currentHeight {
|
|
||||||
Log.Debugf("Conf details for txid=%v found above current "+
|
|
||||||
"height, waiting to dispatch at tip", txid)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Debugf("Updating conf details for txid=%v details", txid)
|
|
||||||
|
|
||||||
err := tcn.hintCache.CommitConfirmHint(details.BlockHeight, txid)
|
|
||||||
if err != nil {
|
|
||||||
// The error is not fatal, so we should not return an error to
|
|
||||||
// the caller.
|
|
||||||
Log.Errorf("Unable to update confirm hint to %d for %v: %v",
|
|
||||||
details.BlockHeight, txid, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache the details found in the rescan and attempt to dispatch any
|
|
||||||
// notifications that have not yet been delivered.
|
|
||||||
confSet.details = details
|
|
||||||
for _, ntfn := range confSet.ntfns {
|
|
||||||
err = tcn.dispatchConfDetails(ntfn, details)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// dispatchConfDetails attempts to cache and dispatch details to a particular
|
|
||||||
// client if the transaction has sufficiently confirmed. If the provided details
|
|
||||||
// are nil, this method will be a no-op.
|
|
||||||
func (tcn *TxConfNotifier) dispatchConfDetails(
|
|
||||||
ntfn *ConfNtfn, details *TxConfirmation) error {
|
|
||||||
|
|
||||||
// If no details are provided, return early as we can't dispatch.
|
|
||||||
if details == nil {
|
|
||||||
Log.Debugf("Unable to dispatch %v, no details provided",
|
|
||||||
ntfn.TxID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now, we'll examine whether the transaction of this
|
|
||||||
// notification request has reached its required number of
|
|
||||||
// confirmations. If it has, we'll dispatch a confirmation
|
|
||||||
// notification to the caller.
|
|
||||||
confHeight := details.BlockHeight + ntfn.NumConfirmations - 1
|
|
||||||
if confHeight <= tcn.currentHeight {
|
|
||||||
Log.Infof("Dispatching %v conf notification for %v",
|
|
||||||
ntfn.NumConfirmations, ntfn.TxID)
|
|
||||||
|
|
||||||
// We'll send a 0 value to the Updates channel,
|
|
||||||
// indicating that the transaction has already been
|
|
||||||
// confirmed.
|
|
||||||
select {
|
|
||||||
case ntfn.Event.Updates <- 0:
|
|
||||||
case <-tcn.quit:
|
|
||||||
return ErrTxConfNotifierExiting
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case ntfn.Event.Confirmed <- details:
|
|
||||||
ntfn.dispatched = true
|
|
||||||
case <-tcn.quit:
|
|
||||||
return ErrTxConfNotifierExiting
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.Debugf("Queueing %v conf notification for %v at tip ",
|
|
||||||
ntfn.NumConfirmations, ntfn.TxID)
|
|
||||||
|
|
||||||
// Otherwise, we'll keep track of the notification
|
|
||||||
// request by the height at which we should dispatch the
|
|
||||||
// confirmation notification.
|
|
||||||
ntfnSet, exists := tcn.ntfnsByConfirmHeight[confHeight]
|
|
||||||
if !exists {
|
|
||||||
ntfnSet = make(map[*ConfNtfn]struct{})
|
|
||||||
tcn.ntfnsByConfirmHeight[confHeight] = ntfnSet
|
|
||||||
}
|
|
||||||
ntfnSet[ntfn] = struct{}{}
|
|
||||||
|
|
||||||
// We'll also send an update to the client of how many
|
|
||||||
// confirmations are left for the transaction to be
|
|
||||||
// confirmed.
|
|
||||||
numConfsLeft := confHeight - tcn.currentHeight
|
|
||||||
select {
|
|
||||||
case ntfn.Event.Updates <- numConfsLeft:
|
|
||||||
case <-tcn.quit:
|
|
||||||
return ErrTxConfNotifierExiting
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// As a final check, we'll also watch the transaction if it's
|
|
||||||
// still possible for it to get reorged out of the chain.
|
|
||||||
blockHeight := details.BlockHeight
|
|
||||||
reorgSafeHeight := blockHeight + tcn.reorgSafetyLimit
|
|
||||||
if reorgSafeHeight > tcn.currentHeight {
|
|
||||||
txSet, exists := tcn.txsByInitialHeight[blockHeight]
|
|
||||||
if !exists {
|
|
||||||
txSet = make(map[chainhash.Hash]struct{})
|
|
||||||
tcn.txsByInitialHeight[blockHeight] = txSet
|
|
||||||
}
|
|
||||||
txSet[*ntfn.TxID] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConnectTip handles a new block extending the current chain. This checks each
|
|
||||||
// transaction in the block to see if any watched transactions are included.
|
|
||||||
// Also, if any watched transactions now have the required number of
|
|
||||||
// confirmations as a result of this block being connected, this dispatches
|
|
||||||
// notifications.
|
|
||||||
func (tcn *TxConfNotifier) ConnectTip(blockHash *chainhash.Hash,
|
|
||||||
blockHeight uint32, txns []*btcutil.Tx) error {
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-tcn.quit:
|
|
||||||
return ErrTxConfNotifierExiting
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
tcn.Lock()
|
|
||||||
defer tcn.Unlock()
|
|
||||||
|
|
||||||
if blockHeight != tcn.currentHeight+1 {
|
|
||||||
return fmt.Errorf("Received blocks out of order: "+
|
|
||||||
"current height=%d, new height=%d",
|
|
||||||
tcn.currentHeight, blockHeight)
|
|
||||||
}
|
|
||||||
tcn.currentHeight++
|
|
||||||
tcn.reorgDepth = 0
|
|
||||||
|
|
||||||
// Record any newly confirmed transactions by their confirmed height so
|
|
||||||
// that notifications get dispatched when the transactions reach their
|
|
||||||
// required number of confirmations. We'll also watch these transactions
|
|
||||||
// at the height they were included in the chain so reorgs can be
|
|
||||||
// handled correctly.
|
|
||||||
for _, tx := range txns {
|
|
||||||
txHash := tx.Hash()
|
|
||||||
|
|
||||||
// Check if we have any pending notifications for this txid. If
|
|
||||||
// none are found, we can proceed to the next transaction.
|
|
||||||
confSet, ok := tcn.confNotifications[*txHash]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Debugf("Block contains txid=%v, constructing details",
|
|
||||||
txHash)
|
|
||||||
|
|
||||||
// If we have any, we'll record its confirmed height so that
|
|
||||||
// notifications get dispatched when the transaction reaches the
|
|
||||||
// clients' desired number of confirmations.
|
|
||||||
details := &TxConfirmation{
|
|
||||||
BlockHash: blockHash,
|
|
||||||
BlockHeight: blockHeight,
|
|
||||||
TxIndex: uint32(tx.Index()),
|
|
||||||
}
|
|
||||||
|
|
||||||
confSet.rescanStatus = rescanComplete
|
|
||||||
confSet.details = details
|
|
||||||
for _, ntfn := range confSet.ntfns {
|
|
||||||
// In the event that this notification was aware that
|
|
||||||
// the transaction was reorged out of the chain, we'll
|
|
||||||
// consume the reorg notification if it hasn't been done
|
|
||||||
// yet already.
|
|
||||||
select {
|
|
||||||
case <-ntfn.Event.NegativeConf:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll note this client's required number of
|
|
||||||
// confirmations so that we can notify them when
|
|
||||||
// expected.
|
|
||||||
confHeight := blockHeight + ntfn.NumConfirmations - 1
|
|
||||||
ntfnSet, exists := tcn.ntfnsByConfirmHeight[confHeight]
|
|
||||||
if !exists {
|
|
||||||
ntfnSet = make(map[*ConfNtfn]struct{})
|
|
||||||
tcn.ntfnsByConfirmHeight[confHeight] = ntfnSet
|
|
||||||
}
|
|
||||||
ntfnSet[ntfn] = struct{}{}
|
|
||||||
|
|
||||||
// We'll also note the initial confirmation height in
|
|
||||||
// order to correctly handle dispatching notifications
|
|
||||||
// when the transaction gets reorged out of the chain.
|
|
||||||
txSet, exists := tcn.txsByInitialHeight[blockHeight]
|
|
||||||
if !exists {
|
|
||||||
txSet = make(map[chainhash.Hash]struct{})
|
|
||||||
tcn.txsByInitialHeight[blockHeight] = txSet
|
|
||||||
}
|
|
||||||
txSet[*txHash] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In order to update the height hint for all the required transactions
|
|
||||||
// under one database transaction, we'll gather the set of unconfirmed
|
|
||||||
// transactions along with the ones that confirmed at the current
|
|
||||||
// height. To do so, we'll iterate over the confNotifications map, which
|
|
||||||
// contains the transactions we currently have notifications for. Since
|
|
||||||
// this map doesn't tell us whether the transaction has confirmed or
|
|
||||||
// not, we'll need to look at txsByInitialHeight to determine so.
|
|
||||||
var txsToUpdateHints []chainhash.Hash
|
|
||||||
for confirmedTx := range tcn.txsByInitialHeight[tcn.currentHeight] {
|
|
||||||
txsToUpdateHints = append(txsToUpdateHints, confirmedTx)
|
|
||||||
}
|
|
||||||
out:
|
|
||||||
for maybeUnconfirmedTx, confSet := range tcn.confNotifications {
|
|
||||||
// We shouldn't update the confirm hints if we still have a
|
|
||||||
// pending rescan in progress. We'll skip writing any for
|
|
||||||
// notification sets that haven't reached rescanComplete.
|
|
||||||
if confSet.rescanStatus != rescanComplete {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for height, confirmedTxs := range tcn.txsByInitialHeight {
|
|
||||||
// Skip the transactions that confirmed at the new block
|
|
||||||
// height as those have already been added.
|
|
||||||
if height == blockHeight {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the transaction was found within the set of
|
|
||||||
// confirmed transactions at this height, we'll skip it.
|
|
||||||
if _, ok := confirmedTxs[maybeUnconfirmedTx]; ok {
|
|
||||||
continue out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
txsToUpdateHints = append(txsToUpdateHints, maybeUnconfirmedTx)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(txsToUpdateHints) > 0 {
|
|
||||||
err := tcn.hintCache.CommitConfirmHint(
|
|
||||||
tcn.currentHeight, txsToUpdateHints...,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
// The error is not fatal, so we should not return an
|
|
||||||
// error to the caller.
|
|
||||||
Log.Errorf("Unable to update confirm hint to %d for "+
|
|
||||||
"%v: %v", tcn.currentHeight, txsToUpdateHints,
|
|
||||||
err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, we'll dispatch an update to all of the notification clients for
|
|
||||||
// our watched transactions with the number of confirmations left at
|
|
||||||
// this new height.
|
|
||||||
for _, txHashes := range tcn.txsByInitialHeight {
|
|
||||||
for txHash := range txHashes {
|
|
||||||
confSet := tcn.confNotifications[txHash]
|
|
||||||
for _, ntfn := range confSet.ntfns {
|
|
||||||
txConfHeight := confSet.details.BlockHeight +
|
|
||||||
ntfn.NumConfirmations - 1
|
|
||||||
numConfsLeft := txConfHeight - blockHeight
|
|
||||||
|
|
||||||
// Since we don't clear notifications until
|
|
||||||
// transactions are no longer under the risk of
|
|
||||||
// being reorganized out of the chain, we'll
|
|
||||||
// skip sending updates for transactions that
|
|
||||||
// have already been confirmed.
|
|
||||||
if int32(numConfsLeft) < 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case ntfn.Event.Updates <- numConfsLeft:
|
|
||||||
case <-tcn.quit:
|
|
||||||
return ErrTxConfNotifierExiting
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then, we'll dispatch notifications for all the transactions that have
|
|
||||||
// become confirmed at this new block height.
|
|
||||||
for ntfn := range tcn.ntfnsByConfirmHeight[blockHeight] {
|
|
||||||
confSet := tcn.confNotifications[*ntfn.TxID]
|
|
||||||
|
|
||||||
Log.Infof("Dispatching %v conf notification for %v",
|
|
||||||
ntfn.NumConfirmations, ntfn.TxID)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case ntfn.Event.Confirmed <- confSet.details:
|
|
||||||
ntfn.dispatched = true
|
|
||||||
case <-tcn.quit:
|
|
||||||
return ErrTxConfNotifierExiting
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete(tcn.ntfnsByConfirmHeight, tcn.currentHeight)
|
|
||||||
|
|
||||||
// Clear entries from confNotifications and confTxsByInitialHeight. We
|
|
||||||
// assume that reorgs deeper than the reorg safety limit do not happen,
|
|
||||||
// so we can clear out entries for the block that is now mature.
|
|
||||||
if tcn.currentHeight >= tcn.reorgSafetyLimit {
|
|
||||||
matureBlockHeight := tcn.currentHeight - tcn.reorgSafetyLimit
|
|
||||||
for txHash := range tcn.txsByInitialHeight[matureBlockHeight] {
|
|
||||||
delete(tcn.confNotifications, txHash)
|
|
||||||
}
|
|
||||||
delete(tcn.txsByInitialHeight, matureBlockHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DisconnectTip handles the tip of the current chain being disconnected during
|
|
||||||
// a chain reorganization. If any watched transactions were included in this
|
|
||||||
// block, internal structures are updated to ensure a confirmation notification
|
|
||||||
// is not sent unless the transaction is included in the new chain.
|
|
||||||
func (tcn *TxConfNotifier) DisconnectTip(blockHeight uint32) error {
|
|
||||||
select {
|
|
||||||
case <-tcn.quit:
|
|
||||||
return ErrTxConfNotifierExiting
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
tcn.Lock()
|
|
||||||
defer tcn.Unlock()
|
|
||||||
|
|
||||||
if blockHeight != tcn.currentHeight {
|
|
||||||
return fmt.Errorf("Received blocks out of order: "+
|
|
||||||
"current height=%d, disconnected height=%d",
|
|
||||||
tcn.currentHeight, blockHeight)
|
|
||||||
}
|
|
||||||
tcn.currentHeight--
|
|
||||||
tcn.reorgDepth++
|
|
||||||
|
|
||||||
// Rewind the height hint for all watched transactions.
|
|
||||||
var txs []chainhash.Hash
|
|
||||||
for tx := range tcn.confNotifications {
|
|
||||||
txs = append(txs, tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := tcn.hintCache.CommitConfirmHint(tcn.currentHeight, txs...)
|
|
||||||
if err != nil {
|
|
||||||
Log.Errorf("Unable to update confirm hint to %d for %v: %v",
|
|
||||||
tcn.currentHeight, txs, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll go through all of our watched transactions and attempt to drain
|
|
||||||
// their notification channels to ensure sending notifications to the
|
|
||||||
// clients is always non-blocking.
|
|
||||||
for initialHeight, txHashes := range tcn.txsByInitialHeight {
|
|
||||||
for txHash := range txHashes {
|
|
||||||
// If the transaction has been reorged out of the chain,
|
|
||||||
// we'll make sure to remove the cached confirmation
|
|
||||||
// details to prevent notifying clients with old
|
|
||||||
// information.
|
|
||||||
confSet := tcn.confNotifications[txHash]
|
|
||||||
if initialHeight == blockHeight {
|
|
||||||
confSet.details = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ntfn := range confSet.ntfns {
|
|
||||||
// First, we'll attempt to drain an update
|
|
||||||
// from each notification to ensure sends to the
|
|
||||||
// Updates channel are always non-blocking.
|
|
||||||
select {
|
|
||||||
case <-ntfn.Event.Updates:
|
|
||||||
case <-tcn.quit:
|
|
||||||
return ErrTxConfNotifierExiting
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then, we'll check if the current transaction
|
|
||||||
// was included in the block currently being
|
|
||||||
// disconnected. If it was, we'll need to
|
|
||||||
// dispatch a reorg notification to the client.
|
|
||||||
if initialHeight == blockHeight {
|
|
||||||
err := tcn.dispatchConfReorg(
|
|
||||||
ntfn, blockHeight,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, we can remove the transactions we're currently watching that
|
|
||||||
// were included in this block height.
|
|
||||||
delete(tcn.txsByInitialHeight, blockHeight)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// dispatchConfReorg dispatches a reorg notification to the client if the
|
|
||||||
// confirmation notification was already delivered.
|
|
||||||
//
|
|
||||||
// NOTE: This must be called with the TxNotifier's lock held.
|
|
||||||
func (tcn *TxConfNotifier) dispatchConfReorg(
|
|
||||||
ntfn *ConfNtfn, heightDisconnected uint32) error {
|
|
||||||
|
|
||||||
// If the transaction's confirmation notification has yet to be
|
|
||||||
// dispatched, we'll need to clear its entry within the
|
|
||||||
// ntfnsByConfirmHeight index to prevent from notifiying the client once
|
|
||||||
// the notifier reaches the confirmation height.
|
|
||||||
if !ntfn.dispatched {
|
|
||||||
confHeight := heightDisconnected + ntfn.NumConfirmations - 1
|
|
||||||
ntfnSet, exists := tcn.ntfnsByConfirmHeight[confHeight]
|
|
||||||
if exists {
|
|
||||||
delete(ntfnSet, ntfn)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, the entry within the ntfnsByConfirmHeight has already been
|
|
||||||
// deleted, so we'll attempt to drain the confirmation notification to
|
|
||||||
// ensure sends to the Confirmed channel are always non-blocking.
|
|
||||||
select {
|
|
||||||
case <-ntfn.Event.Confirmed:
|
|
||||||
case <-tcn.quit:
|
|
||||||
return ErrTxConfNotifierExiting
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
ntfn.dispatched = false
|
|
||||||
|
|
||||||
// Send a negative confirmation notification to the client indicating
|
|
||||||
// how many blocks have been disconnected successively.
|
|
||||||
select {
|
|
||||||
case ntfn.Event.NegativeConf <- int32(tcn.reorgDepth):
|
|
||||||
case <-tcn.quit:
|
|
||||||
return ErrTxConfNotifierExiting
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TearDown is to be called when the owner of the TxConfNotifier is exiting.
|
|
||||||
// This closes the event channels of all registered notifications that have
|
|
||||||
// not been dispatched yet.
|
|
||||||
func (tcn *TxConfNotifier) TearDown() {
|
|
||||||
tcn.Lock()
|
|
||||||
defer tcn.Unlock()
|
|
||||||
|
|
||||||
close(tcn.quit)
|
|
||||||
|
|
||||||
for _, confSet := range tcn.confNotifications {
|
|
||||||
for _, ntfn := range confSet.ntfns {
|
|
||||||
if ntfn.dispatched {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
close(ntfn.Event.Confirmed)
|
|
||||||
close(ntfn.Event.Updates)
|
|
||||||
close(ntfn.Event.NegativeConf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,1013 +0,0 @@
|
|||||||
package chainntnfs_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
||||||
"github.com/btcsuite/btcd/wire"
|
|
||||||
"github.com/btcsuite/btcutil"
|
|
||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
var zeroHash chainhash.Hash
|
|
||||||
|
|
||||||
type mockHintCache struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
confHints map[chainhash.Hash]uint32
|
|
||||||
spendHints map[wire.OutPoint]uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ chainntnfs.SpendHintCache = (*mockHintCache)(nil)
|
|
||||||
var _ chainntnfs.ConfirmHintCache = (*mockHintCache)(nil)
|
|
||||||
|
|
||||||
func (c *mockHintCache) CommitSpendHint(heightHint uint32, ops ...wire.OutPoint) error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
for _, op := range ops {
|
|
||||||
c.spendHints[op] = heightHint
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *mockHintCache) QuerySpendHint(op wire.OutPoint) (uint32, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
hint, ok := c.spendHints[op]
|
|
||||||
if !ok {
|
|
||||||
return 0, chainntnfs.ErrSpendHintNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return hint, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *mockHintCache) PurgeSpendHint(ops ...wire.OutPoint) error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
for _, op := range ops {
|
|
||||||
delete(c.spendHints, op)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *mockHintCache) CommitConfirmHint(heightHint uint32, txids ...chainhash.Hash) error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
for _, txid := range txids {
|
|
||||||
c.confHints[txid] = heightHint
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *mockHintCache) QueryConfirmHint(txid chainhash.Hash) (uint32, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
hint, ok := c.confHints[txid]
|
|
||||||
if !ok {
|
|
||||||
return 0, chainntnfs.ErrConfirmHintNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return hint, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *mockHintCache) PurgeConfirmHint(txids ...chainhash.Hash) error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
for _, txid := range txids {
|
|
||||||
delete(c.confHints, txid)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMockHintCache() *mockHintCache {
|
|
||||||
return &mockHintCache{
|
|
||||||
confHints: make(map[chainhash.Hash]uint32),
|
|
||||||
spendHints: make(map[wire.OutPoint]uint32),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTxConfFutureDispatch tests that the TxConfNotifier dispatches
|
|
||||||
// registered notifications when the transaction confirms after registration.
|
|
||||||
func TestTxConfFutureDispatch(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const (
|
|
||||||
tx1NumConfs uint32 = 1
|
|
||||||
tx2NumConfs uint32 = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
tx1 = wire.MsgTx{Version: 1}
|
|
||||||
tx2 = wire.MsgTx{Version: 2}
|
|
||||||
tx3 = wire.MsgTx{Version: 3}
|
|
||||||
)
|
|
||||||
|
|
||||||
hintCache := newMockHintCache()
|
|
||||||
tcn := chainntnfs.NewTxConfNotifier(10, 100, hintCache)
|
|
||||||
|
|
||||||
// Create the test transactions and register them with the
|
|
||||||
// TxConfNotifier before including them in a block to receive future
|
|
||||||
// notifications.
|
|
||||||
tx1Hash := tx1.TxHash()
|
|
||||||
ntfn1 := chainntnfs.ConfNtfn{
|
|
||||||
TxID: &tx1Hash,
|
|
||||||
NumConfirmations: tx1NumConfs,
|
|
||||||
Event: chainntnfs.NewConfirmationEvent(tx1NumConfs),
|
|
||||||
}
|
|
||||||
if _, err := tcn.Register(&ntfn1); err != nil {
|
|
||||||
t.Fatalf("unable to register ntfn: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tx2Hash := tx2.TxHash()
|
|
||||||
ntfn2 := chainntnfs.ConfNtfn{
|
|
||||||
TxID: &tx2Hash,
|
|
||||||
NumConfirmations: tx2NumConfs,
|
|
||||||
Event: chainntnfs.NewConfirmationEvent(tx2NumConfs),
|
|
||||||
}
|
|
||||||
if _, err := tcn.Register(&ntfn2); err != nil {
|
|
||||||
t.Fatalf("unable to register ntfn: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should not receive any notifications from both transactions
|
|
||||||
// since they have not been included in a block yet.
|
|
||||||
select {
|
|
||||||
case <-ntfn1.Event.Updates:
|
|
||||||
t.Fatal("Received unexpected confirmation update for tx1")
|
|
||||||
case txConf := <-ntfn1.Event.Confirmed:
|
|
||||||
t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ntfn2.Event.Updates:
|
|
||||||
t.Fatal("Received unexpected confirmation update for tx2")
|
|
||||||
case txConf := <-ntfn2.Event.Confirmed:
|
|
||||||
t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include the transactions in a block and add it to the TxConfNotifier.
|
|
||||||
// This should confirm tx1, but not tx2.
|
|
||||||
block1 := btcutil.NewBlock(&wire.MsgBlock{
|
|
||||||
Transactions: []*wire.MsgTx{&tx1, &tx2, &tx3},
|
|
||||||
})
|
|
||||||
|
|
||||||
err := tcn.ConnectTip(
|
|
||||||
block1.Hash(), 11, block1.Transactions(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to connect block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should only receive one update for tx1 since it only requires
|
|
||||||
// one confirmation and it already met it.
|
|
||||||
select {
|
|
||||||
case numConfsLeft := <-ntfn1.Event.Updates:
|
|
||||||
const expected = 0
|
|
||||||
if numConfsLeft != expected {
|
|
||||||
t.Fatalf("Received incorrect confirmation update: tx1 "+
|
|
||||||
"expected %d confirmations left, got %d",
|
|
||||||
expected, numConfsLeft)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
t.Fatal("Expected confirmation update for tx1")
|
|
||||||
}
|
|
||||||
|
|
||||||
// A confirmation notification for this tranaction should be dispatched,
|
|
||||||
// as it only required one confirmation.
|
|
||||||
select {
|
|
||||||
case txConf := <-ntfn1.Event.Confirmed:
|
|
||||||
expectedConf := chainntnfs.TxConfirmation{
|
|
||||||
BlockHash: block1.Hash(),
|
|
||||||
BlockHeight: 11,
|
|
||||||
TxIndex: 0,
|
|
||||||
}
|
|
||||||
assertEqualTxConf(t, txConf, &expectedConf)
|
|
||||||
default:
|
|
||||||
t.Fatalf("Expected confirmation for tx1")
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should only receive one update for tx2 since it only has one
|
|
||||||
// confirmation so far and it requires two.
|
|
||||||
select {
|
|
||||||
case numConfsLeft := <-ntfn2.Event.Updates:
|
|
||||||
const expected = 1
|
|
||||||
if numConfsLeft != expected {
|
|
||||||
t.Fatalf("Received incorrect confirmation update: tx2 "+
|
|
||||||
"expected %d confirmations left, got %d",
|
|
||||||
expected, numConfsLeft)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
t.Fatal("Expected confirmation update for tx2")
|
|
||||||
}
|
|
||||||
|
|
||||||
// A confirmation notification for tx2 should not be dispatched yet, as
|
|
||||||
// it requires one more confirmation.
|
|
||||||
select {
|
|
||||||
case txConf := <-ntfn2.Event.Confirmed:
|
|
||||||
t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new block and add it to the TxConfNotifier at the next
|
|
||||||
// height. This should confirm tx2.
|
|
||||||
block2 := btcutil.NewBlock(&wire.MsgBlock{
|
|
||||||
Transactions: []*wire.MsgTx{&tx3},
|
|
||||||
})
|
|
||||||
|
|
||||||
err = tcn.ConnectTip(block2.Hash(), 12, block2.Transactions())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to connect block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should not receive any event notifications for tx1 since it has
|
|
||||||
// already been confirmed.
|
|
||||||
select {
|
|
||||||
case <-ntfn1.Event.Updates:
|
|
||||||
t.Fatal("Received unexpected confirmation update for tx1")
|
|
||||||
case txConf := <-ntfn1.Event.Confirmed:
|
|
||||||
t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should only receive one update since the last at the new height,
|
|
||||||
// indicating how many confirmations are still left.
|
|
||||||
select {
|
|
||||||
case numConfsLeft := <-ntfn2.Event.Updates:
|
|
||||||
const expected = 0
|
|
||||||
if numConfsLeft != expected {
|
|
||||||
t.Fatalf("Received incorrect confirmation update: tx2 "+
|
|
||||||
"expected %d confirmations left, got %d",
|
|
||||||
expected, numConfsLeft)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
t.Fatal("Expected confirmation update for tx2")
|
|
||||||
}
|
|
||||||
|
|
||||||
// A confirmation notification for tx2 should be dispatched, since it
|
|
||||||
// now meets its required number of confirmations.
|
|
||||||
select {
|
|
||||||
case txConf := <-ntfn2.Event.Confirmed:
|
|
||||||
expectedConf := chainntnfs.TxConfirmation{
|
|
||||||
BlockHash: block1.Hash(),
|
|
||||||
BlockHeight: 11,
|
|
||||||
TxIndex: 1,
|
|
||||||
}
|
|
||||||
assertEqualTxConf(t, txConf, &expectedConf)
|
|
||||||
default:
|
|
||||||
t.Fatalf("Expected confirmation for tx2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTxConfHistoricalDispatch tests that the TxConfNotifier dispatches
|
|
||||||
// registered notifications when the transaction is confirmed before
|
|
||||||
// registration.
|
|
||||||
func TestTxConfHistoricalDispatch(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const (
|
|
||||||
tx1NumConfs uint32 = 1
|
|
||||||
tx2NumConfs uint32 = 3
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
tx1 = wire.MsgTx{Version: 1}
|
|
||||||
tx2 = wire.MsgTx{Version: 2}
|
|
||||||
tx3 = wire.MsgTx{Version: 3}
|
|
||||||
)
|
|
||||||
|
|
||||||
hintCache := newMockHintCache()
|
|
||||||
tcn := chainntnfs.NewTxConfNotifier(10, 100, hintCache)
|
|
||||||
|
|
||||||
// Create the test transactions at a height before the TxConfNotifier's
|
|
||||||
// starting height so that they are confirmed once registering them.
|
|
||||||
tx1Hash := tx1.TxHash()
|
|
||||||
ntfn1 := chainntnfs.ConfNtfn{
|
|
||||||
ConfID: 0,
|
|
||||||
TxID: &tx1Hash,
|
|
||||||
NumConfirmations: tx1NumConfs,
|
|
||||||
Event: chainntnfs.NewConfirmationEvent(tx1NumConfs),
|
|
||||||
}
|
|
||||||
if _, err := tcn.Register(&ntfn1); err != nil {
|
|
||||||
t.Fatalf("unable to register ntfn: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tx2Hash := tx2.TxHash()
|
|
||||||
ntfn2 := chainntnfs.ConfNtfn{
|
|
||||||
ConfID: 1,
|
|
||||||
TxID: &tx2Hash,
|
|
||||||
NumConfirmations: tx2NumConfs,
|
|
||||||
Event: chainntnfs.NewConfirmationEvent(tx2NumConfs),
|
|
||||||
}
|
|
||||||
if _, err := tcn.Register(&ntfn2); err != nil {
|
|
||||||
t.Fatalf("unable to register ntfn: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update tx1 with its confirmation details. We should only receive one
|
|
||||||
// update since it only requires one confirmation and it already met it.
|
|
||||||
txConf1 := chainntnfs.TxConfirmation{
|
|
||||||
BlockHash: &zeroHash,
|
|
||||||
BlockHeight: 9,
|
|
||||||
TxIndex: 1,
|
|
||||||
}
|
|
||||||
err := tcn.UpdateConfDetails(tx1Hash, &txConf1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to update conf details: %v", err)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case numConfsLeft := <-ntfn1.Event.Updates:
|
|
||||||
const expected = 0
|
|
||||||
if numConfsLeft != expected {
|
|
||||||
t.Fatalf("Received incorrect confirmation update: tx1 "+
|
|
||||||
"expected %d confirmations left, got %d",
|
|
||||||
expected, numConfsLeft)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
t.Fatal("Expected confirmation update for tx1")
|
|
||||||
}
|
|
||||||
|
|
||||||
// A confirmation notification for tx1 should also be dispatched.
|
|
||||||
select {
|
|
||||||
case txConf := <-ntfn1.Event.Confirmed:
|
|
||||||
assertEqualTxConf(t, txConf, &txConf1)
|
|
||||||
default:
|
|
||||||
t.Fatalf("Expected confirmation for tx1")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update tx2 with its confirmation details. This should not trigger a
|
|
||||||
// confirmation notification since it hasn't reached its required number
|
|
||||||
// of confirmations, but we should receive a confirmation update
|
|
||||||
// indicating how many confirmation are left.
|
|
||||||
txConf2 := chainntnfs.TxConfirmation{
|
|
||||||
BlockHash: &zeroHash,
|
|
||||||
BlockHeight: 9,
|
|
||||||
TxIndex: 2,
|
|
||||||
}
|
|
||||||
err = tcn.UpdateConfDetails(tx2Hash, &txConf2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to update conf details: %v", err)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case numConfsLeft := <-ntfn2.Event.Updates:
|
|
||||||
const expected = 1
|
|
||||||
if numConfsLeft != expected {
|
|
||||||
t.Fatalf("Received incorrect confirmation update: tx2 "+
|
|
||||||
"expected %d confirmations left, got %d",
|
|
||||||
expected, numConfsLeft)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
t.Fatal("Expected confirmation update for tx2")
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case txConf := <-ntfn2.Event.Confirmed:
|
|
||||||
t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new block and add it to the TxConfNotifier at the next
|
|
||||||
// height. This should confirm tx2.
|
|
||||||
block := btcutil.NewBlock(&wire.MsgBlock{
|
|
||||||
Transactions: []*wire.MsgTx{&tx3},
|
|
||||||
})
|
|
||||||
|
|
||||||
err = tcn.ConnectTip(block.Hash(), 11, block.Transactions())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to connect block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should not receive any event notifications for tx1 since it has
|
|
||||||
// already been confirmed.
|
|
||||||
select {
|
|
||||||
case <-ntfn1.Event.Updates:
|
|
||||||
t.Fatal("Received unexpected confirmation update for tx1")
|
|
||||||
case txConf := <-ntfn1.Event.Confirmed:
|
|
||||||
t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should only receive one update for tx2 since the last one,
|
|
||||||
// indicating how many confirmations are still left.
|
|
||||||
select {
|
|
||||||
case numConfsLeft := <-ntfn2.Event.Updates:
|
|
||||||
const expected = 0
|
|
||||||
if numConfsLeft != expected {
|
|
||||||
t.Fatalf("Received incorrect confirmation update: tx2 "+
|
|
||||||
"expected %d confirmations left, got %d",
|
|
||||||
expected, numConfsLeft)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
t.Fatal("Expected confirmation update for tx2")
|
|
||||||
}
|
|
||||||
|
|
||||||
// A confirmation notification for tx2 should be dispatched, as it met
|
|
||||||
// its required number of confirmations.
|
|
||||||
select {
|
|
||||||
case txConf := <-ntfn2.Event.Confirmed:
|
|
||||||
assertEqualTxConf(t, txConf, &txConf2)
|
|
||||||
default:
|
|
||||||
t.Fatalf("Expected confirmation for tx2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTxConfChainReorg tests that TxConfNotifier dispatches Confirmed and
|
|
||||||
// NegativeConf notifications appropriately when there is a chain
|
|
||||||
// reorganization.
|
|
||||||
func TestTxConfChainReorg(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const (
|
|
||||||
tx1NumConfs uint32 = 2
|
|
||||||
tx2NumConfs uint32 = 1
|
|
||||||
tx3NumConfs uint32 = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
tx1 = wire.MsgTx{Version: 1}
|
|
||||||
tx2 = wire.MsgTx{Version: 2}
|
|
||||||
tx3 = wire.MsgTx{Version: 3}
|
|
||||||
)
|
|
||||||
|
|
||||||
hintCache := newMockHintCache()
|
|
||||||
tcn := chainntnfs.NewTxConfNotifier(7, 100, hintCache)
|
|
||||||
|
|
||||||
// Tx 1 will be confirmed in block 9 and requires 2 confs.
|
|
||||||
tx1Hash := tx1.TxHash()
|
|
||||||
ntfn1 := chainntnfs.ConfNtfn{
|
|
||||||
TxID: &tx1Hash,
|
|
||||||
NumConfirmations: tx1NumConfs,
|
|
||||||
Event: chainntnfs.NewConfirmationEvent(tx1NumConfs),
|
|
||||||
}
|
|
||||||
if _, err := tcn.Register(&ntfn1); err != nil {
|
|
||||||
t.Fatalf("unable to register ntfn: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tcn.UpdateConfDetails(*ntfn1.TxID, nil); err != nil {
|
|
||||||
t.Fatalf("unable to deliver conf details: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tx 2 will be confirmed in block 10 and requires 1 conf.
|
|
||||||
tx2Hash := tx2.TxHash()
|
|
||||||
ntfn2 := chainntnfs.ConfNtfn{
|
|
||||||
TxID: &tx2Hash,
|
|
||||||
NumConfirmations: tx2NumConfs,
|
|
||||||
Event: chainntnfs.NewConfirmationEvent(tx2NumConfs),
|
|
||||||
}
|
|
||||||
if _, err := tcn.Register(&ntfn2); err != nil {
|
|
||||||
t.Fatalf("unable to register ntfn: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tcn.UpdateConfDetails(*ntfn2.TxID, nil); err != nil {
|
|
||||||
t.Fatalf("unable to deliver conf details: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tx 3 will be confirmed in block 10 and requires 2 confs.
|
|
||||||
tx3Hash := tx3.TxHash()
|
|
||||||
ntfn3 := chainntnfs.ConfNtfn{
|
|
||||||
TxID: &tx3Hash,
|
|
||||||
NumConfirmations: tx3NumConfs,
|
|
||||||
Event: chainntnfs.NewConfirmationEvent(tx3NumConfs),
|
|
||||||
}
|
|
||||||
if _, err := tcn.Register(&ntfn3); err != nil {
|
|
||||||
t.Fatalf("unable to register ntfn: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tcn.UpdateConfDetails(*ntfn3.TxID, nil); err != nil {
|
|
||||||
t.Fatalf("unable to deliver conf details: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync chain to block 10. Txs 1 & 2 should be confirmed.
|
|
||||||
block1 := btcutil.NewBlock(&wire.MsgBlock{
|
|
||||||
Transactions: []*wire.MsgTx{&tx1},
|
|
||||||
})
|
|
||||||
err := tcn.ConnectTip(nil, 8, block1.Transactions())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to connect block: %v", err)
|
|
||||||
}
|
|
||||||
err = tcn.ConnectTip(nil, 9, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to connect block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
block2 := btcutil.NewBlock(&wire.MsgBlock{
|
|
||||||
Transactions: []*wire.MsgTx{&tx2, &tx3},
|
|
||||||
})
|
|
||||||
err = tcn.ConnectTip(nil, 10, block2.Transactions())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to connect block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should receive two updates for tx1 since it requires two
|
|
||||||
// confirmations and it has already met them.
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
select {
|
|
||||||
case <-ntfn1.Event.Updates:
|
|
||||||
default:
|
|
||||||
t.Fatal("Expected confirmation update for tx1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A confirmation notification for tx1 should be dispatched, as it met
|
|
||||||
// its required number of confirmations.
|
|
||||||
select {
|
|
||||||
case <-ntfn1.Event.Confirmed:
|
|
||||||
default:
|
|
||||||
t.Fatalf("Expected confirmation for tx1")
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should only receive one update for tx2 since it only requires
|
|
||||||
// one confirmation and it already met it.
|
|
||||||
select {
|
|
||||||
case <-ntfn2.Event.Updates:
|
|
||||||
default:
|
|
||||||
t.Fatal("Expected confirmation update for tx2")
|
|
||||||
}
|
|
||||||
|
|
||||||
// A confirmation notification for tx2 should be dispatched, as it met
|
|
||||||
// its required number of confirmations.
|
|
||||||
select {
|
|
||||||
case <-ntfn2.Event.Confirmed:
|
|
||||||
default:
|
|
||||||
t.Fatalf("Expected confirmation for tx2")
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should only receive one update for tx3 since it only has one
|
|
||||||
// confirmation so far and it requires two.
|
|
||||||
select {
|
|
||||||
case <-ntfn3.Event.Updates:
|
|
||||||
default:
|
|
||||||
t.Fatal("Expected confirmation update for tx3")
|
|
||||||
}
|
|
||||||
|
|
||||||
// A confirmation notification for tx3 should not be dispatched yet, as
|
|
||||||
// it requires one more confirmation.
|
|
||||||
select {
|
|
||||||
case txConf := <-ntfn3.Event.Confirmed:
|
|
||||||
t.Fatalf("Received unexpected confirmation for tx3: %v", txConf)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// The block that included tx2 and tx3 is disconnected and two next
|
|
||||||
// blocks without them are connected.
|
|
||||||
err = tcn.DisconnectTip(10)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to connect block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tcn.ConnectTip(nil, 10, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to connect block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tcn.ConnectTip(nil, 11, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to connect block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case reorgDepth := <-ntfn2.Event.NegativeConf:
|
|
||||||
if reorgDepth != 1 {
|
|
||||||
t.Fatalf("Incorrect value for negative conf notification: "+
|
|
||||||
"expected %d, got %d", 1, reorgDepth)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
t.Fatalf("Expected negative conf notification for tx1")
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should not receive any event notifications from all of the
|
|
||||||
// transactions because tx1 has already been confirmed and tx2 and tx3
|
|
||||||
// have not been included in the chain since the reorg.
|
|
||||||
select {
|
|
||||||
case <-ntfn1.Event.Updates:
|
|
||||||
t.Fatal("Received unexpected confirmation update for tx1")
|
|
||||||
case txConf := <-ntfn1.Event.Confirmed:
|
|
||||||
t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ntfn2.Event.Updates:
|
|
||||||
t.Fatal("Received unexpected confirmation update for tx2")
|
|
||||||
case txConf := <-ntfn2.Event.Confirmed:
|
|
||||||
t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ntfn3.Event.Updates:
|
|
||||||
t.Fatal("Received unexpected confirmation update for tx3")
|
|
||||||
case txConf := <-ntfn3.Event.Confirmed:
|
|
||||||
t.Fatalf("Received unexpected confirmation for tx3: %v", txConf)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now transactions 2 & 3 are re-included in a new block.
|
|
||||||
block3 := btcutil.NewBlock(&wire.MsgBlock{
|
|
||||||
Transactions: []*wire.MsgTx{&tx2, &tx3},
|
|
||||||
})
|
|
||||||
block4 := btcutil.NewBlock(&wire.MsgBlock{})
|
|
||||||
|
|
||||||
err = tcn.ConnectTip(block3.Hash(), 12, block3.Transactions())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to connect block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tcn.ConnectTip(block4.Hash(), 13, block4.Transactions())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to connect block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should only receive one update for tx2 since it only requires
|
|
||||||
// one confirmation and it already met it.
|
|
||||||
select {
|
|
||||||
case numConfsLeft := <-ntfn2.Event.Updates:
|
|
||||||
const expected = 0
|
|
||||||
if numConfsLeft != expected {
|
|
||||||
t.Fatalf("Received incorrect confirmation update: tx2 "+
|
|
||||||
"expected %d confirmations left, got %d",
|
|
||||||
expected, numConfsLeft)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
t.Fatal("Expected confirmation update for tx2")
|
|
||||||
}
|
|
||||||
|
|
||||||
// A confirmation notification for tx2 should be dispatched, as it met
|
|
||||||
// its required number of confirmations.
|
|
||||||
select {
|
|
||||||
case txConf := <-ntfn2.Event.Confirmed:
|
|
||||||
expectedConf := chainntnfs.TxConfirmation{
|
|
||||||
BlockHash: block3.Hash(),
|
|
||||||
BlockHeight: 12,
|
|
||||||
TxIndex: 0,
|
|
||||||
}
|
|
||||||
assertEqualTxConf(t, txConf, &expectedConf)
|
|
||||||
default:
|
|
||||||
t.Fatalf("Expected confirmation for tx2")
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should receive two updates for tx3 since it requires two
|
|
||||||
// confirmations and it has already met them.
|
|
||||||
for i := uint32(1); i <= 2; i++ {
|
|
||||||
select {
|
|
||||||
case numConfsLeft := <-ntfn3.Event.Updates:
|
|
||||||
expected := tx3NumConfs - i
|
|
||||||
if numConfsLeft != expected {
|
|
||||||
t.Fatalf("Received incorrect confirmation update: tx3 "+
|
|
||||||
"expected %d confirmations left, got %d",
|
|
||||||
expected, numConfsLeft)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
t.Fatal("Expected confirmation update for tx2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A confirmation notification for tx3 should be dispatched, as it met
|
|
||||||
// its required number of confirmations.
|
|
||||||
select {
|
|
||||||
case txConf := <-ntfn3.Event.Confirmed:
|
|
||||||
expectedConf := chainntnfs.TxConfirmation{
|
|
||||||
BlockHash: block3.Hash(),
|
|
||||||
BlockHeight: 12,
|
|
||||||
TxIndex: 1,
|
|
||||||
}
|
|
||||||
assertEqualTxConf(t, txConf, &expectedConf)
|
|
||||||
default:
|
|
||||||
t.Fatalf("Expected confirmation for tx3")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTxConfHeightHintCache ensures that the height hints for transactions are
|
|
||||||
// kept track of correctly with each new block connected/disconnected. This test
|
|
||||||
// also asserts that the height hints are not updated until the simulated
|
|
||||||
// historical dispatches have returned, and we know the transactions aren't
|
|
||||||
// already in the chain.
|
|
||||||
func TestTxConfHeightHintCache(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const (
|
|
||||||
startingHeight = 200
|
|
||||||
txDummyHeight = 201
|
|
||||||
tx1Height = 202
|
|
||||||
tx2Height = 203
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initialize our TxConfNotifier instance backed by a height hint cache.
|
|
||||||
hintCache := newMockHintCache()
|
|
||||||
tcn := chainntnfs.NewTxConfNotifier(
|
|
||||||
startingHeight, 100, hintCache,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create two test transactions and register them for notifications.
|
|
||||||
tx1 := wire.MsgTx{Version: 1}
|
|
||||||
tx1Hash := tx1.TxHash()
|
|
||||||
ntfn1 := &chainntnfs.ConfNtfn{
|
|
||||||
TxID: &tx1Hash,
|
|
||||||
NumConfirmations: 1,
|
|
||||||
Event: chainntnfs.NewConfirmationEvent(1),
|
|
||||||
}
|
|
||||||
|
|
||||||
tx2 := wire.MsgTx{Version: 2}
|
|
||||||
tx2Hash := tx2.TxHash()
|
|
||||||
ntfn2 := &chainntnfs.ConfNtfn{
|
|
||||||
TxID: &tx2Hash,
|
|
||||||
NumConfirmations: 2,
|
|
||||||
Event: chainntnfs.NewConfirmationEvent(2),
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := tcn.Register(ntfn1); err != nil {
|
|
||||||
t.Fatalf("unable to register tx1: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := tcn.Register(ntfn2); err != nil {
|
|
||||||
t.Fatalf("unable to register tx2: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both transactions should not have a height hint set, as Register
|
|
||||||
// should not alter the cache state.
|
|
||||||
_, err := hintCache.QueryConfirmHint(tx1Hash)
|
|
||||||
if err != chainntnfs.ErrConfirmHintNotFound {
|
|
||||||
t.Fatalf("unexpected error when querying for height hint "+
|
|
||||||
"want: %v, got %v",
|
|
||||||
chainntnfs.ErrConfirmHintNotFound, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = hintCache.QueryConfirmHint(tx2Hash)
|
|
||||||
if err != chainntnfs.ErrConfirmHintNotFound {
|
|
||||||
t.Fatalf("unexpected error when querying for height hint "+
|
|
||||||
"want: %v, got %v",
|
|
||||||
chainntnfs.ErrConfirmHintNotFound, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new block that will include the dummy transaction and extend
|
|
||||||
// the chain.
|
|
||||||
txDummy := wire.MsgTx{Version: 3}
|
|
||||||
block1 := btcutil.NewBlock(&wire.MsgBlock{
|
|
||||||
Transactions: []*wire.MsgTx{&txDummy},
|
|
||||||
})
|
|
||||||
|
|
||||||
err = tcn.ConnectTip(
|
|
||||||
block1.Hash(), txDummyHeight, block1.Transactions(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to connect block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since UpdateConfDetails has not been called for either transaction,
|
|
||||||
// the height hints should remain unchanged. This simulates blocks
|
|
||||||
// confirming while the historical dispatch is processing the
|
|
||||||
// registration.
|
|
||||||
hint, err := hintCache.QueryConfirmHint(tx1Hash)
|
|
||||||
if err != chainntnfs.ErrConfirmHintNotFound {
|
|
||||||
t.Fatalf("unexpected error when querying for height hint "+
|
|
||||||
"want: %v, got %v",
|
|
||||||
chainntnfs.ErrConfirmHintNotFound, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hint, err = hintCache.QueryConfirmHint(tx2Hash)
|
|
||||||
if err != chainntnfs.ErrConfirmHintNotFound {
|
|
||||||
t.Fatalf("unexpected error when querying for height hint "+
|
|
||||||
"want: %v, got %v",
|
|
||||||
chainntnfs.ErrConfirmHintNotFound, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now, update the conf details reporting that the neither txn was found
|
|
||||||
// in the historical dispatch.
|
|
||||||
if err := tcn.UpdateConfDetails(tx1Hash, nil); err != nil {
|
|
||||||
t.Fatalf("unable to update conf details: %v", err)
|
|
||||||
}
|
|
||||||
if err := tcn.UpdateConfDetails(tx2Hash, nil); err != nil {
|
|
||||||
t.Fatalf("unable to update conf details: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll create another block that will include the first transaction
|
|
||||||
// and extend the chain.
|
|
||||||
block2 := btcutil.NewBlock(&wire.MsgBlock{
|
|
||||||
Transactions: []*wire.MsgTx{&tx1},
|
|
||||||
})
|
|
||||||
|
|
||||||
err = tcn.ConnectTip(
|
|
||||||
block2.Hash(), tx1Height, block2.Transactions(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to connect block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that both notifications are waiting at tip for confirmations,
|
|
||||||
// they should have their height hints updated to the latest block
|
|
||||||
// height.
|
|
||||||
hint, err = hintCache.QueryConfirmHint(tx1Hash)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to query for hint: %v", err)
|
|
||||||
}
|
|
||||||
if hint != tx1Height {
|
|
||||||
t.Fatalf("expected hint %d, got %d",
|
|
||||||
tx1Height, hint)
|
|
||||||
}
|
|
||||||
|
|
||||||
hint, err = hintCache.QueryConfirmHint(tx2Hash)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to query for hint: %v", err)
|
|
||||||
}
|
|
||||||
if hint != tx1Height {
|
|
||||||
t.Fatalf("expected hint %d, got %d",
|
|
||||||
tx2Height, hint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, we'll create another block that will include the second
|
|
||||||
// transaction and extend the chain.
|
|
||||||
block3 := btcutil.NewBlock(&wire.MsgBlock{
|
|
||||||
Transactions: []*wire.MsgTx{&tx2},
|
|
||||||
})
|
|
||||||
|
|
||||||
err = tcn.ConnectTip(
|
|
||||||
block3.Hash(), tx2Height, block3.Transactions(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to connect block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The height hint for the first transaction should remain the same.
|
|
||||||
hint, err = hintCache.QueryConfirmHint(tx1Hash)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to query for hint: %v", err)
|
|
||||||
}
|
|
||||||
if hint != tx1Height {
|
|
||||||
t.Fatalf("expected hint %d, got %d",
|
|
||||||
tx1Height, hint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The height hint for the second transaction should now be updated to
|
|
||||||
// reflect its confirmation.
|
|
||||||
hint, err = hintCache.QueryConfirmHint(tx2Hash)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to query for hint: %v", err)
|
|
||||||
}
|
|
||||||
if hint != tx2Height {
|
|
||||||
t.Fatalf("expected hint %d, got %d",
|
|
||||||
tx2Height, hint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, we'll attempt do disconnect the last block in order to
|
|
||||||
// simulate a chain reorg.
|
|
||||||
if err := tcn.DisconnectTip(tx2Height); err != nil {
|
|
||||||
t.Fatalf("Failed to disconnect block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should update the second transaction's height hint within the
|
|
||||||
// cache to the previous height.
|
|
||||||
hint, err = hintCache.QueryConfirmHint(tx2Hash)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to query for hint: %v", err)
|
|
||||||
}
|
|
||||||
if hint != tx1Height {
|
|
||||||
t.Fatalf("expected hint %d, got %d",
|
|
||||||
tx1Height, hint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The first transaction's height hint should remain at the original
|
|
||||||
// confirmation height.
|
|
||||||
hint, err = hintCache.QueryConfirmHint(tx2Hash)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to query for hint: %v", err)
|
|
||||||
}
|
|
||||||
if hint != tx1Height {
|
|
||||||
t.Fatalf("expected hint %d, got %d",
|
|
||||||
tx1Height, hint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTxConfTearDown(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
tx1 = wire.MsgTx{Version: 1}
|
|
||||||
tx2 = wire.MsgTx{Version: 2}
|
|
||||||
)
|
|
||||||
|
|
||||||
hintCache := newMockHintCache()
|
|
||||||
tcn := chainntnfs.NewTxConfNotifier(10, 100, hintCache)
|
|
||||||
|
|
||||||
// Create the test transactions and register them with the
|
|
||||||
// TxConfNotifier to receive notifications.
|
|
||||||
tx1Hash := tx1.TxHash()
|
|
||||||
ntfn1 := chainntnfs.ConfNtfn{
|
|
||||||
TxID: &tx1Hash,
|
|
||||||
NumConfirmations: 1,
|
|
||||||
Event: chainntnfs.NewConfirmationEvent(1),
|
|
||||||
}
|
|
||||||
if _, err := tcn.Register(&ntfn1); err != nil {
|
|
||||||
t.Fatalf("unable to register ntfn: %v", err)
|
|
||||||
}
|
|
||||||
if err := tcn.UpdateConfDetails(*ntfn1.TxID, nil); err != nil {
|
|
||||||
t.Fatalf("unable to update conf details: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tx2Hash := tx2.TxHash()
|
|
||||||
ntfn2 := chainntnfs.ConfNtfn{
|
|
||||||
TxID: &tx2Hash,
|
|
||||||
NumConfirmations: 2,
|
|
||||||
Event: chainntnfs.NewConfirmationEvent(2),
|
|
||||||
}
|
|
||||||
if _, err := tcn.Register(&ntfn2); err != nil {
|
|
||||||
t.Fatalf("unable to register ntfn: %v", err)
|
|
||||||
}
|
|
||||||
if err := tcn.UpdateConfDetails(*ntfn2.TxID, nil); err != nil {
|
|
||||||
t.Fatalf("unable to update conf details: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include the transactions in a block and add it to the TxConfNotifier.
|
|
||||||
// This should confirm tx1, but not tx2.
|
|
||||||
block := btcutil.NewBlock(&wire.MsgBlock{
|
|
||||||
Transactions: []*wire.MsgTx{&tx1, &tx2},
|
|
||||||
})
|
|
||||||
|
|
||||||
err := tcn.ConnectTip(block.Hash(), 11, block.Transactions())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to connect block: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We do not care about the correctness of the notifications since they
|
|
||||||
// are tested in other methods, but we'll still attempt to retrieve them
|
|
||||||
// for the sake of not being able to later once the notification
|
|
||||||
// channels are closed.
|
|
||||||
select {
|
|
||||||
case <-ntfn1.Event.Updates:
|
|
||||||
default:
|
|
||||||
t.Fatal("Expected confirmation update for tx1")
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ntfn1.Event.Confirmed:
|
|
||||||
default:
|
|
||||||
t.Fatalf("Expected confirmation for tx1")
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ntfn2.Event.Updates:
|
|
||||||
default:
|
|
||||||
t.Fatal("Expected confirmation update for tx2")
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case txConf := <-ntfn2.Event.Confirmed:
|
|
||||||
t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// The notification channels should be closed for notifications that
|
|
||||||
// have not been dispatched yet, so we should not expect to receive any
|
|
||||||
// more updates.
|
|
||||||
tcn.TearDown()
|
|
||||||
|
|
||||||
// tx1 should not receive any more updates because it has already been
|
|
||||||
// confirmed and the TxConfNotifier has been shut down.
|
|
||||||
select {
|
|
||||||
case <-ntfn1.Event.Updates:
|
|
||||||
t.Fatal("Received unexpected confirmation update for tx1")
|
|
||||||
case txConf := <-ntfn1.Event.Confirmed:
|
|
||||||
t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// tx2 should not receive any more updates after the notifications
|
|
||||||
// channels have been closed and the TxConfNotifier shut down.
|
|
||||||
select {
|
|
||||||
case _, more := <-ntfn2.Event.Updates:
|
|
||||||
if more {
|
|
||||||
t.Fatal("Expected closed Updates channel for tx2")
|
|
||||||
}
|
|
||||||
case _, more := <-ntfn2.Event.Confirmed:
|
|
||||||
if more {
|
|
||||||
t.Fatalf("Expected closed Confirmed channel for tx2")
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
t.Fatalf("Expected closed notification channels for tx2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertEqualTxConf(t *testing.T,
|
|
||||||
actualConf, expectedConf *chainntnfs.TxConfirmation) {
|
|
||||||
|
|
||||||
if actualConf.BlockHeight != expectedConf.BlockHeight {
|
|
||||||
t.Fatalf("Incorrect block height in confirmation details: "+
|
|
||||||
"expected %d, got %d",
|
|
||||||
expectedConf.BlockHeight, actualConf.BlockHeight)
|
|
||||||
}
|
|
||||||
if !actualConf.BlockHash.IsEqual(expectedConf.BlockHash) {
|
|
||||||
t.Fatalf("Incorrect block hash in confirmation details: "+
|
|
||||||
"expected %d, got %d", expectedConf.BlockHash, actualConf.BlockHash)
|
|
||||||
}
|
|
||||||
if actualConf.TxIndex != expectedConf.TxIndex {
|
|
||||||
t.Fatalf("Incorrect tx index in confirmation details: "+
|
|
||||||
"expected %d, got %d", expectedConf.TxIndex, actualConf.TxIndex)
|
|
||||||
}
|
|
||||||
}
|
|
1388
chainntnfs/txnotifier.go
Normal file
1388
chainntnfs/txnotifier.go
Normal file
@ -0,0 +1,1388 @@
|
|||||||
|
package chainntnfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrTxNotifierExiting is an error returned when attempting to interact
|
||||||
|
// with the TxNotifier but it been shut down.
|
||||||
|
ErrTxNotifierExiting = errors.New("TxNotifier is exiting")
|
||||||
|
|
||||||
|
// ErrTxMaxConfs signals that the user requested a number of
|
||||||
|
// confirmations beyond the reorg safety limit.
|
||||||
|
ErrTxMaxConfs = errors.New("too many confirmations requested")
|
||||||
|
)
|
||||||
|
|
||||||
|
// rescanState indicates the progression of a registration before the notifier
|
||||||
|
// can begin dispatching confirmations at tip.
|
||||||
|
type rescanState byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
// rescanNotStarted is the initial state, denoting that a historical
|
||||||
|
// dispatch may be required.
|
||||||
|
rescanNotStarted rescanState = iota
|
||||||
|
|
||||||
|
// rescanPending indicates that a dispatch has already been made, and we
|
||||||
|
// are waiting for its completion. No other rescans should be dispatched
|
||||||
|
// while in this state.
|
||||||
|
rescanPending
|
||||||
|
|
||||||
|
// rescanComplete signals either that a rescan was dispatched and has
|
||||||
|
// completed, or that we began watching at tip immediately. In either
|
||||||
|
// case, the notifier can only dispatch notifications from tip when in
|
||||||
|
// this state.
|
||||||
|
rescanComplete
|
||||||
|
)
|
||||||
|
|
||||||
|
// confNtfnSet holds all known, registered confirmation notifications for a
|
||||||
|
// single txid. If duplicates notifications are requested, only one historical
|
||||||
|
// dispatch will be spawned to ensure redundant scans are not permitted. A
|
||||||
|
// single conf detail will be constructed and dispatched to all interested
|
||||||
|
// clients.
|
||||||
|
type confNtfnSet struct {
|
||||||
|
// ntfns keeps tracks of all the active client notification requests for
|
||||||
|
// a transaction.
|
||||||
|
ntfns map[uint64]*ConfNtfn
|
||||||
|
|
||||||
|
// rescanStatus represents the current rescan state for the transaction.
|
||||||
|
rescanStatus rescanState
|
||||||
|
|
||||||
|
// details serves as a cache of the confirmation details of a
|
||||||
|
// transaction that we'll use to determine if a transaction has already
|
||||||
|
// confirmed at the time of registration.
|
||||||
|
details *TxConfirmation
|
||||||
|
}
|
||||||
|
|
||||||
|
// newConfNtfnSet constructs a fresh confNtfnSet for a group of clients
|
||||||
|
// interested in a notification for a particular txid.
|
||||||
|
func newConfNtfnSet() *confNtfnSet {
|
||||||
|
return &confNtfnSet{
|
||||||
|
ntfns: make(map[uint64]*ConfNtfn),
|
||||||
|
rescanStatus: rescanNotStarted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// spendNtfnSet holds all known, registered spend notifications for an outpoint.
|
||||||
|
// If duplicate notifications are requested, only one historical dispatch will
|
||||||
|
// be spawned to ensure redundant scans are not permitted.
|
||||||
|
type spendNtfnSet struct {
|
||||||
|
// ntfns keeps tracks of all the active client notification requests for
|
||||||
|
// an outpoint.
|
||||||
|
ntfns map[uint64]*SpendNtfn
|
||||||
|
|
||||||
|
// rescanStatus represents the current rescan state for the outpoint.
|
||||||
|
rescanStatus rescanState
|
||||||
|
|
||||||
|
// details serves as a cache of the spend details for an outpoint that
|
||||||
|
// we'll use to determine if an outpoint has already been spent at the
|
||||||
|
// time of registration.
|
||||||
|
details *SpendDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSpendNtfnSet constructs a new spend notification set.
|
||||||
|
func newSpendNtfnSet() *spendNtfnSet {
|
||||||
|
return &spendNtfnSet{
|
||||||
|
ntfns: make(map[uint64]*SpendNtfn),
|
||||||
|
rescanStatus: rescanNotStarted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfNtfn represents a notifier client's request to receive a notification
|
||||||
|
// once the target transaction gets sufficient confirmations. The client is
|
||||||
|
// asynchronously notified via the ConfirmationEvent channels.
|
||||||
|
type ConfNtfn struct {
|
||||||
|
// ConfID uniquely identifies the confirmation notification request for
|
||||||
|
// the specified transaction.
|
||||||
|
ConfID uint64
|
||||||
|
|
||||||
|
// TxID is the hash of the transaction for which confirmation notifications
|
||||||
|
// are requested.
|
||||||
|
TxID *chainhash.Hash
|
||||||
|
|
||||||
|
// PkScript is the public key script of an outpoint created in this
|
||||||
|
// transaction.
|
||||||
|
//
|
||||||
|
// NOTE: This value MUST be set when the dispatch is to be performed
|
||||||
|
// using compact filters.
|
||||||
|
PkScript []byte
|
||||||
|
|
||||||
|
// NumConfirmations is the number of confirmations after which the
|
||||||
|
// notification is to be sent.
|
||||||
|
NumConfirmations uint32
|
||||||
|
|
||||||
|
// Event contains references to the channels that the notifications are to
|
||||||
|
// be sent over.
|
||||||
|
Event *ConfirmationEvent
|
||||||
|
|
||||||
|
// HeightHint is the minimum height in the chain that we expect to find
|
||||||
|
// this txid.
|
||||||
|
HeightHint uint32
|
||||||
|
|
||||||
|
// dispatched is false if the confirmed notification has not been sent yet.
|
||||||
|
dispatched bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistoricalConfDispatch parameterizes a manual rescan for a particular
|
||||||
|
// transaction identifier. The parameters include the start and end block
|
||||||
|
// heights specifying the range of blocks to scan.
|
||||||
|
type HistoricalConfDispatch struct {
|
||||||
|
// TxID is the transaction ID to search for in the historical dispatch.
|
||||||
|
TxID *chainhash.Hash
|
||||||
|
|
||||||
|
// PkScript is a public key script from an output created by this
|
||||||
|
// transaction.
|
||||||
|
//
|
||||||
|
// NOTE: This value MUST be set when the dispatch is to be performed
|
||||||
|
// using compact filters.
|
||||||
|
PkScript []byte
|
||||||
|
|
||||||
|
// StartHeight specifies the block height at which to being the
|
||||||
|
// historical rescan.
|
||||||
|
StartHeight uint32
|
||||||
|
|
||||||
|
// EndHeight specifies the last block height (inclusive) that the
|
||||||
|
// historical scan should consider.
|
||||||
|
EndHeight uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpendNtfn represents a client's request to receive a notification once an
|
||||||
|
// outpoint has been spent on-chain. The client is asynchronously notified via
|
||||||
|
// the SpendEvent channels.
|
||||||
|
type SpendNtfn struct {
|
||||||
|
// SpendID uniquely identies the spend notification request for the
|
||||||
|
// specified outpoint.
|
||||||
|
SpendID uint64
|
||||||
|
|
||||||
|
// OutPoint is the outpoint for which a client has requested a spend
|
||||||
|
// notification for.
|
||||||
|
OutPoint wire.OutPoint
|
||||||
|
|
||||||
|
// PkScript is the script of the outpoint. This is needed in order to
|
||||||
|
// match compact filters when attempting a historical rescan to
|
||||||
|
// determine if the outpoint has already been spent.
|
||||||
|
PkScript []byte
|
||||||
|
|
||||||
|
// Event contains references to the channels that the notifications are
|
||||||
|
// to be sent over.
|
||||||
|
Event *SpendEvent
|
||||||
|
|
||||||
|
// HeightHint is the earliest height in the chain that we expect to find
|
||||||
|
// the spending transaction of the specified outpoint. This value will
|
||||||
|
// be overridden by the spend hint cache if it contains an entry for it.
|
||||||
|
HeightHint uint32
|
||||||
|
|
||||||
|
// dispatched signals whether a spend notification has been disptached
|
||||||
|
// to the client.
|
||||||
|
dispatched bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistoricalSpendDispatch parameterizes a manual rescan to determine the
|
||||||
|
// spending details (if any) of an outpoint. The parameters include the start
|
||||||
|
// and end block heights specifying the range of blocks to scan.
|
||||||
|
type HistoricalSpendDispatch struct {
|
||||||
|
// OutPoint is the outpoint which we should attempt to find the spending
|
||||||
|
OutPoint wire.OutPoint
|
||||||
|
|
||||||
|
// PkScript is the script of the outpoint. This is needed in order to
|
||||||
|
// match compact filters when attempting a historical rescan.
|
||||||
|
PkScript []byte
|
||||||
|
|
||||||
|
// StartHeight specified the block height at which to begin the
|
||||||
|
// historical rescan.
|
||||||
|
StartHeight uint32
|
||||||
|
|
||||||
|
// EndHeight specifies the last block height (inclusive) that the
|
||||||
|
// historical rescan should consider.
|
||||||
|
EndHeight uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// TxNotifier is a struct responsible for delivering transaction notifications
|
||||||
|
// to subscribers. These notifications can be of two different types:
|
||||||
|
// transaction confirmations and/or outpoint spends. The TxNotifier will watch
|
||||||
|
// the blockchain as new blocks come in, in order to satisfy its client
|
||||||
|
// requests.
|
||||||
|
type TxNotifier struct {
|
||||||
|
// currentHeight is the height of the tracked blockchain. It is used to
|
||||||
|
// determine the number of confirmations a tx has and ensure blocks are
|
||||||
|
// connected and disconnected in order.
|
||||||
|
currentHeight uint32
|
||||||
|
|
||||||
|
// reorgSafetyLimit is the chain depth beyond which it is assumed a
|
||||||
|
// block will not be reorganized out of the chain. This is used to
|
||||||
|
// determine when to prune old notification requests so that reorgs are
|
||||||
|
// handled correctly. The coinbase maturity period is a reasonable value
|
||||||
|
// to use.
|
||||||
|
reorgSafetyLimit uint32
|
||||||
|
|
||||||
|
// reorgDepth is the depth of a chain organization that this system is
|
||||||
|
// being informed of. This is incremented as long as a sequence of
|
||||||
|
// blocks are disconnected without being interrupted by a new block.
|
||||||
|
reorgDepth uint32
|
||||||
|
|
||||||
|
// confNotifications is an index of notification requests by transaction
|
||||||
|
// hash.
|
||||||
|
confNotifications map[chainhash.Hash]*confNtfnSet
|
||||||
|
|
||||||
|
// txsByInitialHeight is an index of watched transactions by the height
|
||||||
|
// that they are included at in the blockchain. This is tracked so that
|
||||||
|
// incorrect notifications are not sent if a transaction is reorged out
|
||||||
|
// of the chain and so that negative confirmations can be recognized.
|
||||||
|
txsByInitialHeight map[uint32]map[chainhash.Hash]struct{}
|
||||||
|
|
||||||
|
// ntfnsByConfirmHeight is an index of notification requests by the
|
||||||
|
// height at which the transaction will have sufficient confirmations.
|
||||||
|
ntfnsByConfirmHeight map[uint32]map[*ConfNtfn]struct{}
|
||||||
|
|
||||||
|
// spendNotifications is an index of all active notification requests
|
||||||
|
// per outpoint.
|
||||||
|
spendNotifications map[wire.OutPoint]*spendNtfnSet
|
||||||
|
|
||||||
|
// opsBySpendHeight is an index that keeps tracks of the spending height
|
||||||
|
// of an outpoint we are currently tracking notifications for. This is
|
||||||
|
// used in order to recover from the spending transaction of an outpoint
|
||||||
|
// being reorged out of the chain.
|
||||||
|
opsBySpendHeight map[uint32]map[wire.OutPoint]struct{}
|
||||||
|
|
||||||
|
// confirmHintCache is a cache used to maintain the latest height hints
|
||||||
|
// for transactions. Each height hint represents the earliest height at
|
||||||
|
// which the transactions could have been confirmed within the chain.
|
||||||
|
confirmHintCache ConfirmHintCache
|
||||||
|
|
||||||
|
// spendHintCache is a cache used to maintain the latest height hints
|
||||||
|
// for outpoints. Each height hint represents the earliest height at
|
||||||
|
// which the outpoints could have been spent within the chain.
|
||||||
|
spendHintCache SpendHintCache
|
||||||
|
|
||||||
|
// quit is closed in order to signal that the notifier is gracefully
|
||||||
|
// exiting.
|
||||||
|
quit chan struct{}
|
||||||
|
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTxNotifier creates a TxNotifier. The current height of the blockchain is
|
||||||
|
// accepted as a parameter. The different hint caches (confirm and spend) are
|
||||||
|
// used as an optimization in order to retrieve a better starting point when
|
||||||
|
// dispatching a recan for a historical event in the chain.
|
||||||
|
func NewTxNotifier(startHeight uint32, reorgSafetyLimit uint32,
|
||||||
|
confirmHintCache ConfirmHintCache,
|
||||||
|
spendHintCache SpendHintCache) *TxNotifier {
|
||||||
|
|
||||||
|
return &TxNotifier{
|
||||||
|
currentHeight: startHeight,
|
||||||
|
reorgSafetyLimit: reorgSafetyLimit,
|
||||||
|
confNotifications: make(map[chainhash.Hash]*confNtfnSet),
|
||||||
|
txsByInitialHeight: make(map[uint32]map[chainhash.Hash]struct{}),
|
||||||
|
ntfnsByConfirmHeight: make(map[uint32]map[*ConfNtfn]struct{}),
|
||||||
|
spendNotifications: make(map[wire.OutPoint]*spendNtfnSet),
|
||||||
|
opsBySpendHeight: make(map[uint32]map[wire.OutPoint]struct{}),
|
||||||
|
confirmHintCache: confirmHintCache,
|
||||||
|
spendHintCache: spendHintCache,
|
||||||
|
quit: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterConf handles a new notification request. The client will be notified
|
||||||
|
// when the transaction gets a sufficient number of confirmations on the
|
||||||
|
// blockchain. The registration succeeds if no error is returned. If the
|
||||||
|
// returned HistoricalConfDispatch is non-nil, the caller is responsible for
|
||||||
|
// attempting to manually rescan blocks for the txid between the start and end
|
||||||
|
// heights.
|
||||||
|
//
|
||||||
|
// NOTE: If the transaction has already been included in a block on the chain,
|
||||||
|
// the confirmation details must be provided with the UpdateConfDetails method,
|
||||||
|
// otherwise we will wait for the transaction to confirm even though it already
|
||||||
|
// has.
|
||||||
|
func (n *TxNotifier) RegisterConf(ntfn *ConfNtfn) (*HistoricalConfDispatch, error) {
|
||||||
|
select {
|
||||||
|
case <-n.quit:
|
||||||
|
return nil, ErrTxNotifierExiting
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce that we will not dispatch confirmations beyond the reorg
|
||||||
|
// safety limit.
|
||||||
|
if ntfn.NumConfirmations > n.reorgSafetyLimit {
|
||||||
|
return nil, ErrTxMaxConfs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before proceeding to register the notification, we'll query our
|
||||||
|
// height hint cache to determine whether a better one exists.
|
||||||
|
//
|
||||||
|
// TODO(conner): verify that all submitted height hints are identical.
|
||||||
|
startHeight := ntfn.HeightHint
|
||||||
|
hint, err := n.confirmHintCache.QueryConfirmHint(*ntfn.TxID)
|
||||||
|
if err == nil {
|
||||||
|
if hint > startHeight {
|
||||||
|
Log.Debugf("Using height hint %d retrieved "+
|
||||||
|
"from cache for %v", hint, *ntfn.TxID)
|
||||||
|
startHeight = hint
|
||||||
|
}
|
||||||
|
} else if err != ErrConfirmHintNotFound {
|
||||||
|
Log.Errorf("Unable to query confirm hint for %v: %v",
|
||||||
|
*ntfn.TxID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n.Lock()
|
||||||
|
defer n.Unlock()
|
||||||
|
|
||||||
|
confSet, ok := n.confNotifications[*ntfn.TxID]
|
||||||
|
if !ok {
|
||||||
|
// If this is the first registration for this txid, construct a
|
||||||
|
// confSet to coalesce all notifications for the same txid.
|
||||||
|
confSet = newConfNtfnSet()
|
||||||
|
n.confNotifications[*ntfn.TxID] = confSet
|
||||||
|
}
|
||||||
|
|
||||||
|
confSet.ntfns[ntfn.ConfID] = ntfn
|
||||||
|
|
||||||
|
switch confSet.rescanStatus {
|
||||||
|
|
||||||
|
// A prior rescan has already completed and we are actively watching at
|
||||||
|
// tip for this txid.
|
||||||
|
case rescanComplete:
|
||||||
|
// If conf details for this set of notifications has already
|
||||||
|
// been found, we'll attempt to deliver them immediately to this
|
||||||
|
// client.
|
||||||
|
Log.Debugf("Attempting to dispatch conf for txid=%v "+
|
||||||
|
"on registration since rescan has finished", ntfn.TxID)
|
||||||
|
return nil, n.dispatchConfDetails(ntfn, confSet.details)
|
||||||
|
|
||||||
|
// A rescan is already in progress, return here to prevent dispatching
|
||||||
|
// another. When the scan returns, this notifications details will be
|
||||||
|
// updated as well.
|
||||||
|
case rescanPending:
|
||||||
|
Log.Debugf("Waiting for pending rescan to finish before "+
|
||||||
|
"notifying txid=%v at tip", ntfn.TxID)
|
||||||
|
return nil, nil
|
||||||
|
|
||||||
|
// If no rescan has been dispatched, attempt to do so now.
|
||||||
|
case rescanNotStarted:
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the provided or cached height hint indicates that the transaction
|
||||||
|
// is to be confirmed at a height greater than the conf notifier's
|
||||||
|
// current height, we'll refrain from spawning a historical dispatch.
|
||||||
|
if startHeight > n.currentHeight {
|
||||||
|
Log.Debugf("Height hint is above current height, not dispatching "+
|
||||||
|
"historical rescan for txid=%v ", ntfn.TxID)
|
||||||
|
// Set the rescan status to complete, which will allow the conf
|
||||||
|
// notifier to start delivering messages for this set
|
||||||
|
// immediately.
|
||||||
|
confSet.rescanStatus = rescanComplete
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Debugf("Dispatching historical rescan for txid=%v ", ntfn.TxID)
|
||||||
|
|
||||||
|
// Construct the parameters for historical dispatch, scanning the range
|
||||||
|
// of blocks between our best known height hint and the notifier's
|
||||||
|
// current height. The notifier will begin also watching for
|
||||||
|
// confirmations at tip starting with the next block.
|
||||||
|
dispatch := &HistoricalConfDispatch{
|
||||||
|
TxID: ntfn.TxID,
|
||||||
|
PkScript: ntfn.PkScript,
|
||||||
|
StartHeight: startHeight,
|
||||||
|
EndHeight: n.currentHeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set this confSet's status to pending, ensuring subsequent
|
||||||
|
// registrations don't also attempt a dispatch.
|
||||||
|
confSet.rescanStatus = rescanPending
|
||||||
|
|
||||||
|
return dispatch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateConfDetails attempts to update the confirmation details for an active
|
||||||
|
// notification within the notifier. This should only be used in the case of a
|
||||||
|
// transaction that has confirmed before the notifier's current height.
|
||||||
|
//
|
||||||
|
// NOTE: The notification should be registered first to ensure notifications are
|
||||||
|
// dispatched correctly.
|
||||||
|
func (n *TxNotifier) UpdateConfDetails(txid chainhash.Hash,
|
||||||
|
details *TxConfirmation) error {
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-n.quit:
|
||||||
|
return ErrTxNotifierExiting
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we hold the lock throughout handling the notification to
|
||||||
|
// prevent the notifier from advancing its height underneath us.
|
||||||
|
n.Lock()
|
||||||
|
defer n.Unlock()
|
||||||
|
|
||||||
|
// First, we'll determine whether we have an active notification for
|
||||||
|
// this transaction with the given ID.
|
||||||
|
confSet, ok := n.confNotifications[txid]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("no notification found with TxID %v", txid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the conf details were already found at tip, all existing
|
||||||
|
// notifications will have been dispatched or queued for dispatch. We
|
||||||
|
// can exit early to avoid sending too many notifications on the
|
||||||
|
// buffered channels.
|
||||||
|
if confSet.details != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The historical dispatch has been completed for this confSet. We'll
|
||||||
|
// update the rescan status and cache any details that were found. If
|
||||||
|
// the details are nil, that implies we did not find them and will
|
||||||
|
// continue to watch for them at tip.
|
||||||
|
confSet.rescanStatus = rescanComplete
|
||||||
|
|
||||||
|
// The notifier has yet to reach the height at which the transaction was
|
||||||
|
// included in a block, so we should defer until handling it then within
|
||||||
|
// ConnectTip.
|
||||||
|
if details == nil {
|
||||||
|
Log.Debugf("Conf details for txid=%v not found during "+
|
||||||
|
"historical dispatch, waiting to dispatch at tip", txid)
|
||||||
|
|
||||||
|
// We'll commit the current height as the confirm hint to
|
||||||
|
// prevent another potentially long rescan if we restart before
|
||||||
|
// a new block comes in.
|
||||||
|
err := n.confirmHintCache.CommitConfirmHint(
|
||||||
|
n.currentHeight, txid,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// The error is not fatal as this is an optimistic
|
||||||
|
// optimization, so we'll avoid returning an error.
|
||||||
|
Log.Debugf("Unable to update confirm hint to %d for "+
|
||||||
|
"%v: %v", n.currentHeight, txid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if details.BlockHeight > n.currentHeight {
|
||||||
|
Log.Debugf("Conf details for txid=%v found above current "+
|
||||||
|
"height, waiting to dispatch at tip", txid)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Debugf("Updating conf details for txid=%v details", txid)
|
||||||
|
|
||||||
|
err := n.confirmHintCache.CommitConfirmHint(details.BlockHeight, txid)
|
||||||
|
if err != nil {
|
||||||
|
// The error is not fatal, so we should not return an error to
|
||||||
|
// the caller.
|
||||||
|
Log.Errorf("Unable to update confirm hint to %d for %v: %v",
|
||||||
|
details.BlockHeight, txid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the details found in the rescan and attempt to dispatch any
|
||||||
|
// notifications that have not yet been delivered.
|
||||||
|
confSet.details = details
|
||||||
|
for _, ntfn := range confSet.ntfns {
|
||||||
|
err = n.dispatchConfDetails(ntfn, details)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatchConfDetails attempts to cache and dispatch details to a particular
|
||||||
|
// client if the transaction has sufficiently confirmed. If the provided details
|
||||||
|
// are nil, this method will be a no-op.
|
||||||
|
func (n *TxNotifier) dispatchConfDetails(
|
||||||
|
ntfn *ConfNtfn, details *TxConfirmation) error {
|
||||||
|
|
||||||
|
// If no details are provided, return early as we can't dispatch.
|
||||||
|
if details == nil {
|
||||||
|
Log.Debugf("Unable to dispatch %v, no details provided",
|
||||||
|
ntfn.TxID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, we'll examine whether the transaction of this
|
||||||
|
// notification request has reached its required number of
|
||||||
|
// confirmations. If it has, we'll dispatch a confirmation
|
||||||
|
// notification to the caller.
|
||||||
|
confHeight := details.BlockHeight + ntfn.NumConfirmations - 1
|
||||||
|
if confHeight <= n.currentHeight {
|
||||||
|
Log.Infof("Dispatching %v conf notification for %v",
|
||||||
|
ntfn.NumConfirmations, ntfn.TxID)
|
||||||
|
|
||||||
|
// We'll send a 0 value to the Updates channel,
|
||||||
|
// indicating that the transaction has already been
|
||||||
|
// confirmed.
|
||||||
|
select {
|
||||||
|
case ntfn.Event.Updates <- 0:
|
||||||
|
case <-n.quit:
|
||||||
|
return ErrTxNotifierExiting
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case ntfn.Event.Confirmed <- details:
|
||||||
|
ntfn.dispatched = true
|
||||||
|
case <-n.quit:
|
||||||
|
return ErrTxNotifierExiting
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.Debugf("Queueing %v conf notification for %v at tip ",
|
||||||
|
ntfn.NumConfirmations, ntfn.TxID)
|
||||||
|
|
||||||
|
// Otherwise, we'll keep track of the notification
|
||||||
|
// request by the height at which we should dispatch the
|
||||||
|
// confirmation notification.
|
||||||
|
ntfnSet, exists := n.ntfnsByConfirmHeight[confHeight]
|
||||||
|
if !exists {
|
||||||
|
ntfnSet = make(map[*ConfNtfn]struct{})
|
||||||
|
n.ntfnsByConfirmHeight[confHeight] = ntfnSet
|
||||||
|
}
|
||||||
|
ntfnSet[ntfn] = struct{}{}
|
||||||
|
|
||||||
|
// We'll also send an update to the client of how many
|
||||||
|
// confirmations are left for the transaction to be
|
||||||
|
// confirmed.
|
||||||
|
numConfsLeft := confHeight - n.currentHeight
|
||||||
|
select {
|
||||||
|
case ntfn.Event.Updates <- numConfsLeft:
|
||||||
|
case <-n.quit:
|
||||||
|
return ErrTxNotifierExiting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// As a final check, we'll also watch the transaction if it's
|
||||||
|
// still possible for it to get reorged out of the chain.
|
||||||
|
blockHeight := details.BlockHeight
|
||||||
|
reorgSafeHeight := blockHeight + n.reorgSafetyLimit
|
||||||
|
if reorgSafeHeight > n.currentHeight {
|
||||||
|
txSet, exists := n.txsByInitialHeight[blockHeight]
|
||||||
|
if !exists {
|
||||||
|
txSet = make(map[chainhash.Hash]struct{})
|
||||||
|
n.txsByInitialHeight[blockHeight] = txSet
|
||||||
|
}
|
||||||
|
txSet[*ntfn.TxID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterSpend handles a new spend notification request. The client will be
|
||||||
|
// notified once the outpoint is detected as spent within the chain.
|
||||||
|
//
|
||||||
|
// The registration succeeds if no error is returned. If the returned
|
||||||
|
// HistoricalSpendDisaptch is non-nil, the caller is responsible for attempting
|
||||||
|
// to determine whether the outpoint has been spent between the start and end
|
||||||
|
// heights.
|
||||||
|
//
|
||||||
|
// NOTE: If the outpoint has already been spent within the chain before the
|
||||||
|
// notifier's current tip, the spend details must be provided with the
|
||||||
|
// UpdateSpendDetails method, otherwise we will wait for the outpoint to
|
||||||
|
// be spent at tip, even though it already has.
|
||||||
|
func (n *TxNotifier) RegisterSpend(ntfn *SpendNtfn) (*HistoricalSpendDispatch, error) {
|
||||||
|
select {
|
||||||
|
case <-n.quit:
|
||||||
|
return nil, ErrTxNotifierExiting
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before proceeding to register the notification, we'll query our spend
|
||||||
|
// hint cache to determine whether a better one exists.
|
||||||
|
startHeight := ntfn.HeightHint
|
||||||
|
hint, err := n.spendHintCache.QuerySpendHint(ntfn.OutPoint)
|
||||||
|
if err == nil {
|
||||||
|
if hint > startHeight {
|
||||||
|
Log.Debugf("Using height hint %d retrieved from cache "+
|
||||||
|
"for %v", startHeight, ntfn.OutPoint)
|
||||||
|
startHeight = hint
|
||||||
|
}
|
||||||
|
} else if err != ErrSpendHintNotFound {
|
||||||
|
Log.Errorf("Unable to query spend hint for %v: %v",
|
||||||
|
ntfn.OutPoint, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n.Lock()
|
||||||
|
defer n.Unlock()
|
||||||
|
|
||||||
|
Log.Infof("New spend subscription: spend_id=%d, outpoint=%v, "+
|
||||||
|
"height_hint=%d", ntfn.SpendID, ntfn.OutPoint, ntfn.HeightHint)
|
||||||
|
|
||||||
|
// Keep track of the notification request so that we can properly
|
||||||
|
// dispatch a spend notification later on.
|
||||||
|
spendSet, ok := n.spendNotifications[ntfn.OutPoint]
|
||||||
|
if !ok {
|
||||||
|
// If this is the first registration for the outpoint, we'll
|
||||||
|
// construct a spendNtfnSet to coalesce all notifications.
|
||||||
|
spendSet = newSpendNtfnSet()
|
||||||
|
n.spendNotifications[ntfn.OutPoint] = spendSet
|
||||||
|
}
|
||||||
|
spendSet.ntfns[ntfn.SpendID] = ntfn
|
||||||
|
|
||||||
|
// We'll now let the caller know whether a historical rescan is needed
|
||||||
|
// depending on the current rescan status.
|
||||||
|
switch spendSet.rescanStatus {
|
||||||
|
|
||||||
|
// If the spending details for this outpoint have already been
|
||||||
|
// determined and cached, then we can use them to immediately dispatch
|
||||||
|
// the spend notification to the client.
|
||||||
|
case rescanComplete:
|
||||||
|
return nil, n.dispatchSpendDetails(ntfn, spendSet.details)
|
||||||
|
|
||||||
|
// If there is an active rescan to determine whether the outpoint has
|
||||||
|
// been spent, then we won't trigger another one.
|
||||||
|
case rescanPending:
|
||||||
|
return nil, nil
|
||||||
|
|
||||||
|
// Otherwise, we'll fall through and let the caller know that a rescan
|
||||||
|
// should be dispatched to determine whether the outpoint has already
|
||||||
|
// been spent.
|
||||||
|
case rescanNotStarted:
|
||||||
|
}
|
||||||
|
|
||||||
|
// However, if the spend hint, either provided by the caller or
|
||||||
|
// retrieved from the cache, is found to be at a later height than the
|
||||||
|
// TxNotifier is aware of, then we'll refrain from dispatching a
|
||||||
|
// historical rescan and wait for the spend to come in at tip.
|
||||||
|
if startHeight > n.currentHeight {
|
||||||
|
Log.Debugf("Spend hint of %d for %v is above current height %d",
|
||||||
|
startHeight, ntfn.OutPoint, n.currentHeight)
|
||||||
|
|
||||||
|
// We'll also set the rescan status as complete to ensure that
|
||||||
|
// spend hints for this outpoint get updated upon
|
||||||
|
// connected/disconnected blocks.
|
||||||
|
spendSet.rescanStatus = rescanComplete
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll set the rescan status to pending to ensure subsequent
|
||||||
|
// notifications don't also attempt a historical dispatch.
|
||||||
|
spendSet.rescanStatus = rescanPending
|
||||||
|
|
||||||
|
return &HistoricalSpendDispatch{
|
||||||
|
OutPoint: ntfn.OutPoint,
|
||||||
|
PkScript: ntfn.PkScript,
|
||||||
|
StartHeight: startHeight,
|
||||||
|
EndHeight: n.currentHeight,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelSpend cancels an existing request for a spend notification of an
|
||||||
|
// outpoint. The request is identified by its spend ID.
|
||||||
|
func (n *TxNotifier) CancelSpend(op wire.OutPoint, spendID uint64) {
|
||||||
|
select {
|
||||||
|
case <-n.quit:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
n.Lock()
|
||||||
|
defer n.Unlock()
|
||||||
|
|
||||||
|
Log.Infof("Canceling spend notification: spend_id=%d, outpoint=%v",
|
||||||
|
spendID, op)
|
||||||
|
|
||||||
|
spendSet, ok := n.spendNotifications[op]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ntfn, ok := spendSet.ntfns[spendID]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll close all the notification channels to let the client know
|
||||||
|
// their cancel request has been fulfilled.
|
||||||
|
close(ntfn.Event.Spend)
|
||||||
|
close(ntfn.Event.Reorg)
|
||||||
|
delete(spendSet.ntfns, spendID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessRelevantSpendTx processes a transaction provided externally. This will
|
||||||
|
// check whether the transaction is relevant to the notifier if it spends any
|
||||||
|
// outputs for which we currently have registered notifications for. If it is
|
||||||
|
// relevant, spend notifications will be dispatched to the caller.
|
||||||
|
func (n *TxNotifier) ProcessRelevantSpendTx(tx *wire.MsgTx, txHeight int32) error {
|
||||||
|
select {
|
||||||
|
case <-n.quit:
|
||||||
|
return ErrTxNotifierExiting
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we hold the lock throughout handling the notification to
|
||||||
|
// prevent the notifier from advancing its height underneath us.
|
||||||
|
n.Lock()
|
||||||
|
defer n.Unlock()
|
||||||
|
|
||||||
|
// Grab the set of active registered outpoints to determine if the
|
||||||
|
// transaction spends any of them.
|
||||||
|
spendNtfns := n.spendNotifications
|
||||||
|
|
||||||
|
// We'll check if this transaction spends an output that has an existing
|
||||||
|
// spend notification for it.
|
||||||
|
for i, txIn := range tx.TxIn {
|
||||||
|
// If this input doesn't spend an existing registered outpoint,
|
||||||
|
// we'll go on to the next.
|
||||||
|
prevOut := txIn.PreviousOutPoint
|
||||||
|
if _, ok := spendNtfns[prevOut]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we'll create a spend summary and send off the
|
||||||
|
// details to the notification subscribers.
|
||||||
|
txHash := tx.TxHash()
|
||||||
|
details := &SpendDetail{
|
||||||
|
SpentOutPoint: &prevOut,
|
||||||
|
SpenderTxHash: &txHash,
|
||||||
|
SpendingTx: tx,
|
||||||
|
SpenderInputIndex: uint32(i),
|
||||||
|
SpendingHeight: txHeight,
|
||||||
|
}
|
||||||
|
if err := n.updateSpendDetails(prevOut, details); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSpendDetails attempts to update the spend details for all active spend
|
||||||
|
// notification requests for an outpoint. This method should be used once a
|
||||||
|
// historical scan of the chain has finished. If the historical scan did not
|
||||||
|
// find a spending transaction for the outpoint, the spend details may be nil.
|
||||||
|
//
|
||||||
|
// NOTE: A notification request for the outpoint must be registered first to
|
||||||
|
// ensure notifications are delivered.
|
||||||
|
func (n *TxNotifier) UpdateSpendDetails(op wire.OutPoint,
|
||||||
|
details *SpendDetail) error {
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-n.quit:
|
||||||
|
return ErrTxNotifierExiting
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we hold the lock throughout handling the notification to
|
||||||
|
// prevent the notifier from advancing its height underneath us.
|
||||||
|
n.Lock()
|
||||||
|
defer n.Unlock()
|
||||||
|
|
||||||
|
return n.updateSpendDetails(op, details)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateSpendDetails attempts to update the spend details for all active spend
|
||||||
|
// notification requests for an outpoint. This method should be used once a
|
||||||
|
// historical scan of the chain has finished. If the historical scan did not
|
||||||
|
// find a spending transaction for the outpoint, the spend details may be nil.
|
||||||
|
//
|
||||||
|
// NOTE: This method must be called with the TxNotifier's lock held.
|
||||||
|
func (n *TxNotifier) updateSpendDetails(op wire.OutPoint,
|
||||||
|
details *SpendDetail) error {
|
||||||
|
|
||||||
|
// Mark the ongoing historical rescan for this outpoint as finished.
|
||||||
|
// This will allow us to update the spend hints for this outpoint at
|
||||||
|
// tip.
|
||||||
|
spendSet, ok := n.spendNotifications[op]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("no notifications found for outpoint %v", op)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the spend details have already been found either at tip, then the
|
||||||
|
// notifications should have already been dispatched, so we can exit
|
||||||
|
// early to prevent sending duplicate notifications.
|
||||||
|
if spendSet.details != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since the historical rescan has completed for this outpoint, we'll
|
||||||
|
// mark its rescan status as complete in order to ensure that the
|
||||||
|
// TxNotifier can properly update its spend hints upon
|
||||||
|
// connected/disconnected blocks.
|
||||||
|
spendSet.rescanStatus = rescanComplete
|
||||||
|
|
||||||
|
// If the historical rescan was not able to find a spending transaction
|
||||||
|
// for this outpoint, then we can track the spend at tip.
|
||||||
|
if details == nil {
|
||||||
|
// We'll commit the current height as the spend hint to prevent
|
||||||
|
// another potentially long rescan if we restart before a new
|
||||||
|
// block comes in.
|
||||||
|
err := n.spendHintCache.CommitSpendHint(n.currentHeight, op)
|
||||||
|
if err != nil {
|
||||||
|
// The error is not fatal as this is an optimistic
|
||||||
|
// optimization, so we'll avoid returning an error.
|
||||||
|
Log.Debugf("Unable to update spend hint to %d for %v: %v",
|
||||||
|
n.currentHeight, op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the historical rescan found the spending transaction for this
|
||||||
|
// outpoint, but it's at a later height than the notifier (this can
|
||||||
|
// happen due to latency with the backend during a reorg), then we'll
|
||||||
|
// defer handling the notification until the notifier has caught up to
|
||||||
|
// such height.
|
||||||
|
if uint32(details.SpendingHeight) > n.currentHeight {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we've determined the outpoint has been spent, we'll commit
|
||||||
|
// its spending height as its hint in the cache and dispatch
|
||||||
|
// notifications to all of its respective clients.
|
||||||
|
err := n.spendHintCache.CommitSpendHint(
|
||||||
|
uint32(details.SpendingHeight), op,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// The error is not fatal as this is an optimistic optimization,
|
||||||
|
// so we'll avoid returning an error.
|
||||||
|
Log.Debugf("Unable to update spend hint to %d for %v: %v",
|
||||||
|
details.SpendingHeight, op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spendSet.details = details
|
||||||
|
for _, ntfn := range spendSet.ntfns {
|
||||||
|
err := n.dispatchSpendDetails(ntfn, spendSet.details)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatchSpendDetails dispatches a spend notification to the client.
|
||||||
|
//
|
||||||
|
// NOTE: This must be called with the TxNotifier's lock held.
|
||||||
|
func (n *TxNotifier) dispatchSpendDetails(ntfn *SpendNtfn, details *SpendDetail) error {
|
||||||
|
// If there are no spend details to dispatch or if the notification has
|
||||||
|
// already been dispatched, then we can skip dispatching to this client.
|
||||||
|
if details == nil || ntfn.dispatched {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Infof("Dispatching spend notification for outpoint=%v at height=%d",
|
||||||
|
ntfn.OutPoint, n.currentHeight)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case ntfn.Event.Spend <- details:
|
||||||
|
ntfn.dispatched = true
|
||||||
|
case <-n.quit:
|
||||||
|
return ErrTxNotifierExiting
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectTip handles a new block extending the current chain. It will go
|
||||||
|
// through every transaction and determine if it is relevant to any of its
|
||||||
|
// clients. A transaction can be relevant in either of the following two ways:
|
||||||
|
//
|
||||||
|
// 1. One of the inputs in the transaction spends an outpoint for which we
|
||||||
|
// currently have an active spend registration for.
|
||||||
|
//
|
||||||
|
// 2. The transaction is a transaction for which we currently have an active
|
||||||
|
// confirmation registration for.
|
||||||
|
//
|
||||||
|
// In the event that the transaction is relevant, a confirmation/spend
|
||||||
|
// notification will be dispatched to the relevant clients. Confirmation
|
||||||
|
// notifications will only be dispatched for transactions that have met the
|
||||||
|
// required number of confirmations required by the client.
|
||||||
|
func (n *TxNotifier) ConnectTip(blockHash *chainhash.Hash, blockHeight uint32,
|
||||||
|
txns []*btcutil.Tx) error {
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-n.quit:
|
||||||
|
return ErrTxNotifierExiting
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
n.Lock()
|
||||||
|
defer n.Unlock()
|
||||||
|
|
||||||
|
if blockHeight != n.currentHeight+1 {
|
||||||
|
return fmt.Errorf("Received blocks out of order: "+
|
||||||
|
"current height=%d, new height=%d",
|
||||||
|
n.currentHeight, blockHeight)
|
||||||
|
}
|
||||||
|
n.currentHeight++
|
||||||
|
n.reorgDepth = 0
|
||||||
|
|
||||||
|
// First, we'll iterate over all the transactions found in this block to
|
||||||
|
// determine if it includes any relevant transactions to the TxNotifier.
|
||||||
|
for _, tx := range txns {
|
||||||
|
txHash := tx.Hash()
|
||||||
|
|
||||||
|
// In order to determine if this transaction is relevant to the
|
||||||
|
// notifier, we'll check its inputs for any outstanding spend
|
||||||
|
// notifications.
|
||||||
|
for i, txIn := range tx.MsgTx().TxIn {
|
||||||
|
prevOut := txIn.PreviousOutPoint
|
||||||
|
spendSet, ok := n.spendNotifications[prevOut]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have any, we'll record its spend height so that
|
||||||
|
// notifications get dispatched to the respective
|
||||||
|
// clients.
|
||||||
|
spendDetails := &SpendDetail{
|
||||||
|
SpentOutPoint: &prevOut,
|
||||||
|
SpenderTxHash: txHash,
|
||||||
|
SpendingTx: tx.MsgTx(),
|
||||||
|
SpenderInputIndex: uint32(i),
|
||||||
|
SpendingHeight: int32(blockHeight),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(wilmer): cancel pending historical rescans if any?
|
||||||
|
spendSet.rescanStatus = rescanComplete
|
||||||
|
spendSet.details = spendDetails
|
||||||
|
for _, ntfn := range spendSet.ntfns {
|
||||||
|
// In the event that this notification was aware
|
||||||
|
// that the spending transaction of its outpoint
|
||||||
|
// was reorged out of the chain, we'll consume
|
||||||
|
// the reorg notification if it hasn't been
|
||||||
|
// done yet already.
|
||||||
|
select {
|
||||||
|
case <-ntfn.Event.Reorg:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll note the outpoints spending height in order to
|
||||||
|
// correctly handle dispatching notifications when the
|
||||||
|
// spending transactions gets reorged out of the chain.
|
||||||
|
opSet, exists := n.opsBySpendHeight[blockHeight]
|
||||||
|
if !exists {
|
||||||
|
opSet = make(map[wire.OutPoint]struct{})
|
||||||
|
n.opsBySpendHeight[blockHeight] = opSet
|
||||||
|
}
|
||||||
|
opSet[prevOut] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have any pending notifications for this txid. If
|
||||||
|
// none are found, we can proceed to the next transaction.
|
||||||
|
confSet, ok := n.confNotifications[*txHash]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Debugf("Block contains txid=%v, constructing details",
|
||||||
|
txHash)
|
||||||
|
|
||||||
|
// If we have any, we'll record its confirmed height so that
|
||||||
|
// notifications get dispatched when the transaction reaches the
|
||||||
|
// clients' desired number of confirmations.
|
||||||
|
details := &TxConfirmation{
|
||||||
|
BlockHash: blockHash,
|
||||||
|
BlockHeight: blockHeight,
|
||||||
|
TxIndex: uint32(tx.Index()),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(wilmer): cancel pending historical rescans if any?
|
||||||
|
confSet.rescanStatus = rescanComplete
|
||||||
|
confSet.details = details
|
||||||
|
for _, ntfn := range confSet.ntfns {
|
||||||
|
// In the event that this notification was aware that
|
||||||
|
// the transaction was reorged out of the chain, we'll
|
||||||
|
// consume the reorg notification if it hasn't been done
|
||||||
|
// yet already.
|
||||||
|
select {
|
||||||
|
case <-ntfn.Event.NegativeConf:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll note this client's required number of
|
||||||
|
// confirmations so that we can notify them when
|
||||||
|
// expected.
|
||||||
|
confHeight := blockHeight + ntfn.NumConfirmations - 1
|
||||||
|
ntfnSet, exists := n.ntfnsByConfirmHeight[confHeight]
|
||||||
|
if !exists {
|
||||||
|
ntfnSet = make(map[*ConfNtfn]struct{})
|
||||||
|
n.ntfnsByConfirmHeight[confHeight] = ntfnSet
|
||||||
|
}
|
||||||
|
ntfnSet[ntfn] = struct{}{}
|
||||||
|
|
||||||
|
// We'll also note the initial confirmation height in
|
||||||
|
// order to correctly handle dispatching notifications
|
||||||
|
// when the transaction gets reorged out of the chain.
|
||||||
|
txSet, exists := n.txsByInitialHeight[blockHeight]
|
||||||
|
if !exists {
|
||||||
|
txSet = make(map[chainhash.Hash]struct{})
|
||||||
|
n.txsByInitialHeight[blockHeight] = txSet
|
||||||
|
}
|
||||||
|
txSet[*txHash] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we've determined which transactions were confirmed and which
|
||||||
|
// outpoints were spent within the new block, we can update their
|
||||||
|
// entries in their respective caches, along with all of our unconfirmed
|
||||||
|
// transactions and unspent outpoints.
|
||||||
|
n.updateHints(blockHeight)
|
||||||
|
|
||||||
|
// Next, we'll dispatch an update to all of the notification clients for
|
||||||
|
// our watched transactions with the number of confirmations left at
|
||||||
|
// this new height.
|
||||||
|
for _, txHashes := range n.txsByInitialHeight {
|
||||||
|
for txHash := range txHashes {
|
||||||
|
confSet := n.confNotifications[txHash]
|
||||||
|
for _, ntfn := range confSet.ntfns {
|
||||||
|
txConfHeight := confSet.details.BlockHeight +
|
||||||
|
ntfn.NumConfirmations - 1
|
||||||
|
numConfsLeft := txConfHeight - blockHeight
|
||||||
|
|
||||||
|
// Since we don't clear notifications until
|
||||||
|
// transactions are no longer under the risk of
|
||||||
|
// being reorganized out of the chain, we'll
|
||||||
|
// skip sending updates for transactions that
|
||||||
|
// have already been confirmed.
|
||||||
|
if int32(numConfsLeft) < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case ntfn.Event.Updates <- numConfsLeft:
|
||||||
|
case <-n.quit:
|
||||||
|
return ErrTxNotifierExiting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, we'll dispatch notifications for all the transactions that have
|
||||||
|
// become confirmed at this new block height.
|
||||||
|
for ntfn := range n.ntfnsByConfirmHeight[blockHeight] {
|
||||||
|
confSet := n.confNotifications[*ntfn.TxID]
|
||||||
|
|
||||||
|
Log.Infof("Dispatching %v conf notification for %v",
|
||||||
|
ntfn.NumConfirmations, ntfn.TxID)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case ntfn.Event.Confirmed <- confSet.details:
|
||||||
|
ntfn.dispatched = true
|
||||||
|
case <-n.quit:
|
||||||
|
return ErrTxNotifierExiting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(n.ntfnsByConfirmHeight, blockHeight)
|
||||||
|
|
||||||
|
// We'll also dispatch spend notifications for all the outpoints that
|
||||||
|
// were spent at this new block height.
|
||||||
|
for op := range n.opsBySpendHeight[blockHeight] {
|
||||||
|
spendSet := n.spendNotifications[op]
|
||||||
|
for _, ntfn := range spendSet.ntfns {
|
||||||
|
err := n.dispatchSpendDetails(ntfn, spendSet.details)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we'll clear the entries from our set of notifications for
|
||||||
|
// transactions and outpoints that are no longer under the risk of being
|
||||||
|
// reorged out of the chain.
|
||||||
|
if blockHeight >= n.reorgSafetyLimit {
|
||||||
|
matureBlockHeight := blockHeight - n.reorgSafetyLimit
|
||||||
|
for tx := range n.txsByInitialHeight[matureBlockHeight] {
|
||||||
|
delete(n.confNotifications, tx)
|
||||||
|
}
|
||||||
|
delete(n.txsByInitialHeight, matureBlockHeight)
|
||||||
|
for op := range n.opsBySpendHeight[matureBlockHeight] {
|
||||||
|
delete(n.spendNotifications, op)
|
||||||
|
}
|
||||||
|
delete(n.opsBySpendHeight, matureBlockHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisconnectTip handles the tip of the current chain being disconnected during
|
||||||
|
// a chain reorganization. If any watched transactions or spending transactions
|
||||||
|
// for registered outpoints were included in this block, internal structures are
|
||||||
|
// updated to ensure confirmation/spend notifications are consumed (if not
|
||||||
|
// already), and reorg notifications are dispatched instead. Confirmation/spend
|
||||||
|
// notifications will be dispatched again upon block inclusion.
|
||||||
|
func (n *TxNotifier) DisconnectTip(blockHeight uint32) error {
|
||||||
|
select {
|
||||||
|
case <-n.quit:
|
||||||
|
return ErrTxNotifierExiting
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
n.Lock()
|
||||||
|
defer n.Unlock()
|
||||||
|
|
||||||
|
if blockHeight != n.currentHeight {
|
||||||
|
return fmt.Errorf("Received blocks out of order: "+
|
||||||
|
"current height=%d, disconnected height=%d",
|
||||||
|
n.currentHeight, blockHeight)
|
||||||
|
}
|
||||||
|
n.currentHeight--
|
||||||
|
n.reorgDepth++
|
||||||
|
|
||||||
|
// With the block disconnected, we'll update the confirm and spend hints
|
||||||
|
// for our transactions and outpoints to reflect the new height, except
|
||||||
|
// for those that have confirmed/spent at previous heights.
|
||||||
|
n.updateHints(blockHeight)
|
||||||
|
|
||||||
|
// We'll go through all of our watched transactions and attempt to drain
|
||||||
|
// their notification channels to ensure sending notifications to the
|
||||||
|
// clients is always non-blocking.
|
||||||
|
for initialHeight, txHashes := range n.txsByInitialHeight {
|
||||||
|
for txHash := range txHashes {
|
||||||
|
// If the transaction has been reorged out of the chain,
|
||||||
|
// we'll make sure to remove the cached confirmation
|
||||||
|
// details to prevent notifying clients with old
|
||||||
|
// information.
|
||||||
|
confSet := n.confNotifications[txHash]
|
||||||
|
if initialHeight == blockHeight {
|
||||||
|
confSet.details = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ntfn := range confSet.ntfns {
|
||||||
|
// First, we'll attempt to drain an update
|
||||||
|
// from each notification to ensure sends to the
|
||||||
|
// Updates channel are always non-blocking.
|
||||||
|
select {
|
||||||
|
case <-ntfn.Event.Updates:
|
||||||
|
case <-n.quit:
|
||||||
|
return ErrTxNotifierExiting
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, we'll check if the current transaction
|
||||||
|
// was included in the block currently being
|
||||||
|
// disconnected. If it was, we'll need to
|
||||||
|
// dispatch a reorg notification to the client.
|
||||||
|
if initialHeight == blockHeight {
|
||||||
|
err := n.dispatchConfReorg(
|
||||||
|
ntfn, blockHeight,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll also go through our watched outpoints and attempt to drain
|
||||||
|
// their dispatched notifications to ensure dispatching notifications to
|
||||||
|
// clients later on is always non-blocking. We're only interested in
|
||||||
|
// outpoints whose spending transaction was included at the height being
|
||||||
|
// disconnected.
|
||||||
|
for op := range n.opsBySpendHeight[blockHeight] {
|
||||||
|
// Since the spending transaction is being reorged out of the
|
||||||
|
// chain, we'll need to clear out the spending details of the
|
||||||
|
// outpoint.
|
||||||
|
spendSet := n.spendNotifications[op]
|
||||||
|
spendSet.details = nil
|
||||||
|
|
||||||
|
// For all requests which have had a spend notification
|
||||||
|
// dispatched, we'll attempt to drain it and send a reorg
|
||||||
|
// notification instead.
|
||||||
|
for _, ntfn := range spendSet.ntfns {
|
||||||
|
if err := n.dispatchSpendReorg(ntfn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we can remove the transactions that were confirmed and the
|
||||||
|
// outpoints that were spent at the height being disconnected. We'll
|
||||||
|
// still continue to track them until they have been confirmed/spent and
|
||||||
|
// are no longer under the risk of being reorged out of the chain again.
|
||||||
|
delete(n.txsByInitialHeight, blockHeight)
|
||||||
|
delete(n.opsBySpendHeight, blockHeight)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateHints attempts to update the confirm and spend hints for all relevant
|
||||||
|
// transactions and outpoints respectively. The height parameter is used to
|
||||||
|
// determine which transactions and outpoints we should update based on whether
|
||||||
|
// a new block is being connected/disconnected.
|
||||||
|
//
|
||||||
|
// NOTE: This must be called with the TxNotifier's lock held and after its
|
||||||
|
// height has already been reflected by a block being connected/disconnected.
|
||||||
|
func (n *TxNotifier) updateHints(height uint32) {
|
||||||
|
// TODO(wilmer): update under one database transaction.
|
||||||
|
//
|
||||||
|
// To update the height hint for all the required transactions under one
|
||||||
|
// database transaction, we'll gather the set of unconfirmed
|
||||||
|
// transactions along with the ones that confirmed at the height being
|
||||||
|
// connected/disconnected.
|
||||||
|
txsToUpdateHints := n.unconfirmedTxs()
|
||||||
|
for confirmedTx := range n.txsByInitialHeight[height] {
|
||||||
|
txsToUpdateHints = append(txsToUpdateHints, confirmedTx)
|
||||||
|
}
|
||||||
|
err := n.confirmHintCache.CommitConfirmHint(
|
||||||
|
n.currentHeight, txsToUpdateHints...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// The error is not fatal as this is an optimistic optimization,
|
||||||
|
// so we'll avoid returning an error.
|
||||||
|
Log.Debugf("Unable to update confirm hints to %d for "+
|
||||||
|
"%v: %v", n.currentHeight, txsToUpdateHints, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similarly, to update the height hint for all the required outpoints
|
||||||
|
// under one database transaction, we'll gather the set of unspent
|
||||||
|
// outpoints along with the ones that were spent at the height being
|
||||||
|
// connected/disconnected.
|
||||||
|
opsToUpdateHints := n.unspentOutPoints()
|
||||||
|
for spentOp := range n.opsBySpendHeight[height] {
|
||||||
|
opsToUpdateHints = append(opsToUpdateHints, spentOp)
|
||||||
|
}
|
||||||
|
err = n.spendHintCache.CommitSpendHint(
|
||||||
|
n.currentHeight, opsToUpdateHints...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// The error is not fatal as this is an optimistic optimization,
|
||||||
|
// so we'll avoid returning an error.
|
||||||
|
Log.Debugf("Unable to update spend hints to %d for "+
|
||||||
|
"%v: %v", n.currentHeight, opsToUpdateHints, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unconfirmedTxs returns the set of transactions that are still seen as
|
||||||
|
// unconfirmed by the TxNotifier.
|
||||||
|
//
|
||||||
|
// NOTE: This method must be called with the TxNotifier's lock held.
|
||||||
|
func (n *TxNotifier) unconfirmedTxs() []chainhash.Hash {
|
||||||
|
var unconfirmedTxs []chainhash.Hash
|
||||||
|
for tx, confNtfnSet := range n.confNotifications {
|
||||||
|
// If the notification is already aware of its confirmation
|
||||||
|
// details, or it's in the process of learning them, we'll skip
|
||||||
|
// it as we can't yet determine if it's confirmed or not.
|
||||||
|
if confNtfnSet.rescanStatus != rescanComplete ||
|
||||||
|
confNtfnSet.details != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
unconfirmedTxs = append(unconfirmedTxs, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return unconfirmedTxs
|
||||||
|
}
|
||||||
|
|
||||||
|
// unspentOutPoints returns the set of outpoints that are still seen as unspent
|
||||||
|
// by the TxNotifier.
|
||||||
|
//
|
||||||
|
// NOTE: This method must be called with the TxNotifier's lock held.
|
||||||
|
func (n *TxNotifier) unspentOutPoints() []wire.OutPoint {
|
||||||
|
var unspentOps []wire.OutPoint
|
||||||
|
for op, spendNtfnSet := range n.spendNotifications {
|
||||||
|
// If the notification is already aware of its spend details, or
|
||||||
|
// it's in the process of learning them, we'll skip it as we
|
||||||
|
// can't yet determine if it's unspent or not.
|
||||||
|
if spendNtfnSet.rescanStatus != rescanComplete ||
|
||||||
|
spendNtfnSet.details != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
unspentOps = append(unspentOps, op)
|
||||||
|
}
|
||||||
|
|
||||||
|
return unspentOps
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatchConfReorg dispatches a reorg notification to the client if the
|
||||||
|
// confirmation notification was already delivered.
|
||||||
|
//
|
||||||
|
// NOTE: This must be called with the TxNotifier's lock held.
|
||||||
|
func (n *TxNotifier) dispatchConfReorg(ntfn *ConfNtfn,
|
||||||
|
heightDisconnected uint32) error {
|
||||||
|
|
||||||
|
// If the transaction's confirmation notification has yet to be
|
||||||
|
// dispatched, we'll need to clear its entry within the
|
||||||
|
// ntfnsByConfirmHeight index to prevent from notifying the client once
|
||||||
|
// the notifier reaches the confirmation height.
|
||||||
|
if !ntfn.dispatched {
|
||||||
|
confHeight := heightDisconnected + ntfn.NumConfirmations - 1
|
||||||
|
ntfnSet, exists := n.ntfnsByConfirmHeight[confHeight]
|
||||||
|
if exists {
|
||||||
|
delete(ntfnSet, ntfn)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, the entry within the ntfnsByConfirmHeight has already been
|
||||||
|
// deleted, so we'll attempt to drain the confirmation notification to
|
||||||
|
// ensure sends to the Confirmed channel are always non-blocking.
|
||||||
|
select {
|
||||||
|
case <-ntfn.Event.Confirmed:
|
||||||
|
case <-n.quit:
|
||||||
|
return ErrTxNotifierExiting
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
ntfn.dispatched = false
|
||||||
|
|
||||||
|
// Send a negative confirmation notification to the client indicating
|
||||||
|
// how many blocks have been disconnected successively.
|
||||||
|
select {
|
||||||
|
case ntfn.Event.NegativeConf <- int32(n.reorgDepth):
|
||||||
|
case <-n.quit:
|
||||||
|
return ErrTxNotifierExiting
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatchSpendReorg dispatches a reorg notification to the client if a spend
|
||||||
|
// notiification was already delivered.
|
||||||
|
//
|
||||||
|
// NOTE: This must be called with the TxNotifier's lock held.
|
||||||
|
func (n *TxNotifier) dispatchSpendReorg(ntfn *SpendNtfn) error {
|
||||||
|
if !ntfn.dispatched {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to drain the spend notification to ensure sends to the Spend
|
||||||
|
// channel are always non-blocking.
|
||||||
|
select {
|
||||||
|
case <-ntfn.Event.Spend:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a reorg notification to the client in order for them to
|
||||||
|
// correctly handle reorgs.
|
||||||
|
select {
|
||||||
|
case ntfn.Event.Reorg <- struct{}{}:
|
||||||
|
case <-n.quit:
|
||||||
|
return ErrTxNotifierExiting
|
||||||
|
}
|
||||||
|
|
||||||
|
ntfn.dispatched = false
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TearDown is to be called when the owner of the TxNotifier is exiting. This
|
||||||
|
// closes the event channels of all registered notifications that have not been
|
||||||
|
// dispatched yet.
|
||||||
|
func (n *TxNotifier) TearDown() {
|
||||||
|
n.Lock()
|
||||||
|
defer n.Unlock()
|
||||||
|
|
||||||
|
close(n.quit)
|
||||||
|
|
||||||
|
for _, confSet := range n.confNotifications {
|
||||||
|
for _, ntfn := range confSet.ntfns {
|
||||||
|
close(ntfn.Event.Confirmed)
|
||||||
|
close(ntfn.Event.Updates)
|
||||||
|
close(ntfn.Event.NegativeConf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, spendSet := range n.spendNotifications {
|
||||||
|
for _, ntfn := range spendSet.ntfns {
|
||||||
|
close(ntfn.Event.Spend)
|
||||||
|
close(ntfn.Event.Reorg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1903
chainntnfs/txnotifier_test.go
Normal file
1903
chainntnfs/txnotifier_test.go
Normal file
@ -0,0 +1,1903 @@
|
|||||||
|
package chainntnfs_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
zeroHash chainhash.Hash
|
||||||
|
zeroOutPoint wire.OutPoint
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockHintCache struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
confHints map[chainhash.Hash]uint32
|
||||||
|
spendHints map[wire.OutPoint]uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ chainntnfs.SpendHintCache = (*mockHintCache)(nil)
|
||||||
|
var _ chainntnfs.ConfirmHintCache = (*mockHintCache)(nil)
|
||||||
|
|
||||||
|
func (c *mockHintCache) CommitSpendHint(heightHint uint32, ops ...wire.OutPoint) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
for _, op := range ops {
|
||||||
|
c.spendHints[op] = heightHint
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mockHintCache) QuerySpendHint(op wire.OutPoint) (uint32, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
hint, ok := c.spendHints[op]
|
||||||
|
if !ok {
|
||||||
|
return 0, chainntnfs.ErrSpendHintNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return hint, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mockHintCache) PurgeSpendHint(ops ...wire.OutPoint) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
for _, op := range ops {
|
||||||
|
delete(c.spendHints, op)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mockHintCache) CommitConfirmHint(heightHint uint32, txids ...chainhash.Hash) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
for _, txid := range txids {
|
||||||
|
c.confHints[txid] = heightHint
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mockHintCache) QueryConfirmHint(txid chainhash.Hash) (uint32, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
hint, ok := c.confHints[txid]
|
||||||
|
if !ok {
|
||||||
|
return 0, chainntnfs.ErrConfirmHintNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return hint, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mockHintCache) PurgeConfirmHint(txids ...chainhash.Hash) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
for _, txid := range txids {
|
||||||
|
delete(c.confHints, txid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockHintCache() *mockHintCache {
|
||||||
|
return &mockHintCache{
|
||||||
|
confHints: make(map[chainhash.Hash]uint32),
|
||||||
|
spendHints: make(map[wire.OutPoint]uint32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTxNotifierFutureConfDispatch tests that the TxNotifier dispatches
|
||||||
|
// registered notifications when a transaction confirms after registration.
|
||||||
|
func TestTxNotifierFutureConfDispatch(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
tx1NumConfs uint32 = 1
|
||||||
|
tx2NumConfs uint32 = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tx1 = wire.MsgTx{Version: 1}
|
||||||
|
tx2 = wire.MsgTx{Version: 2}
|
||||||
|
tx3 = wire.MsgTx{Version: 3}
|
||||||
|
)
|
||||||
|
|
||||||
|
hintCache := newMockHintCache()
|
||||||
|
n := chainntnfs.NewTxNotifier(10, 100, hintCache, hintCache)
|
||||||
|
|
||||||
|
// Create the test transactions and register them with the TxNotifier
|
||||||
|
// before including them in a block to receive future
|
||||||
|
// notifications.
|
||||||
|
tx1Hash := tx1.TxHash()
|
||||||
|
ntfn1 := chainntnfs.ConfNtfn{
|
||||||
|
TxID: &tx1Hash,
|
||||||
|
NumConfirmations: tx1NumConfs,
|
||||||
|
Event: chainntnfs.NewConfirmationEvent(tx1NumConfs),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterConf(&ntfn1); err != nil {
|
||||||
|
t.Fatalf("unable to register ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx2Hash := tx2.TxHash()
|
||||||
|
ntfn2 := chainntnfs.ConfNtfn{
|
||||||
|
TxID: &tx2Hash,
|
||||||
|
NumConfirmations: tx2NumConfs,
|
||||||
|
Event: chainntnfs.NewConfirmationEvent(tx2NumConfs),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterConf(&ntfn2); err != nil {
|
||||||
|
t.Fatalf("unable to register ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should not receive any notifications from both transactions
|
||||||
|
// since they have not been included in a block yet.
|
||||||
|
select {
|
||||||
|
case <-ntfn1.Event.Updates:
|
||||||
|
t.Fatal("Received unexpected confirmation update for tx1")
|
||||||
|
case txConf := <-ntfn1.Event.Confirmed:
|
||||||
|
t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ntfn2.Event.Updates:
|
||||||
|
t.Fatal("Received unexpected confirmation update for tx2")
|
||||||
|
case txConf := <-ntfn2.Event.Confirmed:
|
||||||
|
t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include the transactions in a block and add it to the TxNotifier.
|
||||||
|
// This should confirm tx1, but not tx2.
|
||||||
|
block1 := btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{&tx1, &tx2, &tx3},
|
||||||
|
})
|
||||||
|
|
||||||
|
err := n.ConnectTip(
|
||||||
|
block1.Hash(), 11, block1.Transactions(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should only receive one update for tx1 since it only requires
|
||||||
|
// one confirmation and it already met it.
|
||||||
|
select {
|
||||||
|
case numConfsLeft := <-ntfn1.Event.Updates:
|
||||||
|
const expected = 0
|
||||||
|
if numConfsLeft != expected {
|
||||||
|
t.Fatalf("Received incorrect confirmation update: tx1 "+
|
||||||
|
"expected %d confirmations left, got %d",
|
||||||
|
expected, numConfsLeft)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected confirmation update for tx1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A confirmation notification for this tranaction should be dispatched,
|
||||||
|
// as it only required one confirmation.
|
||||||
|
select {
|
||||||
|
case txConf := <-ntfn1.Event.Confirmed:
|
||||||
|
expectedConf := chainntnfs.TxConfirmation{
|
||||||
|
BlockHash: block1.Hash(),
|
||||||
|
BlockHeight: 11,
|
||||||
|
TxIndex: 0,
|
||||||
|
}
|
||||||
|
assertConfDetails(t, txConf, &expectedConf)
|
||||||
|
default:
|
||||||
|
t.Fatalf("Expected confirmation for tx1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should only receive one update for tx2 since it only has one
|
||||||
|
// confirmation so far and it requires two.
|
||||||
|
select {
|
||||||
|
case numConfsLeft := <-ntfn2.Event.Updates:
|
||||||
|
const expected = 1
|
||||||
|
if numConfsLeft != expected {
|
||||||
|
t.Fatalf("Received incorrect confirmation update: tx2 "+
|
||||||
|
"expected %d confirmations left, got %d",
|
||||||
|
expected, numConfsLeft)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected confirmation update for tx2")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A confirmation notification for tx2 should not be dispatched yet, as
|
||||||
|
// it requires one more confirmation.
|
||||||
|
select {
|
||||||
|
case txConf := <-ntfn2.Event.Confirmed:
|
||||||
|
t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new block and add it to the TxNotifier at the next height.
|
||||||
|
// This should confirm tx2.
|
||||||
|
block2 := btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{&tx3},
|
||||||
|
})
|
||||||
|
|
||||||
|
err = n.ConnectTip(block2.Hash(), 12, block2.Transactions())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should not receive any event notifications for tx1 since it has
|
||||||
|
// already been confirmed.
|
||||||
|
select {
|
||||||
|
case <-ntfn1.Event.Updates:
|
||||||
|
t.Fatal("Received unexpected confirmation update for tx1")
|
||||||
|
case txConf := <-ntfn1.Event.Confirmed:
|
||||||
|
t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should only receive one update since the last at the new height,
|
||||||
|
// indicating how many confirmations are still left.
|
||||||
|
select {
|
||||||
|
case numConfsLeft := <-ntfn2.Event.Updates:
|
||||||
|
const expected = 0
|
||||||
|
if numConfsLeft != expected {
|
||||||
|
t.Fatalf("Received incorrect confirmation update: tx2 "+
|
||||||
|
"expected %d confirmations left, got %d",
|
||||||
|
expected, numConfsLeft)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected confirmation update for tx2")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A confirmation notification for tx2 should be dispatched, since it
|
||||||
|
// now meets its required number of confirmations.
|
||||||
|
select {
|
||||||
|
case txConf := <-ntfn2.Event.Confirmed:
|
||||||
|
expectedConf := chainntnfs.TxConfirmation{
|
||||||
|
BlockHash: block1.Hash(),
|
||||||
|
BlockHeight: 11,
|
||||||
|
TxIndex: 1,
|
||||||
|
}
|
||||||
|
assertConfDetails(t, txConf, &expectedConf)
|
||||||
|
default:
|
||||||
|
t.Fatalf("Expected confirmation for tx2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTxNotifierHistoricalConfDispatch tests that the TxNotifier dispatches
|
||||||
|
// registered notifications when the transaction is confirmed before
|
||||||
|
// registration.
|
||||||
|
func TestTxNotifierHistoricalConfDispatch(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
tx1NumConfs uint32 = 1
|
||||||
|
tx2NumConfs uint32 = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tx1 = wire.MsgTx{Version: 1}
|
||||||
|
tx2 = wire.MsgTx{Version: 2}
|
||||||
|
tx3 = wire.MsgTx{Version: 3}
|
||||||
|
)
|
||||||
|
|
||||||
|
hintCache := newMockHintCache()
|
||||||
|
n := chainntnfs.NewTxNotifier(10, 100, hintCache, hintCache)
|
||||||
|
|
||||||
|
// Create the test transactions at a height before the TxNotifier's
|
||||||
|
// starting height so that they are confirmed once registering them.
|
||||||
|
tx1Hash := tx1.TxHash()
|
||||||
|
ntfn1 := chainntnfs.ConfNtfn{
|
||||||
|
ConfID: 0,
|
||||||
|
TxID: &tx1Hash,
|
||||||
|
NumConfirmations: tx1NumConfs,
|
||||||
|
Event: chainntnfs.NewConfirmationEvent(tx1NumConfs),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterConf(&ntfn1); err != nil {
|
||||||
|
t.Fatalf("unable to register ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx2Hash := tx2.TxHash()
|
||||||
|
ntfn2 := chainntnfs.ConfNtfn{
|
||||||
|
ConfID: 1,
|
||||||
|
TxID: &tx2Hash,
|
||||||
|
NumConfirmations: tx2NumConfs,
|
||||||
|
Event: chainntnfs.NewConfirmationEvent(tx2NumConfs),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterConf(&ntfn2); err != nil {
|
||||||
|
t.Fatalf("unable to register ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update tx1 with its confirmation details. We should only receive one
|
||||||
|
// update since it only requires one confirmation and it already met it.
|
||||||
|
txConf1 := chainntnfs.TxConfirmation{
|
||||||
|
BlockHash: &zeroHash,
|
||||||
|
BlockHeight: 9,
|
||||||
|
TxIndex: 1,
|
||||||
|
}
|
||||||
|
err := n.UpdateConfDetails(tx1Hash, &txConf1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to update conf details: %v", err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case numConfsLeft := <-ntfn1.Event.Updates:
|
||||||
|
const expected = 0
|
||||||
|
if numConfsLeft != expected {
|
||||||
|
t.Fatalf("Received incorrect confirmation update: tx1 "+
|
||||||
|
"expected %d confirmations left, got %d",
|
||||||
|
expected, numConfsLeft)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected confirmation update for tx1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A confirmation notification for tx1 should also be dispatched.
|
||||||
|
select {
|
||||||
|
case txConf := <-ntfn1.Event.Confirmed:
|
||||||
|
assertConfDetails(t, txConf, &txConf1)
|
||||||
|
default:
|
||||||
|
t.Fatalf("Expected confirmation for tx1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update tx2 with its confirmation details. This should not trigger a
|
||||||
|
// confirmation notification since it hasn't reached its required number
|
||||||
|
// of confirmations, but we should receive a confirmation update
|
||||||
|
// indicating how many confirmation are left.
|
||||||
|
txConf2 := chainntnfs.TxConfirmation{
|
||||||
|
BlockHash: &zeroHash,
|
||||||
|
BlockHeight: 9,
|
||||||
|
TxIndex: 2,
|
||||||
|
}
|
||||||
|
err = n.UpdateConfDetails(tx2Hash, &txConf2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to update conf details: %v", err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case numConfsLeft := <-ntfn2.Event.Updates:
|
||||||
|
const expected = 1
|
||||||
|
if numConfsLeft != expected {
|
||||||
|
t.Fatalf("Received incorrect confirmation update: tx2 "+
|
||||||
|
"expected %d confirmations left, got %d",
|
||||||
|
expected, numConfsLeft)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected confirmation update for tx2")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case txConf := <-ntfn2.Event.Confirmed:
|
||||||
|
t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new block and add it to the TxNotifier at the next height.
|
||||||
|
// This should confirm tx2.
|
||||||
|
block := btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{&tx3},
|
||||||
|
})
|
||||||
|
|
||||||
|
err = n.ConnectTip(block.Hash(), 11, block.Transactions())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should not receive any event notifications for tx1 since it has
|
||||||
|
// already been confirmed.
|
||||||
|
select {
|
||||||
|
case <-ntfn1.Event.Updates:
|
||||||
|
t.Fatal("Received unexpected confirmation update for tx1")
|
||||||
|
case txConf := <-ntfn1.Event.Confirmed:
|
||||||
|
t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should only receive one update for tx2 since the last one,
|
||||||
|
// indicating how many confirmations are still left.
|
||||||
|
select {
|
||||||
|
case numConfsLeft := <-ntfn2.Event.Updates:
|
||||||
|
const expected = 0
|
||||||
|
if numConfsLeft != expected {
|
||||||
|
t.Fatalf("Received incorrect confirmation update: tx2 "+
|
||||||
|
"expected %d confirmations left, got %d",
|
||||||
|
expected, numConfsLeft)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected confirmation update for tx2")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A confirmation notification for tx2 should be dispatched, as it met
|
||||||
|
// its required number of confirmations.
|
||||||
|
select {
|
||||||
|
case txConf := <-ntfn2.Event.Confirmed:
|
||||||
|
assertConfDetails(t, txConf, &txConf2)
|
||||||
|
default:
|
||||||
|
t.Fatalf("Expected confirmation for tx2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTxNotifierFutureSpendDispatch tests that the TxNotifier dispatches
|
||||||
|
// registered notifications when an outpoint is spent after registration.
|
||||||
|
func TestTxNotifierFutureSpendDispatch(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
hintCache := newMockHintCache()
|
||||||
|
n := chainntnfs.NewTxNotifier(10, 100, hintCache, hintCache)
|
||||||
|
|
||||||
|
// We'll start off by registering for a spend notification of an
|
||||||
|
// outpoint.
|
||||||
|
ntfn := &chainntnfs.SpendNtfn{
|
||||||
|
OutPoint: zeroOutPoint,
|
||||||
|
Event: chainntnfs.NewSpendEvent(nil),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterSpend(ntfn); err != nil {
|
||||||
|
t.Fatalf("unable to register spend ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should not receive a notification as the outpoint has not been
|
||||||
|
// spent yet.
|
||||||
|
select {
|
||||||
|
case <-ntfn.Event.Spend:
|
||||||
|
t.Fatal("received unexpected spend notification")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the details of the spending transaction of the outpoint
|
||||||
|
// above. We'll include it in the next block, which should trigger a
|
||||||
|
// spend notification.
|
||||||
|
spendTx := wire.NewMsgTx(2)
|
||||||
|
spendTx.AddTxIn(&wire.TxIn{PreviousOutPoint: zeroOutPoint})
|
||||||
|
spendTxHash := spendTx.TxHash()
|
||||||
|
block := btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{spendTx},
|
||||||
|
})
|
||||||
|
err := n.ConnectTip(block.Hash(), 11, block.Transactions())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedSpendDetails := &chainntnfs.SpendDetail{
|
||||||
|
SpentOutPoint: &ntfn.OutPoint,
|
||||||
|
SpenderTxHash: &spendTxHash,
|
||||||
|
SpendingTx: spendTx,
|
||||||
|
SpenderInputIndex: 0,
|
||||||
|
SpendingHeight: 11,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the details of the notification match as expected.
|
||||||
|
select {
|
||||||
|
case spendDetails := <-ntfn.Event.Spend:
|
||||||
|
assertSpendDetails(t, spendDetails, expectedSpendDetails)
|
||||||
|
default:
|
||||||
|
t.Fatal("expected to receive spend details")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we'll ensure that if the spending transaction has also been
|
||||||
|
// spent, then we don't receive another spend notification.
|
||||||
|
prevOut := wire.OutPoint{Hash: spendTxHash, Index: 0}
|
||||||
|
spendOfSpend := wire.NewMsgTx(2)
|
||||||
|
spendOfSpend.AddTxIn(&wire.TxIn{PreviousOutPoint: prevOut})
|
||||||
|
block = btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{spendOfSpend},
|
||||||
|
})
|
||||||
|
err = n.ConnectTip(block.Hash(), 12, block.Transactions())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ntfn.Event.Spend:
|
||||||
|
t.Fatal("received unexpected spend notification")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTxNotifierHistoricalSpendDispatch tests that the TxNotifier dispatches
|
||||||
|
// registered notifications when an outpoint is spent before registration.
|
||||||
|
func TestTxNotifierHistoricalSpendDispatch(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const startingHeight = 10
|
||||||
|
|
||||||
|
hintCache := newMockHintCache()
|
||||||
|
n := chainntnfs.NewTxNotifier(startingHeight, 100, hintCache, hintCache)
|
||||||
|
|
||||||
|
// We'll start by constructing the spending details of the outpoint
|
||||||
|
// below.
|
||||||
|
spentOutpoint := zeroOutPoint
|
||||||
|
spendTx := wire.NewMsgTx(2)
|
||||||
|
spendTx.AddTxIn(&wire.TxIn{PreviousOutPoint: zeroOutPoint})
|
||||||
|
spendTxHash := spendTx.TxHash()
|
||||||
|
|
||||||
|
expectedSpendDetails := &chainntnfs.SpendDetail{
|
||||||
|
SpentOutPoint: &spentOutpoint,
|
||||||
|
SpenderTxHash: &spendTxHash,
|
||||||
|
SpendingTx: spendTx,
|
||||||
|
SpenderInputIndex: 0,
|
||||||
|
SpendingHeight: startingHeight - 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll register for a spend notification of the outpoint and ensure
|
||||||
|
// that a notification isn't dispatched.
|
||||||
|
ntfn := &chainntnfs.SpendNtfn{
|
||||||
|
OutPoint: spentOutpoint,
|
||||||
|
Event: chainntnfs.NewSpendEvent(nil),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterSpend(ntfn); err != nil {
|
||||||
|
t.Fatalf("unable to register spend ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ntfn.Event.Spend:
|
||||||
|
t.Fatal("received unexpected spend notification")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because we're interested in testing the case of a historical spend,
|
||||||
|
// we'll hand off the spending details of the outpoint to the notifier
|
||||||
|
// as it is not possible for it to view historical events in the chain.
|
||||||
|
// By doing this, we replicate the functionality of the ChainNotifier.
|
||||||
|
err := n.UpdateSpendDetails(ntfn.OutPoint, expectedSpendDetails)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to update spend details: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we have the spending details, we should receive a spend
|
||||||
|
// notification. We'll ensure that the details match as intended.
|
||||||
|
select {
|
||||||
|
case spendDetails := <-ntfn.Event.Spend:
|
||||||
|
assertSpendDetails(t, spendDetails, expectedSpendDetails)
|
||||||
|
default:
|
||||||
|
t.Fatalf("expected to receive spend details")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we'll ensure that if the spending transaction has also been
|
||||||
|
// spent, then we don't receive another spend notification.
|
||||||
|
prevOut := wire.OutPoint{Hash: spendTxHash, Index: 0}
|
||||||
|
spendOfSpend := wire.NewMsgTx(2)
|
||||||
|
spendOfSpend.AddTxIn(&wire.TxIn{PreviousOutPoint: prevOut})
|
||||||
|
block := btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{spendOfSpend},
|
||||||
|
})
|
||||||
|
err = n.ConnectTip(block.Hash(), startingHeight+1, block.Transactions())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ntfn.Event.Spend:
|
||||||
|
t.Fatal("received unexpected spend notification")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTxNotifierMultipleHistoricalRescans ensures that we don't attempt to
|
||||||
|
// request multiple historical confirmation rescans per transactions.
|
||||||
|
func TestTxNotifierMultipleHistoricalConfRescans(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const startingHeight = 10
|
||||||
|
hintCache := newMockHintCache()
|
||||||
|
n := chainntnfs.NewTxNotifier(startingHeight, 100, hintCache, hintCache)
|
||||||
|
|
||||||
|
// The first registration for a transaction in the notifier should
|
||||||
|
// request a historical confirmation rescan as it does not have a
|
||||||
|
// historical view of the chain.
|
||||||
|
confNtfn1 := &chainntnfs.ConfNtfn{
|
||||||
|
ConfID: 0,
|
||||||
|
TxID: &zeroHash,
|
||||||
|
Event: chainntnfs.NewConfirmationEvent(1),
|
||||||
|
}
|
||||||
|
historicalConfDispatch1, err := n.RegisterConf(confNtfn1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to register spend ntfn: %v", err)
|
||||||
|
}
|
||||||
|
if historicalConfDispatch1 == nil {
|
||||||
|
t.Fatal("expected to receive historical dispatch request")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll register another confirmation notification for the same
|
||||||
|
// transaction. This should not request a historical confirmation rescan
|
||||||
|
// since the first one is still pending.
|
||||||
|
confNtfn2 := &chainntnfs.ConfNtfn{
|
||||||
|
ConfID: 1,
|
||||||
|
TxID: &zeroHash,
|
||||||
|
Event: chainntnfs.NewConfirmationEvent(1),
|
||||||
|
}
|
||||||
|
historicalConfDispatch2, err := n.RegisterConf(confNtfn2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to register spend ntfn: %v", err)
|
||||||
|
}
|
||||||
|
if historicalConfDispatch2 != nil {
|
||||||
|
t.Fatal("received unexpected historical rescan request")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we'll mark the ongoing historical rescan as complete and
|
||||||
|
// register another notification. We should also expect not to see a
|
||||||
|
// historical rescan request since the confirmation details should be
|
||||||
|
// cached.
|
||||||
|
confDetails := &chainntnfs.TxConfirmation{
|
||||||
|
BlockHeight: startingHeight - 1,
|
||||||
|
}
|
||||||
|
if err := n.UpdateConfDetails(*confNtfn2.TxID, confDetails); err != nil {
|
||||||
|
t.Fatalf("unable to update conf details: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
confNtfn3 := &chainntnfs.ConfNtfn{
|
||||||
|
ConfID: 2,
|
||||||
|
TxID: &zeroHash,
|
||||||
|
Event: chainntnfs.NewConfirmationEvent(1),
|
||||||
|
}
|
||||||
|
historicalConfDispatch3, err := n.RegisterConf(confNtfn3)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to register spend ntfn: %v", err)
|
||||||
|
}
|
||||||
|
if historicalConfDispatch3 != nil {
|
||||||
|
t.Fatal("received unexpected historical rescan request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTxNotifierMultipleHistoricalRescans ensures that we don't attempt to
|
||||||
|
// request multiple historical spend rescans per outpoints.
|
||||||
|
func TestTxNotifierMultipleHistoricalSpendRescans(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const startingHeight = 10
|
||||||
|
hintCache := newMockHintCache()
|
||||||
|
n := chainntnfs.NewTxNotifier(startingHeight, 100, hintCache, hintCache)
|
||||||
|
|
||||||
|
// The first registration for an outpoint in the notifier should request
|
||||||
|
// a historical spend rescan as it does not have a historical view of
|
||||||
|
// the chain.
|
||||||
|
ntfn1 := &chainntnfs.SpendNtfn{
|
||||||
|
SpendID: 0,
|
||||||
|
OutPoint: zeroOutPoint,
|
||||||
|
Event: chainntnfs.NewSpendEvent(nil),
|
||||||
|
}
|
||||||
|
historicalDispatch1, err := n.RegisterSpend(ntfn1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to register spend ntfn: %v", err)
|
||||||
|
}
|
||||||
|
if historicalDispatch1 == nil {
|
||||||
|
t.Fatal("expected to receive historical dispatch request")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll register another spend notification for the same outpoint. This
|
||||||
|
// should not request a historical spend rescan since the first one is
|
||||||
|
// still pending.
|
||||||
|
ntfn2 := &chainntnfs.SpendNtfn{
|
||||||
|
SpendID: 1,
|
||||||
|
OutPoint: zeroOutPoint,
|
||||||
|
Event: chainntnfs.NewSpendEvent(nil),
|
||||||
|
}
|
||||||
|
historicalDispatch2, err := n.RegisterSpend(ntfn2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to register spend ntfn: %v", err)
|
||||||
|
}
|
||||||
|
if historicalDispatch2 != nil {
|
||||||
|
t.Fatal("received unexpected historical rescan request")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we'll mark the ongoing historical rescan as complete and
|
||||||
|
// register another notification. We should also expect not to see a
|
||||||
|
// historical rescan request since the confirmation details should be
|
||||||
|
// cached.
|
||||||
|
spendDetails := &chainntnfs.SpendDetail{
|
||||||
|
SpentOutPoint: &ntfn2.OutPoint,
|
||||||
|
SpenderTxHash: &zeroHash,
|
||||||
|
SpendingTx: wire.NewMsgTx(2),
|
||||||
|
SpenderInputIndex: 0,
|
||||||
|
SpendingHeight: startingHeight - 1,
|
||||||
|
}
|
||||||
|
err = n.UpdateSpendDetails(ntfn2.OutPoint, spendDetails)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to update spend details: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ntfn3 := &chainntnfs.SpendNtfn{
|
||||||
|
SpendID: 2,
|
||||||
|
OutPoint: zeroOutPoint,
|
||||||
|
Event: chainntnfs.NewSpendEvent(nil),
|
||||||
|
}
|
||||||
|
historicalDispatch3, err := n.RegisterSpend(ntfn3)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to register spend ntfn: %v", err)
|
||||||
|
}
|
||||||
|
if historicalDispatch3 != nil {
|
||||||
|
t.Fatal("received unexpected historical rescan request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTxNotifierMultipleHistoricalNtfns ensures that the TxNotifier will only
|
||||||
|
// request one rescan for a transaction/outpoint when having multiple client
|
||||||
|
// registrations. Once the rescan has completed and retrieved the
|
||||||
|
// confirmation/spend details, a notification should be dispatched to _all_
|
||||||
|
// clients.
|
||||||
|
func TestTxNotifierMultipleHistoricalNtfns(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
numNtfns = 5
|
||||||
|
startingHeight = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
hintCache := newMockHintCache()
|
||||||
|
n := chainntnfs.NewTxNotifier(startingHeight, 100, hintCache, hintCache)
|
||||||
|
|
||||||
|
// We'll start off by registered 5 clients for a confirmation
|
||||||
|
// notification on the same transaction.
|
||||||
|
confNtfns := make([]*chainntnfs.ConfNtfn, numNtfns)
|
||||||
|
for i := uint64(0); i < numNtfns; i++ {
|
||||||
|
confNtfns[i] = &chainntnfs.ConfNtfn{
|
||||||
|
ConfID: i,
|
||||||
|
TxID: &zeroHash,
|
||||||
|
Event: chainntnfs.NewConfirmationEvent(1),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterConf(confNtfns[i]); err != nil {
|
||||||
|
t.Fatalf("unable to register conf ntfn #%d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure none of them have received the confirmation details.
|
||||||
|
for i, ntfn := range confNtfns {
|
||||||
|
select {
|
||||||
|
case <-ntfn.Event.Confirmed:
|
||||||
|
t.Fatalf("request #%d received unexpected confirmation "+
|
||||||
|
"notification", i)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll assume a historical rescan was dispatched and found the
|
||||||
|
// following confirmation details. We'll let the notifier know so that
|
||||||
|
// it can stop watching at tip.
|
||||||
|
expectedConfDetails := &chainntnfs.TxConfirmation{
|
||||||
|
BlockHeight: startingHeight - 1,
|
||||||
|
}
|
||||||
|
err := n.UpdateConfDetails(*confNtfns[0].TxID, expectedConfDetails)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to update conf details: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the confirmation details retrieved, each client should now have
|
||||||
|
// been notified of the confirmation.
|
||||||
|
for i, ntfn := range confNtfns {
|
||||||
|
select {
|
||||||
|
case confDetails := <-ntfn.Event.Confirmed:
|
||||||
|
assertConfDetails(t, confDetails, expectedConfDetails)
|
||||||
|
default:
|
||||||
|
t.Fatalf("request #%d expected to received "+
|
||||||
|
"confirmation notification", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In order to ensure that the confirmation details are properly cached,
|
||||||
|
// we'll register another client for the same transaction. We should not
|
||||||
|
// see a historical rescan request and the confirmation notification
|
||||||
|
// should come through immediately.
|
||||||
|
extraConfNtfn := &chainntnfs.ConfNtfn{
|
||||||
|
ConfID: numNtfns + 1,
|
||||||
|
TxID: &zeroHash,
|
||||||
|
Event: chainntnfs.NewConfirmationEvent(1),
|
||||||
|
}
|
||||||
|
historicalConfRescan, err := n.RegisterConf(extraConfNtfn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to register conf ntfn: %v", err)
|
||||||
|
}
|
||||||
|
if historicalConfRescan != nil {
|
||||||
|
t.Fatal("received unexpected historical rescan request")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case confDetails := <-extraConfNtfn.Event.Confirmed:
|
||||||
|
assertConfDetails(t, confDetails, expectedConfDetails)
|
||||||
|
default:
|
||||||
|
t.Fatal("expected to receive spend notification")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similarly, we'll do the same thing but for spend notifications.
|
||||||
|
spendNtfns := make([]*chainntnfs.SpendNtfn, numNtfns)
|
||||||
|
for i := uint64(0); i < numNtfns; i++ {
|
||||||
|
spendNtfns[i] = &chainntnfs.SpendNtfn{
|
||||||
|
SpendID: i,
|
||||||
|
OutPoint: zeroOutPoint,
|
||||||
|
Event: chainntnfs.NewSpendEvent(nil),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterSpend(spendNtfns[i]); err != nil {
|
||||||
|
t.Fatalf("unable to register spend ntfn #%d: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure none of them have received the spend details.
|
||||||
|
for i, ntfn := range spendNtfns {
|
||||||
|
select {
|
||||||
|
case <-ntfn.Event.Spend:
|
||||||
|
t.Fatalf("request #%d received unexpected spend "+
|
||||||
|
"notification", i)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll assume a historical rescan was dispatched and found the
|
||||||
|
// following spend details. We'll let the notifier know so that it can
|
||||||
|
// stop watching at tip.
|
||||||
|
expectedSpendDetails := &chainntnfs.SpendDetail{
|
||||||
|
SpentOutPoint: &spendNtfns[0].OutPoint,
|
||||||
|
SpenderTxHash: &zeroHash,
|
||||||
|
SpendingTx: wire.NewMsgTx(2),
|
||||||
|
SpenderInputIndex: 0,
|
||||||
|
SpendingHeight: startingHeight - 1,
|
||||||
|
}
|
||||||
|
err = n.UpdateSpendDetails(spendNtfns[0].OutPoint, expectedSpendDetails)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to update spend details: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the spend details retrieved, each client should now have been
|
||||||
|
// notified of the spend.
|
||||||
|
for i, ntfn := range spendNtfns {
|
||||||
|
select {
|
||||||
|
case spendDetails := <-ntfn.Event.Spend:
|
||||||
|
assertSpendDetails(t, spendDetails, expectedSpendDetails)
|
||||||
|
default:
|
||||||
|
t.Fatalf("request #%d expected to received spend "+
|
||||||
|
"notification", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, in order to ensure that the spend details are properly
|
||||||
|
// cached, we'll register another client for the same outpoint. We
|
||||||
|
// should not see a historical rescan request and the spend notification
|
||||||
|
// should come through immediately.
|
||||||
|
extraSpendNtfn := &chainntnfs.SpendNtfn{
|
||||||
|
SpendID: numNtfns + 1,
|
||||||
|
OutPoint: zeroOutPoint,
|
||||||
|
Event: chainntnfs.NewSpendEvent(nil),
|
||||||
|
}
|
||||||
|
historicalSpendRescan, err := n.RegisterSpend(extraSpendNtfn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to register spend ntfn: %v", err)
|
||||||
|
}
|
||||||
|
if historicalSpendRescan != nil {
|
||||||
|
t.Fatal("received unexpected historical rescan request")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case spendDetails := <-extraSpendNtfn.Event.Spend:
|
||||||
|
assertSpendDetails(t, spendDetails, expectedSpendDetails)
|
||||||
|
default:
|
||||||
|
t.Fatal("expected to receive spend notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTxNotifierCancelSpend ensures that a spend notification after a client
|
||||||
|
// has canceled their intent to receive one.
|
||||||
|
func TestTxNotifierCancelSpend(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const startingHeight = 10
|
||||||
|
hintCache := newMockHintCache()
|
||||||
|
n := chainntnfs.NewTxNotifier(startingHeight, 100, hintCache, hintCache)
|
||||||
|
|
||||||
|
// We'll register two notification requests. Only the second one will be
|
||||||
|
// canceled.
|
||||||
|
ntfn1 := &chainntnfs.SpendNtfn{
|
||||||
|
SpendID: 0,
|
||||||
|
OutPoint: zeroOutPoint,
|
||||||
|
Event: chainntnfs.NewSpendEvent(nil),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterSpend(ntfn1); err != nil {
|
||||||
|
t.Fatalf("unable to register spend ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ntfn2 := &chainntnfs.SpendNtfn{
|
||||||
|
SpendID: 1,
|
||||||
|
OutPoint: zeroOutPoint,
|
||||||
|
Event: chainntnfs.NewSpendEvent(nil),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterSpend(ntfn2); err != nil {
|
||||||
|
t.Fatalf("unable to register spend ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the spending details of the outpoint and create a dummy
|
||||||
|
// block containing it.
|
||||||
|
spendTx := wire.NewMsgTx(2)
|
||||||
|
spendTx.AddTxIn(&wire.TxIn{PreviousOutPoint: ntfn1.OutPoint})
|
||||||
|
spendTxHash := spendTx.TxHash()
|
||||||
|
expectedSpendDetails := &chainntnfs.SpendDetail{
|
||||||
|
SpentOutPoint: &ntfn1.OutPoint,
|
||||||
|
SpenderTxHash: &spendTxHash,
|
||||||
|
SpendingTx: spendTx,
|
||||||
|
SpenderInputIndex: 0,
|
||||||
|
SpendingHeight: startingHeight + 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
block := btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{spendTx},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Before extending the notifier's tip with the dummy block above, we'll
|
||||||
|
// cancel the second request.
|
||||||
|
n.CancelSpend(ntfn2.OutPoint, ntfn2.SpendID)
|
||||||
|
|
||||||
|
err := n.ConnectTip(block.Hash(), startingHeight+1, block.Transactions())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first request should still be active, so we should receive a
|
||||||
|
// spend notification with the correct spending details.
|
||||||
|
select {
|
||||||
|
case spendDetails := <-ntfn1.Event.Spend:
|
||||||
|
assertSpendDetails(t, spendDetails, expectedSpendDetails)
|
||||||
|
default:
|
||||||
|
t.Fatalf("expected to receive spend notification")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The second one, however, should not have. The event's Spend channel
|
||||||
|
// must have also been closed to indicate the caller that the TxNotifier
|
||||||
|
// can no longer fulfill their canceled request.
|
||||||
|
select {
|
||||||
|
case _, ok := <-ntfn2.Event.Spend:
|
||||||
|
if ok {
|
||||||
|
t.Fatal("expected Spend channel to be closed")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatal("expected Spend channel to be closed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTxNotifierConfReorg ensures that clients are notified of a reorg when a
|
||||||
|
// transaction for which they registered a confirmation notification has been
|
||||||
|
// reorged out of the chain.
|
||||||
|
func TestTxNotifierConfReorg(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
tx1NumConfs uint32 = 2
|
||||||
|
tx2NumConfs uint32 = 1
|
||||||
|
tx3NumConfs uint32 = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tx1 = wire.MsgTx{Version: 1}
|
||||||
|
tx2 = wire.MsgTx{Version: 2}
|
||||||
|
tx3 = wire.MsgTx{Version: 3}
|
||||||
|
)
|
||||||
|
|
||||||
|
hintCache := newMockHintCache()
|
||||||
|
n := chainntnfs.NewTxNotifier(7, 100, hintCache, hintCache)
|
||||||
|
|
||||||
|
// Tx 1 will be confirmed in block 9 and requires 2 confs.
|
||||||
|
tx1Hash := tx1.TxHash()
|
||||||
|
ntfn1 := chainntnfs.ConfNtfn{
|
||||||
|
TxID: &tx1Hash,
|
||||||
|
NumConfirmations: tx1NumConfs,
|
||||||
|
Event: chainntnfs.NewConfirmationEvent(tx1NumConfs),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterConf(&ntfn1); err != nil {
|
||||||
|
t.Fatalf("unable to register ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := n.UpdateConfDetails(*ntfn1.TxID, nil); err != nil {
|
||||||
|
t.Fatalf("unable to deliver conf details: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tx 2 will be confirmed in block 10 and requires 1 conf.
|
||||||
|
tx2Hash := tx2.TxHash()
|
||||||
|
ntfn2 := chainntnfs.ConfNtfn{
|
||||||
|
TxID: &tx2Hash,
|
||||||
|
NumConfirmations: tx2NumConfs,
|
||||||
|
Event: chainntnfs.NewConfirmationEvent(tx2NumConfs),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterConf(&ntfn2); err != nil {
|
||||||
|
t.Fatalf("unable to register ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := n.UpdateConfDetails(*ntfn2.TxID, nil); err != nil {
|
||||||
|
t.Fatalf("unable to deliver conf details: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tx 3 will be confirmed in block 10 and requires 2 confs.
|
||||||
|
tx3Hash := tx3.TxHash()
|
||||||
|
ntfn3 := chainntnfs.ConfNtfn{
|
||||||
|
TxID: &tx3Hash,
|
||||||
|
NumConfirmations: tx3NumConfs,
|
||||||
|
Event: chainntnfs.NewConfirmationEvent(tx3NumConfs),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterConf(&ntfn3); err != nil {
|
||||||
|
t.Fatalf("unable to register ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := n.UpdateConfDetails(*ntfn3.TxID, nil); err != nil {
|
||||||
|
t.Fatalf("unable to deliver conf details: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync chain to block 10. Txs 1 & 2 should be confirmed.
|
||||||
|
block1 := btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{&tx1},
|
||||||
|
})
|
||||||
|
err := n.ConnectTip(nil, 8, block1.Transactions())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect block: %v", err)
|
||||||
|
}
|
||||||
|
err = n.ConnectTip(nil, 9, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
block2 := btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{&tx2, &tx3},
|
||||||
|
})
|
||||||
|
err = n.ConnectTip(nil, 10, block2.Transactions())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should receive two updates for tx1 since it requires two
|
||||||
|
// confirmations and it has already met them.
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
select {
|
||||||
|
case <-ntfn1.Event.Updates:
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected confirmation update for tx1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A confirmation notification for tx1 should be dispatched, as it met
|
||||||
|
// its required number of confirmations.
|
||||||
|
select {
|
||||||
|
case <-ntfn1.Event.Confirmed:
|
||||||
|
default:
|
||||||
|
t.Fatalf("Expected confirmation for tx1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should only receive one update for tx2 since it only requires
|
||||||
|
// one confirmation and it already met it.
|
||||||
|
select {
|
||||||
|
case <-ntfn2.Event.Updates:
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected confirmation update for tx2")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A confirmation notification for tx2 should be dispatched, as it met
|
||||||
|
// its required number of confirmations.
|
||||||
|
select {
|
||||||
|
case <-ntfn2.Event.Confirmed:
|
||||||
|
default:
|
||||||
|
t.Fatalf("Expected confirmation for tx2")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should only receive one update for tx3 since it only has one
|
||||||
|
// confirmation so far and it requires two.
|
||||||
|
select {
|
||||||
|
case <-ntfn3.Event.Updates:
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected confirmation update for tx3")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A confirmation notification for tx3 should not be dispatched yet, as
|
||||||
|
// it requires one more confirmation.
|
||||||
|
select {
|
||||||
|
case txConf := <-ntfn3.Event.Confirmed:
|
||||||
|
t.Fatalf("Received unexpected confirmation for tx3: %v", txConf)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// The block that included tx2 and tx3 is disconnected and two next
|
||||||
|
// blocks without them are connected.
|
||||||
|
err = n.DisconnectTip(10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.ConnectTip(nil, 10, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.ConnectTip(nil, 11, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case reorgDepth := <-ntfn2.Event.NegativeConf:
|
||||||
|
if reorgDepth != 1 {
|
||||||
|
t.Fatalf("Incorrect value for negative conf notification: "+
|
||||||
|
"expected %d, got %d", 1, reorgDepth)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("Expected negative conf notification for tx1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should not receive any event notifications from all of the
|
||||||
|
// transactions because tx1 has already been confirmed and tx2 and tx3
|
||||||
|
// have not been included in the chain since the reorg.
|
||||||
|
select {
|
||||||
|
case <-ntfn1.Event.Updates:
|
||||||
|
t.Fatal("Received unexpected confirmation update for tx1")
|
||||||
|
case txConf := <-ntfn1.Event.Confirmed:
|
||||||
|
t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ntfn2.Event.Updates:
|
||||||
|
t.Fatal("Received unexpected confirmation update for tx2")
|
||||||
|
case txConf := <-ntfn2.Event.Confirmed:
|
||||||
|
t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ntfn3.Event.Updates:
|
||||||
|
t.Fatal("Received unexpected confirmation update for tx3")
|
||||||
|
case txConf := <-ntfn3.Event.Confirmed:
|
||||||
|
t.Fatalf("Received unexpected confirmation for tx3: %v", txConf)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now transactions 2 & 3 are re-included in a new block.
|
||||||
|
block3 := btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{&tx2, &tx3},
|
||||||
|
})
|
||||||
|
block4 := btcutil.NewBlock(&wire.MsgBlock{})
|
||||||
|
|
||||||
|
err = n.ConnectTip(block3.Hash(), 12, block3.Transactions())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.ConnectTip(block4.Hash(), 13, block4.Transactions())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should only receive one update for tx2 since it only requires
|
||||||
|
// one confirmation and it already met it.
|
||||||
|
select {
|
||||||
|
case numConfsLeft := <-ntfn2.Event.Updates:
|
||||||
|
const expected = 0
|
||||||
|
if numConfsLeft != expected {
|
||||||
|
t.Fatalf("Received incorrect confirmation update: tx2 "+
|
||||||
|
"expected %d confirmations left, got %d",
|
||||||
|
expected, numConfsLeft)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected confirmation update for tx2")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A confirmation notification for tx2 should be dispatched, as it met
|
||||||
|
// its required number of confirmations.
|
||||||
|
select {
|
||||||
|
case txConf := <-ntfn2.Event.Confirmed:
|
||||||
|
expectedConf := chainntnfs.TxConfirmation{
|
||||||
|
BlockHash: block3.Hash(),
|
||||||
|
BlockHeight: 12,
|
||||||
|
TxIndex: 0,
|
||||||
|
}
|
||||||
|
assertConfDetails(t, txConf, &expectedConf)
|
||||||
|
default:
|
||||||
|
t.Fatalf("Expected confirmation for tx2")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should receive two updates for tx3 since it requires two
|
||||||
|
// confirmations and it has already met them.
|
||||||
|
for i := uint32(1); i <= 2; i++ {
|
||||||
|
select {
|
||||||
|
case numConfsLeft := <-ntfn3.Event.Updates:
|
||||||
|
expected := tx3NumConfs - i
|
||||||
|
if numConfsLeft != expected {
|
||||||
|
t.Fatalf("Received incorrect confirmation update: tx3 "+
|
||||||
|
"expected %d confirmations left, got %d",
|
||||||
|
expected, numConfsLeft)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatal("Expected confirmation update for tx2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A confirmation notification for tx3 should be dispatched, as it met
|
||||||
|
// its required number of confirmations.
|
||||||
|
select {
|
||||||
|
case txConf := <-ntfn3.Event.Confirmed:
|
||||||
|
expectedConf := chainntnfs.TxConfirmation{
|
||||||
|
BlockHash: block3.Hash(),
|
||||||
|
BlockHeight: 12,
|
||||||
|
TxIndex: 1,
|
||||||
|
}
|
||||||
|
assertConfDetails(t, txConf, &expectedConf)
|
||||||
|
default:
|
||||||
|
t.Fatalf("Expected confirmation for tx3")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTxNotifierSpendReorg ensures that clients are notified of a reorg when
|
||||||
|
// the spending transaction of an outpoint for which they registered a spend
|
||||||
|
// notification for has been reorged out of the chain.
|
||||||
|
func TestTxNotifierSpendReorg(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const startingHeight = 10
|
||||||
|
hintCache := newMockHintCache()
|
||||||
|
n := chainntnfs.NewTxNotifier(startingHeight, 100, hintCache, hintCache)
|
||||||
|
|
||||||
|
// We'll have two outpoints that will be spent throughout the test. The
|
||||||
|
// first will be spent and will not experience a reorg, while the second
|
||||||
|
// one will.
|
||||||
|
op1 := zeroOutPoint
|
||||||
|
op1.Index = 1
|
||||||
|
spendTx1 := wire.NewMsgTx(2)
|
||||||
|
spendTx1.AddTxIn(&wire.TxIn{PreviousOutPoint: op1})
|
||||||
|
spendTxHash1 := spendTx1.TxHash()
|
||||||
|
expectedSpendDetails1 := &chainntnfs.SpendDetail{
|
||||||
|
SpentOutPoint: &op1,
|
||||||
|
SpenderTxHash: &spendTxHash1,
|
||||||
|
SpendingTx: spendTx1,
|
||||||
|
SpenderInputIndex: 0,
|
||||||
|
SpendingHeight: startingHeight + 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
op2 := zeroOutPoint
|
||||||
|
op2.Index = 2
|
||||||
|
spendTx2 := wire.NewMsgTx(2)
|
||||||
|
spendTx2.AddTxIn(&wire.TxIn{PreviousOutPoint: zeroOutPoint})
|
||||||
|
spendTx2.AddTxIn(&wire.TxIn{PreviousOutPoint: op2})
|
||||||
|
spendTxHash2 := spendTx2.TxHash()
|
||||||
|
|
||||||
|
// The second outpoint will experience a reorg and get re-spent at a
|
||||||
|
// different height, so we'll need to construct the spend details for
|
||||||
|
// before and after the reorg.
|
||||||
|
expectedSpendDetails2BeforeReorg := chainntnfs.SpendDetail{
|
||||||
|
SpentOutPoint: &op2,
|
||||||
|
SpenderTxHash: &spendTxHash2,
|
||||||
|
SpendingTx: spendTx2,
|
||||||
|
SpenderInputIndex: 1,
|
||||||
|
SpendingHeight: startingHeight + 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The spend details after the reorg will be exactly the same, except
|
||||||
|
// for the spend confirming at the next height.
|
||||||
|
expectedSpendDetails2AfterReorg := expectedSpendDetails2BeforeReorg
|
||||||
|
expectedSpendDetails2AfterReorg.SpendingHeight++
|
||||||
|
|
||||||
|
// We'll register for a spend notification for each outpoint above.
|
||||||
|
ntfn1 := &chainntnfs.SpendNtfn{
|
||||||
|
SpendID: 78,
|
||||||
|
OutPoint: op1,
|
||||||
|
Event: chainntnfs.NewSpendEvent(nil),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterSpend(ntfn1); err != nil {
|
||||||
|
t.Fatalf("unable to register spend ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ntfn2 := &chainntnfs.SpendNtfn{
|
||||||
|
SpendID: 21,
|
||||||
|
OutPoint: op2,
|
||||||
|
Event: chainntnfs.NewSpendEvent(nil),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterSpend(ntfn2); err != nil {
|
||||||
|
t.Fatalf("unable to register spend ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll extend the chain by connecting a new block at tip. This block
|
||||||
|
// will only contain the spending transaction of the first outpoint.
|
||||||
|
block1 := btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{spendTx1},
|
||||||
|
})
|
||||||
|
err := n.ConnectTip(
|
||||||
|
block1.Hash(), startingHeight+1, block1.Transactions(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should receive a spend notification for the first outpoint with
|
||||||
|
// its correct spending details.
|
||||||
|
select {
|
||||||
|
case spendDetails := <-ntfn1.Event.Spend:
|
||||||
|
assertSpendDetails(t, spendDetails, expectedSpendDetails1)
|
||||||
|
default:
|
||||||
|
t.Fatal("expected to receive spend details")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should not, however, receive one for the second outpoint as it has
|
||||||
|
// yet to be spent.
|
||||||
|
select {
|
||||||
|
case <-ntfn2.Event.Spend:
|
||||||
|
t.Fatal("received unexpected spend notification")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, we'll extend the chain again, this time with a block containing
|
||||||
|
// the spending transaction of the second outpoint.
|
||||||
|
block2 := btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{spendTx2},
|
||||||
|
})
|
||||||
|
err = n.ConnectTip(
|
||||||
|
block2.Hash(), startingHeight+2, block2.Transactions(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should not receive another spend notification for the first
|
||||||
|
// outpoint.
|
||||||
|
select {
|
||||||
|
case <-ntfn1.Event.Spend:
|
||||||
|
t.Fatal("received unexpected spend notification")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should receive one for the second outpoint.
|
||||||
|
select {
|
||||||
|
case spendDetails := <-ntfn2.Event.Spend:
|
||||||
|
assertSpendDetails(
|
||||||
|
t, spendDetails, &expectedSpendDetails2BeforeReorg,
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
t.Fatal("expected to receive spend details")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, to replicate a chain reorg, we'll disconnect the block that
|
||||||
|
// contained the spending transaction of the second outpoint.
|
||||||
|
if err := n.DisconnectTip(startingHeight + 2); err != nil {
|
||||||
|
t.Fatalf("unable to disconnect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No notifications should be dispatched for the first outpoint as it
|
||||||
|
// was spent at a previous height.
|
||||||
|
select {
|
||||||
|
case <-ntfn1.Event.Spend:
|
||||||
|
t.Fatal("received unexpected spend notification")
|
||||||
|
case <-ntfn1.Event.Reorg:
|
||||||
|
t.Fatal("received unexpected spend reorg notification")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should receive a reorg notification for the second outpoint.
|
||||||
|
select {
|
||||||
|
case <-ntfn2.Event.Spend:
|
||||||
|
t.Fatal("received unexpected spend notification")
|
||||||
|
case <-ntfn2.Event.Reorg:
|
||||||
|
default:
|
||||||
|
t.Fatal("expected spend reorg notification")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll now extend the chain with an empty block, to ensure that we can
|
||||||
|
// properly detect when an outpoint has been re-spent at a later height.
|
||||||
|
emptyBlock := btcutil.NewBlock(&wire.MsgBlock{})
|
||||||
|
err = n.ConnectTip(
|
||||||
|
emptyBlock.Hash(), startingHeight+2, emptyBlock.Transactions(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to disconnect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We shouldn't receive notifications for either of the outpoints.
|
||||||
|
select {
|
||||||
|
case <-ntfn1.Event.Spend:
|
||||||
|
t.Fatal("received unexpected spend notification")
|
||||||
|
case <-ntfn1.Event.Reorg:
|
||||||
|
t.Fatal("received unexpected spend reorg notification")
|
||||||
|
case <-ntfn2.Event.Spend:
|
||||||
|
t.Fatal("received unexpected spend notification")
|
||||||
|
case <-ntfn2.Event.Reorg:
|
||||||
|
t.Fatal("received unexpected spend reorg notification")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, extend the chain with another block containing the same
|
||||||
|
// spending transaction of the second outpoint.
|
||||||
|
err = n.ConnectTip(
|
||||||
|
block2.Hash(), startingHeight+3, block2.Transactions(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should now receive a spend notification once again for the second
|
||||||
|
// outpoint containing the new spend details.
|
||||||
|
select {
|
||||||
|
case spendDetails := <-ntfn2.Event.Spend:
|
||||||
|
assertSpendDetails(
|
||||||
|
t, spendDetails, &expectedSpendDetails2AfterReorg,
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
t.Fatalf("expected to receive spend notification")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once again, we should not receive one for the first outpoint.
|
||||||
|
select {
|
||||||
|
case <-ntfn1.Event.Spend:
|
||||||
|
t.Fatal("received unexpected spend notification")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTxNotifierConfirmHintCache ensures that the height hints for transactions
|
||||||
|
// are kept track of correctly with each new block connected/disconnected. This
|
||||||
|
// test also asserts that the height hints are not updated until the simulated
|
||||||
|
// historical dispatches have returned, and we know the transactions aren't
|
||||||
|
// already in the chain.
|
||||||
|
func TestTxNotifierConfirmHintCache(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
startingHeight = 200
|
||||||
|
txDummyHeight = 201
|
||||||
|
tx1Height = 202
|
||||||
|
tx2Height = 203
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize our TxNotifier instance backed by a height hint cache.
|
||||||
|
hintCache := newMockHintCache()
|
||||||
|
n := chainntnfs.NewTxNotifier(startingHeight, 100, hintCache, hintCache)
|
||||||
|
|
||||||
|
// Create two test transactions and register them for notifications.
|
||||||
|
tx1 := wire.MsgTx{Version: 1}
|
||||||
|
tx1Hash := tx1.TxHash()
|
||||||
|
ntfn1 := &chainntnfs.ConfNtfn{
|
||||||
|
TxID: &tx1Hash,
|
||||||
|
NumConfirmations: 1,
|
||||||
|
Event: chainntnfs.NewConfirmationEvent(1),
|
||||||
|
}
|
||||||
|
|
||||||
|
tx2 := wire.MsgTx{Version: 2}
|
||||||
|
tx2Hash := tx2.TxHash()
|
||||||
|
ntfn2 := &chainntnfs.ConfNtfn{
|
||||||
|
TxID: &tx2Hash,
|
||||||
|
NumConfirmations: 2,
|
||||||
|
Event: chainntnfs.NewConfirmationEvent(2),
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := n.RegisterConf(ntfn1); err != nil {
|
||||||
|
t.Fatalf("unable to register tx1: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterConf(ntfn2); err != nil {
|
||||||
|
t.Fatalf("unable to register tx2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both transactions should not have a height hint set, as RegisterConf
|
||||||
|
// should not alter the cache state.
|
||||||
|
_, err := hintCache.QueryConfirmHint(tx1Hash)
|
||||||
|
if err != chainntnfs.ErrConfirmHintNotFound {
|
||||||
|
t.Fatalf("unexpected error when querying for height hint "+
|
||||||
|
"want: %v, got %v",
|
||||||
|
chainntnfs.ErrConfirmHintNotFound, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = hintCache.QueryConfirmHint(tx2Hash)
|
||||||
|
if err != chainntnfs.ErrConfirmHintNotFound {
|
||||||
|
t.Fatalf("unexpected error when querying for height hint "+
|
||||||
|
"want: %v, got %v",
|
||||||
|
chainntnfs.ErrConfirmHintNotFound, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new block that will include the dummy transaction and extend
|
||||||
|
// the chain.
|
||||||
|
txDummy := wire.MsgTx{Version: 3}
|
||||||
|
block1 := btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{&txDummy},
|
||||||
|
})
|
||||||
|
|
||||||
|
err = n.ConnectTip(
|
||||||
|
block1.Hash(), txDummyHeight, block1.Transactions(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since UpdateConfDetails has not been called for either transaction,
|
||||||
|
// the height hints should remain unchanged. This simulates blocks
|
||||||
|
// confirming while the historical dispatch is processing the
|
||||||
|
// registration.
|
||||||
|
hint, err := hintCache.QueryConfirmHint(tx1Hash)
|
||||||
|
if err != chainntnfs.ErrConfirmHintNotFound {
|
||||||
|
t.Fatalf("unexpected error when querying for height hint "+
|
||||||
|
"want: %v, got %v",
|
||||||
|
chainntnfs.ErrConfirmHintNotFound, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hint, err = hintCache.QueryConfirmHint(tx2Hash)
|
||||||
|
if err != chainntnfs.ErrConfirmHintNotFound {
|
||||||
|
t.Fatalf("unexpected error when querying for height hint "+
|
||||||
|
"want: %v, got %v",
|
||||||
|
chainntnfs.ErrConfirmHintNotFound, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, update the conf details reporting that the neither txn was found
|
||||||
|
// in the historical dispatch.
|
||||||
|
if err := n.UpdateConfDetails(tx1Hash, nil); err != nil {
|
||||||
|
t.Fatalf("unable to update conf details: %v", err)
|
||||||
|
}
|
||||||
|
if err := n.UpdateConfDetails(tx2Hash, nil); err != nil {
|
||||||
|
t.Fatalf("unable to update conf details: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll create another block that will include the first transaction
|
||||||
|
// and extend the chain.
|
||||||
|
block2 := btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{&tx1},
|
||||||
|
})
|
||||||
|
|
||||||
|
err = n.ConnectTip(
|
||||||
|
block2.Hash(), tx1Height, block2.Transactions(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that both notifications are waiting at tip for confirmations,
|
||||||
|
// they should have their height hints updated to the latest block
|
||||||
|
// height.
|
||||||
|
hint, err = hintCache.QueryConfirmHint(tx1Hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for hint: %v", err)
|
||||||
|
}
|
||||||
|
if hint != tx1Height {
|
||||||
|
t.Fatalf("expected hint %d, got %d",
|
||||||
|
tx1Height, hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
hint, err = hintCache.QueryConfirmHint(tx2Hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for hint: %v", err)
|
||||||
|
}
|
||||||
|
if hint != tx1Height {
|
||||||
|
t.Fatalf("expected hint %d, got %d",
|
||||||
|
tx2Height, hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we'll create another block that will include the second
|
||||||
|
// transaction and extend the chain.
|
||||||
|
block3 := btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{&tx2},
|
||||||
|
})
|
||||||
|
|
||||||
|
err = n.ConnectTip(
|
||||||
|
block3.Hash(), tx2Height, block3.Transactions(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The height hint for the first transaction should remain the same.
|
||||||
|
hint, err = hintCache.QueryConfirmHint(tx1Hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for hint: %v", err)
|
||||||
|
}
|
||||||
|
if hint != tx1Height {
|
||||||
|
t.Fatalf("expected hint %d, got %d",
|
||||||
|
tx1Height, hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The height hint for the second transaction should now be updated to
|
||||||
|
// reflect its confirmation.
|
||||||
|
hint, err = hintCache.QueryConfirmHint(tx2Hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for hint: %v", err)
|
||||||
|
}
|
||||||
|
if hint != tx2Height {
|
||||||
|
t.Fatalf("expected hint %d, got %d",
|
||||||
|
tx2Height, hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we'll attempt do disconnect the last block in order to
|
||||||
|
// simulate a chain reorg.
|
||||||
|
if err := n.DisconnectTip(tx2Height); err != nil {
|
||||||
|
t.Fatalf("Failed to disconnect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should update the second transaction's height hint within the
|
||||||
|
// cache to the previous height.
|
||||||
|
hint, err = hintCache.QueryConfirmHint(tx2Hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for hint: %v", err)
|
||||||
|
}
|
||||||
|
if hint != tx1Height {
|
||||||
|
t.Fatalf("expected hint %d, got %d",
|
||||||
|
tx1Height, hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first transaction's height hint should remain at the original
|
||||||
|
// confirmation height.
|
||||||
|
hint, err = hintCache.QueryConfirmHint(tx2Hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for hint: %v", err)
|
||||||
|
}
|
||||||
|
if hint != tx1Height {
|
||||||
|
t.Fatalf("expected hint %d, got %d",
|
||||||
|
tx1Height, hint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTxNotifierSpendHintCache ensures that the height hints for outpoints are
|
||||||
|
// kept track of correctly with each new block connected/disconnected. This test
|
||||||
|
// also asserts that the height hints are not updated until the simulated
|
||||||
|
// historical dispatches have returned, and we know the outpoints haven't
|
||||||
|
// already been spent in the chain.
|
||||||
|
func TestTxNotifierSpendHintCache(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
startingHeight = 200
|
||||||
|
dummyHeight = 201
|
||||||
|
op1Height = 202
|
||||||
|
op2Height = 203
|
||||||
|
)
|
||||||
|
|
||||||
|
// Intiialize our TxNotifier instance backed by a height hint cache.
|
||||||
|
hintCache := newMockHintCache()
|
||||||
|
n := chainntnfs.NewTxNotifier(startingHeight, 100, hintCache, hintCache)
|
||||||
|
|
||||||
|
// Create two test outpoints and register them for spend notifications.
|
||||||
|
op1 := wire.OutPoint{Hash: zeroHash, Index: 1}
|
||||||
|
ntfn1 := &chainntnfs.SpendNtfn{
|
||||||
|
OutPoint: op1,
|
||||||
|
Event: chainntnfs.NewSpendEvent(nil),
|
||||||
|
}
|
||||||
|
op2 := wire.OutPoint{Hash: zeroHash, Index: 2}
|
||||||
|
ntfn2 := &chainntnfs.SpendNtfn{
|
||||||
|
OutPoint: op2,
|
||||||
|
Event: chainntnfs.NewSpendEvent(nil),
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := n.RegisterSpend(ntfn1); err != nil {
|
||||||
|
t.Fatalf("unable to register spend for op1: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterSpend(ntfn2); err != nil {
|
||||||
|
t.Fatalf("unable to register spend for op2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both outpoints should not have a spend hint set upon registration, as
|
||||||
|
// we must first determine whether they have already been spent in the
|
||||||
|
// chain.
|
||||||
|
_, err := hintCache.QuerySpendHint(op1)
|
||||||
|
if err != chainntnfs.ErrSpendHintNotFound {
|
||||||
|
t.Fatalf("unexpected error when querying for height hint "+
|
||||||
|
"expected: %v, got %v", chainntnfs.ErrSpendHintNotFound,
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
_, err = hintCache.QuerySpendHint(op2)
|
||||||
|
if err != chainntnfs.ErrSpendHintNotFound {
|
||||||
|
t.Fatalf("unexpected error when querying for height hint "+
|
||||||
|
"expected: %v, got %v", chainntnfs.ErrSpendHintNotFound,
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new empty block and extend the chain.
|
||||||
|
emptyBlock := btcutil.NewBlock(&wire.MsgBlock{})
|
||||||
|
err = n.ConnectTip(
|
||||||
|
emptyBlock.Hash(), dummyHeight, emptyBlock.Transactions(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we haven't called UpdateSpendDetails on any of the test
|
||||||
|
// outpoints, this implies that there is a still a pending historical
|
||||||
|
// rescan for them, so their spend hints should not be created/updated.
|
||||||
|
_, err = hintCache.QuerySpendHint(op1)
|
||||||
|
if err != chainntnfs.ErrSpendHintNotFound {
|
||||||
|
t.Fatalf("unexpected error when querying for height hint "+
|
||||||
|
"expected: %v, got %v", chainntnfs.ErrSpendHintNotFound,
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
_, err = hintCache.QuerySpendHint(op2)
|
||||||
|
if err != chainntnfs.ErrSpendHintNotFound {
|
||||||
|
t.Fatalf("unexpected error when querying for height hint "+
|
||||||
|
"expected: %v, got %v", chainntnfs.ErrSpendHintNotFound,
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, we'll simulate that their historical rescans have finished by
|
||||||
|
// calling UpdateSpendDetails. This should allow their spend hints to be
|
||||||
|
// updated upon every block connected/disconnected.
|
||||||
|
if err := n.UpdateSpendDetails(ntfn1.OutPoint, nil); err != nil {
|
||||||
|
t.Fatalf("unable to update spend details: %v", err)
|
||||||
|
}
|
||||||
|
if err := n.UpdateSpendDetails(ntfn2.OutPoint, nil); err != nil {
|
||||||
|
t.Fatalf("unable to update spend details: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll create a new block that only contains the spending transaction
|
||||||
|
// of the first outpoint.
|
||||||
|
spendTx1 := wire.NewMsgTx(2)
|
||||||
|
spendTx1.AddTxIn(&wire.TxIn{PreviousOutPoint: ntfn1.OutPoint})
|
||||||
|
block1 := btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{spendTx1},
|
||||||
|
})
|
||||||
|
err = n.ConnectTip(block1.Hash(), op1Height, block1.Transactions())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both outpoints should have their spend hints reflect the height of
|
||||||
|
// the new block being connected due to the first outpoint being spent
|
||||||
|
// at this height, and the second outpoint still being unspent.
|
||||||
|
op1Hint, err := hintCache.QuerySpendHint(ntfn1.OutPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for spend hint of op1: %v", err)
|
||||||
|
}
|
||||||
|
if op1Hint != op1Height {
|
||||||
|
t.Fatalf("expected hint %d, got %d", op1Height, op1Hint)
|
||||||
|
}
|
||||||
|
op2Hint, err := hintCache.QuerySpendHint(ntfn2.OutPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for spend hint of op2: %v", err)
|
||||||
|
}
|
||||||
|
if op2Hint != op1Height {
|
||||||
|
t.Fatalf("expected hint %d, got %d", op1Height, op2Hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, we'll create another block that spends the second outpoint.
|
||||||
|
spendTx2 := wire.NewMsgTx(2)
|
||||||
|
spendTx2.AddTxIn(&wire.TxIn{PreviousOutPoint: ntfn2.OutPoint})
|
||||||
|
block2 := btcutil.NewBlock(&wire.MsgBlock{
|
||||||
|
Transactions: []*wire.MsgTx{spendTx2},
|
||||||
|
})
|
||||||
|
err = n.ConnectTip(block2.Hash(), op2Height, block2.Transactions())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to connect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the second outpoint should have its spend hint updated due to
|
||||||
|
// being spent within the new block. The first outpoint's spend hint
|
||||||
|
// should remain the same as it's already been spent before.
|
||||||
|
op1Hint, err = hintCache.QuerySpendHint(ntfn1.OutPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for spend hint of op1: %v", err)
|
||||||
|
}
|
||||||
|
if op1Hint != op1Height {
|
||||||
|
t.Fatalf("expected hint %d, got %d", op1Height, op1Hint)
|
||||||
|
}
|
||||||
|
op2Hint, err = hintCache.QuerySpendHint(ntfn2.OutPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for spend hint of op2: %v", err)
|
||||||
|
}
|
||||||
|
if op2Hint != op2Height {
|
||||||
|
t.Fatalf("expected hint %d, got %d", op2Height, op2Hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we'll attempt do disconnect the last block in order to
|
||||||
|
// simulate a chain reorg.
|
||||||
|
if err := n.DisconnectTip(op2Height); err != nil {
|
||||||
|
t.Fatalf("unable to disconnect block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should update the second outpoint's spend hint within the cache
|
||||||
|
// to the previous height, as that's where its spending transaction was
|
||||||
|
// included in within the chain. The first outpoint's spend hint should
|
||||||
|
// remain the same.
|
||||||
|
op1Hint, err = hintCache.QuerySpendHint(ntfn1.OutPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for spend hint of op1: %v", err)
|
||||||
|
}
|
||||||
|
if op1Hint != op1Height {
|
||||||
|
t.Fatalf("expected hint %d, got %d", op1Height, op1Hint)
|
||||||
|
}
|
||||||
|
op2Hint, err = hintCache.QuerySpendHint(ntfn2.OutPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for spend hint of op2: %v", err)
|
||||||
|
}
|
||||||
|
if op2Hint != op1Height {
|
||||||
|
t.Fatalf("expected hint %d, got %d", op1Height, op2Hint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTxNotifierTearDown ensures that the TxNotifier properly alerts clients
|
||||||
|
// that it is shutting down and will be unable to deliver notifications.
|
||||||
|
func TestTxNotifierTearDown(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
hintCache := newMockHintCache()
|
||||||
|
n := chainntnfs.NewTxNotifier(10, 100, hintCache, hintCache)
|
||||||
|
|
||||||
|
// To begin the test, we'll register for a confirmation and spend
|
||||||
|
// notification.
|
||||||
|
confNtfn := &chainntnfs.ConfNtfn{
|
||||||
|
TxID: &zeroHash,
|
||||||
|
NumConfirmations: 1,
|
||||||
|
Event: chainntnfs.NewConfirmationEvent(1),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterConf(confNtfn); err != nil {
|
||||||
|
t.Fatalf("unable to register conf ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spendNtfn := &chainntnfs.SpendNtfn{
|
||||||
|
OutPoint: zeroOutPoint,
|
||||||
|
Event: chainntnfs.NewSpendEvent(nil),
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterSpend(spendNtfn); err != nil {
|
||||||
|
t.Fatalf("unable to register spend ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the notifications registered, we'll now tear down the notifier.
|
||||||
|
// The notification channels should be closed for notifications, whether
|
||||||
|
// they have been dispatched or not, so we should not expect to receive
|
||||||
|
// any more updates.
|
||||||
|
n.TearDown()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case _, ok := <-confNtfn.Event.Confirmed:
|
||||||
|
if ok {
|
||||||
|
t.Fatal("expected closed Confirmed channel for conf ntfn")
|
||||||
|
}
|
||||||
|
case _, ok := <-confNtfn.Event.Updates:
|
||||||
|
if ok {
|
||||||
|
t.Fatal("expected closed Updates channel for conf ntfn")
|
||||||
|
}
|
||||||
|
case _, ok := <-confNtfn.Event.NegativeConf:
|
||||||
|
if ok {
|
||||||
|
t.Fatal("expected closed NegativeConf channel for conf ntfn")
|
||||||
|
}
|
||||||
|
case _, ok := <-spendNtfn.Event.Spend:
|
||||||
|
if ok {
|
||||||
|
t.Fatal("expected closed Spend channel for spend ntfn")
|
||||||
|
}
|
||||||
|
case _, ok := <-spendNtfn.Event.Reorg:
|
||||||
|
if ok {
|
||||||
|
t.Fatalf("expected closed Reorg channel for spend ntfn")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("expected closed notification channels for all ntfns")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the notifier is torn down, we should no longer be able to
|
||||||
|
// register notification requests.
|
||||||
|
if _, err := n.RegisterConf(confNtfn); err == nil {
|
||||||
|
t.Fatal("expected confirmation registration to fail")
|
||||||
|
}
|
||||||
|
if _, err := n.RegisterSpend(spendNtfn); err == nil {
|
||||||
|
t.Fatal("expected spend registration to fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertConfDetails(t *testing.T, result, expected *chainntnfs.TxConfirmation) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if result.BlockHeight != expected.BlockHeight {
|
||||||
|
t.Fatalf("Incorrect block height in confirmation details: "+
|
||||||
|
"expected %d, got %d", expected.BlockHeight,
|
||||||
|
result.BlockHeight)
|
||||||
|
}
|
||||||
|
if !result.BlockHash.IsEqual(expected.BlockHash) {
|
||||||
|
t.Fatalf("Incorrect block hash in confirmation details: "+
|
||||||
|
"expected %d, got %d", expected.BlockHash,
|
||||||
|
result.BlockHash)
|
||||||
|
}
|
||||||
|
if result.TxIndex != expected.TxIndex {
|
||||||
|
t.Fatalf("Incorrect tx index in confirmation details: "+
|
||||||
|
"expected %d, got %d", expected.TxIndex, result.TxIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertSpendDetails(t *testing.T, result, expected *chainntnfs.SpendDetail) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if *result.SpentOutPoint != *expected.SpentOutPoint {
|
||||||
|
t.Fatalf("expected spent outpoint %v, got %v",
|
||||||
|
expected.SpentOutPoint, result.SpentOutPoint)
|
||||||
|
}
|
||||||
|
if !result.SpenderTxHash.IsEqual(expected.SpenderTxHash) {
|
||||||
|
t.Fatalf("expected spender tx hash %v, got %v",
|
||||||
|
expected.SpenderTxHash, result.SpenderTxHash)
|
||||||
|
}
|
||||||
|
if result.SpenderInputIndex != expected.SpenderInputIndex {
|
||||||
|
t.Fatalf("expected spender input index %d, got %d",
|
||||||
|
expected.SpenderInputIndex, result.SpenderInputIndex)
|
||||||
|
}
|
||||||
|
if result.SpendingHeight != expected.SpendingHeight {
|
||||||
|
t.Fatalf("expected spending height %d, got %d",
|
||||||
|
expected.SpendingHeight, result.SpendingHeight)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user