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 }