github.com/decred/dcrlnd@v0.7.6/chainscan/csdrivers/dcrwdriver_test.go (about) 1 package csdrivers 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "testing" 9 "time" 10 11 "decred.org/dcrwallet/v4/rpc/walletrpc" 12 "github.com/decred/dcrd/chaincfg/chainhash" 13 "github.com/decred/dcrd/chaincfg/v3" 14 "github.com/decred/dcrd/gcs/v4" 15 "github.com/decred/dcrlnd/chainscan" 16 "github.com/decred/dcrlnd/internal/testutils" 17 "github.com/decred/dcrlnd/lntest/wait" 18 rpctest "github.com/decred/dcrtest/dcrdtest" 19 ) 20 21 var ( 22 defaultTimeout = 5 * time.Second 23 ) 24 25 type runnable interface { 26 Run(context.Context) error 27 } 28 29 type testHarness struct { 30 testutils.TB 31 32 d interface{} // driver 33 miner *rpctest.Harness 34 vw *rpctest.VotingWallet 35 } 36 37 func (t *testHarness) generate(nb uint32) []*chainhash.Hash { 38 t.Helper() 39 40 bls, err := t.vw.GenerateBlocks(context.Background(), nb) 41 if err != nil { 42 t.Fatalf("unable to generate %d blocks: %v", nb, err) 43 } 44 return bls 45 } 46 47 // assertMinerBlockHeightDelta ensures that tempMiner is 'delta' blocks ahead 48 // of miner. 49 func assertMinerBlockHeightDelta(t *testHarness, 50 miner, tempMiner *rpctest.Harness, delta int64) { 51 52 ctxb := context.Background() 53 54 // Ensure the chain lengths are what we expect. 55 var predErr error 56 err := wait.Predicate(func() bool { 57 _, tempMinerHeight, err := tempMiner.Node.GetBestBlock(ctxb) 58 if err != nil { 59 predErr = fmt.Errorf("unable to get current "+ 60 "blockheight %v", err) 61 return false 62 } 63 64 _, minerHeight, err := miner.Node.GetBestBlock(ctxb) 65 if err != nil { 66 predErr = fmt.Errorf("unable to get current "+ 67 "blockheight %v", err) 68 return false 69 } 70 71 if tempMinerHeight != minerHeight+delta { 72 predErr = fmt.Errorf("expected new miner(%d) to be %d "+ 73 "blocks ahead of original miner(%d)", 74 tempMinerHeight, delta, minerHeight) 75 return false 76 } 77 return true 78 }, time.Second*15) 79 if err != nil { 80 t.Fatalf(predErr.Error()) 81 } 82 } 83 84 func assertMatchesMinerCF(t *testHarness, bh *chainhash.Hash, key [16]byte, filter *gcs.FilterV2) { 85 t.Helper() 86 ctxb := context.Background() 87 88 resp, err := t.miner.Node.GetCFilterV2(ctxb, bh) 89 if err != nil { 90 t.Fatal(err) 91 } 92 if !bytes.Equal(resp.Filter.Bytes(), filter.Bytes()) { 93 t.Fatal("filter bytes do not match") 94 } 95 mbl, err := t.miner.Node.GetBlock(ctxb, bh) 96 if err != nil { 97 t.Fatal(err) 98 } 99 if !bytes.Equal(mbl.Header.MerkleRoot[:16], key[:]) { 100 t.Fatal("key does not match") 101 } 102 } 103 104 func testCurrentTip(t *testHarness) { 105 d := t.d.(chainscan.ChainSource) 106 107 // Generate 5 blocks. The tip should match in every one. 108 for i := 0; i < 5; i++ { 109 err := wait.NoError(func() error { 110 ctxt, cancel := context.WithTimeout(context.Background(), defaultTimeout) 111 defer cancel() 112 bh, h, err := d.CurrentTip(ctxt) 113 if err != nil { 114 return fmt.Errorf("unable to get current tip: %v", err) 115 } 116 117 // Compare to current miner tip. 118 hash, height, err := t.miner.Node.GetBestBlock(ctxt) 119 if err != nil { 120 return fmt.Errorf("unable to get best block: %v", err) 121 } 122 if int32(height) != h { 123 return fmt.Errorf("unexpected tip height. want=%d got=%d", height, h) 124 } 125 if *bh != *hash { 126 return fmt.Errorf("unexpected tip hash. want=%s got=%s", hash, bh) 127 } 128 129 return nil 130 }, defaultTimeout) 131 if err != nil { 132 t.Fatal(err) 133 } 134 135 t.generate(1) 136 } 137 } 138 139 func testGetCFilters(t *testHarness) { 140 d := t.d.(chainscan.HistoricalChainSource) 141 142 // Fetch a bunch of cfilters and compare it to the miner returned ones. 143 _, tipHeight, err := t.miner.Node.GetBestBlock(context.Background()) 144 if err != nil { 145 t.Fatal(err) 146 } 147 148 for height := tipHeight - 10; height <= tipHeight; height++ { 149 ctxt, cancel := context.WithTimeout(context.Background(), defaultTimeout) 150 defer cancel() 151 bh, key, filter, err := d.GetCFilter(ctxt, int32(height)) 152 if err != nil { 153 t.Fatalf("unable to get cfilter: %v", err) 154 } 155 156 // Compare to the miner. 157 mbh, err := t.miner.Node.GetBlockHash(context.Background(), height) 158 if err != nil { 159 t.Fatal(err) 160 } 161 if *mbh != *bh { 162 t.Fatalf("unexpected block hash at height %d. want=%s got=%s", 163 height, mbh, bh) 164 } 165 assertMatchesMinerCF(t, bh, key, filter) 166 } 167 168 // Requesting a cfilter for a block past tip should return 169 // ErrBlockAfterTip. 170 ctxt, cancel := context.WithTimeout(context.Background(), defaultTimeout) 171 defer cancel() 172 _, _, _, err = d.GetCFilter(ctxt, int32(tipHeight+1)) 173 if !errors.Is(err, chainscan.ErrBlockAfterTip{}) { 174 t.Fatalf("unexpected error at tipHeight+1. want=%v got=%v", 175 chainscan.ErrBlockAfterTip{}, err) 176 } 177 178 // Requesting a cfilter for tip again shouldn't error. 179 ctxt, cancel = context.WithTimeout(context.Background(), defaultTimeout) 180 defer cancel() 181 _, _, _, err = d.GetCFilter(ctxt, int32(tipHeight)) 182 if err != nil { 183 t.Fatalf("unexpected error at tipHeight. want=%v got=%v", 184 nil, err) 185 } 186 } 187 188 func testGetBlock(t *testHarness) { 189 d := t.d.(chainscan.ChainSource) 190 191 ctxb := context.Background() 192 _, tipHeight, err := t.miner.Node.GetBestBlock(ctxb) 193 if err != nil { 194 t.Fatal(err) 195 } 196 197 // Fetch a bunch of blocks near tipHeight and ensure they match the 198 // ones from the miner. 199 for height := tipHeight - 5; height <= tipHeight; height++ { 200 mbh, err := t.miner.Node.GetBlockHash(ctxb, height) 201 if err != nil { 202 t.Fatal(err) 203 } 204 205 mbl, err := t.miner.Node.GetBlock(ctxb, mbh) 206 if err != nil { 207 t.Fatal(err) 208 } 209 210 ctxt, cancel := context.WithTimeout(context.Background(), defaultTimeout) 211 defer cancel() 212 bl, err := d.GetBlock(ctxt, mbh) 213 if err != nil { 214 t.Fatalf("unable to get block: %v", err) 215 } 216 217 blBytes, err := bl.Bytes() 218 if err != nil { 219 t.Fatal(err) 220 } 221 mblBytes, err := mbl.Bytes() 222 if err != nil { 223 t.Fatal(err) 224 } 225 if !bytes.Equal(blBytes, mblBytes) { 226 t.Fatalf("bytes from miner block do not equal bytes from driver block") 227 } 228 } 229 } 230 231 func testChainEvents(t *testHarness) { 232 d := t.d.(chainscan.TipChainSource) 233 if r, isRunnable := t.d.(runnable); isRunnable { 234 runCtx, cancelRun := context.WithCancel(context.Background()) 235 go r.Run(runCtx) 236 defer cancelRun() 237 } 238 239 _, tipHeight, err := t.miner.Node.GetBestBlock(context.Background()) 240 if err != nil { 241 t.Fatal(err) 242 } 243 244 var ( 245 bh *chainhash.Hash 246 height int32 247 key [16]byte 248 filter *gcs.FilterV2 249 ) 250 251 eventsCtx, cancel := context.WithCancel(context.Background()) 252 defer cancel() 253 events := d.ChainEvents(eventsCtx) 254 255 // Repeat the test 5 times. 256 for i := int32(0); i < 5; i++ { 257 mbh := t.generate(1)[0] 258 select { 259 case ce := <-events: 260 e := ce.(chainscan.BlockConnectedEvent) 261 bh, height = e.BlockHash(), e.BlockHeight() 262 key, filter = e.CFKey, e.Filter 263 case <-time.After(defaultTimeout): 264 t.Fatalf("timeout waiting for block %d", i) 265 } 266 if err != nil { 267 t.Fatalf("unexpected error: %v", err) 268 } 269 270 if *bh != *mbh { 271 t.Fatalf("unexpected block hash. want=%s got=%s", 272 mbh, bh) 273 } 274 if height != int32(tipHeight)+i+1 { 275 t.Fatalf("unexpected height. want=%d got=%d", 276 int32(tipHeight)+i+1, height) 277 } 278 279 assertMatchesMinerCF(t, bh, key, filter) 280 } 281 } 282 283 // tests that using nextTip() when a reorg happens makes the driver get all new 284 // (reorged in) blocks. 285 func testChainEventsWithReorg(t *testHarness) { 286 d := t.d.(chainscan.TipChainSource) 287 if r, isRunnable := t.d.(runnable); isRunnable { 288 runCtx, cancelRun := context.WithCancel(context.Background()) 289 go r.Run(runCtx) 290 defer cancelRun() 291 } 292 293 _, tipHeight, err := t.miner.Node.GetBestBlock(context.Background()) 294 if err != nil { 295 t.Fatal(err) 296 } 297 298 // Create a second miner. 299 ctxb := context.Background() 300 netParams := chaincfg.SimNetParams() 301 tempMinerDir := ".dcrd-alt-miner" 302 tempMinerArgs := []string{"--debuglevel=debug", "--logdir=" + tempMinerDir} 303 tempMiner, err := rpctest.New(t.TB.(*testing.T), netParams, nil, tempMinerArgs) 304 if err != nil { 305 t.Fatal(err) 306 } 307 err = tempMiner.SetUp(ctxb, false, 0) 308 if err != nil { 309 t.Fatal(err) 310 } 311 defer tempMiner.TearDown() 312 313 // Connect the temp miner with the orignal test miner and let them sync 314 // up. 315 if err := rpctest.ConnectNode(ctxb, t.miner, tempMiner); err != nil { 316 t.Fatalf("unable to connect harnesses: %v", err) 317 } 318 nodeSlice := []*rpctest.Harness{t.miner, tempMiner} 319 if err := rpctest.JoinNodes(ctxb, nodeSlice, rpctest.Blocks); err != nil { 320 t.Fatalf("unable to join node on blocks: %v", err) 321 } 322 323 // The two miners should be on the same blockheight. 324 assertMinerBlockHeightDelta(t, t.miner, tempMiner, 0) 325 326 // Disconnect both nodes. 327 err = rpctest.RemoveNode(ctxb, t.miner, tempMiner) 328 if err != nil { 329 t.Fatalf("unable to remove node: %v", err) 330 } 331 332 // Create the chain events channel. 333 ctx, cancel := context.WithCancel(context.Background()) 334 defer cancel() 335 events := d.ChainEvents(ctx) 336 337 // Mine 3 blocks in the original miner and 6 in the temp miner. 338 t.generate(3) 339 _, err = rpctest.AdjustedSimnetMiner(context.Background(), tempMiner.Node, 6) 340 if err != nil { 341 t.Fatal(err) 342 } 343 344 // The two miners should be on different blockheights. 345 assertMinerBlockHeightDelta(t, t.miner, tempMiner, 3) 346 347 var ( 348 bh *chainhash.Hash 349 height int32 350 key [16]byte 351 filter *gcs.FilterV2 352 ) 353 354 // We should get 3 new tips when calling NextTip() 355 for i := int32(0); i < 3; i++ { 356 select { 357 case ce := <-events: 358 e := ce.(chainscan.BlockConnectedEvent) 359 bh, height = e.BlockHash(), e.BlockHeight() 360 key, filter = e.CFKey, e.Filter 361 case <-time.After(defaultTimeout): 362 t.Fatalf("timeout waiting for block %d", i) 363 } 364 365 if height != int32(tipHeight)+i+1 { 366 t.Fatalf("unexpected height. want=%d got=%d", 367 int32(tipHeight)+i+1, height) 368 } 369 370 assertMatchesMinerCF(t, bh, key, filter) 371 } 372 373 // Re-connect the miners. This should cause 6 news blocks to be 374 // connected (including ones for heights the we already just checked 375 // were connected). 376 if err := rpctest.ConnectNode(ctxb, t.miner, tempMiner); err != nil { 377 t.Fatalf("unable to connect harnesses: %v", err) 378 } 379 if err := rpctest.JoinNodes(ctxb, nodeSlice, rpctest.Blocks); err != nil { 380 t.Fatalf("unable to join node on blocks: %v", err) 381 } 382 assertMinerBlockHeightDelta(t, t.miner, tempMiner, 0) 383 384 // We should get 3 BlockDisconnected events from the old chain. 385 for i := int32(0); i < 3; i++ { 386 select { 387 case ce := <-events: 388 e := ce.(chainscan.BlockDisconnectedEvent) 389 _, height = e.BlockHash(), e.BlockHeight() 390 wantHeight := int32(tipHeight) + 3 - i 391 if height != wantHeight { 392 t.Fatalf("unexpected BlockDisconnectedEvent "+ 393 "height. want=%d got=%d", wantHeight, 394 height) 395 } 396 case <-time.After(defaultTimeout): 397 t.Fatalf("timeout waiting for block disconnect %d", i) 398 } 399 } 400 401 // We should get 6 new tips when calling NextTip() 402 for i := int32(0); i < 6; i++ { 403 select { 404 case ce := <-events: 405 e := ce.(chainscan.BlockConnectedEvent) 406 bh, height = e.BlockHash(), e.BlockHeight() 407 key, filter = e.CFKey, e.Filter 408 case <-time.After(defaultTimeout): 409 t.Fatalf("timeout waiting for block %d", i) 410 } 411 412 // This is the important bit of this test. We've never reset 413 // tipHeight, therefore we should obverve again 414 // tipHeight+1..tipHeight+1+3. 415 if height != int32(tipHeight)+i+1 { 416 t.Fatalf("unexpected height. want=%d got=%d", 417 int32(tipHeight)+i+1, height) 418 } 419 420 assertMatchesMinerCF(t, bh, key, filter) 421 } 422 } 423 424 func setupTestChain(t testutils.TB, testName string) (*rpctest.Harness, *rpctest.VotingWallet, func()) { 425 tearDown := func() {} 426 defer func() { 427 if t.Failed() { 428 tearDown() 429 } 430 }() 431 432 ctxb := context.Background() 433 netParams := chaincfg.SimNetParams() 434 minerLogDir := fmt.Sprintf(".dcrd-%s", testName) 435 minerArgs := []string{"--debuglevel=debug", "--logdir=" + minerLogDir} 436 miner, err := rpctest.New(t.(*testing.T), netParams, nil, minerArgs) 437 if err != nil { 438 t.Fatal(err) 439 } 440 err = miner.SetUp(ctxb, false, 0) 441 if err != nil { 442 t.Fatal(err) 443 } 444 tearDown = func() { 445 miner.TearDown() 446 } 447 448 _, err = miner.Node.Generate(context.Background(), 1) 449 if err != nil { 450 t.Fatal(err) 451 } 452 453 _, err = rpctest.AdjustedSimnetMiner(context.Background(), miner.Node, 64) 454 if err != nil { 455 t.Fatal(err) 456 } 457 458 // Setup a voting wallet for when the chain passes SVH. 459 vwCtx, vwCancel := context.WithCancel(ctxb) 460 vw, err := rpctest.NewVotingWallet(vwCtx, miner) 461 if err != nil { 462 t.Fatalf("unable to create voting wallet: %v", err) 463 } 464 vw.SetErrorReporting(func(err error) { 465 t.Logf("Voting wallet error: %v", err) 466 }) 467 vw.SetMiner(func(ctx context.Context, nb uint32) ([]*chainhash.Hash, error) { 468 return rpctest.AdjustedSimnetMiner(ctx, miner.Node, nb) 469 }) 470 if err = vw.Start(vwCtx); err != nil { 471 t.Fatalf("unable to start voting wallet: %v", err) 472 } 473 tearDown = func() { 474 vwCancel() 475 miner.TearDown() 476 } 477 478 return miner, vw, tearDown 479 } 480 481 type testCase struct { 482 name string 483 f func(*testHarness) 484 } 485 486 var testCases = []testCase{ 487 // The reorg test needs to be the first one to ensure voting 488 // doesn't need to be taken into account. 489 { 490 name: "ChainEvents with reorg", 491 f: testChainEventsWithReorg, 492 }, 493 { 494 name: "CurrentTip", 495 f: testCurrentTip, 496 }, 497 { 498 name: "GetCFilters", 499 f: testGetCFilters, 500 }, 501 { 502 name: "GetBlock", 503 f: testGetBlock, 504 }, 505 { 506 name: "ChainEvents", 507 f: testChainEvents, 508 }, 509 } 510 511 func TestDcrwalletCSDriver(t *testing.T) { 512 miner, vw, tearDownMiner := setupTestChain(t, "dcwallet-csd") 513 defer tearDownMiner() 514 515 rpcConfig := miner.RPCConfig() 516 w, tearDownWallet := testutils.NewRPCSyncingTestWallet(t, &rpcConfig) 517 defer tearDownWallet() 518 519 for _, tc := range testCases { 520 tc := tc 521 succ := t.Run(tc.name, func(t *testing.T) { 522 d := NewDcrwalletCSDriver(w, nil) 523 524 // Lower the cache size so we're sure to trigger cases 525 // where the cache is both used and filled. 526 d.cache = make([]cfilter, 3) 527 528 th := &testHarness{ 529 d: d, 530 TB: t, 531 miner: miner, 532 vw: vw, 533 } 534 tc.f(th) 535 }) 536 if !succ { 537 break 538 } 539 } 540 } 541 542 func TestRemoteDcrwalletCSDriver(t *testing.T) { 543 miner, vw, tearDownMiner := setupTestChain(t, "remotewallet-csd") 544 defer tearDownMiner() 545 546 rpcConfig := miner.RPCConfig() 547 conn, tearDownWallet := testutils.NewRPCSyncingTestRemoteDcrwallet(t, &rpcConfig) 548 wsvc := walletrpc.NewWalletServiceClient(conn) 549 nsvc := walletrpc.NewNetworkServiceClient(conn) 550 defer tearDownWallet() 551 552 for _, tc := range testCases { 553 tc := tc 554 succ := t.Run(tc.name, func(t *testing.T) { 555 d := NewRemoteWalletCSDriver(wsvc, nsvc, nil) 556 557 // Lower the cache size so we're sure to trigger cases 558 // where the cache is both used and filled. 559 d.cache = make([]cfilter, 3) 560 561 th := &testHarness{ 562 d: d, 563 TB: t, 564 miner: miner, 565 vw: vw, 566 } 567 tc.f(th) 568 }) 569 if !succ { 570 break 571 } 572 } 573 } 574 575 // BenchmarkDcrwalletCSDriver benchmarks a series of GetCFilter calls. 576 // 577 // This ends up mostly testing your IO performnace. Note that you might want to 578 // run with `-benchtime=500x` to prevent the benchmark runtime from generating 579 // a large N (and therefore a large chain). 580 func BenchmarkDcrwalletCSDriver(b *testing.B) { 581 miner, vw, tearDownMiner := setupTestChain(b, "dcrwallet-bench-csd") 582 defer tearDownMiner() 583 584 rpcConfig := miner.RPCConfig() 585 w, tearDownWallet := testutils.NewRPCSyncingTestWallet(b, &rpcConfig) 586 defer tearDownWallet() 587 588 d := NewDcrwalletCSDriver(w, nil) 589 th := &testHarness{ 590 d: d, 591 TB: b, 592 miner: miner, 593 vw: vw, 594 } 595 596 _, tipHeight, err := th.miner.Node.GetBestBlock(context.Background()) 597 if err != nil { 598 th.Fatal(err) 599 } 600 601 targetBlockCount := int32(b.N) 602 if int32(tipHeight) < targetBlockCount { 603 th.generate(uint32(targetBlockCount - int32(tipHeight))) 604 } 605 606 ctxt, cancel := context.WithTimeout(context.Background(), defaultTimeout*5) 607 defer cancel() 608 609 b.ReportAllocs() 610 b.ResetTimer() 611 612 for height := int32(0); height < targetBlockCount; height++ { 613 _, _, _, err := d.GetCFilter(ctxt, height) 614 if err != nil { 615 th.Fatalf("unable to get cfilter: %v", err) 616 } 617 } 618 }