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 }