gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/renter_test.go (about) 1 package renter 2 3 import ( 4 "io/ioutil" 5 "os" 6 "path/filepath" 7 "testing" 8 "time" 9 10 "gitlab.com/NebulousLabs/errors" 11 "gitlab.com/NebulousLabs/fastrand" 12 "gitlab.com/NebulousLabs/ratelimit" 13 "gitlab.com/NebulousLabs/siamux" 14 15 "gitlab.com/SkynetLabs/skyd/build" 16 skydPersist "gitlab.com/SkynetLabs/skyd/persist" 17 "gitlab.com/SkynetLabs/skyd/skymodules" 18 "gitlab.com/SkynetLabs/skyd/skymodules/renter/contractor" 19 "gitlab.com/SkynetLabs/skyd/skymodules/renter/filesystem" 20 "gitlab.com/SkynetLabs/skyd/skymodules/renter/hostdb" 21 "gitlab.com/SkynetLabs/skyd/skymodules/renter/proto" 22 "go.sia.tech/siad/crypto" 23 "go.sia.tech/siad/modules" 24 "go.sia.tech/siad/modules/consensus" 25 "go.sia.tech/siad/modules/gateway" 26 "go.sia.tech/siad/modules/host" 27 "go.sia.tech/siad/modules/miner" 28 "go.sia.tech/siad/modules/transactionpool" 29 "go.sia.tech/siad/modules/wallet" 30 "go.sia.tech/siad/persist" 31 "go.sia.tech/siad/types" 32 ) 33 34 type ( 35 // testSiacoinSender is a implementation of the SiacoinSender interface 36 // which remembers the arguments of the last call to SendSiacoins. 37 testSiacoinSender struct { 38 lastSend types.Currency 39 lastSendAddr types.UnlockHash 40 } 41 ) 42 43 // LastSend returns the arguments of the last call to SendSiacoins. 44 func (tss *testSiacoinSender) LastSend() (types.Currency, types.UnlockHash) { 45 return tss.lastSend, tss.lastSendAddr 46 } 47 48 // SendSiacoins implements the SiacoinSender interface. 49 func (tss *testSiacoinSender) SendSiacoins(amt types.Currency, addr types.UnlockHash) ([]types.Transaction, error) { 50 tss.lastSend = amt 51 tss.lastSendAddr = addr 52 return nil, nil 53 } 54 55 // renterTester contains all of the modules that are used while testing the renter. 56 type renterTester struct { 57 cs modules.ConsensusSet 58 gateway modules.Gateway 59 miner modules.TestMiner 60 tpool modules.TransactionPool 61 wallet modules.Wallet 62 63 mux *siamux.SiaMux 64 65 renter *Renter 66 dir string 67 } 68 69 // Close shuts down the renter tester. 70 func (rt *renterTester) Close() error { 71 err1 := rt.cs.Close() 72 err2 := rt.gateway.Close() 73 err3 := rt.miner.Close() 74 err4 := rt.tpool.Close() 75 err5 := rt.wallet.Close() 76 err6 := rt.mux.Close() 77 err7 := rt.renter.Close() 78 return errors.Compose(err1, err2, err3, err4, err5, err6, err7) 79 } 80 81 // addCustomHost adds a host to the test group so that it appears in the host db 82 func (rt *renterTester) addCustomHost(testdir string, deps modules.Dependencies) (modules.Host, error) { 83 // create a siamux for this particular host 84 siaMuxDir := filepath.Join(testdir, modules.SiaMuxDir) 85 mux, err := modules.NewSiaMux(siaMuxDir, testdir, "localhost:0", "localhost:0") 86 if err != nil { 87 return nil, err 88 } 89 90 h, err := host.NewCustomHost(deps, rt.cs, rt.gateway, rt.tpool, rt.wallet, mux, "localhost:0", filepath.Join(testdir, modules.HostDir)) 91 if err != nil { 92 return nil, err 93 } 94 95 // configure host to accept contracts and to have a registry. 96 settings := h.InternalSettings() 97 settings.AcceptingContracts = true 98 settings.RegistrySize = 640 * modules.RegistryEntrySize 99 err = h.SetInternalSettings(settings) 100 if err != nil { 101 return nil, err 102 } 103 104 // add storage to host 105 storageFolder := filepath.Join(testdir, "storage") 106 err = os.MkdirAll(storageFolder, 0700) 107 if err != nil { 108 return nil, err 109 } 110 err = h.AddStorageFolder(storageFolder, 1<<20) // 1 MiB 111 if err != nil { 112 return nil, err 113 } 114 115 // announce the host 116 err = h.Announce() 117 if err != nil { 118 return nil, build.ExtendErr("error announcing host", err) 119 } 120 121 // mine a block, processing the announcement 122 _, err = rt.miner.AddBlock() 123 if err != nil { 124 return nil, err 125 } 126 127 // wait for hostdb to scan host 128 activeHosts, err := rt.renter.ActiveHosts() 129 if err != nil { 130 return nil, err 131 } 132 for i := 0; i < 50 && len(activeHosts) == 0; i++ { 133 time.Sleep(time.Millisecond * 100) 134 } 135 activeHosts, err = rt.renter.ActiveHosts() 136 if err != nil { 137 return nil, err 138 } 139 if len(activeHosts) == 0 { 140 return nil, errors.New("host did not make it into the contractor hostdb in time") 141 } 142 143 return h, nil 144 } 145 146 // addHost adds a host to the test group so that it appears in the host db 147 func (rt *renterTester) addHost(name string) (modules.Host, error) { 148 return rt.addCustomHost(filepath.Join(rt.dir, name), modules.ProdDependencies) 149 } 150 151 // addRenter adds a renter to the renter tester and then make sure there is 152 // money in the wallet 153 func (rt *renterTester) addRenter(r *Renter) error { 154 rt.renter = r 155 // Mine blocks until there is money in the wallet. 156 for i := types.BlockHeight(0); i <= types.MaturityDelay; i++ { 157 _, err := rt.miner.AddBlock() 158 if err != nil { 159 return err 160 } 161 } 162 return nil 163 } 164 165 // createZeroByteFileOnDisk creates a 0 byte file on disk so that a Stat of the 166 // local path won't return an error 167 func (rt *renterTester) createZeroByteFileOnDisk() (string, error) { 168 path := filepath.Join(rt.renter.staticFileSystem.Root(), persist.RandomSuffix()) 169 err := ioutil.WriteFile(path, []byte{}, 0600) 170 if err != nil { 171 return "", err 172 } 173 return path, nil 174 } 175 176 // newTestSiaFile creates and returns a new siafile for testing. This file is 177 // marked as finished for backwards compatibility in testing. 178 func (rt *renterTester) newTestSiaFile(siaPath skymodules.SiaPath, source string, rc skymodules.ErasureCoder, size uint64) (*filesystem.FileNode, error) { 179 // Create the siafile 180 err := rt.renter.staticFileSystem.NewSiaFile(siaPath, source, rc, crypto.GenerateSiaKey(crypto.RandomCipherType()), size, persist.DefaultDiskPermissionsTest) 181 if err != nil { 182 return nil, err 183 } 184 // Open the file 185 f, err := rt.renter.staticFileSystem.OpenSiaFile(siaPath) 186 if err != nil { 187 return nil, err 188 } 189 // Mark it as finished for backwards compatibility in testing 190 err = f.SetFinished(0) 191 if err != nil { 192 return nil, err 193 } 194 return f, nil 195 } 196 197 // reloadRenter closes the given renter and then re-adds it, effectively 198 // reloading the renter. 199 func (rt *renterTester) reloadRenter(r *Renter) (*Renter, error) { 200 return rt.reloadRenterWithDependency(r, r.staticDeps) 201 } 202 203 // reloadRenterWithDependency closes the given renter and recreates it using the 204 // given dependency, it then re-adds the renter on the renter tester effectively 205 // reloading it. 206 func (rt *renterTester) reloadRenterWithDependency(r *Renter, deps skymodules.SkydDependencies) (*Renter, error) { 207 err := r.Close() 208 if err != nil { 209 return nil, err 210 } 211 212 r, err = newRenterWithDependency(rt.gateway, rt.cs, rt.wallet, rt.tpool, rt.mux, filepath.Join(rt.dir, skymodules.RenterDir), deps) 213 if err != nil { 214 return nil, err 215 } 216 217 err = rt.addRenter(r) 218 if err != nil { 219 return nil, err 220 } 221 return r, nil 222 } 223 224 // newRenterTester creates a ready-to-use renter tester with money in the 225 // wallet. 226 func newRenterTester(name string) (*renterTester, error) { 227 testdir := build.TempDir("renter", name) 228 rt, err := newRenterTesterNoRenter(testdir) 229 if err != nil { 230 return nil, err 231 } 232 233 rl := ratelimit.NewRateLimit(0, 0, 0) 234 tus := NewSkynetTUSInMemoryUploadStore() 235 r, errChan := New(rt.gateway, rt.cs, rt.wallet, rt.tpool, rt.mux, tus, rl, filepath.Join(testdir, skymodules.RenterDir)) 236 if err := <-errChan; err != nil { 237 return nil, err 238 } 239 err = rt.addRenter(r) 240 if err != nil { 241 return nil, err 242 } 243 return rt, nil 244 } 245 246 // newRenterTesterNoRenter creates all the modules for the renter tester except 247 // the renter. A renter will need to be added and blocks mined to add money to 248 // the wallet. 249 func newRenterTesterNoRenter(testdir string) (*renterTester, error) { 250 // Create the siamux 251 siaMuxDir := filepath.Join(testdir, modules.SiaMuxDir) 252 mux, err := modules.NewSiaMux(siaMuxDir, testdir, "localhost:0", "localhost:0") 253 if err != nil { 254 return nil, err 255 } 256 257 // Create the skymodules. 258 g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir)) 259 if err != nil { 260 return nil, err 261 } 262 cs, errChan := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir)) 263 if err := <-errChan; err != nil { 264 return nil, err 265 } 266 tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir)) 267 if err != nil { 268 return nil, err 269 } 270 w, err := wallet.New(cs, tp, filepath.Join(testdir, modules.WalletDir)) 271 if err != nil { 272 return nil, err 273 } 274 key := crypto.GenerateSiaKey(crypto.TypeDefaultWallet) 275 _, err = w.Encrypt(key) 276 if err != nil { 277 return nil, err 278 } 279 err = w.Unlock(key) 280 if err != nil { 281 return nil, err 282 } 283 m, err := miner.New(cs, tp, w, filepath.Join(testdir, modules.MinerDir)) 284 if err != nil { 285 return nil, err 286 } 287 288 // Assemble all pieces into a renter tester. 289 return &renterTester{ 290 mux: mux, 291 292 cs: cs, 293 gateway: g, 294 miner: m, 295 tpool: tp, 296 wallet: w, 297 298 dir: testdir, 299 }, nil 300 } 301 302 // newRenterTesterWithDependency creates a ready-to-use renter tester with money 303 // in the wallet. 304 func newRenterTesterWithDependency(name string, deps skymodules.SkydDependencies) (*renterTester, error) { 305 testdir := build.TempDir("renter", name) 306 rt, err := newRenterTesterNoRenter(testdir) 307 if err != nil { 308 return nil, err 309 } 310 311 // Create the siamux 312 siaMuxDir := filepath.Join(testdir, modules.SiaMuxDir) 313 mux, err := modules.NewSiaMux(siaMuxDir, testdir, "localhost:0", "localhost:0") 314 if err != nil { 315 return nil, err 316 } 317 318 r, err := newRenterWithDependency(rt.gateway, rt.cs, rt.wallet, rt.tpool, mux, filepath.Join(testdir, skymodules.RenterDir), deps) 319 if err != nil { 320 return nil, err 321 } 322 err = rt.addRenter(r) 323 if err != nil { 324 return nil, err 325 } 326 return rt, nil 327 } 328 329 // newRenterWithDependency creates a Renter with custom dependency 330 func newRenterWithDependency(g modules.Gateway, cs modules.ConsensusSet, wallet modules.Wallet, tpool modules.TransactionPool, mux *siamux.SiaMux, persistDir string, deps skymodules.SkydDependencies) (*Renter, error) { 331 hdb, errChan := hostdb.NewCustomHostDB(g, cs, tpool, mux, persistDir, deps) 332 if err := <-errChan; err != nil { 333 return nil, err 334 } 335 rl := ratelimit.NewRateLimit(0, 0, 0) 336 contractSet, err := proto.NewContractSet(filepath.Join(persistDir, "contracts"), rl, modules.ProdDependencies) 337 if err != nil { 338 return nil, err 339 } 340 341 logger, err := persist.NewFileLogger(filepath.Join(persistDir, "contractor.log")) 342 if err != nil { 343 return nil, err 344 } 345 346 hc, errChan := contractor.NewCustomContractor(cs, wallet, tpool, hdb, persistDir, contractSet, logger, deps) 347 if err := <-errChan; err != nil { 348 return nil, err 349 } 350 tus := NewSkynetTUSInMemoryUploadStore() 351 renter, errChan := NewCustomRenter(g, cs, tpool, hdb, wallet, hc, mux, tus, persistDir, rl, deps) 352 return renter, <-errChan 353 } 354 355 // TestRenterCanAccessEphemeralAccountHostSettings verifies that the renter has 356 // access to the host's external settings and that they include the new 357 // ephemeral account setting fields. 358 func TestRenterCanAccessEphemeralAccountHostSettings(t *testing.T) { 359 if testing.Short() { 360 t.SkipNow() 361 } 362 t.Parallel() 363 rt, err := newRenterTester(t.Name()) 364 if err != nil { 365 t.Fatal(err) 366 } 367 defer func() { 368 if err := rt.Close(); err != nil { 369 t.Fatal(err) 370 } 371 }() 372 373 // Add a host to the test group 374 h, err := rt.addHost(t.Name()) 375 if err != nil { 376 t.Fatal(err) 377 } 378 379 hostEntry, found, err := rt.renter.staticHostDB.Host(h.PublicKey()) 380 if err != nil { 381 t.Fatal(err) 382 } 383 if !found { 384 t.Fatal("Expected the newly added host to be found in the hostDB") 385 } 386 387 if hostEntry.EphemeralAccountExpiry != modules.DefaultEphemeralAccountExpiry { 388 t.Fatal("Unexpected account expiry") 389 } 390 391 if !hostEntry.MaxEphemeralAccountBalance.Equals(modules.DefaultMaxEphemeralAccountBalance) { 392 t.Fatal("Unexpected max account balance") 393 } 394 } 395 396 // TestRenterPricesDivideByZero verifies that the Price Estimation catches 397 // divide by zero errors. 398 func TestRenterPricesDivideByZero(t *testing.T) { 399 if testing.Short() { 400 t.SkipNow() 401 } 402 t.Parallel() 403 rt, err := newRenterTester(t.Name()) 404 if err != nil { 405 t.Fatal(err) 406 } 407 defer func() { 408 if err := rt.Close(); err != nil { 409 t.Fatal(err) 410 } 411 }() 412 413 // Confirm price estimation returns error if there are no hosts available 414 _, _, err = rt.renter.PriceEstimation(skymodules.Allowance{}) 415 if err == nil { 416 t.Fatal("Expected error due to no hosts") 417 } 418 419 // Add a host to the test group 420 _, err = rt.addHost(t.Name()) 421 if err != nil { 422 t.Fatal(err) 423 } 424 425 // Confirm price estimation does not return an error now that there is a 426 // host available 427 _, _, err = rt.renter.PriceEstimation(skymodules.Allowance{}) 428 if err != nil { 429 t.Fatal(err) 430 } 431 } 432 433 // TestPaySkynetFee is a unit test for paySkynetFee. 434 func TestPaySkynetFee(t *testing.T) { 435 if testing.Short() { 436 t.SkipNow() 437 } 438 t.Parallel() 439 440 testDir := build.TempDir("renter", t.Name()) 441 fileName := "test" 442 443 // Create a new history. 444 sh, err := NewSpendingHistory(testDir, fileName) 445 if err != nil { 446 t.Fatal(err) 447 } 448 449 // Declare helper for contract creation. 450 randomContract := func() skymodules.RenterContract { 451 return skymodules.RenterContract{ 452 DownloadSpending: types.NewCurrency64(fastrand.Uint64n(100)), 453 FundAccountSpending: types.NewCurrency64(fastrand.Uint64n(100)), 454 MaintenanceSpending: skymodules.MaintenanceSpending{ 455 AccountBalanceCost: types.NewCurrency64(fastrand.Uint64n(100)), 456 FundAccountCost: types.NewCurrency64(fastrand.Uint64n(100)), 457 UpdatePriceTableCost: types.NewCurrency64(fastrand.Uint64n(100)), 458 }, 459 StorageSpending: types.NewCurrency64(fastrand.Uint64n(100)), 460 UploadSpending: types.NewCurrency64(fastrand.Uint64n(100)), 461 } 462 } 463 464 // Create a contract. 465 contracts := []skymodules.RenterContract{ 466 randomContract(), 467 } 468 469 // Helper to compute spending of contracts. 470 spending := func(contracts []skymodules.RenterContract) types.Currency { 471 var spending types.Currency 472 for _, c := range contracts { 473 spending = spending.Add(c.SkynetSpending()) 474 } 475 return spending 476 } 477 478 // Helper to compute the fee from a given spending delta. 479 fee := func(delta types.Currency) types.Currency { 480 return delta.Div64(5) // 20% 481 } 482 483 // Create an address. 484 var uh types.UnlockHash 485 fastrand.Read(uh[:]) 486 487 // Create a test sender. 488 ts := &testSiacoinSender{} 489 490 // Dummy log. 491 log, err := skydPersist.NewLogger(ioutil.Discard) 492 if err != nil { 493 t.Fatal(err) 494 } 495 496 // Pay a skynet fee but the time is now so not enough time has passed. 497 threshold := types.ZeroCurrency 498 expectedTime := time.Now() 499 sh.AddSpending(types.ZeroCurrency, nil, expectedTime) 500 err = paySkynetFee(sh, ts, contracts, uh, threshold, log) 501 if err != nil { 502 t.Fatal(err) 503 } 504 if ls, lsa := ts.LastSend(); !ls.IsZero() || lsa != (types.UnlockHash{}) { 505 t.Fatal("money was paid even though it shouldn't", ls, lsa) 506 } 507 if ls, lsbd := sh.LastSpending(); !ls.IsZero() || lsbd != expectedTime { 508 t.Fatal("history shouldn't have been updated", ls, lsbd, expectedTime) 509 } 510 511 // Last spending was 48 hours ago which is enough. 512 expectedTime = time.Now().AddDate(0, 0, -2) 513 sh.AddSpending(types.ZeroCurrency, nil, expectedTime) 514 err = paySkynetFee(sh, ts, contracts, uh, threshold, log) 515 if err != nil { 516 t.Fatal(err) 517 } 518 expectedSpending := spending(contracts) 519 expectedFee := fee(expectedSpending) 520 if ls, lsa := ts.LastSend(); !ls.Equals(expectedFee) || lsa != uh { 521 t.Fatal("wrong payment", ls, expectedFee, lsa, uh) 522 } 523 if ls, lsbd := sh.LastSpending(); !ls.Equals(expectedSpending) || lsbd.Before(expectedTime) { 524 t.Fatal("wrong history", ls, expectedFee, lsbd, expectedTime) 525 } 526 527 // Add another contract. 528 contracts = append(contracts, randomContract()) 529 530 // Time is 48 hours ago but spending didn't change. Nothing happens. 531 expectedTime = time.Now().AddDate(0, 0, -2) 532 sh.AddSpending(expectedSpending, nil, expectedTime) 533 oldContracts := contracts[:1] 534 err = paySkynetFee(sh, ts, oldContracts, uh, threshold, log) 535 if err != nil { 536 t.Fatal(err) 537 } 538 if ls, lsa := ts.LastSend(); !ls.Equals(expectedFee) || lsa != uh { 539 t.Fatal("wrong payment", ls, expectedFee, lsa, uh) 540 } 541 if ls, lsbd := sh.LastSpending(); !ls.Equals(expectedSpending) || lsbd != expectedTime { 542 t.Fatal("wrong history", ls, expectedSpending, lsbd, expectedTime) 543 } 544 545 // Spending increased. Payment expected. 546 err = paySkynetFee(sh, ts, contracts, uh, threshold, log) 547 if err != nil { 548 t.Fatal(err) 549 } 550 expectedSpending = spending(contracts) 551 expectedFee = fee(expectedSpending.Sub(spending(oldContracts))) 552 if ls, lsa := ts.LastSend(); !ls.Equals(expectedFee) || lsa != uh { 553 t.Fatal("wrong payment", ls, expectedFee, lsa, uh) 554 } 555 if ls, lsbd := sh.LastSpending(); !ls.Equals(expectedSpending) || lsbd.Before(expectedTime) { 556 t.Fatal("wrong history", ls, expectedSpending, lsbd, expectedTime) 557 } 558 559 // Add another contract. 560 contracts = append(contracts, randomContract()) 561 oldContracts = contracts[:2] 562 563 // Spending increased again but the threshold is too low. 564 oldExpectedSpending := expectedSpending 565 oldExpectedFee := expectedFee 566 expectedSpending = spending(contracts) 567 expectedFee = fee(expectedSpending.Sub(spending(oldContracts))) 568 threshold = expectedFee.Add64(1) 569 err = paySkynetFee(sh, ts, contracts, uh, threshold, log) 570 if err != nil { 571 t.Fatal(err) 572 } 573 if ls, lsa := ts.LastSend(); !ls.Equals(oldExpectedFee) || lsa != uh { 574 t.Fatal("wrong payment", ls, oldExpectedFee, lsa, uh) 575 } 576 if ls, lsbd := sh.LastSpending(); !ls.Equals(oldExpectedSpending) || lsbd.Before(expectedTime) { 577 t.Fatal("wrong history", ls, oldExpectedSpending, lsbd, expectedTime) 578 } 579 } 580 581 // TestUpdateRentercontractsAndUtilities is a unit test for updateRenterContractsAndUtilities. 582 func TestUpdateRenterContractsAndUtilities(t *testing.T) { 583 _, pkOffline := crypto.GenerateKeyPair() 584 _, pkOnline := crypto.GenerateKeyPair() 585 586 isOffline := func(spk types.SiaPublicKey) bool { 587 return spk.String() != types.Ed25519PublicKey(pkOnline).String() 588 } 589 590 // GFR contract that is offline. 591 gfrOffline := skymodules.RenterContract{ 592 HostPublicKey: types.Ed25519PublicKey(pkOffline), 593 Utility: skymodules.ContractUtility{ 594 GoodForRenew: true, 595 }, 596 } 597 fastrand.Read(gfrOffline.ID[:]) 598 599 // !GFR contract that is online 600 nGFROnline := skymodules.RenterContract{ 601 HostPublicKey: types.Ed25519PublicKey(pkOnline), 602 Utility: skymodules.ContractUtility{ 603 GoodForRenew: false, 604 }, 605 } 606 fastrand.Read(nGFROnline.ID[:]) 607 608 // !GFR contract that is offline and got the same host key as 609 // gfrOffline. 610 nGFRSameHost := skymodules.RenterContract{ 611 HostPublicKey: gfrOffline.HostPublicKey, 612 Utility: skymodules.ContractUtility{ 613 GoodForRenew: false, 614 }, 615 } 616 fastrand.Read(nGFRSameHost.ID[:]) 617 618 // Run this multiple times to make sure different ordering with the 619 // in-memory maps doesn't produce different results. 620 for i := 0; i < 100; i++ { 621 // Run updateRenterContractsAndUtilities. gfOffline is added 622 // twice to make sure we deduplicate the 'used' keys. 623 cu := updateRenterContractsAndUtilities([]skymodules.RenterContract{gfrOffline, gfrOffline, nGFROnline, nGFRSameHost}, isOffline) 624 625 // Check number of contracts. 626 if len(cu.contracts) != 2 { 627 t.Fatal("invalid number of contracts", len(cu.contracts)) 628 } 629 // Check number of gfr hosts. 630 if len(cu.goodForRenew) != 2 { 631 t.Fatal("invalid number of gfr hosts", len(cu.goodForRenew)) 632 } 633 gfr, ok := cu.goodForRenew[gfrOffline.HostPublicKey.String()] 634 if !ok { 635 t.Fatal("key not found") 636 } 637 if !gfr { 638 t.Fatal("should be gfr") 639 } 640 gfr, ok = cu.goodForRenew[nGFROnline.HostPublicKey.String()] 641 if !ok { 642 t.Fatal("key not found") 643 } 644 if gfr { 645 t.Fatal("should be !gfr") 646 } 647 // Check number of offline hosts. 648 if len(cu.goodForRenew) != 2 { 649 t.Fatal("invalid number of gfr hosts", len(cu.goodForRenew)) 650 } 651 offline, ok := cu.goodForRenew[gfrOffline.HostPublicKey.String()] 652 if !ok { 653 t.Fatal("key not found") 654 } 655 if !offline { 656 t.Fatal("should be offline") 657 } 658 offline, ok = cu.goodForRenew[nGFROnline.HostPublicKey.String()] 659 if !ok { 660 t.Fatal("key not found") 661 } 662 if offline { 663 t.Fatal("should be online") 664 } 665 // Check used hosts. 666 if len(cu.used) != 1 { 667 t.Fatal("invalid number of used hosts", len(cu.used)) 668 } 669 } 670 }