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  }