github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/modules/renter/contractor/contractor_test.go (about) 1 package contractor 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "testing" 8 "time" 9 10 "SiaPrime/build" 11 "SiaPrime/modules" 12 "SiaPrime/types" 13 ) 14 15 // newStub is used to test the New function. It implements all of the contractor's 16 // dependencies. 17 type newStub struct{} 18 19 // consensus set stubs 20 func (newStub) ConsensusSetSubscribe(modules.ConsensusSetSubscriber, modules.ConsensusChangeID, <-chan struct{}) error { 21 return nil 22 } 23 func (newStub) Synced() bool { return true } 24 func (newStub) Unsubscribe(modules.ConsensusSetSubscriber) { return } 25 26 // wallet stubs 27 func (newStub) NextAddress() (uc types.UnlockConditions, err error) { return } 28 func (newStub) StartTransaction() (tb modules.TransactionBuilder, err error) { return } 29 30 // transaction pool stubs 31 func (newStub) AcceptTransactionSet([]types.Transaction) error { return nil } 32 func (newStub) FeeEstimation() (a types.Currency, b types.Currency) { return } 33 34 // hdb stubs 35 func (newStub) AllHosts() []modules.HostDBEntry { return nil } 36 func (newStub) ActiveHosts() []modules.HostDBEntry { return nil } 37 func (newStub) CheckForIPViolations([]types.SiaPublicKey) []types.SiaPublicKey { return nil } 38 func (newStub) Host(types.SiaPublicKey) (settings modules.HostDBEntry, ok bool) { return } 39 func (newStub) IncrementSuccessfulInteractions(key types.SiaPublicKey) { return } 40 func (newStub) IncrementFailedInteractions(key types.SiaPublicKey) { return } 41 func (newStub) RandomHosts(int, []types.SiaPublicKey, []types.SiaPublicKey) ([]modules.HostDBEntry, error) { 42 return nil, nil 43 } 44 func (newStub) ScoreBreakdown(modules.HostDBEntry) modules.HostScoreBreakdown { 45 return modules.HostScoreBreakdown{} 46 } 47 func (newStub) SetAllowance(allowance modules.Allowance) error { return nil } 48 49 // TestNew tests the New function. 50 func TestNew(t *testing.T) { 51 if testing.Short() { 52 t.SkipNow() 53 } 54 // Using a stub implementation of the dependencies is fine, as long as its 55 // non-nil. 56 var stub newStub 57 dir := build.TempDir("contractor", t.Name()) 58 59 // Sane values. 60 _, err := New(stub, stub, stub, stub, dir) 61 if err != nil { 62 t.Fatalf("expected nil, got %v", err) 63 } 64 65 // Nil consensus set. 66 _, err = New(nil, stub, stub, stub, dir) 67 if err != errNilCS { 68 t.Fatalf("expected %v, got %v", errNilCS, err) 69 } 70 71 // Nil wallet. 72 _, err = New(stub, nil, stub, stub, dir) 73 if err != errNilWallet { 74 t.Fatalf("expected %v, got %v", errNilWallet, err) 75 } 76 77 // Nil transaction pool. 78 _, err = New(stub, stub, nil, stub, dir) 79 if err != errNilTpool { 80 t.Fatalf("expected %v, got %v", errNilTpool, err) 81 } 82 83 // Bad persistDir. 84 _, err = New(stub, stub, stub, stub, "") 85 if !os.IsNotExist(err) { 86 t.Fatalf("expected invalid directory, got %v", err) 87 } 88 } 89 90 // TestAllowance tests the Allowance method. 91 func TestAllowance(t *testing.T) { 92 c := &Contractor{ 93 allowance: modules.Allowance{ 94 Funds: types.NewCurrency64(1), 95 Period: 2, 96 Hosts: 3, 97 }, 98 } 99 a := c.Allowance() 100 if a.Funds.Cmp(c.allowance.Funds) != 0 || 101 a.Period != c.allowance.Period || 102 a.Hosts != c.allowance.Hosts { 103 t.Fatal("Allowance did not return correct allowance:", a, c.allowance) 104 } 105 } 106 107 // stubHostDB mocks the hostDB dependency using zero-valued implementations of 108 // its methods. 109 type stubHostDB struct{} 110 111 func (stubHostDB) AllHosts() (hs []modules.HostDBEntry) { return } 112 func (stubHostDB) ActiveHosts() (hs []modules.HostDBEntry) { return } 113 func (stubHostDB) CheckForIPViolations([]types.SiaPublicKey) []types.SiaPublicKey { return nil } 114 func (stubHostDB) Host(types.SiaPublicKey) (h modules.HostDBEntry, ok bool) { return } 115 func (stubHostDB) IncrementSuccessfulInteractions(key types.SiaPublicKey) { return } 116 func (stubHostDB) IncrementFailedInteractions(key types.SiaPublicKey) { return } 117 func (stubHostDB) PublicKey() (spk types.SiaPublicKey) { return } 118 func (stubHostDB) RandomHosts(int, []types.SiaPublicKey, []types.SiaPublicKey) (hs []modules.HostDBEntry, _ error) { 119 return 120 } 121 func (stubHostDB) ScoreBreakdown(modules.HostDBEntry) modules.HostScoreBreakdown { 122 return modules.HostScoreBreakdown{} 123 } 124 func (stubHostDB) SetAllowance(allowance modules.Allowance) error { return nil } 125 126 // TestAllowanceSpending verifies that the contractor will not spend more or 127 // less than the allowance if uploading causes repeated early renewal, and that 128 // correct spending metrics are returned, even across renewals. 129 func TestAllowanceSpending(t *testing.T) { 130 if testing.Short() { 131 t.SkipNow() 132 } 133 t.Parallel() 134 135 // create testing trio 136 h, c, m, err := newTestingTrio(t.Name()) 137 if err != nil { 138 t.Fatal(err) 139 } 140 141 // make the host's upload price very high so this test requires less 142 // computation 143 settings := h.InternalSettings() 144 settings.MinUploadBandwidthPrice = types.SiacoinPrecision.Div64(10) 145 err = h.SetInternalSettings(settings) 146 if err != nil { 147 t.Fatal(err) 148 } 149 err = h.Announce() 150 if err != nil { 151 t.Fatal(err) 152 } 153 _, err = m.AddBlock() 154 if err != nil { 155 t.Fatal(err) 156 } 157 err = build.Retry(50, 100*time.Millisecond, func() error { 158 hosts, err := c.hdb.RandomHosts(1, nil, nil) 159 if err != nil { 160 return err 161 } 162 if len(hosts) == 0 { 163 return errors.New("host has not been scanned yet") 164 } 165 return nil 166 }) 167 if err != nil { 168 t.Fatal(err) 169 } 170 171 // set an allowance 172 testAllowance := modules.Allowance{ 173 Funds: types.SiacoinPrecision.Mul64(6000), 174 RenewWindow: 100, 175 Hosts: 1, 176 Period: 200, 177 } 178 err = c.SetAllowance(testAllowance) 179 if err != nil { 180 t.Fatal(err) 181 } 182 err = build.Retry(50, 100*time.Millisecond, func() error { 183 if len(c.Contracts()) != 1 { 184 return errors.New("allowance forming seems to have failed") 185 } 186 return nil 187 }) 188 if err != nil { 189 t.Error(err) 190 } 191 192 // exhaust a contract and add a block several times. Despite repeatedly 193 // running out of funds, the contractor should not spend more than the 194 // allowance. 195 for i := 0; i < 15; i++ { 196 for _, contract := range c.Contracts() { 197 ed, err := c.Editor(contract.HostPublicKey, nil) 198 if err != nil { 199 continue 200 } 201 202 // upload 10 sectors to the contract 203 for sec := 0; sec < 10; sec++ { 204 ed.Upload(make([]byte, modules.SectorSize)) 205 } 206 err = ed.Close() 207 if err != nil { 208 t.Fatal(err) 209 } 210 } 211 _, err := m.AddBlock() 212 if err != nil { 213 t.Fatal(err) 214 } 215 } 216 217 var minerRewards types.Currency 218 w := c.wallet.(*WalletBridge).W.(modules.Wallet) 219 txns, err := w.Transactions(0, 1000) 220 if err != nil { 221 t.Fatal(err) 222 } 223 for _, txn := range txns { 224 for _, so := range txn.Outputs { 225 if so.FundType == types.SpecifierMinerPayout { 226 minerRewards = minerRewards.Add(so.Value) 227 } 228 } 229 } 230 balance, _, _, err := w.ConfirmedBalance() 231 if err != nil { 232 t.Fatal(err) 233 } 234 spent := minerRewards.Sub(balance) 235 if spent.Cmp(testAllowance.Funds) > 0 { 236 t.Fatal("contractor spent too much money: spent", spent.HumanString(), "allowance funds:", testAllowance.Funds.HumanString()) 237 } 238 239 // we should have spent at least the allowance minus the cost of one more refresh 240 refreshCost := c.Contracts()[0].TotalCost.Mul64(2) 241 expectedMinSpending := testAllowance.Funds.Sub(refreshCost) 242 if spent.Cmp(expectedMinSpending) < 0 { 243 t.Fatal("contractor spent to little money: spent", spent.HumanString(), "expected at least:", expectedMinSpending.HumanString()) 244 } 245 246 // PeriodSpending should reflect the amount of spending accurately 247 reportedSpending := c.PeriodSpending() 248 if reportedSpending.TotalAllocated.Cmp(spent) != 0 { 249 t.Fatal("reported incorrect spending for this billing cycle: got", reportedSpending.TotalAllocated.HumanString(), "wanted", spent.HumanString()) 250 } 251 // COMPATv132 totalallocated should equal contractspending field. 252 if reportedSpending.ContractSpendingDeprecated.Cmp(reportedSpending.TotalAllocated) != 0 { 253 t.Fatal("TotalAllocated should be equal to ContractSpending for compatibility") 254 } 255 256 var expectedFees types.Currency 257 for _, contract := range c.Contracts() { 258 expectedFees = expectedFees.Add(contract.TxnFee) 259 expectedFees = expectedFees.Add(contract.SiafundFee) 260 expectedFees = expectedFees.Add(contract.ContractFee) 261 } 262 if expectedFees.Cmp(reportedSpending.ContractFees) != 0 { 263 t.Fatalf("expected %v reported fees but was %v", 264 expectedFees.HumanString(), reportedSpending.ContractFees.HumanString()) 265 } 266 } 267 268 // TestIntegrationSetAllowance tests the SetAllowance method. 269 func TestIntegrationSetAllowance(t *testing.T) { 270 if testing.Short() { 271 t.SkipNow() 272 } 273 // create testing trio 274 _, c, m, err := newTestingTrio(t.Name()) 275 if err != nil { 276 t.Fatal(err) 277 } 278 279 // this test requires two hosts: create another one 280 h, err := newTestingHost(build.TempDir("hostdata", ""), c.cs.(modules.ConsensusSet), c.tpool.(modules.TransactionPool)) 281 if err != nil { 282 t.Fatal(err) 283 } 284 285 // announce the extra host 286 err = h.Announce() 287 if err != nil { 288 t.Fatal(err) 289 } 290 291 // mine a block, processing the announcement 292 _, err = m.AddBlock() 293 if err != nil { 294 t.Fatal(err) 295 } 296 297 // wait for hostdb to scan 298 hosts, err := c.hdb.RandomHosts(1, nil, nil) 299 if err != nil { 300 t.Fatal("failed to get hosts", err) 301 } 302 for i := 0; i < 100 && len(hosts) == 0; i++ { 303 time.Sleep(time.Millisecond * 50) 304 } 305 306 // cancel allowance 307 var a modules.Allowance 308 err = c.SetAllowance(a) 309 if err != nil { 310 t.Fatal(err) 311 } 312 313 // bad args 314 a.Hosts = 1 315 err = c.SetAllowance(a) 316 if err != errAllowanceZeroPeriod { 317 t.Errorf("expected %q, got %q", errAllowanceZeroPeriod, err) 318 } 319 a.Period = 20 320 err = c.SetAllowance(a) 321 if err != ErrAllowanceZeroWindow { 322 t.Errorf("expected %q, got %q", ErrAllowanceZeroWindow, err) 323 } 324 a.RenewWindow = 20 325 err = c.SetAllowance(a) 326 if err != errAllowanceWindowSize { 327 t.Errorf("expected %q, got %q", errAllowanceWindowSize, err) 328 } 329 330 // reasonable values; should succeed 331 a.Funds = types.SiacoinPrecision.Mul64(100) 332 a.RenewWindow = 10 333 err = c.SetAllowance(a) 334 if err != nil { 335 t.Fatal(err) 336 } 337 err = build.Retry(50, 100*time.Millisecond, func() error { 338 if len(c.Contracts()) != 1 { 339 return errors.New("allowance forming seems to have failed") 340 } 341 return nil 342 }) 343 if err != nil { 344 t.Error(err) 345 } 346 347 // set same allowance; should no-op 348 err = c.SetAllowance(a) 349 if err != nil { 350 t.Fatal(err) 351 } 352 clen := c.staticContracts.Len() 353 if clen != 1 { 354 t.Fatal("expected 1 contract, got", clen) 355 } 356 357 _, err = m.AddBlock() 358 if err != nil { 359 t.Fatal(err) 360 } 361 362 // set allowance with Hosts = 2; should only form one new contract 363 a.Hosts = 2 364 err = c.SetAllowance(a) 365 if err != nil { 366 t.Fatal(err) 367 } 368 err = build.Retry(50, 100*time.Millisecond, func() error { 369 if len(c.Contracts()) != 2 { 370 return errors.New("allowance forming seems to have failed") 371 } 372 return nil 373 }) 374 if err != nil { 375 t.Fatal(err) 376 } 377 378 // set allowance with Funds*2; should trigger renewal of both contracts 379 a.Funds = a.Funds.Mul64(2) 380 err = c.SetAllowance(a) 381 if err != nil { 382 t.Fatal(err) 383 } 384 err = build.Retry(50, 100*time.Millisecond, func() error { 385 if len(c.Contracts()) != 2 { 386 return errors.New("allowance forming seems to have failed") 387 } 388 return nil 389 }) 390 if err != nil { 391 t.Error(err) 392 } 393 394 // delete one of the contracts and set allowance with Funds*2; should 395 // trigger 1 renewal and 1 new contract 396 c.mu.Lock() 397 ids := c.staticContracts.IDs() 398 contract, _ := c.staticContracts.Acquire(ids[0]) 399 c.staticContracts.Delete(contract) 400 c.mu.Unlock() 401 a.Funds = a.Funds.Mul64(2) 402 err = c.SetAllowance(a) 403 if err != nil { 404 t.Fatal(err) 405 } 406 err = build.Retry(50, 100*time.Millisecond, func() error { 407 if len(c.Contracts()) != 2 { 408 return errors.New("allowance forming seems to have failed") 409 } 410 return nil 411 }) 412 if err != nil { 413 t.Fatal(err) 414 } 415 } 416 417 // TestHostMaxDuration tests that a host will not be used if their max duration 418 // is not sufficient when renewing contracts 419 func TestHostMaxDuration(t *testing.T) { 420 if testing.Short() { 421 t.SkipNow() 422 } 423 t.Parallel() 424 425 // create testing trio 426 h, c, m, err := newTestingTrio(t.Name()) 427 if err != nil { 428 t.Fatal(err) 429 } 430 431 // Set host's MaxDuration to 5 to test if host will be skipped when contract 432 // is formed 433 settings := h.InternalSettings() 434 settings.MaxDuration = types.BlockHeight(5) 435 if err := h.SetInternalSettings(settings); err != nil { 436 t.Fatal(err) 437 } 438 // Let host settings permeate 439 err = build.Retry(50, 100*time.Millisecond, func() error { 440 host, _ := c.hdb.Host(h.PublicKey()) 441 if settings.MaxDuration != host.MaxDuration { 442 return fmt.Errorf("host max duration not set, expected %v, got %v", settings.MaxDuration, host.MaxDuration) 443 } 444 return nil 445 }) 446 if err != nil { 447 t.Fatal(err) 448 } 449 450 // Create allowance 451 a := modules.Allowance{ 452 Funds: types.SiacoinPrecision.Mul64(100), 453 Hosts: 1, 454 Period: 30, 455 RenewWindow: 20, 456 } 457 err = c.SetAllowance(a) 458 if err != nil { 459 t.Fatal(err) 460 } 461 462 // Wait for and confirm no Contract creation 463 err = build.Retry(50, 100*time.Millisecond, func() error { 464 if len(c.Contracts()) == 0 { 465 return errors.New("no contract created") 466 } 467 return nil 468 }) 469 if err == nil { 470 t.Fatal("Contract should not have been created") 471 } 472 473 // Set host's MaxDuration to 50 to test if host will now form contract 474 settings = h.InternalSettings() 475 settings.MaxDuration = types.BlockHeight(50) 476 if err := h.SetInternalSettings(settings); err != nil { 477 t.Fatal(err) 478 } 479 // Let host settings permeate 480 err = build.Retry(50, 100*time.Millisecond, func() error { 481 host, _ := c.hdb.Host(h.PublicKey()) 482 if settings.MaxDuration != host.MaxDuration { 483 return fmt.Errorf("host max duration not set, expected %v, got %v", settings.MaxDuration, host.MaxDuration) 484 } 485 return nil 486 }) 487 if err != nil { 488 t.Fatal(err) 489 } 490 _, err = m.AddBlock() 491 if err != nil { 492 t.Fatal(err) 493 } 494 495 // Wait for Contract creation 496 err = build.Retry(600, 100*time.Millisecond, func() error { 497 if len(c.Contracts()) != 1 { 498 return errors.New("no contract created") 499 } 500 return nil 501 }) 502 if err != nil { 503 t.Error(err) 504 } 505 506 // Set host's MaxDuration to 5 to test if host will be skipped when contract 507 // is renewed 508 settings = h.InternalSettings() 509 settings.MaxDuration = types.BlockHeight(5) 510 if err := h.SetInternalSettings(settings); err != nil { 511 t.Fatal(err) 512 } 513 // Let host settings permeate 514 err = build.Retry(50, 100*time.Millisecond, func() error { 515 host, _ := c.hdb.Host(h.PublicKey()) 516 if settings.MaxDuration != host.MaxDuration { 517 return fmt.Errorf("host max duration not set, expected %v, got %v", settings.MaxDuration, host.MaxDuration) 518 } 519 return nil 520 }) 521 if err != nil { 522 t.Fatal(err) 523 } 524 525 // Mine blocks to renew contract 526 for i := types.BlockHeight(0); i <= c.allowance.Period-c.allowance.RenewWindow; i++ { 527 _, err = m.AddBlock() 528 if err != nil { 529 t.Fatal(err) 530 } 531 } 532 533 // Confirm Contract is not renewed 534 err = build.Retry(50, 100*time.Millisecond, func() error { 535 if len(c.OldContracts()) == 0 { 536 return errors.New("no contract renewed") 537 } 538 return nil 539 }) 540 if err == nil { 541 t.Fatal("Contract should not have been renewed") 542 } 543 } 544 545 // TestLinkedContracts tests that the contractors maps are updated correctly 546 // when renewing contracts 547 func TestLinkedContracts(t *testing.T) { 548 if testing.Short() { 549 t.SkipNow() 550 } 551 t.Parallel() 552 553 // create testing trio 554 h, c, m, err := newTestingTrio(t.Name()) 555 if err != nil { 556 t.Fatal(err) 557 } 558 559 // Create allowance 560 a := modules.Allowance{ 561 Funds: types.SiacoinPrecision.Mul64(100), 562 Hosts: 1, 563 Period: 20, 564 RenewWindow: 10, 565 } 566 err = c.SetAllowance(a) 567 if err != nil { 568 t.Fatal(err) 569 } 570 571 // Wait for Contract creation 572 err = build.Retry(200, 100*time.Millisecond, func() error { 573 if len(c.Contracts()) != 1 { 574 return errors.New("no contract created") 575 } 576 return nil 577 }) 578 if err != nil { 579 t.Error(err) 580 } 581 582 // Confirm that maps are empty 583 if len(c.renewedFrom) != 0 { 584 t.Fatal("renewedFrom map should be empty") 585 } 586 if len(c.renewedTo) != 0 { 587 t.Fatal("renewedTo map should be empty") 588 } 589 590 // Set host's uploadbandwidthprice to zero to test divide by zero check when 591 // contracts are renewed 592 settings := h.InternalSettings() 593 settings.MinUploadBandwidthPrice = types.ZeroCurrency 594 if err := h.SetInternalSettings(settings); err != nil { 595 t.Fatal(err) 596 } 597 598 // Mine blocks to renew contract 599 for i := types.BlockHeight(0); i < c.allowance.Period-c.allowance.RenewWindow; i++ { 600 _, err = m.AddBlock() 601 if err != nil { 602 t.Fatal(err) 603 } 604 } 605 606 // Confirm Contracts got renewed 607 err = build.Retry(200, 100*time.Millisecond, func() error { 608 if len(c.Contracts()) != 1 { 609 return errors.New("no contract") 610 } 611 if len(c.OldContracts()) != 1 { 612 return errors.New("no old contract") 613 } 614 return nil 615 }) 616 if err != nil { 617 t.Error(err) 618 } 619 620 // Confirm maps are updated as expected 621 if len(c.renewedFrom) != 1 { 622 t.Fatalf("renewedFrom map should have 1 entry but has %v", len(c.renewedFrom)) 623 } 624 if len(c.renewedTo) != 1 { 625 t.Fatalf("renewedTo map should have 1 entry but has %v", len(c.renewedTo)) 626 } 627 if c.renewedFrom[c.Contracts()[0].ID] != c.OldContracts()[0].ID { 628 t.Fatalf(`Map assignment incorrect, 629 expected: 630 map[%v:%v] 631 got: 632 %v`, c.Contracts()[0].ID, c.OldContracts()[0].ID, c.renewedFrom) 633 } 634 if c.renewedTo[c.OldContracts()[0].ID] != c.Contracts()[0].ID { 635 t.Fatalf(`Map assignment incorrect, 636 expected: 637 map[%v:%v] 638 got: 639 %v`, c.OldContracts()[0].ID, c.Contracts()[0].ID, c.renewedTo) 640 } 641 } 642 643 // testWalletShim is used to test the walletBridge type. 644 type testWalletShim struct { 645 nextAddressCalled bool 646 startTxnCalled bool 647 } 648 649 // These stub implementations for the walletShim interface set their respective 650 // booleans to true, allowing tests to verify that they have been called. 651 func (ws *testWalletShim) NextAddress() (types.UnlockConditions, error) { 652 ws.nextAddressCalled = true 653 return types.UnlockConditions{}, nil 654 } 655 func (ws *testWalletShim) StartTransaction() (modules.TransactionBuilder, error) { 656 ws.startTxnCalled = true 657 return nil, nil 658 } 659 660 // TestWalletBridge tests the walletBridge type. 661 func TestWalletBridge(t *testing.T) { 662 shim := new(testWalletShim) 663 bridge := WalletBridge{shim} 664 bridge.NextAddress() 665 if !shim.nextAddressCalled { 666 t.Error("NextAddress was not called on the shim") 667 } 668 bridge.StartTransaction() 669 if !shim.startTxnCalled { 670 t.Error("StartTransaction was not called on the shim") 671 } 672 }