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  }