chainntnfs/bitcoindnotify: support registration for script spends

In this commit, we extend the BitcoindNotifier to support registering
scripts for spends notifications. Once the script has been detected as
spent within the chain, a spend notification will be dispatched through
the Spend channel of the SpendEvent returned upon registration.

For scripts that have been spent in the past, the rescan logic has been
modified to match on the script rather than the outpoint. This is done
by re-deriving the script of the output a transaction input is spending
and checking whether it matches ours.

For scripts that are unspent, a request to the backend will be sent to
alert the BitcoindNotifier of when the script was spent by a
transaction. To make this request we encode the script as an address, as
this is what the backend uses to detect the spend. The transaction will
then be proxied through the txUpdates concurrent queue, which will hand
it off to the underlying txNotifier and dispatch spend notifications to
the relevant clients.

Along the way, we also address an issue where we'd miss detecting that
an outpoint/script has been spent in the future due to not receiving a
historical dispatch request from the underlying txNotifier. To fix this,
we ensure that we always request the backend to notify us of the spend
once it detects it at tip, regardless of whether a historical rescan was
detected or not.
This commit is contained in:
Wilmer Paulino 2018-12-06 21:14:28 -08:00
parent 808c6ae660
commit 482f05a3bc
No known key found for this signature in database
GPG Key ID: 6DF57B9F9514972F
2 changed files with 140 additions and 62 deletions

