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

     1  package contractor
     2  
     3  import (
     4  	"io/ioutil"
     5  	"math"
     6  	"reflect"
     7  	"testing"
     8  
     9  	"gitlab.com/NebulousLabs/fastrand"
    10  	"gitlab.com/SkynetLabs/skyd/skymodules"
    11  	"go.sia.tech/siad/crypto"
    12  	"go.sia.tech/siad/modules"
    13  	"go.sia.tech/siad/persist"
    14  	"go.sia.tech/siad/types"
    15  )
    16  
    17  // TestCheckFormContractGouging checks that the upload price gouging checker is
    18  // correctly detecting price gouging from a host.
    19  //
    20  // Test looks a bit funny because it was adapated from the other price gouging
    21  // tests.
    22  func TestCheckFormContractGouging(t *testing.T) {
    23  	oneCurrency := types.NewCurrency64(1)
    24  
    25  	// minAllowance contains only the fields necessary to test the price gouging
    26  	// function. The min allowance doesn't include any of the max prices,
    27  	// because the function will ignore them if they are not set.
    28  	minAllowance := skymodules.Allowance{}
    29  	// minHostSettings contains only the fields necessary to test the price
    30  	// gouging function.
    31  	//
    32  	// The cost is set to be exactly equal to the price gouging limit, such that
    33  	// slightly decreasing any of the values evades the price gouging detector.
    34  	minHostSettings := modules.HostExternalSettings{
    35  		BaseRPCPrice:  types.SiacoinPrecision,
    36  		ContractPrice: types.SiacoinPrecision,
    37  	}
    38  
    39  	// Set min settings on the allowance that are just below that should be
    40  	// acceptable.
    41  	maxAllowance := minAllowance
    42  	maxAllowance.Funds = maxAllowance.Funds.Add(oneCurrency)
    43  	maxAllowance.MaxRPCPrice = types.SiacoinPrecision.Add(oneCurrency)
    44  	maxAllowance.MaxContractPrice = types.SiacoinPrecision.Add(oneCurrency)
    45  	maxAllowance.MaxDownloadBandwidthPrice = oneCurrency
    46  	maxAllowance.MaxSectorAccessPrice = oneCurrency
    47  	maxAllowance.MaxStoragePrice = oneCurrency
    48  	maxAllowance.MaxUploadBandwidthPrice = oneCurrency
    49  
    50  	// The max allowance should have no issues with price gouging.
    51  	err := checkFormContractGouging(maxAllowance, minHostSettings)
    52  	if err != nil {
    53  		t.Fatal(err)
    54  	}
    55  
    56  	// Should fail if the MaxRPCPrice is dropped.
    57  	failAllowance := maxAllowance
    58  	failAllowance.MaxRPCPrice = types.SiacoinPrecision.Sub(oneCurrency)
    59  	err = checkFormContractGouging(failAllowance, minHostSettings)
    60  	if err == nil {
    61  		t.Fatal("expecting price gouging check to fail")
    62  	}
    63  
    64  	// Should fail if the MaxContractPrice is dropped.
    65  	failAllowance = maxAllowance
    66  	failAllowance.MaxContractPrice = types.SiacoinPrecision.Sub(oneCurrency)
    67  	err = checkFormContractGouging(failAllowance, minHostSettings)
    68  	if err == nil {
    69  		t.Fatal("expecting price gouging check to fail")
    70  	}
    71  }
    72  
    73  // TestInitialContractFunding is a unit test for
    74  // initialContractFunding.
    75  func TestInitialContractFunding(t *testing.T) {
    76  	// Declare inputs.
    77  	tests := []struct {
    78  		pcif          uint64
    79  		contractPrice uint64
    80  		txnFee        uint64
    81  		min           uint64
    82  		max           uint64
    83  		result        uint64
    84  	}{
    85  		{
    86  			// Portal mode overrules everything.
    87  			pcif:          42,
    88  			contractPrice: fastrand.Uint64n(1000),
    89  			txnFee:        fastrand.Uint64n(1000),
    90  			min:           fastrand.Uint64n(1000),
    91  			max:           fastrand.Uint64n(1000),
    92  			result:        42,
    93  		},
    94  		{
    95  			// Regular case without hitting min or max.
    96  			pcif:          0,
    97  			contractPrice: 100,
    98  			txnFee:        200,
    99  			min:           0,
   100  			max:           math.MaxUint64,
   101  			result:        3000,
   102  		},
   103  		{
   104  			// Hit max.
   105  			pcif:          0,
   106  			contractPrice: 100,
   107  			txnFee:        200,
   108  			min:           0,
   109  			max:           1,
   110  			result:        1,
   111  		},
   112  		{
   113  			// No max.
   114  			pcif:          0,
   115  			contractPrice: 100,
   116  			txnFee:        200,
   117  			min:           1,
   118  			max:           0,
   119  			result:        3000,
   120  		},
   121  		{
   122  			// Hit min.
   123  			pcif:          0,
   124  			contractPrice: 100,
   125  			txnFee:        200,
   126  			min:           math.MaxUint64,
   127  			max:           math.MaxUint64,
   128  			result:        math.MaxUint64,
   129  		},
   130  	}
   131  
   132  	// Run tests
   133  	for i, test := range tests {
   134  		a := skymodules.Allowance{
   135  			PaymentContractInitialFunding: types.NewCurrency64(test.pcif),
   136  		}
   137  		host := skymodules.HostDBEntry{
   138  			HostExternalSettings: modules.HostExternalSettings{
   139  				ContractPrice: types.NewCurrency64(test.contractPrice),
   140  			},
   141  		}
   142  
   143  		result := initialContractFunding(a, host, types.NewCurrency64(test.txnFee), types.NewCurrency64(test.min), types.NewCurrency64(test.max))
   144  		if !result.Equals(types.NewCurrency64(test.result)) {
   145  			t.Fatalf("%v: %v != %v", i, result, test.result)
   146  		}
   147  	}
   148  }
   149  
   150  // TestHostsForPortalFormation is a unit test for hostsForPortalFormation.
   151  func TestHostsForPortalFormation(t *testing.T) {
   152  	a := skymodules.Allowance{
   153  		MaxRPCPrice:                   types.SiacoinPrecision,
   154  		PaymentContractInitialFunding: types.SiacoinPrecision,
   155  	}
   156  
   157  	// helpers
   158  	randomID := func() types.FileContractID {
   159  		var id types.FileContractID
   160  		fastrand.Read(id[:])
   161  		return id
   162  	}
   163  	randomPK := func() types.SiaPublicKey {
   164  		var spk types.SiaPublicKey
   165  		spk.Key = fastrand.Bytes(crypto.PublicKeySize)
   166  		return spk
   167  	}
   168  
   169  	// declare 5 known hosts.
   170  	// 0 will be valid
   171  	// 1 will be skipped due to an existing contract.
   172  	// 2 will be skipped due a dead score.
   173  	// 3 will be skipped due to a recoverable contract.
   174  	// 4 will be skipped due to gouging.
   175  	var activeHosts []skymodules.HostDBEntry
   176  	for i := 0; i < 5; i++ {
   177  		activeHosts = append(activeHosts, skymodules.HostDBEntry{
   178  			PublicKey: randomPK(),
   179  		})
   180  	}
   181  	// The existing contract contain a contract with host 1.
   182  	allContracts := []skymodules.RenterContract{
   183  		{
   184  			ID:            randomID(),
   185  			HostPublicKey: activeHosts[1].PublicKey,
   186  		},
   187  	}
   188  	// Host 2 gets a dead score.
   189  	scoreBreakdown := func(host skymodules.HostDBEntry) (skymodules.HostScoreBreakdown, error) {
   190  		var sb skymodules.HostScoreBreakdown
   191  		if host.PublicKey.Equals(activeHosts[2].PublicKey) {
   192  			sb.Score = types.NewCurrency64(1)
   193  		} else {
   194  			sb.Score = types.NewCurrency64(2)
   195  		}
   196  		return sb, nil
   197  	}
   198  	// The recoverable contracts contain a contract with host 3.
   199  	recoverableContracts := []skymodules.RecoverableContract{
   200  		{
   201  			ID:            randomID(),
   202  			HostPublicKey: activeHosts[3].PublicKey,
   203  		},
   204  	}
   205  	l, err := persist.NewLogger(ioutil.Discard)
   206  	if err != nil {
   207  		t.Fatal(err)
   208  	}
   209  	// Host 4 got a ridiculous base price.
   210  	activeHosts[4].BaseRPCPrice = types.SiacoinPrecision.Mul64(math.MaxUint64)
   211  
   212  	// 1 host should be returned and 4 should be skipped.
   213  	needed, hosts := hostsForPortalFormation(a, allContracts, recoverableContracts, activeHosts, l, scoreBreakdown)
   214  	if len(hosts) != 1 {
   215  		t.Fatal("wrong number of hosts", len(hosts))
   216  	}
   217  	// needed is simply set to len(hosts) for portals.
   218  	if needed != len(hosts) {
   219  		t.Fatal("needed not set")
   220  	}
   221  }
   222  
   223  // TestHostsForRegularFormation is a unit test for hostsForRegularFormation.
   224  func TestHostsForRegularFormation(t *testing.T) {
   225  	a := skymodules.Allowance{
   226  		Hosts: 5,
   227  	}
   228  
   229  	// helpers
   230  	randomID := func() types.FileContractID {
   231  		var id types.FileContractID
   232  		fastrand.Read(id[:])
   233  		return id
   234  	}
   235  	randomPK := func() types.SiaPublicKey {
   236  		var spk types.SiaPublicKey
   237  		spk.Key = fastrand.Bytes(crypto.PublicKeySize)
   238  		return spk
   239  	}
   240  
   241  	// Create one active contract and one inactive contract for each reason.
   242  	allContracts := []skymodules.RenterContract{
   243  		// Active
   244  		{
   245  			ID:            randomID(),
   246  			HostPublicKey: randomPK(),
   247  		},
   248  		// Locked
   249  		{
   250  			ID:            randomID(),
   251  			HostPublicKey: randomPK(),
   252  			Utility: skymodules.ContractUtility{
   253  				Locked: true,
   254  			},
   255  		},
   256  		// Not good for renew.
   257  		{
   258  			ID:            randomID(),
   259  			HostPublicKey: randomPK(),
   260  			Utility: skymodules.ContractUtility{
   261  				GoodForRenew: true,
   262  			},
   263  		},
   264  		// Not good for upload.
   265  		{
   266  			ID:            randomID(),
   267  			HostPublicKey: randomPK(),
   268  			Utility: skymodules.ContractUtility{
   269  				GoodForUpload: true,
   270  			},
   271  		},
   272  	}
   273  	// Create one recoverable contracts.
   274  	recoverableContracts := []skymodules.RecoverableContract{
   275  		{
   276  			ID:            randomID(),
   277  			HostPublicKey: randomPK(),
   278  		},
   279  	}
   280  	l, err := persist.NewLogger(ioutil.Discard)
   281  	if err != nil {
   282  		t.Fatal(err)
   283  	}
   284  
   285  	// Make sure random hosts is called with the right args.
   286  	var returnedHosts []skymodules.HostDBEntry
   287  	randomHosts := func(n int, blacklist, addressBlacklist []types.SiaPublicKey) ([]skymodules.HostDBEntry, error) {
   288  		// Check the expected n. -1 comes from the one host we have in
   289  		// allContracts that's gfu.
   290  		expectedN := (a.Hosts-1)*4 + uint64(randomHostsBufferForScore)
   291  		if uint64(n) != expectedN {
   292  			t.Fatal("random host called with wrong n", n, expectedN)
   293  		}
   294  		// Compute expected blacklist.
   295  		var expectedBlacklist []types.SiaPublicKey
   296  		for _, c := range allContracts {
   297  			expectedBlacklist = append(expectedBlacklist, c.HostPublicKey)
   298  		}
   299  		expectedBlacklist = append(expectedBlacklist, recoverableContracts[0].HostPublicKey)
   300  
   301  		// Compute expected address blacklist.
   302  		var expectedAddressBlacklist []types.SiaPublicKey
   303  		for _, c := range allContracts {
   304  			u := c.Utility
   305  			if !u.Locked || u.GoodForRenew || u.GoodForUpload {
   306  				expectedAddressBlacklist = append(expectedAddressBlacklist, c.HostPublicKey)
   307  			}
   308  		}
   309  
   310  		// Compare them.
   311  		if !reflect.DeepEqual(blacklist, expectedBlacklist) {
   312  			t.Log(blacklist)
   313  			t.Log(expectedBlacklist)
   314  			t.Fatal("wrong blacklist")
   315  		}
   316  		if !reflect.DeepEqual(addressBlacklist, expectedAddressBlacklist) {
   317  			t.Log(addressBlacklist)
   318  			t.Log(expectedAddressBlacklist)
   319  			t.Fatal("wrong address blacklist")
   320  		}
   321  
   322  		// Return some random hosts and remember them for later.
   323  		var hosts []skymodules.HostDBEntry
   324  		for i := 0; i < n; i++ {
   325  			hosts = append(hosts, skymodules.HostDBEntry{
   326  				PublicKey: randomPK(),
   327  			})
   328  		}
   329  		returnedHosts = hosts
   330  		return hosts, nil
   331  	}
   332  
   333  	// Check returned hosts and needed hosts.
   334  	needed, hosts := hostsForRegularFormation(a, allContracts, recoverableContracts, randomHosts, l)
   335  	if !reflect.DeepEqual(hosts, returnedHosts) {
   336  		t.Fatal("wrong hosts returned")
   337  	}
   338  	if needed != int(a.Hosts)-1 {
   339  		t.Fatal("needed not set")
   340  	}
   341  }
   342  
   343  // TestIsLateRenewal is a small unit test that verifies the functionality of the
   344  // 'isLateRenewal' helper
   345  func TestIsLateRenewal(t *testing.T) {
   346  	t.Parallel()
   347  
   348  	// a renewal is late if the renew window has passed for a certain amount,
   349  	// the amount is decided by the 'renewWindowLeewayDivisor' which is set to
   350  	// 4, meaning a renewal is late if one fourth of the renew window has passed
   351  
   352  	// start at blockheight 0 and use a default allowance and contract period
   353  	var blockHeight types.BlockHeight
   354  	allowance := skymodules.DefaultAllowance
   355  	contract := skymodules.RenterContract{
   356  		EndHeight: blockHeight + allowance.Period,
   357  	}
   358  
   359  	// base case: renewal is not late
   360  	if isLateRenewal(allowance, contract, blockHeight) {
   361  		t.Fatal("unexpected")
   362  	}
   363  
   364  	// at start of renew window it's not late
   365  	blockHeight += contract.EndHeight - allowance.RenewWindow
   366  	if isLateRenewal(allowance, contract, blockHeight) {
   367  		t.Fatal("unexpected")
   368  	}
   369  
   370  	// just before it's late
   371  	blockHeight += allowance.RenewWindow / renewWindowLeewayDivisor
   372  	if isLateRenewal(allowance, contract, blockHeight) {
   373  		t.Fatal("unexpected")
   374  	}
   375  
   376  	// just after it's late
   377  	blockHeight += 1
   378  	if !isLateRenewal(allowance, contract, blockHeight) {
   379  		t.Fatal("unexpected")
   380  	}
   381  }