github.com/Synthesix/Sia@v1.3.3-0.20180413141344-f863baeed3ca/modules/renter/contractor/contractor_test.go (about) 1 package contractor 2 3 import ( 4 "errors" 5 "os" 6 "reflect" 7 "testing" 8 "time" 9 10 "github.com/Synthesix/Sia/build" 11 "github.com/Synthesix/Sia/modules" 12 "github.com/Synthesix/Sia/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() modules.TransactionBuilder { return nil } 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) Host(types.SiaPublicKey) (settings modules.HostDBEntry, ok bool) { return } 38 func (newStub) IncrementSuccessfulInteractions(key types.SiaPublicKey) { return } 39 func (newStub) IncrementFailedInteractions(key types.SiaPublicKey) { return } 40 func (newStub) RandomHosts(int, []types.SiaPublicKey) []modules.HostDBEntry { return nil } 41 func (newStub) ScoreBreakdown(modules.HostDBEntry) modules.HostScoreBreakdown { 42 return modules.HostScoreBreakdown{} 43 } 44 45 // TestNew tests the New function. 46 func TestNew(t *testing.T) { 47 if testing.Short() { 48 t.SkipNow() 49 } 50 // Using a stub implementation of the dependencies is fine, as long as its 51 // non-nil. 52 var stub newStub 53 dir := build.TempDir("contractor", t.Name()) 54 55 // Sane values. 56 _, err := New(stub, stub, stub, stub, dir) 57 if err != nil { 58 t.Fatalf("expected nil, got %v", err) 59 } 60 61 // Nil consensus set. 62 _, err = New(nil, stub, stub, stub, dir) 63 if err != errNilCS { 64 t.Fatalf("expected %v, got %v", errNilCS, err) 65 } 66 67 // Nil wallet. 68 _, err = New(stub, nil, stub, stub, dir) 69 if err != errNilWallet { 70 t.Fatalf("expected %v, got %v", errNilWallet, err) 71 } 72 73 // Nil transaction pool. 74 _, err = New(stub, stub, nil, stub, dir) 75 if err != errNilTpool { 76 t.Fatalf("expected %v, got %v", errNilTpool, err) 77 } 78 79 // Bad persistDir. 80 _, err = New(stub, stub, stub, stub, "") 81 if !os.IsNotExist(err) { 82 t.Fatalf("expected invalid directory, got %v", err) 83 } 84 } 85 86 // TestResolveID tests the ResolveID method. 87 func TestResolveID(t *testing.T) { 88 c := &Contractor{ 89 renewedIDs: map[types.FileContractID]types.FileContractID{ 90 {1}: {2}, 91 {2}: {3}, 92 {3}: {4}, 93 {5}: {6}, 94 }, 95 } 96 tests := []struct { 97 id types.FileContractID 98 resolved types.FileContractID 99 }{ 100 {types.FileContractID{0}, types.FileContractID{0}}, 101 {types.FileContractID{1}, types.FileContractID{4}}, 102 {types.FileContractID{2}, types.FileContractID{4}}, 103 {types.FileContractID{3}, types.FileContractID{4}}, 104 {types.FileContractID{4}, types.FileContractID{4}}, 105 {types.FileContractID{5}, types.FileContractID{6}}, 106 } 107 for _, test := range tests { 108 if r := c.ResolveID(test.id); r != test.resolved { 109 t.Errorf("expected %v -> %v, got %v", test.id, test.resolved, r) 110 } 111 } 112 } 113 114 // TestAllowance tests the Allowance method. 115 func TestAllowance(t *testing.T) { 116 c := &Contractor{ 117 allowance: modules.Allowance{ 118 Funds: types.NewCurrency64(1), 119 Period: 2, 120 Hosts: 3, 121 }, 122 } 123 a := c.Allowance() 124 if a.Funds.Cmp(c.allowance.Funds) != 0 || 125 a.Period != c.allowance.Period || 126 a.Hosts != c.allowance.Hosts { 127 t.Fatal("Allowance did not return correct allowance:", a, c.allowance) 128 } 129 } 130 131 // stubHostDB mocks the hostDB dependency using zero-valued implementations of 132 // its methods. 133 type stubHostDB struct{} 134 135 func (stubHostDB) AllHosts() (hs []modules.HostDBEntry) { return } 136 func (stubHostDB) ActiveHosts() (hs []modules.HostDBEntry) { return } 137 func (stubHostDB) Host(types.SiaPublicKey) (h modules.HostDBEntry, ok bool) { return } 138 func (stubHostDB) IncrementSuccessfulInteractions(key types.SiaPublicKey) { return } 139 func (stubHostDB) IncrementFailedInteractions(key types.SiaPublicKey) { return } 140 func (stubHostDB) PublicKey() (spk types.SiaPublicKey) { return } 141 func (stubHostDB) RandomHosts(int, []types.SiaPublicKey) (hs []modules.HostDBEntry) { return } 142 func (stubHostDB) ScoreBreakdown(modules.HostDBEntry) modules.HostScoreBreakdown { 143 return modules.HostScoreBreakdown{} 144 } 145 146 // TestAllowancePeriodTracking verifies that the contractor tracks its current 147 // period correctly as renewals occur. 148 func TestAllowancePeriodTracking(t *testing.T) { 149 if testing.Short() { 150 t.SkipNow() 151 } 152 t.Parallel() 153 154 _, c, m, err := newTestingTrio(t.Name()) 155 if err != nil { 156 t.Fatal(err) 157 } 158 159 // set an allowance 160 c.mu.Lock() 161 initialHeight := c.blockHeight 162 c.mu.Unlock() 163 testAllowance := modules.Allowance{ 164 Funds: types.SiacoinPrecision.Mul64(5000), 165 RenewWindow: 10, 166 Hosts: 1, 167 Period: 20, 168 } 169 err = c.SetAllowance(testAllowance) 170 if err != nil { 171 t.Fatal(err) 172 } 173 err = build.Retry(50, 100*time.Millisecond, func() error { 174 if len(c.Contracts()) != 1 { 175 return errors.New("allowance forming seems to have failed") 176 } 177 return nil 178 }) 179 if err != nil { 180 t.Error(err) 181 } 182 if c.CurrentPeriod() != initialHeight { 183 t.Fatal("expected current period to start at", initialHeight, "got", c.CurrentPeriod()) 184 } 185 // mine until one before the renew window, current period should stay 186 // constant 187 for i := types.BlockHeight(0); i < testAllowance.RenewWindow-1; i++ { 188 _, err = m.AddBlock() 189 if err != nil { 190 t.Fatal(err) 191 } 192 } 193 if c.CurrentPeriod() != initialHeight { 194 t.Fatal("current period should not have incremented, wanted", initialHeight, "got", c.CurrentPeriod()) 195 } 196 // mine another another block. current period should increment. 197 _, err = m.AddBlock() 198 if err != nil { 199 t.Fatal(err) 200 } 201 c.mu.Lock() 202 height := c.blockHeight 203 c.mu.Unlock() 204 if c.CurrentPeriod() != height { 205 t.Fatal("unexpected period", c.CurrentPeriod(), "wanted", height) 206 } 207 } 208 209 // TestAllowanceSpending verifies that the contractor will not spend more or 210 // less than the allowance if uploading causes repeated early renewal, and that 211 // correct spending metrics are returned, even across renewals. 212 func TestAllowanceSpending(t *testing.T) { 213 if testing.Short() { 214 t.SkipNow() 215 } 216 t.Parallel() 217 218 // create testing trio 219 h, c, m, err := newTestingTrio(t.Name()) 220 if err != nil { 221 t.Fatal(err) 222 } 223 224 // make the host's upload price very high so this test requires less 225 // computation 226 settings := h.InternalSettings() 227 settings.MinUploadBandwidthPrice = types.SiacoinPrecision.Div64(10) 228 err = h.SetInternalSettings(settings) 229 if err != nil { 230 t.Fatal(err) 231 } 232 err = h.Announce() 233 if err != nil { 234 t.Fatal(err) 235 } 236 _, err = m.AddBlock() 237 if err != nil { 238 t.Fatal(err) 239 } 240 err = build.Retry(50, 100*time.Millisecond, func() error { 241 if len(c.hdb.RandomHosts(1, nil)) == 0 { 242 return errors.New("host has not been scanned yet") 243 } 244 return nil 245 }) 246 if err != nil { 247 t.Fatal(err) 248 } 249 250 // set an allowance 251 testAllowance := modules.Allowance{ 252 Funds: types.SiacoinPrecision.Mul64(6000), 253 RenewWindow: 100, 254 Hosts: 1, 255 Period: 200, 256 } 257 err = c.SetAllowance(testAllowance) 258 if err != nil { 259 t.Fatal(err) 260 } 261 err = build.Retry(50, 100*time.Millisecond, func() error { 262 if len(c.Contracts()) != 1 { 263 return errors.New("allowance forming seems to have failed") 264 } 265 return nil 266 }) 267 if err != nil { 268 t.Error(err) 269 } 270 271 // exhaust a contract and add a block several times. Despite repeatedly 272 // running out of funds, the contractor should not spend more than the 273 // allowance. 274 for i := 0; i < 15; i++ { 275 for _, contract := range c.Contracts() { 276 ed, err := c.Editor(contract.ID, nil) 277 if err != nil { 278 continue 279 } 280 281 // upload 10 sectors to the contract 282 for sec := 0; sec < 10; sec++ { 283 ed.Upload(make([]byte, modules.SectorSize)) 284 } 285 err = ed.Close() 286 if err != nil { 287 t.Fatal(err) 288 } 289 } 290 _, err := m.AddBlock() 291 if err != nil { 292 t.Fatal(err) 293 } 294 } 295 296 var minerRewards types.Currency 297 w := c.wallet.(*WalletBridge).W.(modules.Wallet) 298 txns, err := w.Transactions(0, 1000) 299 if err != nil { 300 t.Fatal(err) 301 } 302 for _, txn := range txns { 303 for _, so := range txn.Outputs { 304 if so.FundType == types.SpecifierMinerPayout { 305 minerRewards = minerRewards.Add(so.Value) 306 } 307 } 308 } 309 balance, _, _ := w.ConfirmedBalance() 310 spent := minerRewards.Sub(balance) 311 if spent.Cmp(testAllowance.Funds) > 0 { 312 t.Fatal("contractor spent too much money: spent", spent.HumanString(), "allowance funds:", testAllowance.Funds.HumanString()) 313 } 314 315 // we should have spent at least the allowance minus the cost of one more refresh 316 refreshCost := c.Contracts()[0].TotalCost.Mul64(2) 317 expectedMinSpending := testAllowance.Funds.Sub(refreshCost) 318 if spent.Cmp(expectedMinSpending) < 0 { 319 t.Fatal("contractor spent to little money: spent", spent.HumanString(), "expected at least:", expectedMinSpending.HumanString()) 320 } 321 322 // PeriodSpending should reflect the amount of spending accurately 323 reportedSpending := c.PeriodSpending() 324 if reportedSpending.ContractSpending.Cmp(spent) != 0 { 325 t.Fatal("reported incorrect spending for this billing cycle: got", reportedSpending.ContractSpending.HumanString(), "wanted", spent.HumanString()) 326 } 327 328 // enter a new period. PeriodSpending should reset. 329 c.mu.Lock() 330 renewHeight := c.blockHeight + c.allowance.RenewWindow 331 blocksToMine := renewHeight - c.blockHeight 332 c.mu.Unlock() 333 for i := types.BlockHeight(0); i < blocksToMine; i++ { 334 _, err = m.AddBlock() 335 if err != nil { 336 t.Fatal(err) 337 } 338 } 339 340 // Retry to give the threadedMaintenenace some time to finish 341 var newReportedSpending modules.ContractorSpending 342 err = build.Retry(100, 100*time.Millisecond, func() error { 343 newReportedSpending = c.PeriodSpending() 344 if reflect.DeepEqual(newReportedSpending, reportedSpending) { 345 return errors.New("reported spending was identical after entering a renew period") 346 } 347 return nil 348 }) 349 if err != nil { 350 t.Fatal(err) 351 } 352 353 if newReportedSpending.Unspent.Cmp(reportedSpending.Unspent) <= 0 { 354 t.Fatal("expected newReportedSpending to have more unspent") 355 } 356 } 357 358 // TestIntegrationSetAllowance tests the SetAllowance method. 359 func TestIntegrationSetAllowance(t *testing.T) { 360 if testing.Short() { 361 t.SkipNow() 362 } 363 // create testing trio 364 _, c, m, err := newTestingTrio(t.Name()) 365 if err != nil { 366 t.Fatal(err) 367 } 368 369 // this test requires two hosts: create another one 370 h, err := newTestingHost(build.TempDir("hostdata", ""), c.cs.(modules.ConsensusSet), c.tpool.(modules.TransactionPool)) 371 if err != nil { 372 t.Fatal(err) 373 } 374 375 // announce the extra host 376 err = h.Announce() 377 if err != nil { 378 t.Fatal(err) 379 } 380 381 // mine a block, processing the announcement 382 _, err = m.AddBlock() 383 if err != nil { 384 t.Fatal(err) 385 } 386 387 // wait for hostdb to scan host 388 for i := 0; i < 100 && len(c.hdb.RandomHosts(1, nil)) == 0; i++ { 389 time.Sleep(time.Millisecond * 50) 390 } 391 392 // cancel allowance 393 var a modules.Allowance 394 err = c.SetAllowance(a) 395 if err != nil { 396 t.Fatal(err) 397 } 398 399 // bad args 400 a.Hosts = 1 401 err = c.SetAllowance(a) 402 if err != errAllowanceZeroPeriod { 403 t.Errorf("expected %q, got %q", errAllowanceZeroPeriod, err) 404 } 405 a.Period = 20 406 err = c.SetAllowance(a) 407 if err != ErrAllowanceZeroWindow { 408 t.Errorf("expected %q, got %q", ErrAllowanceZeroWindow, err) 409 } 410 a.RenewWindow = 20 411 err = c.SetAllowance(a) 412 if err != errAllowanceWindowSize { 413 t.Errorf("expected %q, got %q", errAllowanceWindowSize, err) 414 } 415 416 // reasonable values; should succeed 417 a.Funds = types.SiacoinPrecision.Mul64(100) 418 a.RenewWindow = 10 419 err = c.SetAllowance(a) 420 if err != nil { 421 t.Fatal(err) 422 } 423 err = build.Retry(50, 100*time.Millisecond, func() error { 424 if len(c.Contracts()) != 1 { 425 return errors.New("allowance forming seems to have failed") 426 } 427 return nil 428 }) 429 if err != nil { 430 t.Error(err) 431 } 432 433 // set same allowance; should no-op 434 err = c.SetAllowance(a) 435 if err != nil { 436 t.Fatal(err) 437 } 438 c.mu.Lock() 439 clen := c.contracts.Len() 440 c.mu.Unlock() 441 if clen != 1 { 442 t.Fatal("expected 1 contract, got", clen) 443 } 444 445 _, err = m.AddBlock() 446 if err != nil { 447 t.Fatal(err) 448 } 449 450 // set allowance with Hosts = 2; should only form one new contract 451 a.Hosts = 2 452 err = c.SetAllowance(a) 453 if err != nil { 454 t.Fatal(err) 455 } 456 err = build.Retry(50, 100*time.Millisecond, func() error { 457 if len(c.Contracts()) != 2 { 458 return errors.New("allowance forming seems to have failed") 459 } 460 return nil 461 }) 462 if err != nil { 463 t.Fatal(err) 464 } 465 466 // set allowance with Funds*2; should trigger renewal of both contracts 467 a.Funds = a.Funds.Mul64(2) 468 err = c.SetAllowance(a) 469 if err != nil { 470 t.Fatal(err) 471 } 472 err = build.Retry(50, 100*time.Millisecond, func() error { 473 if len(c.Contracts()) != 2 { 474 return errors.New("allowance forming seems to have failed") 475 } 476 return nil 477 }) 478 if err != nil { 479 t.Error(err) 480 } 481 482 // delete one of the contracts and set allowance with Funds*2; should 483 // trigger 1 renewal and 1 new contract 484 c.mu.Lock() 485 ids := c.contracts.IDs() 486 contract, _ := c.contracts.Acquire(ids[0]) 487 c.contracts.Delete(contract) 488 c.mu.Unlock() 489 a.Funds = a.Funds.Mul64(2) 490 err = c.SetAllowance(a) 491 if err != nil { 492 t.Fatal(err) 493 } 494 err = build.Retry(50, 100*time.Millisecond, func() error { 495 if len(c.Contracts()) != 2 { 496 return errors.New("allowance forming seems to have failed") 497 } 498 return nil 499 }) 500 if err != nil { 501 t.Fatal(err) 502 } 503 } 504 505 // testWalletShim is used to test the walletBridge type. 506 type testWalletShim struct { 507 nextAddressCalled bool 508 startTxnCalled bool 509 } 510 511 // These stub implementations for the walletShim interface set their respective 512 // booleans to true, allowing tests to verify that they have been called. 513 func (ws *testWalletShim) NextAddress() (types.UnlockConditions, error) { 514 ws.nextAddressCalled = true 515 return types.UnlockConditions{}, nil 516 } 517 func (ws *testWalletShim) StartTransaction() modules.TransactionBuilder { 518 ws.startTxnCalled = true 519 return nil 520 } 521 522 // TestWalletBridge tests the walletBridge type. 523 func TestWalletBridge(t *testing.T) { 524 shim := new(testWalletShim) 525 bridge := WalletBridge{shim} 526 bridge.NextAddress() 527 if !shim.nextAddressCalled { 528 t.Error("NextAddress was not called on the shim") 529 } 530 bridge.StartTransaction() 531 if !shim.startTxnCalled { 532 t.Error("StartTransaction was not called on the shim") 533 } 534 }