@ -1,7 +1,6 @@
package bitcoindnotify package bitcoindnotify
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
@ -272,8 +271,11 @@ out:
if err != nil { if err != nil {
chainntnfs.Log.Errorf("Rescan to "+ chainntnfs.Log.Errorf("Rescan to "+
"determine the spend "+ "determine the spend "+
"details of %v failed: %v", "details of %v within "+
msg.OutPoint, err) "range %d-%d failed: %v",
msg.SpendRequest,
msg.StartHeight,
msg.EndHeight, err)
} }
}() }()
@ -379,14 +381,14 @@ out:
continue continue
} }
tx := &item.TxRecord.MsgTx tx := btcutil.NewTx(&item.TxRecord.MsgTx)
err := b.txNotifier.ProcessRelevantSpendTx( err := b.txNotifier.ProcessRelevantSpendTx(
tx, item.Block.Height, tx, uint32(item.Block.Height),
) )
if err != nil { if err != nil {
chainntnfs.Log.Errorf("Unable to "+ chainntnfs.Log.Errorf("Unable to "+
"process transaction %v: %v", "process transaction %v: %v",
tx.TxHash(), err) tx.Hash(), err)
} }
} }
@ -639,33 +641,57 @@ func (b *BitcoindNotifier) notifyBlockEpochClient(epochClient *blockEpochRegistr
} }
// RegisterSpendNtfn registers an intent to be notified once the target // RegisterSpendNtfn registers an intent to be notified once the target
// outpoint has been spent by a transaction on-chain. Once a spend of the target // outpoint/output script has been spent by a transaction on-chain. When
// outpoint has been detected, the details of the spending event will be sent // intending to be notified of the spend of an output script, a nil outpoint
// across the 'Spend' channel. The heightHint should represent the earliest // must be used. The heightHint should represent the earliest height in the
// height in the chain where the transaction could have been spent in. // chain of the transaction that spent the outpoint/output script.
//
// Once a spend of has been detected, the details of the spending event will be
// sent across the 'Spend' channel.
func (b *BitcoindNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, func (b *BitcoindNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) { pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) {
// First, we'll construct a spend notification request and hand it off // First, we'll construct a spend notification request and hand it off
// to the txNotifier. // to the txNotifier.
spendID := atomic.AddUint64(&b.spendClientCounter, 1) spendID := atomic.AddUint64(&b.spendClientCounter, 1)
cancel := func() { spendRequest, err := chainntnfs.NewSpendRequest(outpoint, pkScript)
b.txNotifier.CancelSpend(*outpoint, spendID) if err != nil {
return nil, err
} }
ntfn := &chainntnfs.SpendNtfn{ ntfn := &chainntnfs.SpendNtfn{
SpendID: spendID, SpendID: spendID,
OutPoint: *outpoint, SpendRequest: spendRequest,
PkScript: pkScript, Event: chainntnfs.NewSpendEvent(func() {
Event: chainntnfs.NewSpendEvent(cancel), b.txNotifier.CancelSpend(spendRequest, spendID)
}),
HeightHint: heightHint, HeightHint: heightHint,
} }
historicalDispatch, err := b.txNotifier.RegisterSpend(ntfn) historicalDispatch, _, err := b.txNotifier.RegisterSpend(ntfn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// We'll then request the backend to notify us when it has detected the
// outpoint/output script as spent.
//
// TODO(wilmer): use LoadFilter API instead.
if spendRequest.OutPoint == chainntnfs.ZeroOutPoint {
addr, err := spendRequest.PkScript.Address(b.chainParams)
if err != nil {
return nil, err
}
addrs := []btcutil.Address{addr}
if err := b.chainConn.NotifyReceived(addrs); err != nil {
return nil, err
}
} else {
ops := []*wire.OutPoint{&spendRequest.OutPoint}
if err := b.chainConn.NotifySpent(ops); err != nil {
return nil, err
}
}
// If the txNotifier didn't return any details to perform a historical // If the txNotifier didn't return any details to perform a historical
// scan of the chain, then we can return early as there's nothing left // scan of the chain, then we can return early as there's nothing left
// for us to do. // for us to do.
@ -673,23 +699,39 @@ func (b *BitcoindNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
return ntfn.Event, nil return ntfn.Event, nil
} }
// We'll then request the backend to notify us when it has detected the // Otherwise, we'll need to dispatch a historical rescan to determine if
// outpoint as spent. // the outpoint was already spent at a previous height.
if err := b.chainConn.NotifySpent([]*wire.OutPoint{outpoint}); err != nil { //
return nil, err // We'll short-circuit the path when dispatching the spend of a script,
// rather than an outpoint, as there aren't any additional checks we can
// make for scripts.
if spendRequest.OutPoint == chainntnfs.ZeroOutPoint {
select {
case b.notificationRegistry <- historicalDispatch:
case <-b.quit:
return nil, ErrChainNotifierShuttingDown
}
return ntfn.Event, nil
} }
// In addition to the check above, we'll also check the backend's UTXO // When dispatching spends of outpoints, there are a number of checks we
// set to determine whether the outpoint has been spent. If it hasn't, // can make to start our rescan from a better height or completely avoid
// we can return to the caller as well. // it.
txOut, err := b.chainConn.GetTxOut(&outpoint.Hash, outpoint.Index, true) //
// We'll start by checking the backend's UTXO set to determine whether
// the outpoint has been spent. If it hasn't, we can return to the
// caller as well.
txOut, err := b.chainConn.GetTxOut(
&spendRequest.OutPoint.Hash, spendRequest.OutPoint.Index, true,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if txOut != nil { if txOut != nil {
// We'll let the txNotifier know the outpoint is still unspent // We'll let the txNotifier know the outpoint is still unspent
// in order to begin updating its spend hint. // in order to begin updating its spend hint.
err := b.txNotifier.UpdateSpendDetails(*outpoint, nil) err := b.txNotifier.UpdateSpendDetails(spendRequest, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -697,22 +739,21 @@ func (b *BitcoindNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
return ntfn.Event, nil return ntfn.Event, nil
} }
// Otherwise, we'll determine when the output was spent by scanning the // Since the outpoint was spent, as it no longer exists within the UTXO
// chain. We'll begin by determining where to start our historical // set, we'll determine when it happened by scanning the chain.
// rescan.
// //
// As a minimal optimization, we'll query the backend's transaction // As a minimal optimization, we'll query the backend's transaction
// index (if enabled) to determine if we have a better rescan starting // index (if enabled) to determine if we have a better rescan starting
// height. We can do this as the GetRawTransaction call will return the // height. We can do this as the GetRawTransaction call will return the
// hash of the block it was included in within the chain. // hash of the block it was included in within the chain.
tx, err := b.chainConn.GetRawTransactionVerbose(&outpoint.Hash) tx, err := b.chainConn.GetRawTransactionVerbose(&spendRequest.OutPoint.Hash)
if err != nil { if err != nil {
// Avoid returning an error if the transaction was not found to // Avoid returning an error if the transaction was not found to
// proceed with fallback methods. // proceed with fallback methods.
jsonErr, ok := err.(*btcjson.RPCError) jsonErr, ok := err.(*btcjson.RPCError)
if !ok || jsonErr.Code != btcjson.ErrRPCNoTxInfo { if !ok || jsonErr.Code != btcjson.ErrRPCNoTxInfo {
return nil, fmt.Errorf("unable to query for "+ return nil, fmt.Errorf("unable to query for txid %v: %v",
"txid %v: %v", outpoint.Hash, err) spendRequest.OutPoint.Hash, err)
} }
} }
@ -741,23 +782,24 @@ func (b *BitcoindNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint,
} }
// Now that we've determined the starting point of our rescan, we can // Now that we've determined the starting point of our rescan, we can
// dispatch it. // dispatch it and return.
select { select {
case b.notificationRegistry <- historicalDispatch: case b.notificationRegistry <- historicalDispatch:
return ntfn.Event, nil
case <-b.quit: case <-b.quit:
return nil, ErrChainNotifierShuttingDown return nil, ErrChainNotifierShuttingDown
} }
return ntfn.Event, nil
} }
// disaptchSpendDetailsManually attempts to manually scan the chain within the // disaptchSpendDetailsManually attempts to manually scan the chain within the
// given height range for a transaction that spends the given outpoint. If one // given height range for a transaction that spends the given outpoint/output
// is found, it's spending details are sent to the notifier dispatcher, which // script. If one is found, it's spending details are sent to the TxNotifier,
// will then dispatch the notification to all of its clients. // which will then dispatch the notification to all of its clients.
func (b *BitcoindNotifier) dispatchSpendDetailsManually( func (b *BitcoindNotifier) dispatchSpendDetailsManually(
historicalDispatchDetails *chainntnfs.HistoricalSpendDispatch) error { historicalDispatchDetails *chainntnfs.HistoricalSpendDispatch) error {
op := historicalDispatchDetails.OutPoint spendRequest := historicalDispatchDetails.SpendRequest
startHeight := historicalDispatchDetails.StartHeight startHeight := historicalDispatchDetails.StartHeight
endHeight := historicalDispatchDetails.EndHeight endHeight := historicalDispatchDetails.EndHeight
@ -784,31 +826,31 @@ func (b *BitcoindNotifier) dispatchSpendDetailsManually(
"%v: %v", blockHash, err) "%v: %v", blockHash, err)
} }
// Then, we'll manually go over every transaction in it and // Then, we'll manually go over every input in every transaction
// determine whether it spends the outpoint in question. // in it and determine whether it spends the request in
// question. If we find one, we'll dispatch the spend details.
for _, tx := range block.Transactions { for _, tx := range block.Transactions {
for i, txIn := range tx.TxIn { matches, inputIdx, err := spendRequest.MatchesTx(tx)
if txIn.PreviousOutPoint != op { if err != nil {
continue return err
}
// If it does, we'll construct its spend details
// and hand them over to the TxNotifier so that
// it can properly notify its registered
// clients.
txHash := tx.TxHash()
details := &chainntnfs.SpendDetail{
SpentOutPoint: &op,
SpenderTxHash: &txHash,
SpendingTx: tx,
SpenderInputIndex: uint32(i),
SpendingHeight: int32(height),
}
return b.txNotifier.UpdateSpendDetails(
op, details,
)
} }
if !matches {
continue
}
txHash := tx.TxHash()
details := &chainntnfs.SpendDetail{
SpentOutPoint: &tx.TxIn[inputIdx].PreviousOutPoint,
SpenderTxHash: &txHash,
SpendingTx: tx,
SpenderInputIndex: inputIdx,
SpendingHeight: int32(height),
}
return b.txNotifier.UpdateSpendDetails(
historicalDispatchDetails.SpendRequest,
details,
)
} }
} }

