github.com/nebulouslabs/sia@v1.3.7/modules/renter/hostdb/hostdb_test.go (about)

     1  package hostdb
     2  
     3  import (
     4  	"io/ioutil"
     5  	"math"
     6  	"os"
     7  	"path/filepath"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/NebulousLabs/Sia/build"
    12  	"github.com/NebulousLabs/Sia/crypto"
    13  	"github.com/NebulousLabs/Sia/modules"
    14  	"github.com/NebulousLabs/Sia/modules/consensus"
    15  	"github.com/NebulousLabs/Sia/modules/gateway"
    16  	"github.com/NebulousLabs/Sia/modules/miner"
    17  	"github.com/NebulousLabs/Sia/modules/renter/hostdb/hosttree"
    18  	"github.com/NebulousLabs/Sia/modules/transactionpool"
    19  	"github.com/NebulousLabs/Sia/modules/wallet"
    20  	"github.com/NebulousLabs/Sia/persist"
    21  	"github.com/NebulousLabs/Sia/types"
    22  )
    23  
    24  // hdbTester contains a hostdb and all dependencies.
    25  type hdbTester struct {
    26  	cs        modules.ConsensusSet
    27  	gateway   modules.Gateway
    28  	miner     modules.TestMiner
    29  	tpool     modules.TransactionPool
    30  	wallet    modules.Wallet
    31  	walletKey crypto.TwofishKey
    32  
    33  	hdb *HostDB
    34  
    35  	persistDir string
    36  }
    37  
    38  // bareHostDB returns a HostDB with its fields initialized, but without any
    39  // dependencies or scanning threads. It is only intended for use in unit tests.
    40  func bareHostDB() *HostDB {
    41  	hdb := &HostDB{
    42  		log: persist.NewLogger(ioutil.Discard),
    43  	}
    44  	hdb.hostTree = hosttree.New(hdb.calculateHostWeight)
    45  	return hdb
    46  }
    47  
    48  // makeHostDBEntry makes a new host entry with a random public key
    49  func makeHostDBEntry() modules.HostDBEntry {
    50  	dbe := modules.HostDBEntry{}
    51  	_, pk := crypto.GenerateKeyPair()
    52  
    53  	dbe.AcceptingContracts = true
    54  	dbe.PublicKey = types.Ed25519PublicKey(pk)
    55  	dbe.ScanHistory = modules.HostDBScans{{
    56  		Timestamp: time.Now(),
    57  		Success:   true,
    58  	}}
    59  	return dbe
    60  }
    61  
    62  // newHDBTester returns a tester object wrapping a HostDB and some extra
    63  // information for testing.
    64  func newHDBTester(name string) (*hdbTester, error) {
    65  	return newHDBTesterDeps(name, modules.ProdDependencies)
    66  }
    67  
    68  // newHDBTesterDeps returns a tester object wrapping a HostDB and some extra
    69  // information for testing, using the provided dependencies for the hostdb.
    70  func newHDBTesterDeps(name string, deps modules.Dependencies) (*hdbTester, error) {
    71  	if testing.Short() {
    72  		panic("should not be calling newHDBTester during short tests")
    73  	}
    74  	testDir := build.TempDir("HostDB", name)
    75  
    76  	g, err := gateway.New("localhost:0", false, filepath.Join(testDir, modules.GatewayDir))
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	cs, err := consensus.New(g, false, filepath.Join(testDir, modules.ConsensusDir))
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	tp, err := transactionpool.New(cs, g, filepath.Join(testDir, modules.TransactionPoolDir))
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	w, err := wallet.New(cs, tp, filepath.Join(testDir, modules.WalletDir))
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  	m, err := miner.New(cs, tp, w, filepath.Join(testDir, modules.MinerDir))
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  	hdb, err := NewCustomHostDB(g, cs, filepath.Join(testDir, modules.RenterDir), deps)
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  
   101  	hdbt := &hdbTester{
   102  		cs:      cs,
   103  		gateway: g,
   104  		miner:   m,
   105  		tpool:   tp,
   106  		wallet:  w,
   107  
   108  		hdb: hdb,
   109  
   110  		persistDir: testDir,
   111  	}
   112  
   113  	err = hdbt.initWallet()
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	return hdbt, nil
   119  }
   120  
   121  // initWallet creates a wallet key, then initializes and unlocks the wallet.
   122  func (hdbt *hdbTester) initWallet() error {
   123  	hdbt.walletKey = crypto.GenerateTwofishKey()
   124  	_, err := hdbt.wallet.Encrypt(hdbt.walletKey)
   125  	if err != nil {
   126  		return err
   127  	}
   128  	err = hdbt.wallet.Unlock(hdbt.walletKey)
   129  	if err != nil {
   130  		return err
   131  	}
   132  	return nil
   133  }
   134  
   135  // TestAverageContractPrice tests the AverageContractPrice method, which also depends on the
   136  // randomHosts method.
   137  func TestAverageContractPrice(t *testing.T) {
   138  	hdb := bareHostDB()
   139  
   140  	// empty
   141  	if avg := hdb.AverageContractPrice(); !avg.IsZero() {
   142  		t.Error("average of empty hostdb should be zero:", avg)
   143  	}
   144  
   145  	// with one host
   146  	h1 := makeHostDBEntry()
   147  	h1.ContractPrice = types.NewCurrency64(100)
   148  	hdb.hostTree.Insert(h1)
   149  	if avg := hdb.AverageContractPrice(); avg.Cmp(h1.ContractPrice) != 0 {
   150  		t.Error("average of one host should be that host's price:", avg)
   151  	}
   152  
   153  	// with two hosts
   154  	h2 := makeHostDBEntry()
   155  	h2.ContractPrice = types.NewCurrency64(300)
   156  	hdb.hostTree.Insert(h2)
   157  	if avg := hdb.AverageContractPrice(); avg.Cmp64(200) != 0 {
   158  		t.Error("average of two hosts should be their sum/2:", avg)
   159  	}
   160  }
   161  
   162  // TestNew tests the New function.
   163  func TestNew(t *testing.T) {
   164  	if testing.Short() {
   165  		t.SkipNow()
   166  	}
   167  	testDir := build.TempDir("HostDB", t.Name())
   168  	g, err := gateway.New("localhost:0", false, filepath.Join(testDir, modules.GatewayDir))
   169  	if err != nil {
   170  		t.Fatal(err)
   171  	}
   172  	cs, err := consensus.New(g, false, filepath.Join(testDir, modules.ConsensusDir))
   173  	if err != nil {
   174  		t.Fatal(err)
   175  	}
   176  
   177  	// Vanilla HDB, nothing should go wrong.
   178  	hdbName := filepath.Join(testDir, modules.RenterDir)
   179  	_, err = New(g, cs, hdbName+"1")
   180  	if err != nil {
   181  		t.Fatal(err)
   182  	}
   183  
   184  	// Nil gateway.
   185  	_, err = New(nil, cs, hdbName+"2")
   186  	if err != errNilGateway {
   187  		t.Fatalf("expected %v, got %v", errNilGateway, err)
   188  	}
   189  	// Nil consensus set.
   190  	_, err = New(g, nil, hdbName+"3")
   191  	if err != errNilCS {
   192  		t.Fatalf("expected %v, got %v", errNilCS, err)
   193  	}
   194  	// Bad persistDir.
   195  	_, err = New(g, cs, "")
   196  	if !os.IsNotExist(err) {
   197  		t.Fatalf("expected invalid directory, got %v", err)
   198  	}
   199  }
   200  
   201  // quitAfterLoadDeps will quit startup in newHostDB
   202  type disableScanLoopDeps struct {
   203  	modules.ProductionDependencies
   204  }
   205  
   206  // Send a disrupt signal to the quitAfterLoad codebreak.
   207  func (*disableScanLoopDeps) Disrupt(s string) bool {
   208  	if s == "disableScanLoop" {
   209  		return true
   210  	}
   211  	return false
   212  }
   213  
   214  // TestRandomHosts tests the hostdb's exported RandomHosts method.
   215  func TestRandomHosts(t *testing.T) {
   216  	if testing.Short() {
   217  		t.SkipNow()
   218  	}
   219  	hdbt, err := newHDBTesterDeps(t.Name(), &disableScanLoopDeps{})
   220  	if err != nil {
   221  		t.Fatal(err)
   222  	}
   223  
   224  	entries := make(map[string]modules.HostDBEntry)
   225  	nEntries := int(1e3)
   226  	for i := 0; i < nEntries; i++ {
   227  		entry := makeHostDBEntry()
   228  		entries[string(entry.PublicKey.Key)] = entry
   229  		err := hdbt.hdb.hostTree.Insert(entry)
   230  		if err != nil {
   231  			t.Error(err)
   232  		}
   233  	}
   234  
   235  	// Check that all hosts can be queried.
   236  	for i := 0; i < 25; i++ {
   237  		hosts, err := hdbt.hdb.RandomHosts(nEntries, nil)
   238  		if err != nil {
   239  			t.Fatal("Failed to get hosts", err)
   240  		}
   241  		if len(hosts) != nEntries {
   242  			t.Errorf("RandomHosts returned few entries. got %v wanted %v\n", len(hosts), nEntries)
   243  		}
   244  		dupCheck := make(map[string]modules.HostDBEntry)
   245  		for _, host := range hosts {
   246  			_, exists := entries[string(host.PublicKey.Key)]
   247  			if !exists {
   248  				t.Error("hostdb returning host that doesn't exist.")
   249  			}
   250  			_, exists = dupCheck[string(host.PublicKey.Key)]
   251  			if exists {
   252  				t.Error("RandomHosts returning duplicates")
   253  			}
   254  			dupCheck[string(host.PublicKey.Key)] = host
   255  		}
   256  	}
   257  
   258  	// Base case, fill out a map exposing hosts from a single RH query.
   259  	dupCheck1 := make(map[string]modules.HostDBEntry)
   260  	hosts, err := hdbt.hdb.RandomHosts(nEntries/2, nil)
   261  	if err != nil {
   262  		t.Fatal("Failed to get hosts", err)
   263  	}
   264  	if len(hosts) != nEntries/2 {
   265  		t.Fatalf("RandomHosts returned few entries. got %v wanted %v\n", len(hosts), nEntries/2)
   266  	}
   267  	for _, host := range hosts {
   268  		_, exists := entries[string(host.PublicKey.Key)]
   269  		if !exists {
   270  			t.Error("hostdb returning host that doesn't exist.")
   271  		}
   272  		_, exists = dupCheck1[string(host.PublicKey.Key)]
   273  		if exists {
   274  			t.Error("RandomHosts returning duplicates")
   275  		}
   276  		dupCheck1[string(host.PublicKey.Key)] = host
   277  	}
   278  
   279  	// Iterative case. Check that every time you query for random hosts, you
   280  	// get different responses.
   281  	for i := 0; i < 10; i++ {
   282  		dupCheck2 := make(map[string]modules.HostDBEntry)
   283  		var overlap, disjoint bool
   284  		hosts, err = hdbt.hdb.RandomHosts(nEntries/2, nil)
   285  		if err != nil {
   286  			t.Fatal("Failed to get hosts", err)
   287  		}
   288  		if len(hosts) != nEntries/2 {
   289  			t.Fatalf("RandomHosts returned few entries. got %v wanted %v\n", len(hosts), nEntries/2)
   290  		}
   291  		for _, host := range hosts {
   292  			_, exists := entries[string(host.PublicKey.Key)]
   293  			if !exists {
   294  				t.Error("hostdb returning host that doesn't exist.")
   295  			}
   296  			_, exists = dupCheck2[string(host.PublicKey.Key)]
   297  			if exists {
   298  				t.Error("RandomHosts returning duplicates")
   299  			}
   300  			_, exists = dupCheck1[string(host.PublicKey.Key)]
   301  			if exists {
   302  				overlap = true
   303  			} else {
   304  				disjoint = true
   305  			}
   306  			dupCheck2[string(host.PublicKey.Key)] = host
   307  
   308  		}
   309  		if !overlap || !disjoint {
   310  			t.Error("Random hosts does not seem to be random")
   311  		}
   312  		dupCheck1 = dupCheck2
   313  	}
   314  
   315  	// Try exclude list by excluding every host except for the last one, and
   316  	// doing a random select.
   317  	for i := 0; i < 25; i++ {
   318  		hosts, err := hdbt.hdb.RandomHosts(nEntries, nil)
   319  		if err != nil {
   320  			t.Fatal("Failed to get hosts", err)
   321  		}
   322  		var exclude []types.SiaPublicKey
   323  		for j := 1; j < len(hosts); j++ {
   324  			exclude = append(exclude, hosts[j].PublicKey)
   325  		}
   326  		rand, err := hdbt.hdb.RandomHosts(1, exclude)
   327  		if err != nil {
   328  			t.Fatal("Failed to get hosts", err)
   329  		}
   330  		if len(rand) != 1 {
   331  			t.Fatal("wrong number of hosts returned")
   332  		}
   333  		if string(rand[0].PublicKey.Key) != string(hosts[0].PublicKey.Key) {
   334  			t.Error("exclude list seems to be excluding the wrong hosts.")
   335  		}
   336  
   337  		// Try again but request more hosts than are available.
   338  		rand, err = hdbt.hdb.RandomHosts(5, exclude)
   339  		if err != nil {
   340  			t.Fatal("Failed to get hosts", err)
   341  		}
   342  		if len(rand) != 1 {
   343  			t.Fatal("wrong number of hosts returned")
   344  		}
   345  		if string(rand[0].PublicKey.Key) != string(hosts[0].PublicKey.Key) {
   346  			t.Error("exclude list seems to be excluding the wrong hosts.")
   347  		}
   348  
   349  		// Create an include map, and decrease the number of excluded hosts.
   350  		// Make sure all hosts returned by rand function are in the include
   351  		// map.
   352  		includeMap := make(map[string]struct{})
   353  		for j := 0; j < 50; j++ {
   354  			includeMap[string(hosts[j].PublicKey.Key)] = struct{}{}
   355  		}
   356  		exclude = exclude[49:]
   357  
   358  		// Select only 20 hosts.
   359  		dupCheck := make(map[string]struct{})
   360  		rand, err = hdbt.hdb.RandomHosts(20, exclude)
   361  		if err != nil {
   362  			t.Fatal("Failed to get hosts", err)
   363  		}
   364  		if len(rand) != 20 {
   365  			t.Error("random hosts is returning the wrong number of hosts")
   366  		}
   367  		for _, host := range rand {
   368  			_, exists := dupCheck[string(host.PublicKey.Key)]
   369  			if exists {
   370  				t.Error("RandomHosts is seleccting duplicates")
   371  			}
   372  			dupCheck[string(host.PublicKey.Key)] = struct{}{}
   373  			_, exists = includeMap[string(host.PublicKey.Key)]
   374  			if !exists {
   375  				t.Error("RandomHosts returning excluded hosts")
   376  			}
   377  		}
   378  
   379  		// Select exactly 50 hosts.
   380  		dupCheck = make(map[string]struct{})
   381  		rand, err = hdbt.hdb.RandomHosts(50, exclude)
   382  		if err != nil {
   383  			t.Fatal("Failed to get hosts", err)
   384  		}
   385  		if len(rand) != 50 {
   386  			t.Error("random hosts is returning the wrong number of hosts")
   387  		}
   388  		for _, host := range rand {
   389  			_, exists := dupCheck[string(host.PublicKey.Key)]
   390  			if exists {
   391  				t.Error("RandomHosts is seleccting duplicates")
   392  			}
   393  			dupCheck[string(host.PublicKey.Key)] = struct{}{}
   394  			_, exists = includeMap[string(host.PublicKey.Key)]
   395  			if !exists {
   396  				t.Error("RandomHosts returning excluded hosts")
   397  			}
   398  		}
   399  
   400  		// Select 100 hosts.
   401  		dupCheck = make(map[string]struct{})
   402  		rand, err = hdbt.hdb.RandomHosts(100, exclude)
   403  		if err != nil {
   404  			t.Fatal("Failed to get hosts", err)
   405  		}
   406  		if len(rand) != 50 {
   407  			t.Error("random hosts is returning the wrong number of hosts")
   408  		}
   409  		for _, host := range rand {
   410  			_, exists := dupCheck[string(host.PublicKey.Key)]
   411  			if exists {
   412  				t.Error("RandomHosts is seleccting duplicates")
   413  			}
   414  			dupCheck[string(host.PublicKey.Key)] = struct{}{}
   415  			_, exists = includeMap[string(host.PublicKey.Key)]
   416  			if !exists {
   417  				t.Error("RandomHosts returning excluded hosts")
   418  			}
   419  		}
   420  	}
   421  }
   422  
   423  // TestRemoveNonexistingHostFromHostTree checks that the host tree interface
   424  // correctly responds to having a nonexisting host removed from the host tree.
   425  func TestRemoveNonexistingHostFromHostTree(t *testing.T) {
   426  	if testing.Short() {
   427  		t.SkipNow()
   428  	}
   429  	hdbt, err := newHDBTester(t.Name())
   430  	if err != nil {
   431  		t.Fatal(err)
   432  	}
   433  
   434  	// Remove a host that doesn't exist from the tree.
   435  	err = hdbt.hdb.hostTree.Remove(types.SiaPublicKey{})
   436  	if err == nil {
   437  		t.Fatal("There should be an error, but not a panic:", err)
   438  	}
   439  }
   440  
   441  // TestUpdateHistoricInteractions is a simple check to ensure that incrementing
   442  // the recent and historic host interactions works
   443  func TestUpdateHistoricInteractions(t *testing.T) {
   444  	if testing.Short() {
   445  		t.SkipNow()
   446  	}
   447  
   448  	// create a HostDB tester without scanloop to be able to manually increment
   449  	// the interactions without interference.
   450  	hdbt, err := newHDBTesterDeps(t.Name(), &disableScanLoopDeps{})
   451  	if err != nil {
   452  		t.Fatal(err)
   453  	}
   454  
   455  	// create a HostDBEntry and add it to the tree
   456  	host := makeHostDBEntry()
   457  	err = hdbt.hdb.hostTree.Insert(host)
   458  	if err != nil {
   459  		t.Error(err)
   460  	}
   461  
   462  	// increment successful and failed interactions by 100
   463  	interactions := 100.0
   464  	for i := 0.0; i < interactions; i++ {
   465  		hdbt.hdb.IncrementSuccessfulInteractions(host.PublicKey)
   466  		hdbt.hdb.IncrementFailedInteractions(host.PublicKey)
   467  	}
   468  
   469  	// get updated host from hostdb
   470  	host, ok := hdbt.hdb.Host(host.PublicKey)
   471  	if !ok {
   472  		t.Fatal("Modified host not found in hostdb")
   473  	}
   474  
   475  	// check that recent interactions are exactly 100 and historic interactions are 0
   476  	if host.RecentFailedInteractions != interactions || host.RecentSuccessfulInteractions != interactions {
   477  		t.Errorf("Interactions should be %v but were %v and %v", interactions,
   478  			host.RecentFailedInteractions, host.RecentSuccessfulInteractions)
   479  	}
   480  	if host.HistoricFailedInteractions != 0 || host.HistoricSuccessfulInteractions != 0 {
   481  		t.Errorf("Historic Interactions should be %v but were %v and %v", 0,
   482  			host.HistoricFailedInteractions, host.HistoricSuccessfulInteractions)
   483  	}
   484  
   485  	// add single block to consensus
   486  	_, err = hdbt.miner.AddBlock()
   487  	if err != nil {
   488  		t.Fatal(err)
   489  	}
   490  
   491  	// increment interactions again by 100
   492  	for i := 0.0; i < interactions; i++ {
   493  		hdbt.hdb.IncrementSuccessfulInteractions(host.PublicKey)
   494  		hdbt.hdb.IncrementFailedInteractions(host.PublicKey)
   495  	}
   496  
   497  	// get updated host from hostdb
   498  	host, ok = hdbt.hdb.Host(host.PublicKey)
   499  	if !ok {
   500  		t.Fatal("Modified host not found in hostdb")
   501  	}
   502  
   503  	// historic actions should have incremented slightly, due to the clamp the
   504  	// full interactions should not have made it into the historic group.
   505  	if host.RecentFailedInteractions != interactions || host.RecentSuccessfulInteractions != interactions {
   506  		t.Errorf("Interactions should be %v but were %v and %v", interactions,
   507  			host.RecentFailedInteractions, host.RecentSuccessfulInteractions)
   508  	}
   509  	if host.HistoricFailedInteractions == 0 || host.HistoricSuccessfulInteractions == 0 {
   510  		t.Error("historic actions should have updated")
   511  	}
   512  
   513  	// add 200 blocks to consensus, adding large numbers of historic actions
   514  	// each time, so that the clamp does not need to be in effect anymore.
   515  	for i := 0; i < 200; i++ {
   516  		for j := uint64(0); j < 10; j++ {
   517  			hdbt.hdb.IncrementSuccessfulInteractions(host.PublicKey)
   518  			hdbt.hdb.IncrementFailedInteractions(host.PublicKey)
   519  		}
   520  		_, err = hdbt.miner.AddBlock()
   521  		if err != nil {
   522  			t.Fatal(err)
   523  		}
   524  	}
   525  
   526  	// Add five interactions
   527  	for i := 0; i < 5; i++ {
   528  		hdbt.hdb.IncrementSuccessfulInteractions(host.PublicKey)
   529  		hdbt.hdb.IncrementFailedInteractions(host.PublicKey)
   530  	}
   531  
   532  	// get updated host from hostdb
   533  	host, ok = hdbt.hdb.Host(host.PublicKey)
   534  	if !ok {
   535  		t.Fatal("Modified host not found in hostdb")
   536  	}
   537  
   538  	// check that recent interactions are exactly 5. Save the historic actions
   539  	// to check that decay is being handled correctly, and that the recent
   540  	// interactions are moved over correctly.
   541  	if host.RecentFailedInteractions != 5 || host.RecentSuccessfulInteractions != 5 {
   542  		t.Errorf("Interactions should be %v but were %v and %v", interactions,
   543  			host.RecentFailedInteractions, host.RecentSuccessfulInteractions)
   544  	}
   545  	historicFailed := host.HistoricFailedInteractions
   546  	if host.HistoricFailedInteractions != host.HistoricSuccessfulInteractions {
   547  		t.Error("historic failed and successful should have the same values")
   548  	}
   549  
   550  	// Add a single block to apply one round of decay.
   551  	_, err = hdbt.miner.AddBlock()
   552  	if err != nil {
   553  		t.Fatal(err)
   554  	}
   555  	host, ok = hdbt.hdb.Host(host.PublicKey)
   556  	if !ok {
   557  		t.Fatal("Modified host not found in hostdb")
   558  	}
   559  
   560  	// Get the historic successful and failed interactions, and see that they
   561  	// are decaying properly.
   562  	expected := historicFailed*math.Pow(historicInteractionDecay, 1) + 5
   563  	if host.HistoricFailedInteractions != expected || host.HistoricSuccessfulInteractions != expected {
   564  		t.Errorf("Historic Interactions should be %v but were %v and %v", expected,
   565  			host.HistoricFailedInteractions, host.HistoricSuccessfulInteractions)
   566  	}
   567  
   568  	// Add 10 more blocks and check the decay again, make sure it's being
   569  	// applied correctly.
   570  	for i := 0; i < 10; i++ {
   571  		_, err := hdbt.miner.AddBlock()
   572  		if err != nil {
   573  			t.Fatal(err)
   574  		}
   575  	}
   576  	host, ok = hdbt.hdb.Host(host.PublicKey)
   577  	if !ok {
   578  		t.Fatal("Modified host not found in hostdb")
   579  	}
   580  	expected = expected * math.Pow(historicInteractionDecay, 10)
   581  	if host.HistoricFailedInteractions != expected || host.HistoricSuccessfulInteractions != expected {
   582  		t.Errorf("Historic Interactions should be %v but were %v and %v", expected,
   583  			host.HistoricFailedInteractions, host.HistoricSuccessfulInteractions)
   584  	}
   585  }