chainntfs: add checks for previously confirmed transactions and spends
Without these checks, “zombie” notification requests that would never be dispatched could be registered. This would occur if notification requests were made for events (transaction confirmation and output spent) that had already been recorded on the blockchain.
This commit is contained in:
parent
e39dc9eec1
commit
909c3f8df9
@ -2,6 +2,7 @@ package btcdnotify
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"container/heap"
|
"container/heap"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@ -436,6 +437,7 @@ type spendNotification struct {
|
|||||||
// outpoint has been detected, the details of the spending event will be sent
|
// outpoint has been detected, the details of the spending event will be sent
|
||||||
// across the 'Spend' channel.
|
// across the 'Spend' channel.
|
||||||
func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint) (*chainntnfs.SpendEvent, error) {
|
func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint) (*chainntnfs.SpendEvent, error) {
|
||||||
|
|
||||||
if err := b.chainConn.NotifySpent([]*wire.OutPoint{outpoint}); err != nil {
|
if err := b.chainConn.NotifySpent([]*wire.OutPoint{outpoint}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -447,6 +449,33 @@ func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint) (*chainntnfs.S
|
|||||||
|
|
||||||
b.notificationRegistry <- ntfn
|
b.notificationRegistry <- ntfn
|
||||||
|
|
||||||
|
// The following conditional checks to ensure that when a spend notification
|
||||||
|
// is registered, the output hasn't already been spent. If the output
|
||||||
|
// is no longer in the UTXO set, the chain will be rescanned from the point
|
||||||
|
// where the output was added. The rescan will dispatch the notification.
|
||||||
|
txout, err := b.chainConn.GetTxOut(&outpoint.Hash, outpoint.Index, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if txout == nil {
|
||||||
|
transaction, err := b.chainConn.GetRawTransactionVerbose(&outpoint.Hash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
blockhash, err := wire.NewShaHashFromStr(transaction.BlockHash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ops := []*wire.OutPoint{outpoint}
|
||||||
|
if err := b.chainConn.Rescan(blockhash, nil, ops); err != nil {
|
||||||
|
chainntnfs.Log.Errorf("Rescan for spend notification txout failed: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &chainntnfs.SpendEvent{ntfn.spendChan}, nil
|
return &chainntnfs.SpendEvent{ntfn.spendChan}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -477,6 +506,19 @@ func (b *BtcdNotifier) RegisterConfirmationsNtfn(txid *wire.ShaHash,
|
|||||||
|
|
||||||
b.notificationRegistry <- ntfn
|
b.notificationRegistry <- ntfn
|
||||||
|
|
||||||
|
// The following conditional checks transaction confirmation notification
|
||||||
|
// requests so that if the transaction has already been included in a block
|
||||||
|
// with the requested number of confirmations, the notification will be
|
||||||
|
// dispatched immediately.
|
||||||
|
tx, err := b.chainConn.GetRawTransactionVerbose(txid)
|
||||||
|
if err != nil {
|
||||||
|
if !strings.Contains(err.Error(), "No information") {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if uint32(tx.Confirmations) > numConfs {
|
||||||
|
ntfn.finConf <- int32(tx.Confirmations)
|
||||||
|
}
|
||||||
|
|
||||||
return &chainntnfs.ConfirmationEvent{
|
return &chainntnfs.ConfirmationEvent{
|
||||||
Confirmed: ntfn.finConf,
|
Confirmed: ntfn.finConf,
|
||||||
NegativeConf: ntfn.negativeConf,
|
NegativeConf: ntfn.negativeConf,
|
||||||
|
@ -412,6 +412,161 @@ func testMultiClientConfirmationNotification(miner *rpctest.Harness,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests the case in which a confirmation notification is requested for a
|
||||||
|
// transaction that has already been included in a block. In this case,
|
||||||
|
// the confirmation notification should be dispatched immediately.
|
||||||
|
func testTxConfirmedBeforeNtfnRegistration(miner *rpctest.Harness,
|
||||||
|
notifier chainntnfs.ChainNotifier, t *testing.T) {
|
||||||
|
|
||||||
|
t.Logf("testing transaction confirmed before notification registration")
|
||||||
|
|
||||||
|
// First, let's send some coins to "ourself", obtainig a txid.
|
||||||
|
// We're spending from a coinbase output here, so we use the dedicated
|
||||||
|
// function.
|
||||||
|
|
||||||
|
txid, err := getTestTxId(miner)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create test tx: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now generate one block. The notifier must check older blocks when the
|
||||||
|
// confirmation event is registered below to ensure that the TXID hasn't
|
||||||
|
// already been included in the chain, otherwise the notification will
|
||||||
|
// never be sent.
|
||||||
|
if _, err := miner.Node.Generate(1); err != nil {
|
||||||
|
t.Fatalf("unable to generate two blocks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we have a txid, register a confirmation notification with
|
||||||
|
// the chainntfn source.
|
||||||
|
numConfs := uint32(1)
|
||||||
|
confIntent, err := notifier.RegisterConfirmationsNtfn(txid, numConfs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to register ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
confSent := make(chan int32)
|
||||||
|
go func() {
|
||||||
|
confSent <- <-confIntent.Confirmed
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-confSent:
|
||||||
|
break
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatalf("confirmation notification never received")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests the case in which a spend notification is requested for a spend that
|
||||||
|
// has already been included in a block. In this case, the spend notification
|
||||||
|
// should be dispatched immediately.
|
||||||
|
func testSpendBeforeNtfnRegistration(miner *rpctest.Harness,
|
||||||
|
notifier chainntnfs.ChainNotifier, t *testing.T) {
|
||||||
|
|
||||||
|
t.Logf("testing spend broadcast before notification registration")
|
||||||
|
|
||||||
|
// We'd like to test the spend notifications for all
|
||||||
|
// ChainNotifier concrete implemenations.
|
||||||
|
//
|
||||||
|
// To do so, we first create a new output to our test target
|
||||||
|
// address.
|
||||||
|
txid, err := getTestTxId(miner)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create test addr: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mine a single block which should include that txid above.
|
||||||
|
if _, err := miner.Node.Generate(1); err != nil {
|
||||||
|
t.Fatalf("unable to generate single block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we have the txid, fetch the transaction itself.
|
||||||
|
wrappedTx, err := miner.Node.GetRawTransaction(txid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get new tx: %v", err)
|
||||||
|
}
|
||||||
|
tx := wrappedTx.MsgTx()
|
||||||
|
|
||||||
|
// Locate the output index sent to us. We need this so we can
|
||||||
|
// construct a spending txn below.
|
||||||
|
outIndex := -1
|
||||||
|
var pkScript []byte
|
||||||
|
for i, txOut := range tx.TxOut {
|
||||||
|
if bytes.Contains(txOut.PkScript, testAddr.ScriptAddress()) {
|
||||||
|
pkScript = txOut.PkScript
|
||||||
|
outIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if outIndex == -1 {
|
||||||
|
t.Fatalf("unable to locate new output")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we've found the output index, register for a spentness
|
||||||
|
// notification for the newly created output.
|
||||||
|
outpoint := wire.NewOutPoint(txid, uint32(outIndex))
|
||||||
|
|
||||||
|
// Next, create a new transaction spending that output.
|
||||||
|
spendingTx := wire.NewMsgTx()
|
||||||
|
spendingTx.AddTxIn(&wire.TxIn{
|
||||||
|
PreviousOutPoint: *outpoint,
|
||||||
|
})
|
||||||
|
spendingTx.AddTxOut(&wire.TxOut{
|
||||||
|
Value: 1e8,
|
||||||
|
PkScript: pkScript,
|
||||||
|
})
|
||||||
|
sigScript, err := txscript.SignatureScript(spendingTx, 0, pkScript,
|
||||||
|
txscript.SigHashAll, privKey, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to sign tx: %v", err)
|
||||||
|
}
|
||||||
|
spendingTx.TxIn[0].SignatureScript = sigScript
|
||||||
|
|
||||||
|
// Broadcast our spending transaction.
|
||||||
|
spenderSha, err := miner.Node.SendRawTransaction(spendingTx, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to brodacst tx: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we mine an additional block, which should include our spend.
|
||||||
|
if _, err := miner.Node.Generate(1); err != nil {
|
||||||
|
t.Fatalf("unable to generate single block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, we register to be notified of a spend that has already happened.
|
||||||
|
// The notifier should dispatch a spend notification immediately.
|
||||||
|
spentIntent, err := notifier.RegisterSpendNtfn(outpoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to register for spend ntfn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spentNtfn := make(chan *chainntnfs.SpendDetail)
|
||||||
|
go func() {
|
||||||
|
spentNtfn <- <-spentIntent.Spend
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case ntfn := <-spentNtfn:
|
||||||
|
// We've received the spend nftn. So now verify all the fields
|
||||||
|
// have been set properly.
|
||||||
|
if ntfn.SpentOutPoint != outpoint {
|
||||||
|
t.Fatalf("ntfn includes wrong output, reports %v instead of %v",
|
||||||
|
ntfn.SpentOutPoint, outpoint)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(ntfn.SpenderTxHash.Bytes(), spenderSha.Bytes()) {
|
||||||
|
t.Fatalf("ntfn includes wrong spender tx sha, reports %v intead of %v",
|
||||||
|
ntfn.SpenderTxHash.Bytes(), spenderSha.Bytes())
|
||||||
|
}
|
||||||
|
if ntfn.SpenderInputIndex != 0 {
|
||||||
|
t.Fatalf("ntfn includes wrong spending input index, reports %v, should be %v",
|
||||||
|
ntfn.SpenderInputIndex, 0)
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatalf("spend ntfn never received")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var ntfnTests = []func(node *rpctest.Harness, notifier chainntnfs.ChainNotifier, t *testing.T){
|
var ntfnTests = []func(node *rpctest.Harness, notifier chainntnfs.ChainNotifier, t *testing.T){
|
||||||
testSingleConfirmationNotification,
|
testSingleConfirmationNotification,
|
||||||
testMultiConfirmationNotification,
|
testMultiConfirmationNotification,
|
||||||
@ -419,6 +574,8 @@ var ntfnTests = []func(node *rpctest.Harness, notifier chainntnfs.ChainNotifier,
|
|||||||
testMultiClientConfirmationNotification,
|
testMultiClientConfirmationNotification,
|
||||||
testSpendNotification,
|
testSpendNotification,
|
||||||
testBlockEpochNotification,
|
testBlockEpochNotification,
|
||||||
|
testTxConfirmedBeforeNtfnRegistration,
|
||||||
|
testSpendBeforeNtfnRegistration,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestInterfaces tests all registered interfaces with a unified set of tests
|
// TestInterfaces tests all registered interfaces with a unified set of tests
|
||||||
|
Loading…
Reference in New Issue
Block a user