@ -310,6 +310,42 @@ func (r SpendRequest) SpendHintKey() ([]byte, error) {
return outpoint.Bytes(), nil return outpoint.Bytes(), nil
} }
// MatchesTx determines whether the given transaction satisfies the spend
// request. If the spend request is for an outpoint, then we'll check all of
// the outputs being spent by the inputs of the transaction to determine if it
// matches. Otherwise, we'll need to match on the output script being spent, so
// we'll recompute it for each input of the transaction to determine if it
// matches.
func (r SpendRequest) MatchesTx(tx *wire.MsgTx) (bool, uint32, error) {
if r.OutPoint != ZeroOutPoint {
for i, txIn := range tx.TxIn {
if txIn.PreviousOutPoint == r.OutPoint {
return true, uint32(i), nil
}
}
return false, 0, nil
}
for i, txIn := range tx.TxIn {
pkScript, err := txscript.ComputePkScript(
txIn.SignatureScript, txIn.Witness,
)
if err == txscript.ErrUnsupportedScriptType {
continue
}
if err != nil {
return false, 0, err
}
if bytes.Equal(pkScript.Script(), r.PkScript.Script()) {
return true, uint32(i), nil
}
}
return false, 0, nil
}
// SpendNtfn represents a client's request to receive a notification once an // SpendNtfn represents a client's request to receive a notification once an
// outpoint/output script has been spent on-chain. The client is asynchronously // outpoint/output script has been spent on-chain. The client is asynchronously
// notified via the SpendEvent channels. // notified via the SpendEvent channels.