gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/renter/renter_test.go (about)

     1  package renter
     2  
     3  import (
     4  	"io/ioutil"
     5  	"path/filepath"
     6  	"reflect"
     7  	"testing"
     8  
     9  	"gitlab.com/NebulousLabs/fastrand"
    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/miner"
    16  	"gitlab.com/SiaPrime/SiaPrime/modules/renter/contractor"
    17  	"gitlab.com/SiaPrime/SiaPrime/modules/renter/hostdb"
    18  	"gitlab.com/SiaPrime/SiaPrime/modules/transactionpool"
    19  	"gitlab.com/SiaPrime/SiaPrime/modules/wallet"
    20  	"gitlab.com/SiaPrime/SiaPrime/persist"
    21  	"gitlab.com/SiaPrime/SiaPrime/types"
    22  )
    23  
    24  // renterTester contains all of the modules that are used while testing the renter.
    25  type renterTester struct {
    26  	cs        modules.ConsensusSet
    27  	gateway   modules.Gateway
    28  	miner     modules.TestMiner
    29  	tpool     modules.TransactionPool
    30  	wallet    modules.Wallet
    31  	walletKey crypto.CipherKey
    32  
    33  	renter *Renter
    34  	dir    string
    35  }
    36  
    37  // Close shuts down the renter tester.
    38  func (rt *renterTester) Close() error {
    39  	rt.wallet.Lock()
    40  	rt.cs.Close()
    41  	rt.gateway.Close()
    42  	return nil
    43  }
    44  
    45  // addRenter adds a renter to the renter tester and then make sure there is
    46  // money in the wallet
    47  func (rt *renterTester) addRenter(r *Renter) error {
    48  	rt.renter = r
    49  	// Mine blocks until there is money in the wallet.
    50  	for i := types.BlockHeight(0); i <= types.MaturityDelay; i++ {
    51  		_, err := rt.miner.AddBlock()
    52  		if err != nil {
    53  			return err
    54  		}
    55  	}
    56  	return nil
    57  }
    58  
    59  // createZeroByteFileOnDisk creates a 0 byte file on disk so that a Stat of the
    60  // local path won't return an error
    61  func (rt *renterTester) createZeroByteFileOnDisk() (string, error) {
    62  	path := filepath.Join(rt.renter.staticFilesDir, persist.RandomSuffix())
    63  	err := ioutil.WriteFile(path, []byte{}, 0600)
    64  	if err != nil {
    65  		return "", err
    66  	}
    67  	return path, nil
    68  }
    69  
    70  // newRenterTester creates a ready-to-use renter tester with money in the
    71  // wallet.
    72  func newRenterTester(name string) (*renterTester, error) {
    73  	testdir := build.TempDir("renter", name)
    74  	rt, err := newRenterTesterNoRenter(testdir)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  	r, err := New(rt.gateway, rt.cs, rt.wallet, rt.tpool, filepath.Join(testdir, modules.RenterDir))
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  	err = rt.addRenter(r)
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  	return rt, nil
    87  }
    88  
    89  // newRenterTesterNoRenter creates all the modules for the renter tester except
    90  // the renter. A renter will need to be added and blocks mined to add money to
    91  // the wallet.
    92  func newRenterTesterNoRenter(testdir string) (*renterTester, error) {
    93  	// Create the modules.
    94  	g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir))
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  	cs, err := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir))
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir))
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	w, err := wallet.New(cs, tp, filepath.Join(testdir, modules.WalletDir))
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	key := crypto.GenerateSiaKey(crypto.TypeDefaultWallet)
   111  	_, err = w.Encrypt(key)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  	err = w.Unlock(key)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  	m, err := miner.New(cs, tp, w, filepath.Join(testdir, modules.MinerDir))
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	// Assemble all pieces into a renter tester.
   125  	return &renterTester{
   126  		cs:      cs,
   127  		gateway: g,
   128  		miner:   m,
   129  		tpool:   tp,
   130  		wallet:  w,
   131  
   132  		dir: testdir,
   133  	}, nil
   134  }
   135  
   136  // newRenterTesterWithDependency creates a ready-to-use renter tester with money in the
   137  // wallet.
   138  func newRenterTesterWithDependency(name string, deps modules.Dependencies) (*renterTester, error) {
   139  	testdir := build.TempDir("renter", name)
   140  	rt, err := newRenterTesterNoRenter(testdir)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  	r, err := newRenterWithDependency(rt.gateway, rt.cs, rt.wallet, rt.tpool, filepath.Join(testdir, modules.RenterDir), deps)
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  	err = rt.addRenter(r)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  	return rt, nil
   153  }
   154  
   155  // newRenterWithDependency creates a Renter with custom dependency
   156  func newRenterWithDependency(g modules.Gateway, cs modules.ConsensusSet, wallet modules.Wallet, tpool modules.TransactionPool, persistDir string, deps modules.Dependencies) (*Renter, error) {
   157  	hdb, err := hostdb.New(g, cs, tpool, persistDir)
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  	hc, err := contractor.New(cs, wallet, tpool, hdb, persistDir)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  	return NewCustomRenter(g, cs, tpool, hdb, wallet, hc, persistDir, deps)
   166  }
   167  
   168  // stubHostDB is the minimal implementation of the hostDB interface. It can be
   169  // embedded in other mock hostDB types, removing the need to re-implement all
   170  // of the hostDB's methods on every mock.
   171  type stubHostDB struct{}
   172  
   173  func (stubHostDB) ActiveHosts() []modules.HostDBEntry   { return nil }
   174  func (stubHostDB) AllHosts() []modules.HostDBEntry      { return nil }
   175  func (stubHostDB) AverageContractPrice() types.Currency { return types.Currency{} }
   176  func (stubHostDB) Close() error                         { return nil }
   177  func (stubHostDB) Filter() (modules.FilterMode, map[string]types.SiaPublicKey) {
   178  	return 0, make(map[string]types.SiaPublicKey)
   179  }
   180  func (stubHostDB) SetFilterMode(fm modules.FilterMode, hosts []types.SiaPublicKey) error { return nil }
   181  func (stubHostDB) IsOffline(modules.NetAddress) bool                                     { return true }
   182  func (stubHostDB) RandomHosts(int, []types.SiaPublicKey) ([]modules.HostDBEntry, error) {
   183  	return []modules.HostDBEntry{}, nil
   184  }
   185  func (stubHostDB) EstimateHostScore(modules.HostDBEntry, modules.Allowance) (modules.HostScoreBreakdown, error) {
   186  	return modules.HostScoreBreakdown{}, nil
   187  }
   188  func (stubHostDB) Host(types.SiaPublicKey) (modules.HostDBEntry, bool) {
   189  	return modules.HostDBEntry{}, false
   190  }
   191  func (stubHostDB) ScoreBreakdown(modules.HostDBEntry) (modules.HostScoreBreakdown, error) {
   192  	return modules.HostScoreBreakdown{}, nil
   193  }
   194  
   195  // stubContractor is the minimal implementation of the hostContractor
   196  // interface.
   197  type stubContractor struct{}
   198  
   199  func (stubContractor) SetAllowance(modules.Allowance) error { return nil }
   200  func (stubContractor) Allowance() modules.Allowance         { return modules.Allowance{} }
   201  func (stubContractor) Contract(modules.NetAddress) (modules.RenterContract, bool) {
   202  	return modules.RenterContract{}, false
   203  }
   204  func (stubContractor) Contracts() []modules.RenterContract                    { return nil }
   205  func (stubContractor) CurrentPeriod() types.BlockHeight                       { return 0 }
   206  func (stubContractor) IsOffline(modules.NetAddress) bool                      { return false }
   207  func (stubContractor) Editor(types.FileContractID) (contractor.Editor, error) { return nil, nil }
   208  func (stubContractor) Downloader(types.FileContractID) (contractor.Downloader, error) {
   209  	return nil, nil
   210  }
   211  
   212  type pricesStub struct {
   213  	stubHostDB
   214  
   215  	dbEntries []modules.HostDBEntry
   216  }
   217  
   218  func (pricesStub) InitialScanComplete() (bool, error) { return true, nil }
   219  func (pricesStub) IPViolationsCheck() bool            { return true }
   220  
   221  func (ps pricesStub) RandomHosts(_ int, _, _ []types.SiaPublicKey) ([]modules.HostDBEntry, error) {
   222  	return ps.dbEntries, nil
   223  }
   224  func (ps pricesStub) RandomHostsWithAllowance(_ int, _, _ []types.SiaPublicKey, _ modules.Allowance) ([]modules.HostDBEntry, error) {
   225  	return ps.dbEntries, nil
   226  }
   227  func (ps pricesStub) SetIPViolationCheck(enabled bool) { return }
   228  
   229  // TestRenterPricesDivideByZero verifies that the Price Estimation catches
   230  // divide by zero errors.
   231  func TestRenterPricesDivideByZero(t *testing.T) {
   232  	if testing.Short() {
   233  		t.SkipNow()
   234  	}
   235  	rt, err := newRenterTester(t.Name())
   236  	if err != nil {
   237  		t.Fatal(err)
   238  	}
   239  	defer rt.Close()
   240  
   241  	// Confirm price estimation returns error if there are no hosts available
   242  	_, _, err = rt.renter.PriceEstimation(modules.Allowance{})
   243  	if err == nil {
   244  		t.Fatal("Expected error due to no hosts")
   245  	}
   246  
   247  	// Create a stubbed hostdb, add an entry.
   248  	hdb := &pricesStub{}
   249  	id := rt.renter.mu.Lock()
   250  	rt.renter.hostDB = hdb
   251  	rt.renter.mu.Unlock(id)
   252  	dbe := modules.HostDBEntry{}
   253  	dbe.ContractPrice = types.SiacoinPrecision
   254  	dbe.DownloadBandwidthPrice = types.SiacoinPrecision
   255  	dbe.UploadBandwidthPrice = types.SiacoinPrecision
   256  	dbe.StoragePrice = types.SiacoinPrecision
   257  	pk := fastrand.Bytes(crypto.EntropySize)
   258  	dbe.PublicKey = types.SiaPublicKey{Key: pk}
   259  	hdb.dbEntries = append(hdb.dbEntries, dbe)
   260  
   261  	// Confirm price estimation does not return an error now that there is a
   262  	// host available
   263  	_, _, err = rt.renter.PriceEstimation(modules.Allowance{})
   264  	if err != nil {
   265  		t.Fatal(err)
   266  	}
   267  
   268  	// Set allowance funds and host contract price such that the allowance funds
   269  	// are not sufficient to cover the contract price
   270  	allowance := modules.Allowance{
   271  		Funds:       types.SiacoinPrecision,
   272  		Hosts:       1,
   273  		Period:      12096,
   274  		RenewWindow: 4032,
   275  	}
   276  	dbe.ContractPrice = allowance.Funds.Mul64(2)
   277  
   278  	// Confirm price estimation returns error because of the contract and
   279  	// funding prices
   280  	_, _, err = rt.renter.PriceEstimation(allowance)
   281  	if err == nil {
   282  		t.Fatal("Expected error due to allowance funds inefficient")
   283  	}
   284  }
   285  
   286  // TestRenterPricesVolatility verifies that the renter caches its price
   287  // estimation, and subsequent calls result in non-volatile results.
   288  func TestRenterPricesVolatility(t *testing.T) {
   289  	if testing.Short() {
   290  		t.SkipNow()
   291  	}
   292  	rt, err := newRenterTester(t.Name())
   293  	if err != nil {
   294  		t.Fatal(err)
   295  	}
   296  	defer rt.Close()
   297  
   298  	// create a stubbed hostdb, query it with one contract, add another, verify
   299  	// the price estimation remains constant until the timeout has passed.
   300  	hdb := &pricesStub{}
   301  	id := rt.renter.mu.Lock()
   302  	rt.renter.hostDB = hdb
   303  	rt.renter.mu.Unlock(id)
   304  	dbe := modules.HostDBEntry{}
   305  	dbe.ContractPrice = types.SiacoinPrecision
   306  	dbe.DownloadBandwidthPrice = types.SiacoinPrecision
   307  	dbe.UploadBandwidthPrice = types.SiacoinPrecision
   308  	dbe.StoragePrice = types.SiacoinPrecision
   309  	// Add 4 host entries in the database with different public keys.
   310  	for len(hdb.dbEntries) < modules.PriceEstimationScope {
   311  		pk := fastrand.Bytes(crypto.EntropySize)
   312  		dbe.PublicKey = types.SiaPublicKey{Key: pk}
   313  		hdb.dbEntries = append(hdb.dbEntries, dbe)
   314  	}
   315  	allowance := modules.Allowance{}
   316  	initial, _, err := rt.renter.PriceEstimation(allowance)
   317  	if err != nil {
   318  		t.Fatal(err)
   319  	}
   320  	// Changing the contract price should be enough to trigger a change
   321  	// if the hosts are not cached.
   322  	dbe.ContractPrice = dbe.ContractPrice.Mul64(2)
   323  	pk := fastrand.Bytes(crypto.EntropySize)
   324  	dbe.PublicKey = types.SiaPublicKey{Key: pk}
   325  	hdb.dbEntries = append(hdb.dbEntries, dbe)
   326  	after, _, err := rt.renter.PriceEstimation(allowance)
   327  	if err != nil {
   328  		t.Fatal(err)
   329  	}
   330  	if !reflect.DeepEqual(initial, after) {
   331  		t.Log(initial)
   332  		t.Log(after)
   333  		t.Fatal("expected renter price estimation to be constant")
   334  	}
   335  }