gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/workerjobrenewcontract_test.go (about)

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