github.com/nebulouslabs/sia@v1.3.7/modules/renter/contractor/contractor_test.go (about)

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