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:
bryanvu 2016-11-30 00:00:20 -08:00 committed by Olaoluwa Osuntokun
parent e39dc9eec1
commit 909c3f8df9
2 changed files with 199 additions and 0 deletions

View File

@ -2,6 +2,7 @@ package btcdnotify
import (
"container/heap"
"strings"
"sync"
"sync/atomic"
"time"
@ -436,6 +437,7 @@ type spendNotification struct {
// outpoint has been detected, the details of the spending event will be sent
// across the 'Spend' channel.
func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint) (*chainntnfs.SpendEvent, error) {
if err := b.chainConn.NotifySpent([]*wire.OutPoint{outpoint}); err != nil {
return nil, err
}
@ -447,6 +449,33 @@ func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint) (*chainntnfs.S
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
}
@ -477,6 +506,19 @@ func (b *BtcdNotifier) RegisterConfirmationsNtfn(txid *wire.ShaHash,
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{
Confirmed: ntfn.finConf,
NegativeConf: ntfn.negativeConf,

View File

@ -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){
testSingleConfirmationNotification,
testMultiConfirmationNotification,
@ -419,6 +574,8 @@ var ntfnTests = []func(node *rpctest.Harness, notifier chainntnfs.ChainNotifier,
testMultiClientConfirmationNotification,
testSpendNotification,
testBlockEpochNotification,
testTxConfirmedBeforeNtfnRegistration,
testSpendBeforeNtfnRegistration,
}
// TestInterfaces tests all registered interfaces with a unified set of tests