lnd.xprv/chainntnfs/txnotifier_test.go
Wilmer Paulino 17b6205f3a
chainntnfs: validate conf/spend ntfn registration parameters
A height hint not being set would cause lnd to scan for the
confirmation/spend of a txid/outpoint/address from genesis.

The number of confirmations not being set within a confirmation request
would cause the internal TxNotifier to deadlock when dispatching
updates.
2019-08-22 13:49:37 -07:00

2237 lines
67 KiB
Go

package chainntnfs_test
import (
"bytes"
"sync"
"testing"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/chainntnfs"
)
var (
testRawScript = []byte{
// OP_HASH160
0xa9,
// OP_DATA_20
0x14,
// <20-byte script hash>
0x90, 0x1c, 0x86, 0x94, 0xc0, 0x3f, 0xaf, 0xd5,
0x52, 0x28, 0x10, 0xe0, 0x33, 0x0f, 0x26, 0xe6,
0x7a, 0x85, 0x33, 0xcd,
// OP_EQUAL
0x87,
}
testSigScript = []byte{
// OP_DATA_16
0x16,
// <22-byte redeem script>
0x00, 0x14, 0x1d, 0x7c, 0xd6, 0xc7, 0x5c, 0x2e,
0x86, 0xf4, 0xcb, 0xf9, 0x8e, 0xae, 0xd2, 0x21,
0xb3, 0x0b, 0xd9, 0xa0, 0xb9, 0x28,
}
testScript, _ = txscript.ParsePkScript(testRawScript)
)
type mockHintCache struct {
mu sync.Mutex
confHints map[chainntnfs.ConfRequest]uint32
spendHints map[chainntnfs.SpendRequest]uint32
}
var _ chainntnfs.SpendHintCache = (*mockHintCache)(nil)
var _ chainntnfs.ConfirmHintCache = (*mockHintCache)(nil)
func (c *mockHintCache) CommitSpendHint(heightHint uint32,
spendRequests ...chainntnfs.SpendRequest) error {
c.mu.Lock()
defer c.mu.Unlock()
for _, spendRequest := range spendRequests {
c.spendHints[spendRequest] = heightHint
}
return nil
}
func (c *mockHintCache) QuerySpendHint(spendRequest chainntnfs.SpendRequest) (uint32, error) {
c.mu.Lock()
defer c.mu.Unlock()
hint, ok := c.spendHints[spendRequest]
if !ok {
return 0, chainntnfs.ErrSpendHintNotFound
}
return hint, nil
}
func (c *mockHintCache) PurgeSpendHint(spendRequests ...chainntnfs.SpendRequest) error {
c.mu.Lock()
defer c.mu.Unlock()
for _, spendRequest := range spendRequests {
delete(c.spendHints, spendRequest)
}
return nil
}
func (c *mockHintCache) CommitConfirmHint(heightHint uint32,
confRequests ...chainntnfs.ConfRequest) error {
c.mu.Lock()
defer c.mu.Unlock()
for _, confRequest := range confRequests {
c.confHints[confRequest] = heightHint
}
return nil
}
func (c *mockHintCache) QueryConfirmHint(confRequest chainntnfs.ConfRequest) (uint32, error) {
c.mu.Lock()
defer c.mu.Unlock()
hint, ok := c.confHints[confRequest]
if !ok {
return 0, chainntnfs.ErrConfirmHintNotFound
}
return hint, nil
}
func (c *mockHintCache) PurgeConfirmHint(confRequests ...chainntnfs.ConfRequest) error {
c.mu.Lock()
defer c.mu.Unlock()
for _, confRequest := range confRequests {
delete(c.confHints, confRequest)
}
return nil
}
func newMockHintCache() *mockHintCache {
return &mockHintCache{
confHints: make(map[chainntnfs.ConfRequest]uint32),
spendHints: make(map[chainntnfs.SpendRequest]uint32),
}
}
// TestTxNotifierRegistrationValidation ensures that we are not able to register
// requests with invalid parameters.
func TestTxNotifierRegistrationValidation(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
pkScript []byte
numConfs uint32
heightHint uint32
checkSpend bool
err error
}{
{
name: "empty output script",
pkScript: nil,
numConfs: 1,
heightHint: 1,
checkSpend: true,
err: chainntnfs.ErrNoScript,
},
{
name: "zero num confs",
pkScript: testRawScript,
numConfs: 0,
heightHint: 1,
err: chainntnfs.ErrNumConfsOutOfRange,
},
{
name: "exceed max num confs",
pkScript: testRawScript,
numConfs: chainntnfs.MaxNumConfs + 1,
heightHint: 1,
err: chainntnfs.ErrNumConfsOutOfRange,
},
{
name: "empty height hint",
pkScript: testRawScript,
numConfs: 1,
heightHint: 0,
checkSpend: true,
err: chainntnfs.ErrNoHeightHint,
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
10, chainntnfs.ReorgSafetyLimit, hintCache, hintCache,
)
_, err := n.RegisterConf(
&chainntnfs.ZeroHash, testCase.pkScript,
testCase.numConfs, testCase.heightHint,
)
if err != testCase.err {
t.Fatalf("conf registration expected error "+
"\"%v\", got \"%v\"", testCase.err, err)
}
if !testCase.checkSpend {
return
}
_, err = n.RegisterSpend(
&chainntnfs.ZeroOutPoint, testCase.pkScript,
testCase.heightHint,
)
if err != testCase.err {
t.Fatalf("spend registration expected error "+
"\"%v\", got \"%v\"", testCase.err, err)
}
})
}
}
// 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
)
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
10, chainntnfs.ReorgSafetyLimit, hintCache, hintCache,
)
// Create the test transactions and register them with the TxNotifier
// before including them in a block to receive future
// notifications.
tx1 := wire.MsgTx{Version: 1}
tx1.AddTxOut(&wire.TxOut{PkScript: testRawScript})
tx1Hash := tx1.TxHash()
ntfn1, err := n.RegisterConf(&tx1Hash, testRawScript, tx1NumConfs, 1)
if err != nil {
t.Fatalf("unable to register ntfn: %v", err)
}
tx2 := wire.MsgTx{Version: 2}
tx2.AddTxOut(&wire.TxOut{PkScript: testRawScript})
tx2Hash := tx2.TxHash()
ntfn2, err := n.RegisterConf(&tx2Hash, testRawScript, tx2NumConfs, 1)
if 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},
})
err = n.ConnectTip(block1.Hash(), 11, block1.Transactions())
if err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(11); err != nil {
t.Fatalf("unable to dispatch notifications: %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,
Tx: &tx1,
}
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{})
err = n.ConnectTip(block2.Hash(), 12, block2.Transactions())
if err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(12); err != nil {
t.Fatalf("unable to dispatch notifications: %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,
Tx: &tx2,
}
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, chainntnfs.ReorgSafetyLimit, 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, err := n.RegisterConf(&tx1Hash, testRawScript, tx1NumConfs, 1)
if err != nil {
t.Fatalf("unable to register ntfn: %v", err)
}
tx2Hash := tx2.TxHash()
ntfn2, err := n.RegisterConf(&tx2Hash, testRawScript, tx2NumConfs, 1)
if 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: &chainntnfs.ZeroHash,
BlockHeight: 9,
TxIndex: 1,
Tx: &tx1,
}
err = n.UpdateConfDetails(ntfn1.HistoricalDispatch.ConfRequest, &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: &chainntnfs.ZeroHash,
BlockHeight: 9,
TxIndex: 2,
Tx: &tx2,
}
err = n.UpdateConfDetails(ntfn2.HistoricalDispatch.ConfRequest, &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)
}
if err := n.NotifyHeight(11); err != nil {
t.Fatalf("unable to dispatch notifications: %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, chainntnfs.ReorgSafetyLimit, hintCache, hintCache,
)
// We'll start off by registering for a spend notification of an
// outpoint.
op := wire.OutPoint{Index: 1}
ntfn, err := n.RegisterSpend(&op, testRawScript, 1)
if 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: op,
SignatureScript: testSigScript,
})
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)
}
if err := n.NotifyHeight(11); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
expectedSpendDetails := &chainntnfs.SpendDetail{
SpentOutPoint: &op,
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,
SignatureScript: testSigScript,
})
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)
}
if err := n.NotifyHeight(12); err != nil {
t.Fatalf("unable to dispatch notifications: %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, chainntnfs.ReorgSafetyLimit, hintCache,
hintCache,
)
// We'll start by constructing the spending details of the outpoint
// below.
spentOutpoint := wire.OutPoint{Index: 1}
spendTx := wire.NewMsgTx(2)
spendTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: spentOutpoint,
SignatureScript: testSigScript,
})
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, err := n.RegisterSpend(&spentOutpoint, testRawScript, 1)
if 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.HistoricalDispatch.SpendRequest, 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,
SignatureScript: testSigScript,
})
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)
}
if err := n.NotifyHeight(startingHeight + 1); err != nil {
t.Fatalf("unable to dispatch notifications: %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, chainntnfs.ReorgSafetyLimit, 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.
ntfn1, err := n.RegisterConf(&chainntnfs.ZeroHash, testRawScript, 1, 1)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
if ntfn1.HistoricalDispatch == 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.
ntfn2, err := n.RegisterConf(&chainntnfs.ZeroHash, testRawScript, 1, 1)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
if ntfn2.HistoricalDispatch != 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,
}
err = n.UpdateConfDetails(ntfn1.HistoricalDispatch.ConfRequest, confDetails)
if err != nil {
t.Fatalf("unable to update conf details: %v", err)
}
ntfn3, err := n.RegisterConf(&chainntnfs.ZeroHash, testRawScript, 1, 1)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
if ntfn3.HistoricalDispatch != 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, chainntnfs.ReorgSafetyLimit, 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.
op := wire.OutPoint{Index: 1}
ntfn1, err := n.RegisterSpend(&op, testRawScript, 1)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
if ntfn1.HistoricalDispatch == 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, err := n.RegisterSpend(&op, testRawScript, 1)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
if ntfn2.HistoricalDispatch != 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: &op,
SpenderTxHash: &chainntnfs.ZeroHash,
SpendingTx: wire.NewMsgTx(2),
SpenderInputIndex: 0,
SpendingHeight: startingHeight - 1,
}
err = n.UpdateSpendDetails(
ntfn1.HistoricalDispatch.SpendRequest, spendDetails,
)
if err != nil {
t.Fatalf("unable to update spend details: %v", err)
}
ntfn3, err := n.RegisterSpend(&op, testRawScript, 1)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
if ntfn3.HistoricalDispatch != 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, chainntnfs.ReorgSafetyLimit, hintCache,
hintCache,
)
var txid chainhash.Hash
copy(txid[:], bytes.Repeat([]byte{0x01}, 32))
// We'll start off by registered 5 clients for a confirmation
// notification on the same transaction.
confNtfns := make([]*chainntnfs.ConfRegistration, numNtfns)
for i := uint64(0); i < numNtfns; i++ {
ntfn, err := n.RegisterConf(&txid, testRawScript, 1, 1)
if err != nil {
t.Fatalf("unable to register conf ntfn #%d: %v", i, err)
}
confNtfns[i] = ntfn
}
// 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,
Tx: wire.NewMsgTx(1),
}
err := n.UpdateConfDetails(
confNtfns[0].HistoricalDispatch.ConfRequest, 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, err := n.RegisterConf(&txid, testRawScript, 1, 1)
if err != nil {
t.Fatalf("unable to register conf ntfn: %v", err)
}
if extraConfNtfn.HistoricalDispatch != 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.
op := wire.OutPoint{Index: 1}
spendNtfns := make([]*chainntnfs.SpendRegistration, numNtfns)
for i := uint64(0); i < numNtfns; i++ {
ntfn, err := n.RegisterSpend(&op, testRawScript, 1)
if err != nil {
t.Fatalf("unable to register spend ntfn #%d: %v", i, err)
}
spendNtfns[i] = ntfn
}
// 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: &op,
SpenderTxHash: &chainntnfs.ZeroHash,
SpendingTx: wire.NewMsgTx(2),
SpenderInputIndex: 0,
SpendingHeight: startingHeight - 1,
}
err = n.UpdateSpendDetails(
spendNtfns[0].HistoricalDispatch.SpendRequest, 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, err := n.RegisterSpend(&op, testRawScript, 1)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
if extraSpendNtfn.HistoricalDispatch != 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")
}
}
// TestTxNotifierCancelConf ensures that a confirmation notification after a
// client has canceled their intent to receive one.
func TestTxNotifierCancelConf(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.
tx1 := wire.NewMsgTx(1)
tx1.AddTxOut(&wire.TxOut{PkScript: testRawScript})
tx1Hash := tx1.TxHash()
ntfn1, err := n.RegisterConf(&tx1Hash, testRawScript, 1, 1)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
tx2 := wire.NewMsgTx(2)
tx2.AddTxOut(&wire.TxOut{PkScript: testRawScript})
tx2Hash := tx2.TxHash()
ntfn2, err := n.RegisterConf(&tx2Hash, testRawScript, 1, 1)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
// Construct a block that will confirm both transactions.
block := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{tx1, tx2},
})
tx1ConfDetails := &chainntnfs.TxConfirmation{
BlockHeight: startingHeight + 1,
BlockHash: block.Hash(),
TxIndex: 0,
Tx: tx1,
}
// Before extending the notifier's tip with the block above, we'll
// cancel the second request.
n.CancelConf(ntfn2.HistoricalDispatch.ConfRequest, 2)
err = n.ConnectTip(block.Hash(), startingHeight+1, block.Transactions())
if err != nil {
t.Fatalf("unable to connect block: %v", err)
}
if err := n.NotifyHeight(startingHeight + 1); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// The first request should still be active, so we should receive a
// confirmation notification with the correct details.
select {
case confDetails := <-ntfn1.Event.Confirmed:
assertConfDetails(t, confDetails, tx1ConfDetails)
default:
t.Fatalf("expected to receive confirmation notification")
}
// The second one, however, should not have. The event's Confirmed
// 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.Confirmed:
if ok {
t.Fatal("expected Confirmed channel to be closed")
}
default:
t.Fatal("expected Confirmed channel to be closed")
}
}
// 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, chainntnfs.ReorgSafetyLimit, hintCache,
hintCache,
)
// We'll register two notification requests. Only the second one will be
// canceled.
op1 := wire.OutPoint{Index: 1}
ntfn1, err := n.RegisterSpend(&op1, testRawScript, 1)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
op2 := wire.OutPoint{Index: 2}
ntfn2, err := n.RegisterSpend(&op2, testRawScript, 1)
if 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: op1,
SignatureScript: testSigScript,
})
spendTxHash := spendTx.TxHash()
expectedSpendDetails := &chainntnfs.SpendDetail{
SpentOutPoint: &op1,
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.HistoricalDispatch.SpendRequest, 2)
err = n.ConnectTip(block.Hash(), startingHeight+1, block.Transactions())
if err != nil {
t.Fatalf("unable to connect block: %v", err)
}
if err := n.NotifyHeight(startingHeight + 1); err != nil {
t.Fatalf("unable to dispatch notifications: %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
)
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
7, chainntnfs.ReorgSafetyLimit, hintCache, hintCache,
)
// Tx 1 will be confirmed in block 9 and requires 2 confs.
tx1 := wire.MsgTx{Version: 1}
tx1.AddTxOut(&wire.TxOut{PkScript: testRawScript})
tx1Hash := tx1.TxHash()
ntfn1, err := n.RegisterConf(&tx1Hash, testRawScript, tx1NumConfs, 1)
if err != nil {
t.Fatalf("unable to register ntfn: %v", err)
}
err = n.UpdateConfDetails(ntfn1.HistoricalDispatch.ConfRequest, nil)
if err != nil {
t.Fatalf("unable to deliver conf details: %v", err)
}
// Tx 2 will be confirmed in block 10 and requires 1 conf.
tx2 := wire.MsgTx{Version: 2}
tx2.AddTxOut(&wire.TxOut{PkScript: testRawScript})
tx2Hash := tx2.TxHash()
ntfn2, err := n.RegisterConf(&tx2Hash, testRawScript, tx2NumConfs, 1)
if err != nil {
t.Fatalf("unable to register ntfn: %v", err)
}
err = n.UpdateConfDetails(ntfn2.HistoricalDispatch.ConfRequest, nil)
if err != nil {
t.Fatalf("unable to deliver conf details: %v", err)
}
// Tx 3 will be confirmed in block 10 and requires 2 confs.
tx3 := wire.MsgTx{Version: 3}
tx3.AddTxOut(&wire.TxOut{PkScript: testRawScript})
tx3Hash := tx3.TxHash()
ntfn3, err := n.RegisterConf(&tx3Hash, testRawScript, tx3NumConfs, 1)
if err != nil {
t.Fatalf("unable to register ntfn: %v", err)
}
err = n.UpdateConfDetails(ntfn3.HistoricalDispatch.ConfRequest, nil)
if 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},
})
if err := n.ConnectTip(nil, 8, block1.Transactions()); err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(8); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
if err := n.ConnectTip(nil, 9, nil); err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(9); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
block2 := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{&tx2, &tx3},
})
if err := n.ConnectTip(nil, 10, block2.Transactions()); err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(10); err != nil {
t.Fatalf("unable to dispatch notifications: %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.
if err := n.DisconnectTip(10); err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.ConnectTip(nil, 10, nil); err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(10); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
if err := n.ConnectTip(nil, 11, nil); err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(11); err != nil {
t.Fatalf("unable to dispatch notifications: %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)
}
if err := n.NotifyHeight(12); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
err = n.ConnectTip(block4.Hash(), 13, block4.Transactions())
if err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(13); err != nil {
t.Fatalf("unable to dispatch notifications: %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,
Tx: &tx2,
}
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,
Tx: &tx3,
}
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, chainntnfs.ReorgSafetyLimit, 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 := wire.OutPoint{Index: 1}
spendTx1 := wire.NewMsgTx(2)
spendTx1.AddTxIn(&wire.TxIn{
PreviousOutPoint: op1,
SignatureScript: testSigScript,
})
spendTxHash1 := spendTx1.TxHash()
expectedSpendDetails1 := &chainntnfs.SpendDetail{
SpentOutPoint: &op1,
SpenderTxHash: &spendTxHash1,
SpendingTx: spendTx1,
SpenderInputIndex: 0,
SpendingHeight: startingHeight + 1,
}
op2 := wire.OutPoint{Index: 2}
spendTx2 := wire.NewMsgTx(2)
spendTx2.AddTxIn(&wire.TxIn{
PreviousOutPoint: chainntnfs.ZeroOutPoint,
SignatureScript: testSigScript,
})
spendTx2.AddTxIn(&wire.TxIn{
PreviousOutPoint: op2,
SignatureScript: testSigScript,
})
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, err := n.RegisterSpend(&op1, testRawScript, 1)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
ntfn2, err := n.RegisterSpend(&op2, testRawScript, 1)
if 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)
}
if err := n.NotifyHeight(startingHeight + 1); err != nil {
t.Fatalf("unable to dispatch notifications: %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)
}
if err := n.NotifyHeight(startingHeight + 2); err != nil {
t.Fatalf("unable to dispatch notifications: %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)
}
if err := n.NotifyHeight(startingHeight + 2); err != nil {
t.Fatalf("unable to dispatch notifications: %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)
}
if err := n.NotifyHeight(startingHeight + 3); err != nil {
t.Fatalf("unable to dispatch notifications: %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, chainntnfs.ReorgSafetyLimit, hintCache,
hintCache,
)
// Create two test transactions and register them for notifications.
tx1 := wire.MsgTx{Version: 1}
tx1.AddTxOut(&wire.TxOut{PkScript: testRawScript})
tx1Hash := tx1.TxHash()
ntfn1, err := n.RegisterConf(&tx1Hash, testRawScript, 1, 1)
if err != nil {
t.Fatalf("unable to register tx1: %v", err)
}
tx2 := wire.MsgTx{Version: 2}
tx2.AddTxOut(&wire.TxOut{PkScript: testRawScript})
tx2Hash := tx2.TxHash()
ntfn2, err := n.RegisterConf(&tx2Hash, testRawScript, 2, 1)
if 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(ntfn1.HistoricalDispatch.ConfRequest)
if err != chainntnfs.ErrConfirmHintNotFound {
t.Fatalf("unexpected error when querying for height hint "+
"want: %v, got %v",
chainntnfs.ErrConfirmHintNotFound, err)
}
_, err = hintCache.QueryConfirmHint(ntfn2.HistoricalDispatch.ConfRequest)
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)
}
if err := n.NotifyHeight(txDummyHeight); err != nil {
t.Fatalf("unable to dispatch notifications: %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(ntfn1.HistoricalDispatch.ConfRequest)
if err != chainntnfs.ErrConfirmHintNotFound {
t.Fatalf("unexpected error when querying for height hint "+
"want: %v, got %v",
chainntnfs.ErrConfirmHintNotFound, err)
}
hint, err = hintCache.QueryConfirmHint(ntfn2.HistoricalDispatch.ConfRequest)
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.
err = n.UpdateConfDetails(ntfn1.HistoricalDispatch.ConfRequest, nil)
if err != nil {
t.Fatalf("unable to update conf details: %v", err)
}
err = n.UpdateConfDetails(ntfn2.HistoricalDispatch.ConfRequest, nil)
if 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)
}
if err := n.NotifyHeight(tx1Height); err != nil {
t.Fatalf("unable to dispatch notifications: %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(ntfn1.HistoricalDispatch.ConfRequest)
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(ntfn2.HistoricalDispatch.ConfRequest)
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)
}
if err := n.NotifyHeight(tx2Height); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// The height hint for the first transaction should remain the same.
hint, err = hintCache.QueryConfirmHint(ntfn1.HistoricalDispatch.ConfRequest)
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(ntfn2.HistoricalDispatch.ConfRequest)
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(ntfn2.HistoricalDispatch.ConfRequest)
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(ntfn2.HistoricalDispatch.ConfRequest)
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, chainntnfs.ReorgSafetyLimit, hintCache,
hintCache,
)
// Create two test outpoints and register them for spend notifications.
op1 := wire.OutPoint{Index: 1}
ntfn1, err := n.RegisterSpend(&op1, testRawScript, 1)
if err != nil {
t.Fatalf("unable to register spend for op1: %v", err)
}
op2 := wire.OutPoint{Index: 2}
ntfn2, err := n.RegisterSpend(&op2, testRawScript, 1)
if 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(ntfn1.HistoricalDispatch.SpendRequest)
if err != chainntnfs.ErrSpendHintNotFound {
t.Fatalf("unexpected error when querying for height hint "+
"expected: %v, got %v", chainntnfs.ErrSpendHintNotFound,
err)
}
_, err = hintCache.QuerySpendHint(ntfn2.HistoricalDispatch.SpendRequest)
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)
}
if err := n.NotifyHeight(dummyHeight); err != nil {
t.Fatalf("unable to dispatch notifications: %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(ntfn1.HistoricalDispatch.SpendRequest)
if err != chainntnfs.ErrSpendHintNotFound {
t.Fatalf("unexpected error when querying for height hint "+
"expected: %v, got %v", chainntnfs.ErrSpendHintNotFound,
err)
}
_, err = hintCache.QuerySpendHint(ntfn2.HistoricalDispatch.SpendRequest)
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.
err = n.UpdateSpendDetails(ntfn1.HistoricalDispatch.SpendRequest, nil)
if err != nil {
t.Fatalf("unable to update spend details: %v", err)
}
err = n.UpdateSpendDetails(ntfn2.HistoricalDispatch.SpendRequest, nil)
if 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: op1,
SignatureScript: testSigScript,
})
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)
}
if err := n.NotifyHeight(op1Height); err != nil {
t.Fatalf("unable to dispatch notifications: %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.HistoricalDispatch.SpendRequest)
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.HistoricalDispatch.SpendRequest)
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: op2,
SignatureScript: testSigScript,
})
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)
}
if err := n.NotifyHeight(op2Height); err != nil {
t.Fatalf("unable to dispatch notifications: %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.HistoricalDispatch.SpendRequest)
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.HistoricalDispatch.SpendRequest)
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.HistoricalDispatch.SpendRequest)
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.HistoricalDispatch.SpendRequest)
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)
}
}
// TestTxNotifierNtfnDone ensures that a notification is sent to registered
// clients through the Done channel once the notification request is no longer
// under the risk of being reorged out of the chain.
func TestTxNotifierNtfnDone(t *testing.T) {
t.Parallel()
hintCache := newMockHintCache()
const reorgSafetyLimit = 100
n := chainntnfs.NewTxNotifier(10, reorgSafetyLimit, hintCache, hintCache)
// We'll start by creating two notification requests: one confirmation
// and one spend.
confNtfn, err := n.RegisterConf(&chainntnfs.ZeroHash, testRawScript, 1, 1)
if err != nil {
t.Fatalf("unable to register conf ntfn: %v", err)
}
spendNtfn, err := n.RegisterSpend(&chainntnfs.ZeroOutPoint, testRawScript, 1)
if err != nil {
t.Fatalf("unable to register spend: %v", err)
}
// We'll create two transactions that will satisfy the notification
// requests above and include them in the next block of the chain.
tx := wire.NewMsgTx(1)
tx.AddTxOut(&wire.TxOut{PkScript: testRawScript})
spendTx := wire.NewMsgTx(1)
spendTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{Index: 1},
SignatureScript: testSigScript,
})
block := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{tx, spendTx},
})
err = n.ConnectTip(block.Hash(), 11, block.Transactions())
if err != nil {
t.Fatalf("unable to connect block: %v", err)
}
if err := n.NotifyHeight(11); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// With the chain extended, we should see notifications dispatched for
// both requests.
select {
case <-confNtfn.Event.Confirmed:
default:
t.Fatal("expected to receive confirmation notification")
}
select {
case <-spendNtfn.Event.Spend:
default:
t.Fatal("expected to receive spend notification")
}
// The done notifications should not be dispatched yet as the requests
// are still under the risk of being reorged out the chain.
select {
case <-confNtfn.Event.Done:
t.Fatal("received unexpected done notification for confirmation")
case <-spendNtfn.Event.Done:
t.Fatal("received unexpected done notification for spend")
default:
}
// Now, we'll disconnect the block at tip to simulate a reorg. The reorg
// notifications should be dispatched to the respective clients.
if err := n.DisconnectTip(11); err != nil {
t.Fatalf("unable to disconnect block: %v", err)
}
select {
case <-confNtfn.Event.NegativeConf:
default:
t.Fatal("expected to receive reorg notification for confirmation")
}
select {
case <-spendNtfn.Event.Reorg:
default:
t.Fatal("expected to receive reorg notification for spend")
}
// We'll reconnect the block that satisfies both of these requests.
// We should see notifications dispatched for both once again.
err = n.ConnectTip(block.Hash(), 11, block.Transactions())
if err != nil {
t.Fatalf("unable to connect block: %v", err)
}
if err := n.NotifyHeight(11); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
select {
case <-confNtfn.Event.Confirmed:
default:
t.Fatal("expected to receive confirmation notification")
}
select {
case <-spendNtfn.Event.Spend:
default:
t.Fatal("expected to receive spend notification")
}
// Finally, we'll extend the chain with blocks until the requests are no
// longer under the risk of being reorged out of the chain. We should
// expect the done notifications to be dispatched.
nextHeight := uint32(12)
for i := nextHeight; i < nextHeight+reorgSafetyLimit; i++ {
dummyBlock := btcutil.NewBlock(&wire.MsgBlock{})
if err := n.ConnectTip(dummyBlock.Hash(), i, nil); err != nil {
t.Fatalf("unable to connect block: %v", err)
}
}
select {
case <-confNtfn.Event.Done:
default:
t.Fatal("expected to receive done notification for confirmation")
}
select {
case <-spendNtfn.Event.Done:
default:
t.Fatal("expected to receive done notification for spend")
}
}
// 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, chainntnfs.ReorgSafetyLimit, hintCache, hintCache,
)
// To begin the test, we'll register for a confirmation and spend
// notification.
confNtfn, err := n.RegisterConf(&chainntnfs.ZeroHash, testRawScript, 1, 1)
if err != nil {
t.Fatalf("unable to register conf ntfn: %v", err)
}
spendNtfn, err := n.RegisterSpend(&chainntnfs.ZeroOutPoint, testRawScript, 1)
if 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.
_, err = n.RegisterConf(&chainntnfs.ZeroHash, testRawScript, 1, 1)
if err == nil {
t.Fatal("expected confirmation registration to fail")
}
_, err = n.RegisterSpend(&chainntnfs.ZeroOutPoint, testRawScript, 1)
if 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)
}
if result.Tx.TxHash() != expected.Tx.TxHash() {
t.Fatalf("expected tx hash %v, got %v", expected.Tx.TxHash(),
result.Tx.TxHash())
}
}
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)
}
}