github.com/Synthesix/Sia@v1.3.3-0.20180413141344-f863baeed3ca/modules/renter/contractor/contractor_test.go (about)

     1  package contractor
     2  
     3  import (
     4  	"errors"
     5  	"os"
     6  	"reflect"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/Synthesix/Sia/build"
    11  	"github.com/Synthesix/Sia/modules"
    12  	"github.com/Synthesix/Sia/types"
    13  )
    14  
    15  // newStub is used to test the New function. It implements all of the contractor's
    16  // dependencies.
    17  type newStub struct{}
    18  
    19  // consensus set stubs
    20  func (newStub) ConsensusSetSubscribe(modules.ConsensusSetSubscriber, modules.ConsensusChangeID, <-chan struct{}) error {
    21  	return nil
    22  }
    23  func (newStub) Synced() bool                               { return true }
    24  func (newStub) Unsubscribe(modules.ConsensusSetSubscriber) { return }
    25  
    26  // wallet stubs
    27  func (newStub) NextAddress() (uc types.UnlockConditions, err error) { return }
    28  func (newStub) StartTransaction() modules.TransactionBuilder        { return nil }
    29  
    30  // transaction pool stubs
    31  func (newStub) AcceptTransactionSet([]types.Transaction) error      { return nil }
    32  func (newStub) FeeEstimation() (a types.Currency, b types.Currency) { return }
    33  
    34  // hdb stubs
    35  func (newStub) AllHosts() []modules.HostDBEntry                                 { return nil }
    36  func (newStub) ActiveHosts() []modules.HostDBEntry                              { return nil }
    37  func (newStub) Host(types.SiaPublicKey) (settings modules.HostDBEntry, ok bool) { return }
    38  func (newStub) IncrementSuccessfulInteractions(key types.SiaPublicKey)          { return }
    39  func (newStub) IncrementFailedInteractions(key types.SiaPublicKey)              { return }
    40  func (newStub) RandomHosts(int, []types.SiaPublicKey) []modules.HostDBEntry     { return nil }
    41  func (newStub) ScoreBreakdown(modules.HostDBEntry) modules.HostScoreBreakdown {
    42  	return modules.HostScoreBreakdown{}
    43  }
    44  
    45  // TestNew tests the New function.
    46  func TestNew(t *testing.T) {
    47  	if testing.Short() {
    48  		t.SkipNow()
    49  	}
    50  	// Using a stub implementation of the dependencies is fine, as long as its
    51  	// non-nil.
    52  	var stub newStub
    53  	dir := build.TempDir("contractor", t.Name())
    54  
    55  	// Sane values.
    56  	_, err := New(stub, stub, stub, stub, dir)
    57  	if err != nil {
    58  		t.Fatalf("expected nil, got %v", err)
    59  	}
    60  
    61  	// Nil consensus set.
    62  	_, err = New(nil, stub, stub, stub, dir)
    63  	if err != errNilCS {
    64  		t.Fatalf("expected %v, got %v", errNilCS, err)
    65  	}
    66  
    67  	// Nil wallet.
    68  	_, err = New(stub, nil, stub, stub, dir)
    69  	if err != errNilWallet {
    70  		t.Fatalf("expected %v, got %v", errNilWallet, err)
    71  	}
    72  
    73  	// Nil transaction pool.
    74  	_, err = New(stub, stub, nil, stub, dir)
    75  	if err != errNilTpool {
    76  		t.Fatalf("expected %v, got %v", errNilTpool, err)
    77  	}
    78  
    79  	// Bad persistDir.
    80  	_, err = New(stub, stub, stub, stub, "")
    81  	if !os.IsNotExist(err) {
    82  		t.Fatalf("expected invalid directory, got %v", err)
    83  	}
    84  }
    85  
    86  // TestResolveID tests the ResolveID method.
    87  func TestResolveID(t *testing.T) {
    88  	c := &Contractor{
    89  		renewedIDs: map[types.FileContractID]types.FileContractID{
    90  			{1}: {2},
    91  			{2}: {3},
    92  			{3}: {4},
    93  			{5}: {6},
    94  		},
    95  	}
    96  	tests := []struct {
    97  		id       types.FileContractID
    98  		resolved types.FileContractID
    99  	}{
   100  		{types.FileContractID{0}, types.FileContractID{0}},
   101  		{types.FileContractID{1}, types.FileContractID{4}},
   102  		{types.FileContractID{2}, types.FileContractID{4}},
   103  		{types.FileContractID{3}, types.FileContractID{4}},
   104  		{types.FileContractID{4}, types.FileContractID{4}},
   105  		{types.FileContractID{5}, types.FileContractID{6}},
   106  	}
   107  	for _, test := range tests {
   108  		if r := c.ResolveID(test.id); r != test.resolved {
   109  			t.Errorf("expected %v -> %v, got %v", test.id, test.resolved, r)
   110  		}
   111  	}
   112  }
   113  
   114  // TestAllowance tests the Allowance method.
   115  func TestAllowance(t *testing.T) {
   116  	c := &Contractor{
   117  		allowance: modules.Allowance{
   118  			Funds:  types.NewCurrency64(1),
   119  			Period: 2,
   120  			Hosts:  3,
   121  		},
   122  	}
   123  	a := c.Allowance()
   124  	if a.Funds.Cmp(c.allowance.Funds) != 0 ||
   125  		a.Period != c.allowance.Period ||
   126  		a.Hosts != c.allowance.Hosts {
   127  		t.Fatal("Allowance did not return correct allowance:", a, c.allowance)
   128  	}
   129  }
   130  
   131  // stubHostDB mocks the hostDB dependency using zero-valued implementations of
   132  // its methods.
   133  type stubHostDB struct{}
   134  
   135  func (stubHostDB) AllHosts() (hs []modules.HostDBEntry)                             { return }
   136  func (stubHostDB) ActiveHosts() (hs []modules.HostDBEntry)                          { return }
   137  func (stubHostDB) Host(types.SiaPublicKey) (h modules.HostDBEntry, ok bool)         { return }
   138  func (stubHostDB) IncrementSuccessfulInteractions(key types.SiaPublicKey)           { return }
   139  func (stubHostDB) IncrementFailedInteractions(key types.SiaPublicKey)               { return }
   140  func (stubHostDB) PublicKey() (spk types.SiaPublicKey)                              { return }
   141  func (stubHostDB) RandomHosts(int, []types.SiaPublicKey) (hs []modules.HostDBEntry) { return }
   142  func (stubHostDB) ScoreBreakdown(modules.HostDBEntry) modules.HostScoreBreakdown {
   143  	return modules.HostScoreBreakdown{}
   144  }
   145  
   146  // TestAllowancePeriodTracking verifies that the contractor tracks its current
   147  // period correctly as renewals occur.
   148  func TestAllowancePeriodTracking(t *testing.T) {
   149  	if testing.Short() {
   150  		t.SkipNow()
   151  	}
   152  	t.Parallel()
   153  
   154  	_, c, m, err := newTestingTrio(t.Name())
   155  	if err != nil {
   156  		t.Fatal(err)
   157  	}
   158  
   159  	// set an allowance
   160  	c.mu.Lock()
   161  	initialHeight := c.blockHeight
   162  	c.mu.Unlock()
   163  	testAllowance := modules.Allowance{
   164  		Funds:       types.SiacoinPrecision.Mul64(5000),
   165  		RenewWindow: 10,
   166  		Hosts:       1,
   167  		Period:      20,
   168  	}
   169  	err = c.SetAllowance(testAllowance)
   170  	if err != nil {
   171  		t.Fatal(err)
   172  	}
   173  	err = build.Retry(50, 100*time.Millisecond, func() error {
   174  		if len(c.Contracts()) != 1 {
   175  			return errors.New("allowance forming seems to have failed")
   176  		}
   177  		return nil
   178  	})
   179  	if err != nil {
   180  		t.Error(err)
   181  	}
   182  	if c.CurrentPeriod() != initialHeight {
   183  		t.Fatal("expected current period to start at", initialHeight, "got", c.CurrentPeriod())
   184  	}
   185  	// mine until one before the renew window, current period should stay
   186  	// constant
   187  	for i := types.BlockHeight(0); i < testAllowance.RenewWindow-1; i++ {
   188  		_, err = m.AddBlock()
   189  		if err != nil {
   190  			t.Fatal(err)
   191  		}
   192  	}
   193  	if c.CurrentPeriod() != initialHeight {
   194  		t.Fatal("current period should not have incremented, wanted", initialHeight, "got", c.CurrentPeriod())
   195  	}
   196  	// mine another another block. current period should increment.
   197  	_, err = m.AddBlock()
   198  	if err != nil {
   199  		t.Fatal(err)
   200  	}
   201  	c.mu.Lock()
   202  	height := c.blockHeight
   203  	c.mu.Unlock()
   204  	if c.CurrentPeriod() != height {
   205  		t.Fatal("unexpected period", c.CurrentPeriod(), "wanted", height)
   206  	}
   207  }
   208  
   209  // TestAllowanceSpending verifies that the contractor will not spend more or
   210  // less than the allowance if uploading causes repeated early renewal, and that
   211  // correct spending metrics are returned, even across renewals.
   212  func TestAllowanceSpending(t *testing.T) {
   213  	if testing.Short() {
   214  		t.SkipNow()
   215  	}
   216  	t.Parallel()
   217  
   218  	// create testing trio
   219  	h, c, m, err := newTestingTrio(t.Name())
   220  	if err != nil {
   221  		t.Fatal(err)
   222  	}
   223  
   224  	// make the host's upload price very high so this test requires less
   225  	// computation
   226  	settings := h.InternalSettings()
   227  	settings.MinUploadBandwidthPrice = types.SiacoinPrecision.Div64(10)
   228  	err = h.SetInternalSettings(settings)
   229  	if err != nil {
   230  		t.Fatal(err)
   231  	}
   232  	err = h.Announce()
   233  	if err != nil {
   234  		t.Fatal(err)
   235  	}
   236  	_, err = m.AddBlock()
   237  	if err != nil {
   238  		t.Fatal(err)
   239  	}
   240  	err = build.Retry(50, 100*time.Millisecond, func() error {
   241  		if len(c.hdb.RandomHosts(1, nil)) == 0 {
   242  			return errors.New("host has not been scanned yet")
   243  		}
   244  		return nil
   245  	})
   246  	if err != nil {
   247  		t.Fatal(err)
   248  	}
   249  
   250  	// set an allowance
   251  	testAllowance := modules.Allowance{
   252  		Funds:       types.SiacoinPrecision.Mul64(6000),
   253  		RenewWindow: 100,
   254  		Hosts:       1,
   255  		Period:      200,
   256  	}
   257  	err = c.SetAllowance(testAllowance)
   258  	if err != nil {
   259  		t.Fatal(err)
   260  	}
   261  	err = build.Retry(50, 100*time.Millisecond, func() error {
   262  		if len(c.Contracts()) != 1 {
   263  			return errors.New("allowance forming seems to have failed")
   264  		}
   265  		return nil
   266  	})
   267  	if err != nil {
   268  		t.Error(err)
   269  	}
   270  
   271  	// exhaust a contract and add a block several times. Despite repeatedly
   272  	// running out of funds, the contractor should not spend more than the
   273  	// allowance.
   274  	for i := 0; i < 15; i++ {
   275  		for _, contract := range c.Contracts() {
   276  			ed, err := c.Editor(contract.ID, nil)
   277  			if err != nil {
   278  				continue
   279  			}
   280  
   281  			// upload 10 sectors to the contract
   282  			for sec := 0; sec < 10; sec++ {
   283  				ed.Upload(make([]byte, modules.SectorSize))
   284  			}
   285  			err = ed.Close()
   286  			if err != nil {
   287  				t.Fatal(err)
   288  			}
   289  		}
   290  		_, err := m.AddBlock()
   291  		if err != nil {
   292  			t.Fatal(err)
   293  		}
   294  	}
   295  
   296  	var minerRewards types.Currency
   297  	w := c.wallet.(*WalletBridge).W.(modules.Wallet)
   298  	txns, err := w.Transactions(0, 1000)
   299  	if err != nil {
   300  		t.Fatal(err)
   301  	}
   302  	for _, txn := range txns {
   303  		for _, so := range txn.Outputs {
   304  			if so.FundType == types.SpecifierMinerPayout {
   305  				minerRewards = minerRewards.Add(so.Value)
   306  			}
   307  		}
   308  	}
   309  	balance, _, _ := w.ConfirmedBalance()
   310  	spent := minerRewards.Sub(balance)
   311  	if spent.Cmp(testAllowance.Funds) > 0 {
   312  		t.Fatal("contractor spent too much money: spent", spent.HumanString(), "allowance funds:", testAllowance.Funds.HumanString())
   313  	}
   314  
   315  	// we should have spent at least the allowance minus the cost of one more refresh
   316  	refreshCost := c.Contracts()[0].TotalCost.Mul64(2)
   317  	expectedMinSpending := testAllowance.Funds.Sub(refreshCost)
   318  	if spent.Cmp(expectedMinSpending) < 0 {
   319  		t.Fatal("contractor spent to little money: spent", spent.HumanString(), "expected at least:", expectedMinSpending.HumanString())
   320  	}
   321  
   322  	// PeriodSpending should reflect the amount of spending accurately
   323  	reportedSpending := c.PeriodSpending()
   324  	if reportedSpending.ContractSpending.Cmp(spent) != 0 {
   325  		t.Fatal("reported incorrect spending for this billing cycle: got", reportedSpending.ContractSpending.HumanString(), "wanted", spent.HumanString())
   326  	}
   327  
   328  	// enter a new period. PeriodSpending should reset.
   329  	c.mu.Lock()
   330  	renewHeight := c.blockHeight + c.allowance.RenewWindow
   331  	blocksToMine := renewHeight - c.blockHeight
   332  	c.mu.Unlock()
   333  	for i := types.BlockHeight(0); i < blocksToMine; i++ {
   334  		_, err = m.AddBlock()
   335  		if err != nil {
   336  			t.Fatal(err)
   337  		}
   338  	}
   339  
   340  	// Retry to give the threadedMaintenenace some time to finish
   341  	var newReportedSpending modules.ContractorSpending
   342  	err = build.Retry(100, 100*time.Millisecond, func() error {
   343  		newReportedSpending = c.PeriodSpending()
   344  		if reflect.DeepEqual(newReportedSpending, reportedSpending) {
   345  			return errors.New("reported spending was identical after entering a renew period")
   346  		}
   347  		return nil
   348  	})
   349  	if err != nil {
   350  		t.Fatal(err)
   351  	}
   352  
   353  	if newReportedSpending.Unspent.Cmp(reportedSpending.Unspent) <= 0 {
   354  		t.Fatal("expected newReportedSpending to have more unspent")
   355  	}
   356  }
   357  
   358  // TestIntegrationSetAllowance tests the SetAllowance method.
   359  func TestIntegrationSetAllowance(t *testing.T) {
   360  	if testing.Short() {
   361  		t.SkipNow()
   362  	}
   363  	// create testing trio
   364  	_, c, m, err := newTestingTrio(t.Name())
   365  	if err != nil {
   366  		t.Fatal(err)
   367  	}
   368  
   369  	// this test requires two hosts: create another one
   370  	h, err := newTestingHost(build.TempDir("hostdata", ""), c.cs.(modules.ConsensusSet), c.tpool.(modules.TransactionPool))
   371  	if err != nil {
   372  		t.Fatal(err)
   373  	}
   374  
   375  	// announce the extra host
   376  	err = h.Announce()
   377  	if err != nil {
   378  		t.Fatal(err)
   379  	}
   380  
   381  	// mine a block, processing the announcement
   382  	_, err = m.AddBlock()
   383  	if err != nil {
   384  		t.Fatal(err)
   385  	}
   386  
   387  	// wait for hostdb to scan host
   388  	for i := 0; i < 100 && len(c.hdb.RandomHosts(1, nil)) == 0; i++ {
   389  		time.Sleep(time.Millisecond * 50)
   390  	}
   391  
   392  	// cancel allowance
   393  	var a modules.Allowance
   394  	err = c.SetAllowance(a)
   395  	if err != nil {
   396  		t.Fatal(err)
   397  	}
   398  
   399  	// bad args
   400  	a.Hosts = 1
   401  	err = c.SetAllowance(a)
   402  	if err != errAllowanceZeroPeriod {
   403  		t.Errorf("expected %q, got %q", errAllowanceZeroPeriod, err)
   404  	}
   405  	a.Period = 20
   406  	err = c.SetAllowance(a)
   407  	if err != ErrAllowanceZeroWindow {
   408  		t.Errorf("expected %q, got %q", ErrAllowanceZeroWindow, err)
   409  	}
   410  	a.RenewWindow = 20
   411  	err = c.SetAllowance(a)
   412  	if err != errAllowanceWindowSize {
   413  		t.Errorf("expected %q, got %q", errAllowanceWindowSize, err)
   414  	}
   415  
   416  	// reasonable values; should succeed
   417  	a.Funds = types.SiacoinPrecision.Mul64(100)
   418  	a.RenewWindow = 10
   419  	err = c.SetAllowance(a)
   420  	if err != nil {
   421  		t.Fatal(err)
   422  	}
   423  	err = build.Retry(50, 100*time.Millisecond, func() error {
   424  		if len(c.Contracts()) != 1 {
   425  			return errors.New("allowance forming seems to have failed")
   426  		}
   427  		return nil
   428  	})
   429  	if err != nil {
   430  		t.Error(err)
   431  	}
   432  
   433  	// set same allowance; should no-op
   434  	err = c.SetAllowance(a)
   435  	if err != nil {
   436  		t.Fatal(err)
   437  	}
   438  	c.mu.Lock()
   439  	clen := c.contracts.Len()
   440  	c.mu.Unlock()
   441  	if clen != 1 {
   442  		t.Fatal("expected 1 contract, got", clen)
   443  	}
   444  
   445  	_, err = m.AddBlock()
   446  	if err != nil {
   447  		t.Fatal(err)
   448  	}
   449  
   450  	// set allowance with Hosts = 2; should only form one new contract
   451  	a.Hosts = 2
   452  	err = c.SetAllowance(a)
   453  	if err != nil {
   454  		t.Fatal(err)
   455  	}
   456  	err = build.Retry(50, 100*time.Millisecond, func() error {
   457  		if len(c.Contracts()) != 2 {
   458  			return errors.New("allowance forming seems to have failed")
   459  		}
   460  		return nil
   461  	})
   462  	if err != nil {
   463  		t.Fatal(err)
   464  	}
   465  
   466  	// set allowance with Funds*2; should trigger renewal of both contracts
   467  	a.Funds = a.Funds.Mul64(2)
   468  	err = c.SetAllowance(a)
   469  	if err != nil {
   470  		t.Fatal(err)
   471  	}
   472  	err = build.Retry(50, 100*time.Millisecond, func() error {
   473  		if len(c.Contracts()) != 2 {
   474  			return errors.New("allowance forming seems to have failed")
   475  		}
   476  		return nil
   477  	})
   478  	if err != nil {
   479  		t.Error(err)
   480  	}
   481  
   482  	// delete one of the contracts and set allowance with Funds*2; should
   483  	// trigger 1 renewal and 1 new contract
   484  	c.mu.Lock()
   485  	ids := c.contracts.IDs()
   486  	contract, _ := c.contracts.Acquire(ids[0])
   487  	c.contracts.Delete(contract)
   488  	c.mu.Unlock()
   489  	a.Funds = a.Funds.Mul64(2)
   490  	err = c.SetAllowance(a)
   491  	if err != nil {
   492  		t.Fatal(err)
   493  	}
   494  	err = build.Retry(50, 100*time.Millisecond, func() error {
   495  		if len(c.Contracts()) != 2 {
   496  			return errors.New("allowance forming seems to have failed")
   497  		}
   498  		return nil
   499  	})
   500  	if err != nil {
   501  		t.Fatal(err)
   502  	}
   503  }
   504  
   505  // testWalletShim is used to test the walletBridge type.
   506  type testWalletShim struct {
   507  	nextAddressCalled bool
   508  	startTxnCalled    bool
   509  }
   510  
   511  // These stub implementations for the walletShim interface set their respective
   512  // booleans to true, allowing tests to verify that they have been called.
   513  func (ws *testWalletShim) NextAddress() (types.UnlockConditions, error) {
   514  	ws.nextAddressCalled = true
   515  	return types.UnlockConditions{}, nil
   516  }
   517  func (ws *testWalletShim) StartTransaction() modules.TransactionBuilder {
   518  	ws.startTxnCalled = true
   519  	return nil
   520  }
   521  
   522  // TestWalletBridge tests the walletBridge type.
   523  func TestWalletBridge(t *testing.T) {
   524  	shim := new(testWalletShim)
   525  	bridge := WalletBridge{shim}
   526  	bridge.NextAddress()
   527  	if !shim.nextAddressCalled {
   528  		t.Error("NextAddress was not called on the shim")
   529  	}
   530  	bridge.StartTransaction()
   531  	if !shim.startTxnCalled {
   532  		t.Error("StartTransaction was not called on the shim")
   533  	}
   534  }