gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/proto/contract_test.go (about) 1 package proto 2 3 import ( 4 "bytes" 5 "math" 6 "os" 7 "path/filepath" 8 "reflect" 9 "strings" 10 "testing" 11 12 "gitlab.com/NebulousLabs/encoding" 13 "gitlab.com/NebulousLabs/fastrand" 14 "gitlab.com/NebulousLabs/ratelimit" 15 "gitlab.com/SkynetLabs/skyd/build" 16 "gitlab.com/SkynetLabs/skyd/skymodules" 17 "go.sia.tech/siad/crypto" 18 "go.sia.tech/siad/modules" 19 "go.sia.tech/siad/types" 20 ) 21 22 // dependencyIgnoreInvalidUpdate will prevent a critical in NewContractSet during testing when an invalid update is encountered. 23 type dependencyIgnoreInvalidUpdate struct { 24 modules.ProductionDependencies 25 } 26 27 // Disrupt returns true if the correct string is provided. 28 func (d *dependencyIgnoreInvalidUpdate) Disrupt(s string) bool { 29 return s == "IgnoreInvalidUpdate" 30 } 31 32 // dependencyInterruptContractInsertion will interrupt inserting a contract 33 // after writing the header but before writing the roots. 34 type dependencyInterruptContractInsertion struct { 35 modules.ProductionDependencies 36 } 37 38 // Disrupt returns true if the correct string is provided. 39 func (d *dependencyInterruptContractInsertion) Disrupt(s string) bool { 40 return s == "InterruptContractInsertion" 41 } 42 43 // TestContractUncommittedTxn tests that if a contract revision is left in an 44 // uncommitted state, either version of the contract can be recovered. 45 func TestContractUncommittedTxn(t *testing.T) { 46 // initial header every subtests starts out with. 47 initialHeader := contractHeader{ 48 Transaction: types.Transaction{ 49 FileContractRevisions: []types.FileContractRevision{{ 50 NewRevisionNumber: 1, 51 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 52 UnlockConditions: types.UnlockConditions{ 53 PublicKeys: []types.SiaPublicKey{{}, {}}, 54 }, 55 }}, 56 }, 57 } 58 59 // Test RecordRootUpdates. 60 t.Run("RecordRootUpdates", func(t *testing.T) { 61 updateFunc := func(sc *SafeContract) (*unappliedWalTxn, []crypto.Hash, contractHeader, error) { 62 revisedHeader := contractHeader{ 63 Transaction: types.Transaction{ 64 FileContractRevisions: []types.FileContractRevision{{ 65 NewRevisionNumber: 2, 66 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 67 UnlockConditions: types.UnlockConditions{ 68 PublicKeys: []types.SiaPublicKey{{}, {}}, 69 }, 70 }}, 71 }, 72 StorageSpending: types.NewCurrency64(7), 73 UploadSpending: types.NewCurrency64(17), 74 } 75 revisedRoots := []crypto.Hash{{1}, {2}} 76 fcr := revisedHeader.Transaction.FileContractRevisions[0] 77 newRoot := revisedRoots[1] 78 storageCost := revisedHeader.StorageSpending.Sub(initialHeader.StorageSpending) 79 bandwidthCost := revisedHeader.UploadSpending.Sub(initialHeader.UploadSpending) 80 txn, err := sc.managedRecordRootUpdates(fcr, map[uint64]rootUpdate{ 81 uint64(len(revisedRoots) - 1): newRootUpdateAppendRoot(newRoot), 82 }, storageCost, bandwidthCost) 83 return txn, revisedRoots, revisedHeader, err 84 } 85 testContractUncomittedTxn(t, initialHeader, updateFunc) 86 }) 87 // Test RecordDownloadIntent. 88 t.Run("RecordDownloadIntent", func(t *testing.T) { 89 updateFunc := func(sc *SafeContract) (*unappliedWalTxn, []crypto.Hash, contractHeader, error) { 90 revisedHeader := contractHeader{ 91 Transaction: types.Transaction{ 92 FileContractRevisions: []types.FileContractRevision{{ 93 NewRevisionNumber: 2, 94 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 95 UnlockConditions: types.UnlockConditions{ 96 PublicKeys: []types.SiaPublicKey{{}, {}}, 97 }, 98 }}, 99 }, 100 StorageSpending: types.ZeroCurrency, 101 DownloadSpending: types.NewCurrency64(17), 102 } 103 revisedRoots := []crypto.Hash{{1}} 104 fcr := revisedHeader.Transaction.FileContractRevisions[0] 105 bandwidthCost := revisedHeader.DownloadSpending.Sub(initialHeader.DownloadSpending) 106 txn, err := sc.managedRecordDownloadIntent(fcr, bandwidthCost) 107 return txn, revisedRoots, revisedHeader, err 108 } 109 testContractUncomittedTxn(t, initialHeader, updateFunc) 110 }) 111 // Test RecordClearContractIntent. 112 t.Run("RecordClearContractIntent", func(t *testing.T) { 113 updateFunc := func(sc *SafeContract) (*unappliedWalTxn, []crypto.Hash, contractHeader, error) { 114 revisedHeader := contractHeader{ 115 Transaction: types.Transaction{ 116 FileContractRevisions: []types.FileContractRevision{{ 117 NewRevisionNumber: 2, 118 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 119 UnlockConditions: types.UnlockConditions{ 120 PublicKeys: []types.SiaPublicKey{{}, {}}, 121 }, 122 }}, 123 }, 124 StorageSpending: types.ZeroCurrency, 125 UploadSpending: types.NewCurrency64(17), 126 } 127 revisedRoots := []crypto.Hash{{1}} 128 fcr := revisedHeader.Transaction.FileContractRevisions[0] 129 bandwidthCost := revisedHeader.UploadSpending.Sub(initialHeader.UploadSpending) 130 txn, err := sc.managedRecordClearContractIntent(fcr, bandwidthCost) 131 return txn, revisedRoots, revisedHeader, err 132 } 133 testContractUncomittedTxn(t, initialHeader, updateFunc) 134 }) 135 // Test RecordPaymentIntent. 136 t.Run("RecordPaymentIntent", func(t *testing.T) { 137 updateFunc := func(sc *SafeContract) (*unappliedWalTxn, []crypto.Hash, contractHeader, error) { 138 revisedHeader := contractHeader{ 139 Transaction: types.Transaction{ 140 FileContractRevisions: []types.FileContractRevision{{ 141 NewRevisionNumber: 2, 142 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 143 UnlockConditions: types.UnlockConditions{ 144 PublicKeys: []types.SiaPublicKey{{}, {}}, 145 }, 146 }}, 147 }, 148 StorageSpending: types.ZeroCurrency, 149 UploadSpending: types.ZeroCurrency, 150 FundAccountSpending: types.NewCurrency64(42), 151 } 152 revisedRoots := []crypto.Hash{{1}} 153 fcr := revisedHeader.Transaction.FileContractRevisions[0] 154 amount := revisedHeader.FundAccountSpending 155 txn, err := sc.RecordPaymentIntent(fcr, amount, skymodules.SpendingDetails{ 156 FundAccountSpending: revisedHeader.FundAccountSpending, 157 }) 158 return txn, revisedRoots, revisedHeader, err 159 } 160 testContractUncomittedTxn(t, initialHeader, updateFunc) 161 }) 162 } 163 164 // testContractUncommittedTxn tests that if a contract revision is left in an 165 // uncommitted state, either version of the contract can be recovered. 166 func testContractUncomittedTxn(t *testing.T, initialHeader contractHeader, updateFunc func(*SafeContract) (*unappliedWalTxn, []crypto.Hash, contractHeader, error)) { 167 if testing.Short() { 168 t.SkipNow() 169 } 170 t.Parallel() 171 // create contract set with one contract 172 dir := build.TempDir(filepath.Join("proto", t.Name())) 173 rl := ratelimit.NewRateLimit(0, 0, 0) 174 cs, err := NewContractSet(dir, rl, modules.ProdDependencies) 175 if err != nil { 176 t.Fatal(err) 177 } 178 initialRoots := []crypto.Hash{{1}} 179 c, err := cs.managedInsertContract(initialHeader, initialRoots) 180 if err != nil { 181 t.Fatal(err) 182 } 183 184 // apply an update to the contract, but don't commit it 185 sc := cs.managedMustAcquire(t, c.ID) 186 walTxn, revisedRoots, revisedHeader, err := updateFunc(sc) 187 if err != nil { 188 t.Fatal(err) 189 } 190 191 // the state of the contract should match the initial state 192 // NOTE: can't use reflect.DeepEqual for the header because it contains 193 // types.Currency fields 194 merkleRoots, err := sc.merkleRoots.merkleRoots() 195 if err != nil { 196 t.Fatal("failed to get merkle roots", err) 197 } 198 if !bytes.Equal(encoding.Marshal(sc.header), encoding.Marshal(initialHeader)) { 199 t.Fatal("contractHeader should match initial contractHeader") 200 } else if !reflect.DeepEqual(merkleRoots, initialRoots) { 201 t.Fatal("Merkle roots should match initial Merkle roots") 202 } 203 204 // close and reopen the contract set. 205 if err := cs.Close(); err != nil { 206 t.Fatal(err) 207 } 208 cs, err = NewContractSet(dir, rl, modules.ProdDependencies) 209 if err != nil { 210 t.Fatal(err) 211 } 212 // the uncommitted transaction should be stored in the contract 213 sc = cs.managedMustAcquire(t, c.ID) 214 if len(sc.unappliedTxns) != 1 { 215 t.Fatal("expected 1 unappliedTxn, got", len(sc.unappliedTxns)) 216 } else if !bytes.Equal(sc.unappliedTxns[0].Updates[0].Instructions, walTxn.Updates[0].Instructions) { 217 t.Fatal("WAL transaction changed") 218 } 219 // the state of the contract should match the initial state 220 merkleRoots, err = sc.merkleRoots.merkleRoots() 221 if err != nil { 222 t.Fatal("failed to get merkle roots:", err) 223 } 224 if !bytes.Equal(encoding.Marshal(sc.header), encoding.Marshal(initialHeader)) { 225 t.Fatal("contractHeader should match initial contractHeader", sc.header, initialHeader) 226 } else if !reflect.DeepEqual(merkleRoots, initialRoots) { 227 t.Fatal("Merkle roots should match initial Merkle roots") 228 } 229 230 // apply the uncommitted transaction 231 err = sc.managedCommitTxns() 232 if err != nil { 233 t.Fatal(err) 234 } 235 // the uncommitted transaction should be gone now 236 if len(sc.unappliedTxns) != 0 { 237 t.Fatal("expected 0 unappliedTxns, got", len(sc.unappliedTxns)) 238 } 239 // the state of the contract should now match the revised state 240 merkleRoots, err = sc.merkleRoots.merkleRoots() 241 if err != nil { 242 t.Fatal("failed to get merkle roots:", err) 243 } 244 if !bytes.Equal(encoding.Marshal(sc.header), encoding.Marshal(revisedHeader)) { 245 t.Fatal("contractHeader should match revised contractHeader", sc.header, revisedHeader) 246 } else if !reflect.DeepEqual(merkleRoots, revisedRoots) { 247 t.Fatal("Merkle roots should match revised Merkle roots", merkleRoots, revisedRoots) 248 } 249 // close and reopen the contract set. 250 if err := cs.Close(); err != nil { 251 t.Fatal(err) 252 } 253 cs, err = NewContractSet(dir, rl, modules.ProdDependencies) 254 if err != nil { 255 t.Fatal(err) 256 } 257 // the uncommitted transaction should be gone. 258 sc = cs.managedMustAcquire(t, c.ID) 259 if len(sc.unappliedTxns) != 0 { 260 t.Fatal("expected 0 unappliedTxn, got", len(sc.unappliedTxns)) 261 } 262 } 263 264 // TestContractIncompleteWrite tests that if the merkle root section has the wrong 265 // length due to an incomplete write, it is truncated and the wal transactions 266 // are applied. 267 func TestContractIncompleteWrite(t *testing.T) { 268 if testing.Short() { 269 t.SkipNow() 270 } 271 t.Parallel() 272 // create contract set with one contract 273 dir := build.TempDir(filepath.Join("proto", t.Name())) 274 rl := ratelimit.NewRateLimit(0, 0, 0) 275 cs, err := NewContractSet(dir, rl, modules.ProdDependencies) 276 if err != nil { 277 t.Fatal(err) 278 } 279 initialHeader := contractHeader{ 280 Transaction: types.Transaction{ 281 FileContractRevisions: []types.FileContractRevision{{ 282 NewRevisionNumber: 1, 283 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 284 UnlockConditions: types.UnlockConditions{ 285 PublicKeys: []types.SiaPublicKey{{}, {}}, 286 }, 287 }}, 288 }, 289 } 290 initialRoots := []crypto.Hash{{1}} 291 c, err := cs.managedInsertContract(initialHeader, initialRoots) 292 if err != nil { 293 t.Fatal(err) 294 } 295 296 // apply an update to the contract, but don't commit it 297 sc := cs.managedMustAcquire(t, c.ID) 298 revisedHeader := contractHeader{ 299 Transaction: types.Transaction{ 300 FileContractRevisions: []types.FileContractRevision{{ 301 NewRevisionNumber: 2, 302 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 303 UnlockConditions: types.UnlockConditions{ 304 PublicKeys: []types.SiaPublicKey{{}, {}}, 305 }, 306 }}, 307 }, 308 StorageSpending: types.NewCurrency64(7), 309 UploadSpending: types.NewCurrency64(17), 310 } 311 revisedRoots := []crypto.Hash{{1}, {2}} 312 fcr := revisedHeader.Transaction.FileContractRevisions[0] 313 newRoot := revisedRoots[1] 314 storageCost := revisedHeader.StorageSpending.Sub(initialHeader.StorageSpending) 315 bandwidthCost := revisedHeader.UploadSpending.Sub(initialHeader.UploadSpending) 316 _, err = sc.managedRecordRootUpdates(fcr, map[uint64]rootUpdate{ 317 uint64(len(revisedRoots) - 1): newRootUpdateAppendRoot(newRoot), 318 }, storageCost, bandwidthCost) 319 if err != nil { 320 t.Fatal(err) 321 } 322 323 // the state of the contract should match the initial state 324 // NOTE: can't use reflect.DeepEqual for the header because it contains 325 // types.Currency fields 326 merkleRoots, err := sc.merkleRoots.merkleRoots() 327 if err != nil { 328 t.Fatal("failed to get merkle roots", err) 329 } 330 if !bytes.Equal(encoding.Marshal(sc.header), encoding.Marshal(initialHeader)) { 331 t.Fatal("contractHeader should match initial contractHeader") 332 } else if !reflect.DeepEqual(merkleRoots, initialRoots) { 333 t.Fatal("Merkle roots should match initial Merkle roots") 334 } 335 336 // get the size of the merkle roots file. 337 size, err := sc.merkleRoots.rootsFile.Size() 338 if err != nil { 339 t.Fatal(err) 340 } 341 // the size should be crypto.HashSize since we have exactly one root. 342 if size != crypto.HashSize { 343 t.Fatal("unexpected merkle root file size", size) 344 } 345 // truncate the rootsFile to simulate a corruption while writing the second 346 // root. 347 err = sc.merkleRoots.rootsFile.Truncate(size + crypto.HashSize/2) 348 if err != nil { 349 t.Fatal(err) 350 } 351 352 // close and reopen the contract set. 353 if err := cs.Close(); err != nil { 354 t.Fatal(err) 355 } 356 cs, err = NewContractSet(dir, rl, modules.ProdDependencies) 357 if err != nil { 358 t.Fatal(err) 359 } 360 // the uncommitted txn should be gone. 361 sc = cs.managedMustAcquire(t, c.ID) 362 if len(sc.unappliedTxns) != 0 { 363 t.Fatal("expected 0 unappliedTxn, got", len(sc.unappliedTxns)) 364 } 365 if sc.merkleRoots.len() != 2 { 366 t.Fatal("expected 2 roots, got", sc.merkleRoots.len()) 367 } 368 cs.Return(sc) 369 cs.Close() 370 } 371 372 // TestContractLargeHeader tests if adding or modifying a contract with a large 373 // header works as expected. 374 func TestContractLargeHeader(t *testing.T) { 375 if testing.Short() { 376 t.SkipNow() 377 } 378 t.Parallel() 379 // create contract set with one contract 380 dir := build.TempDir(filepath.Join("proto", t.Name())) 381 rl := ratelimit.NewRateLimit(0, 0, 0) 382 cs, err := NewContractSet(dir, rl, modules.ProdDependencies) 383 if err != nil { 384 t.Fatal(err) 385 } 386 largeHeader := contractHeader{ 387 Transaction: types.Transaction{ 388 ArbitraryData: [][]byte{fastrand.Bytes(1 << 20 * 5)}, // excessive 5 MiB Transaction 389 FileContractRevisions: []types.FileContractRevision{{ 390 NewRevisionNumber: 1, 391 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 392 UnlockConditions: types.UnlockConditions{ 393 PublicKeys: []types.SiaPublicKey{{}, {}}, 394 }, 395 }}, 396 }, 397 } 398 initialRoots := []crypto.Hash{{1}} 399 // Inserting a contract with a large header should work. 400 c, err := cs.managedInsertContract(largeHeader, initialRoots) 401 if err != nil { 402 t.Fatal(err) 403 } 404 405 sc, ok := cs.Acquire(c.ID) 406 if !ok { 407 t.Fatal("failed to acquire contract") 408 } 409 // Applying a large header update should also work. 410 if err := sc.applySetHeader(largeHeader); err != nil { 411 t.Fatal(err) 412 } 413 } 414 415 // TestContractSetInsert checks if inserting contracts into the set is ACID. 416 func TestContractSetInsertInterrupted(t *testing.T) { 417 if testing.Short() { 418 t.SkipNow() 419 } 420 t.Parallel() 421 // create contract set with a custom dependency. 422 dir := build.TempDir(filepath.Join("proto", t.Name())) 423 rl := ratelimit.NewRateLimit(0, 0, 0) 424 cs, err := NewContractSet(dir, rl, &dependencyInterruptContractInsertion{}) 425 if err != nil { 426 t.Fatal(err) 427 } 428 contractHeader := contractHeader{ 429 Transaction: types.Transaction{ 430 FileContractRevisions: []types.FileContractRevision{{ 431 NewRevisionNumber: 1, 432 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 433 UnlockConditions: types.UnlockConditions{ 434 PublicKeys: []types.SiaPublicKey{{}, {}}, 435 }, 436 }}, 437 }, 438 } 439 initialRoots := []crypto.Hash{{1}} 440 // Inserting the contract should fail due to the dependency. 441 c, err := cs.managedInsertContract(contractHeader, initialRoots) 442 if err == nil || !strings.Contains(err.Error(), "interrupted") { 443 t.Fatal("insertion should have been interrupted") 444 } 445 446 // Reload the contract set. The contract should be there. 447 cs, err = NewContractSet(dir, rl, modules.ProdDependencies) 448 if err != nil { 449 t.Fatal(err) 450 } 451 sc, ok := cs.Acquire(c.ID) 452 if !ok { 453 t.Fatal("faild to acquire contract") 454 } 455 if !bytes.Equal(encoding.Marshal(sc.header), encoding.Marshal(contractHeader)) { 456 t.Log(sc.header) 457 t.Log(contractHeader) 458 t.Error("header doesn't match") 459 } 460 mr, err := sc.merkleRoots.merkleRoots() 461 if err != nil { 462 t.Fatal(err) 463 } 464 if !reflect.DeepEqual(mr, initialRoots) { 465 t.Error("roots don't match") 466 } 467 } 468 469 // TestContractCommitAndRecordPaymentIntent verifies the functionality of the 470 // RecordPaymentIntent and CommitPaymentIntent methods on the SafeContract 471 func TestContractRecordAndCommitPaymentIntent(t *testing.T) { 472 if testing.Short() { 473 t.SkipNow() 474 } 475 t.Parallel() 476 477 blockHeight := types.BlockHeight(fastrand.Intn(100)) 478 479 // create contract set 480 dir := build.TempDir(filepath.Join("proto", t.Name())) 481 rl := ratelimit.NewRateLimit(0, 0, 0) 482 cs, err := NewContractSet(dir, rl, modules.ProdDependencies) 483 if err != nil { 484 t.Fatal(err) 485 } 486 487 // add a contract 488 initialHeader := contractHeader{ 489 Transaction: types.Transaction{ 490 FileContractRevisions: []types.FileContractRevision{{ 491 NewRevisionNumber: 1, 492 NewValidProofOutputs: []types.SiacoinOutput{ 493 {Value: types.SiacoinPrecision}, 494 {Value: types.ZeroCurrency}, 495 }, 496 NewMissedProofOutputs: []types.SiacoinOutput{ 497 {Value: types.SiacoinPrecision}, 498 {Value: types.ZeroCurrency}, 499 {Value: types.ZeroCurrency}, 500 }, 501 UnlockConditions: types.UnlockConditions{ 502 PublicKeys: []types.SiaPublicKey{{}, {}}, 503 }, 504 }}, 505 }, 506 } 507 initialRoots := []crypto.Hash{{1}} 508 contract, err := cs.managedInsertContract(initialHeader, initialRoots) 509 if err != nil { 510 t.Fatal(err) 511 } 512 sc := cs.managedMustAcquire(t, contract.ID) 513 514 // create a helper function that records the intent, creates the transaction 515 // containing the given revision and then commits the intent depending on 516 // whether the given flag was set to true 517 processTxnWithRevision := func(rev types.FileContractRevision, amount types.Currency, details skymodules.SpendingDetails, commit bool) { 518 // record the payment intent 519 walTxn, err := sc.RecordPaymentIntent(rev, amount, details) 520 if err != nil { 521 t.Fatal("Failed to record payment intent") 522 } 523 524 // create transaction containing the revision 525 signedTxn := rev.ToTransaction() 526 sig := sc.Sign(signedTxn.SigHash(0, blockHeight)) 527 signedTxn.TransactionSignatures[0].Signature = sig[:] 528 529 // only commit the intent if the flag is true 530 if !commit { 531 return 532 } 533 err = sc.CommitPaymentIntent(walTxn, signedTxn, amount, details) 534 if err != nil { 535 t.Fatal("Failed to commit payment intent") 536 } 537 } 538 539 // create a payment revision for a FundAccount RPC 540 curr := sc.LastRevision() 541 amount := types.NewCurrency64(10) 542 rpcCost := types.NewCurrency64(1) 543 rev, err := curr.PaymentRevision(amount.Add(rpcCost)) 544 if err != nil { 545 t.Fatal(err) 546 } 547 processTxnWithRevision(rev, amount, skymodules.SpendingDetails{ 548 FundAccountSpending: amount, 549 MaintenanceSpending: skymodules.MaintenanceSpending{FundAccountCost: rpcCost}, 550 }, true) 551 552 // create another payment revision, this time for an MDM RPC 553 curr = sc.LastRevision() 554 amount = types.NewCurrency64(20) 555 rpcCost = types.ZeroCurrency 556 rev, err = curr.PaymentRevision(amount.Add(rpcCost)) 557 if err != nil { 558 t.Fatal(err) 559 } 560 processTxnWithRevision(rev, amount, skymodules.SpendingDetails{}, true) 561 562 // create another payment revision, this time for a PT update RPC 563 curr = sc.LastRevision() 564 amount = types.NewCurrency64(3) 565 rpcCost = types.NewCurrency64(3) 566 rev, err = curr.PaymentRevision(amount.Add(rpcCost)) 567 if err != nil { 568 t.Fatal(err) 569 } 570 processTxnWithRevision(rev, amount, skymodules.SpendingDetails{ 571 MaintenanceSpending: skymodules.MaintenanceSpending{UpdatePriceTableCost: rpcCost}, 572 }, true) 573 expectedRevNumber := rev.NewRevisionNumber 574 575 // create another payment revision, for an account balance sync, 576 // but this time we don't commit it 577 curr = sc.LastRevision() 578 amount = types.NewCurrency64(4) 579 rpcCost = types.NewCurrency64(4) 580 rev, err = curr.PaymentRevision(amount.Add(rpcCost)) 581 if err != nil { 582 t.Fatal(err) 583 } 584 processTxnWithRevision(rev, amount, skymodules.SpendingDetails{ 585 MaintenanceSpending: skymodules.MaintenanceSpending{AccountBalanceCost: rpcCost}, 586 }, false) 587 588 // reload the contract set 589 cs, err = NewContractSet(dir, rl, modules.ProdDependencies) 590 if err != nil { 591 t.Fatal(err) 592 } 593 sc = cs.managedMustAcquire(t, contract.ID) 594 595 if sc.LastRevision().NewRevisionNumber != expectedRevNumber { 596 t.Fatal("Unexpected revision number after reloading the contract set") 597 } 598 599 // we expect the `FundAccount` spending metric to reflect exactly the amount 600 // of money that should have made it into the EA 601 expectedFundAccountSpending := types.NewCurrency64(10) 602 if !sc.header.FundAccountSpending.Equals(expectedFundAccountSpending) { 603 t.Fatal("unexpected", sc.header.FundAccountSpending) 604 } 605 606 // we expect the `Maintenance` spending metric to reflect the sum of the rpc 607 // cost for the fund account, and the amount spent on updating the price 608 // table. This means that the cost of the MDM RPC and the non committed 609 // account balance sync should not be included 610 expectedMaintenanceSpending := types.NewCurrency64(1).Add(types.NewCurrency64(3)) 611 if !sc.header.MaintenanceSpending.Sum().Equals(expectedMaintenanceSpending) { 612 t.Fatal("unexpected", sc.header.MaintenanceSpending) 613 } 614 } 615 616 // TestContractRefCounter checks if refCounter behaves as expected when called 617 // from Contract 618 func TestContractRefCounter(t *testing.T) { 619 if testing.Short() { 620 t.SkipNow() 621 } 622 t.Parallel() 623 624 // create a contract set 625 dir := build.TempDir(filepath.Join("proto", t.Name())) 626 rl := ratelimit.NewRateLimit(0, 0, 0) 627 cs, err := NewContractSet(dir, rl, modules.ProdDependencies) 628 if err != nil { 629 t.Fatal(err) 630 } 631 // add a contract 632 initialHeader := contractHeader{ 633 Transaction: types.Transaction{ 634 FileContractRevisions: []types.FileContractRevision{{ 635 NewRevisionNumber: 1, 636 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 637 UnlockConditions: types.UnlockConditions{ 638 PublicKeys: []types.SiaPublicKey{{}, {}}, 639 }, 640 }}, 641 }, 642 } 643 initialRoots := []crypto.Hash{{1}} 644 c, err := cs.managedInsertContract(initialHeader, initialRoots) 645 if err != nil { 646 t.Fatal(err) 647 } 648 sc := cs.managedMustAcquire(t, c.ID) 649 // verify that the refcounter exists and has the correct size 650 if sc.staticRC == nil { 651 t.Fatal("refCounter was not created with the contract.") 652 } 653 if sc.staticRC.numSectors != uint64(sc.merkleRoots.numMerkleRoots) { 654 t.Fatalf("refCounter has wrong number of sectors. Expected %d, found %d", uint64(sc.merkleRoots.numMerkleRoots), sc.staticRC.numSectors) 655 } 656 fi, err := os.Stat(sc.staticRC.filepath) 657 if err != nil { 658 t.Fatal("Failed to read refcounter file from disk:", err) 659 } 660 rcFileSize := refCounterHeaderSize + int64(sc.merkleRoots.numMerkleRoots)*2 661 if fi.Size() != rcFileSize { 662 t.Fatalf("refCounter file on disk has wrong size. Expected %d, got %d", rcFileSize, fi.Size()) 663 } 664 665 // upload a new sector 666 txn := types.Transaction{ 667 FileContractRevisions: []types.FileContractRevision{{ 668 NewRevisionNumber: 2, 669 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 670 UnlockConditions: types.UnlockConditions{ 671 PublicKeys: []types.SiaPublicKey{{}, {}}, 672 }, 673 }}, 674 } 675 revisedHeader := contractHeader{ 676 Transaction: txn, 677 StorageSpending: types.NewCurrency64(7), 678 UploadSpending: types.NewCurrency64(17), 679 } 680 newRev := revisedHeader.Transaction.FileContractRevisions[0] 681 newRoot := crypto.Hash{2} 682 storageCost := revisedHeader.StorageSpending.Sub(initialHeader.StorageSpending) 683 bandwidthCost := revisedHeader.UploadSpending.Sub(initialHeader.UploadSpending) 684 walTxn, err := sc.managedRecordRootUpdates(newRev, map[uint64]rootUpdate{ 685 uint64(len(initialRoots)): newRootUpdateAppendRoot(newRoot), 686 }, storageCost, bandwidthCost) 687 if err != nil { 688 t.Fatal(err) 689 } 690 // sign the transaction 691 txn.TransactionSignatures = []types.TransactionSignature{ 692 { 693 ParentID: crypto.Hash(newRev.ParentID), 694 CoveredFields: types.CoveredFields{FileContractRevisions: []uint64{0}}, 695 PublicKeyIndex: 0, // renter key is always first -- see formContract 696 }, 697 { 698 ParentID: crypto.Hash(newRev.ParentID), 699 PublicKeyIndex: 1, 700 CoveredFields: types.CoveredFields{FileContractRevisions: []uint64{0}}, 701 Signature: nil, // to be provided by host 702 }, 703 } 704 // commit the change 705 err = sc.managedCommitAppend(walTxn, txn, storageCost, bandwidthCost) 706 if err != nil { 707 t.Fatal(err) 708 } 709 // verify that the refcounter increased with 1, as expected 710 if sc.staticRC.numSectors != uint64(sc.merkleRoots.numMerkleRoots) { 711 t.Fatalf("refCounter has wrong number of sectors. Expected %d, found %d", uint64(sc.merkleRoots.numMerkleRoots), sc.staticRC.numSectors) 712 } 713 fi, err = os.Stat(sc.staticRC.filepath) 714 if err != nil { 715 t.Fatal("Failed to read refcounter file from disk:", err) 716 } 717 rcFileSize = refCounterHeaderSize + int64(sc.merkleRoots.numMerkleRoots)*2 718 if fi.Size() != rcFileSize { 719 t.Fatalf("refCounter file on disk has wrong size. Expected %d, got %d", rcFileSize, fi.Size()) 720 } 721 } 722 723 // TestContractRecordCommitDownloadIntent tests recording and committing 724 // downloads and makes sure they use the wal correctly. 725 func TestContractRecordCommitDownloadIntent(t *testing.T) { 726 if testing.Short() { 727 t.SkipNow() 728 } 729 t.Parallel() 730 731 blockHeight := types.BlockHeight(fastrand.Intn(100)) 732 733 // create contract set 734 dir := build.TempDir(filepath.Join("proto", t.Name())) 735 rl := ratelimit.NewRateLimit(0, 0, 0) 736 cs, err := NewContractSet(dir, rl, modules.ProdDependencies) 737 if err != nil { 738 t.Fatal(err) 739 } 740 741 // add a contract 742 initialHeader := contractHeader{ 743 Transaction: types.Transaction{ 744 FileContractRevisions: []types.FileContractRevision{{ 745 NewRevisionNumber: 1, 746 NewValidProofOutputs: []types.SiacoinOutput{ 747 {Value: types.SiacoinPrecision}, 748 {Value: types.ZeroCurrency}, 749 }, 750 NewMissedProofOutputs: []types.SiacoinOutput{ 751 {Value: types.SiacoinPrecision}, 752 {Value: types.ZeroCurrency}, 753 {Value: types.ZeroCurrency}, 754 }, 755 UnlockConditions: types.UnlockConditions{ 756 PublicKeys: []types.SiaPublicKey{{}, {}}, 757 }, 758 }}, 759 }, 760 } 761 initialRoots := []crypto.Hash{{1}} 762 contract, err := cs.managedInsertContract(initialHeader, initialRoots) 763 if err != nil { 764 t.Fatal(err) 765 } 766 sc := cs.managedMustAcquire(t, contract.ID) 767 768 // create a download revision 769 curr := sc.LastRevision() 770 amount := types.NewCurrency64(fastrand.Uint64n(100)) 771 rev, err := newDownloadRevision(curr, amount) 772 if err != nil { 773 t.Fatal(err) 774 } 775 776 // record the download intent 777 walTxn, err := sc.managedRecordDownloadIntent(rev, amount) 778 if err != nil { 779 t.Fatal("Failed to record payment intent") 780 } 781 if len(sc.unappliedTxns) != 1 { 782 t.Fatalf("expected %v unapplied txns but got %v", 1, len(sc.unappliedTxns)) 783 } 784 785 // create transaction containing the revision 786 signedTxn := rev.ToTransaction() 787 sig := sc.Sign(signedTxn.SigHash(0, blockHeight)) 788 signedTxn.TransactionSignatures[0].Signature = sig[:] 789 790 // don't commit the download. Instead simulate a crash by reloading the 791 // contract set. 792 cs, err = NewContractSet(dir, rl, modules.ProdDependencies) 793 if err != nil { 794 t.Fatal(err) 795 } 796 sc = cs.managedMustAcquire(t, contract.ID) 797 798 if sc.LastRevision().NewRevisionNumber != curr.NewRevisionNumber { 799 t.Fatal("Unexpected revision number after reloading the contract set") 800 } 801 if len(sc.unappliedTxns) != 1 { 802 t.Fatalf("expected %v unapplied txns but got %v", 0, len(sc.unappliedTxns)) 803 } 804 805 // start a new download 806 walTxn, err = sc.managedRecordDownloadIntent(rev, amount) 807 if err != nil { 808 t.Fatal("Failed to record payment intent") 809 } 810 if len(sc.unappliedTxns) != 2 { 811 t.Fatalf("expected %v unapplied txns but got %v", 2, len(sc.unappliedTxns)) 812 } 813 814 // commit the download. This should remove all unapplied txns. 815 err = sc.managedCommitDownload(walTxn, signedTxn, amount) 816 if err != nil { 817 t.Fatal("Failed to commit payment intent") 818 } 819 if len(sc.unappliedTxns) != 0 { 820 t.Fatalf("expected %v unapplied txns but got %v", 0, len(sc.unappliedTxns)) 821 } 822 823 // restart again. We still expect 0 unapplied txns. 824 cs, err = NewContractSet(dir, rl, modules.ProdDependencies) 825 if err != nil { 826 t.Fatal(err) 827 } 828 sc = cs.managedMustAcquire(t, contract.ID) 829 830 if sc.LastRevision().NewRevisionNumber != rev.NewRevisionNumber { 831 t.Fatal("Unexpected revision number after reloading the contract set") 832 } 833 if len(sc.unappliedTxns) != 0 { 834 t.Fatalf("expected %v unapplied txns but got %v", 0, len(sc.unappliedTxns)) 835 } 836 } 837 838 // TestContractRecordCommitAppendIntent tests recording and committing 839 // downloads and makes sure they use the wal correctly. 840 func TestContractRecordCommitAppendIntent(t *testing.T) { 841 if testing.Short() { 842 t.SkipNow() 843 } 844 t.Parallel() 845 846 blockHeight := types.BlockHeight(fastrand.Intn(100)) 847 848 // create contract set 849 dir := build.TempDir(filepath.Join("proto", t.Name())) 850 rl := ratelimit.NewRateLimit(0, 0, 0) 851 cs, err := NewContractSet(dir, rl, modules.ProdDependencies) 852 if err != nil { 853 t.Fatal(err) 854 } 855 856 // add a contract 857 initialHeader := contractHeader{ 858 Transaction: types.Transaction{ 859 FileContractRevisions: []types.FileContractRevision{{ 860 NewRevisionNumber: 1, 861 NewValidProofOutputs: []types.SiacoinOutput{ 862 {Value: types.SiacoinPrecision}, 863 {Value: types.SiacoinPrecision}, 864 }, 865 NewMissedProofOutputs: []types.SiacoinOutput{ 866 {Value: types.SiacoinPrecision}, 867 {Value: types.SiacoinPrecision}, 868 {Value: types.ZeroCurrency}, 869 }, 870 UnlockConditions: types.UnlockConditions{ 871 PublicKeys: []types.SiaPublicKey{{}, {}}, 872 }, 873 }}, 874 }, 875 } 876 initialRoots := []crypto.Hash{{1}} 877 contract, err := cs.managedInsertContract(initialHeader, initialRoots) 878 if err != nil { 879 t.Fatal(err) 880 } 881 sc := cs.managedMustAcquire(t, contract.ID) 882 883 // create a append revision 884 curr := sc.LastRevision() 885 bandwidth := types.NewCurrency64(fastrand.Uint64n(100)) 886 collateral := types.NewCurrency64(fastrand.Uint64n(100)) 887 storage := types.NewCurrency64(fastrand.Uint64n(100)) 888 newRoot := crypto.Hash{1} 889 rev, err := newUploadRevision(curr, newRoot, bandwidth.Add(storage), collateral) 890 if err != nil { 891 t.Fatal(err) 892 } 893 894 // record the append intent 895 walTxn, err := sc.managedRecordRootUpdates(rev, map[uint64]rootUpdate{ 896 uint64(len(initialRoots)): newRootUpdateAppendRoot(newRoot), 897 }, storage, bandwidth) 898 if err != nil { 899 t.Fatal("Failed to record payment intent") 900 } 901 if len(sc.unappliedTxns) != 1 { 902 t.Fatalf("expected %v unapplied txns but got %v", 1, len(sc.unappliedTxns)) 903 } 904 905 // create transaction containing the revision 906 signedTxn := rev.ToTransaction() 907 sig := sc.Sign(signedTxn.SigHash(0, blockHeight)) 908 signedTxn.TransactionSignatures[0].Signature = sig[:] 909 910 // don't commit the download. Instead simulate a crash by reloading the 911 // contract set. 912 cs, err = NewContractSet(dir, rl, modules.ProdDependencies) 913 if err != nil { 914 t.Fatal(err) 915 } 916 sc = cs.managedMustAcquire(t, contract.ID) 917 918 if sc.LastRevision().NewRevisionNumber != curr.NewRevisionNumber { 919 t.Fatal("Unexpected revision number after reloading the contract set") 920 } 921 if len(sc.unappliedTxns) != 1 { 922 t.Fatalf("expected %v unapplied txns but got %v", 0, len(sc.unappliedTxns)) 923 } 924 925 // start a new append 926 walTxn, err = sc.managedRecordRootUpdates(rev, map[uint64]rootUpdate{ 927 uint64(len(initialRoots) + 1): newRootUpdateAppendRoot(newRoot), 928 }, storage, bandwidth) 929 if err != nil { 930 t.Fatal("Failed to record payment intent") 931 } 932 if len(sc.unappliedTxns) != 2 { 933 t.Fatalf("expected %v unapplied txns but got %v", 2, len(sc.unappliedTxns)) 934 } 935 936 // commit the append. This should remove all unapplied txns. 937 err = sc.managedCommitAppend(walTxn, signedTxn, storage, bandwidth) 938 if err != nil { 939 t.Fatal("Failed to commit payment intent") 940 } 941 if len(sc.unappliedTxns) != 0 { 942 t.Fatalf("expected %v unapplied txns but got %v", 0, len(sc.unappliedTxns)) 943 } 944 945 // restart again. We still expect 0 unapplied txns. 946 cs, err = NewContractSet(dir, rl, modules.ProdDependencies) 947 if err != nil { 948 t.Fatal(err) 949 } 950 sc = cs.managedMustAcquire(t, contract.ID) 951 952 if sc.LastRevision().NewRevisionNumber != rev.NewRevisionNumber { 953 t.Fatal("Unexpected revision number after reloading the contract set") 954 } 955 if len(sc.unappliedTxns) != 0 { 956 t.Fatalf("expected %v unapplied txns but got %v", 0, len(sc.unappliedTxns)) 957 } 958 } 959 960 // TestContractRecordCommitRenewAndClearIntent tests recording and committing 961 // downloads and makes sure they use the wal correctly. 962 func TestContractRecordCommitRenewAndClearIntent(t *testing.T) { 963 if testing.Short() { 964 t.SkipNow() 965 } 966 t.Parallel() 967 968 blockHeight := types.BlockHeight(fastrand.Intn(100)) 969 970 // create contract set 971 dir := build.TempDir(filepath.Join("proto", t.Name())) 972 rl := ratelimit.NewRateLimit(0, 0, 0) 973 cs, err := NewContractSet(dir, rl, modules.ProdDependencies) 974 if err != nil { 975 t.Fatal(err) 976 } 977 978 // add a contract 979 initialHeader := contractHeader{ 980 Transaction: types.Transaction{ 981 FileContractRevisions: []types.FileContractRevision{{ 982 NewRevisionNumber: 1, 983 NewValidProofOutputs: []types.SiacoinOutput{ 984 {Value: types.SiacoinPrecision}, 985 {Value: types.ZeroCurrency}, 986 }, 987 NewMissedProofOutputs: []types.SiacoinOutput{ 988 {Value: types.SiacoinPrecision}, 989 {Value: types.ZeroCurrency}, 990 {Value: types.ZeroCurrency}, 991 }, 992 UnlockConditions: types.UnlockConditions{ 993 PublicKeys: []types.SiaPublicKey{{}, {}}, 994 }, 995 }}, 996 }, 997 } 998 initialRoots := []crypto.Hash{{1}} 999 contract, err := cs.managedInsertContract(initialHeader, initialRoots) 1000 if err != nil { 1001 t.Fatal(err) 1002 } 1003 sc := cs.managedMustAcquire(t, contract.ID) 1004 1005 // create a renew revision. It's the same as a payment revision with small 1006 // differences. 1007 bandwidth := types.NewCurrency64(fastrand.Uint64n(100)) 1008 curr := sc.LastRevision() 1009 rev, err := curr.PaymentRevision(bandwidth) 1010 if err != nil { 1011 t.Fatal(err) 1012 } 1013 rev.NewFileSize = 0 1014 rev.NewFileSize = 0 1015 rev.NewFileMerkleRoot = crypto.Hash{} 1016 rev.NewRevisionNumber = math.MaxUint64 1017 rev.NewMissedProofOutputs = rev.NewValidProofOutputs 1018 1019 // record the clear contract intent 1020 walTxn, err := sc.managedRecordClearContractIntent(rev, bandwidth) 1021 if err != nil { 1022 t.Fatal("Failed to record payment intent") 1023 } 1024 if len(sc.unappliedTxns) != 1 { 1025 t.Fatalf("expected %v unapplied txns but got %v", 1, len(sc.unappliedTxns)) 1026 } 1027 1028 // create transaction containing the revision 1029 signedTxn := rev.ToTransaction() 1030 sig := sc.Sign(signedTxn.SigHash(0, blockHeight)) 1031 signedTxn.TransactionSignatures[0].Signature = sig[:] 1032 1033 // don't commit the download. Instead simulate a crash by reloading the 1034 // contract set. 1035 cs, err = NewContractSet(dir, rl, modules.ProdDependencies) 1036 if err != nil { 1037 t.Fatal(err) 1038 } 1039 sc = cs.managedMustAcquire(t, contract.ID) 1040 1041 if sc.LastRevision().NewRevisionNumber != curr.NewRevisionNumber { 1042 t.Fatal("Unexpected revision number after reloading the contract set") 1043 } 1044 if len(sc.unappliedTxns) != 1 { 1045 t.Fatalf("expected %v unapplied txns but got %v", 0, len(sc.unappliedTxns)) 1046 } 1047 1048 // start a new download 1049 walTxn, err = sc.managedRecordClearContractIntent(rev, bandwidth) 1050 if err != nil { 1051 t.Fatal("Failed to record payment intent") 1052 } 1053 if len(sc.unappliedTxns) != 2 { 1054 t.Fatalf("expected %v unapplied txns but got %v", 2, len(sc.unappliedTxns)) 1055 } 1056 1057 // commit the download. This should remove all unapplied txns. 1058 err = sc.managedCommitClearContract(walTxn, signedTxn, bandwidth) 1059 if err != nil { 1060 t.Fatal("Failed to commit payment intent") 1061 } 1062 if len(sc.unappliedTxns) != 0 { 1063 t.Fatalf("expected %v unapplied txns but got %v", 0, len(sc.unappliedTxns)) 1064 } 1065 1066 // restart again. We still expect 0 unapplied txns. 1067 cs, err = NewContractSet(dir, rl, modules.ProdDependencies) 1068 if err != nil { 1069 t.Fatal(err) 1070 } 1071 sc = cs.managedMustAcquire(t, contract.ID) 1072 1073 if sc.LastRevision().NewRevisionNumber != rev.NewRevisionNumber { 1074 t.Fatal("Unexpected revision number after reloading the contract set") 1075 } 1076 if len(sc.unappliedTxns) != 0 { 1077 t.Fatalf("expected %v unapplied txns but got %v", 0, len(sc.unappliedTxns)) 1078 } 1079 if sc.Utility().GoodForRenew { 1080 t.Fatal("contract shouldn't be good for renew") 1081 } 1082 if sc.Utility().GoodForUpload { 1083 t.Fatal("contract shouldn't be good for upload") 1084 } 1085 if !sc.Utility().Locked { 1086 t.Fatal("contract should be locked") 1087 } 1088 } 1089 1090 // TestPanicOnOverwritingNewerRevision tests if attempting to 1091 // overwrite a contract header with an old revision triggers a panic. 1092 func TestPanicOnOverwritingNewerRevision(t *testing.T) { 1093 if testing.Short() { 1094 t.SkipNow() 1095 } 1096 t.Parallel() 1097 // create contract set with one contract 1098 dir := build.TempDir(filepath.Join("proto", t.Name())) 1099 rl := ratelimit.NewRateLimit(0, 0, 0) 1100 cs, err := NewContractSet(dir, rl, modules.ProdDependencies) 1101 if err != nil { 1102 t.Fatal(err) 1103 } 1104 header := contractHeader{ 1105 Transaction: types.Transaction{ 1106 FileContractRevisions: []types.FileContractRevision{{ 1107 NewRevisionNumber: 2, 1108 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 1109 UnlockConditions: types.UnlockConditions{ 1110 PublicKeys: []types.SiaPublicKey{{}, {}}, 1111 }, 1112 }}, 1113 }, 1114 } 1115 initialRoots := []crypto.Hash{{1}} 1116 c, err := cs.managedInsertContract(header, initialRoots) 1117 if err != nil { 1118 t.Fatal(err) 1119 } 1120 1121 sc, ok := cs.Acquire(c.ID) 1122 if !ok { 1123 t.Fatal("failed to acquire contract") 1124 } 1125 // Trying to set a header with an older revision should trigger a panic. 1126 header.Transaction.FileContractRevisions[0].NewRevisionNumber = 1 1127 defer func() { 1128 if r := recover(); r == nil { 1129 t.Fatalf("expected panic when attempting to overwrite a newer contract header revision") 1130 } 1131 }() 1132 if err := sc.applySetHeader(header); err != nil { 1133 t.Fatal(err) 1134 } 1135 }