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 }