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

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