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  }