github.com/decred/dcrlnd@v0.7.6/chainscan/tip_test.go (about)

     1  package chainscan
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/decred/dcrd/wire"
     9  )
    10  
    11  type twTestCtx struct {
    12  	chain  *mockChain
    13  	tw     *TipWatcher
    14  	cancel func()
    15  	t      *testing.T
    16  }
    17  
    18  func newTwTestCtx(t *testing.T) *twTestCtx {
    19  	ctx, cancel := context.WithCancel(context.Background())
    20  	chain := newMockChain()
    21  	tw := NewTipWatcher(chain)
    22  	chain.extend(chain.newFromTip()) // Genesis block
    23  
    24  	// Instrument tipProcessed.
    25  	tw.tipProcessed = make(chan *blockCFilter)
    26  
    27  	// Use synchronous version of Find() for tests by default.
    28  	tw.syncFind = true
    29  
    30  	go func() {
    31  		tw.Run(ctx)
    32  	}()
    33  	return &twTestCtx{
    34  		chain:  chain,
    35  		tw:     tw,
    36  		cancel: cancel,
    37  		t:      t,
    38  	}
    39  }
    40  
    41  func (t *twTestCtx) cleanup() {
    42  	t.cancel()
    43  }
    44  
    45  func (t *twTestCtx) extendTipWait(b *testBlock) {
    46  	t.t.Helper()
    47  	t.chain.extend(b)
    48  	t.chain.signalNewTip()
    49  	select {
    50  	case <-t.tw.tipProcessed:
    51  	case <-time.After(5 * time.Second):
    52  		t.t.Fatal("new tip not processed in time")
    53  	}
    54  }
    55  
    56  func (t *twTestCtx) extendNewTip(manglers ...blockMangler) *testBlock {
    57  	t.t.Helper()
    58  	b := t.chain.newFromTip(manglers...)
    59  	t.extendTipWait(b)
    60  	return b
    61  }
    62  
    63  // TestTipWatcher tests the basic functionality of the TipWatcher by testing it
    64  // against the scannerTestCases which must be fulfilled by both the tipWatcher
    65  // and historical scanners.
    66  func TestTipWatcher(t *testing.T) {
    67  	runTC := func(c scannerTestCase, t *testing.T) {
    68  		var foundCbEvent, foundChanEvent Event
    69  		foundChan := make(chan Event)
    70  		tc := newTwTestCtx(t)
    71  		defer tc.cleanup()
    72  
    73  		b := tc.chain.newFromTip(c.manglers...)
    74  		err := tc.tw.Find(
    75  			c.target(b),
    76  			WithFoundCallback(func(e Event, _ FindFunc) { foundCbEvent = e }),
    77  			WithFoundChan(foundChan),
    78  		)
    79  		if err != nil {
    80  			t.Fatalf("Find returned error: %v", err)
    81  		}
    82  
    83  		tc.extendTipWait(b)
    84  
    85  		if !c.wantFound {
    86  			// Testing when we don't expect a match.
    87  
    88  			assertFoundChanEmpty(t, foundChan)
    89  			if foundCbEvent != emptyEvent {
    90  				t.Fatalf("unexpected foundCallback triggered with %s", &foundCbEvent)
    91  			}
    92  
    93  			// Nothing else to test since we didn't expect a match.
    94  			return
    95  		}
    96  
    97  		// Testing when we expect a match.
    98  
    99  		select {
   100  		case foundChanEvent = <-foundChan:
   101  		case <-time.After(5 * time.Second):
   102  			t.Fatal("found chan not triggered in time")
   103  		}
   104  
   105  		if foundCbEvent == emptyEvent {
   106  			t.Fatal("foundCallback not triggered")
   107  		}
   108  
   109  		if foundChanEvent != foundCbEvent {
   110  			t.Fatal("cb and chan showed different events")
   111  		}
   112  
   113  		e := foundChanEvent
   114  		if e.MatchedField != c.wantMF {
   115  			t.Fatalf("unexpected matched field. want=%s got=%s",
   116  				c.wantMF, e.MatchedField)
   117  		}
   118  
   119  		if e.BlockHeight != int32(b.block.Header.Height) {
   120  			t.Fatalf("unexpected matched block height. want=%d got=%d",
   121  				b.block.Header.Height, e.BlockHeight)
   122  		}
   123  
   124  		if e.BlockHash != b.block.Header.BlockHash() {
   125  			t.Fatalf("unexpected matched block hash. want=%s got=%s",
   126  				b.block.Header.BlockHash(), e.BlockHash)
   127  		}
   128  
   129  		// All tests always match against the first transaction in the
   130  		// block in either the stake or regular transaction tree.
   131  		var tx *wire.MsgTx
   132  		var tree int8
   133  		if len(b.block.Transactions) > 0 {
   134  			tx = b.block.Transactions[0]
   135  			tree = wire.TxTreeRegular
   136  		} else {
   137  			tx = b.block.STransactions[0]
   138  			tree = wire.TxTreeStake
   139  		}
   140  		if e.Tx.TxHash() != tx.TxHash() {
   141  			t.Fatalf("unexpected tx match. want=%s got=%s",
   142  				b.block.Transactions[0].TxHash(), e.Tx.TxHash())
   143  		}
   144  
   145  		// All tests always match against the second input or output.
   146  		if e.Index != 1 {
   147  			t.Fatalf("unexpected index match. want=%d got=%d",
   148  				1, e.Index)
   149  		}
   150  
   151  		if e.Tree != tree {
   152  			t.Fatalf("unexpected tree match. want=%d got=%d",
   153  				tree, e.Tree)
   154  		}
   155  	}
   156  
   157  	for _, c := range scannerTestCases {
   158  		c := c
   159  		ok := t.Run(c.name, func(t *testing.T) { runTC(c, t) })
   160  		if !ok {
   161  			break
   162  		}
   163  	}
   164  }
   165  
   166  // TestTipWatcherCancelation tests that cancelling the watch for a target works
   167  // as expected.
   168  func TestTipWatcherCancelation(t *testing.T) {
   169  	tc := newTwTestCtx(t)
   170  	defer tc.cleanup()
   171  
   172  	foundChan := make(chan Event)
   173  	cancelChan := make(chan struct{})
   174  	completeChan := make(chan struct{})
   175  	tc.tw.Find(
   176  		ConfirmedScript(0, testPkScript),
   177  		WithFoundChan(foundChan),
   178  		WithCancelChan(cancelChan),
   179  		WithCompleteChan(completeChan),
   180  	)
   181  
   182  	// Mine a few blocks. We force a full block check to ensure the
   183  	// behavior under false positives.
   184  	tc.extendNewTip(cfilterData(testPkScript))
   185  	tc.extendNewTip(cfilterData(testPkScript))
   186  	tc.extendNewTip(cfilterData(testPkScript))
   187  
   188  	// Ensure none of the channels have been triggered yet
   189  	select {
   190  	case <-foundChan:
   191  		t.Fatal("foundChan unexpectedly triggered")
   192  	case <-cancelChan:
   193  		t.Fatal("cancelChan unexpectedly triggered")
   194  	case <-completeChan:
   195  		t.Fatal("completeChan unexpectedly triggered")
   196  	case <-time.After(time.Millisecond * 10):
   197  	}
   198  
   199  	// Generate a new block with a match. This should generate an event in
   200  	// foundChan.
   201  	tc.extendNewTip(
   202  		confirmScript(testPkScript),
   203  		cfilterData(testPkScript),
   204  	)
   205  	assertFoundChanRcv(t, foundChan)
   206  
   207  	// Cancel the request.
   208  	close(cancelChan)
   209  
   210  	// Generate a new block with a match. We don't expect this will trigger
   211  	// foundChan given we just canceled the request.
   212  	tc.extendNewTip(
   213  		confirmScript(testPkScript),
   214  		cfilterData(testPkScript),
   215  	)
   216  
   217  	// Still don't expect a signal in complete and found chans.
   218  	select {
   219  	case <-foundChan:
   220  		t.Fatal("foundChan unexpectedly triggered")
   221  	case <-completeChan:
   222  		t.Fatal("completeChan unexpectedly triggered")
   223  	case <-time.After(time.Millisecond * 10):
   224  	}
   225  
   226  }
   227  
   228  // TestTipWatcherStaleCompletion tests that watching for a target with a
   229  // specific endHeight triggers completion.
   230  func TestTipWatcherStaleCompletion(t *testing.T) {
   231  	tc := newTwTestCtx(t)
   232  	defer tc.cleanup()
   233  
   234  	foundChan := make(chan Event)
   235  	cancelChan := make(chan struct{})
   236  	completeChan := make(chan struct{})
   237  	tc.tw.Find(
   238  		ConfirmedScript(0, testPkScript),
   239  		WithFoundChan(foundChan),
   240  		WithCancelChan(cancelChan),
   241  		WithCompleteChan(completeChan),
   242  		WithEndHeight(5),
   243  	)
   244  
   245  	// Mine a few blocks. We force a full block check to ensure the
   246  	// behavior under false positives.
   247  	tc.extendNewTip(cfilterData(testPkScript))
   248  	tc.extendNewTip(cfilterData(testPkScript))
   249  
   250  	// Ensure none of the channels have been triggered yet.
   251  	select {
   252  	case <-foundChan:
   253  		t.Fatal("foundChan unexpectedly triggered")
   254  	case <-cancelChan:
   255  		t.Fatal("cancelChan unexpectedly triggered")
   256  	case <-completeChan:
   257  		t.Fatal("completeChan unexpectedly triggered")
   258  	case <-time.After(time.Millisecond * 10):
   259  	}
   260  
   261  	// Generate a new block with a match. This should generate an event in
   262  	// foundChan.
   263  	tc.extendNewTip(
   264  		confirmScript(testPkScript),
   265  		cfilterData(testPkScript),
   266  	)
   267  	assertFoundChanRcvHeight(t, foundChan, int32(tc.chain.tip.block.Header.Height))
   268  
   269  	// Generate blocks until endHeight. The completeChan should be closed
   270  	// by then.
   271  	tc.extendNewTip(cfilterData(testPkScript))
   272  	tc.extendNewTip(cfilterData(testPkScript))
   273  	assertCompleted(t, completeChan)
   274  
   275  	// Generate a new block with a match. We don't expect this will trigger
   276  	// foundChan given the request already completed.
   277  	tc.extendNewTip(
   278  		confirmScript(testPkScript),
   279  		cfilterData(testPkScript),
   280  	)
   281  
   282  	// Still don't expect a signal in found chans.
   283  	assertFoundChanEmpty(t, foundChan)
   284  }
   285  
   286  // TestTipWatcherReorg tests that when a reorg occurs that causes the NextTip()
   287  // function to go back to a previous height, watched targets are triggered even
   288  // if they weren't triggered in the previous chain.
   289  func TestTipWatcherReorg(t *testing.T) {
   290  	tc := newTwTestCtx(t)
   291  	defer tc.cleanup()
   292  
   293  	foundChan := make(chan Event)
   294  	completeChan := make(chan struct{})
   295  	tc.tw.Find(
   296  		ConfirmedScript(0, testPkScript),
   297  		WithFoundChan(foundChan),
   298  		WithCompleteChan(completeChan),
   299  	)
   300  
   301  	// Mine a few blocks. We force a full block check to ensure the
   302  	// behavior under false positives.
   303  	forkRoot := tc.extendNewTip(cfilterData(testPkScript))
   304  	tc.extendNewTip(cfilterData(testPkScript))
   305  	tc.extendNewTip(cfilterData(testPkScript))
   306  	forkedTip := tc.extendNewTip(cfilterData(testPkScript))
   307  
   308  	// Ensure none of the channels have been triggered yet.
   309  	select {
   310  	case <-foundChan:
   311  		t.Fatal("foundChan unexpectedly triggered")
   312  	case <-completeChan:
   313  		t.Fatal("completeChan unexpectedly triggered")
   314  	case <-time.After(time.Millisecond * 10):
   315  	}
   316  
   317  	// Force a reorg. We rewind the tip to the forkRoot point and generate
   318  	// new blocks from there. The last generated block matches the desired
   319  	// target at a height lower than the previous forked tip.
   320  	tc.chain.tip = forkRoot
   321  	tc.extendNewTip(cfilterData(testPkScript))
   322  	newTip := tc.extendNewTip(
   323  		confirmScript(testPkScript),
   324  		cfilterData(testPkScript),
   325  	)
   326  
   327  	// foundChan should have been triggered.
   328  	foundEvent := assertFoundChanRcv(t, foundChan)
   329  
   330  	// The height of the match should be the new tip and this tip should be
   331  	// at a height lower than the forked tip.
   332  	wantHeight := int32(newTip.block.Header.Height)
   333  	if foundEvent.BlockHeight != wantHeight {
   334  		t.Fatalf("Event not triggered at correct height. want=%d got=%d",
   335  			wantHeight, foundEvent.BlockHeight)
   336  	}
   337  	if wantHeight >= int32(forkedTip.block.Header.Height) {
   338  		t.Fatalf("New tip has height higher than forked tip. new=%d forked=%d",
   339  			wantHeight, forkedTip.block.Header.Height)
   340  	}
   341  
   342  	// Generate blocks until the new chain has a higher height than the
   343  	// forked tip.
   344  	for tc.chain.tip.block.Header.Height <= forkedTip.block.Header.Height {
   345  		tc.extendNewTip(cfilterData(testPkScript))
   346  	}
   347  
   348  	// Finally generate a new match to ensure the TipWatcher is still
   349  	// finding the target.
   350  	tc.extendNewTip(
   351  		confirmScript(testPkScript),
   352  		cfilterData(testPkScript),
   353  	)
   354  
   355  	select {
   356  	case <-completeChan:
   357  		t.Fatal("completeChan unexpectedly signalled")
   358  	case <-foundChan:
   359  	case <-time.After(5 * time.Second):
   360  		t.Fatal("timeout waiting for foundChan")
   361  	}
   362  }
   363  
   364  // TestTipWatcherMultipleMatchesInBlock tests that the historical search
   365  // correctly sends multiple events when the same script is confirmed multiple
   366  // times in a single block.
   367  func TestTipWatcherMultipleMatchesInBlock(t *testing.T) {
   368  	tc := newTwTestCtx(t)
   369  	defer tc.cleanup()
   370  
   371  	foundChan := make(chan Event)
   372  	tc.tw.Find(
   373  		ConfirmedScript(0, testPkScript),
   374  		WithFoundChan(foundChan),
   375  	)
   376  
   377  	newTip := tc.chain.newFromTip(
   378  		confirmScript(testPkScript),
   379  		cfilterData(testPkScript),
   380  	)
   381  	// Create an additional output.
   382  	newTip.block.Transactions[0].AddTxOut(&wire.TxOut{PkScript: testPkScript})
   383  	tc.chain.extend(newTip)
   384  	tc.chain.signalNewTip()
   385  
   386  	// foundChan should be triggered two (and only two) times.
   387  	e1 := assertFoundChanRcv(t, foundChan)
   388  	e2 := assertFoundChanRcv(t, foundChan)
   389  	assertFoundChanEmpty(t, foundChan)
   390  
   391  	// However the events should *not* be exactly the same: the script was
   392  	// confirmed in two different outputs.
   393  	if e1 == e2 {
   394  		t.Fatal("script confirmed twice in the same output")
   395  	}
   396  }
   397  
   398  // TestTipWatcherBlockDownload tests that TipWatcher only downloads blocks for
   399  // which the cfilter has passed.
   400  func TestTipWatcherBlockDownload(t *testing.T) {
   401  	tc := newTwTestCtx(t)
   402  	defer tc.cleanup()
   403  
   404  	tc.tw.Find(
   405  		ConfirmedScript(0, testPkScript),
   406  	)
   407  
   408  	// Extend the tip with 2 blocks where the cfilter matches the watched
   409  	// content and 3 where it doesn't.
   410  	tc.extendNewTip(cfilterData(testPkScript))
   411  	tc.extendNewTip()
   412  	tc.extendNewTip()
   413  	tc.extendNewTip(cfilterData(testPkScript))
   414  	tc.extendNewTip()
   415  
   416  	// We only expect fetches for 2 blocks of data.
   417  	wantGetBlockCount := uint32(2)
   418  	if tc.chain.getBlockCount != wantGetBlockCount {
   419  		t.Fatalf("Unexpected getBlockCount. want=%d got=%d",
   420  			wantGetBlockCount, tc.chain.getBlockCount)
   421  	}
   422  }
   423  
   424  // TestTipWatcherStartWatchHeight tests whether the correct height is returned
   425  // when starting to watch for a target.
   426  func TestTipWatcherStartWatchHeight(t *testing.T) {
   427  	tc := newTwTestCtx(t)
   428  	defer tc.cleanup()
   429  
   430  	swhChan := make(chan int32)
   431  	var gotHeight int32
   432  
   433  	// Switch to the regular asynchronous version of Find() since that's
   434  	// what is used in production.
   435  	tc.tw.syncFind = false
   436  
   437  	// Test watching when the chain is synced and "quiet".
   438  	tc.tw.Find(
   439  		ConfirmedScript(0, testPkScript),
   440  		WithStartWatchHeightChan(swhChan),
   441  	)
   442  	wantHeight := tc.chain.tip.block.Header.Height
   443  	gotHeight = assertStartWatchHeightSignalled(t, swhChan)
   444  	if wantHeight != uint32(gotHeight) {
   445  		t.Fatalf("Unexpected start watching height. want=%d got=%d",
   446  			wantHeight, gotHeight)
   447  	}
   448  
   449  	// Test watching when the target is watched for in the middle of tip
   450  	// processing. Note we haven't read from t.tw.tipProcessed so the tip
   451  	// still hasn't been fully processed.
   452  	newTip := tc.chain.newFromTip(cfilterData(testPkScript))
   453  	tc.chain.extend(newTip)
   454  	tc.chain.signalNewTip()
   455  
   456  	// Give it enough time for the tip to start being processed.
   457  	time.Sleep(10 * time.Millisecond)
   458  
   459  	// Try to find the target again.
   460  	tc.tw.Find(
   461  		ConfirmedScript(0, testPkScript),
   462  		WithStartWatchHeightChan(swhChan),
   463  	)
   464  
   465  	// Give it enough time to block.
   466  	time.Sleep(10 * time.Millisecond)
   467  
   468  	// Finish processing tip.
   469  	select {
   470  	case <-tc.tw.tipProcessed:
   471  	case <-time.After(5 * time.Second):
   472  		t.Fatal("Timeout waiting for tipProcessed")
   473  	}
   474  
   475  	// We should see the target being watched for after the _new_ tip (vs
   476  	// the one that was in the middle of processing when we tried to add
   477  	// the target).
   478  	wantHeight = newTip.block.Header.Height
   479  	gotHeight = assertStartWatchHeightSignalled(t, swhChan)
   480  	if wantHeight != uint32(gotHeight) {
   481  		t.Fatalf("Unexpected start watching height. want=%d got=%d",
   482  			wantHeight, gotHeight)
   483  	}
   484  }
   485  
   486  // TestTipWatcherMultipleFinds tests whether attempting to find multiple times
   487  // the same target works as expected.
   488  func TestTipWatcherMultipleFinds(t *testing.T) {
   489  
   490  	runTC := func(c scannerTestCase, t *testing.T) {
   491  		tc := newTwTestCtx(t)
   492  		defer tc.cleanup()
   493  
   494  		b := tc.chain.newFromTip(c.manglers...)
   495  
   496  		// Start two searches for the same pkscript.
   497  		foundChan1 := make(chan Event)
   498  		foundChan2 := make(chan Event)
   499  		tc.tw.Find(
   500  			c.target(b),
   501  			WithFoundChan(foundChan1),
   502  		)
   503  		tc.tw.Find(
   504  			c.target(b),
   505  			WithFoundChan(foundChan2),
   506  		)
   507  
   508  		// Confirm it.
   509  		tc.extendTipWait(b)
   510  
   511  		// The two foundChans should be signalled.
   512  		event1 := assertFoundChanRcvHeight(t, foundChan1, int32(b.block.Header.Height))
   513  		event2 := assertFoundChanRcv(t, foundChan2)
   514  		if event1 != event2 {
   515  			t.Fatalf("Different events returned: %s vs %s", &event1, &event2)
   516  		}
   517  	}
   518  
   519  	// Test against all variants of targets.
   520  	for _, c := range scannerTestCases {
   521  		if !c.wantFound {
   522  			continue
   523  		}
   524  		c := c
   525  		ok := t.Run(c.name, func(t *testing.T) { runTC(c, t) })
   526  		if !ok {
   527  			break
   528  		}
   529  	}
   530  }
   531  
   532  // TestTipWatcherAddNewTarget tests that adding a new target during a
   533  // foundCallback works as expected.
   534  func TestTipWatcherAddNewTarget(t *testing.T) {
   535  	tc := newTwTestCtx(t)
   536  	defer tc.cleanup()
   537  
   538  	// Disable syncFind so Find() won't deadlock during foundCb.
   539  	tc.tw.syncFind = false
   540  
   541  	// foundChan should only be triggered in a block _after_ the foundCb
   542  	// callback is called.
   543  	cancelChan := make(chan struct{})
   544  	foundChan := make(chan Event)
   545  	swhChan := make(chan int32)
   546  	foundCb := func(e Event, _ FindFunc) {
   547  		close(cancelChan) // Prevent repeated calls of foundCb.
   548  		tc.tw.Find(
   549  			ConfirmedScript(0, testPkScript),
   550  			WithFoundChan(foundChan),
   551  			WithStartWatchHeightChan(swhChan),
   552  		)
   553  	}
   554  	tc.tw.Find(
   555  		ConfirmedScript(0, testPkScript),
   556  		WithFoundCallback(foundCb),
   557  		WithCancelChan(cancelChan),
   558  	)
   559  
   560  	// Give it enough time for the new target to register in the scanner.
   561  	time.Sleep(10 * time.Millisecond)
   562  
   563  	// Extend the chain with 2 blocks without the target script and then
   564  	// one block with the target script (which triggers foundCb).
   565  	tc.extendNewTip(cfilterData(testPkScript))
   566  	tc.extendNewTip(cfilterData(testPkScript))
   567  	tc.extendNewTip(
   568  		confirmScript(testPkScript),
   569  		cfilterData(testPkScript),
   570  	)
   571  
   572  	// foundChan shoulnd't have been signalled yet.
   573  	assertFoundChanEmpty(t, foundChan)
   574  
   575  	// But the new search should have started.
   576  	assertStartWatchHeightSignalled(t, swhChan)
   577  
   578  	// Extend the chain with a new block with the taget script. We expect
   579  	// foundChan to be triggered now, but only once.
   580  	tc.extendNewTip(
   581  		confirmScript(testPkScript),
   582  		cfilterData(testPkScript),
   583  	)
   584  
   585  	assertFoundChanRcvHeight(t, foundChan, int32(tc.chain.tip.block.Header.Height))
   586  	assertFoundChanEmpty(t, foundChan)
   587  }
   588  
   589  // TestTipWatcherAddNewTargetDuringFcb tests that adding a new target during a
   590  // foundCallback to be searched starting at the same block works as expected.
   591  func TestTipWatcherAddNewTargetDuringFcb(t *testing.T) {
   592  
   593  	runTC := func(c scannerTestCase, t *testing.T) {
   594  		tc := newTwTestCtx(t)
   595  		defer tc.cleanup()
   596  
   597  		b := tc.chain.newFromTip(c.manglers...)
   598  		dupeTestTx(b)
   599  
   600  		// The found callback is called for the main find below and
   601  		// adds a new target that signals via foundChan.
   602  		foundChan := make(chan Event)
   603  		foundCb := func(e Event, addNew FindFunc) {
   604  			assertNoError(t, addNew(
   605  				c.target(b),
   606  				WithStartHeight(e.BlockHeight+1),
   607  				WithFoundChan(foundChan),
   608  			))
   609  		}
   610  
   611  		tc.tw.Find(
   612  			c.target(b),
   613  			WithFoundCallback(foundCb),
   614  		)
   615  
   616  		// Confirm it.
   617  		tc.extendTipWait(b)
   618  
   619  		// We expect foundChan to receive one (and only one) event.
   620  		assertFoundChanRcv(t, foundChan)
   621  		assertFoundChanEmpty(t, foundChan)
   622  	}
   623  
   624  	// Test against all variants of targets.
   625  	for _, c := range scannerTestCases {
   626  		if !c.wantFound {
   627  			continue
   628  		}
   629  		c := c
   630  		ok := t.Run(c.name, func(t *testing.T) { runTC(c, t) })
   631  		if !ok {
   632  			break
   633  		}
   634  	}
   635  
   636  }