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 }