package chainntnfs_test import ( "bytes" "sync" "testing" "time" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/stretchr/testify/require" ) 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, } ) 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) require.NoError(t, err, "unable to register ntfn") tx2 := wire.MsgTx{Version: 2} tx2.AddTxOut(&wire.TxOut{PkScript: testRawScript}) tx2Hash := tx2.TxHash() ntfn2, err := n.RegisterConf(&tx2Hash, testRawScript, tx2NumConfs, 1) require.NoError(t, err, "unable to register ntfn") // 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, 11) require.NoError(t, err, "Failed to connect block") 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 transaction 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, 12) require.NoError(t, err, "Failed to connect block") 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) require.NoError(t, err, "unable to register ntfn") tx2Hash := tx2.TxHash() ntfn2, err := n.RegisterConf(&tx2Hash, testRawScript, tx2NumConfs, 1) require.NoError(t, err, "unable to register ntfn") // 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) require.NoError(t, err, "unable to update conf details") 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) require.NoError(t, err, "unable to update conf details") 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, 11) require.NoError(t, err, "Failed to connect block") 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) require.NoError(t, err, "unable to register spend ntfn") // 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, 11) require.NoError(t, err, "unable to connect block") 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, 12) require.NoError(t, err, "unable to connect block") 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: } } // TestTxNotifierFutureConfDispatchReuseSafe tests that the notifier does not // misbehave even if two confirmation requests for the same script are issued // at different block heights (which means funds are being sent to the same // script multiple times). func TestTxNotifierFutureConfDispatchReuseSafe(t *testing.T) { t.Parallel() currentBlock := uint32(10) hintCache := newMockHintCache() n := chainntnfs.NewTxNotifier( currentBlock, 2, hintCache, hintCache, ) // We'll register a TX that sends to our test script and put it into a // block. Additionally we register a notification request for just the // script which should also be confirmed with that block. tx1 := wire.MsgTx{Version: 1} tx1.AddTxOut(&wire.TxOut{PkScript: testRawScript}) tx1Hash := tx1.TxHash() ntfn1, err := n.RegisterConf(&tx1Hash, testRawScript, 1, 1) require.NoError(t, err, "unable to register ntfn") scriptNtfn1, err := n.RegisterConf(nil, testRawScript, 1, 1) require.NoError(t, err, "unable to register ntfn") block := btcutil.NewBlock(&wire.MsgBlock{ Transactions: []*wire.MsgTx{&tx1}, }) currentBlock++ err = n.ConnectTip(block, currentBlock) require.NoError(t, err, "unable to connect block") if err := n.NotifyHeight(currentBlock); err != nil { t.Fatalf("unable to dispatch notifications: %v", err) } // Expect an update and confirmation of TX 1 at this point. We save the // confirmation details because we expect to receive the same details // for all further registrations. var confDetails *chainntnfs.TxConfirmation select { case <-ntfn1.Event.Updates: default: t.Fatal("expected update of TX 1") } select { case confDetails = <-ntfn1.Event.Confirmed: if confDetails.BlockHeight != currentBlock { t.Fatalf("expected TX to be confirmed in latest block") } default: t.Fatal("expected confirmation of TX 1") } // The notification for the script should also have received a // confirmation. select { case <-scriptNtfn1.Event.Updates: default: t.Fatal("expected update of script ntfn") } select { case details := <-scriptNtfn1.Event.Confirmed: assertConfDetails(t, details, confDetails) default: t.Fatal("expected update of script ntfn") } // Now register a second TX that spends to two outputs with the same // script so we have a different TXID. And again register a confirmation // for just the script. tx2 := wire.MsgTx{Version: 1} tx2.AddTxOut(&wire.TxOut{PkScript: testRawScript}) tx2.AddTxOut(&wire.TxOut{PkScript: testRawScript}) tx2Hash := tx2.TxHash() ntfn2, err := n.RegisterConf(&tx2Hash, testRawScript, 1, 1) require.NoError(t, err, "unable to register ntfn") scriptNtfn2, err := n.RegisterConf(nil, testRawScript, 1, 1) require.NoError(t, err, "unable to register ntfn") block2 := btcutil.NewBlock(&wire.MsgBlock{ Transactions: []*wire.MsgTx{&tx2}, }) currentBlock++ err = n.ConnectTip(block2, currentBlock) require.NoError(t, err, "unable to connect block") if err := n.NotifyHeight(currentBlock); err != nil { t.Fatalf("unable to dispatch notifications: %v", err) } // Transaction 2 should get a confirmation here too. Since it was // a different TXID we wouldn't get the cached details here but the TX // should be confirmed right away still. select { case <-ntfn2.Event.Updates: default: t.Fatal("expected update of TX 2") } select { case details := <-ntfn2.Event.Confirmed: if details.BlockHeight != currentBlock { t.Fatalf("expected TX to be confirmed in latest block") } default: t.Fatal("expected update of TX 2") } // The second notification for the script should also have received a // confirmation. Since it's the same script, we expect to get the cached // details from the first TX back immediately. Nothing should be // registered at the notifier for the current block height for that // script any more. select { case <-scriptNtfn2.Event.Updates: default: t.Fatal("expected update of script ntfn") } select { case details := <-scriptNtfn2.Event.Confirmed: assertConfDetails(t, details, confDetails) default: t.Fatal("expected update of script ntfn") } // Finally, mine a few empty blocks and expect both TXs to be confirmed. for currentBlock < 15 { block := btcutil.NewBlock(&wire.MsgBlock{}) currentBlock++ err = n.ConnectTip(block, currentBlock) if err != nil { t.Fatalf("unable to connect block: %v", err) } if err := n.NotifyHeight(currentBlock); err != nil { t.Fatalf("unable to dispatch notifications: %v", err) } } // Events for both confirmation requests should have been dispatched. select { case <-ntfn1.Event.Done: default: t.Fatal("expected notifications for TX 1 to be done") } select { case <-ntfn2.Event.Done: default: t.Fatal("expected notifications for TX 2 to be done") } } // 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) require.NoError(t, err, "unable to register spend ntfn") 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, ) require.NoError(t, err, "unable to update spend details") // 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, startingHeight+1) require.NoError(t, err, "unable to connect block") 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) require.NoError(t, err, "unable to register spend ntfn") 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) require.NoError(t, err, "unable to register spend ntfn") 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) require.NoError(t, err, "unable to update conf details") ntfn3, err := n.RegisterConf(&chainntnfs.ZeroHash, testRawScript, 1, 1) require.NoError(t, err, "unable to register spend ntfn") 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) require.NoError(t, err, "unable to register spend ntfn") 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) require.NoError(t, err, "unable to register spend ntfn") 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, ) require.NoError(t, err, "unable to update spend details") ntfn3, err := n.RegisterSpend(&op, testRawScript, 1) require.NoError(t, err, "unable to register spend ntfn") 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, ) require.NoError(t, err, "unable to update conf details") // 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) require.NoError(t, err, "unable to register conf ntfn") 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, ) require.NoError(t, err, "unable to update spend details") // 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) require.NoError(t, err, "unable to register spend ntfn") 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 four notification requests. The last three will be // canceled. tx1 := wire.NewMsgTx(1) tx1.AddTxOut(&wire.TxOut{PkScript: testRawScript}) tx1Hash := tx1.TxHash() ntfn1, err := n.RegisterConf(&tx1Hash, testRawScript, 1, 1) require.NoError(t, err, "unable to register spend ntfn") tx2 := wire.NewMsgTx(2) tx2.AddTxOut(&wire.TxOut{PkScript: testRawScript}) tx2Hash := tx2.TxHash() ntfn2, err := n.RegisterConf(&tx2Hash, testRawScript, 1, 1) require.NoError(t, err, "unable to register spend ntfn") ntfn3, err := n.RegisterConf(&tx2Hash, testRawScript, 1, 1) require.NoError(t, err, "unable to register spend ntfn") // This request will have a three block num confs. ntfn4, err := n.RegisterConf(&tx2Hash, testRawScript, 3, 1) require.NoError(t, err, "unable to register spend ntfn") // Extend the chain with a block that will confirm both transactions. // This will queue confirmation notifications to dispatch once their // respective heights have been met. block := btcutil.NewBlock(&wire.MsgBlock{ Transactions: []*wire.MsgTx{tx1, tx2}, }) tx1ConfDetails := &chainntnfs.TxConfirmation{ BlockHeight: startingHeight + 1, BlockHash: block.Hash(), TxIndex: 0, Tx: tx1, } // Cancel the second notification before connecting the block. ntfn2.Event.Cancel() err = n.ConnectTip(block, startingHeight+1) require.NoError(t, err, "unable to connect block") // Cancel the third notification before notifying to ensure its queued // confirmation notification gets removed as well. ntfn3.Event.Cancel() 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 and third, 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") } select { case _, ok := <-ntfn3.Event.Confirmed: if ok { t.Fatal("expected Confirmed channel to be closed") } default: t.Fatal("expected Confirmed channel to be closed") } // Connect yet another block. block1 := btcutil.NewBlock(&wire.MsgBlock{ Transactions: []*wire.MsgTx{}, }) err = n.ConnectTip(block1, startingHeight+2) require.NoError(t, err, "unable to connect block") if err := n.NotifyHeight(startingHeight + 2); err != nil { t.Fatalf("unable to dispatch notifications: %v", err) } // Since neither it reached the set confirmation height or was // canceled, nothing should happen to ntfn4 in this block. select { case <-ntfn4.Event.Confirmed: t.Fatal("expected nothing to happen") case <-time.After(10 * time.Millisecond): } // Now cancel the notification. ntfn4.Event.Cancel() select { case _, ok := <-ntfn4.Event.Confirmed: if ok { t.Fatal("expected Confirmed channel to be closed") } default: t.Fatal("expected Confirmed channel to be closed") } // Finally, confirm a block that would trigger ntfn4 confirmation // hadn't it already been canceled. block2 := btcutil.NewBlock(&wire.MsgBlock{ Transactions: []*wire.MsgTx{}, }) err = n.ConnectTip(block2, startingHeight+3) require.NoError(t, err, "unable to connect block") if err := n.NotifyHeight(startingHeight + 3); err != nil { t.Fatalf("unable to dispatch notifications: %v", err) } } // 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) require.NoError(t, err, "unable to register spend ntfn") op2 := wire.OutPoint{Index: 2} ntfn2, err := n.RegisterSpend(&op2, testRawScript, 1) require.NoError(t, err, "unable to register spend ntfn") // 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, startingHeight+1) require.NoError(t, err, "unable to connect block") 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) require.NoError(t, err, "unable to register ntfn") err = n.UpdateConfDetails(ntfn1.HistoricalDispatch.ConfRequest, nil) require.NoError(t, err, "unable to deliver conf details") // 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) require.NoError(t, err, "unable to register ntfn") err = n.UpdateConfDetails(ntfn2.HistoricalDispatch.ConfRequest, nil) require.NoError(t, err, "unable to deliver conf details") // 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) require.NoError(t, err, "unable to register ntfn") err = n.UpdateConfDetails(ntfn3.HistoricalDispatch.ConfRequest, nil) require.NoError(t, err, "unable to deliver conf details") // Sync chain to block 10. Txs 1 & 2 should be confirmed. block1 := btcutil.NewBlock(&wire.MsgBlock{ Transactions: []*wire.MsgTx{&tx1}, }) if err := n.ConnectTip(block1, 8); 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); 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(block2, 10); 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); 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); 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, 12) require.NoError(t, err, "Failed to connect block") if err := n.NotifyHeight(12); err != nil { t.Fatalf("unable to dispatch notifications: %v", err) } err = n.ConnectTip(block4, 13) require.NoError(t, err, "Failed to connect block") 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) require.NoError(t, err, "unable to register spend ntfn") ntfn2, err := n.RegisterSpend(&op2, testRawScript, 1) require.NoError(t, err, "unable to register spend ntfn") // 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, startingHeight+1) require.NoError(t, err, "unable to connect block") 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, startingHeight+2) require.NoError(t, err, "unable to connect block") 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, startingHeight+2) require.NoError(t, err, "unable to disconnect block") 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, startingHeight+3) require.NoError(t, err, "unable to connect block") 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: } } // TestTxNotifierUpdateSpendReorg tests that a call to RegisterSpend after the // spend has been confirmed, and then UpdateSpendDetails (called by historical // dispatch), followed by a chain re-org will notify on the Reorg channel. This // was not always the case and has since been fixed. func TestTxNotifierSpendReorgMissed(t *testing.T) { t.Parallel() const startingHeight = 10 hintCache := newMockHintCache() n := chainntnfs.NewTxNotifier( startingHeight, chainntnfs.ReorgSafetyLimit, hintCache, hintCache, ) // We'll create a spending transaction that spends the outpoint we'll // watch. op := wire.OutPoint{Index: 1} spendTx := wire.NewMsgTx(2) spendTx.AddTxIn(&wire.TxIn{ PreviousOutPoint: op, SignatureScript: testSigScript, }) spendTxHash := spendTx.TxHash() // Create the spend details that we'll call UpdateSpendDetails with. spendDetails := &chainntnfs.SpendDetail{ SpentOutPoint: &op, SpenderTxHash: &spendTxHash, SpendingTx: spendTx, SpenderInputIndex: 0, SpendingHeight: startingHeight + 1, } // Now confirm the spending transaction. block := btcutil.NewBlock(&wire.MsgBlock{ Transactions: []*wire.MsgTx{spendTx}, }) err := n.ConnectTip(block, startingHeight+1) require.NoError(t, err, "unable to connect block") if err := n.NotifyHeight(startingHeight + 1); err != nil { t.Fatalf("unable to dispatch notifications: %v", err) } // We register for the spend now and will not get a spend notification // until we call UpdateSpendDetails. ntfn, err := n.RegisterSpend(&op, testRawScript, 1) require.NoError(t, err, "unable to register spend") // Assert that the HistoricalDispatch variable is non-nil. We'll use // the SpendRequest member to update the spend details. require.NotEmpty(t, ntfn.HistoricalDispatch) select { case <-ntfn.Event.Spend: t.Fatalf("did not expect to receive spend ntfn") default: } // We now call UpdateSpendDetails with our generated spend details to // simulate a historical spend dispatch being performed. This should // result in a notification being received on the Spend channel. err = n.UpdateSpendDetails( ntfn.HistoricalDispatch.SpendRequest, spendDetails, ) require.Empty(t, err) // Assert that we receive a Spend notification. select { case <-ntfn.Event.Spend: default: t.Fatalf("expected to receive spend ntfn") } // We will now re-org the spending transaction out of the chain, and we // should receive a notification on the Reorg channel. err = n.DisconnectTip(startingHeight + 1) require.Empty(t, err) select { case <-ntfn.Event.Spend: t.Fatalf("received unexpected spend ntfn") case <-ntfn.Event.Reorg: default: t.Fatalf("expected spend reorg ntfn") } } // 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) require.NoError(t, err, "unable to register tx1") tx2 := wire.MsgTx{Version: 2} tx2.AddTxOut(&wire.TxOut{PkScript: testRawScript}) tx2Hash := tx2.TxHash() ntfn2, err := n.RegisterConf(&tx2Hash, testRawScript, 2, 1) require.NoError(t, err, "unable to register tx2") // 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, txDummyHeight) require.NoError(t, err, "Failed to connect block") 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) require.NoError(t, err, "unable to update conf details") err = n.UpdateConfDetails(ntfn2.HistoricalDispatch.ConfRequest, nil) require.NoError(t, err, "unable to update conf details") // 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, tx1Height) require.NoError(t, err, "Failed to connect block") 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) require.NoError(t, err, "unable to query for hint") if hint != tx1Height { t.Fatalf("expected hint %d, got %d", tx1Height, hint) } hint, err = hintCache.QueryConfirmHint(ntfn2.HistoricalDispatch.ConfRequest) require.NoError(t, err, "unable to query for hint") 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, tx2Height) require.NoError(t, err, "Failed to connect block") 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) require.NoError(t, err, "unable to query for hint") 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) require.NoError(t, err, "unable to query for hint") 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) require.NoError(t, err, "unable to query for hint") 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) require.NoError(t, err, "unable to query for hint") 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 ) // Initialize 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) require.NoError(t, err, "unable to register spend for op1") op2 := wire.OutPoint{Index: 2} ntfn2, err := n.RegisterSpend(&op2, testRawScript, 1) require.NoError(t, err, "unable to register spend for op2") // 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, dummyHeight) require.NoError(t, err, "unable to connect block") 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) require.NoError(t, err, "unable to update spend details") err = n.UpdateSpendDetails(ntfn2.HistoricalDispatch.SpendRequest, nil) require.NoError(t, err, "unable to update spend details") // 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, op1Height) require.NoError(t, err, "unable to connect block") 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) require.NoError(t, err, "unable to query for spend hint of op1") if op1Hint != op1Height { t.Fatalf("expected hint %d, got %d", op1Height, op1Hint) } op2Hint, err := hintCache.QuerySpendHint(ntfn2.HistoricalDispatch.SpendRequest) require.NoError(t, err, "unable to query for spend hint of op2") 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, op2Height) require.NoError(t, err, "unable to connect block") 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) require.NoError(t, err, "unable to query for spend hint of op1") if op1Hint != op1Height { t.Fatalf("expected hint %d, got %d", op1Height, op1Hint) } op2Hint, err = hintCache.QuerySpendHint(ntfn2.HistoricalDispatch.SpendRequest) require.NoError(t, err, "unable to query for spend hint of op2") 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) require.NoError(t, err, "unable to query for spend hint of op1") if op1Hint != op1Height { t.Fatalf("expected hint %d, got %d", op1Height, op1Hint) } op2Hint, err = hintCache.QuerySpendHint(ntfn2.HistoricalDispatch.SpendRequest) require.NoError(t, err, "unable to query for spend hint of op2") if op2Hint != op1Height { t.Fatalf("expected hint %d, got %d", op1Height, op2Hint) } } // TestTxNotifierSpendHinthistoricalRescan checks that the height hints and // spend notifications behave as expected when a spend is found at tip during a // historical rescan. func TestTxNotifierSpendDuringHistoricalRescan(t *testing.T) { t.Parallel() const ( startingHeight = 200 reorgSafety = 10 ) // Initialize our TxNotifier instance backed by a height hint cache. hintCache := newMockHintCache() n := chainntnfs.NewTxNotifier( startingHeight, reorgSafety, hintCache, hintCache, ) // Create a test outpoint and register it for spend notifications. op1 := wire.OutPoint{Index: 1} ntfn1, err := n.RegisterSpend(&op1, testRawScript, 1) require.NoError(t, err, "unable to register spend for op1") // A historical rescan should be initiated from the height hint to the // current height. if ntfn1.HistoricalDispatch.StartHeight != 1 { t.Fatalf("expected historical dispatch to start at height hint") } if ntfn1.HistoricalDispatch.EndHeight != startingHeight { t.Fatalf("expected historical dispatch to end at current height") } // It should not have a spend hint set upon registration, as we must // first determine whether it has 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) } // Create a new empty block and extend the chain. height := uint32(startingHeight) + 1 emptyBlock := btcutil.NewBlock(&wire.MsgBlock{}) err = n.ConnectTip(emptyBlock, height) require.NoError(t, err, "unable to connect block") if err := n.NotifyHeight(height); err != nil { t.Fatalf("unable to dispatch notifications: %v", err) } // Since we haven't called UpdateSpendDetails yet, there should be no // spend hint found. _, 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) } // Simulate a bunch of blocks being mined while the historical rescan // is still in progress. We make sure to not mine more than reorgSafety // blocks after the spend, since it will be forgotten then. var spendHeight uint32 for i := 0; i < reorgSafety; i++ { height++ // Let the outpoint we are watching be spent midway. var block *btcutil.Block if i == 5 { // We'll create a new block that only contains the // spending transaction of the outpoint. spendTx1 := wire.NewMsgTx(2) spendTx1.AddTxIn(&wire.TxIn{ PreviousOutPoint: op1, SignatureScript: testSigScript, }) block = btcutil.NewBlock(&wire.MsgBlock{ Transactions: []*wire.MsgTx{spendTx1}, }) spendHeight = height } else { // Otherwise we just create an empty block. block = btcutil.NewBlock(&wire.MsgBlock{}) } err = n.ConnectTip(block, height) if err != nil { t.Fatalf("unable to connect block: %v", err) } if err := n.NotifyHeight(height); err != nil { t.Fatalf("unable to dispatch notifications: %v", err) } } // Check that the height hint was set to the spending block. op1Hint, err := hintCache.QuerySpendHint( ntfn1.HistoricalDispatch.SpendRequest, ) require.NoError(t, err, "unable to query for spend hint of op1") if op1Hint != spendHeight { t.Fatalf("expected hint %d, got %d", spendHeight, op1Hint) } // We should be getting notified about the spend at this point. select { case <-ntfn1.Event.Spend: default: t.Fatal("expected to receive spend notification") } // Now, we'll simulate that the historical rescan finished by // calling UpdateSpendDetails. Since a the spend actually happened at // tip while the rescan was in progress, the height hint should not be // updated to the latest height, but stay at the spend height. err = n.UpdateSpendDetails(ntfn1.HistoricalDispatch.SpendRequest, nil) require.NoError(t, err, "unable to update spend details") op1Hint, err = hintCache.QuerySpendHint( ntfn1.HistoricalDispatch.SpendRequest, ) require.NoError(t, err, "unable to query for spend hint of op1") if op1Hint != spendHeight { t.Fatalf("expected hint %d, got %d", spendHeight, op1Hint) } // Then, we'll create another block that spends a second outpoint. op2 := wire.OutPoint{Index: 2} spendTx2 := wire.NewMsgTx(2) spendTx2.AddTxIn(&wire.TxIn{ PreviousOutPoint: op2, SignatureScript: testSigScript, }) height++ block2 := btcutil.NewBlock(&wire.MsgBlock{ Transactions: []*wire.MsgTx{spendTx2}, }) err = n.ConnectTip(block2, height) require.NoError(t, err, "unable to connect block") if err := n.NotifyHeight(height); err != nil { t.Fatalf("unable to dispatch notifications: %v", err) } // The outpoint's spend hint should remain the same as it's already // been spent before. op1Hint, err = hintCache.QuerySpendHint(ntfn1.HistoricalDispatch.SpendRequest) require.NoError(t, err, "unable to query for spend hint of op1") if op1Hint != spendHeight { t.Fatalf("expected hint %d, got %d", spendHeight, op1Hint) } // Now mine enough blocks for the spend notification to be forgotten. for i := 0; i < 2*reorgSafety; i++ { height++ block := btcutil.NewBlock(&wire.MsgBlock{}) err := n.ConnectTip(block, height) if err != nil { t.Fatalf("unable to connect block: %v", err) } if err := n.NotifyHeight(height); err != nil { t.Fatalf("unable to dispatch notifications: %v", err) } } // Attempting to update spend details at this point should fail, since // the spend request should be removed. This is to ensure the height // hint won't be overwritten if the historical rescan finishes after // the spend request has been notified and removed because it has // matured. err = n.UpdateSpendDetails(ntfn1.HistoricalDispatch.SpendRequest, nil) if err == nil { t.Fatalf("expected updating spend details to fail") } // Finally, check that the height hint is still there, unchanged. op1Hint, err = hintCache.QuerySpendHint(ntfn1.HistoricalDispatch.SpendRequest) require.NoError(t, err, "unable to query for spend hint of op1") if op1Hint != spendHeight { t.Fatalf("expected hint %d, got %d", spendHeight, op1Hint) } } // 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) require.NoError(t, err, "unable to register conf ntfn") spendNtfn, err := n.RegisterSpend(&chainntnfs.ZeroOutPoint, testRawScript, 1) require.NoError(t, err, "unable to register spend") // 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, 11) require.NoError(t, err, "unable to connect block") 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, 11) require.NoError(t, err, "unable to connect block") 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, i); 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) require.NoError(t, err, "unable to register conf ntfn") spendNtfn, err := n.RegisterSpend(&chainntnfs.ZeroOutPoint, testRawScript, 1) require.NoError(t, err, "unable to register spend ntfn") // 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) } }