diff --git a/chainntnfs/txnotifier_test.go b/chainntnfs/txnotifier_test.go index 6675709b..98a62f2f 100644 --- a/chainntnfs/txnotifier_test.go +++ b/chainntnfs/txnotifier_test.go @@ -10,7 +10,10 @@ import ( "github.com/lightningnetwork/lnd/chainntnfs" ) -var zeroHash chainhash.Hash +var ( + zeroHash chainhash.Hash + zeroOutPoint wire.OutPoint +) type mockHintCache struct { mu sync.Mutex @@ -96,9 +99,9 @@ func newMockHintCache() *mockHintCache { } } -// TestTxConfFutureDispatch tests that the TxNotifier dispatches registered -// notifications when the transaction confirms after registration. -func TestTxConfFutureDispatch(t *testing.T) { +// TestTxNotifierFutureConfDispatch tests that the TxNotifier dispatches +// registered notifications when a transaction confirms after registration. +func TestTxNotifierFutureConfDispatch(t *testing.T) { t.Parallel() const ( @@ -113,7 +116,7 @@ func TestTxConfFutureDispatch(t *testing.T) { ) hintCache := newMockHintCache() - n := chainntnfs.NewTxNotifier(10, 100, hintCache) + n := chainntnfs.NewTxNotifier(10, 100, hintCache, hintCache) // Create the test transactions and register them with the TxNotifier // before including them in a block to receive future @@ -192,7 +195,7 @@ func TestTxConfFutureDispatch(t *testing.T) { BlockHeight: 11, TxIndex: 0, } - assertEqualTxConf(t, txConf, &expectedConf) + assertConfDetails(t, txConf, &expectedConf) default: t.Fatalf("Expected confirmation for tx1") } @@ -263,15 +266,16 @@ func TestTxConfFutureDispatch(t *testing.T) { BlockHeight: 11, TxIndex: 1, } - assertEqualTxConf(t, txConf, &expectedConf) + assertConfDetails(t, txConf, &expectedConf) default: t.Fatalf("Expected confirmation for tx2") } } -// TestTxConfHistoricalDispatch tests that the TxNotifier dispatches registered -// notifications when the transaction is confirmed before registration. -func TestTxConfHistoricalDispatch(t *testing.T) { +// TestTxNotifierHistoricalConfDispatch tests that the TxNotifier dispatches +// registered notifications when the transaction is confirmed before +// registration. +func TestTxNotifierHistoricalConfDispatch(t *testing.T) { t.Parallel() const ( @@ -286,7 +290,7 @@ func TestTxConfHistoricalDispatch(t *testing.T) { ) hintCache := newMockHintCache() - n := chainntnfs.NewTxNotifier(10, 100, hintCache) + n := chainntnfs.NewTxNotifier(10, 100, hintCache, hintCache) // Create the test transactions at a height before the TxNotifier's // starting height so that they are confirmed once registering them. @@ -338,7 +342,7 @@ func TestTxConfHistoricalDispatch(t *testing.T) { // A confirmation notification for tx1 should also be dispatched. select { case txConf := <-ntfn1.Event.Confirmed: - assertEqualTxConf(t, txConf, &txConf1) + assertConfDetails(t, txConf, &txConf1) default: t.Fatalf("Expected confirmation for tx1") } @@ -413,16 +417,547 @@ func TestTxConfHistoricalDispatch(t *testing.T) { // its required number of confirmations. select { case txConf := <-ntfn2.Event.Confirmed: - assertEqualTxConf(t, txConf, &txConf2) + assertConfDetails(t, txConf, &txConf2) default: t.Fatalf("Expected confirmation for tx2") } } -// TestTxConfChainReorg tests that TxNotifier dispatches Confirmed and -// NegativeConf notifications appropriately when there is a chain -// reorganization. -func TestTxConfChainReorg(t *testing.T) { +// 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, 100, hintCache, hintCache) + + // We'll start off by registering for a spend notification of an + // outpoint. + ntfn := &chainntnfs.SpendNtfn{ + OutPoint: zeroOutPoint, + Event: chainntnfs.NewSpendEvent(nil), + } + if _, err := n.RegisterSpend(ntfn); 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: zeroOutPoint}) + 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) + } + + expectedSpendDetails := &chainntnfs.SpendDetail{ + SpentOutPoint: &ntfn.OutPoint, + 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}) + 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) + } + + 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, 100, hintCache, hintCache) + + // We'll start by constructing the spending details of the outpoint + // below. + spentOutpoint := zeroOutPoint + spendTx := wire.NewMsgTx(2) + spendTx.AddTxIn(&wire.TxIn{PreviousOutPoint: zeroOutPoint}) + 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 := &chainntnfs.SpendNtfn{ + OutPoint: spentOutpoint, + Event: chainntnfs.NewSpendEvent(nil), + } + if _, err := n.RegisterSpend(ntfn); 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.OutPoint, 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}) + 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) + } + + 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, 100, 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. + confNtfn1 := &chainntnfs.ConfNtfn{ + ConfID: 0, + TxID: &zeroHash, + Event: chainntnfs.NewConfirmationEvent(1), + } + historicalConfDispatch1, err := n.RegisterConf(confNtfn1) + if err != nil { + t.Fatalf("unable to register spend ntfn: %v", err) + } + if historicalConfDispatch1 == 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. + confNtfn2 := &chainntnfs.ConfNtfn{ + ConfID: 1, + TxID: &zeroHash, + Event: chainntnfs.NewConfirmationEvent(1), + } + historicalConfDispatch2, err := n.RegisterConf(confNtfn2) + if err != nil { + t.Fatalf("unable to register spend ntfn: %v", err) + } + if historicalConfDispatch2 != 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, + } + if err := n.UpdateConfDetails(*confNtfn2.TxID, confDetails); err != nil { + t.Fatalf("unable to update conf details: %v", err) + } + + confNtfn3 := &chainntnfs.ConfNtfn{ + ConfID: 2, + TxID: &zeroHash, + Event: chainntnfs.NewConfirmationEvent(1), + } + historicalConfDispatch3, err := n.RegisterConf(confNtfn3) + if err != nil { + t.Fatalf("unable to register spend ntfn: %v", err) + } + if historicalConfDispatch3 != 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, 100, 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. + ntfn1 := &chainntnfs.SpendNtfn{ + SpendID: 0, + OutPoint: zeroOutPoint, + Event: chainntnfs.NewSpendEvent(nil), + } + historicalDispatch1, err := n.RegisterSpend(ntfn1) + if err != nil { + t.Fatalf("unable to register spend ntfn: %v", err) + } + if historicalDispatch1 == 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 := &chainntnfs.SpendNtfn{ + SpendID: 1, + OutPoint: zeroOutPoint, + Event: chainntnfs.NewSpendEvent(nil), + } + historicalDispatch2, err := n.RegisterSpend(ntfn2) + if err != nil { + t.Fatalf("unable to register spend ntfn: %v", err) + } + if historicalDispatch2 != 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: &ntfn2.OutPoint, + SpenderTxHash: &zeroHash, + SpendingTx: wire.NewMsgTx(2), + SpenderInputIndex: 0, + SpendingHeight: startingHeight - 1, + } + err = n.UpdateSpendDetails(ntfn2.OutPoint, spendDetails) + if err != nil { + t.Fatalf("unable to update spend details: %v", err) + } + + ntfn3 := &chainntnfs.SpendNtfn{ + SpendID: 2, + OutPoint: zeroOutPoint, + Event: chainntnfs.NewSpendEvent(nil), + } + historicalDispatch3, err := n.RegisterSpend(ntfn3) + if err != nil { + t.Fatalf("unable to register spend ntfn: %v", err) + } + if historicalDispatch3 != 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, 100, hintCache, hintCache) + + // We'll start off by registered 5 clients for a confirmation + // notification on the same transaction. + confNtfns := make([]*chainntnfs.ConfNtfn, numNtfns) + for i := uint64(0); i < numNtfns; i++ { + confNtfns[i] = &chainntnfs.ConfNtfn{ + ConfID: i, + TxID: &zeroHash, + Event: chainntnfs.NewConfirmationEvent(1), + } + if _, err := n.RegisterConf(confNtfns[i]); err != nil { + t.Fatalf("unable to register conf ntfn #%d: %v", i, err) + } + } + + // 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, + } + err := n.UpdateConfDetails(*confNtfns[0].TxID, 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 := &chainntnfs.ConfNtfn{ + ConfID: numNtfns + 1, + TxID: &zeroHash, + Event: chainntnfs.NewConfirmationEvent(1), + } + historicalConfRescan, err := n.RegisterConf(extraConfNtfn) + if err != nil { + t.Fatalf("unable to register conf ntfn: %v", err) + } + if historicalConfRescan != 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. + spendNtfns := make([]*chainntnfs.SpendNtfn, numNtfns) + for i := uint64(0); i < numNtfns; i++ { + spendNtfns[i] = &chainntnfs.SpendNtfn{ + SpendID: i, + OutPoint: zeroOutPoint, + Event: chainntnfs.NewSpendEvent(nil), + } + if _, err := n.RegisterSpend(spendNtfns[i]); err != nil { + t.Fatalf("unable to register spend ntfn #%d: %v", i, err) + } + } + + // 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: &spendNtfns[0].OutPoint, + SpenderTxHash: &zeroHash, + SpendingTx: wire.NewMsgTx(2), + SpenderInputIndex: 0, + SpendingHeight: startingHeight - 1, + } + err = n.UpdateSpendDetails(spendNtfns[0].OutPoint, 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 := &chainntnfs.SpendNtfn{ + SpendID: numNtfns + 1, + OutPoint: zeroOutPoint, + Event: chainntnfs.NewSpendEvent(nil), + } + historicalSpendRescan, err := n.RegisterSpend(extraSpendNtfn) + if err != nil { + t.Fatalf("unable to register spend ntfn: %v", err) + } + if historicalSpendRescan != 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") + } +} + +// 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, 100, hintCache, hintCache) + + // We'll register two notification requests. Only the second one will be + // canceled. + ntfn1 := &chainntnfs.SpendNtfn{ + SpendID: 0, + OutPoint: zeroOutPoint, + Event: chainntnfs.NewSpendEvent(nil), + } + if _, err := n.RegisterSpend(ntfn1); err != nil { + t.Fatalf("unable to register spend ntfn: %v", err) + } + + ntfn2 := &chainntnfs.SpendNtfn{ + SpendID: 1, + OutPoint: zeroOutPoint, + Event: chainntnfs.NewSpendEvent(nil), + } + if _, err := n.RegisterSpend(ntfn2); 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: ntfn1.OutPoint}) + spendTxHash := spendTx.TxHash() + expectedSpendDetails := &chainntnfs.SpendDetail{ + SpentOutPoint: &ntfn1.OutPoint, + 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.OutPoint, ntfn2.SpendID) + + err := n.ConnectTip(block.Hash(), startingHeight+1, block.Transactions()) + if err != nil { + t.Fatalf("unable to connect block: %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 ( @@ -438,7 +973,7 @@ func TestTxConfChainReorg(t *testing.T) { ) hintCache := newMockHintCache() - n := chainntnfs.NewTxNotifier(7, 100, hintCache) + n := chainntnfs.NewTxNotifier(7, 100, hintCache, hintCache) // Tx 1 will be confirmed in block 9 and requires 2 confs. tx1Hash := tx1.TxHash() @@ -649,7 +1184,7 @@ func TestTxConfChainReorg(t *testing.T) { BlockHeight: 12, TxIndex: 0, } - assertEqualTxConf(t, txConf, &expectedConf) + assertConfDetails(t, txConf, &expectedConf) default: t.Fatalf("Expected confirmation for tx2") } @@ -679,18 +1214,221 @@ func TestTxConfChainReorg(t *testing.T) { BlockHeight: 12, TxIndex: 1, } - assertEqualTxConf(t, txConf, &expectedConf) + assertConfDetails(t, txConf, &expectedConf) default: t.Fatalf("Expected confirmation for tx3") } } -// TestTxConfHeightHintCache 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 +// 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, 100, 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 := zeroOutPoint + op1.Index = 1 + spendTx1 := wire.NewMsgTx(2) + spendTx1.AddTxIn(&wire.TxIn{PreviousOutPoint: op1}) + spendTxHash1 := spendTx1.TxHash() + expectedSpendDetails1 := &chainntnfs.SpendDetail{ + SpentOutPoint: &op1, + SpenderTxHash: &spendTxHash1, + SpendingTx: spendTx1, + SpenderInputIndex: 0, + SpendingHeight: startingHeight + 1, + } + + op2 := zeroOutPoint + op2.Index = 2 + spendTx2 := wire.NewMsgTx(2) + spendTx2.AddTxIn(&wire.TxIn{PreviousOutPoint: zeroOutPoint}) + spendTx2.AddTxIn(&wire.TxIn{PreviousOutPoint: op2}) + 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 := &chainntnfs.SpendNtfn{ + SpendID: 78, + OutPoint: op1, + Event: chainntnfs.NewSpendEvent(nil), + } + if _, err := n.RegisterSpend(ntfn1); err != nil { + t.Fatalf("unable to register spend ntfn: %v", err) + } + + ntfn2 := &chainntnfs.SpendNtfn{ + SpendID: 21, + OutPoint: op2, + Event: chainntnfs.NewSpendEvent(nil), + } + if _, err := n.RegisterSpend(ntfn2); 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) + } + + // 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) + } + + // 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) + } + + // 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) + } + + // 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 TestTxConfHeightHintCache(t *testing.T) { +func TestTxNotifierConfirmHintCache(t *testing.T) { t.Parallel() const ( @@ -702,9 +1440,7 @@ func TestTxConfHeightHintCache(t *testing.T) { // Initialize our TxNotifier instance backed by a height hint cache. hintCache := newMockHintCache() - n := chainntnfs.NewTxNotifier( - startingHeight, 100, hintCache, - ) + n := chainntnfs.NewTxNotifier(startingHeight, 100, hintCache, hintCache) // Create two test transactions and register them for notifications. tx1 := wire.MsgTx{Version: 1} @@ -884,129 +1620,284 @@ func TestTxConfHeightHintCache(t *testing.T) { } } -func TestTxConfTearDown(t *testing.T) { +// 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() - var ( - tx1 = wire.MsgTx{Version: 1} - tx2 = wire.MsgTx{Version: 2} + const ( + startingHeight = 200 + dummyHeight = 201 + op1Height = 202 + op2Height = 203 ) + // Intiialize our TxNotifier instance backed by a height hint cache. hintCache := newMockHintCache() - n := chainntnfs.NewTxNotifier(10, 100, hintCache) + n := chainntnfs.NewTxNotifier(startingHeight, 100, hintCache, hintCache) - // Create the test transactions and register them with the TxNotifier to - // receive notifications. - tx1Hash := tx1.TxHash() - ntfn1 := chainntnfs.ConfNtfn{ - TxID: &tx1Hash, + // Create two test outpoints and register them for spend notifications. + op1 := wire.OutPoint{Hash: zeroHash, Index: 1} + ntfn1 := &chainntnfs.SpendNtfn{ + OutPoint: op1, + Event: chainntnfs.NewSpendEvent(nil), + } + op2 := wire.OutPoint{Hash: zeroHash, Index: 2} + ntfn2 := &chainntnfs.SpendNtfn{ + OutPoint: op2, + Event: chainntnfs.NewSpendEvent(nil), + } + + if _, err := n.RegisterSpend(ntfn1); err != nil { + t.Fatalf("unable to register spend for op1: %v", err) + } + if _, err := n.RegisterSpend(ntfn2); 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(op1) + if err != chainntnfs.ErrSpendHintNotFound { + t.Fatalf("unexpected error when querying for height hint "+ + "expected: %v, got %v", chainntnfs.ErrSpendHintNotFound, + err) + } + _, err = hintCache.QuerySpendHint(op2) + 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) + } + + // 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(op1) + if err != chainntnfs.ErrSpendHintNotFound { + t.Fatalf("unexpected error when querying for height hint "+ + "expected: %v, got %v", chainntnfs.ErrSpendHintNotFound, + err) + } + _, err = hintCache.QuerySpendHint(op2) + 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. + if err := n.UpdateSpendDetails(ntfn1.OutPoint, nil); err != nil { + t.Fatalf("unable to update spend details: %v", err) + } + if err := n.UpdateSpendDetails(ntfn2.OutPoint, nil); 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: ntfn1.OutPoint}) + 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) + } + + // 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.OutPoint) + 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.OutPoint) + 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: ntfn2.OutPoint}) + 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) + } + + // 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.OutPoint) + 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.OutPoint) + 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.OutPoint) + 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.OutPoint) + 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) + } +} + +// 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, 100, hintCache, hintCache) + + // To begin the test, we'll register for a confirmation and spend + // notification. + confNtfn := &chainntnfs.ConfNtfn{ + TxID: &zeroHash, NumConfirmations: 1, Event: chainntnfs.NewConfirmationEvent(1), } - if _, err := n.RegisterConf(&ntfn1); err != nil { - t.Fatalf("unable to register ntfn: %v", err) - } - if err := n.UpdateConfDetails(*ntfn1.TxID, nil); err != nil { - t.Fatalf("unable to update conf details: %v", err) + if _, err := n.RegisterConf(confNtfn); err != nil { + t.Fatalf("unable to register conf ntfn: %v", err) } - tx2Hash := tx2.TxHash() - ntfn2 := chainntnfs.ConfNtfn{ - TxID: &tx2Hash, - NumConfirmations: 2, - Event: chainntnfs.NewConfirmationEvent(2), + spendNtfn := &chainntnfs.SpendNtfn{ + OutPoint: zeroOutPoint, + Event: chainntnfs.NewSpendEvent(nil), } - if _, err := n.RegisterConf(&ntfn2); err != nil { - t.Fatalf("unable to register ntfn: %v", err) - } - if err := n.UpdateConfDetails(*ntfn2.TxID, nil); err != nil { - t.Fatalf("unable to update conf details: %v", err) + if _, err := n.RegisterSpend(spendNtfn); err != nil { + t.Fatalf("unable to register spend ntfn: %v", err) } - // Include the transactions in a block and add it to the TxNotifier. - // This should confirm tx1, but not tx2. - block := btcutil.NewBlock(&wire.MsgBlock{ - Transactions: []*wire.MsgTx{&tx1, &tx2}, - }) - - err := n.ConnectTip(block.Hash(), 11, block.Transactions()) - if err != nil { - t.Fatalf("Failed to connect block: %v", err) - } - - // We do not care about the correctness of the notifications since they - // are tested in other methods, but we'll still attempt to retrieve them - // for the sake of not being able to later once the notification - // channels are closed. - select { - case <-ntfn1.Event.Updates: - default: - t.Fatal("Expected confirmation update for tx1") - } - - select { - case <-ntfn1.Event.Confirmed: - default: - t.Fatalf("Expected confirmation for tx1") - } - - select { - case <-ntfn2.Event.Updates: - default: - t.Fatal("Expected confirmation update for tx2") - } - - select { - case txConf := <-ntfn2.Event.Confirmed: - t.Fatalf("Received unexpected confirmation for tx2: %v", txConf) - default: - } - - // The notification channels should be closed for notifications that - // have not been dispatched yet, so we should not expect to receive any - // more updates. + // 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() - // tx1 should not receive any more updates because it has already been - // confirmed and the TxNotifier has been shut down. 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) + 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") } - // tx2 should not receive any more updates after the notifications - // channels have been closed and the TxNotifier shut down. - select { - case _, more := <-ntfn2.Event.Updates: - if more { - t.Fatal("Expected closed Updates channel for tx2") - } - case _, more := <-ntfn2.Event.Confirmed: - if more { - t.Fatalf("Expected closed Confirmed channel for tx2") - } - default: - t.Fatalf("Expected closed notification channels for tx2") + // Now that the notifier is torn down, we should no longer be able to + // register notification requests. + if _, err := n.RegisterConf(confNtfn); err == nil { + t.Fatal("expected confirmation registration to fail") + } + if _, err := n.RegisterSpend(spendNtfn); err == nil { + t.Fatal("expected spend registration to fail") } } -func assertEqualTxConf(t *testing.T, - actualConf, expectedConf *chainntnfs.TxConfirmation) { +func assertConfDetails(t *testing.T, result, expected *chainntnfs.TxConfirmation) { + t.Helper() - if actualConf.BlockHeight != expectedConf.BlockHeight { + if result.BlockHeight != expected.BlockHeight { t.Fatalf("Incorrect block height in confirmation details: "+ - "expected %d, got %d", - expectedConf.BlockHeight, actualConf.BlockHeight) + "expected %d, got %d", expected.BlockHeight, + result.BlockHeight) } - if !actualConf.BlockHash.IsEqual(expectedConf.BlockHash) { + if !result.BlockHash.IsEqual(expected.BlockHash) { t.Fatalf("Incorrect block hash in confirmation details: "+ - "expected %d, got %d", expectedConf.BlockHash, actualConf.BlockHash) + "expected %d, got %d", expected.BlockHash, + result.BlockHash) } - if actualConf.TxIndex != expectedConf.TxIndex { + if result.TxIndex != expected.TxIndex { t.Fatalf("Incorrect tx index in confirmation details: "+ - "expected %d, got %d", expectedConf.TxIndex, actualConf.TxIndex) + "expected %d, got %d", expected.TxIndex, result.TxIndex) + } +} + +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) } }