gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/contractor/contractor_test.go (about) 1 package contractor 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "os" 8 "path/filepath" 9 "strings" 10 "testing" 11 "time" 12 13 "gitlab.com/SkynetLabs/skyd/build" 14 "gitlab.com/SkynetLabs/skyd/siatest/dependencies" 15 "gitlab.com/SkynetLabs/skyd/skymodules" 16 "gitlab.com/SkynetLabs/skyd/skymodules/renter/hostdb" 17 "go.sia.tech/siad/crypto" 18 "go.sia.tech/siad/modules" 19 "go.sia.tech/siad/modules/consensus" 20 "go.sia.tech/siad/modules/gateway" 21 "go.sia.tech/siad/modules/transactionpool" 22 "go.sia.tech/siad/modules/wallet" 23 "go.sia.tech/siad/types" 24 25 "gitlab.com/NebulousLabs/errors" 26 "gitlab.com/NebulousLabs/ratelimit" 27 "gitlab.com/NebulousLabs/siamux" 28 ) 29 30 // Create a closeFn type that allows helpers which need to be closed to return 31 // methods that close the helpers. 32 type closeFn func() error 33 34 // tryClose is shorthand to run a t.Error() if a closeFn fails. 35 func tryClose(cf closeFn, t *testing.T) { 36 err := cf() 37 if err != nil { 38 t.Error(err) 39 } 40 } 41 42 // newModules initializes the modules needed to test creating a new contractor 43 func newModules(testdir string) (modules.ConsensusSet, modules.Wallet, modules.TransactionPool, *siamux.SiaMux, skymodules.HostDB, closeFn, error) { 44 g, err := gateway.New("localhost:0", false, filepath.Join(testdir, modules.GatewayDir)) 45 if err != nil { 46 return nil, nil, nil, nil, nil, nil, err 47 } 48 cs, errChan := consensus.New(g, false, filepath.Join(testdir, modules.ConsensusDir)) 49 if err := <-errChan; err != nil { 50 return nil, nil, nil, nil, nil, nil, err 51 } 52 tp, err := transactionpool.New(cs, g, filepath.Join(testdir, modules.TransactionPoolDir)) 53 if err != nil { 54 return nil, nil, nil, nil, nil, nil, err 55 } 56 w, err := wallet.New(cs, tp, filepath.Join(testdir, modules.WalletDir)) 57 if err != nil { 58 return nil, nil, nil, nil, nil, nil, err 59 } 60 siaMuxDir := filepath.Join(testdir, modules.SiaMuxDir) 61 mux, err := modules.NewSiaMux(siaMuxDir, testdir, "localhost:0", "localhost:0") 62 if err != nil { 63 return nil, nil, nil, nil, nil, nil, err 64 } 65 hdb, errChanHDB := hostdb.New(g, cs, tp, mux, testdir) 66 if err := <-errChanHDB; err != nil { 67 return nil, nil, nil, nil, nil, nil, err 68 } 69 cf := func() error { 70 return errors.Compose(hdb.Close(), mux.Close(), w.Close(), tp.Close(), cs.Close(), g.Close()) 71 } 72 return cs, w, tp, mux, hdb, cf, nil 73 } 74 75 // newStream is a helper to get a ready-to-use stream that is connected to a 76 // host. 77 func newStream(mux *siamux.SiaMux, h modules.Host) (siamux.Stream, error) { 78 hes := h.ExternalSettings() 79 muxAddress := fmt.Sprintf("%s:%s", hes.NetAddress.Host(), hes.SiaMuxPort) 80 muxPK := modules.SiaPKToMuxPK(h.PublicKey()) 81 return mux.NewStream(modules.HostSiaMuxSubscriberName, muxAddress, muxPK) 82 } 83 84 // TestNew tests the New function. 85 func TestNew(t *testing.T) { 86 if testing.Short() { 87 t.SkipNow() 88 } 89 // Create the skymodules. 90 dir := build.TempDir("contractor", t.Name()) 91 cs, w, tpool, _, hdb, closeFn, err := newModules(dir) 92 if err != nil { 93 t.Fatal(err) 94 } 95 defer tryClose(closeFn, t) 96 97 // Sane values. 98 rl := ratelimit.NewRateLimit(0, 0, 0) 99 _, errChan := New(cs, w, tpool, hdb, rl, dir) 100 if err := <-errChan; err != nil { 101 t.Fatalf("expected nil, got %v", err) 102 } 103 104 // Nil consensus set. 105 _, errChan = New(nil, w, tpool, hdb, rl, dir) 106 if err := <-errChan; !errors.Contains(err, errNilCS) { 107 t.Fatalf("expected %v, got %v", errNilCS, err) 108 } 109 110 // Nil wallet. 111 _, errChan = New(cs, nil, tpool, hdb, rl, dir) 112 if err := <-errChan; !errors.Contains(err, errNilWallet) { 113 t.Fatalf("expected %v, got %v", errNilWallet, err) 114 } 115 116 // Nil transaction pool. 117 _, errChan = New(cs, w, nil, hdb, rl, dir) 118 if err := <-errChan; !errors.Contains(err, errNilTpool) { 119 t.Fatalf("expected %v, got %v", errNilTpool, err) 120 } 121 // Nil hostdb. 122 _, errChan = New(cs, w, tpool, nil, rl, dir) 123 if err := <-errChan; !errors.Contains(err, errNilHDB) { 124 t.Fatalf("expected %v, got %v", errNilHDB, err) 125 } 126 127 // Bad persistDir. 128 _, errChan = New(cs, w, tpool, hdb, rl, "") 129 if err := <-errChan; !os.IsNotExist(err) { 130 t.Fatalf("expected invalid directory, got %v", err) 131 } 132 } 133 134 // TestAllowance tests the Allowance method. 135 func TestAllowance(t *testing.T) { 136 c := &Contractor{ 137 allowance: skymodules.Allowance{ 138 Funds: types.NewCurrency64(1), 139 Period: 2, 140 Hosts: 3, 141 }, 142 } 143 a := c.Allowance() 144 if a.Funds.Cmp(c.allowance.Funds) != 0 || 145 a.Period != c.allowance.Period || 146 a.Hosts != c.allowance.Hosts { 147 t.Fatal("Allowance did not return correct allowance:", a, c.allowance) 148 } 149 } 150 151 // TestIntegrationSetAllowance tests the SetAllowance method. 152 func TestIntegrationSetAllowance(t *testing.T) { 153 if testing.Short() { 154 t.SkipNow() 155 } 156 t.Parallel() 157 158 // create a siamux 159 testdir := build.TempDir("contractor", t.Name()) 160 siaMuxDir := filepath.Join(testdir, modules.SiaMuxDir) 161 mux, err := modules.NewSiaMux(siaMuxDir, testdir, "localhost:0", "localhost:0") 162 if err != nil { 163 t.Fatal(err) 164 } 165 defer tryClose(mux.Close, t) 166 167 // create testing trio 168 h, c, m, cf, err := newTestingTrio(t.Name()) 169 if err != nil { 170 t.Fatal(err) 171 } 172 defer tryClose(cf, t) 173 174 // this test requires two hosts: create another one 175 h, hostCF, err := newTestingHost(build.TempDir("hostdata", ""), c.staticCS.(modules.ConsensusSet), c.staticTPool.(modules.TransactionPool), mux) 176 if err != nil { 177 t.Fatal(err) 178 } 179 defer tryClose(hostCF, t) 180 181 // announce the extra host 182 err = h.Announce() 183 if err != nil { 184 t.Fatal(err) 185 } 186 187 // mine a block, processing the announcement 188 _, err = m.AddBlock() 189 if err != nil { 190 t.Fatal(err) 191 } 192 193 // wait for hostdb to scan 194 hosts, err := c.staticHDB.RandomHosts(1, nil, nil) 195 if err != nil { 196 t.Fatal("failed to get hosts", err) 197 } 198 for i := 0; i < 100 && len(hosts) == 0; i++ { 199 time.Sleep(time.Millisecond * 50) 200 } 201 202 // cancel allowance 203 var a skymodules.Allowance 204 err = c.SetAllowance(a) 205 if err != nil { 206 t.Fatal(err) 207 } 208 209 // bad args 210 a.Hosts = 1 211 err = c.SetAllowance(a) 212 if !errors.Contains(err, ErrAllowanceZeroFunds) { 213 t.Errorf("expected %q, got %q", ErrAllowanceZeroFunds, err) 214 } 215 a.Funds = types.SiacoinPrecision 216 a.Hosts = 0 217 err = c.SetAllowance(a) 218 if !errors.Contains(err, ErrAllowanceNoHosts) { 219 t.Errorf("expected %q, got %q", ErrAllowanceNoHosts, err) 220 } 221 a.Hosts = 1 222 err = c.SetAllowance(a) 223 if !errors.Contains(err, ErrAllowanceZeroPeriod) { 224 t.Errorf("expected %q, got %q", ErrAllowanceZeroPeriod, err) 225 } 226 a.Period = 20 227 err = c.SetAllowance(a) 228 if !errors.Contains(err, ErrAllowanceZeroWindow) { 229 t.Errorf("expected %q, got %q", ErrAllowanceZeroWindow, err) 230 } 231 // There should not be any errors related to RenewWindow size 232 a.RenewWindow = 30 233 err = c.SetAllowance(a) 234 if !errors.Contains(err, ErrAllowanceZeroExpectedStorage) { 235 t.Errorf("expected %q, got %q", ErrAllowanceZeroExpectedStorage, err) 236 } 237 a.RenewWindow = 20 238 err = c.SetAllowance(a) 239 if !errors.Contains(err, ErrAllowanceZeroExpectedStorage) { 240 t.Errorf("expected %q, got %q", ErrAllowanceZeroExpectedStorage, err) 241 } 242 a.RenewWindow = 10 243 err = c.SetAllowance(a) 244 if !errors.Contains(err, ErrAllowanceZeroExpectedStorage) { 245 t.Errorf("expected %q, got %q", ErrAllowanceZeroExpectedStorage, err) 246 } 247 a.ExpectedStorage = skymodules.DefaultAllowance.ExpectedStorage 248 err = c.SetAllowance(a) 249 if !errors.Contains(err, ErrAllowanceZeroExpectedUpload) { 250 t.Errorf("expected %q, got %q", ErrAllowanceZeroExpectedUpload, err) 251 } 252 a.ExpectedUpload = skymodules.DefaultAllowance.ExpectedUpload 253 err = c.SetAllowance(a) 254 if !errors.Contains(err, ErrAllowanceZeroExpectedDownload) { 255 t.Errorf("expected %q, got %q", ErrAllowanceZeroExpectedDownload, err) 256 } 257 a.ExpectedDownload = skymodules.DefaultAllowance.ExpectedDownload 258 err = c.SetAllowance(a) 259 if !errors.Contains(err, ErrAllowanceZeroExpectedRedundancy) { 260 t.Errorf("expected %q, got %q", ErrAllowanceZeroExpectedRedundancy, err) 261 } 262 a.ExpectedRedundancy = skymodules.DefaultAllowance.ExpectedRedundancy 263 a.MaxPeriodChurn = skymodules.DefaultAllowance.MaxPeriodChurn 264 265 // reasonable values; should succeed 266 a.Funds = types.SiacoinPrecision.Mul64(100) 267 err = c.SetAllowance(a) 268 if err != nil { 269 t.Fatal(err) 270 } 271 err = build.Retry(50, 100*time.Millisecond, func() error { 272 if len(c.Contracts()) != 1 { 273 return errors.New("allowance forming seems to have failed") 274 } 275 return nil 276 }) 277 if err != nil { 278 t.Error(err) 279 } 280 281 // set same allowance; should no-op 282 err = c.SetAllowance(a) 283 if err != nil { 284 t.Fatal(err) 285 } 286 clen := c.staticContracts.Len() 287 if clen != 1 { 288 t.Fatal("expected 1 contract, got", clen) 289 } 290 291 _, err = m.AddBlock() 292 if err != nil { 293 t.Fatal(err) 294 } 295 296 // set allowance with Hosts = 2; should only form one new contract 297 a.Hosts = 2 298 err = c.SetAllowance(a) 299 if err != nil { 300 t.Fatal(err) 301 } 302 err = build.Retry(50, 100*time.Millisecond, func() error { 303 if len(c.Contracts()) != 2 { 304 return errors.New("allowance forming seems to have failed") 305 } 306 return nil 307 }) 308 if err != nil { 309 t.Fatal(err) 310 } 311 312 // set allowance with Funds*2; should trigger renewal of both contracts 313 a.Funds = a.Funds.Mul64(2) 314 err = c.SetAllowance(a) 315 if err != nil { 316 t.Fatal(err) 317 } 318 err = build.Retry(50, 100*time.Millisecond, func() error { 319 if len(c.Contracts()) != 2 { 320 return errors.New("allowance forming seems to have failed") 321 } 322 return nil 323 }) 324 if err != nil { 325 t.Error(err) 326 } 327 328 // delete one of the contracts and set allowance with Funds*2; should 329 // trigger 1 renewal and 1 new contract 330 c.mu.Lock() 331 ids := c.staticContracts.IDs() 332 contract, _ := c.staticContracts.Acquire(ids[0]) 333 c.staticContracts.Delete(contract) 334 c.mu.Unlock() 335 a.Funds = a.Funds.Mul64(2) 336 err = c.SetAllowance(a) 337 if err != nil { 338 t.Fatal(err) 339 } 340 err = build.Retry(50, 100*time.Millisecond, func() error { 341 if len(c.Contracts()) != 2 { 342 return errors.New("allowance forming seems to have failed") 343 } 344 return nil 345 }) 346 if err != nil { 347 t.Fatal(err) 348 } 349 } 350 351 // TestHostMaxDuration tests that a host will not be used if their max duration 352 // is not sufficient when renewing contracts 353 func TestHostMaxDuration(t *testing.T) { 354 if testing.Short() { 355 t.SkipNow() 356 } 357 t.Parallel() 358 359 // create testing trio 360 h, c, m, cf, err := newTestingTrio(t.Name()) 361 if err != nil { 362 t.Fatal(err) 363 } 364 defer tryClose(cf, t) 365 366 // Set host's MaxDuration to 5 to test if host will be skipped when contract 367 // is formed 368 settings := h.InternalSettings() 369 settings.MaxDuration = types.BlockHeight(5) 370 if err := h.SetInternalSettings(settings); err != nil { 371 t.Fatal(err) 372 } 373 // Let host settings permeate 374 err = build.Retry(1000, 100*time.Millisecond, func() error { 375 host, _, err := c.staticHDB.Host(h.PublicKey()) 376 if err != nil { 377 return err 378 } 379 if settings.MaxDuration != host.MaxDuration { 380 return fmt.Errorf("host max duration not set, expected %v, got %v", settings.MaxDuration, host.MaxDuration) 381 } 382 return nil 383 }) 384 if err != nil { 385 t.Fatal(err) 386 } 387 388 // Create allowance 389 a := skymodules.Allowance{ 390 Funds: types.SiacoinPrecision.Mul64(100), 391 Hosts: 1, 392 Period: 30, 393 RenewWindow: 20, 394 ExpectedStorage: skymodules.DefaultAllowance.ExpectedStorage, 395 ExpectedUpload: skymodules.DefaultAllowance.ExpectedUpload, 396 ExpectedDownload: skymodules.DefaultAllowance.ExpectedDownload, 397 ExpectedRedundancy: skymodules.DefaultAllowance.ExpectedRedundancy, 398 MaxPeriodChurn: skymodules.DefaultAllowance.MaxPeriodChurn, 399 } 400 err = c.SetAllowance(a) 401 if err != nil { 402 t.Fatal(err) 403 } 404 405 // Wait for and confirm no Contract creation 406 err = build.Retry(50, 100*time.Millisecond, func() error { 407 if len(c.Contracts()) == 0 { 408 return errors.New("no contract created") 409 } 410 return nil 411 }) 412 if err == nil { 413 t.Fatal("Contract should not have been created") 414 } 415 416 // Set host's MaxDuration to 50 to test if host will now form contract 417 settings = h.InternalSettings() 418 settings.MaxDuration = types.BlockHeight(50) 419 if err := h.SetInternalSettings(settings); err != nil { 420 t.Fatal(err) 421 } 422 // Let host settings permeate 423 err = build.Retry(50, 100*time.Millisecond, func() error { 424 host, _, err := c.staticHDB.Host(h.PublicKey()) 425 if err != nil { 426 return err 427 } 428 if settings.MaxDuration != host.MaxDuration { 429 return fmt.Errorf("host max duration not set, expected %v, got %v", settings.MaxDuration, host.MaxDuration) 430 } 431 return nil 432 }) 433 if err != nil { 434 t.Fatal(err) 435 } 436 _, err = m.AddBlock() 437 if err != nil { 438 t.Fatal(err) 439 } 440 441 // Wait for Contract creation 442 err = build.Retry(600, 100*time.Millisecond, func() error { 443 if len(c.Contracts()) != 1 { 444 return errors.New("no contract created") 445 } 446 return nil 447 }) 448 if err != nil { 449 t.Error(err) 450 } 451 452 // Set host's MaxDuration to 5 to test if host will be skipped when contract 453 // is renewed 454 settings = h.InternalSettings() 455 settings.MaxDuration = types.BlockHeight(5) 456 if err := h.SetInternalSettings(settings); err != nil { 457 t.Fatal(err) 458 } 459 // Let host settings permeate 460 err = build.Retry(50, 100*time.Millisecond, func() error { 461 host, _, err := c.staticHDB.Host(h.PublicKey()) 462 if err != nil { 463 return err 464 } 465 if settings.MaxDuration != host.MaxDuration { 466 return fmt.Errorf("host max duration not set, expected %v, got %v", settings.MaxDuration, host.MaxDuration) 467 } 468 return nil 469 }) 470 if err != nil { 471 t.Fatal(err) 472 } 473 474 // Mine blocks to renew contract 475 for i := types.BlockHeight(0); i <= c.allowance.Period-c.allowance.RenewWindow; i++ { 476 _, err = m.AddBlock() 477 if err != nil { 478 t.Fatal(err) 479 } 480 } 481 482 // Confirm Contract is not renewed 483 err = build.Retry(50, 100*time.Millisecond, func() error { 484 if len(c.OldContracts()) == 0 { 485 return errors.New("no contract renewed") 486 } 487 return nil 488 }) 489 if err == nil { 490 t.Fatal("Contract should not have been renewed") 491 } 492 } 493 494 // TestPayment verifies the PaymentProvider interface on the contractor. It does 495 // this by trying to pay the host using a filecontract and verifying if payment 496 // can be made successfully. 497 func TestPayment(t *testing.T) { 498 if testing.Short() { 499 t.SkipNow() 500 } 501 t.Parallel() 502 503 // newStream is a helper to get a ready-to-use stream that is connected to a 504 // host. 505 newStream := func(mux *siamux.SiaMux, h modules.Host) (siamux.Stream, error) { 506 hes := h.ExternalSettings() 507 muxAddress := fmt.Sprintf("%s:%s", hes.NetAddress.Host(), hes.SiaMuxPort) 508 muxPK := modules.SiaPKToMuxPK(h.PublicKey()) 509 return mux.NewStream(modules.HostSiaMuxSubscriberName, muxAddress, muxPK) 510 } 511 512 // create a siamux 513 testdir := build.TempDir("contractor", t.Name()) 514 siaMuxDir := filepath.Join(testdir, modules.SiaMuxDir) 515 mux, err := modules.NewSiaMux(siaMuxDir, testdir, "localhost:0", "localhost:0") 516 if err != nil { 517 t.Fatal(err) 518 } 519 520 // create a testing trio with our mux injected 521 h, c, _, cf, err := newCustomTestingTrio(t.Name(), mux, modules.ProdDependencies, modules.ProdDependencies) 522 if err != nil { 523 t.Fatal(err) 524 } 525 defer tryClose(cf, t) 526 hpk := h.PublicKey() 527 528 // set an allowance and wait for contracts 529 err = c.SetAllowance(skymodules.DefaultAllowance) 530 if err != nil { 531 t.Fatal(err) 532 } 533 534 // create a refund account 535 aid, _ := modules.NewAccountID() 536 537 // Fetch the contracts, there's a race condition between contract creation 538 // and the contractor knowing the contract exists, so do this in a retry. 539 var contract skymodules.RenterContract 540 err = build.Retry(200, 100*time.Millisecond, func() error { 541 var ok bool 542 contract, ok = c.ContractByPublicKey(hpk) 543 if !ok { 544 return errors.New("contract not found") 545 } 546 return nil 547 }) 548 if err != nil { 549 t.Fatal(err) 550 } 551 552 // backup the amount renter funds 553 initial := contract.RenterFunds 554 555 // check spending metrics are zero 556 if !contract.FundAccountSpending.IsZero() || !contract.MaintenanceSpending.Sum().IsZero() { 557 t.Fatal("unexpected spending metrics") 558 } 559 560 // write the rpc id 561 stream, err := newStream(mux, h) 562 if err != nil { 563 t.Fatal(err) 564 } 565 err = modules.RPCWrite(stream, modules.RPCUpdatePriceTable) 566 if err != nil { 567 t.Fatal(err) 568 } 569 570 // read the updated response 571 var update modules.RPCUpdatePriceTableResponse 572 err = modules.RPCRead(stream, &update) 573 if err != nil { 574 t.Fatal(err) 575 } 576 577 // unmarshal the JSON into a price table 578 var pt modules.RPCPriceTable 579 err = json.Unmarshal(update.PriceTableJSON, &pt) 580 if err != nil { 581 t.Fatal(err) 582 } 583 584 // build payment details 585 details := PaymentDetails{ 586 Host: contract.HostPublicKey, 587 Amount: pt.UpdatePriceTableCost, 588 RefundAccount: aid, 589 SpendingDetails: skymodules.SpendingDetails{ 590 MaintenanceSpending: skymodules.MaintenanceSpending{ 591 UpdatePriceTableCost: pt.UpdatePriceTableCost, 592 }, 593 }, 594 } 595 596 // provide payment 597 err = c.ProvidePayment(stream, &pt, details) 598 if err != nil { 599 t.Fatal(err) 600 } 601 602 // await the track response 603 var tracked modules.RPCTrackedPriceTableResponse 604 err = modules.RPCRead(stream, &tracked) 605 if err != nil { 606 t.Fatal(err) 607 } 608 609 // verify the contract was updated 610 contract, _ = c.ContractByPublicKey(hpk) 611 remaining := contract.RenterFunds 612 expected := initial.Sub(pt.UpdatePriceTableCost) 613 if !remaining.Equals(expected) { 614 t.Fatalf("Expected renter contract to reflect the payment, the renter funds should be %v but were %v", expected.HumanString(), remaining.HumanString()) 615 } 616 617 // check maintenance funding metric got updated 618 if !contract.MaintenanceSpending.UpdatePriceTableCost.Equals(pt.UpdatePriceTableCost) { 619 t.Fatal("unexpected maintenance spending metric", contract.MaintenanceSpending) 620 } 621 prev := contract.MaintenanceSpending.FundAccountCost 622 623 // prepare a buffer so we can optimize our writes 624 buffer := bytes.NewBuffer(nil) 625 626 // write the rpc id 627 stream, err = newStream(mux, h) 628 if err != nil { 629 t.Fatal(err) 630 } 631 err = modules.RPCWrite(buffer, modules.RPCFundAccount) 632 if err != nil { 633 t.Fatal(err) 634 } 635 636 // write the price table uid 637 err = modules.RPCWrite(buffer, pt.UID) 638 if err != nil { 639 t.Fatal(err) 640 } 641 642 // send fund account request (re-use the refund account) 643 err = modules.RPCWrite(buffer, modules.FundAccountRequest{Account: aid}) 644 if err != nil { 645 t.Fatal(err) 646 } 647 648 // write contents of the buffer to the stream 649 _, err = stream.Write(buffer.Bytes()) 650 if err != nil { 651 t.Fatal(err) 652 } 653 654 // provide payment 655 funding := remaining.Div64(2) 656 if funding.Cmp(h.InternalSettings().MaxEphemeralAccountBalance) > 0 { 657 funding = h.InternalSettings().MaxEphemeralAccountBalance 658 } 659 660 // build payment details 661 details = PaymentDetails{ 662 Host: hpk, 663 Amount: funding.Add(pt.FundAccountCost), 664 RefundAccount: modules.ZeroAccountID, 665 SpendingDetails: skymodules.SpendingDetails{ 666 FundAccountSpending: funding, 667 MaintenanceSpending: skymodules.MaintenanceSpending{ 668 FundAccountCost: pt.FundAccountCost, 669 }, 670 }, 671 } 672 err = c.ProvidePayment(stream, &pt, details) 673 if err != nil { 674 t.Fatal(err) 675 } 676 677 // receive response 678 var resp modules.FundAccountResponse 679 err = modules.RPCRead(stream, &resp) 680 if err != nil { 681 t.Fatal(err) 682 } 683 684 // verify the receipt 685 receipt := resp.Receipt 686 err = crypto.VerifyHash(crypto.HashAll(receipt), hpk.ToPublicKey(), resp.Signature) 687 if err != nil { 688 t.Fatal(err) 689 } 690 if !receipt.Amount.Equals(funding) { 691 t.Fatalf("Unexpected funded amount in the receipt, expected %v but received %v", funding.HumanString(), receipt.Amount.HumanString()) 692 } 693 if receipt.Account != aid { 694 t.Fatalf("Unexpected account id in the receipt, expected %v but received %v", aid, receipt.Account) 695 } 696 if !receipt.Host.Equals(hpk) { 697 t.Fatalf("Unexpected host pubkey in the receipt, expected %v but received %v", hpk, receipt.Host) 698 } 699 700 // check fund account metric got updated 701 contract, _ = c.ContractByPublicKey(hpk) 702 if !contract.FundAccountSpending.Equals(funding) { 703 t.Fatalf("unexpected funding spending metric %v != %v", contract.FundAccountSpending, funding) 704 } 705 if !contract.MaintenanceSpending.FundAccountCost.Equals(prev.Add(pt.FundAccountCost)) { 706 t.Fatalf("unexpected maintenance spending metric %v != %v", contract.MaintenanceSpending, prev.Add(pt.FundAccountCost)) 707 } 708 } 709 710 // TestLinkedContracts tests that the contractors maps are updated correctly 711 // when renewing contracts 712 func TestLinkedContracts(t *testing.T) { 713 if testing.Short() { 714 t.SkipNow() 715 } 716 t.Parallel() 717 718 // create testing trio 719 h, c, m, cf, err := newTestingTrioWithContractorDeps(t.Name(), &dependencies.DependencyLegacyRenew{}) 720 if err != nil { 721 t.Fatal(err) 722 } 723 defer tryClose(cf, t) 724 725 // Create allowance 726 a := skymodules.Allowance{ 727 Funds: types.SiacoinPrecision.Mul64(100), 728 Hosts: 1, 729 Period: 20, 730 RenewWindow: 10, 731 ExpectedStorage: skymodules.DefaultAllowance.ExpectedStorage, 732 ExpectedUpload: skymodules.DefaultAllowance.ExpectedUpload, 733 ExpectedDownload: skymodules.DefaultAllowance.ExpectedDownload, 734 ExpectedRedundancy: skymodules.DefaultAllowance.ExpectedRedundancy, 735 MaxPeriodChurn: skymodules.DefaultAllowance.MaxPeriodChurn, 736 } 737 err = c.SetAllowance(a) 738 if err != nil { 739 t.Fatal(err) 740 } 741 742 // Wait for Contract creation 743 numRetries := 0 744 err = build.Retry(200, 100*time.Millisecond, func() error { 745 if numRetries%10 == 0 { 746 if _, err := m.AddBlock(); err != nil { 747 return err 748 } 749 } 750 numRetries++ 751 if len(c.Contracts()) != 1 { 752 return errors.New("no contract created") 753 } 754 return nil 755 }) 756 if err != nil { 757 t.Error(err) 758 } 759 760 // Confirm that maps are empty 761 if len(c.renewedFrom) != 0 { 762 t.Fatal("renewedFrom map should be empty") 763 } 764 if len(c.renewedTo) != 0 { 765 t.Fatal("renewedTo map should be empty") 766 } 767 768 // Set host's uploadbandwidthprice to zero to test divide by zero check when 769 // contracts are renewed 770 settings := h.InternalSettings() 771 settings.MinUploadBandwidthPrice = types.ZeroCurrency 772 if err := h.SetInternalSettings(settings); err != nil { 773 t.Fatal(err) 774 } 775 776 // Mine blocks to renew contract 777 for i := types.BlockHeight(0); i < c.allowance.Period-c.allowance.RenewWindow; i++ { 778 _, err = m.AddBlock() 779 if err != nil { 780 t.Fatal(err) 781 } 782 } 783 784 // Confirm Contracts got renewed 785 err = build.Retry(200, 100*time.Millisecond, func() error { 786 if len(c.Contracts()) != 1 { 787 return errors.New("no contract") 788 } 789 if len(c.OldContracts()) != 1 { 790 return errors.New("no old contract") 791 } 792 return nil 793 }) 794 if err != nil { 795 t.Error(err) 796 } 797 798 // Confirm maps are updated as expected 799 if len(c.renewedFrom) != 1 { 800 t.Fatalf("renewedFrom map should have 1 entry but has %v", len(c.renewedFrom)) 801 } 802 if len(c.renewedTo) != 1 { 803 t.Fatalf("renewedTo map should have 1 entry but has %v", len(c.renewedTo)) 804 } 805 if c.renewedFrom[c.Contracts()[0].ID] != c.OldContracts()[0].ID { 806 t.Fatalf(`Map assignment incorrect, 807 expected: 808 map[%v:%v] 809 got: 810 %v`, c.Contracts()[0].ID, c.OldContracts()[0].ID, c.renewedFrom) 811 } 812 if c.renewedTo[c.OldContracts()[0].ID] != c.Contracts()[0].ID { 813 t.Fatalf(`Map assignment incorrect, 814 expected: 815 map[%v:%v] 816 got: 817 %v`, c.OldContracts()[0].ID, c.Contracts()[0].ID, c.renewedTo) 818 } 819 } 820 821 // TestPaymentMissingStorageObligation tests the case where a host can't find a 822 // storage obligation with which to pay. 823 func TestPaymentMissingStorageObligation(t *testing.T) { 824 if testing.Short() { 825 t.SkipNow() 826 } 827 t.Parallel() 828 829 // create a siamux 830 testdir := build.TempDir("contractor", t.Name()) 831 siaMuxDir := filepath.Join(testdir, modules.SiaMuxDir) 832 mux, err := modules.NewSiaMux(siaMuxDir, testdir, "localhost:0", "localhost:0") 833 if err != nil { 834 t.Fatal(err) 835 } 836 837 // create a testing trio with our mux injected 838 deps := &dependencies.DependencyStorageObligationNotFound{} 839 h, c, _, cf, err := newCustomTestingTrio(t.Name(), mux, deps, modules.ProdDependencies) 840 if err != nil { 841 t.Fatal(err) 842 } 843 defer cf() 844 hpk := h.PublicKey() 845 846 // set an allowance and wait for contracts 847 err = c.SetAllowance(skymodules.DefaultAllowance) 848 if err != nil { 849 t.Fatal(err) 850 } 851 852 // create a refund account 853 aid, _ := modules.NewAccountID() 854 855 // Fetch the contracts, there's a race condition between contract creation 856 // and the contractor knowing the contract exists, so do this in a retry. 857 var contract skymodules.RenterContract 858 err = build.Retry(200, 100*time.Millisecond, func() error { 859 var ok bool 860 contract, ok = c.ContractByPublicKey(hpk) 861 if !ok { 862 return errors.New("contract not found") 863 } 864 return nil 865 }) 866 if err != nil { 867 t.Fatal(err) 868 } 869 870 // get a stream 871 stream, err := newStream(mux, h) 872 if err != nil { 873 t.Fatal(err) 874 } 875 // write the rpc id 876 err = modules.RPCWrite(stream, modules.RPCUpdatePriceTable) 877 if err != nil { 878 t.Fatal(err) 879 } 880 881 // read the updated response 882 var update modules.RPCUpdatePriceTableResponse 883 err = modules.RPCRead(stream, &update) 884 if err != nil { 885 t.Fatal(err) 886 } 887 888 // unmarshal the JSON into a price table 889 var pt modules.RPCPriceTable 890 err = json.Unmarshal(update.PriceTableJSON, &pt) 891 if err != nil { 892 t.Fatal(err) 893 } 894 895 // build payment details 896 details := PaymentDetails{ 897 Host: contract.HostPublicKey, 898 Amount: pt.UpdatePriceTableCost, 899 RefundAccount: aid, 900 SpendingDetails: skymodules.SpendingDetails{ 901 MaintenanceSpending: skymodules.MaintenanceSpending{ 902 UpdatePriceTableCost: pt.UpdatePriceTableCost, 903 }, 904 }, 905 } 906 907 // provide payment 908 err = c.ProvidePayment(stream, &pt, details) 909 if err == nil || !strings.Contains(err.Error(), "storage obligation not found") { 910 t.Fatal("expected storage obligation not found but got", err) 911 } 912 913 // verify the contract was updated 914 contract, _ = c.ContractByPublicKey(hpk) 915 if contract.Utility.GoodForRenew { 916 t.Fatal("GFR should be false") 917 } 918 if contract.Utility.GoodForUpload { 919 t.Fatal("GFU should be false") 920 } 921 if !contract.Utility.BadContract { 922 t.Fatal("Contract should be bad") 923 } 924 if contract.Utility.Locked { 925 t.Fatal("Contract should not be locked") 926 } 927 }