From f0048bb662a92d0570eddd46a13b6d48343d5779 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 14 Nov 2019 15:01:21 +0100 Subject: [PATCH] chainntnfs/txnotifier_test: add TestTxNotifierSpendDuringHistoricalRescan --- chainntnfs/txnotifier_test.go | 204 ++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) diff --git a/chainntnfs/txnotifier_test.go b/chainntnfs/txnotifier_test.go index 1e5c267d..1f4ff085 100644 --- a/chainntnfs/txnotifier_test.go +++ b/chainntnfs/txnotifier_test.go @@ -2155,6 +2155,210 @@ func TestTxNotifierSpendHintCache(t *testing.T) { } } +// 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 + ) + + // Intiialize 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) + if err != nil { + t.Fatalf("unable to register spend for op1: %v", err) + } + + // 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.Hash(), height, emptyBlock.Transactions(), + ) + 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) + } + + // 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.Hash(), height, block.Transactions(), + ) + 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, + ) + if err != nil { + t.Fatalf("unable to query for spend hint of op1: %v", err) + } + 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) + if err != nil { + t.Fatalf("unable to update spend details: %v", err) + } + + op1Hint, err = hintCache.QuerySpendHint( + ntfn1.HistoricalDispatch.SpendRequest, + ) + if err != nil { + t.Fatalf("unable to query for spend hint of op1: %v", err) + } + 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.Hash(), height, block2.Transactions()) + 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) + } + + // The outpoint's spend hint should remain the same as it's already + // been spent before. + op1Hint, err = hintCache.QuerySpendHint(ntfn1.HistoricalDispatch.SpendRequest) + if err != nil { + t.Fatalf("unable to query for spend hint of op1: %v", err) + } + if op1Hint != 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.Hash(), height, block.Transactions(), + ) + 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("expcted updating spend details to fail") + } + + // Finally, check that the height hint is still there, unchanged. + op1Hint, err = hintCache.QuerySpendHint(ntfn1.HistoricalDispatch.SpendRequest) + if err != nil { + t.Fatalf("unable to query for spend hint of op1: %v", err) + } + 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.