diff --git a/chainntnfs/bitcoindnotify/bitcoind.go b/chainntnfs/bitcoindnotify/bitcoind.go index 32756903..f4f51821 100644 --- a/chainntnfs/bitcoindnotify/bitcoind.go +++ b/chainntnfs/bitcoindnotify/bitcoind.go @@ -12,7 +12,6 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/chain" - "github.com/btcsuite/btcwallet/wtxmgr" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/queue" ) @@ -69,9 +68,7 @@ type BitcoindNotifier struct { notificationCancels chan interface{} notificationRegistry chan interface{} - spendNotifications map[wire.OutPoint]map[uint64]*spendNotification - - txConfNotifier *chainntnfs.TxConfNotifier + txNotifier *chainntnfs.TxNotifier blockEpochClients map[uint64]*blockEpochRegistration @@ -107,8 +104,6 @@ func New(chainConn *chain.BitcoindConn, spendHintCache chainntnfs.SpendHintCache blockEpochClients: make(map[uint64]*blockEpochRegistration), - spendNotifications: make(map[wire.OutPoint]map[uint64]*spendNotification), - spendHintCache: spendHintCache, confirmHintCache: confirmHintCache, @@ -142,8 +137,9 @@ func (b *BitcoindNotifier) Start() error { return err } - b.txConfNotifier = chainntnfs.NewTxConfNotifier( + b.txNotifier = chainntnfs.NewTxNotifier( uint32(currentHeight), reorgSafetyLimit, b.confirmHintCache, + b.spendHintCache, ) b.bestBlock = chainntnfs.BlockEpoch{ @@ -173,18 +169,13 @@ func (b *BitcoindNotifier) Stop() error { // Notify all pending clients of our shutdown by closing the related // notification channels. - for _, spendClients := range b.spendNotifications { - for _, spendClient := range spendClients { - close(spendClient.spendChan) - } - } for _, epochClient := range b.blockEpochClients { close(epochClient.cancelChan) epochClient.wg.Wait() close(epochClient.epochChan) } - b.txConfNotifier.TearDown() + b.txNotifier.TearDown() return nil } @@ -204,19 +195,6 @@ out: select { case cancelMsg := <-b.notificationCancels: switch msg := cancelMsg.(type) { - case *spendCancel: - chainntnfs.Log.Infof("Cancelling spend "+ - "notification for out_point=%v, "+ - "spend_id=%v", msg.op, msg.spendID) - - // Before we attempt to close the spendChan, - // ensure that the notification hasn't already - // yet been dispatched. - if outPointClients, ok := b.spendNotifications[msg.op]; ok { - close(outPointClients[msg.spendID].spendChan) - delete(b.spendNotifications[msg.op], msg.spendID) - } - case *epochCancel: chainntnfs.Log.Infof("Cancelling epoch "+ "notification, epoch_id=%v", msg.epochID) @@ -244,21 +222,13 @@ out: } case registerMsg := <-b.notificationRegistry: switch msg := registerMsg.(type) { - case *spendNotification: - chainntnfs.Log.Infof("New spend subscription: "+ - "utxo=%v", msg.targetOutpoint) - op := *msg.targetOutpoint - - if _, ok := b.spendNotifications[op]; !ok { - b.spendNotifications[op] = make(map[uint64]*spendNotification) - } - b.spendNotifications[op][msg.spendID] = msg - case *chainntnfs.HistoricalConfDispatch: // Look up whether the transaction is already // included in the active chain. We'll do this // in a goroutine to prevent blocking // potentially long rescans. + // + // TODO(wilmer): add retry logic if rescan fails? b.wg.Add(1) go func() { defer b.wg.Done() @@ -278,7 +248,7 @@ out: // begin safely updating the height hint // cache at tip, since any pending // rescans have now completed. - err = b.txConfNotifier.UpdateConfDetails( + err = b.txNotifier.UpdateConfDetails( *msg.TxID, confDetails, ) 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: chainntnfs.Log.Infof("New block epoch subscription") b.blockEpochClients[msg.epochID] = msg @@ -305,9 +294,6 @@ out: } } msg.errorChan <- nil - - case chain.RelevantTx: - b.handleRelevantTx(msg, b.bestBlock.Height) } case ntfn := <-b.chainConn.Notifications(): @@ -330,7 +316,7 @@ out: newBestBlock, missedBlocks, err := chainntnfs.HandleMissedBlocks( b.chainConn, - b.txConfNotifier, + b.txNotifier, b.bestBlock, item.Height, true, ) @@ -369,7 +355,7 @@ out: } newBestBlock, err := chainntnfs.RewindChain( - b.chainConn, b.txConfNotifier, + b.chainConn, b.txNotifier, b.bestBlock, item.Height-1, ) if err != nil { @@ -383,7 +369,23 @@ out: b.bestBlock = newBestBlock 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: @@ -393,55 +395,6 @@ out: 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 // block in the active chain and, if so, returns details about the confirmation. func (b *BitcoindNotifier) historicalConfDetails(txid *chainhash.Hash, @@ -621,7 +574,7 @@ func (b *BitcoindNotifier) handleBlockConnected(block chainntnfs.BlockEpoch) err } txns := btcutil.NewBlock(rawBlock).Transactions() - err = b.txConfNotifier.ConnectTip( + err = b.txNotifier.ConnectTip( block.Hash, uint32(block.Height), txns) if err != nil { 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, 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 // if any subscribers make queries based on their received block epoch, // 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 // 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 @@ -717,167 +627,120 @@ type spendCancel struct { func (b *BitcoindNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) { - // Before proceeding to register the notification, we'll query our - // height hint cache to determine whether a better one exists. - if hint, err := b.spendHintCache.QuerySpendHint(*outpoint); err == nil { - if hint > heightHint { - chainntnfs.Log.Debugf("Using height hint %d retrieved "+ - "from cache for %v", hint, outpoint) - heightHint = hint - } + // First, we'll construct a spend notification request and hand it off + // to the txNotifier. + spendID := atomic.AddUint64(&b.spendClientCounter, 1) + cancel := func() { + b.txNotifier.CancelSpend(*outpoint, spendID) } - // Construct a notification request for the outpoint and send it to the - // main event loop. - ntfn := &spendNotification{ - targetOutpoint: outpoint, - spendChan: make(chan *chainntnfs.SpendDetail, 1), - spendID: atomic.AddUint64(&b.spendClientCounter, 1), + ntfn := &chainntnfs.SpendNtfn{ + SpendID: spendID, + OutPoint: *outpoint, + PkScript: pkScript, + Event: chainntnfs.NewSpendEvent(cancel), + HeightHint: heightHint, } - select { - 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) + historicalDispatch, err := b.txNotifier.RegisterSpend(ntfn) if err != nil { return nil, err } - // If the output is unspent, then we'll write it to the cache with the - // given height hint. This allows us to increase the height hint as the - // chain extends and the output remains unspent. + // If the txNotifier didn't return any details to perform a historical + // scan of the chain, then we can return early as there's nothing left + // 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 { - 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 { - // The error is not fatal, so we should not return an - // 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) - } + return nil, err } - var blockHash *chainhash.Hash - 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) - } - } + return ntfn.Event, nil + } - // We'll only scan old blocks if the transaction has actually - // been included within a block. Otherwise, we'll encounter an - // error when scanning for blocks. This can happens in the case - // of a race condition, wherein the output itself is unspent, - // and only arrives in the mempool after the getxout call. - if blockHash != nil { - // Rescan all the blocks until the current one. - startHeight, err := b.chainConn.GetBlockHeight( - blockHash, - ) - if err != nil { - return nil, err - } - - _, endHeight, err := b.chainConn.GetBestBlock() - if err != nil { - 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) - } - }() + // Otherwise, we'll determine when the output was spent by scanning the + // chain. We'll begin by determining where to start our historical + // rescan. + // + // As a minimal optimization, we'll query the backend's transaction + // index (if enabled) to determine if we have a better rescan starting + // height. We can do this as the GetRawTransaction call will return the + // hash of the block it was included in within the chain. + 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) } } - return &chainntnfs.SpendEvent{ - Spend: ntfn.spendChan, - Cancel: func() { - cancel := &spendCancel{ - op: *outpoint, - spendID: ntfn.spendID, - } + // If the transaction index was enabled, we'll use the block's hash to + // retrieve its height and check whether it provides a better starting + // point for our rescan. + if tx != nil { + // If the transaction containing the outpoint hasn't confirmed + // on-chain, then there's no need to perform a rescan. + if tx.BlockHash == "" { + return ntfn.Event, nil + } - // Submit spend cancellation to notification dispatcher. - select { - case b.notificationCancels <- cancel: - // Cancellation is being handled, drain the - // spend chan until it is closed before yielding - // to the caller. - for { - select { - case _, ok := <-ntfn.spendChan: - if !ok { - return - } - case <-b.quit: - return - } - } - case <-b.quit: - } - }, - }, nil + blockHash, err := chainhash.NewHashFromStr(tx.BlockHash) + if err != nil { + return nil, err + } + blockHeight, err := b.chainConn.GetBlockHeight(blockHash) + if err != nil { + return nil, err + } + + if uint32(blockHeight) > historicalDispatch.StartHeight { + historicalDispatch.StartHeight = uint32(blockHeight) + } + } + + // Now that we've determined the starting point of our rescan, we can + // dispatch it. + select { + case b.notificationRegistry <- historicalDispatch: + return ntfn.Event, nil + case <-b.quit: + return nil, ErrChainNotifierShuttingDown + } } // disaptchSpendDetailsManually attempts to manually scan the chain within the // 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 // will then dispatch the notification to all of its clients. -func (b *BitcoindNotifier) dispatchSpendDetailsManually(op wire.OutPoint, - startHeight, endHeight int32) error { +func (b *BitcoindNotifier) dispatchSpendDetailsManually( + historicalDispatchDetails *chainntnfs.HistoricalSpendDispatch) error { + + op := historicalDispatchDetails.OutPoint + startHeight := historicalDispatchDetails.StartHeight + endHeight := historicalDispatchDetails.EndHeight // Begin scanning blocks at every height to determine if the outpoint // was spent. @@ -890,6 +753,7 @@ func (b *BitcoindNotifier) dispatchSpendDetailsManually(op wire.OutPoint, default: } + // First, we'll fetch the block for the current height. blockHash, err := b.chainConn.GetBlockHash(int64(height)) if err != nil { return fmt.Errorf("unable to retrieve hash for block "+ @@ -901,38 +765,30 @@ func (b *BitcoindNotifier) dispatchSpendDetailsManually(op wire.OutPoint, "%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 _, in := range tx.TxIn { - if in.PreviousOutPoint != op { + for i, txIn := range tx.TxIn { + if txIn.PreviousOutPoint != op { continue } - // If this transaction input spends the - // outpoint, we'll gather the details of the - // spending transaction and dispatch a spend - // notification to our clients. - relTx := chain.RelevantTx{ - TxRecord: &wtxmgr.TxRecord{ - MsgTx: *tx, - Hash: tx.TxHash(), - Received: block.Header.Timestamp, - }, - Block: &wtxmgr.BlockMeta{ - Block: wtxmgr.Block{ - Hash: *blockHash, - Height: height, - }, - Time: block.Header.Timestamp, - }, + // If it does, we'll construct its spend details + // and hand them over to the TxNotifier so that + // it can properly notify its registered + // clients. + txHash := tx.TxHash() + details := &chainntnfs.SpendDetail{ + SpentOutPoint: &op, + SpenderTxHash: &txHash, + SpendingTx: tx, + SpenderInputIndex: uint32(i), + SpendingHeight: int32(height), } - select { - case b.notificationRegistry <- relTx: - case <-b.quit: - return ErrChainNotifierShuttingDown - } - - return nil + return b.txNotifier.UpdateSpendDetails( + op, details, + ) } } } @@ -959,11 +815,11 @@ func (b *BitcoindNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, chainntnfs.Log.Infof("New confirmation subscription: "+ "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 // manual scan for the confirmation. Otherwise the notifier will begin // watching at tip for the transaction to confirm. - dispatch, err := b.txConfNotifier.Register(ntfn) + dispatch, err := b.txNotifier.RegisterConf(ntfn) if err != nil { return nil, err } diff --git a/chainntnfs/bitcoindnotify/bitcoind_dev.go b/chainntnfs/bitcoindnotify/bitcoind_dev.go index f45cda09..ceeeb8ab 100644 --- a/chainntnfs/bitcoindnotify/bitcoind_dev.go +++ b/chainntnfs/bitcoindnotify/bitcoind_dev.go @@ -12,12 +12,11 @@ import ( ) // UnsafeStart starts the notifier with a specified best height and optional -// best hash. Its bestBlock and txConfNotifier are initialized with -// bestHeight and optionally bestHash. The parameter generateBlocks is -// necessary for the bitcoind notifier to ensure we drain all notifications up -// to syncHeight, since if they are generated ahead of UnsafeStart the chainConn -// may start up with an outdated best block and miss sending ntfns. Used for -// testing. +// best hash. Its bestBlock and txNotifier are initialized with bestHeight and +// optionally bestHash. The parameter generateBlocks is necessary for the +// bitcoind notifier to ensure we drain all notifications up to syncHeight, +// since if they are generated ahead of UnsafeStart the chainConn may start up +// with an outdated best block and miss sending ntfns. Used for testing. func (b *BitcoindNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Hash, syncHeight int32, generateBlocks func() error) error { @@ -30,8 +29,9 @@ func (b *BitcoindNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Has return err } - b.txConfNotifier = chainntnfs.NewTxConfNotifier( + b.txNotifier = chainntnfs.NewTxNotifier( uint32(bestHeight), reorgSafetyLimit, b.confirmHintCache, + b.spendHintCache, ) if generateBlocks != nil { diff --git a/chainntnfs/btcdnotify/btcd.go b/chainntnfs/btcdnotify/btcd.go index 5a60a83f..c7eecfbc 100644 --- a/chainntnfs/btcdnotify/btcd.go +++ b/chainntnfs/btcdnotify/btcd.go @@ -74,9 +74,7 @@ type BtcdNotifier struct { notificationCancels chan interface{} notificationRegistry chan interface{} - spendNotifications map[wire.OutPoint]map[uint64]*spendNotification - - txConfNotifier *chainntnfs.TxConfNotifier + txNotifier *chainntnfs.TxNotifier blockEpochClients map[uint64]*blockEpochRegistration @@ -114,8 +112,6 @@ func New(config *rpcclient.ConnConfig, spendHintCache chainntnfs.SpendHintCache, blockEpochClients: make(map[uint64]*blockEpochRegistration), - spendNotifications: make(map[wire.OutPoint]map[uint64]*spendNotification), - chainUpdates: queue.NewConcurrentQueue(10), txUpdates: queue.NewConcurrentQueue(10), @@ -166,8 +162,9 @@ func (b *BtcdNotifier) Start() error { return err } - b.txConfNotifier = chainntnfs.NewTxConfNotifier( + b.txNotifier = chainntnfs.NewTxNotifier( uint32(currentHeight), reorgSafetyLimit, b.confirmHintCache, + b.spendHintCache, ) b.bestBlock = chainntnfs.BlockEpoch{ @@ -203,18 +200,13 @@ func (b *BtcdNotifier) Stop() error { // Notify all pending clients of our shutdown by closing the related // notification channels. - for _, spendClients := range b.spendNotifications { - for _, spendClient := range spendClients { - close(spendClient.spendChan) - } - } for _, epochClient := range b.blockEpochClients { close(epochClient.cancelChan) epochClient.wg.Wait() close(epochClient.epochChan) } - b.txConfNotifier.TearDown() + b.txNotifier.TearDown() return nil } @@ -275,19 +267,6 @@ out: select { case cancelMsg := <-b.notificationCancels: switch msg := cancelMsg.(type) { - case *spendCancel: - chainntnfs.Log.Infof("Cancelling spend "+ - "notification for out_point=%v, "+ - "spend_id=%v", msg.op, msg.spendID) - - // Before we attempt to close the spendChan, - // ensure that the notification hasn't already - // yet been dispatched. - if outPointClients, ok := b.spendNotifications[msg.op]; ok { - close(outPointClients[msg.spendID].spendChan) - delete(b.spendNotifications[msg.op], msg.spendID) - } - case *epochCancel: chainntnfs.Log.Infof("Cancelling epoch "+ "notification, epoch_id=%v", msg.epochID) @@ -314,21 +293,13 @@ out: } case registerMsg := <-b.notificationRegistry: switch msg := registerMsg.(type) { - case *spendNotification: - chainntnfs.Log.Infof("New spend subscription: "+ - "utxo=%v", msg.targetOutpoint) - op := *msg.targetOutpoint - - if _, ok := b.spendNotifications[op]; !ok { - b.spendNotifications[op] = make(map[uint64]*spendNotification) - } - b.spendNotifications[op][msg.spendID] = msg - case *chainntnfs.HistoricalConfDispatch: // Look up whether the transaction is already // included in the active chain. We'll do this // in a goroutine to prevent blocking // potentially long rescans. + // + // TODO(wilmer): add retry logic if rescan fails? b.wg.Add(1) go func() { defer b.wg.Done() @@ -348,7 +319,7 @@ out: // begin safely updating the height hint // cache at tip, since any pending // rescans have now completed. - err = b.txConfNotifier.UpdateConfDetails( + err = b.txNotifier.UpdateConfDetails( *msg.TxID, confDetails, ) if err != nil { @@ -398,7 +369,7 @@ out: newBestBlock, missedBlocks, err := chainntnfs.HandleMissedBlocks( b.chainConn, - b.txConfNotifier, + b.txNotifier, b.bestBlock, update.blockHeight, true, @@ -436,7 +407,7 @@ out: } newBestBlock, err := chainntnfs.RewindChain( - b.chainConn, b.txConfNotifier, b.bestBlock, + b.chainConn, b.txNotifier, b.bestBlock, update.blockHeight-1, ) if err != nil { @@ -449,60 +420,23 @@ out: // partially completed. 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(): newSpend := item.(*txUpdate) // 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 this is a mempool spend, we can ignore it and wait + // for the spend to appear in on-chain. if newSpend.details == nil { continue } - spendingTx := newSpend.tx - - // First, check if this transaction spends an output - // that has an existing spend notification for it. - for i, txIn := range spendingTx.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 := 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) - } + tx := newSpend.tx.MsgTx() + err := b.txNotifier.ProcessRelevantSpendTx( + tx, newSpend.details.Height, + ) + if err != nil { + chainntnfs.Log.Errorf("Unable to process "+ + "transaction %v: %v", tx.TxHash(), err) } case <-b.quit: @@ -703,7 +637,7 @@ func (b *BtcdNotifier) handleBlockConnected(epoch chainntnfs.BlockEpoch) error { connect: true, } - err = b.txConfNotifier.ConnectTip( + err = b.txNotifier.ConnectTip( &newBlock.hash, newBlock.height, newBlock.txns, ) 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, epoch.Hash) - // Define a helper struct for coalescing the spend notifications we will - // dispatch after trying to commit the spend hints. - type spendNtfnBatch struct { - 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. + // 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 - - // Next we'll notify any subscribed clients of the block. 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 } @@ -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 // 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 @@ -859,145 +689,131 @@ type spendCancel struct { func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) { - // Before proceeding to register the notification, we'll query our - // height hint cache to determine whether a better one exists. - if hint, err := b.spendHintCache.QuerySpendHint(*outpoint); err == nil { - if hint > heightHint { - chainntnfs.Log.Debugf("Using height hint %d retrieved "+ - "from cache for %v", hint, outpoint) - heightHint = hint - } + // First, we'll construct a spend notification request and hand it off + // to the txNotifier. + spendID := atomic.AddUint64(&b.spendClientCounter, 1) + cancel := func() { + b.txNotifier.CancelSpend(*outpoint, spendID) + } + 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 - // 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) + historicalDispatch, err := b.txNotifier.RegisterSpend(ntfn) if err != nil { return nil, err } - // If the output is unspent, then we'll write it to the cache with the - // given height hint. This allows us to increase the height hint as the - // chain extends and the output remains unspent. + // If the txNotifier didn't return any details to perform a historical + // scan of the chain, then we can return early as there's nothing left + // 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 { - 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 { - // The error is not fatal, so we should not return an - // 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) - } + return nil, err } - var blockHash *chainhash.Hash - 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 - } - } + return ntfn.Event, nil + } - // We'll only request a rescan if the transaction has actually - // been included within a block. Otherwise, we'll encounter an - // error when scanning for blocks. This can happen in the case - // of a race condition, wherein the output itself is unspent, - // and only arrives in the mempool after the getxout call. - if blockHash != nil { - ops := []*wire.OutPoint{outpoint} + // Otherwise, we'll determine when the output was spent by scanning the + // chain. We'll begin by determining where to start our historical + // rescan. + startHash, err := b.chainConn.GetBlockHash( + int64(historicalDispatch.StartHeight), + ) + 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 - // what may be a long rescan, we'll launch a new - // goroutine to handle the async result of the rescan. - asyncResult := b.chainConn.RescanAsync( - blockHash, nil, ops, - ) - go func() { - rescanErr := asyncResult.Receive() - if rescanErr != nil { - chainntnfs.Log.Errorf("Rescan for spend "+ - "notification txout(%x) "+ - "failed: %v", outpoint, rescanErr) - } - }() + // As a minimal optimization, we'll query the backend's transaction + // index (if enabled) to determine if we have a better rescan starting + // height. We can do this as the GetRawTransaction call will return the + // hash of the block it was included in within the chain. + 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) } } - return &chainntnfs.SpendEvent{ - Spend: ntfn.spendChan, - Cancel: func() { - cancel := &spendCancel{ - op: *outpoint, - spendID: ntfn.spendID, - } + // If the transaction index was enabled, we'll use the block's hash to + // retrieve its height and check whether it provides a better starting + // point for our rescan. + if tx != nil { + // If the transaction containing the outpoint hasn't confirmed + // on-chain, then there's no need to perform a rescan. + if tx.BlockHash == "" { + return ntfn.Event, nil + } - // Submit spend cancellation to notification dispatcher. - select { - case b.notificationCancels <- cancel: - // Cancellation is being handled, drain the spend chan until it is - // closed before yielding to the caller. - for { - select { - case _, ok := <-ntfn.spendChan: - if !ok { - return - } - case <-b.quit: - return - } - } - case <-b.quit: + blockHash, err := chainhash.NewHashFromStr(tx.BlockHash) + if err != nil { + return nil, err + } + blockHeader, err := b.chainConn.GetBlockHeaderVerbose(blockHash) + if err != nil { + return nil, fmt.Errorf("unable to get header for "+ + "block %v: %v", blockHash, err) + } + + if uint32(blockHeader.Height) > historicalDispatch.StartHeight { + startHash, err = b.chainConn.GetBlockHash( + int64(blockHeader.Height), + ) + if err != nil { + 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 @@ -1019,11 +835,11 @@ func (b *BtcdNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, _ []byte, chainntnfs.Log.Infof("New confirmation subscription: "+ "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 // manual scan for the confirmation. Otherwise the notifier will begin // watching at tip for the transaction to confirm. - dispatch, err := b.txConfNotifier.Register(ntfn) + dispatch, err := b.txNotifier.RegisterConf(ntfn) if err != nil { return nil, err } diff --git a/chainntnfs/btcdnotify/btcd_dev.go b/chainntnfs/btcdnotify/btcd_dev.go index af64dad8..7e39d8a5 100644 --- a/chainntnfs/btcdnotify/btcd_dev.go +++ b/chainntnfs/btcdnotify/btcd_dev.go @@ -11,12 +11,11 @@ import ( ) // UnsafeStart starts the notifier with a specified best height and optional -// best hash. Its bestBlock and txConfNotifier are initialized with -// bestHeight and optionally bestHash. The parameter generateBlocks is -// necessary for the bitcoind notifier to ensure we drain all notifications up -// to syncHeight, since if they are generated ahead of UnsafeStart the chainConn -// may start up with an outdated best block and miss sending ntfns. Used for -// testing. +// best hash. Its bestBlock and txNotifier are initialized with bestHeight and +// optionally bestHash. The parameter generateBlocks is necessary for the +// bitcoind notifier to ensure we drain all notifications up to syncHeight, +// since if they are generated ahead of UnsafeStart the chainConn may start up +// with an outdated best block and miss sending ntfns. Used for testing. func (b *BtcdNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Hash, syncHeight int32, generateBlocks func() error) error { @@ -29,8 +28,9 @@ func (b *BtcdNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Hash, return err } - b.txConfNotifier = chainntnfs.NewTxConfNotifier( + b.txNotifier = chainntnfs.NewTxNotifier( uint32(bestHeight), reorgSafetyLimit, b.confirmHintCache, + b.spendHintCache, ) b.chainUpdates.Start() diff --git a/chainntnfs/height_hint_cache.go b/chainntnfs/height_hint_cache.go index 33dac971..cd499a6e 100644 --- a/chainntnfs/height_hint_cache.go +++ b/chainntnfs/height_hint_cache.go @@ -127,6 +127,10 @@ func (c *HeightHintCache) CommitSpendHint(height uint32, ops ...wire.OutPoint) e return nil } + if len(ops) == 0 { + return nil + } + Log.Tracef("Updating spend hint to height %d for %v", height, ops) return c.db.Batch(func(tx *bolt.Tx) error { @@ -197,6 +201,10 @@ func (c *HeightHintCache) PurgeSpendHint(ops ...wire.OutPoint) error { return nil } + if len(ops) == 0 { + return nil + } + Log.Tracef("Removing spend hints for %v", ops) return c.db.Batch(func(tx *bolt.Tx) error { @@ -228,6 +236,10 @@ func (c *HeightHintCache) CommitConfirmHint(height uint32, txids ...chainhash.Ha return nil } + if len(txids) == 0 { + return nil + } + Log.Tracef("Updating confirm hints to height %d for %v", height, txids) return c.db.Batch(func(tx *bolt.Tx) error { @@ -299,6 +311,10 @@ func (c *HeightHintCache) PurgeConfirmHint(txids ...chainhash.Hash) error { return nil } + if len(txids) == 0 { + return nil + } + Log.Tracef("Removing confirm hints for %v", txids) return c.db.Batch(func(tx *bolt.Tx) error { diff --git a/chainntnfs/interface.go b/chainntnfs/interface.go index 9b878669..f8f386d5 100644 --- a/chainntnfs/interface.go +++ b/chainntnfs/interface.go @@ -159,19 +159,39 @@ type ConfirmationEvent struct { // Confirmed is a channel that will be sent upon once the transaction // has been fully confirmed. The struct sent will contain all the // 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 // confirmation, how many confirmations are left to declare the // 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 // have a struct chan that's closed if funding gets re-org out. Need // to sync, to request another confirmation event ntfn, then re-open // channel after confs. - NegativeConf 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 @@ -196,7 +216,16 @@ type SpendDetail struct { type SpendEvent struct { // Spend is a receive only channel which will be sent upon once the // 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 // case that they wish to prematurely abandon their registered spend @@ -204,6 +233,15 @@ type SpendEvent struct { 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 // main chain. type BlockEpoch struct { @@ -225,7 +263,9 @@ type BlockEpoch struct { type BlockEpochEvent struct { // 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. - 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 // case that they wish to abandon their registered spend notification. @@ -392,10 +432,10 @@ func GetClientMissedBlocks(chainConn ChainConn, clientBestBlock *BlockEpoch, return missedBlocks, nil } -// RewindChain handles internal state updates for the notifier's TxConfNotifier -// It has no effect if given a height greater than or equal to our current best +// RewindChain handles internal state updates for the notifier's TxNotifier It +// 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. -func RewindChain(chainConn ChainConn, txConfNotifier *TxConfNotifier, +func RewindChain(chainConn ChainConn, txNotifier *TxNotifier, currBestBlock BlockEpoch, targetHeight int32) (BlockEpoch, error) { newBestBlock := BlockEpoch{ @@ -414,7 +454,7 @@ func RewindChain(chainConn ChainConn, txConfNotifier *TxConfNotifier, Log.Infof("Block disconnected from main chain: "+ "height=%v, sha=%v", height, newBestBlock.Hash) - err = txConfNotifier.DisconnectTip(uint32(height)) + err = txNotifier.DisconnectTip(uint32(height)) if err != nil { return newBestBlock, fmt.Errorf("unable to "+ " 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 // erroring. In the case where there is no rewind, the notifier's // current best block is returned. -func HandleMissedBlocks(chainConn ChainConn, txConfNotifier *TxConfNotifier, +func HandleMissedBlocks(chainConn ChainConn, txNotifier *TxNotifier, currBestBlock BlockEpoch, newHeight int32, backendStoresReorgs bool) (BlockEpoch, []BlockEpoch, error) { @@ -462,7 +502,7 @@ func HandleMissedBlocks(chainConn ChainConn, txConfNotifier *TxConfNotifier, "common ancestor: %v", err) } - currBestBlock, err = RewindChain(chainConn, txConfNotifier, + currBestBlock, err = RewindChain(chainConn, txNotifier, currBestBlock, startingHeight) if err != nil { return currBestBlock, nil, fmt.Errorf("unable to "+ diff --git a/chainntnfs/interface_test.go b/chainntnfs/interface_test.go index 36006dcc..b4bfa30e 100644 --- a/chainntnfs/interface_test.go +++ b/chainntnfs/interface_test.go @@ -232,6 +232,8 @@ func checkNotificationFields(ntfn *chainntnfs.SpendDetail, outpoint *wire.OutPoint, spenderSha *chainhash.Hash, height int32, t *testing.T) { + t.Helper() + if *ntfn.SpentOutPoint != *outpoint { t.Fatalf("ntfn includes wrong output, reports "+ "%v instead of %v", @@ -756,6 +758,8 @@ func testSpendBeforeNtfnRegistration(miner *rpctest.Harness, // already happened. The notifier should dispatch a spend notification // immediately. checkSpends := func() { + t.Helper() + const numClients = 2 spendClients := make([]*chainntnfs.SpendEvent, numClients) 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 // receiving historical block epoch notifications due to their best known block // 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 // client may not receive all historical notifications. bestHeight := outdatedHeight + numBlocks - if err := notifier.UnsafeStart( - bestHeight, nil, bestHeight, generateBlocks); err != nil { - - t.Fatalf("Unable to unsafe start the notifier: %v", err) + err = notifier.UnsafeStart(bestHeight, nil, bestHeight, generateBlocks) + if err != nil { + t.Fatalf("unable to unsafe start the notifier: %v", err) } + defer notifier.Stop() // Create numClients clients whose best known block is 10 blocks behind // 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. - if err := notifier.UnsafeStart(bestHeight, - nil, bestHeight+numBlocks, generateBlocks); err != nil { - - t.Fatalf("Unable to unsafe start the notifier: %v", err) + err = notifier.UnsafeStart( + bestHeight, nil, bestHeight+numBlocks, generateBlocks, + ) + 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. 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 // the common ancestor between the two chains. syncHeight := nodeHeight1 + numBlocks + 1 - if err := notifier.UnsafeStart(nodeHeight1+numBlocks, - blocks[numBlocks-1], syncHeight, nil); err != nil { - + err = notifier.UnsafeStart( + nodeHeight1+numBlocks, blocks[numBlocks-1], syncHeight, nil, + ) + 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. clients := make([]*chainntnfs.BlockEpochEvent, 0, numClients) @@ -1545,6 +1703,10 @@ var ntfnTests = []testCase{ name: "reorg conf", test: testReorgConf, }, + { + name: "reorg spend", + test: testReorgSpend, + }, } var blockCatchupTests = []blockCatchupTestCase{ @@ -1680,9 +1842,6 @@ func TestInterfaces(t *testing.T) { success := t.Run(testName, func(t *testing.T) { blockCatchupTest.test(miner, notifier, t) }) - - notifier.Stop() - if !success { break } diff --git a/chainntnfs/neutrinonotify/neutrino.go b/chainntnfs/neutrinonotify/neutrino.go index b4455442..f0d4da2e 100644 --- a/chainntnfs/neutrinonotify/neutrino.go +++ b/chainntnfs/neutrinonotify/neutrino.go @@ -68,9 +68,7 @@ type NeutrinoNotifier struct { notificationCancels chan interface{} notificationRegistry chan interface{} - spendNotifications map[wire.OutPoint]map[uint64]*spendNotification - - txConfNotifier *chainntnfs.TxConfNotifier + txNotifier *chainntnfs.TxNotifier blockEpochClients map[uint64]*blockEpochRegistration @@ -109,8 +107,6 @@ func New(node *neutrino.ChainService, spendHintCache chainntnfs.SpendHintCache, blockEpochClients: make(map[uint64]*blockEpochRegistration), - spendNotifications: make(map[wire.OutPoint]map[uint64]*spendNotification), - p2pNode: node, rescanErr: make(chan error), @@ -162,8 +158,9 @@ func (n *NeutrinoNotifier) Start() error { neutrino.WatchInputs(zeroInput), } - n.txConfNotifier = chainntnfs.NewTxConfNotifier( + n.txNotifier = chainntnfs.NewTxNotifier( n.bestHeight, reorgSafetyLimit, n.confirmHintCache, + n.spendHintCache, ) 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 // notification channels. - for _, spendClients := range n.spendNotifications { - for _, spendClient := range spendClients { - close(spendClient.spendChan) - } - } for _, epochClient := range n.blockEpochClients { close(epochClient.cancelChan) epochClient.wg.Wait() close(epochClient.epochChan) } - n.txConfNotifier.TearDown() + n.txNotifier.TearDown() return nil } @@ -225,6 +217,14 @@ type filteredBlock struct { 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 // connected to the end of the main chain. func (n *NeutrinoNotifier) onFilteredBlockConnected(height int32, @@ -263,19 +263,6 @@ out: select { case cancelMsg := <-n.notificationCancels: switch msg := cancelMsg.(type) { - case *spendCancel: - chainntnfs.Log.Infof("Cancelling spend "+ - "notification for out_point=%v, "+ - "spend_id=%v", msg.op, msg.spendID) - - // Before we attempt to close the spendChan, - // ensure that the notification hasn't already - // yet been dispatched. - if outPointClients, ok := n.spendNotifications[msg.op]; ok { - close(outPointClients[msg.spendID].spendChan) - delete(n.spendNotifications[msg.op], msg.spendID) - } - case *epochCancel: chainntnfs.Log.Infof("Cancelling epoch "+ "notification, epoch_id=%v", msg.epochID) @@ -303,21 +290,9 @@ out: case registerMsg := <-n.notificationRegistry: 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: - // Look up whether the transaction is already - // included in the active chain. We'll do this - // in a goroutine to prevent blocking + // We'll start a historical rescan chain of the + // chain asynchronously to prevent blocking // potentially long rescans. n.wg.Add(1) go func() { @@ -331,18 +306,6 @@ out: 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 // without error, we will invoke // UpdateConfDetails even if none were @@ -350,31 +313,12 @@ out: // begin safely updating the height hint // cache at tip, since any pending // rescans have now completed. - err = n.txConfNotifier.UpdateConfDetails( + err = n.txNotifier.UpdateConfDetails( *msg.TxID, confDetails, ) if err != nil { 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: @@ -399,6 +343,14 @@ out: } } 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(): @@ -426,7 +378,7 @@ out: _, missedBlocks, err := chainntnfs.HandleMissedBlocks( n.chainConn, - n.txConfNotifier, + n.txNotifier, bestBlock, int32(update.height), false, @@ -482,7 +434,7 @@ out: Hash: hash, } newBestBlock, err := chainntnfs.RewindChain( - n.chainConn, n.txConfNotifier, notifierBestBlock, + n.chainConn, n.txNotifier, notifierBestBlock, int32(update.height-1), ) 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 // been connected to the main chain. Send out any N confirmation // notifications which may have been triggered by this new block. - err := n.txConfNotifier.ConnectTip( + err := n.txNotifier.ConnectTip( &newBlock.hash, newBlock.height, newBlock.txns, ) 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, 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 // so if any subscribers make queries based on their received // block epoch, our state is fully updated in time. @@ -674,23 +564,6 @@ func (n *NeutrinoNotifier) handleBlockConnected(newBlock *filteredBlock) error { // of the block. 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 } @@ -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 // 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 @@ -766,86 +617,91 @@ type spendCancel struct { func (n *NeutrinoNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) { - n.heightMtx.RLock() - currentHeight := n.bestHeight - n.heightMtx.RUnlock() - - // Before proceeding to register the notification, we'll query our - // height hint cache to determine whether a better one exists. - if hint, err := n.spendHintCache.QuerySpendHint(*outpoint); err == nil { - if hint > heightHint { - chainntnfs.Log.Debugf("Using height hint %d retrieved "+ - "from cache for %v", hint, outpoint) - heightHint = hint - } + // First, we'll construct a spend notification request and hand it off + // to the txNotifier. + spendID := atomic.AddUint64(&n.spendClientCounter, 1) + cancel := func() { + n.txNotifier.CancelSpend(*outpoint, spendID) + } + ntfn := &chainntnfs.SpendNtfn{ + SpendID: spendID, + OutPoint: *outpoint, + Event: chainntnfs.NewSpendEvent(cancel), + HeightHint: heightHint, } - // Construct a notification request for the outpoint. We'll defer - // sending it to the main event loop until after we've guaranteed that - // the outpoint has not been spent. - ntfn := &spendNotification{ - targetOutpoint: outpoint, - spendChan: make(chan *chainntnfs.SpendDetail, 1), - spendID: atomic.AddUint64(&n.spendClientCounter, 1), - heightHint: heightHint, + historicalDispatch, err := n.txNotifier.RegisterSpend(ntfn) + if err != nil { + return nil, err } - spendEvent := &chainntnfs.SpendEvent{ - Spend: ntfn.spendChan, - Cancel: func() { - cancel := &spendCancel{ - op: *outpoint, - 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 + // If the txNotifier didn't return any details to perform a historical + // scan of the chain, then we can return early as there's nothing left + // for us to do. + if historicalDispatch == nil { + return ntfn.Event, nil } + // 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{ OutPoint: *outpoint, 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( neutrino.WatchInputs(inputToWatch), 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") { @@ -854,57 +710,28 @@ func (n *NeutrinoNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, // If a spend report was returned, and the transaction is present, then // this means that the output is already spent. + var spendDetails *chainntnfs.SpendDetail if spendReport != nil && spendReport.SpendingTx != nil { - // As a result, we'll launch a goroutine to immediately - // dispatch the notification with a normal response. - go func() { - txSha := spendReport.SpendingTx.TxHash() - select { - case ntfn.spendChan <- &chainntnfs.SpendDetail{ - SpentOutPoint: outpoint, - SpenderTxHash: &txSha, - SpendingTx: spendReport.SpendingTx, - SpenderInputIndex: spendReport.SpendingInputIndex, - SpendingHeight: int32(spendReport.SpendingTxHeight), - }: - case <-n.quit: - return - } - - }() - - return spendEvent, nil + spendingTxHash := spendReport.SpendingTx.TxHash() + spendDetails = &chainntnfs.SpendDetail{ + SpentOutPoint: outpoint, + SpenderTxHash: &spendingTxHash, + SpendingTx: spendReport.SpendingTx, + SpenderInputIndex: spendReport.SpendingInputIndex, + SpendingHeight: int32(spendReport.SpendingTxHeight), + } } - // If the output is still unspent, then we'll update our rescan's - // filter, and send the request to the dispatcher goroutine. - rescanUpdate := []neutrino.UpdateOption{ - neutrino.AddInputs(inputToWatch), - neutrino.Rewind(currentHeight), - neutrino.DisableDisconnectedNtfns(true), - } - - if err := n.chainView.Update(rescanUpdate...); err != nil { + // Finally, no matter whether the rescan found a spend in the past or + // not, we'll mark our historical rescan as complete to ensure the + // outpoint's spend hint gets updated upon connected/disconnected + // blocks. + err = n.txNotifier.UpdateSpendDetails(*outpoint, spendDetails) + if err != nil { return nil, err } - select { - 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 + return ntfn.Event, nil } // RegisterConfirmationsNtfn registers a notification with NeutrinoNotifier @@ -928,11 +755,11 @@ func (n *NeutrinoNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, chainntnfs.Log.Infof("New confirmation subscription: "+ "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 // manual scan for the confirmation. Otherwise the notifier will begin // watching at tip for the transaction to confirm. - dispatch, err := n.txConfNotifier.Register(ntfn) + dispatch, err := n.txNotifier.RegisterConf(ntfn) if err != nil { return nil, err } @@ -941,12 +768,55 @@ func (n *NeutrinoNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, 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 { - case n.notificationRegistry <- dispatch: - return ntfn.Event, nil + case n.notificationRegistry <- &rescanFilterUpdate{ + updateOptions: []neutrino.UpdateOption{ + neutrino.AddAddrs(addrs...), + neutrino.Rewind(dispatch.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) + } + + // 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 diff --git a/chainntnfs/neutrinonotify/neutrino_dev.go b/chainntnfs/neutrinonotify/neutrino_dev.go index b069ff5d..3a1b19b9 100644 --- a/chainntnfs/neutrinonotify/neutrino_dev.go +++ b/chainntnfs/neutrinonotify/neutrino_dev.go @@ -13,13 +13,14 @@ import ( ) // UnsafeStart starts the notifier with a specified best height and optional -// best hash. Its bestHeight, txConfNotifier and neutrino node are initialized -// with bestHeight. The parameter generateBlocks is necessary for the -// bitcoind notifier to ensure we drain all notifications up to syncHeight, -// since if they are generated ahead of UnsafeStart the chainConn may start -// up with an outdated best block and miss sending ntfns. Used for testing. -func (n *NeutrinoNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Hash, - syncHeight int32, generateBlocks func() error) error { +// best hash. Its bestHeight, txNotifier and neutrino node are initialized with +// bestHeight. The parameter generateBlocks is necessary for the bitcoind +// notifier to ensure we drain all notifications up to syncHeight, since if they +// are generated ahead of UnsafeStart the chainConn may start up with an +// outdated best block and miss sending ntfns. Used for testing. +func (n *NeutrinoNotifier) UnsafeStart(bestHeight int32, + bestHash *chainhash.Hash, syncHeight int32, + generateBlocks func() error) error { // 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 @@ -47,8 +48,9 @@ func (n *NeutrinoNotifier) UnsafeStart(bestHeight int32, bestHash *chainhash.Has neutrino.WatchInputs(zeroInput), } - n.txConfNotifier = chainntnfs.NewTxConfNotifier( + n.txNotifier = chainntnfs.NewTxNotifier( uint32(bestHeight), reorgSafetyLimit, n.confirmHintCache, + n.spendHintCache, ) n.chainConn = &NeutrinoChainConn{n.p2pNode} diff --git a/chainntnfs/txconfnotifier.go b/chainntnfs/txconfnotifier.go deleted file mode 100644 index fb3998c4..00000000 --- a/chainntnfs/txconfnotifier.go +++ /dev/null @@ -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) - } - } -} diff --git a/chainntnfs/txconfnotifier_test.go b/chainntnfs/txconfnotifier_test.go deleted file mode 100644 index 04f13ef5..00000000 --- a/chainntnfs/txconfnotifier_test.go +++ /dev/null @@ -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) - } -} diff --git a/chainntnfs/txnotifier.go b/chainntnfs/txnotifier.go new file mode 100644 index 00000000..e2a682ea --- /dev/null +++ b/chainntnfs/txnotifier.go @@ -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) + } + } +} diff --git a/chainntnfs/txnotifier_test.go b/chainntnfs/txnotifier_test.go new file mode 100644 index 00000000..98a62f2f --- /dev/null +++ b/chainntnfs/txnotifier_test.go @@ -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) + } +}