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 }