gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/renter/hostdb/hosttree/hosttree_test.go (about)

     1  package hosttree
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net"
     7  	"strconv"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	"gitlab.com/NebulousLabs/fastrand"
    13  
    14  	"gitlab.com/SiaPrime/SiaPrime/crypto"
    15  	"gitlab.com/SiaPrime/SiaPrime/modules"
    16  	siasync "gitlab.com/SiaPrime/SiaPrime/sync"
    17  	"gitlab.com/SiaPrime/SiaPrime/types"
    18  )
    19  
    20  // customScoreBreakdown is a helper struct to create scoreBreakdown's for
    21  // testing which return a specific Score.
    22  type customScoreBreakdown struct {
    23  	score types.Currency
    24  }
    25  
    26  func (sb customScoreBreakdown) Score() types.Currency {
    27  	return sb.score
    28  }
    29  func (sb customScoreBreakdown) ConversionRate(_ types.Currency) float64 {
    30  	return 0.0
    31  }
    32  func (sb customScoreBreakdown) HostScoreBreakdown(_ types.Currency, _, _, _ bool) modules.HostScoreBreakdown {
    33  	return modules.HostScoreBreakdown{}
    34  }
    35  func newCustomScoreBreakdown(score types.Currency) ScoreBreakdown {
    36  	return customScoreBreakdown{
    37  		score: score,
    38  	}
    39  }
    40  
    41  func verifyTree(tree *HostTree, nentries int) error {
    42  	expectedWeight := tree.root.entry.weight.Mul64(uint64(nentries))
    43  	if tree.root.weight.Cmp(expectedWeight) != 0 {
    44  		return fmt.Errorf("expected weight is incorrect: got %v wanted %v", tree.root.weight, expectedWeight)
    45  	}
    46  
    47  	// Check that the length of activeHosts and the count of hostTree are
    48  	// consistent.
    49  	if len(tree.hosts) != nentries {
    50  		return fmt.Errorf("unexpected number of hosts: got %v wanted %v", len(tree.hosts), nentries)
    51  	}
    52  
    53  	// Select many random hosts and do naive statistical analysis on the
    54  	// results.
    55  	if !testing.Short() {
    56  		// Pull a bunch of random hosts and count how many times we pull each
    57  		// host.
    58  		selectionMap := make(map[string]int)
    59  		expected := 100
    60  		for i := 0; i < expected*nentries; i++ {
    61  			entries := tree.SelectRandom(1, nil, nil)
    62  			if len(entries) == 0 {
    63  				return errors.New("no hosts")
    64  			}
    65  			selectionMap[entries[0].PublicKey.String()]++
    66  		}
    67  
    68  		// See if each host was selected enough times.
    69  		errorBound := 64 // Pretty large, but will still detect if something is seriously wrong.
    70  		for _, count := range selectionMap {
    71  			if count < expected-errorBound || count > expected+errorBound {
    72  				return errors.New("error bound was breached")
    73  			}
    74  		}
    75  	}
    76  
    77  	// Try removing an re-adding all hosts.
    78  	var removedEntries []*hostEntry
    79  	for {
    80  		if tree.root.weight.IsZero() {
    81  			break
    82  		}
    83  		randWeight := fastrand.BigIntn(tree.root.weight.Big())
    84  		node := tree.root.nodeAtWeight(types.NewCurrency(randWeight))
    85  		node.remove()
    86  		delete(tree.hosts, node.entry.PublicKey.String())
    87  
    88  		// remove the entry from the hostdb so it won't be selected as a
    89  		// repeat
    90  		removedEntries = append(removedEntries, node.entry)
    91  	}
    92  	for _, entry := range removedEntries {
    93  		tree.Insert(entry.HostDBEntry)
    94  	}
    95  	return nil
    96  }
    97  
    98  // makeHostDBEntry makes a new host entry with a random public key.
    99  func makeHostDBEntry() modules.HostDBEntry {
   100  	dbe := modules.HostDBEntry{}
   101  
   102  	_, pk := crypto.GenerateKeyPair()
   103  	dbe.AcceptingContracts = true
   104  	dbe.PublicKey = types.Ed25519PublicKey(pk)
   105  	dbe.ScanHistory = modules.HostDBScans{{
   106  		Timestamp: time.Now(),
   107  		Success:   true,
   108  	}}
   109  
   110  	return dbe
   111  }
   112  
   113  func TestHostTree(t *testing.T) {
   114  	tree := New(func(hdbe modules.HostDBEntry) ScoreBreakdown {
   115  		return newCustomScoreBreakdown(types.NewCurrency64(20))
   116  	}, modules.ProductionResolver{})
   117  
   118  	// Create a bunch of host entries of equal weight.
   119  	firstInsertions := 64
   120  	var keys []types.SiaPublicKey
   121  	for i := 0; i < firstInsertions; i++ {
   122  		entry := makeHostDBEntry()
   123  		keys = append(keys, entry.PublicKey)
   124  		err := tree.Insert(entry)
   125  		if err != nil {
   126  			t.Fatal(err)
   127  		}
   128  	}
   129  	err := verifyTree(tree, firstInsertions)
   130  	if err != nil {
   131  		t.Error(err)
   132  	}
   133  
   134  	var removed []types.SiaPublicKey
   135  	// Randomly remove hosts from the tree and check that it is still in order.
   136  	for _, key := range keys {
   137  		if fastrand.Intn(1) == 0 {
   138  			err := tree.Remove(key)
   139  			if err != nil {
   140  				t.Fatal(err)
   141  			}
   142  			removed = append(removed, key)
   143  		}
   144  	}
   145  
   146  	err = verifyTree(tree, firstInsertions-len(removed))
   147  	if err != nil {
   148  		t.Error(err)
   149  	}
   150  
   151  	// Do some more insertions.
   152  	secondInsertions := 64
   153  	for i := firstInsertions; i < firstInsertions+secondInsertions; i++ {
   154  		entry := makeHostDBEntry()
   155  		tree.Insert(entry)
   156  	}
   157  	err = verifyTree(tree, firstInsertions-len(removed)+secondInsertions)
   158  	if err != nil {
   159  		t.Error(err)
   160  	}
   161  }
   162  
   163  // Verify that inserting, fetching, deleting, and modifying in parallel from
   164  // the hosttree does not cause inconsistency.
   165  func TestHostTreeParallel(t *testing.T) {
   166  	if testing.Short() {
   167  		t.SkipNow()
   168  	}
   169  
   170  	tree := New(func(dbe modules.HostDBEntry) ScoreBreakdown {
   171  		return newCustomScoreBreakdown(types.NewCurrency64(10))
   172  	}, modules.ProductionResolver{})
   173  
   174  	// spin up 100 goroutines all randomly inserting, removing, modifying, and
   175  	// fetching nodes from the tree.
   176  	var tg siasync.ThreadGroup
   177  	nthreads := 100
   178  	nelements := 0
   179  	var mu sync.Mutex
   180  	for i := 0; i < nthreads; i++ {
   181  		go func() {
   182  			tg.Add()
   183  			defer tg.Done()
   184  
   185  			inserted := make(map[string]modules.HostDBEntry)
   186  			randEntry := func() *modules.HostDBEntry {
   187  				for _, entry := range inserted {
   188  					return &entry
   189  				}
   190  				return nil
   191  			}
   192  
   193  			for {
   194  				select {
   195  				case <-tg.StopChan():
   196  					return
   197  				default:
   198  					switch fastrand.Intn(4) {
   199  
   200  					// INSERT
   201  					case 0:
   202  						entry := makeHostDBEntry()
   203  						err := tree.Insert(entry)
   204  						if err != nil {
   205  							t.Error(err)
   206  						}
   207  						inserted[entry.PublicKey.String()] = entry
   208  
   209  						mu.Lock()
   210  						nelements++
   211  						mu.Unlock()
   212  
   213  					// REMOVE
   214  					case 1:
   215  						entry := randEntry()
   216  						if entry == nil {
   217  							continue
   218  						}
   219  						err := tree.Remove(entry.PublicKey)
   220  						if err != nil {
   221  							t.Error(err)
   222  						}
   223  						delete(inserted, entry.PublicKey.String())
   224  
   225  						mu.Lock()
   226  						nelements--
   227  						mu.Unlock()
   228  
   229  					// MODIFY
   230  					case 2:
   231  						entry := randEntry()
   232  						if entry == nil {
   233  							continue
   234  						}
   235  						newentry := makeHostDBEntry()
   236  						newentry.PublicKey = entry.PublicKey
   237  						newentry.NetAddress = "127.0.0.1:31337"
   238  
   239  						err := tree.Modify(newentry)
   240  						if err != nil {
   241  							t.Error(err)
   242  						}
   243  						inserted[entry.PublicKey.String()] = newentry
   244  
   245  					// FETCH
   246  					case 3:
   247  						tree.SelectRandom(3, nil, nil)
   248  					}
   249  				}
   250  			}
   251  		}()
   252  	}
   253  
   254  	// let these goroutines operate on the tree for 5 seconds
   255  	time.Sleep(time.Second * 5)
   256  
   257  	// stop the goroutines
   258  	tg.Stop()
   259  
   260  	// verify the consistency of the tree
   261  	err := verifyTree(tree, int(nelements))
   262  	if err != nil {
   263  		t.Fatal(err)
   264  	}
   265  }
   266  
   267  func TestHostTreeModify(t *testing.T) {
   268  	tree := New(func(dbe modules.HostDBEntry) ScoreBreakdown {
   269  		return newCustomScoreBreakdown(types.NewCurrency64(10))
   270  	}, modules.ProductionResolver{})
   271  
   272  	treeSize := 100
   273  	var keys []types.SiaPublicKey
   274  	for i := 0; i < treeSize; i++ {
   275  		entry := makeHostDBEntry()
   276  		keys = append(keys, entry.PublicKey)
   277  		err := tree.Insert(entry)
   278  		if err != nil {
   279  			t.Fatal(err)
   280  		}
   281  	}
   282  
   283  	// should fail with a nonexistent key
   284  	err := tree.Modify(modules.HostDBEntry{})
   285  	if err != ErrNoSuchHost {
   286  		t.Fatalf("modify should fail with ErrNoSuchHost when provided a nonexistent public key. Got error: %v\n", err)
   287  	}
   288  
   289  	targetKey := keys[fastrand.Intn(treeSize)]
   290  
   291  	oldEntry := tree.hosts[targetKey.String()].entry
   292  	newEntry := makeHostDBEntry()
   293  	newEntry.AcceptingContracts = false
   294  	newEntry.PublicKey = oldEntry.PublicKey
   295  
   296  	err = tree.Modify(newEntry)
   297  	if err != nil {
   298  		t.Fatal(err)
   299  	}
   300  
   301  	if tree.hosts[targetKey.String()].entry.AcceptingContracts {
   302  		t.Fatal("modify did not update host entry")
   303  	}
   304  }
   305  
   306  // TestVariedWeights runs broad statistical tests on selecting hosts with
   307  // multiple different weights.
   308  func TestVariedWeights(t *testing.T) {
   309  	if testing.Short() {
   310  		t.SkipNow()
   311  	}
   312  
   313  	// insert i hosts with the weights 0, 1, ..., i-1. 100e3 selections will be made
   314  	// per weight added to the tree, the total number of selections necessary
   315  	// will be tallied up as hosts are created.
   316  	i := 0
   317  
   318  	tree := New(func(dbe modules.HostDBEntry) ScoreBreakdown {
   319  		return newCustomScoreBreakdown(types.NewCurrency64(uint64(i)))
   320  	}, modules.ProductionResolver{})
   321  
   322  	hostCount := 5
   323  	expectedPerWeight := int(10e3)
   324  	selections := 0
   325  	for i = 0; i < hostCount; i++ {
   326  		entry := makeHostDBEntry()
   327  		tree.Insert(entry)
   328  		selections += i * expectedPerWeight
   329  	}
   330  
   331  	// Perform many random selections, noting which host was selected each
   332  	// time.
   333  	selectionMap := make(map[string]int)
   334  	for i := 0; i < selections; i++ {
   335  		randEntry := tree.SelectRandom(1, nil, nil)
   336  		if len(randEntry) == 0 {
   337  			t.Fatal("no hosts!")
   338  		}
   339  		node, exists := tree.hosts[randEntry[0].PublicKey.String()]
   340  		if !exists {
   341  			t.Fatal("can't find randomly selected node in tree")
   342  		}
   343  		selectionMap[node.entry.weight.String()]++
   344  	}
   345  
   346  	// Check that each host was selected an expected number of times. An error
   347  	// will be reported if the host of 0 weight is ever selected.
   348  	acceptableError := 0.2
   349  	for weight, timesSelected := range selectionMap {
   350  		intWeight, err := strconv.Atoi(weight)
   351  		if err != nil {
   352  			t.Fatal(err)
   353  		}
   354  
   355  		expectedSelected := float64(intWeight * expectedPerWeight)
   356  		if float64(expectedSelected)*acceptableError > float64(timesSelected) || float64(expectedSelected)/acceptableError < float64(timesSelected) {
   357  			t.Error("weighted list not selecting in a uniform distribution based on weight")
   358  			t.Error(expectedSelected)
   359  			t.Error(timesSelected)
   360  		}
   361  	}
   362  }
   363  
   364  // TestRepeatInsert inserts 2 hosts with the same public key.
   365  func TestRepeatInsert(t *testing.T) {
   366  	if testing.Short() {
   367  		t.SkipNow()
   368  	}
   369  
   370  	tree := New(func(dbe modules.HostDBEntry) ScoreBreakdown {
   371  		return newCustomScoreBreakdown(types.NewCurrency64(10))
   372  	}, modules.ProductionResolver{})
   373  
   374  	entry1 := makeHostDBEntry()
   375  	entry2 := entry1
   376  
   377  	tree.Insert(entry1)
   378  	tree.Insert(entry2)
   379  	if len(tree.hosts) != 1 {
   380  		t.Error("inserting the same entry twice should result in only 1 entry")
   381  	}
   382  }
   383  
   384  // TestNodeAtWeight tests the nodeAtWeight method.
   385  func TestNodeAtWeight(t *testing.T) {
   386  	weight := types.NewCurrency64(10)
   387  	// create hostTree
   388  	tree := New(func(dbe modules.HostDBEntry) ScoreBreakdown {
   389  		return newCustomScoreBreakdown(weight)
   390  	}, modules.ProductionResolver{})
   391  
   392  	entry := makeHostDBEntry()
   393  	err := tree.Insert(entry)
   394  	if err != nil {
   395  		t.Fatal(err)
   396  	}
   397  
   398  	h := tree.root.nodeAtWeight(weight)
   399  	if h.entry.HostDBEntry.PublicKey.String() != entry.PublicKey.String() {
   400  		t.Errorf("nodeAtWeight returned wrong node: expected %v, got %v", entry, h.entry)
   401  	}
   402  }
   403  
   404  // TestRandomHosts probes the SelectRandom method.
   405  func TestRandomHosts(t *testing.T) {
   406  	// Create the tree.
   407  	tree := New(func(dbe modules.HostDBEntry) ScoreBreakdown {
   408  		return newCustomScoreBreakdown(dbe.StoragePrice)
   409  	}, modules.ProductionResolver{})
   410  
   411  	// Empty.
   412  	hosts := tree.SelectRandom(1, nil, nil)
   413  	if len(hosts) != 0 {
   414  		t.Errorf("empty hostdb returns %v hosts: %v", len(hosts), hosts)
   415  	}
   416  
   417  	// Insert 3 hosts to be selected.
   418  	entry1 := makeHostDBEntry()
   419  	entry1.StoragePrice = types.NewCurrency64(2)
   420  	entry2 := makeHostDBEntry()
   421  	entry2.StoragePrice = types.NewCurrency64(3)
   422  	entry3 := makeHostDBEntry()
   423  	entry3.StoragePrice = types.NewCurrency64(4)
   424  
   425  	if err := tree.Insert(entry1); err != nil {
   426  		t.Fatal(err)
   427  	}
   428  	if err := tree.Insert(entry2); err != nil {
   429  		t.Fatal(err)
   430  	}
   431  	if err := tree.Insert(entry3); err != nil {
   432  		t.Fatal(err)
   433  	}
   434  
   435  	if len(tree.hosts) != 3 {
   436  		t.Error("wrong number of hosts")
   437  	}
   438  	if tree.root.weight.Cmp(types.NewCurrency64(9)) != 0 {
   439  		t.Error("unexpected weight at initialization")
   440  		t.Error(tree.root.weight)
   441  	}
   442  
   443  	// Grab 1 random host.
   444  	randHosts := tree.SelectRandom(1, nil, nil)
   445  	if len(randHosts) != 1 {
   446  		t.Error("didn't get 1 hosts")
   447  	}
   448  
   449  	// Grab 2 random hosts.
   450  	randHosts = tree.SelectRandom(2, nil, nil)
   451  	if len(randHosts) != 2 {
   452  		t.Fatal("didn't get 2 hosts")
   453  	}
   454  	if randHosts[0].PublicKey.String() == randHosts[1].PublicKey.String() {
   455  		t.Error("doubled up")
   456  	}
   457  
   458  	// Grab 3 random hosts.
   459  	randHosts = tree.SelectRandom(3, nil, nil)
   460  	if len(randHosts) != 3 {
   461  		t.Fatal("didn't get 3 hosts", len(randHosts))
   462  	}
   463  
   464  	if randHosts[0].PublicKey.String() == randHosts[1].PublicKey.String() || randHosts[0].PublicKey.String() == randHosts[2].PublicKey.String() || randHosts[1].PublicKey.String() == randHosts[2].PublicKey.String() {
   465  		t.Error("doubled up")
   466  	}
   467  
   468  	// Grab 4 random hosts. 3 should be returned.
   469  	randHosts = tree.SelectRandom(4, nil, nil)
   470  	if len(randHosts) != 3 {
   471  		t.Fatal("didn't get 3 hosts", len(randHosts))
   472  	}
   473  
   474  	entry4 := makeHostDBEntry()
   475  	entry4.StoragePrice = types.NewCurrency64(1)
   476  	if err := tree.Insert(entry4); err != nil {
   477  		t.Fatal(err)
   478  	}
   479  
   480  	// Grab 4 random hosts. 3 should be returned because the fourth has a score
   481  	// of 1.
   482  	randHosts = tree.SelectRandom(4, nil, nil)
   483  	if len(randHosts) != 3 {
   484  		t.Error("didn't get 3 hosts")
   485  	}
   486  
   487  	// Grab 4 random hosts. 3 should be returned.
   488  	randHosts = tree.SelectRandom(4, nil, nil)
   489  	if len(randHosts) != 3 {
   490  		t.Error("didn't get 3 hosts")
   491  	}
   492  
   493  	if randHosts[0].PublicKey.String() == randHosts[1].PublicKey.String() || randHosts[0].PublicKey.String() == randHosts[2].PublicKey.String() || randHosts[1].PublicKey.String() == randHosts[2].PublicKey.String() {
   494  		t.Error("doubled up")
   495  	}
   496  
   497  	// Ask for 3 hosts that are not in randHosts. No hosts should be
   498  	// returned.
   499  	uniqueHosts := tree.SelectRandom(3, []types.SiaPublicKey{
   500  		randHosts[0].PublicKey,
   501  		randHosts[1].PublicKey,
   502  		randHosts[2].PublicKey,
   503  	}, nil)
   504  	if len(uniqueHosts) != 0 {
   505  		t.Error("didn't get 0 hosts")
   506  	}
   507  
   508  	// Ask for 3 hosts, blacklisting non-existent hosts. 3 should be returned.
   509  	randHosts = tree.SelectRandom(3, []types.SiaPublicKey{{}, {}, {}}, nil)
   510  	if len(randHosts) != 3 {
   511  		t.Error("didn't get 3 hosts")
   512  	}
   513  
   514  	if randHosts[0].PublicKey.String() == randHosts[1].PublicKey.String() || randHosts[0].PublicKey.String() == randHosts[2].PublicKey.String() || randHosts[1].PublicKey.String() == randHosts[2].PublicKey.String() {
   515  		t.Error("doubled up")
   516  	}
   517  }
   518  
   519  // testHostTreeFilterResolver is a resolver for the TestTwoAddresses test.
   520  type testHostTreeFilterResolver struct{}
   521  
   522  func (testHostTreeFilterResolver) LookupIP(host string) ([]net.IP, error) {
   523  	switch host {
   524  	case "host1":
   525  		return []net.IP{{127, 0, 0, 1}}, nil
   526  	case "host2":
   527  		return []net.IP{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}, nil
   528  	case "host3":
   529  		return []net.IP{{127, 0, 0, 2}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}}, nil
   530  	default:
   531  		panic("shouldn't happen")
   532  	}
   533  }
   534  
   535  // TestHostTreeFilter verifies that two hosts with the IP submask won't be
   536  // returned by SelectRandom.
   537  func TestHostTreeFilter(t *testing.T) {
   538  	// Insert 3 hosts to be selected.
   539  	entry1 := makeHostDBEntry()
   540  	entry1.NetAddress = "host1:1234"
   541  	entry2 := makeHostDBEntry()
   542  	entry2.NetAddress = "host2:1234"
   543  	entry3 := makeHostDBEntry()
   544  	entry3.NetAddress = "host3:1234"
   545  
   546  	// Create the tree.
   547  	tree := New(func(dbe modules.HostDBEntry) ScoreBreakdown {
   548  		// All entries have the same weight.
   549  		return newCustomScoreBreakdown(types.NewCurrency64(uint64(10)))
   550  	}, testHostTreeFilterResolver{})
   551  
   552  	// Insert host1 and host2. Both should be returned by SelectRandom.
   553  	tree.Insert(entry1)
   554  	tree.Insert(entry2)
   555  	if len(tree.SelectRandom(2, nil, nil)) != 2 {
   556  		t.Error("Expected both hosts to be returned")
   557  	}
   558  
   559  	// Get a new empty tree.
   560  	tree = New(func(dbe modules.HostDBEntry) ScoreBreakdown {
   561  		// All entries have the same weight.
   562  		return newCustomScoreBreakdown(types.NewCurrency64(uint64(10)))
   563  	}, testHostTreeFilterResolver{})
   564  
   565  	// Insert host1 and host3. Only a single host should be returned if IPFiltering switched on
   566  	// and both if IPFilter switched off
   567  	tree.Insert(entry1)
   568  	tree.Insert(entry3)
   569  	tree.SetFilterByIPEnabled(true)
   570  	if numHosts := len(tree.SelectRandom(2, nil, nil)); numHosts != 1 {
   571  		t.Error("Expected only one host with FilterByIP enabled but was", numHosts)
   572  	}
   573  	tree.SetFilterByIPEnabled(false)
   574  	if numHosts := len(tree.SelectRandom(2, nil, nil)); numHosts != 2 {
   575  		t.Error("Expected two hosts with FilterByIP disabled but was", numHosts)
   576  	}
   577  
   578  	// Add host2 to the tree to have all 3 hosts in it.
   579  	tree.Insert(entry2)
   580  
   581  	// Call SelectRandom again but ignore host 2. This should give us only 1
   582  	// host.
   583  	if numHosts := len(tree.SelectRandom(2, nil, []types.SiaPublicKey{entry2.PublicKey})); numHosts != 1 {
   584  		t.Error("Expected only one host but was", numHosts)
   585  	}
   586  
   587  	// Call SelectRandom again but ignore host 3. This should give us no host.
   588  	if numHosts := len(tree.SelectRandom(2, nil, []types.SiaPublicKey{entry3.PublicKey})); numHosts != 0 {
   589  		t.Error("Expected 0 hosts but was", numHosts)
   590  	}
   591  }