gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/workerjobrenewcontract_test.go (about) 1 package renter 2 3 import ( 4 "context" 5 "reflect" 6 "testing" 7 8 "gitlab.com/NebulousLabs/fastrand" 9 "gitlab.com/SkynetLabs/skyd/skymodules" 10 "gitlab.com/SkynetLabs/skyd/skymodules/renter/proto" 11 "go.sia.tech/siad/crypto" 12 "go.sia.tech/siad/modules" 13 "go.sia.tech/siad/types" 14 ) 15 16 // TestRenewContract is a unit test for the worker's RenewContract method. 17 func TestRenewContract(t *testing.T) { 18 if testing.Short() { 19 t.SkipNow() 20 } 21 t.Parallel() 22 23 // Create a worker. 24 wt, err := newWorkerTester(t.Name()) 25 if err != nil { 26 t.Fatal(err) 27 } 28 29 // Close the worker. 30 defer func() { 31 if err := wt.Close(); err != nil { 32 t.Fatal(err) 33 } 34 }() 35 36 // Upload a snapshot to the snapshot table. This makes sure that we got some 37 // data in the contract. 38 err = wt.UploadSnapshot(context.Background(), skymodules.UploadedBackup{UID: [16]byte{1, 2, 3}}, fastrand.Bytes(100)) 39 if err != nil { 40 t.Fatal(err) 41 } 42 43 // Get a transaction builder and add the funding. 44 funding := types.SiacoinPrecision 45 46 // Get the host from the hostdb. 47 host, _, err := wt.staticRenter.staticHostDB.Host(wt.staticHostPubKey) 48 if err != nil { 49 t.Fatal(err) 50 } 51 52 // Get the wallet seed 53 seed, _, err := wt.rt.wallet.PrimarySeed() 54 if err != nil { 55 t.Fatal(err) 56 } 57 58 // Get some more vars. 59 allowance := wt.rt.renter.staticHostContractor.Allowance() 60 bh, _ := wt.managedSyncInfo() 61 rs := skymodules.DeriveRenterSeed(seed) 62 63 // Define some params for the contract. 64 params := skymodules.ContractParams{ 65 Allowance: allowance, 66 Host: host, 67 Funding: funding, 68 StartHeight: bh, 69 EndHeight: bh + allowance.Period, 70 RefundAddress: types.UnlockHash{}, 71 RenterSeed: rs.EphemeralRenterSeed(bh + allowance.Period), 72 } 73 74 // Get a txnbuilder. 75 txnBuilder, err := wt.rt.wallet.StartTransaction() 76 if err != nil { 77 t.Fatal(err) 78 } 79 err = txnBuilder.FundSiacoins(funding) 80 if err != nil { 81 t.Fatal(err) 82 } 83 84 // Check contract before renewal. 85 oldContractPreRenew, ok := wt.staticRenter.staticHostContractor.ContractByPublicKey(params.Host.PublicKey) 86 if !ok { 87 t.Fatal("contract doesn't exist") 88 } 89 if oldContractPreRenew.Size() == 0 { 90 t.Fatal("contract shouldnt be empty pre renewal") 91 } 92 if len(oldContractPreRenew.Transaction.FileContractRevisions) == 0 { 93 t.Fatal("no Revisions") 94 } 95 oldRevisionPreRenew := oldContractPreRenew.Transaction.FileContractRevisions[0] 96 oldMerkleRoot := oldRevisionPreRenew.NewFileMerkleRoot 97 if oldMerkleRoot == (crypto.Hash{}) { 98 t.Fatal("empty root") 99 } 100 101 // Renew the contract. 102 _, _, err = wt.RenewContract(context.Background(), oldContractPreRenew.ID, params, txnBuilder) 103 if err != nil { 104 t.Fatal(err) 105 } 106 107 // Mine a block to mine the contract and trigger maintenance. 108 b, err := wt.rt.miner.AddBlock() 109 if err != nil { 110 t.Fatal(err) 111 } 112 113 // Check if the right txn was mined. It should contain both a revision and 114 // contract. 115 found := false 116 var fcr types.FileContractRevision 117 var newContract types.FileContract 118 var fcTxn types.Transaction 119 for _, txn := range b.Transactions { 120 if len(txn.FileContractRevisions) == 1 && len(txn.FileContracts) == 1 { 121 fcr = txn.FileContractRevisions[0] 122 newContract = txn.FileContracts[0] 123 fcTxn = txn 124 found = true 125 break 126 } 127 } 128 if !found { 129 t.Fatal("txn containing both the final revision and contract wasn't mined") 130 } 131 132 // Get the old contract and revision. 133 var oldContract skymodules.RenterContract 134 oldContracts := wt.staticRenter.OldContracts() 135 for _, c := range oldContracts { 136 if c.HostPublicKey.String() == params.Host.PublicKey.String() { 137 oldContract = c 138 } 139 } 140 if len(oldContract.Transaction.FileContractRevisions) == 0 { 141 t.Fatal("no Revisions") 142 } 143 oldRevision := oldContract.Transaction.FileContractRevisions[0] 144 145 // Old contract should be empty after renewal. 146 size := oldRevision.NewFileSize 147 if size != 0 { 148 t.Fatal("size should be zero") 149 } 150 merkleRoot := oldRevision.NewFileMerkleRoot 151 if merkleRoot != (crypto.Hash{}) { 152 t.Fatal("root should be empty") 153 } 154 155 // Check final revision. 156 // 157 // ParentID should match the old contract's. 158 if fcr.ParentID != oldContract.ID { 159 t.Fatalf("expected fcr to have parent %v but was %v", oldContract.ID, fcr.ParentID) 160 } 161 // Valid and missed outputs should match. 162 if !reflect.DeepEqual(fcr.NewMissedProofOutputs, fcr.NewValidProofOutputs) { 163 t.Fatal("expected valid outputs to match missed ones") 164 } 165 // Filesize should be 0 and root should be empty. 166 if fcr.NewFileSize != 0 { 167 t.Fatal("size should be 0", fcr.NewFileSize) 168 } 169 if fcr.NewFileMerkleRoot != (crypto.Hash{}) { 170 t.Fatal("size should be 0", fcr.NewFileSize) 171 } 172 // Valid and missed outputs should match for renter and host between final 173 // revision and the one before that. 174 if !fcr.ValidRenterPayout().Equals(oldRevision.ValidRenterPayout()) { 175 t.Fatal("renter payouts don't match between old revision and final revision") 176 } 177 if !fcr.ValidHostPayout().Equals(oldRevision.ValidHostPayout()) { 178 t.Fatal("host payouts don't match between old revision and final revision") 179 } 180 181 // Compute the expected payouts of the new contract. 182 pt := wt.staticPriceTable().staticPriceTable 183 basePrice, baseCollateral := skymodules.RenewBaseCosts(oldRevisionPreRenew, &pt, params.EndHeight) 184 allowance, startHeight, endHeight, host, funding := params.Allowance, params.StartHeight, params.EndHeight, params.Host, params.Funding 185 period := endHeight - startHeight 186 txnFee := pt.TxnFeeMaxRecommended.Mul64(proto.FileContractTxnEstimateMultiplier * skymodules.EstimatedFileContractTransactionSetSize) 187 renterPayout, hostPayout, hostCollateral, err := skymodules.RenterPayoutsPreTax(host, funding, txnFee, basePrice, baseCollateral, period, allowance.ExpectedStorage/allowance.Hosts) 188 if err != nil { 189 t.Fatal(err) 190 } 191 totalPayout := renterPayout.Add(hostPayout) 192 193 // Check the new contract. 194 if newContract.FileMerkleRoot != oldMerkleRoot { 195 t.Fatal("new contract got wrong root") 196 } 197 if newContract.FileSize != oldContractPreRenew.Size() { 198 t.Fatal("new contract got wrong size") 199 } 200 if !newContract.Payout.Equals(totalPayout) { 201 t.Fatal("wrong payout") 202 } 203 if newContract.WindowStart != params.EndHeight { 204 t.Fatal("wrong window start") 205 } 206 if newContract.WindowEnd != params.EndHeight+params.Host.WindowSize { 207 t.Fatal("wrong window end") 208 } 209 _, ourPK := skymodules.GenerateContractKeyPair(params.RenterSeed, fcTxn) 210 uh := types.UnlockConditions{ 211 PublicKeys: []types.SiaPublicKey{ 212 types.Ed25519PublicKey(ourPK), 213 wt.staticHostPubKey, 214 }, 215 SignaturesRequired: 2, 216 }.UnlockHash() 217 if newContract.UnlockHash != uh { 218 t.Fatal("unlock hash doesn't match") 219 } 220 if newContract.RevisionNumber != 0 { 221 t.Fatal("revision number isn't 0") 222 } 223 expectedValidRenterOutput := types.SiacoinOutput{ 224 Value: types.PostTax(params.StartHeight, totalPayout).Sub(hostPayout), 225 UnlockHash: params.RefundAddress, 226 } 227 expectedMissedRenterOutput := expectedValidRenterOutput 228 expectedValidHostOutput := types.SiacoinOutput{ 229 Value: hostPayout, 230 UnlockHash: params.Host.UnlockHash, 231 } 232 expectedMissedHostOutput := types.SiacoinOutput{ 233 Value: hostCollateral.Sub(baseCollateral).Add(params.Host.ContractPrice), 234 UnlockHash: params.Host.UnlockHash, 235 } 236 expectedVoidOutput := types.SiacoinOutput{ 237 Value: basePrice.Add(baseCollateral), 238 UnlockHash: types.UnlockHash{}, 239 } 240 if !reflect.DeepEqual(newContract.ValidRenterOutput(), expectedValidRenterOutput) { 241 t.Fatal("wrong output") 242 } 243 if !reflect.DeepEqual(newContract.ValidHostOutput(), expectedValidHostOutput) { 244 t.Fatal("wrong output") 245 } 246 if !reflect.DeepEqual(newContract.MissedRenterOutput(), expectedMissedRenterOutput) { 247 t.Fatal("wrong output") 248 } 249 if !reflect.DeepEqual(newContract.MissedHostOutput(), expectedMissedHostOutput) { 250 t.Fatal("wrong output") 251 } 252 mvo, err := newContract.MissedVoidOutput() 253 if err != nil { 254 t.Fatal(err) 255 } 256 if !reflect.DeepEqual(mvo, expectedVoidOutput) { 257 t.Fatal("wrong output") 258 } 259 260 // Get the new contract's revision from the contractor. 261 c, found := wt.staticRenter.staticHostContractor.ContractByPublicKey(wt.staticHostPubKey) 262 if !found { 263 t.Fatal("contract not found in contractor") 264 } 265 if len(c.Transaction.FileContractRevisions) == 0 { 266 t.Fatal("no Revisions") 267 } 268 rev := c.Transaction.FileContractRevisions[0] 269 270 // Check the revision. 271 if rev.NewFileMerkleRoot != oldMerkleRoot { 272 t.Fatalf("new contract revision got wrong root %v", rev.NewFileMerkleRoot) 273 } 274 if rev.NewFileSize != oldContractPreRenew.Size() { 275 t.Fatal("new contract revision got wrong size") 276 } 277 if rev.NewWindowStart != params.EndHeight { 278 t.Fatal("wrong window start") 279 } 280 if rev.NewWindowEnd != params.EndHeight+params.Host.WindowSize { 281 t.Fatal("wrong window end") 282 } 283 if rev.NewUnlockHash != uh { 284 t.Fatal("unlock hash doesn't match") 285 } 286 if rev.NewRevisionNumber != 1 { 287 t.Fatal("revision number isn't 1") 288 } 289 if !reflect.DeepEqual(rev.ValidRenterOutput(), expectedValidRenterOutput) { 290 t.Fatal("wrong output") 291 } 292 if !reflect.DeepEqual(rev.ValidHostOutput(), expectedValidHostOutput) { 293 t.Fatal("wrong output") 294 } 295 if !reflect.DeepEqual(rev.MissedRenterOutput(), expectedMissedRenterOutput) { 296 t.Fatal("wrong output") 297 } 298 if !reflect.DeepEqual(rev.MissedHostOutput(), expectedMissedHostOutput) { 299 t.Fatal("wrong output") 300 } 301 302 // Try using the contract now. Should work. 303 err = wt.UploadSnapshot(context.Background(), skymodules.UploadedBackup{UID: [16]byte{3, 2, 1}}, fastrand.Bytes(100)) 304 if err != nil { 305 t.Fatal(err) 306 } 307 _, err = wt.ReadOffset(context.Background(), categorySnapshotDownload, 0, modules.SectorSize) 308 if err != nil { 309 t.Fatal(err) 310 } 311 } 312 313 // TestRenewContractEmptyPriceTableUID is a unit test for the worker's 314 // RenewContract method with a price table that has an empty UID. 315 func TestRenewContractEmptyPriceTableUID(t *testing.T) { 316 if testing.Short() { 317 t.SkipNow() 318 } 319 t.Parallel() 320 321 // Create a worker. 322 wt, err := newWorkerTester(t.Name()) 323 if err != nil { 324 t.Fatal(err) 325 } 326 327 // Close the worker. 328 defer func() { 329 if err := wt.Close(); err != nil { 330 t.Fatal(err) 331 } 332 }() 333 334 // Get a transaction builder and add the funding. 335 funding := types.SiacoinPrecision 336 337 // Get the host from the hostdb. 338 host, _, err := wt.staticRenter.staticHostDB.Host(wt.staticHostPubKey) 339 if err != nil { 340 t.Fatal(err) 341 } 342 343 // Get the wallet seed 344 seed, _, err := wt.rt.wallet.PrimarySeed() 345 if err != nil { 346 t.Fatal(err) 347 } 348 349 // Get some more vars. 350 allowance := wt.rt.renter.staticHostContractor.Allowance() 351 bh, _ := wt.managedSyncInfo() 352 rs := skymodules.DeriveRenterSeed(seed) 353 354 // Define some params for the contract. 355 params := skymodules.ContractParams{ 356 Allowance: allowance, 357 Host: host, 358 Funding: funding, 359 StartHeight: bh, 360 EndHeight: bh + allowance.Period, 361 RefundAddress: types.UnlockHash{}, 362 RenterSeed: rs.EphemeralRenterSeed(bh + allowance.Period), 363 } 364 365 // Get a txnbuilder. 366 txnBuilder, err := wt.rt.wallet.StartTransaction() 367 if err != nil { 368 t.Fatal(err) 369 } 370 err = txnBuilder.FundSiacoins(funding) 371 if err != nil { 372 t.Fatal(err) 373 } 374 375 // Check contract before renewal. 376 oldContractPreRenew, ok := wt.staticRenter.staticHostContractor.ContractByPublicKey(params.Host.PublicKey) 377 if !ok { 378 t.Fatal("contract doesn't exist") 379 } 380 381 // Overwrite the UID of the price table. 382 // 383 // Neet to copy the received price table before modifying it to prevent a 384 // race condition. 385 var pt workerPriceTable 386 wpt := wt.staticPriceTable() 387 pt = *wpt 388 pt.staticPriceTable.UID = modules.UniqueID{} 389 wt.staticSetPriceTable(&pt) 390 391 // Renew the contract. This should work without error. 392 _, _, err = wt.RenewContract(context.Background(), oldContractPreRenew.ID, params, txnBuilder) 393 if err != nil { 394 t.Fatal(err) 395 } 396 }