gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/miner/miner_test.go (about)

     1  package miner
     2  
     3  import (
     4  	"bytes"
     5  	"path/filepath"
     6  	"testing"
     7  	"time"
     8  	"unsafe"
     9  
    10  	"gitlab.com/SiaPrime/SiaPrime/build"
    11  	"gitlab.com/SiaPrime/SiaPrime/crypto"
    12  	"gitlab.com/SiaPrime/SiaPrime/modules"
    13  	"gitlab.com/SiaPrime/SiaPrime/modules/consensus"
    14  	"gitlab.com/SiaPrime/SiaPrime/modules/gateway"
    15  	"gitlab.com/SiaPrime/SiaPrime/modules/transactionpool"
    16  	"gitlab.com/SiaPrime/SiaPrime/modules/wallet"
    17  	"gitlab.com/SiaPrime/SiaPrime/types"
    18  )
    19  
    20  // A minerTester is the helper object for miner testing.
    21  type minerTester struct {
    22  	gateway   modules.Gateway
    23  	cs        modules.ConsensusSet
    24  	tpool     modules.TransactionPool
    25  	wallet    modules.Wallet
    26  	walletKey crypto.CipherKey
    27  
    28  	miner *Miner
    29  
    30  	minedBlocks []types.Block
    31  	persistDir  string
    32  }
    33  
    34  // createMinerTester creates a minerTester that's ready for use.
    35  func createMinerTester(name string) (*minerTester, error) {
    36  	testdir := build.TempDir(modules.MinerDir, name)
    37  
    38  	// Create the modules.
    39  	g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir))
    40  	if err != nil {
    41  		return nil, err
    42  	}
    43  	cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir))
    44  	if err != nil {
    45  		return nil, err
    46  	}
    47  	tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir))
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  	w, err := wallet.New(cs, tp, filepath.Join(testdir, modules.WalletDir))
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  	key := crypto.GenerateSiaKey(crypto.TypeDefaultWallet)
    56  	_, err = w.Encrypt(key)
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  	err = w.Unlock(key)
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  	m, err := New(cs, tp, w, filepath.Join(testdir, modules.MinerDir))
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  
    69  	// Assemble the minerTester.
    70  	mt := &minerTester{
    71  		gateway:   g,
    72  		cs:        cs,
    73  		tpool:     tp,
    74  		wallet:    w,
    75  		walletKey: key,
    76  
    77  		miner: m,
    78  
    79  		persistDir: testdir,
    80  	}
    81  
    82  	// Mine until the wallet has money.
    83  	for i := types.BlockHeight(0); i <= types.MaturityDelay; i++ {
    84  		b, err := m.AddBlock()
    85  		if err != nil {
    86  			return nil, err
    87  		}
    88  		mt.minedBlocks = append(mt.minedBlocks, b)
    89  	}
    90  
    91  	return mt, nil
    92  }
    93  
    94  // TestIntegrationMiner creates a miner, mines a few blocks, and checks that
    95  // the wallet balance is updating as the blocks get mined.
    96  func TestIntegrationMiner(t *testing.T) {
    97  	if testing.Short() {
    98  		t.SkipNow()
    99  	}
   100  	mt, err := createMinerTester(t.Name())
   101  	if err != nil {
   102  		t.Fatal(err)
   103  	}
   104  
   105  	// Check that the wallet has money.
   106  	siacoins, _, _, err := mt.wallet.ConfirmedBalance()
   107  	if err != nil {
   108  		t.Error(err)
   109  	}
   110  	if siacoins.IsZero() {
   111  		t.Error("expecting mining full balance to not be zero")
   112  	}
   113  
   114  	// Mine a bunch of blocks.
   115  	for i := 0; i < 50; i++ {
   116  		b, _ := mt.miner.FindBlock()
   117  		err = mt.cs.AcceptBlock(b)
   118  		if err != nil {
   119  			t.Fatal(err)
   120  		}
   121  	}
   122  	morecoins, _, _, err := mt.wallet.ConfirmedBalance()
   123  	if err != nil {
   124  		t.Error(err)
   125  	}
   126  	if siacoins.Cmp(morecoins) >= 0 {
   127  		t.Error("wallet is not gaining balance while mining")
   128  	}
   129  }
   130  
   131  // TestIntegrationNilMinerDependencies tests that the miner properly handles
   132  // nil inputs for its dependencies.
   133  func TestIntegrationNilMinerDependencies(t *testing.T) {
   134  	if testing.Short() {
   135  		t.SkipNow()
   136  	}
   137  	mt, err := createMinerTester(t.Name())
   138  	if err != nil {
   139  		t.Fatal(err)
   140  	}
   141  	_, err = New(mt.cs, mt.tpool, nil, "")
   142  	if err != errNilWallet {
   143  		t.Fatal(err)
   144  	}
   145  	_, err = New(mt.cs, nil, mt.wallet, "")
   146  	if err != errNilTpool {
   147  		t.Fatal(err)
   148  	}
   149  	_, err = New(nil, mt.tpool, mt.wallet, "")
   150  	if err != errNilCS {
   151  		t.Fatal(err)
   152  	}
   153  	_, err = New(nil, nil, nil, "")
   154  	if err == nil {
   155  		t.Fatal(err)
   156  	}
   157  }
   158  
   159  // TestIntegrationBlocksMined checks that the BlocksMined function correctly
   160  // indicates the number of real blocks and stale blocks that have been mined.
   161  func TestIntegrationBlocksMined(t *testing.T) {
   162  	if testing.Short() {
   163  		t.SkipNow()
   164  	}
   165  	mt, err := createMinerTester(t.Name())
   166  	if err != nil {
   167  		t.Fatal(err)
   168  	}
   169  
   170  	// Get an unsolved header.
   171  	unsolvedHeader, target, err := mt.miner.HeaderForWork()
   172  	if err != nil {
   173  		t.Fatal(err)
   174  	}
   175  	// Solve the header - necessary because the target is very low when
   176  	// mining.
   177  	for {
   178  		*(*uint64)(unsafe.Pointer(&unsolvedHeader.Nonce)) += types.ASICHardforkFactor
   179  		id := crypto.HashObject(unsolvedHeader)
   180  		if bytes.Compare(target[:], id[:]) < 0 {
   181  			break
   182  		}
   183  	}
   184  
   185  	// Get two solved headers.
   186  	header1, target, err := mt.miner.HeaderForWork()
   187  	if err != nil {
   188  		t.Fatal(err)
   189  	}
   190  	header1 = solveHeader(header1, target)
   191  	header2, target, err := mt.miner.HeaderForWork()
   192  	if err != nil {
   193  		t.Fatal(err)
   194  	}
   195  	header2 = solveHeader(header2, target)
   196  
   197  	// Submit the unsolved header followed by the two solved headers, this
   198  	// should result in 1 real block mined and 1 stale block mined.
   199  	err = mt.miner.SubmitHeader(unsolvedHeader)
   200  	if err != modules.ErrBlockUnsolved {
   201  		t.Fatal(err)
   202  	}
   203  	err = mt.miner.SubmitHeader(header1)
   204  	if err != nil {
   205  		t.Fatal(err)
   206  	}
   207  	err = mt.miner.SubmitHeader(header2)
   208  	if err != modules.ErrNonExtendingBlock {
   209  		t.Fatal(err)
   210  	}
   211  	goodBlocks, staleBlocks := mt.miner.BlocksMined()
   212  	if goodBlocks != 1 {
   213  		t.Error("expecting 1 good block")
   214  	}
   215  	if staleBlocks != 1 {
   216  		t.Error("expecting 1 stale block, got", staleBlocks)
   217  	}
   218  
   219  	// Reboot the miner and verify that the block record has persisted.
   220  	err = mt.miner.Close()
   221  	if err != nil {
   222  		t.Fatal(err)
   223  	}
   224  	rebootMiner, err := New(mt.cs, mt.tpool, mt.wallet, filepath.Join(mt.persistDir, modules.MinerDir))
   225  	if err != nil {
   226  		t.Fatal(err)
   227  	}
   228  	goodBlocks, staleBlocks = rebootMiner.BlocksMined()
   229  	if goodBlocks != 1 {
   230  		t.Error("expecting 1 good block")
   231  	}
   232  	if staleBlocks != 1 {
   233  		t.Error("expecting 1 stale block, got", staleBlocks)
   234  	}
   235  }
   236  
   237  // TestIntegrationAutoRescan triggers a rescan during a call to New and
   238  // verifies that the rescanning happens correctly. The rescan is triggered by
   239  // a call to New, instead of getting called directly.
   240  func TestIntegrationAutoRescan(t *testing.T) {
   241  	if testing.Short() {
   242  		t.SkipNow()
   243  	}
   244  	mt, err := createMinerTester(t.Name())
   245  	if err != nil {
   246  		t.Fatal(err)
   247  	}
   248  	_, err = mt.miner.AddBlock()
   249  	if err != nil {
   250  		t.Fatal(err)
   251  	}
   252  
   253  	// Get the persist data of the current miner.
   254  	oldChange := mt.miner.persist.RecentChange
   255  	oldHeight := mt.miner.persist.Height
   256  	oldTarget := mt.miner.persist.Target
   257  
   258  	// Corrupt the miner, close the miner, and make a new one from the same
   259  	// directory.
   260  	mt.miner.persist.RecentChange[0]++
   261  	mt.miner.persist.Height += 1e5
   262  	mt.miner.persist.Target[0]++
   263  	err = mt.miner.Close() // miner saves when it closes.
   264  	if err != nil {
   265  		t.Fatal(err)
   266  	}
   267  
   268  	// Verify that rescanning resolved the corruption in the miner.
   269  	m, err := New(mt.cs, mt.tpool, mt.wallet, filepath.Join(mt.persistDir, modules.MinerDir))
   270  	if err != nil {
   271  		t.Fatal(err)
   272  	}
   273  	// Check that after rescanning, the values have returned to the usual values.
   274  	if m.persist.RecentChange != oldChange {
   275  		t.Error("rescan failed, ended up on the wrong change")
   276  	}
   277  	if m.persist.Height != oldHeight {
   278  		t.Error("rescan failed, ended up at the wrong height")
   279  	}
   280  	if m.persist.Target != oldTarget {
   281  		t.Error("rescan failed, ended up at the wrong target")
   282  	}
   283  }
   284  
   285  // TestIntegrationStartupRescan probes the startupRescan function, checking
   286  // that it works in the naive case. Rescan is called directly.
   287  func TestIntegrationStartupRescan(t *testing.T) {
   288  	if testing.Short() {
   289  		t.SkipNow()
   290  	}
   291  	mt, err := createMinerTester(t.Name())
   292  	if err != nil {
   293  		t.Fatal(err)
   294  	}
   295  
   296  	// Check that the miner's persist variables have been initialized to the
   297  	// first few blocks.
   298  	if mt.miner.persist.RecentChange == (modules.ConsensusChangeID{}) || mt.miner.persist.Height == 0 || mt.miner.persist.Target == (types.Target{}) {
   299  		t.Fatal("miner persist variables not initialized")
   300  	}
   301  	oldChange := mt.miner.persist.RecentChange
   302  	oldHeight := mt.miner.persist.Height
   303  	oldTarget := mt.miner.persist.Target
   304  
   305  	// Corrupt the miner and verify that a rescan repairs the corruption.
   306  	mt.miner.persist.RecentChange[0]++
   307  	mt.miner.persist.Height += 500
   308  	mt.miner.persist.Target[0]++
   309  	mt.cs.Unsubscribe(mt.miner)
   310  	err = mt.miner.startupRescan()
   311  	if err != nil {
   312  		t.Fatal(err)
   313  	}
   314  	if mt.miner.persist.RecentChange != oldChange {
   315  		t.Error("rescan failed, ended up on the wrong change")
   316  	}
   317  	if mt.miner.persist.Height != oldHeight {
   318  		t.Error("rescan failed, ended up at the wrong height")
   319  	}
   320  	if mt.miner.persist.Target != oldTarget {
   321  		t.Error("rescan failed, ended up at the wrong target")
   322  	}
   323  }
   324  
   325  // TestMinerCloseDeadlock checks that the miner can cleanly close even if the
   326  // CPU miner is running.
   327  func TestMinerCloseDeadlock(t *testing.T) {
   328  	if testing.Short() {
   329  		t.SkipNow()
   330  	}
   331  	mt, err := createMinerTester(t.Name())
   332  	if err != nil {
   333  		t.Fatal(err)
   334  	}
   335  	// StartCPUMining calls `go threadedMine()`, which needs to access the miner
   336  	// before Close() does in the next goroutine, otherwise m.tg.Add() fails
   337  	// at the top of threadedMine() and threadedMine() exits (silently!).
   338  	// I haven't seen this behavior since sticking Close() inside a goroutine,
   339  	// but I'm not sure that's comfort enough.
   340  	mt.miner.StartCPUMining()
   341  	time.Sleep(time.Millisecond * 250)
   342  
   343  	closed := make(chan struct{})
   344  	go func() {
   345  		if err := mt.miner.Close(); err != nil {
   346  			t.Fatal(err)
   347  		}
   348  		closed <- struct{}{}
   349  	}()
   350  	select {
   351  	case <-closed:
   352  	case <-time.After(5 * time.Second):
   353  		t.Fatal("mt.miner.Close never completed")
   354  	}
   355  }