gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/renter_test.go (about)

     1  package renter
     2  
     3  import (
     4  	"io/ioutil"
     5  	"os"
     6  	"path/filepath"
     7  	"testing"
     8  	"time"
     9  
    10  	"gitlab.com/NebulousLabs/errors"
    11  	"gitlab.com/NebulousLabs/fastrand"
    12  	"gitlab.com/NebulousLabs/ratelimit"
    13  	"gitlab.com/NebulousLabs/siamux"
    14  
    15  	"gitlab.com/SkynetLabs/skyd/build"
    16  	skydPersist "gitlab.com/SkynetLabs/skyd/persist"
    17  	"gitlab.com/SkynetLabs/skyd/skymodules"
    18  	"gitlab.com/SkynetLabs/skyd/skymodules/renter/contractor"
    19  	"gitlab.com/SkynetLabs/skyd/skymodules/renter/filesystem"
    20  	"gitlab.com/SkynetLabs/skyd/skymodules/renter/hostdb"
    21  	"gitlab.com/SkynetLabs/skyd/skymodules/renter/proto"
    22  	"go.sia.tech/siad/crypto"
    23  	"go.sia.tech/siad/modules"
    24  	"go.sia.tech/siad/modules/consensus"
    25  	"go.sia.tech/siad/modules/gateway"
    26  	"go.sia.tech/siad/modules/host"
    27  	"go.sia.tech/siad/modules/miner"
    28  	"go.sia.tech/siad/modules/transactionpool"
    29  	"go.sia.tech/siad/modules/wallet"
    30  	"go.sia.tech/siad/persist"
    31  	"go.sia.tech/siad/types"
    32  )
    33  
    34  type (
    35  	// testSiacoinSender is a implementation of the SiacoinSender interface
    36  	// which remembers the arguments of the last call to SendSiacoins.
    37  	testSiacoinSender struct {
    38  		lastSend     types.Currency
    39  		lastSendAddr types.UnlockHash
    40  	}
    41  )
    42  
    43  // LastSend returns the arguments of the last call to SendSiacoins.
    44  func (tss *testSiacoinSender) LastSend() (types.Currency, types.UnlockHash) {
    45  	return tss.lastSend, tss.lastSendAddr
    46  }
    47  
    48  // SendSiacoins implements the SiacoinSender interface.
    49  func (tss *testSiacoinSender) SendSiacoins(amt types.Currency, addr types.UnlockHash) ([]types.Transaction, error) {
    50  	tss.lastSend = amt
    51  	tss.lastSendAddr = addr
    52  	return nil, nil
    53  }
    54  
    55  // renterTester contains all of the modules that are used while testing the renter.
    56  type renterTester struct {
    57  	cs      modules.ConsensusSet
    58  	gateway modules.Gateway
    59  	miner   modules.TestMiner
    60  	tpool   modules.TransactionPool
    61  	wallet  modules.Wallet
    62  
    63  	mux *siamux.SiaMux
    64  
    65  	renter *Renter
    66  	dir    string
    67  }
    68  
    69  // Close shuts down the renter tester.
    70  func (rt *renterTester) Close() error {
    71  	err1 := rt.cs.Close()
    72  	err2 := rt.gateway.Close()
    73  	err3 := rt.miner.Close()
    74  	err4 := rt.tpool.Close()
    75  	err5 := rt.wallet.Close()
    76  	err6 := rt.mux.Close()
    77  	err7 := rt.renter.Close()
    78  	return errors.Compose(err1, err2, err3, err4, err5, err6, err7)
    79  }
    80  
    81  // addCustomHost adds a host to the test group so that it appears in the host db
    82  func (rt *renterTester) addCustomHost(testdir string, deps modules.Dependencies) (modules.Host, error) {
    83  	// create a siamux for this particular host
    84  	siaMuxDir := filepath.Join(testdir, modules.SiaMuxDir)
    85  	mux, err := modules.NewSiaMux(siaMuxDir, testdir, "localhost:0", "localhost:0")
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	h, err := host.NewCustomHost(deps, rt.cs, rt.gateway, rt.tpool, rt.wallet, mux, "localhost:0", filepath.Join(testdir, modules.HostDir))
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	// configure host to accept contracts and to have a registry.
    96  	settings := h.InternalSettings()
    97  	settings.AcceptingContracts = true
    98  	settings.RegistrySize = 640 * modules.RegistryEntrySize
    99  	err = h.SetInternalSettings(settings)
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  
   104  	// add storage to host
   105  	storageFolder := filepath.Join(testdir, "storage")
   106  	err = os.MkdirAll(storageFolder, 0700)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	err = h.AddStorageFolder(storageFolder, 1<<20) // 1 MiB
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	// announce the host
   116  	err = h.Announce()
   117  	if err != nil {
   118  		return nil, build.ExtendErr("error announcing host", err)
   119  	}
   120  
   121  	// mine a block, processing the announcement
   122  	_, err = rt.miner.AddBlock()
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  
   127  	// wait for hostdb to scan host
   128  	activeHosts, err := rt.renter.ActiveHosts()
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  	for i := 0; i < 50 && len(activeHosts) == 0; i++ {
   133  		time.Sleep(time.Millisecond * 100)
   134  	}
   135  	activeHosts, err = rt.renter.ActiveHosts()
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  	if len(activeHosts) == 0 {
   140  		return nil, errors.New("host did not make it into the contractor hostdb in time")
   141  	}
   142  
   143  	return h, nil
   144  }
   145  
   146  // addHost adds a host to the test group so that it appears in the host db
   147  func (rt *renterTester) addHost(name string) (modules.Host, error) {
   148  	return rt.addCustomHost(filepath.Join(rt.dir, name), modules.ProdDependencies)
   149  }
   150  
   151  // addRenter adds a renter to the renter tester and then make sure there is
   152  // money in the wallet
   153  func (rt *renterTester) addRenter(r *Renter) error {
   154  	rt.renter = r
   155  	// Mine blocks until there is money in the wallet.
   156  	for i := types.BlockHeight(0); i <= types.MaturityDelay; i++ {
   157  		_, err := rt.miner.AddBlock()
   158  		if err != nil {
   159  			return err
   160  		}
   161  	}
   162  	return nil
   163  }
   164  
   165  // createZeroByteFileOnDisk creates a 0 byte file on disk so that a Stat of the
   166  // local path won't return an error
   167  func (rt *renterTester) createZeroByteFileOnDisk() (string, error) {
   168  	path := filepath.Join(rt.renter.staticFileSystem.Root(), persist.RandomSuffix())
   169  	err := ioutil.WriteFile(path, []byte{}, 0600)
   170  	if err != nil {
   171  		return "", err
   172  	}
   173  	return path, nil
   174  }
   175  
   176  // newTestSiaFile creates and returns a new siafile for testing. This file is
   177  // marked as finished for backwards compatibility in testing.
   178  func (rt *renterTester) newTestSiaFile(siaPath skymodules.SiaPath, source string, rc skymodules.ErasureCoder, size uint64) (*filesystem.FileNode, error) {
   179  	// Create the siafile
   180  	err := rt.renter.staticFileSystem.NewSiaFile(siaPath, source, rc, crypto.GenerateSiaKey(crypto.RandomCipherType()), size, persist.DefaultDiskPermissionsTest)
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  	// Open the file
   185  	f, err := rt.renter.staticFileSystem.OpenSiaFile(siaPath)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  	// Mark it as finished for backwards compatibility in testing
   190  	err = f.SetFinished(0)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	return f, nil
   195  }
   196  
   197  // reloadRenter closes the given renter and then re-adds it, effectively
   198  // reloading the renter.
   199  func (rt *renterTester) reloadRenter(r *Renter) (*Renter, error) {
   200  	return rt.reloadRenterWithDependency(r, r.staticDeps)
   201  }
   202  
   203  // reloadRenterWithDependency closes the given renter and recreates it using the
   204  // given dependency, it then re-adds the renter on the renter tester effectively
   205  // reloading it.
   206  func (rt *renterTester) reloadRenterWithDependency(r *Renter, deps skymodules.SkydDependencies) (*Renter, error) {
   207  	err := r.Close()
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  
   212  	r, err = newRenterWithDependency(rt.gateway, rt.cs, rt.wallet, rt.tpool, rt.mux, filepath.Join(rt.dir, skymodules.RenterDir), deps)
   213  	if err != nil {
   214  		return nil, err
   215  	}
   216  
   217  	err = rt.addRenter(r)
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  	return r, nil
   222  }
   223  
   224  // newRenterTester creates a ready-to-use renter tester with money in the
   225  // wallet.
   226  func newRenterTester(name string) (*renterTester, error) {
   227  	testdir := build.TempDir("renter", name)
   228  	rt, err := newRenterTesterNoRenter(testdir)
   229  	if err != nil {
   230  		return nil, err
   231  	}
   232  
   233  	rl := ratelimit.NewRateLimit(0, 0, 0)
   234  	tus := NewSkynetTUSInMemoryUploadStore()
   235  	r, errChan := New(rt.gateway, rt.cs, rt.wallet, rt.tpool, rt.mux, tus, rl, filepath.Join(testdir, skymodules.RenterDir))
   236  	if err := <-errChan; err != nil {
   237  		return nil, err
   238  	}
   239  	err = rt.addRenter(r)
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  	return rt, nil
   244  }
   245  
   246  // newRenterTesterNoRenter creates all the modules for the renter tester except
   247  // the renter. A renter will need to be added and blocks mined to add money to
   248  // the wallet.
   249  func newRenterTesterNoRenter(testdir string) (*renterTester, error) {
   250  	// Create the siamux
   251  	siaMuxDir := filepath.Join(testdir, modules.SiaMuxDir)
   252  	mux, err := modules.NewSiaMux(siaMuxDir, testdir, "localhost:0", "localhost:0")
   253  	if err != nil {
   254  		return nil, err
   255  	}
   256  
   257  	// Create the skymodules.
   258  	g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir))
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  	cs, errChan := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir))
   263  	if err := <-errChan; err != nil {
   264  		return nil, err
   265  	}
   266  	tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir))
   267  	if err != nil {
   268  		return nil, err
   269  	}
   270  	w, err := wallet.New(cs, tp, filepath.Join(testdir, modules.WalletDir))
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  	key := crypto.GenerateSiaKey(crypto.TypeDefaultWallet)
   275  	_, err = w.Encrypt(key)
   276  	if err != nil {
   277  		return nil, err
   278  	}
   279  	err = w.Unlock(key)
   280  	if err != nil {
   281  		return nil, err
   282  	}
   283  	m, err := miner.New(cs, tp, w, filepath.Join(testdir, modules.MinerDir))
   284  	if err != nil {
   285  		return nil, err
   286  	}
   287  
   288  	// Assemble all pieces into a renter tester.
   289  	return &renterTester{
   290  		mux: mux,
   291  
   292  		cs:      cs,
   293  		gateway: g,
   294  		miner:   m,
   295  		tpool:   tp,
   296  		wallet:  w,
   297  
   298  		dir: testdir,
   299  	}, nil
   300  }
   301  
   302  // newRenterTesterWithDependency creates a ready-to-use renter tester with money
   303  // in the wallet.
   304  func newRenterTesterWithDependency(name string, deps skymodules.SkydDependencies) (*renterTester, error) {
   305  	testdir := build.TempDir("renter", name)
   306  	rt, err := newRenterTesterNoRenter(testdir)
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  
   311  	// Create the siamux
   312  	siaMuxDir := filepath.Join(testdir, modules.SiaMuxDir)
   313  	mux, err := modules.NewSiaMux(siaMuxDir, testdir, "localhost:0", "localhost:0")
   314  	if err != nil {
   315  		return nil, err
   316  	}
   317  
   318  	r, err := newRenterWithDependency(rt.gateway, rt.cs, rt.wallet, rt.tpool, mux, filepath.Join(testdir, skymodules.RenterDir), deps)
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  	err = rt.addRenter(r)
   323  	if err != nil {
   324  		return nil, err
   325  	}
   326  	return rt, nil
   327  }
   328  
   329  // newRenterWithDependency creates a Renter with custom dependency
   330  func newRenterWithDependency(g modules.Gateway, cs modules.ConsensusSet, wallet modules.Wallet, tpool modules.TransactionPool, mux *siamux.SiaMux, persistDir string, deps skymodules.SkydDependencies) (*Renter, error) {
   331  	hdb, errChan := hostdb.NewCustomHostDB(g, cs, tpool, mux, persistDir, deps)
   332  	if err := <-errChan; err != nil {
   333  		return nil, err
   334  	}
   335  	rl := ratelimit.NewRateLimit(0, 0, 0)
   336  	contractSet, err := proto.NewContractSet(filepath.Join(persistDir, "contracts"), rl, modules.ProdDependencies)
   337  	if err != nil {
   338  		return nil, err
   339  	}
   340  
   341  	logger, err := persist.NewFileLogger(filepath.Join(persistDir, "contractor.log"))
   342  	if err != nil {
   343  		return nil, err
   344  	}
   345  
   346  	hc, errChan := contractor.NewCustomContractor(cs, wallet, tpool, hdb, persistDir, contractSet, logger, deps)
   347  	if err := <-errChan; err != nil {
   348  		return nil, err
   349  	}
   350  	tus := NewSkynetTUSInMemoryUploadStore()
   351  	renter, errChan := NewCustomRenter(g, cs, tpool, hdb, wallet, hc, mux, tus, persistDir, rl, deps)
   352  	return renter, <-errChan
   353  }
   354  
   355  // TestRenterCanAccessEphemeralAccountHostSettings verifies that the renter has
   356  // access to the host's external settings and that they include the new
   357  // ephemeral account setting fields.
   358  func TestRenterCanAccessEphemeralAccountHostSettings(t *testing.T) {
   359  	if testing.Short() {
   360  		t.SkipNow()
   361  	}
   362  	t.Parallel()
   363  	rt, err := newRenterTester(t.Name())
   364  	if err != nil {
   365  		t.Fatal(err)
   366  	}
   367  	defer func() {
   368  		if err := rt.Close(); err != nil {
   369  			t.Fatal(err)
   370  		}
   371  	}()
   372  
   373  	// Add a host to the test group
   374  	h, err := rt.addHost(t.Name())
   375  	if err != nil {
   376  		t.Fatal(err)
   377  	}
   378  
   379  	hostEntry, found, err := rt.renter.staticHostDB.Host(h.PublicKey())
   380  	if err != nil {
   381  		t.Fatal(err)
   382  	}
   383  	if !found {
   384  		t.Fatal("Expected the newly added host to be found in the hostDB")
   385  	}
   386  
   387  	if hostEntry.EphemeralAccountExpiry != modules.DefaultEphemeralAccountExpiry {
   388  		t.Fatal("Unexpected account expiry")
   389  	}
   390  
   391  	if !hostEntry.MaxEphemeralAccountBalance.Equals(modules.DefaultMaxEphemeralAccountBalance) {
   392  		t.Fatal("Unexpected max account balance")
   393  	}
   394  }
   395  
   396  // TestRenterPricesDivideByZero verifies that the Price Estimation catches
   397  // divide by zero errors.
   398  func TestRenterPricesDivideByZero(t *testing.T) {
   399  	if testing.Short() {
   400  		t.SkipNow()
   401  	}
   402  	t.Parallel()
   403  	rt, err := newRenterTester(t.Name())
   404  	if err != nil {
   405  		t.Fatal(err)
   406  	}
   407  	defer func() {
   408  		if err := rt.Close(); err != nil {
   409  			t.Fatal(err)
   410  		}
   411  	}()
   412  
   413  	// Confirm price estimation returns error if there are no hosts available
   414  	_, _, err = rt.renter.PriceEstimation(skymodules.Allowance{})
   415  	if err == nil {
   416  		t.Fatal("Expected error due to no hosts")
   417  	}
   418  
   419  	// Add a host to the test group
   420  	_, err = rt.addHost(t.Name())
   421  	if err != nil {
   422  		t.Fatal(err)
   423  	}
   424  
   425  	// Confirm price estimation does not return an error now that there is a
   426  	// host available
   427  	_, _, err = rt.renter.PriceEstimation(skymodules.Allowance{})
   428  	if err != nil {
   429  		t.Fatal(err)
   430  	}
   431  }
   432  
   433  // TestPaySkynetFee is a unit test for paySkynetFee.
   434  func TestPaySkynetFee(t *testing.T) {
   435  	if testing.Short() {
   436  		t.SkipNow()
   437  	}
   438  	t.Parallel()
   439  
   440  	testDir := build.TempDir("renter", t.Name())
   441  	fileName := "test"
   442  
   443  	// Create a new history.
   444  	sh, err := NewSpendingHistory(testDir, fileName)
   445  	if err != nil {
   446  		t.Fatal(err)
   447  	}
   448  
   449  	// Declare helper for contract creation.
   450  	randomContract := func() skymodules.RenterContract {
   451  		return skymodules.RenterContract{
   452  			DownloadSpending:    types.NewCurrency64(fastrand.Uint64n(100)),
   453  			FundAccountSpending: types.NewCurrency64(fastrand.Uint64n(100)),
   454  			MaintenanceSpending: skymodules.MaintenanceSpending{
   455  				AccountBalanceCost:   types.NewCurrency64(fastrand.Uint64n(100)),
   456  				FundAccountCost:      types.NewCurrency64(fastrand.Uint64n(100)),
   457  				UpdatePriceTableCost: types.NewCurrency64(fastrand.Uint64n(100)),
   458  			},
   459  			StorageSpending: types.NewCurrency64(fastrand.Uint64n(100)),
   460  			UploadSpending:  types.NewCurrency64(fastrand.Uint64n(100)),
   461  		}
   462  	}
   463  
   464  	// Create a contract.
   465  	contracts := []skymodules.RenterContract{
   466  		randomContract(),
   467  	}
   468  
   469  	// Helper to compute spending of contracts.
   470  	spending := func(contracts []skymodules.RenterContract) types.Currency {
   471  		var spending types.Currency
   472  		for _, c := range contracts {
   473  			spending = spending.Add(c.SkynetSpending())
   474  		}
   475  		return spending
   476  	}
   477  
   478  	// Helper to compute the fee from a given spending delta.
   479  	fee := func(delta types.Currency) types.Currency {
   480  		return delta.Div64(5) // 20%
   481  	}
   482  
   483  	// Create an address.
   484  	var uh types.UnlockHash
   485  	fastrand.Read(uh[:])
   486  
   487  	// Create a test sender.
   488  	ts := &testSiacoinSender{}
   489  
   490  	// Dummy log.
   491  	log, err := skydPersist.NewLogger(ioutil.Discard)
   492  	if err != nil {
   493  		t.Fatal(err)
   494  	}
   495  
   496  	// Pay a skynet fee but the time is now so not enough time has passed.
   497  	threshold := types.ZeroCurrency
   498  	expectedTime := time.Now()
   499  	sh.AddSpending(types.ZeroCurrency, nil, expectedTime)
   500  	err = paySkynetFee(sh, ts, contracts, uh, threshold, log)
   501  	if err != nil {
   502  		t.Fatal(err)
   503  	}
   504  	if ls, lsa := ts.LastSend(); !ls.IsZero() || lsa != (types.UnlockHash{}) {
   505  		t.Fatal("money was paid even though it shouldn't", ls, lsa)
   506  	}
   507  	if ls, lsbd := sh.LastSpending(); !ls.IsZero() || lsbd != expectedTime {
   508  		t.Fatal("history shouldn't have been updated", ls, lsbd, expectedTime)
   509  	}
   510  
   511  	// Last spending was 48 hours ago which is enough.
   512  	expectedTime = time.Now().AddDate(0, 0, -2)
   513  	sh.AddSpending(types.ZeroCurrency, nil, expectedTime)
   514  	err = paySkynetFee(sh, ts, contracts, uh, threshold, log)
   515  	if err != nil {
   516  		t.Fatal(err)
   517  	}
   518  	expectedSpending := spending(contracts)
   519  	expectedFee := fee(expectedSpending)
   520  	if ls, lsa := ts.LastSend(); !ls.Equals(expectedFee) || lsa != uh {
   521  		t.Fatal("wrong payment", ls, expectedFee, lsa, uh)
   522  	}
   523  	if ls, lsbd := sh.LastSpending(); !ls.Equals(expectedSpending) || lsbd.Before(expectedTime) {
   524  		t.Fatal("wrong history", ls, expectedFee, lsbd, expectedTime)
   525  	}
   526  
   527  	// Add another contract.
   528  	contracts = append(contracts, randomContract())
   529  
   530  	// Time is 48 hours ago but spending didn't change. Nothing happens.
   531  	expectedTime = time.Now().AddDate(0, 0, -2)
   532  	sh.AddSpending(expectedSpending, nil, expectedTime)
   533  	oldContracts := contracts[:1]
   534  	err = paySkynetFee(sh, ts, oldContracts, uh, threshold, log)
   535  	if err != nil {
   536  		t.Fatal(err)
   537  	}
   538  	if ls, lsa := ts.LastSend(); !ls.Equals(expectedFee) || lsa != uh {
   539  		t.Fatal("wrong payment", ls, expectedFee, lsa, uh)
   540  	}
   541  	if ls, lsbd := sh.LastSpending(); !ls.Equals(expectedSpending) || lsbd != expectedTime {
   542  		t.Fatal("wrong history", ls, expectedSpending, lsbd, expectedTime)
   543  	}
   544  
   545  	// Spending increased. Payment expected.
   546  	err = paySkynetFee(sh, ts, contracts, uh, threshold, log)
   547  	if err != nil {
   548  		t.Fatal(err)
   549  	}
   550  	expectedSpending = spending(contracts)
   551  	expectedFee = fee(expectedSpending.Sub(spending(oldContracts)))
   552  	if ls, lsa := ts.LastSend(); !ls.Equals(expectedFee) || lsa != uh {
   553  		t.Fatal("wrong payment", ls, expectedFee, lsa, uh)
   554  	}
   555  	if ls, lsbd := sh.LastSpending(); !ls.Equals(expectedSpending) || lsbd.Before(expectedTime) {
   556  		t.Fatal("wrong history", ls, expectedSpending, lsbd, expectedTime)
   557  	}
   558  
   559  	// Add another contract.
   560  	contracts = append(contracts, randomContract())
   561  	oldContracts = contracts[:2]
   562  
   563  	// Spending increased again but the threshold is too low.
   564  	oldExpectedSpending := expectedSpending
   565  	oldExpectedFee := expectedFee
   566  	expectedSpending = spending(contracts)
   567  	expectedFee = fee(expectedSpending.Sub(spending(oldContracts)))
   568  	threshold = expectedFee.Add64(1)
   569  	err = paySkynetFee(sh, ts, contracts, uh, threshold, log)
   570  	if err != nil {
   571  		t.Fatal(err)
   572  	}
   573  	if ls, lsa := ts.LastSend(); !ls.Equals(oldExpectedFee) || lsa != uh {
   574  		t.Fatal("wrong payment", ls, oldExpectedFee, lsa, uh)
   575  	}
   576  	if ls, lsbd := sh.LastSpending(); !ls.Equals(oldExpectedSpending) || lsbd.Before(expectedTime) {
   577  		t.Fatal("wrong history", ls, oldExpectedSpending, lsbd, expectedTime)
   578  	}
   579  }
   580  
   581  // TestUpdateRentercontractsAndUtilities is a unit test for updateRenterContractsAndUtilities.
   582  func TestUpdateRenterContractsAndUtilities(t *testing.T) {
   583  	_, pkOffline := crypto.GenerateKeyPair()
   584  	_, pkOnline := crypto.GenerateKeyPair()
   585  
   586  	isOffline := func(spk types.SiaPublicKey) bool {
   587  		return spk.String() != types.Ed25519PublicKey(pkOnline).String()
   588  	}
   589  
   590  	// GFR contract that is offline.
   591  	gfrOffline := skymodules.RenterContract{
   592  		HostPublicKey: types.Ed25519PublicKey(pkOffline),
   593  		Utility: skymodules.ContractUtility{
   594  			GoodForRenew: true,
   595  		},
   596  	}
   597  	fastrand.Read(gfrOffline.ID[:])
   598  
   599  	// !GFR contract that is online
   600  	nGFROnline := skymodules.RenterContract{
   601  		HostPublicKey: types.Ed25519PublicKey(pkOnline),
   602  		Utility: skymodules.ContractUtility{
   603  			GoodForRenew: false,
   604  		},
   605  	}
   606  	fastrand.Read(nGFROnline.ID[:])
   607  
   608  	// !GFR contract that is offline and got the same host key as
   609  	// gfrOffline.
   610  	nGFRSameHost := skymodules.RenterContract{
   611  		HostPublicKey: gfrOffline.HostPublicKey,
   612  		Utility: skymodules.ContractUtility{
   613  			GoodForRenew: false,
   614  		},
   615  	}
   616  	fastrand.Read(nGFRSameHost.ID[:])
   617  
   618  	// Run this multiple times to make sure different ordering with the
   619  	// in-memory maps doesn't produce different results.
   620  	for i := 0; i < 100; i++ {
   621  		// Run updateRenterContractsAndUtilities. gfOffline is added
   622  		// twice to make sure we deduplicate the 'used' keys.
   623  		cu := updateRenterContractsAndUtilities([]skymodules.RenterContract{gfrOffline, gfrOffline, nGFROnline, nGFRSameHost}, isOffline)
   624  
   625  		// Check number of contracts.
   626  		if len(cu.contracts) != 2 {
   627  			t.Fatal("invalid number of contracts", len(cu.contracts))
   628  		}
   629  		// Check number of gfr hosts.
   630  		if len(cu.goodForRenew) != 2 {
   631  			t.Fatal("invalid number of gfr hosts", len(cu.goodForRenew))
   632  		}
   633  		gfr, ok := cu.goodForRenew[gfrOffline.HostPublicKey.String()]
   634  		if !ok {
   635  			t.Fatal("key not found")
   636  		}
   637  		if !gfr {
   638  			t.Fatal("should be gfr")
   639  		}
   640  		gfr, ok = cu.goodForRenew[nGFROnline.HostPublicKey.String()]
   641  		if !ok {
   642  			t.Fatal("key not found")
   643  		}
   644  		if gfr {
   645  			t.Fatal("should be !gfr")
   646  		}
   647  		// Check number of offline hosts.
   648  		if len(cu.goodForRenew) != 2 {
   649  			t.Fatal("invalid number of gfr hosts", len(cu.goodForRenew))
   650  		}
   651  		offline, ok := cu.goodForRenew[gfrOffline.HostPublicKey.String()]
   652  		if !ok {
   653  			t.Fatal("key not found")
   654  		}
   655  		if !offline {
   656  			t.Fatal("should be offline")
   657  		}
   658  		offline, ok = cu.goodForRenew[nGFROnline.HostPublicKey.String()]
   659  		if !ok {
   660  			t.Fatal("key not found")
   661  		}
   662  		if offline {
   663  			t.Fatal("should be online")
   664  		}
   665  		// Check used hosts.
   666  		if len(cu.used) != 1 {
   667  			t.Fatal("invalid number of used hosts", len(cu.used))
   668  		}
   669  	}
   670  }