github.com/nebulouslabs/sia@v1.3.7/modules/renter/contractor/contractor_test.go (about) 1 package contractor 2 3 import ( 4 "errors" 5 "os" 6 "testing" 7 "time" 8 9 "github.com/NebulousLabs/Sia/build" 10 "github.com/NebulousLabs/Sia/modules" 11 "github.com/NebulousLabs/Sia/types" 12 ) 13 14 // newStub is used to test the New function. It implements all of the contractor's 15 // dependencies. 16 type newStub struct{} 17 18 // consensus set stubs 19 func (newStub) ConsensusSetSubscribe(modules.ConsensusSetSubscriber, modules.ConsensusChangeID, <-chan struct{}) error { 20 return nil 21 } 22 func (newStub) Synced() bool { return true } 23 func (newStub) Unsubscribe(modules.ConsensusSetSubscriber) { return } 24 25 // wallet stubs 26 func (newStub) NextAddress() (uc types.UnlockConditions, err error) { return } 27 func (newStub) StartTransaction() (tb modules.TransactionBuilder, err error) { return } 28 29 // transaction pool stubs 30 func (newStub) AcceptTransactionSet([]types.Transaction) error { return nil } 31 func (newStub) FeeEstimation() (a types.Currency, b types.Currency) { return } 32 33 // hdb stubs 34 func (newStub) AllHosts() []modules.HostDBEntry { return nil } 35 func (newStub) ActiveHosts() []modules.HostDBEntry { return nil } 36 func (newStub) Host(types.SiaPublicKey) (settings modules.HostDBEntry, ok bool) { return } 37 func (newStub) IncrementSuccessfulInteractions(key types.SiaPublicKey) { return } 38 func (newStub) IncrementFailedInteractions(key types.SiaPublicKey) { return } 39 func (newStub) RandomHosts(int, []types.SiaPublicKey) ([]modules.HostDBEntry, error) { return nil, nil } 40 func (newStub) ScoreBreakdown(modules.HostDBEntry) modules.HostScoreBreakdown { 41 return modules.HostScoreBreakdown{} 42 } 43 44 // TestNew tests the New function. 45 func TestNew(t *testing.T) { 46 if testing.Short() { 47 t.SkipNow() 48 } 49 // Using a stub implementation of the dependencies is fine, as long as its 50 // non-nil. 51 var stub newStub 52 dir := build.TempDir("contractor", t.Name()) 53 54 // Sane values. 55 _, err := New(stub, stub, stub, stub, dir) 56 if err != nil { 57 t.Fatalf("expected nil, got %v", err) 58 } 59 60 // Nil consensus set. 61 _, err = New(nil, stub, stub, stub, dir) 62 if err != errNilCS { 63 t.Fatalf("expected %v, got %v", errNilCS, err) 64 } 65 66 // Nil wallet. 67 _, err = New(stub, nil, stub, stub, dir) 68 if err != errNilWallet { 69 t.Fatalf("expected %v, got %v", errNilWallet, err) 70 } 71 72 // Nil transaction pool. 73 _, err = New(stub, stub, nil, stub, dir) 74 if err != errNilTpool { 75 t.Fatalf("expected %v, got %v", errNilTpool, err) 76 } 77 78 // Bad persistDir. 79 _, err = New(stub, stub, stub, stub, "") 80 if !os.IsNotExist(err) { 81 t.Fatalf("expected invalid directory, got %v", err) 82 } 83 } 84 85 // TestAllowance tests the Allowance method. 86 func TestAllowance(t *testing.T) { 87 c := &Contractor{ 88 allowance: modules.Allowance{ 89 Funds: types.NewCurrency64(1), 90 Period: 2, 91 Hosts: 3, 92 }, 93 } 94 a := c.Allowance() 95 if a.Funds.Cmp(c.allowance.Funds) != 0 || 96 a.Period != c.allowance.Period || 97 a.Hosts != c.allowance.Hosts { 98 t.Fatal("Allowance did not return correct allowance:", a, c.allowance) 99 } 100 } 101 102 // stubHostDB mocks the hostDB dependency using zero-valued implementations of 103 // its methods. 104 type stubHostDB struct{} 105 106 func (stubHostDB) AllHosts() (hs []modules.HostDBEntry) { return } 107 func (stubHostDB) ActiveHosts() (hs []modules.HostDBEntry) { return } 108 func (stubHostDB) Host(types.SiaPublicKey) (h modules.HostDBEntry, ok bool) { return } 109 func (stubHostDB) IncrementSuccessfulInteractions(key types.SiaPublicKey) { return } 110 func (stubHostDB) IncrementFailedInteractions(key types.SiaPublicKey) { return } 111 func (stubHostDB) PublicKey() (spk types.SiaPublicKey) { return } 112 func (stubHostDB) RandomHosts(int, []types.SiaPublicKey) (hs []modules.HostDBEntry, _ error) { return } 113 func (stubHostDB) ScoreBreakdown(modules.HostDBEntry) modules.HostScoreBreakdown { 114 return modules.HostScoreBreakdown{} 115 } 116 117 // TestAllowanceSpending verifies that the contractor will not spend more or 118 // less than the allowance if uploading causes repeated early renewal, and that 119 // correct spending metrics are returned, even across renewals. 120 func TestAllowanceSpending(t *testing.T) { 121 if testing.Short() { 122 t.SkipNow() 123 } 124 t.Parallel() 125 126 // create testing trio 127 h, c, m, err := newTestingTrio(t.Name()) 128 if err != nil { 129 t.Fatal(err) 130 } 131 132 // make the host's upload price very high so this test requires less 133 // computation 134 settings := h.InternalSettings() 135 settings.MinUploadBandwidthPrice = types.SiacoinPrecision.Div64(10) 136 err = h.SetInternalSettings(settings) 137 if err != nil { 138 t.Fatal(err) 139 } 140 err = h.Announce() 141 if err != nil { 142 t.Fatal(err) 143 } 144 _, err = m.AddBlock() 145 if err != nil { 146 t.Fatal(err) 147 } 148 err = build.Retry(50, 100*time.Millisecond, func() error { 149 hosts, err := c.hdb.RandomHosts(1, nil) 150 if err != nil { 151 return err 152 } 153 if len(hosts) == 0 { 154 return errors.New("host has not been scanned yet") 155 } 156 return nil 157 }) 158 if err != nil { 159 t.Fatal(err) 160 } 161 162 // set an allowance 163 testAllowance := modules.Allowance{ 164 Funds: types.SiacoinPrecision.Mul64(6000), 165 RenewWindow: 100, 166 Hosts: 1, 167 Period: 200, 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 183 // exhaust a contract and add a block several times. Despite repeatedly 184 // running out of funds, the contractor should not spend more than the 185 // allowance. 186 for i := 0; i < 15; i++ { 187 for _, contract := range c.Contracts() { 188 ed, err := c.Editor(contract.HostPublicKey, nil) 189 if err != nil { 190 continue 191 } 192 193 // upload 10 sectors to the contract 194 for sec := 0; sec < 10; sec++ { 195 ed.Upload(make([]byte, modules.SectorSize)) 196 } 197 err = ed.Close() 198 if err != nil { 199 t.Fatal(err) 200 } 201 } 202 _, err := m.AddBlock() 203 if err != nil { 204 t.Fatal(err) 205 } 206 } 207 208 var minerRewards types.Currency 209 w := c.wallet.(*WalletBridge).W.(modules.Wallet) 210 txns, err := w.Transactions(0, 1000) 211 if err != nil { 212 t.Fatal(err) 213 } 214 for _, txn := range txns { 215 for _, so := range txn.Outputs { 216 if so.FundType == types.SpecifierMinerPayout { 217 minerRewards = minerRewards.Add(so.Value) 218 } 219 } 220 } 221 balance, _, _, err := w.ConfirmedBalance() 222 if err != nil { 223 t.Fatal(err) 224 } 225 spent := minerRewards.Sub(balance) 226 if spent.Cmp(testAllowance.Funds) > 0 { 227 t.Fatal("contractor spent too much money: spent", spent.HumanString(), "allowance funds:", testAllowance.Funds.HumanString()) 228 } 229 230 // we should have spent at least the allowance minus the cost of one more refresh 231 refreshCost := c.Contracts()[0].TotalCost.Mul64(2) 232 expectedMinSpending := testAllowance.Funds.Sub(refreshCost) 233 if spent.Cmp(expectedMinSpending) < 0 { 234 t.Fatal("contractor spent to little money: spent", spent.HumanString(), "expected at least:", expectedMinSpending.HumanString()) 235 } 236 237 // PeriodSpending should reflect the amount of spending accurately 238 reportedSpending := c.PeriodSpending() 239 if reportedSpending.TotalAllocated.Cmp(spent) != 0 { 240 t.Fatal("reported incorrect spending for this billing cycle: got", reportedSpending.TotalAllocated.HumanString(), "wanted", spent.HumanString()) 241 } 242 // COMPATv132 totalallocated should equal contractspending field. 243 if reportedSpending.ContractSpendingDeprecated.Cmp(reportedSpending.TotalAllocated) != 0 { 244 t.Fatal("TotalAllocated should be equal to ContractSpending for compatibility") 245 } 246 247 var expectedFees types.Currency 248 for _, contract := range c.Contracts() { 249 expectedFees = expectedFees.Add(contract.TxnFee) 250 expectedFees = expectedFees.Add(contract.SiafundFee) 251 expectedFees = expectedFees.Add(contract.ContractFee) 252 } 253 if expectedFees.Cmp(reportedSpending.ContractFees) != 0 { 254 t.Fatalf("expected %v reported fees but was %v", 255 expectedFees.HumanString(), reportedSpending.ContractFees.HumanString()) 256 } 257 } 258 259 // TestIntegrationSetAllowance tests the SetAllowance method. 260 func TestIntegrationSetAllowance(t *testing.T) { 261 if testing.Short() { 262 t.SkipNow() 263 } 264 // create testing trio 265 _, c, m, err := newTestingTrio(t.Name()) 266 if err != nil { 267 t.Fatal(err) 268 } 269 270 // this test requires two hosts: create another one 271 h, err := newTestingHost(build.TempDir("hostdata", ""), c.cs.(modules.ConsensusSet), c.tpool.(modules.TransactionPool)) 272 if err != nil { 273 t.Fatal(err) 274 } 275 276 // announce the extra host 277 err = h.Announce() 278 if err != nil { 279 t.Fatal(err) 280 } 281 282 // mine a block, processing the announcement 283 _, err = m.AddBlock() 284 if err != nil { 285 t.Fatal(err) 286 } 287 288 // wait for hostdb to scan 289 hosts, err := c.hdb.RandomHosts(1, nil) 290 if err != nil { 291 t.Fatal("failed to get hosts", err) 292 } 293 for i := 0; i < 100 && len(hosts) == 0; i++ { 294 time.Sleep(time.Millisecond * 50) 295 } 296 297 // cancel allowance 298 var a modules.Allowance 299 err = c.SetAllowance(a) 300 if err != nil { 301 t.Fatal(err) 302 } 303 304 // bad args 305 a.Hosts = 1 306 err = c.SetAllowance(a) 307 if err != errAllowanceZeroPeriod { 308 t.Errorf("expected %q, got %q", errAllowanceZeroPeriod, err) 309 } 310 a.Period = 20 311 err = c.SetAllowance(a) 312 if err != ErrAllowanceZeroWindow { 313 t.Errorf("expected %q, got %q", ErrAllowanceZeroWindow, err) 314 } 315 a.RenewWindow = 20 316 err = c.SetAllowance(a) 317 if err != errAllowanceWindowSize { 318 t.Errorf("expected %q, got %q", errAllowanceWindowSize, err) 319 } 320 321 // reasonable values; should succeed 322 a.Funds = types.SiacoinPrecision.Mul64(100) 323 a.RenewWindow = 10 324 err = c.SetAllowance(a) 325 if err != nil { 326 t.Fatal(err) 327 } 328 err = build.Retry(50, 100*time.Millisecond, func() error { 329 if len(c.Contracts()) != 1 { 330 return errors.New("allowance forming seems to have failed") 331 } 332 return nil 333 }) 334 if err != nil { 335 t.Error(err) 336 } 337 338 // set same allowance; should no-op 339 err = c.SetAllowance(a) 340 if err != nil { 341 t.Fatal(err) 342 } 343 clen := c.staticContracts.Len() 344 if clen != 1 { 345 t.Fatal("expected 1 contract, got", clen) 346 } 347 348 _, err = m.AddBlock() 349 if err != nil { 350 t.Fatal(err) 351 } 352 353 // set allowance with Hosts = 2; should only form one new contract 354 a.Hosts = 2 355 err = c.SetAllowance(a) 356 if err != nil { 357 t.Fatal(err) 358 } 359 err = build.Retry(50, 100*time.Millisecond, func() error { 360 if len(c.Contracts()) != 2 { 361 return errors.New("allowance forming seems to have failed") 362 } 363 return nil 364 }) 365 if err != nil { 366 t.Fatal(err) 367 } 368 369 // set allowance with Funds*2; should trigger renewal of both contracts 370 a.Funds = a.Funds.Mul64(2) 371 err = c.SetAllowance(a) 372 if err != nil { 373 t.Fatal(err) 374 } 375 err = build.Retry(50, 100*time.Millisecond, func() error { 376 if len(c.Contracts()) != 2 { 377 return errors.New("allowance forming seems to have failed") 378 } 379 return nil 380 }) 381 if err != nil { 382 t.Error(err) 383 } 384 385 // delete one of the contracts and set allowance with Funds*2; should 386 // trigger 1 renewal and 1 new contract 387 c.mu.Lock() 388 ids := c.staticContracts.IDs() 389 contract, _ := c.staticContracts.Acquire(ids[0]) 390 c.staticContracts.Delete(contract) 391 c.mu.Unlock() 392 a.Funds = a.Funds.Mul64(2) 393 err = c.SetAllowance(a) 394 if err != nil { 395 t.Fatal(err) 396 } 397 err = build.Retry(50, 100*time.Millisecond, func() error { 398 if len(c.Contracts()) != 2 { 399 return errors.New("allowance forming seems to have failed") 400 } 401 return nil 402 }) 403 if err != nil { 404 t.Fatal(err) 405 } 406 } 407 408 // testWalletShim is used to test the walletBridge type. 409 type testWalletShim struct { 410 nextAddressCalled bool 411 startTxnCalled bool 412 } 413 414 // These stub implementations for the walletShim interface set their respective 415 // booleans to true, allowing tests to verify that they have been called. 416 func (ws *testWalletShim) NextAddress() (types.UnlockConditions, error) { 417 ws.nextAddressCalled = true 418 return types.UnlockConditions{}, nil 419 } 420 func (ws *testWalletShim) StartTransaction() (modules.TransactionBuilder, error) { 421 ws.startTxnCalled = true 422 return nil, nil 423 } 424 425 // TestWalletBridge tests the walletBridge type. 426 func TestWalletBridge(t *testing.T) { 427 shim := new(testWalletShim) 428 bridge := WalletBridge{shim} 429 bridge.NextAddress() 430 if !shim.nextAddressCalled { 431 t.Error("NextAddress was not called on the shim") 432 } 433 bridge.StartTransaction() 434 if !shim.startTxnCalled { 435 t.Error("StartTransaction was not called on the shim") 436 } 437 }