diff --git a/chainntnfs/btcdnotify/btcd.go b/chainntnfs/btcdnotify/btcd.go index 33f15a40..8f7fe57f 100644 --- a/chainntnfs/btcdnotify/btcd.go +++ b/chainntnfs/btcdnotify/btcd.go @@ -431,13 +431,13 @@ out: continue } - tx := newSpend.tx.MsgTx() err := b.txNotifier.ProcessRelevantSpendTx( - tx, newSpend.details.Height, + newSpend.tx, uint32(newSpend.details.Height), ) if err != nil { chainntnfs.Log.Errorf("Unable to process "+ - "transaction %v: %v", tx.TxHash(), err) + "transaction %v: %v", + newSpend.tx.Hash(), err) } case <-b.quit: @@ -703,32 +703,57 @@ func (b *BtcdNotifier) notifyBlockEpochClient(epochClient *blockEpochRegistratio } // RegisterSpendNtfn registers an intent to be notified once the target -// outpoint has been spent by a transaction on-chain. Once a spend of the target -// outpoint has been detected, the details of the spending event will be sent -// across the 'Spend' channel. The heightHint should represent the earliest -// height in the chain where the transaction could have been spent in. +// outpoint/output script has been spent by a transaction on-chain. When +// intending to be notified of the spend of an output script, a nil outpoint +// must be used. The heightHint should represent the earliest height in the +// chain of the transaction that spent the outpoint/output script. +// +// Once a spend of has been detected, the details of the spending event will be +// sent across the 'Spend' channel. func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) { // 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) + spendRequest, err := chainntnfs.NewSpendRequest(outpoint, pkScript) + if err != nil { + return nil, err } ntfn := &chainntnfs.SpendNtfn{ - SpendID: spendID, - OutPoint: *outpoint, - PkScript: pkScript, - Event: chainntnfs.NewSpendEvent(cancel), + SpendID: spendID, + SpendRequest: spendRequest, + Event: chainntnfs.NewSpendEvent(func() { + b.txNotifier.CancelSpend(spendRequest, spendID) + }), HeightHint: heightHint, } - historicalDispatch, err := b.txNotifier.RegisterSpend(ntfn) + historicalDispatch, _, err := b.txNotifier.RegisterSpend(ntfn) if err != nil { return nil, err } + // We'll then request the backend to notify us when it has detected the + // outpoint/output script as spent. + // + // TODO(wilmer): use LoadFilter API instead. + if spendRequest.OutPoint == chainntnfs.ZeroOutPoint { + addr, err := spendRequest.PkScript.Address(b.chainParams) + if err != nil { + return nil, err + } + addrs := []btcutil.Address{addr} + if err := b.chainConn.NotifyReceived(addrs); err != nil { + return nil, err + } + } else { + ops := []*wire.OutPoint{&spendRequest.OutPoint} + if err := b.chainConn.NotifySpent(ops); err != nil { + return nil, err + } + } + // If the txNotifier didn't return any details to perform a historical // scan of the chain, then we can return early as there's nothing left // for us to do. @@ -736,24 +761,55 @@ func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, 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 + // Otherwise, we'll need to dispatch a historical rescan to determine if + // the outpoint was already spent at a previous height. + // + // We'll short-circuit the path when dispatching the spend of a script, + // rather than an outpoint, as there aren't any additional checks we can + // make for scripts. + if spendRequest.OutPoint == chainntnfs.ZeroOutPoint { + startHash, err := b.chainConn.GetBlockHash( + int64(historicalDispatch.StartHeight), + ) + if err != nil { + return nil, err + } + + // TODO(wilmer): add retry logic if rescan fails? + addr, err := spendRequest.PkScript.Address(b.chainParams) + if err != nil { + return nil, err + } + addrs := []btcutil.Address{addr} + asyncResult := b.chainConn.RescanAsync(startHash, addrs, nil) + go func() { + if rescanErr := asyncResult.Receive(); rescanErr != nil { + chainntnfs.Log.Errorf("Rescan to determine "+ + "the spend details of %v failed: %v", + spendRequest, rescanErr) + } + }() + + return ntfn.Event, nil } - // In addition to the check above, we'll also check the backend's UTXO - // 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) + // When dispatching spends of outpoints, there are a number of checks we + // can make to start our rescan from a better height or completely avoid + // it. + // + // We'll start by checking the backend's UTXO set to determine whether + // the outpoint has been spent. If it hasn't, we can return to the + // caller as well. + txOut, err := b.chainConn.GetTxOut( + &spendRequest.OutPoint.Hash, spendRequest.OutPoint.Index, true, + ) if err != nil { return nil, err } if txOut != nil { // 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) + err := b.txNotifier.UpdateSpendDetails(spendRequest, nil) if err != nil { return nil, err } @@ -761,9 +817,9 @@ func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, return ntfn.Event, nil } - // Otherwise, we'll determine when the output was spent by scanning the - // chain. We'll begin by determining where to start our historical - // rescan. + // Since the outpoint was spent, as it no longer exists within the UTXO + // set, we'll determine when it happened by scanning the chain. We'll + // begin by fetching the block hash of our starting height. startHash, err := b.chainConn.GetBlockHash( int64(historicalDispatch.StartHeight), ) @@ -776,14 +832,14 @@ func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, // 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) + tx, err := b.chainConn.GetRawTransactionVerbose(&spendRequest.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, fmt.Errorf("unable to query for txid %v: %v", + spendRequest.OutPoint.Hash, err) } } @@ -819,6 +875,9 @@ func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, } } + // Now that we've determined the best starting point for our rescan, + // we can go ahead and dispatch it. + // // In order to ensure that we don't block the caller on what may be a // long rescan, we'll launch a new goroutine to handle the async result // of the rescan. We purposefully prevent from adding this goroutine to @@ -826,11 +885,14 @@ func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, // asyncResult channel not being exposed. // // TODO(wilmer): add retry logic if rescan fails? - asyncResult := b.chainConn.RescanAsync(startHash, nil, ops) + asyncResult := b.chainConn.RescanAsync( + startHash, nil, []*wire.OutPoint{&spendRequest.OutPoint}, + ) go func() { if rescanErr := asyncResult.Receive(); rescanErr != nil { chainntnfs.Log.Errorf("Rescan to determine the spend "+ - "details of %v failed: %v", outpoint, rescanErr) + "details of %v failed: %v", spendRequest, + rescanErr) } }()