gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/contractor/watchdog_test.go (about) 1 package contractor 2 3 import ( 4 "math" 5 "sync" 6 "testing" 7 "time" 8 9 "gitlab.com/NebulousLabs/errors" 10 "gitlab.com/NebulousLabs/fastrand" 11 12 "gitlab.com/SkynetLabs/skyd/build" 13 "gitlab.com/SkynetLabs/skyd/siatest/dependencies" 14 "gitlab.com/SkynetLabs/skyd/skymodules" 15 "go.sia.tech/siad/crypto" 16 "go.sia.tech/siad/modules" 17 "go.sia.tech/siad/types" 18 ) 19 20 type tpoolGate struct { 21 mu sync.Mutex 22 gateClosed bool 23 logTxns bool 24 txnSets [][]types.Transaction 25 } 26 27 // gatedTpool is a transaction pool that can be toggled off silently. 28 // This is used to simulate failures in transaction propagation. Closing the 29 // gate also allows for simpler testing in which formation transaction sets are 30 // clearly invalid, but the watchdog won't be notified by the transaction pool 31 // if we so choose. 32 type gatedTpool struct { 33 *tpoolGate 34 transactionPool 35 } 36 37 func (gp gatedTpool) AcceptTransactionSet(txnSet []types.Transaction) error { 38 gp.mu.Lock() 39 gateClosed := gp.gateClosed 40 logTxns := gp.logTxns 41 if logTxns { 42 gp.txnSets = append(gp.txnSets, txnSet) 43 } 44 gp.mu.Unlock() 45 46 if gateClosed { 47 return nil 48 } 49 return gp.transactionPool.AcceptTransactionSet(txnSet) 50 } 51 52 func createFakeRevisionTxn(fcID types.FileContractID, revNum uint64, windowStart, windowEnd types.BlockHeight) types.Transaction { 53 rev := types.FileContractRevision{ 54 ParentID: fcID, 55 NewRevisionNumber: revNum, 56 NewWindowStart: windowStart, 57 NewWindowEnd: windowEnd, 58 } 59 60 return types.Transaction{ 61 FileContractRevisions: []types.FileContractRevision{rev}, 62 } 63 } 64 65 // Creates transaction tree with many root transactions, a "subroot" transaction 66 // that spends all the root transaction outputs, and a chain of transactions 67 // spending the subroot transaction's output. 68 // 69 // Visualized: (each '+' is a transaction) 70 // 71 // + + + + + + 72 // | | | | | | 73 // | | | ... | | | 74 // | | | | | | 75 // ----------------------+---------------------- 76 // 77 // | 78 // + 79 // | 80 // . 81 // . 82 // . 83 // | 84 // + 85 func createTestTransactionTree(numRoots int, chainLength int) (txnSet []types.Transaction, roots []types.Transaction, subRootTx types.Transaction, fcTxn types.Transaction, rootParentOutputs map[types.SiacoinOutputID]bool) { 86 roots = make([]types.Transaction, 0, numRoots) 87 rootParents := make(map[types.SiacoinOutputID]bool) 88 89 // All the root txs create outputs spent in subRootTx. 90 subRootTx = types.Transaction{ 91 SiacoinInputs: make([]types.SiacoinInput, 0, numRoots), 92 SiacoinOutputs: []types.SiacoinOutput{ 93 {UnlockHash: types.UnlockHash(crypto.HashObject(fastrand.Bytes(32)))}, 94 }, 95 } 96 for i := 0; i < numRoots; i++ { 97 nextRootTx := types.Transaction{ 98 SiacoinInputs: []types.SiacoinInput{ 99 { 100 ParentID: types.SiacoinOutputID(crypto.HashObject(fastrand.Bytes(32))), 101 }, 102 }, 103 SiacoinOutputs: []types.SiacoinOutput{ 104 {UnlockHash: types.UnlockHash(crypto.HashObject(fastrand.Bytes(32)))}, 105 }, 106 } 107 rootParents[nextRootTx.SiacoinInputs[0].ParentID] = true 108 109 subRootTx.SiacoinInputs = append(subRootTx.SiacoinInputs, types.SiacoinInput{ 110 ParentID: nextRootTx.SiacoinOutputID(0), 111 }) 112 roots = append(roots, nextRootTx) 113 } 114 txnSet = append([]types.Transaction{subRootTx}, roots...) 115 116 subRootTx = txnSet[0] 117 subRootSet := getParentOutputIDs(txnSet) 118 // Sanity check on the subRootTx. 119 for _, oid := range subRootSet { 120 if !rootParents[oid] { 121 panic("non root in parent set") 122 } 123 } 124 125 // Now create a straight chain of transactions below subRootTx. 126 for i := 0; i < chainLength; i++ { 127 txnSet = addChildren(txnSet, 1, true) 128 // Only the subroottx is supposed to have more than one input. 129 for i := 0; i < len(txnSet); i++ { 130 if len(txnSet[i].SiacoinInputs) > 1 { 131 if txnSet[i].ID() != subRootTx.ID() { 132 panic("non subroottx with more than one input") 133 } 134 } 135 } 136 } 137 138 // Create a file contract transaction at the very bottom of this transaction 139 // tree. 140 fcTxn = types.Transaction{ 141 SiacoinInputs: []types.SiacoinInput{{ParentID: txnSet[0].SiacoinOutputID(0)}}, 142 FileContracts: []types.FileContract{{UnlockHash: types.UnlockHash(crypto.HashObject(fastrand.Bytes(32)))}}, 143 } 144 txnSet = append([]types.Transaction{fcTxn}, txnSet...) 145 146 // Sanity Check on the txnSet: 147 // Only the subroottx is supposed to have more than one input. 148 for i := 0; i < len(txnSet); i++ { 149 if len(txnSet[i].SiacoinInputs) > 1 { 150 if txnSet[i].ID() != subRootTx.ID() { 151 panic("non subroottx with more than one input") 152 } 153 } 154 } 155 156 return txnSet, roots, subRootTx, fcTxn, rootParents 157 } 158 159 func addChildren(txnSet []types.Transaction, numDependencies int, hasOutputs bool) []types.Transaction { 160 newTxns := make([]types.Transaction, 0, numDependencies) 161 162 // Create new parent outputs. 163 if !hasOutputs { 164 for i := 0; i < numDependencies; i++ { 165 txnSet[0].SiacoinOutputs = append(txnSet[0].SiacoinOutputs, types.SiacoinOutput{ 166 UnlockHash: types.UnlockHash(crypto.HashObject(fastrand.Bytes(32))), 167 }) 168 } 169 } 170 171 for i := 0; i < numDependencies; i++ { 172 newChild := types.Transaction{ 173 SiacoinInputs: make([]types.SiacoinInput, 1), 174 SiacoinOutputs: make([]types.SiacoinOutput, 0), 175 } 176 177 newChild.SiacoinInputs[0] = types.SiacoinInput{ 178 ParentID: txnSet[0].SiacoinOutputID(uint64(i)), 179 } 180 181 newChild.SiacoinOutputs = append(newChild.SiacoinOutputs, types.SiacoinOutput{ 182 UnlockHash: types.UnlockHash(crypto.HashObject(fastrand.Bytes(32))), 183 }) 184 185 newTxns = append([]types.Transaction{newChild}, newTxns...) 186 } 187 188 return append(newTxns, txnSet...) 189 } 190 191 // TestWatchdogRevisionCheck checks that the watchdog is monitoring the correct 192 // contract for the relevant revisions, and that it attempts to broadcast the 193 // latest revision if it hasn't observed it yet on-chain. 194 func TestWatchdogRevisionCheck(t *testing.T) { 195 if testing.Short() { 196 t.SkipNow() 197 } 198 t.Parallel() 199 // create testing trio 200 _, c, m, cf, err := newTestingTrioWithContractorDeps(t.Name(), &dependencies.DependencyLegacyRenew{}) 201 if err != nil { 202 t.Fatal(err) 203 } 204 defer tryClose(cf, t) 205 206 // form a contract with the host 207 a := skymodules.Allowance{ 208 Funds: types.SiacoinPrecision.Mul64(100), // 100 SC 209 Hosts: 1, 210 Period: 50, 211 RenewWindow: 10, 212 ExpectedStorage: skymodules.DefaultAllowance.ExpectedStorage, 213 ExpectedUpload: skymodules.DefaultAllowance.ExpectedUpload, 214 ExpectedDownload: skymodules.DefaultAllowance.ExpectedDownload, 215 ExpectedRedundancy: skymodules.DefaultAllowance.ExpectedRedundancy, 216 MaxPeriodChurn: skymodules.DefaultAllowance.MaxPeriodChurn, 217 } 218 err = c.SetAllowance(a) 219 if err != nil { 220 t.Fatal(err) 221 } 222 err = build.Retry(50, 200*time.Millisecond, func() error { 223 if len(c.Contracts()) == 0 { 224 return errors.New("contracts were not formed") 225 } 226 return nil 227 }) 228 if err != nil { 229 t.Fatal(err) 230 } 231 contract := c.Contracts()[0] 232 233 // Give the watchdog a gated transaction pool, just to log transactions it 234 // sends. 235 gatedTpool := gatedTpool{ 236 tpoolGate: &tpoolGate{gateClosed: false, 237 logTxns: true, 238 txnSets: make([][]types.Transaction, 0, 0), 239 }, 240 transactionPool: c.staticTPool, 241 } 242 c.staticWatchdog.mu.Lock() 243 c.staticWatchdog.staticTPool = gatedTpool 244 c.staticWatchdog.mu.Unlock() 245 246 // Mine a block, and check that the watchdog finds it, and is watching for the 247 // revision. 248 _, err = m.AddBlock() 249 if err != nil { 250 t.Fatal(err) 251 } 252 253 c.staticWatchdog.mu.Lock() 254 contractData, ok := c.staticWatchdog.contracts[contract.ID] 255 if !ok { 256 t.Fatal("Contract not found") 257 } 258 found := contractData.contractFound 259 revFound := contractData.revisionFound 260 revHeight := contractData.windowStart - c.staticWatchdog.renewWindow 261 c.staticWatchdog.mu.Unlock() 262 263 if !found || (revFound != 0) { 264 t.Fatal("Expected to find contract in watchdog watch-revision state") 265 } 266 267 fcr := contract.Transaction.FileContractRevisions[0] 268 if contractData.windowStart != fcr.NewWindowStart || contractData.windowEnd != fcr.NewWindowEnd { 269 t.Fatal("fileContractStatus and initial revision have differing storage proof window") 270 } 271 272 // Do several revisions on the contract. Check that watchdog is notified. 273 numRevisions := 6 // 1 no-op revision at start + 5 in this loop. 274 275 var lastRevisionTxn types.Transaction 276 // Store the intermediate revisions to post on-chain. 277 intermediateRevisions := make([]types.Transaction, 0) 278 for i := 0; i < numRevisions-1; i++ { 279 // revise the contract 280 editor, err := c.Editor(contract.HostPublicKey, nil) 281 if err != nil { 282 t.Fatal(err) 283 } 284 data := fastrand.Bytes(int(modules.SectorSize)) 285 _, err = editor.Upload(data) 286 if err != nil { 287 t.Fatal(err) 288 } 289 err = editor.Close() 290 if err != nil { 291 t.Fatal(err) 292 } 293 294 newContractState, ok := c.staticContracts.Acquire(contract.ID) 295 if !ok { 296 t.Fatal("Contract should not have been removed from set") 297 } 298 intermediateRevisions = append(intermediateRevisions, newContractState.Metadata().Transaction) 299 // save the last revision transaction 300 if i == numRevisions-2 { 301 lastRevisionTxn = newContractState.Metadata().Transaction 302 } 303 c.staticContracts.Return(newContractState) 304 } 305 306 // Mine until the height the watchdog is supposed to post the latest revision by 307 // itself. Along the way, make sure it doesn't act earlier than expected. 308 // (10 initial blocks + 1 mined in this test) 309 for i := 11; i < int(revHeight); i++ { 310 // Send out the 0th, 2nd, and 4th revisions, but not the most recent one. 311 if i == 0 || i == 2 || i == 4 { 312 gatedTpool.transactionPool.AcceptTransactionSet([]types.Transaction{intermediateRevisions[i]}) 313 } 314 315 _, err = m.AddBlock() 316 if err != nil { 317 t.Fatal(err) 318 } 319 gatedTpool.mu.Lock() 320 numTxnsPosted := len(gatedTpool.txnSets) 321 gatedTpool.mu.Unlock() 322 if numTxnsPosted != 0 { 323 t.Fatal("watchdog should not have sent any transactions yet", numTxnsPosted) 324 } 325 326 // Check the watchdog internal state to make sure it has seen some revisions 327 if i == 0 || i == 2 || i == 4 { 328 c.staticWatchdog.mu.Lock() 329 contractData, ok := c.staticWatchdog.contracts[contract.ID] 330 if !ok { 331 t.Fatal("Expected to find contract") 332 } 333 334 revNum := contractData.revisionFound 335 if revNum == 0 { 336 t.Fatal("Expected watchdog to find revision") 337 } 338 // Add 1 to i to get revNum because there is a no-op revision from 339 // formation. 340 if revNum != uint64(i+1) { 341 t.Fatal("Expected different revision number", revNum, i+1) 342 } 343 c.staticWatchdog.mu.Unlock() 344 } 345 } 346 347 // Mine one more block, and see that the watchdog has posted the revision 348 // transaction. 349 _, err = m.AddBlock() 350 if err != nil { 351 t.Fatal(err) 352 } 353 354 var revTxn types.Transaction 355 err = build.Retry(10, time.Second, func() error { 356 gatedTpool.mu.Lock() 357 defer gatedTpool.mu.Unlock() 358 359 if len(gatedTpool.txnSets) < 1 { 360 return errors.New("Expected at least one txn") 361 } 362 foundRevTxn := false 363 for _, txnSet := range gatedTpool.txnSets { 364 if (len(txnSet) != 1) || (len(txnSet[0].FileContractRevisions) != 1) { 365 continue 366 } 367 revTxn = txnSet[0] 368 foundRevTxn = true 369 break 370 } 371 372 if !foundRevTxn { 373 return errors.New("did not find transaction with revision") 374 } 375 return nil 376 }) 377 gatedTpool.mu.Lock() 378 379 if err != nil { 380 t.Fatal("watchdog should have sent exactly one transaction with a revision", err) 381 } 382 383 if revTxn.FileContractRevisions[0].ParentID != contract.ID { 384 t.Fatal("watchdog revision sent for wrong contract ID") 385 } 386 // The last revision will be cleared and refreshed so it has a special 387 // revision number. 388 if revTxn.FileContractRevisions[0].NewRevisionNumber != math.MaxUint64 { 389 t.Fatalf("watchdog sent wrong revision number %v != %v", revTxn.FileContractRevisions[0].NewRevisionNumber, uint64(math.MaxUint64)) 390 } 391 gatedTpool.mu.Unlock() 392 393 // Check that the watchdog finds its own revision. 394 for i := 0; i < 5; i++ { 395 _, err = m.AddBlock() 396 if err != nil { 397 t.Fatal(err) 398 } 399 } 400 c.staticWatchdog.mu.Lock() 401 contractData, ok = c.staticWatchdog.contracts[contract.ID] 402 if !ok { 403 t.Fatal("Expected watchdog to have found a revision") 404 } 405 revFound = contractData.revisionFound 406 c.staticWatchdog.mu.Unlock() 407 408 if revFound == 0 { 409 t.Fatal("expected watchdog to have found a revision") 410 } 411 412 // LastRevisionTxn is the max revision number. 413 lastRevisionNumber := uint64(math.MaxUint64) 414 if revFound != lastRevisionNumber { 415 t.Fatalf("Expected watchdog to have found the most recent revision, that it posted %v != %v", revFound, lastRevisionNumber) 416 } 417 418 // Create a fake reorg and remove the file contract revision. 419 c.staticWatchdog.mu.Lock() 420 revertedBlock := types.Block{ 421 Transactions: []types.Transaction{lastRevisionTxn}, 422 } 423 c.staticWatchdog.mu.Unlock() 424 425 revertedCC := modules.ConsensusChange{ 426 RevertedBlocks: []types.Block{revertedBlock}, 427 } 428 c.staticWatchdog.callScanConsensusChange(revertedCC) 429 430 c.staticWatchdog.mu.Lock() 431 contractData, ok = c.staticWatchdog.contracts[contract.ID] 432 if !ok { 433 t.Fatal("Expected watchdog to have found a revision") 434 } 435 revFound = contractData.revisionFound 436 c.staticWatchdog.mu.Unlock() 437 438 if revFound != 0 { 439 t.Fatal("Expected to find contract in watchdog watching state, not found", ok, revFound) 440 } 441 } 442 443 // TestWatchdogStorageProofCheck tests that the watchdog correctly notifies the 444 // contractor when a storage proof is not found in time. Currently the 445 // watchdog doesn't take any actions, so this test must be updated when that 446 // functionality is implemented 447 func TestWatchdogStorageProofCheck(t *testing.T) { 448 t.SkipNow() 449 450 if testing.Short() { 451 t.SkipNow() 452 } 453 t.Parallel() 454 // create testing trio 455 _, c, m, cf, err := newTestingTrio(t.Name()) 456 if err != nil { 457 t.Fatal(err) 458 } 459 defer tryClose(cf, t) 460 461 // form a contract with the host 462 a := skymodules.Allowance{ 463 Funds: types.SiacoinPrecision.Mul64(100), // 100 SC 464 Hosts: 1, 465 Period: 30, 466 RenewWindow: 10, 467 ExpectedStorage: skymodules.DefaultAllowance.ExpectedStorage, 468 ExpectedUpload: skymodules.DefaultAllowance.ExpectedUpload, 469 ExpectedDownload: skymodules.DefaultAllowance.ExpectedDownload, 470 ExpectedRedundancy: skymodules.DefaultAllowance.ExpectedRedundancy, 471 MaxPeriodChurn: skymodules.DefaultAllowance.MaxPeriodChurn, 472 } 473 err = c.SetAllowance(a) 474 if err != nil { 475 t.Fatal(err) 476 } 477 err = build.Retry(50, 100*time.Millisecond, func() error { 478 if len(c.Contracts()) == 0 { 479 return errors.New("contracts were not formed") 480 } 481 return nil 482 }) 483 if err != nil { 484 t.Fatal(err) 485 } 486 contract := c.Contracts()[0] 487 488 // Give the watchdog a gated transaction pool. 489 gatedTpool := gatedTpool{ 490 tpoolGate: &tpoolGate{gateClosed: true, 491 logTxns: true, 492 txnSets: make([][]types.Transaction, 0, 0), 493 }, 494 transactionPool: c.staticTPool, 495 } 496 c.staticWatchdog.mu.Lock() 497 c.staticWatchdog.staticTPool = gatedTpool 498 c.staticWatchdog.mu.Unlock() 499 500 // Mine a block, and check that the watchdog finds it, and is watching for the 501 // revision. 502 _, err = m.AddBlock() 503 if err != nil { 504 t.Fatal(err) 505 } 506 507 c.staticWatchdog.mu.Lock() 508 contractData, ok := c.staticWatchdog.contracts[contract.ID] 509 if !ok { 510 t.Fatal("Expected watchdog to have found a revision") 511 } 512 found := contractData.contractFound 513 revFound := contractData.revisionFound 514 proofFound := contractData.storageProofFound != 0 515 storageProofHeight := contractData.windowEnd 516 c.staticWatchdog.mu.Unlock() 517 518 if !found || revFound != 0 || proofFound { 519 t.Fatal("Expected to find contract in watchdog watch-revision state") 520 } 521 fcr := contract.Transaction.FileContractRevisions[0] 522 if contractData.windowStart != fcr.NewWindowStart || contractData.windowEnd != fcr.NewWindowEnd { 523 t.Fatal("fileContractStatus and initial revision have differing storage proof window") 524 } 525 526 for i := 11; i < int(storageProofHeight)-1; i++ { 527 _, err = m.AddBlock() 528 if err != nil { 529 t.Fatal(err) 530 } 531 } 532 533 c.staticWatchdog.mu.Lock() 534 contractData, ok = c.staticWatchdog.contracts[contract.ID] 535 if !ok { 536 t.Fatal("Expected watchdog to have found a revision") 537 } 538 found = contractData.contractFound 539 revFound = contractData.revisionFound 540 proofFound = contractData.storageProofFound != 0 541 c.staticWatchdog.mu.Unlock() 542 543 // The watchdog shuold have posted a revision, and should be watching for the 544 // proof still. 545 if !found || (revFound == 0) || proofFound { 546 t.Fatal("Watchdog should be only watching for a storage proof now") 547 } 548 549 // Mine one more block, and see that the watchdog has posted the revision 550 // transaction. 551 _, err = m.AddBlock() 552 if err != nil { 553 t.Fatal(err) 554 } 555 556 err = build.Retry(50, 100*time.Millisecond, func() error { 557 contractStatus, ok := c.ContractStatus(contract.ID) 558 if !ok { 559 return errors.New("no contract status") 560 } 561 if contractStatus.StorageProofFoundAtHeight != 0 { 562 return errors.New("storage proof was found") 563 } 564 565 return nil 566 }) 567 if err != nil { 568 t.Fatal(err) 569 } 570 } 571 572 // Test getParentOutputIDs 573 func TestWatchdogGetParents(t *testing.T) { 574 // Create a txn set that is a long chain of transactions. 575 rootTx := types.Transaction{ 576 SiacoinInputs: []types.SiacoinInput{ 577 { 578 ParentID: types.SiacoinOutputID(crypto.HashObject(fastrand.Bytes(32))), 579 }, 580 }, 581 SiacoinOutputs: []types.SiacoinOutput{ 582 { 583 UnlockHash: types.UnlockHash(crypto.HashObject(fastrand.Bytes(32))), 584 }, 585 }, 586 } 587 txnSet := []types.Transaction{rootTx} 588 for i := 0; i < 10; i++ { 589 txnSet = addChildren(txnSet, 1, true) 590 } 591 // Verify that this is a complete chain. 592 for i := 0; i < 10-1; i++ { 593 parentID := txnSet[i].SiacoinInputs[0].ParentID 594 expectedID := txnSet[i+1].SiacoinOutputID(0) 595 if parentID != expectedID { 596 t.Fatal("Invalid txn chain", i, parentID, expectedID) 597 } 598 } 599 // Check that there is only one parent output, the parent of rootTx. 600 parentOids := getParentOutputIDs(txnSet) 601 if len(parentOids) != 1 { 602 t.Fatal("expected exactly one parent output", len(parentOids)) 603 } 604 if parentOids[0] != rootTx.SiacoinInputs[0].ParentID { 605 t.Fatal("Wrong parent output id found") 606 } 607 608 rootTx2 := types.Transaction{ 609 SiacoinInputs: []types.SiacoinInput{ 610 { 611 ParentID: types.SiacoinOutputID(crypto.HashObject(fastrand.Bytes(32))), 612 }, 613 { 614 ParentID: types.SiacoinOutputID(crypto.HashObject(fastrand.Bytes(32))), 615 }, 616 }, 617 } 618 txnSet2 := []types.Transaction{rootTx2} 619 // Make a transaction tree. 620 for i := 0; i < 10; i++ { 621 txnSet2 = addChildren(txnSet2, i*2, false) 622 } 623 parentOids2 := getParentOutputIDs(txnSet2) 624 if len(parentOids2) != 2 { 625 t.Fatal("expected exactly one parent output", len(parentOids2)) 626 } 627 for i := 0; i < len(rootTx2.SiacoinInputs); i++ { 628 foundParent := false 629 for j := 0; j < len(parentOids2); j++ { 630 if parentOids2[i] == rootTx2.SiacoinInputs[j].ParentID { 631 foundParent = true 632 } 633 } 634 if !foundParent { 635 t.Fatal("didn't find parent output") 636 } 637 } 638 639 // Create a txn set with A LOT of root transactions/outputs. 640 numBigRoots := 100 641 bigRoots := make([]types.Transaction, 0, numBigRoots) 642 bigRootIDs := make(map[types.SiacoinOutputID]bool) 643 644 // All the root txs create outputs spent in subRootTx. 645 subRootTx := types.Transaction{ 646 SiacoinInputs: make([]types.SiacoinInput, numBigRoots), 647 } 648 649 for i := 0; i < numBigRoots; i++ { 650 nextRootTx := types.Transaction{ 651 SiacoinInputs: []types.SiacoinInput{ 652 { 653 ParentID: types.SiacoinOutputID(crypto.HashObject(fastrand.Bytes(32))), 654 }, 655 }, 656 SiacoinOutputs: []types.SiacoinOutput{ 657 { 658 UnlockHash: types.UnlockHash(crypto.HashObject(fastrand.Bytes(32))), 659 }, 660 }, 661 } 662 663 subRootTx.SiacoinInputs[i] = types.SiacoinInput{ 664 ParentID: nextRootTx.SiacoinOutputID(0), 665 } 666 bigRoots = append(bigRoots, nextRootTx) 667 bigRootIDs[nextRootTx.SiacoinInputs[0].ParentID] = true 668 } 669 670 txnSet3 := append([]types.Transaction{subRootTx}, bigRoots...) 671 parentOids3 := getParentOutputIDs(txnSet3) 672 if len(parentOids3) != numBigRoots { 673 t.Fatal("Wrong number of parent output ids", len(parentOids3)) 674 } 675 676 for _, outputID := range parentOids3 { 677 if !bigRootIDs[outputID] { 678 t.Fatal("parent ID is not a root !", outputID) 679 } 680 } 681 682 // Now create a bunch of transactions below subRootTx. This should NOT change the parentOids. 683 chainLength := 12 684 for i := 0; i < chainLength; i++ { 685 txnSet3 = addChildren(txnSet3, i, false) 686 } 687 parentOids4 := getParentOutputIDs(txnSet3) 688 if len(parentOids4) != numBigRoots { 689 t.Fatal("Wrong number of parent output ids", len(parentOids4)) 690 } 691 for _, outputID := range parentOids4 { 692 if !bigRootIDs[outputID] { 693 t.Fatal("parent ID is not a root !", outputID) 694 } 695 } 696 } 697 698 // TestWatchdogPruning tests watchdog pruning of formation transction sets by 699 // creating a large dependency set for the file contract transaction. 700 func TestWatchdogPruning(t *testing.T) { 701 if testing.Short() { 702 t.SkipNow() 703 } 704 t.Parallel() 705 706 // Create a txn set with A LOT of root transactions/outputs. 707 numRoots := 10 708 chainLength := 12 709 txnSet, roots, subRootTx, fcTxn, rootParents := createTestTransactionTree(numRoots, chainLength) 710 fcID := fcTxn.FileContractID(0) 711 712 // create testing trio 713 _, c, _, cf, err := newTestingTrio(t.Name()) 714 if err != nil { 715 t.Fatal(err) 716 } 717 defer tryClose(cf, t) 718 revisionTxn := createFakeRevisionTxn(fcID, 1, 10000, 10005) 719 720 // Give the watchdog a gated transaction pool. This precents it from 721 // rejecting the fake formation transaction set. 722 gatedTpool := gatedTpool{ 723 tpoolGate: &tpoolGate{gateClosed: true, 724 logTxns: true, 725 txnSets: make([][]types.Transaction, 0, 0), 726 }, 727 transactionPool: c.staticTPool, 728 } 729 c.staticWatchdog.mu.Lock() 730 c.staticWatchdog.staticTPool = gatedTpool 731 c.staticWatchdog.mu.Unlock() 732 733 // Signal the watchdog with this file contract and formation transaction. 734 monitorContractArgs := monitorContractArgs{ 735 false, 736 fcID, 737 revisionTxn, 738 txnSet, 739 txnSet[0], 740 nil, 741 5000, 742 } 743 err = c.staticWatchdog.callMonitorContract(monitorContractArgs) 744 if err != nil { 745 t.Fatal(err) 746 } 747 748 c.staticWatchdog.mu.Lock() 749 updatedTxnSet := c.staticWatchdog.contracts[fcID].formationTxnSet 750 dependencies := getParentOutputIDs(updatedTxnSet) 751 c.staticWatchdog.mu.Unlock() 752 753 originalSetLen := len(updatedTxnSet) 754 755 if len(dependencies) != numRoots { 756 t.Fatal("Expected different number of dependencies", len(dependencies), numRoots) 757 } 758 759 // "mine" the root transactions in one at a time. 760 for i := 0; i < numRoots; i++ { 761 block := types.Block{ 762 Transactions: []types.Transaction{ 763 roots[i], 764 }, 765 } 766 767 newBlockCC := modules.ConsensusChange{ 768 AppliedBlocks: []types.Block{block}, 769 } 770 c.staticWatchdog.callScanConsensusChange(newBlockCC) 771 772 // The number of dependencies should be going down 773 c.staticWatchdog.mu.Lock() 774 updatedTxnSet = c.staticWatchdog.contracts[fcID].formationTxnSet 775 dependencies = getParentOutputIDs(updatedTxnSet) 776 c.staticWatchdog.mu.Unlock() 777 778 // The number of dependencies shouldn't change, but the number of 779 // transactions in the set should be decreasing. 780 if len(dependencies) != numRoots { 781 t.Fatal("Expected num of dependencies to stay the same") 782 } 783 if len(updatedTxnSet) != originalSetLen-i-1 { 784 t.Fatal("unexpected txn set length", len(updatedTxnSet), i, originalSetLen-i) 785 } 786 } 787 // Mine the subRootTx. The number of dependencies should go down to just 1 788 // now. 789 block := types.Block{ 790 Transactions: []types.Transaction{ 791 subRootTx, 792 }, 793 } 794 newBlockCC := modules.ConsensusChange{ 795 AppliedBlocks: []types.Block{block}, 796 } 797 c.staticWatchdog.callScanConsensusChange(newBlockCC) 798 799 // The number of dependencies should be going down 800 c.staticWatchdog.mu.Lock() 801 updatedTxnSet = c.staticWatchdog.contracts[fcID].formationTxnSet 802 dependencies = getParentOutputIDs(updatedTxnSet) 803 c.staticWatchdog.mu.Unlock() 804 805 // Check that all the root transactions are gone. 806 for i := 0; i < numRoots; i++ { 807 rootTxID := roots[i].ID() 808 for j := 0; j < len(updatedTxnSet); j++ { 809 if updatedTxnSet[j].ID() == rootTxID { 810 t.Fatal("Expected root to be gone") 811 } 812 } 813 } 814 // Check that the subRootTx is gone now. 815 for _, txn := range updatedTxnSet { 816 if txn.ID() == subRootTx.ID() { 817 t.Fatal("subroot tx still present") 818 } 819 } 820 821 // The number of dependencies shouldn't change, but the number of 822 // transactions in the set should be decreasing. 823 if len(dependencies) != 1 { 824 t.Fatal("Expected num of dependencies to go down", len(dependencies)) 825 } 826 if rootParents[dependencies[0]] { 827 t.Fatal("the remaining dependency should not be a root output") 828 } 829 if len(updatedTxnSet) != originalSetLen-numRoots-1 { 830 t.Fatal("unexpected txn set length", len(updatedTxnSet), originalSetLen-numRoots) 831 } 832 833 c.mu.Lock() 834 _, doubleSpendDetected := c.doubleSpentContracts[fcID] 835 c.mu.Unlock() 836 if doubleSpendDetected { 837 t.Fatal("non-existent double spend found!") 838 } 839 } 840 841 func TestWatchdogDependencyAdding(t *testing.T) { 842 if testing.Short() { 843 t.SkipNow() 844 } 845 t.Parallel() 846 847 // Create a txn set with 10 root transactions/outputs. 848 numRoots := 10 849 chainLength := 12 850 txnSet, _, subRootTx, fcTxn, rootParents := createTestTransactionTree(numRoots, chainLength) 851 fcID := fcTxn.FileContractID(0) 852 853 // create testing trio 854 _, c, _, cf, err := newTestingTrio(t.Name()) 855 if err != nil { 856 t.Fatal(err) 857 } 858 defer tryClose(cf, t) 859 revisionTxn := createFakeRevisionTxn(fcID, 1, 10000, 10005) 860 formationSet := []types.Transaction{fcTxn} 861 862 // Signal the watchdog with this file contract and formation transaction. 863 monitorContractArgs := monitorContractArgs{ 864 false, 865 fcID, 866 revisionTxn, 867 formationSet, 868 txnSet[0], 869 nil, 870 5000, 871 } 872 err = c.staticWatchdog.callMonitorContract(monitorContractArgs) 873 if err != nil { 874 t.Fatal(err) 875 } 876 877 c.staticWatchdog.mu.Lock() 878 _, inWatchdog := c.staticWatchdog.contracts[fcID] 879 if !inWatchdog { 880 t.Fatal("Expected watchdog to be aware of contract", fcID) 881 } 882 updatedTxnSet := c.staticWatchdog.contracts[fcID].formationTxnSet 883 outputDependencies := getParentOutputIDs(updatedTxnSet) 884 c.staticWatchdog.mu.Unlock() 885 886 if len(outputDependencies) != 1 { 887 t.Fatal("Expected different number of outputDependencies", len(outputDependencies)) 888 } 889 890 // Revert the chain transactions in 1 block. 891 txnLength := len(txnSet) - numRoots - 1 892 block1 := types.Block{ 893 Transactions: txnSet[1:txnLength], 894 } 895 revertCC1 := modules.ConsensusChange{ 896 RevertedBlocks: []types.Block{block1}, 897 } 898 c.staticWatchdog.callScanConsensusChange(revertCC1) 899 900 // The number of outputDependencies should be going up 901 c.staticWatchdog.mu.Lock() 902 updatedTxnSet = c.staticWatchdog.contracts[fcID].formationTxnSet 903 outputDependencies = getParentOutputIDs(updatedTxnSet) 904 c.staticWatchdog.mu.Unlock() 905 // The number of outputDependencies shouldn't change, but the number of 906 // transactions in the set should be increasing. 907 if len(outputDependencies) != 1 { 908 t.Fatal("Expected num of outputDependencies to stay the same", len(outputDependencies)) 909 } 910 if len(updatedTxnSet) != len(block1.Transactions)+1 { 911 t.Fatal("unexpected txn set length", len(updatedTxnSet), len(block1.Transactions)+1) 912 } 913 // Revert the subRootTx. The number of outputDependencies should go up now. 914 // now. 915 block := types.Block{ 916 Transactions: []types.Transaction{ 917 subRootTx, 918 }, 919 } 920 revertCC := modules.ConsensusChange{ 921 RevertedBlocks: []types.Block{block}, 922 } 923 c.staticWatchdog.callScanConsensusChange(revertCC) 924 925 c.staticWatchdog.mu.Lock() 926 contractData, ok := c.staticWatchdog.contracts[fcID] 927 if !ok { 928 t.Fatal("Expected watchdog to have contract") 929 } 930 updatedTxnSet = contractData.formationTxnSet 931 outputDependencies = getParentOutputIDs(updatedTxnSet) 932 found := contractData.contractFound 933 c.staticWatchdog.mu.Unlock() 934 935 if found { 936 t.Fatal("contract should not have been found already") 937 } 938 939 // Sanity check: make sure the subRootTx is actually found in the set. 940 foundSubRoot := false 941 for i := 0; i < len(updatedTxnSet); i++ { 942 if updatedTxnSet[i].ID() == subRootTx.ID() { 943 foundSubRoot = true 944 break 945 } 946 } 947 if !foundSubRoot { 948 t.Fatal("Subroot transaction should now be a dependency", len(updatedTxnSet)) 949 } 950 951 // The number of outputDependencies shouldn't change, but the number of 952 // transactions in the set should be decreasing. 953 if len(outputDependencies) != numRoots { 954 t.Fatal("Expected num of outputDependencies to go down", len(outputDependencies)) 955 } 956 if rootParents[outputDependencies[0]] { 957 t.Fatal("the remaining dependency should not be a root output") 958 } 959 960 // All transactions except for the roots should be dependency transactions. 961 if len(updatedTxnSet) != len(txnSet)-numRoots { 962 t.Fatal("unexpected txn set length", len(updatedTxnSet), len(txnSet)-numRoots) 963 } 964 }