gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/proto/contractset_test.go (about) 1 package proto 2 3 import ( 4 "bytes" 5 "io/ioutil" 6 "os" 7 "path/filepath" 8 "reflect" 9 "sync" 10 "testing" 11 "time" 12 13 "gitlab.com/NebulousLabs/errors" 14 "gitlab.com/NebulousLabs/fastrand" 15 "gitlab.com/NebulousLabs/ratelimit" 16 "gitlab.com/NebulousLabs/writeaheadlog" 17 18 "gitlab.com/NebulousLabs/encoding" 19 "gitlab.com/SkynetLabs/skyd/build" 20 "gitlab.com/SkynetLabs/skyd/skymodules" 21 "go.sia.tech/siad/crypto" 22 "go.sia.tech/siad/modules" 23 "go.sia.tech/siad/types" 24 ) 25 26 // managedMustAcquire is a convenience function for acquiring contracts that are 27 // known to be in the set. 28 func (cs *ContractSet) managedMustAcquire(t *testing.T, id types.FileContractID) *SafeContract { 29 t.Helper() 30 c, ok := cs.Acquire(id) 31 if !ok { 32 t.Fatal("no contract with that id") 33 } 34 return c 35 } 36 37 // TestContractSet tests that the ContractSet type is safe for concurrent use. 38 func TestContractSet(t *testing.T) { 39 if testing.Short() { 40 t.SkipNow() 41 } 42 t.Parallel() 43 // create contract set 44 testDir := build.TempDir(t.Name()) 45 rl := ratelimit.NewRateLimit(0, 0, 0) 46 cs, err := NewContractSet(testDir, rl, modules.ProdDependencies) 47 if err != nil { 48 t.Fatal(err) 49 } 50 51 header1 := contractHeader{Transaction: types.Transaction{ 52 FileContractRevisions: []types.FileContractRevision{{ 53 ParentID: types.FileContractID{1}, 54 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 55 UnlockConditions: types.UnlockConditions{ 56 PublicKeys: []types.SiaPublicKey{{}, {}}, 57 }, 58 }}, 59 }} 60 header2 := contractHeader{Transaction: types.Transaction{ 61 FileContractRevisions: []types.FileContractRevision{{ 62 ParentID: types.FileContractID{2}, 63 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 64 UnlockConditions: types.UnlockConditions{ 65 PublicKeys: []types.SiaPublicKey{{}, {}}, 66 }, 67 }}, 68 }} 69 id1 := header1.ID() 70 id2 := header2.ID() 71 72 _, err = cs.managedInsertContract(header1, []crypto.Hash{}) 73 if err != nil { 74 t.Fatal(err) 75 } 76 _, err = cs.managedInsertContract(header2, []crypto.Hash{}) 77 if err != nil { 78 t.Fatal(err) 79 } 80 81 // uncontested acquire/release 82 c1 := cs.managedMustAcquire(t, id1) 83 cs.Return(c1) 84 85 // 100 concurrent serialized mutations 86 var wg sync.WaitGroup 87 for i := 0; i < 100; i++ { 88 wg.Add(1) 89 go func() { 90 defer wg.Done() 91 c1 := cs.managedMustAcquire(t, id1) 92 c1.header.Transaction.FileContractRevisions[0].NewRevisionNumber++ 93 time.Sleep(time.Duration(fastrand.Intn(100))) 94 cs.Return(c1) 95 }() 96 } 97 wg.Wait() 98 c1 = cs.managedMustAcquire(t, id1) 99 cs.Return(c1) 100 if c1.header.LastRevision().NewRevisionNumber != 100 { 101 t.Fatal("expected exactly 100 increments, got", c1.header.LastRevision().NewRevisionNumber) 102 } 103 104 // a blocked acquire shouldn't prevent a return 105 c1 = cs.managedMustAcquire(t, id1) 106 go func() { 107 time.Sleep(time.Millisecond) 108 cs.Return(c1) 109 }() 110 c1 = cs.managedMustAcquire(t, id1) 111 cs.Return(c1) 112 113 // delete and reinsert id2 114 c2 := cs.managedMustAcquire(t, id2) 115 cs.Delete(c2) 116 roots, err := c2.merkleRoots.merkleRoots() 117 if err != nil { 118 t.Fatal(err) 119 } 120 cs.managedInsertContract(c2.header, roots) 121 122 // call all the methods in parallel haphazardly 123 funcs := []func(){ 124 func() { cs.Len() }, 125 func() { cs.IDs() }, 126 func() { cs.View(id1); cs.View(id2) }, 127 func() { cs.ViewAll() }, 128 func() { cs.Return(cs.managedMustAcquire(t, id1)) }, 129 func() { cs.Return(cs.managedMustAcquire(t, id2)) }, 130 func() { 131 header3 := contractHeader{ 132 Transaction: types.Transaction{ 133 FileContractRevisions: []types.FileContractRevision{{ 134 ParentID: types.FileContractID{3}, 135 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 136 UnlockConditions: types.UnlockConditions{ 137 PublicKeys: []types.SiaPublicKey{{}, {}}, 138 }, 139 }}, 140 }, 141 } 142 id3 := header3.ID() 143 _, err := cs.managedInsertContract(header3, []crypto.Hash{}) 144 if err != nil { 145 t.Fatal(err) 146 } 147 c3 := cs.managedMustAcquire(t, id3) 148 cs.Delete(c3) 149 }, 150 } 151 wg = sync.WaitGroup{} 152 for _, fn := range funcs { 153 wg.Add(1) 154 go func(fn func()) { 155 defer wg.Done() 156 for i := 0; i < 100; i++ { 157 time.Sleep(time.Duration(fastrand.Intn(100))) 158 fn() 159 } 160 }(fn) 161 } 162 wg.Wait() 163 } 164 165 // TestCompatV146SplitContracts tests the compat code for converting single file 166 // contracts into split contracts. 167 func TestCompatV146SplitContracts(t *testing.T) { 168 if testing.Short() { 169 t.SkipNow() 170 } 171 t.Parallel() 172 // get the dir of the contractset. 173 testDir := build.TempDir(t.Name()) 174 if err := os.MkdirAll(testDir, skymodules.DefaultDirPerm); err != nil { 175 t.Fatal(err) 176 } 177 // manually create a legacy contract. 178 var id types.FileContractID 179 fastrand.Read(id[:]) 180 contractHeader := contractHeader{ 181 Transaction: types.Transaction{ 182 FileContractRevisions: []types.FileContractRevision{{ 183 NewRevisionNumber: 1, 184 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 185 ParentID: id, 186 UnlockConditions: types.UnlockConditions{ 187 PublicKeys: []types.SiaPublicKey{{}, {}}, 188 }, 189 }}, 190 }, 191 } 192 initialRoot := crypto.Hash{1} 193 // Place the legacy contract in the dir. 194 pathNoExt := filepath.Join(testDir, id.String()) 195 legacyPath := pathNoExt + v146ContractExtension 196 file, err := os.Create(legacyPath) 197 if err != nil { 198 t.Fatal(err) 199 } 200 headerBytes := encoding.Marshal(contractHeader) 201 rootsBytes := initialRoot[:] 202 _, err1 := file.WriteAt(headerBytes, 0) 203 _, err2 := file.WriteAt(rootsBytes, 4088) 204 if err := errors.Compose(err1, err2); err != nil { 205 t.Fatal(err) 206 } 207 if err := file.Close(); err != nil { 208 t.Fatal(err) 209 } 210 // load contract set 211 rl := ratelimit.NewRateLimit(0, 0, 0) 212 cs, err := NewContractSet(testDir, rl, modules.ProdDependencies) 213 if err != nil { 214 t.Fatal(err) 215 } 216 // The legacy file should be gone. 217 if _, err := os.Stat(legacyPath); !os.IsNotExist(err) { 218 t.Fatal("legacy contract still exists") 219 } 220 // The new files should exist. 221 if _, err := os.Stat(pathNoExt + contractHeaderExtension); err != nil { 222 t.Fatal(err) 223 } 224 if _, err := os.Stat(pathNoExt + contractRootsExtension); err != nil { 225 t.Fatal(err) 226 } 227 // Acquire the contract. 228 sc, ok := cs.Acquire(id) 229 if !ok { 230 t.Fatal("failed to acquire contract") 231 } 232 // Make sure the header and roots match. 233 if !bytes.Equal(encoding.Marshal(sc.header), headerBytes) { 234 t.Fatal("header doesn't match") 235 } 236 roots, err := sc.merkleRoots.merkleRoots() 237 if err != nil { 238 t.Fatal(err) 239 } 240 if !reflect.DeepEqual(roots, []crypto.Hash{initialRoot}) { 241 t.Fatal("roots don't match") 242 } 243 } 244 245 // TestContractSetApplyInsertUpdateAtStartup makes sure that a valid insert 246 // update gets applied at startup and an invalid one won't. 247 func TestContractSetApplyInsertUpdateAtStartup(t *testing.T) { 248 if testing.Short() { 249 t.SkipNow() 250 } 251 t.Parallel() 252 // Prepare a header for the test. 253 header := contractHeader{Transaction: types.Transaction{ 254 FileContractRevisions: []types.FileContractRevision{{ 255 ParentID: types.FileContractID{1}, 256 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 257 UnlockConditions: types.UnlockConditions{ 258 PublicKeys: []types.SiaPublicKey{{}, {}}, 259 }, 260 }}, 261 }} 262 initialRoots := []crypto.Hash{{}, {}, {}} 263 // Prepare a valid and one invalid update. 264 validUpdate, err := makeUpdateInsertContract(header, initialRoots) 265 if err != nil { 266 t.Fatal(err) 267 } 268 invalidUpdate, err := makeUpdateInsertContract(header, initialRoots) 269 if err != nil { 270 t.Fatal(err) 271 } 272 invalidUpdate.Name = "invalidname" 273 // create contract set and close it. 274 testDir := build.TempDir(t.Name()) 275 rl := ratelimit.NewRateLimit(0, 0, 0) 276 cs, err := NewContractSet(testDir, rl, modules.ProdDependencies) 277 if err != nil { 278 t.Fatal(err) 279 } 280 // Prepare the insertion of the invalid contract. 281 txn, err := cs.staticWal.NewTransaction([]writeaheadlog.Update{invalidUpdate}) 282 if err != nil { 283 t.Fatal(err) 284 } 285 err = <-txn.SignalSetupComplete() 286 if err != nil { 287 t.Fatal(err) 288 } 289 // Close the contract set. 290 if err := cs.Close(); err != nil { 291 t.Fatal(err) 292 } 293 // Load the set again. This should ignore the invalid update and succeed. 294 cs, err = NewContractSet(testDir, rl, &dependencyIgnoreInvalidUpdate{}) 295 if err != nil { 296 t.Fatal(err) 297 } 298 // Make sure we can't acquire the contract. 299 _, ok := cs.Acquire(header.ID()) 300 if ok { 301 t.Fatal("shouldn't be able to acquire the contract") 302 } 303 // Prepare the insertion of 2 valid contracts within a single txn. This 304 // should be ignored at startup. 305 txn, err = cs.staticWal.NewTransaction([]writeaheadlog.Update{validUpdate, validUpdate}) 306 if err != nil { 307 t.Fatal(err) 308 } 309 err = <-txn.SignalSetupComplete() 310 if err != nil { 311 t.Fatal(err) 312 } 313 // Close the contract set. 314 if err := cs.Close(); err != nil { 315 t.Fatal(err) 316 } 317 // Load the set again. This should apply the invalid update and fail at 318 // startup. 319 cs, err = NewContractSet(testDir, rl, &dependencyIgnoreInvalidUpdate{}) 320 if err != nil { 321 t.Fatal(err) 322 } 323 // Make sure we can't acquire the contract. 324 _, ok = cs.Acquire(header.ID()) 325 if ok { 326 t.Fatal("shouldn't be able to acquire the contract") 327 } 328 // Prepare the insertion of a valid contract by writing the change to the 329 // wal but not applying it. 330 txn, err = cs.staticWal.NewTransaction([]writeaheadlog.Update{validUpdate}) 331 if err != nil { 332 t.Fatal(err) 333 } 334 err = <-txn.SignalSetupComplete() 335 if err != nil { 336 t.Fatal(err) 337 } 338 // Close the contract set. 339 if err := cs.Close(); err != nil { 340 t.Fatal(err) 341 } 342 // Load the set again. This should apply the valid update and not return an 343 // error. 344 cs, err = NewContractSet(testDir, rl, modules.ProdDependencies) 345 if err != nil { 346 t.Fatal(err) 347 } 348 // Make sure we can acquire the contract. 349 _, ok = cs.Acquire(header.ID()) 350 if !ok { 351 t.Fatal("failed to acquire contract after applying valid update") 352 } 353 } 354 355 // TestInsertContractTotalCost tests that InsertContrct sets a good estimate for 356 // TotalCost and TxnFee on recovered contracts. 357 func TestInsertContractTotalCost(t *testing.T) { 358 if testing.Short() { 359 t.SkipNow() 360 } 361 t.Parallel() 362 363 renterPayout := types.SiacoinPrecision 364 txnFee := types.SiacoinPrecision.Mul64(2) 365 fc := types.FileContract{ 366 ValidProofOutputs: []types.SiacoinOutput{ 367 {}, {}, 368 }, 369 MissedProofOutputs: []types.SiacoinOutput{ 370 {}, {}, 371 }, 372 } 373 fc.SetValidRenterPayout(renterPayout) 374 375 txn := types.Transaction{ 376 FileContractRevisions: []types.FileContractRevision{ 377 { 378 NewValidProofOutputs: fc.ValidProofOutputs, 379 NewMissedProofOutputs: fc.MissedProofOutputs, 380 UnlockConditions: types.UnlockConditions{ 381 PublicKeys: []types.SiaPublicKey{ 382 {}, {}, 383 }, 384 }, 385 }, 386 }, 387 } 388 389 rc := skymodules.RecoverableContract{ 390 FileContract: fc, 391 TxnFee: txnFee, 392 } 393 394 // get the dir of the contractset. 395 testDir := build.TempDir(t.Name()) 396 if err := os.MkdirAll(testDir, skymodules.DefaultDirPerm); err != nil { 397 t.Fatal(err) 398 } 399 rl := ratelimit.NewRateLimit(0, 0, 0) 400 cs, err := NewContractSet(testDir, rl, modules.ProdDependencies) 401 if err != nil { 402 t.Fatal(err) 403 } 404 405 // Insert the contract and check its total cost and fee. 406 contract, err := cs.InsertContract(rc, txn, []crypto.Hash{}, crypto.SecretKey{}) 407 if err != nil { 408 t.Fatal(err) 409 } 410 if !contract.TxnFee.Equals(txnFee) { 411 t.Fatal("wrong fee", contract.TxnFee, txnFee) 412 } 413 expectedTotalCost := renterPayout.Add(txnFee) 414 if !contract.TotalCost.Equals(expectedTotalCost) { 415 t.Fatal("wrong TotalCost", contract.TotalCost, expectedTotalCost) 416 } 417 } 418 419 // TestContractDelete tests the contractsets Delete method and makes sure that 420 // not only the contract files are gone but also the unapplied txns in the wal. 421 func TestContractDelete(t *testing.T) { 422 if testing.Short() { 423 t.SkipNow() 424 } 425 t.Parallel() 426 427 // create contract set 428 testDir := build.TempDir(t.Name()) 429 rl := ratelimit.NewRateLimit(0, 0, 0) 430 cs, err := NewContractSet(testDir, rl, modules.ProdDependencies) 431 if err != nil { 432 t.Fatal(err) 433 } 434 435 header := contractHeader{Transaction: types.Transaction{ 436 FileContractRevisions: []types.FileContractRevision{{ 437 ParentID: types.FileContractID{1}, 438 NewValidProofOutputs: []types.SiacoinOutput{{}, {}}, 439 UnlockConditions: types.UnlockConditions{ 440 PublicKeys: []types.SiaPublicKey{{}, {}}, 441 }, 442 }}, 443 }} 444 id := header.ID() 445 446 _, err = cs.managedInsertContract(header, []crypto.Hash{}) 447 if err != nil { 448 t.Fatal(err) 449 } 450 451 sc, ok := cs.Acquire(id) 452 if !ok { 453 t.Fatal("failed to acquire") 454 } 455 456 // There should be 4 files. The wal, the rc file the contract header and 457 // body. 458 fis, err := ioutil.ReadDir(testDir) 459 if err != nil { 460 t.Fatal(err) 461 } 462 if len(fis) != 4 { 463 t.Fatal("wrong number of files", len(fis)) 464 } 465 466 // Add a wal txn to the contract. 467 insertUpdate := sc.makeUpdateSetHeader(header) 468 txn, err := cs.staticWal.NewTransaction([]writeaheadlog.Update{insertUpdate}) 469 if err != nil { 470 t.Fatal(err) 471 } 472 <-txn.SignalSetupComplete() 473 474 sc.unappliedTxns = append(sc.unappliedTxns, newUnappliedWalTxn(txn)) 475 cs.Return(sc) 476 477 // Load the contractset again. 478 cs2, err := NewContractSet(testDir, rl, modules.ProdDependencies) 479 if err != nil { 480 t.Fatal(err) 481 } 482 483 // Should be able to acquire the contract. 484 sc, ok = cs2.Acquire(id) 485 if !ok { 486 t.Fatal("failed to acquire") 487 } 488 489 // Should have one txn. 490 if len(sc.unappliedTxns) != 1 { 491 t.Fatal("wrong number of txns", len(sc.unappliedTxns)) 492 } 493 494 // Delete the contract this time. 495 cs.Delete(sc) 496 497 // Load the contractset again. 498 cs3, err := NewContractSet(testDir, rl, modules.ProdDependencies) 499 if err != nil { 500 t.Fatal(err) 501 } 502 503 // Shouldn't be able to acquire the contract. 504 sc, ok = cs3.Acquire(id) 505 if ok { 506 t.Fatal("shouldn't be able to do this") 507 } 508 509 // Load the wal. Shouldn't return any contracts. 510 txns, _, err := writeaheadlog.New(filepath.Join(testDir, "contractset.wal")) 511 if err != nil { 512 t.Fatal(err) 513 } 514 if len(txns) != 0 { 515 t.Fatal("wal not empty") 516 } 517 518 // There should be 2 files. The wal and the rc file. 519 fis, err = ioutil.ReadDir(testDir) 520 if err != nil { 521 t.Fatal(err) 522 } 523 if len(fis) != 2 { 524 t.Fatal("wrong number of files", len(fis)) 525 } 526 }