gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/workeraccount.go (about) 1 package renter 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "math/big" 8 "net" 9 "runtime" 10 "strings" 11 "sync" 12 "sync/atomic" 13 "time" 14 15 "gitlab.com/SkynetLabs/skyd/build" 16 "gitlab.com/SkynetLabs/skyd/persist" 17 "gitlab.com/SkynetLabs/skyd/skymodules" 18 "gitlab.com/SkynetLabs/skyd/skymodules/renter/contractor" 19 "go.sia.tech/siad/crypto" 20 "go.sia.tech/siad/modules" 21 "go.sia.tech/siad/modules/host" 22 "go.sia.tech/siad/types" 23 24 "gitlab.com/NebulousLabs/errors" 25 "gitlab.com/NebulousLabs/fastrand" 26 "gitlab.com/NebulousLabs/siamux" 27 ) 28 29 const ( 30 // accountBalanceDriftPercentageThreshold is the percentage threshold, in 31 // relation to the balance target, at which we consider the drift to be 32 // large enough to register an alert for it 33 accountBalanceDriftPercentageThreshold = .1 34 35 // withdrawalValidityPeriod defines the period (in blocks) a withdrawal 36 // message remains spendable after it has been created. Together with the 37 // current block height at time of creation, this period makes up the 38 // WithdrawalMessage's expiry height. 39 withdrawalValidityPeriod = 6 40 41 // fundAccountGougingPercentageThreshold is the percentage threshold, in 42 // relation to the allowance, at which we consider the cost of funding an 43 // account to be too expensive. E.g. the cost of funding the account as many 44 // times as necessary to spend the total allowance should never exceed 1% of 45 // the total allowance. 46 fundAccountGougingPercentageThreshold = .01 47 ) 48 49 const ( 50 // the following categories are constants used to determine the 51 // corresponding spending field in the account's spending details whenever 52 // we pay for an rpc request using the ephemeral account as payment method. 53 categoryErr spendingCategory = iota 54 categoryAccountBalance 55 categoryDownload 56 categoryRegistryRead 57 categoryRegistryWrite 58 categoryRepairDownload 59 categoryRepairUpload 60 categorySnapshotDownload 61 categorySnapshotUpload 62 categorySubscription 63 categoryUpdatePriceTable 64 categoryUpload 65 ) 66 67 var ( 68 // accountIdleCheckFrequency establishes how frequently the sync function 69 // should check whether the worker is idle. A relatively high frequency is 70 // okay, because this function only runs while the worker is frozen and 71 // expecting to perform an expensive sync operation. 72 accountIdleCheckFrequency = build.Select(build.Var{ 73 Dev: time.Second * 4, 74 Standard: time.Second * 5, 75 Testing: time.Second * 3, 76 }).(time.Duration) 77 78 // accountSyncRandWaitMilliseconds defines the number of random milliseconds 79 // that are added to the wait time. Randomness is used to ensure that 80 // workers are not all syncing at the same time - the sync operation freezes 81 // workers. This number should be larger than the expected amount of time a 82 // worker will be frozen multiplied by the total number of workers. 83 accountSyncRandWaitMilliseconds = build.Select(build.Var{ 84 Dev: int(1e3 * 60), // 1 minute 85 Standard: int(3 * 1e3 * 60 * 60), // 3 hours 86 Testing: int(1e3 * 15), // 15 seconds - needs to be long even in testing 87 }).(int) 88 89 // accountSyncMinWaitTime defines the minimum amount of time that a worker 90 // will wait between performing sync checks. This should be a large number, 91 // on the order of the amount of time a worker is expected to be frozen 92 // multiplied by the total number of workers. 93 accountSyncMinWaitTime = build.Select(build.Var{ 94 Dev: time.Minute, 95 Standard: 60 * time.Minute, // 1 hour 96 Testing: 10 * time.Second, // needs to be long even in testing 97 }).(time.Duration) 98 99 // accountIdleMaxWait defines the max amount of time that the worker will 100 // wait to reach an idle state before firing a build.Critical and giving up 101 // on becoming idle. Generally this will indicate that somewhere in the 102 // worker code there is a job that is not timing out correctly. 103 accountIdleMaxWait = build.Select(build.Var{ 104 Dev: 10 * time.Minute, 105 Standard: 40 * time.Minute, 106 Testing: 5 * time.Minute, // needs to be long even in testing 107 }).(time.Duration) 108 ) 109 110 type ( 111 // account represents a renter's ephemeral account on a host. 112 account struct { 113 // Information related to host communications. 114 staticID modules.AccountID 115 staticHostKey types.SiaPublicKey 116 staticSecretKey crypto.SecretKey 117 118 // Money has multiple states in an account, this is all the information 119 // we need to understand the current state of the account's balance and 120 // pending updates. 121 // 122 // The two drift fields keep track of the delta between our version of 123 // the balance and the host's version of the balance. We want to keep 124 // track of this drift as in the future we might add code that acts upon 125 // it and penalizes the host if we find they are cheating us, or 126 // behaving sub-optimally. 127 balance types.Currency 128 balanceDriftPositive types.Currency 129 balanceDriftNegative types.Currency 130 pendingDeposits types.Currency 131 pendingWithdrawals types.Currency 132 negativeBalance types.Currency 133 134 // hostBalance is a field in which we store the balance the host claims 135 // is in the account when we perform a sync. We can not reset our 136 // balance to that as we can not blindly trust the host, but we need to 137 // store it nonetheless to be able to accurately reflect the accrued 138 // positive or negative balance drift 139 // 140 // hostBalanceNegative allows the hostBalance to go negative 141 hostBalance types.Currency 142 hostBalanceNegative types.Currency 143 144 // residue is the amount of money that is still left in the 145 // ephemeral account at the time the contract renews and the 146 // FundAccountCost resets, we need to store this field in order to 147 // properly represent the spending details in the next period. 148 residue types.Currency 149 150 // Spending details contain a breakdown of how much money from the 151 // ephemeral account got spent on what type of action. Examples of such 152 // actions are downloads, registry reads, registry writes, etc. 153 spending spendingDetails 154 155 // Error tracking. 156 recentErr error 157 recentErrTime time.Time 158 recentSuccessTime time.Time 159 160 // syncAt defines what time the renter should be syncing the account to 161 // the host. 162 syncAt time.Time 163 164 // forcedSyncAt is the time at which the renter scheduled a forced sync 165 // of the renter's ea balance with the host 166 forcedSyncAt time.Time 167 168 // Variables to manage a race condition around account creation, where 169 // the account must be available in the data structure before it has 170 // been synced to disk successfully (to avoid holding a lock on the 171 // account manager during a disk fsync). Anyone trying to use the 172 // account will need to block on 'staticReady', and then after that is 173 // closed needs to check the status of 'externActive', 'false' 174 // indicating that account creation failed and the account was deleted. 175 // 176 // 'externActive' can be accessed freely once 'staticReady' has been 177 // closed. 178 staticReady chan struct{} 179 externActive bool 180 181 // Utils. The offset refers to the offset within the file that the 182 // account uses. 183 mu sync.Mutex 184 staticAlerter *modules.GenericAlerter 185 staticBalanceTarget types.Currency 186 staticFile modules.File 187 staticLog *persist.Logger 188 staticOffset int64 189 } 190 191 // spendingDetails contains a breakdown of all spending metrics, all money 192 // that is being spent from an ephemeral account is accounted for in one of 193 // these categories. Every field of this struct should have a corresponding 194 // 'spendingCategory'. 195 spendingDetails struct { 196 accountBalance types.Currency 197 downloads types.Currency 198 registryReads types.Currency 199 registryWrites types.Currency 200 repairDownloads types.Currency 201 repairUploads types.Currency 202 snapshotDownloads types.Currency 203 snapshotUploads types.Currency 204 subscriptions types.Currency 205 updatePriceTable types.Currency 206 uploads types.Currency 207 } 208 209 // spendingCategory defines an enum that represent a category in the 210 // spending details 211 spendingCategory uint64 212 ) 213 214 // sum returns the sum of all spending details 215 func (s *spendingDetails) sum() types.Currency { 216 var total types.Currency 217 total = total.Add(s.accountBalance) 218 total = total.Add(s.downloads) 219 total = total.Add(s.registryReads) 220 total = total.Add(s.registryWrites) 221 total = total.Add(s.repairDownloads) 222 total = total.Add(s.repairUploads) 223 total = total.Add(s.snapshotDownloads) 224 total = total.Add(s.snapshotUploads) 225 total = total.Add(s.subscriptions) 226 total = total.Add(s.updatePriceTable) 227 total = total.Add(s.uploads) 228 return total 229 } 230 231 // add will add the given amount to the appropriate spending category 232 func (s *spendingDetails) add(category spendingCategory, amount types.Currency) { 233 s.set(category, s.get(category).Add(amount)) 234 } 235 236 // get returns the spending details for given category 237 func (s *spendingDetails) get(category spendingCategory) types.Currency { 238 switch category { 239 case categoryAccountBalance: 240 return s.accountBalance 241 case categoryDownload: 242 return s.downloads 243 case categorySnapshotDownload: 244 return s.snapshotDownloads 245 case categorySnapshotUpload: 246 return s.snapshotUploads 247 case categoryRegistryRead: 248 return s.registryReads 249 case categoryRegistryWrite: 250 return s.registryWrites 251 case categoryRepairDownload: 252 return s.repairDownloads 253 case categoryRepairUpload: 254 return s.repairUploads 255 case categorySubscription: 256 return s.subscriptions 257 case categoryUpdatePriceTable: 258 return s.updatePriceTable 259 case categoryUpload: 260 return s.uploads 261 case categoryErr: 262 build.Critical("category is not set, developer error") 263 default: 264 build.Critical("category is not handled, developer error") 265 } 266 267 return types.ZeroCurrency 268 } 269 270 // set sets the spending details for given category and amount 271 func (s *spendingDetails) set(category spendingCategory, amount types.Currency) { 272 switch category { 273 case categoryAccountBalance: 274 s.accountBalance = amount 275 case categoryDownload: 276 s.downloads = amount 277 case categorySnapshotDownload: 278 s.snapshotDownloads = amount 279 case categorySnapshotUpload: 280 s.snapshotUploads = amount 281 case categoryRegistryRead: 282 s.registryReads = amount 283 case categoryRegistryWrite: 284 s.registryWrites = amount 285 case categoryRepairDownload: 286 s.repairDownloads = amount 287 case categoryRepairUpload: 288 s.repairUploads = amount 289 case categorySubscription: 290 s.subscriptions = amount 291 case categoryUpdatePriceTable: 292 s.updatePriceTable = amount 293 case categoryUpload: 294 s.uploads = amount 295 case categoryErr: 296 build.Critical("category is not set, developer error") 297 default: 298 build.Critical("category is not handled, developer error") 299 } 300 } 301 302 // sub will subtract the given amount from the appropriate spending category 303 func (s *spendingDetails) sub(category spendingCategory, amount types.Currency) { 304 // safeSub is a helper function that subtracts two currencies, if the result 305 // would be negative the zero currency gets returned 306 safeSub := func(x, y types.Currency) types.Currency { 307 if y.Cmp(x) > 0 { 308 return types.ZeroCurrency 309 } 310 return x.Sub(y) 311 } 312 313 s.set(category, safeSub(s.get(category), amount)) 314 } 315 316 // ProvidePayment takes a stream and various payment details and handles the 317 // payment by sending and processing payment request and response objects. 318 // Returns an error in case of failure. 319 // 320 // Note that this implementation does not 'Read' from the stream. This allows 321 // the caller to pass in a buffer if he so pleases in order to optimise the 322 // amount of writes on the actual stream. 323 func (a *account) ProvidePayment(stream io.ReadWriter, amount types.Currency, blockHeight types.BlockHeight) error { 324 // NOTE: we purposefully do not verify if the account has sufficient funds. 325 // Seeing as withdrawals are a blocking action on the host, it is perfectly 326 // ok to trigger them from an account with insufficient balance. 327 328 // create a withdrawal message 329 msg := newWithdrawalMessage(a.staticID, amount, blockHeight) 330 sig := crypto.SignHash(crypto.HashObject(msg), a.staticSecretKey) 331 332 buffer := staticPoolProvidePaymentBuffers.Get() 333 defer staticPoolProvidePaymentBuffers.Put(buffer) 334 335 // send PaymentRequest 336 err := modules.RPCWrite(buffer, modules.PaymentRequest{Type: modules.PayByEphemeralAccount}) 337 if err != nil { 338 return err 339 } 340 341 // send PayByEphemeralAccountRequest 342 err = modules.RPCWrite(buffer, modules.PayByEphemeralAccountRequest{ 343 Message: msg, 344 Signature: sig, 345 }) 346 if err != nil { 347 return err 348 } 349 _, err = buffer.WriteTo(stream) 350 return err 351 } 352 353 // String returns a string representation of the account 354 func (a *account) String() string { 355 var sb strings.Builder 356 sb.WriteString("HostKey: " + a.staticHostKey.String() + "\n") 357 sb.WriteString("\n") 358 sb.WriteString("Balance: " + a.balance.HumanString() + "\n") 359 sb.WriteString(" Balance Drift Pos : " + a.balanceDriftPositive.HumanString() + "\n") 360 sb.WriteString(" Balance Drift Neg : " + a.balanceDriftNegative.HumanString() + "\n") 361 sb.WriteString(" Pending Deposits : " + a.pendingDeposits.HumanString() + "\n") 362 sb.WriteString(" Pending Withdrawals: " + a.pendingWithdrawals.HumanString() + "\n") 363 sb.WriteString(" Negative Balance : " + a.negativeBalance.HumanString() + "\n") 364 sb.WriteString("Residue: " + a.residue.HumanString() + "\n") 365 sb.WriteString("\n") 366 sb.WriteString("Spending Details: \n") 367 sb.WriteString(" Account Balance : " + a.spending.accountBalance.HumanString() + "\n") 368 sb.WriteString(" Downloads : " + a.spending.downloads.HumanString() + "\n") 369 sb.WriteString(" Registry Reads : " + a.spending.registryReads.HumanString() + "\n") 370 sb.WriteString(" Registry Writes : " + a.spending.registryWrites.HumanString() + "\n") 371 sb.WriteString(" Repair Downloads : " + a.spending.repairDownloads.HumanString() + "\n") 372 sb.WriteString(" Repair Uploads : " + a.spending.repairUploads.HumanString() + "\n") 373 sb.WriteString(" Snapshot Downloads: " + a.spending.snapshotDownloads.HumanString() + "\n") 374 sb.WriteString(" Snapshot Uploads : " + a.spending.snapshotUploads.HumanString() + "\n") 375 sb.WriteString(" Subscriptions : " + a.spending.subscriptions.HumanString() + "\n") 376 sb.WriteString(" Update PriceTable : " + a.spending.updatePriceTable.HumanString() + "\n") 377 sb.WriteString(" Uploads : " + a.spending.uploads.HumanString() + "\n") 378 sb.WriteString("\n") 379 sb.WriteString("Host Balance: \n") 380 sb.WriteString(" Host Balance : " + a.hostBalance.HumanString() + "\n") 381 sb.WriteString(" Host Balance Neg: " + a.hostBalanceNegative.HumanString() + "\n") 382 sb.WriteString("\n") 383 return sb.String() 384 } 385 386 // availableBalance returns a new balance that takes into account pending spends 387 // and pending funds, yielding the balance that is available 388 func (a *account) availableBalance() types.Currency { 389 return availableBalance(a.balance, a.negativeBalance, a.pendingDeposits, a.pendingWithdrawals) 390 } 391 392 // availableHostBalance returns a new balance, based on the host balance fields, 393 // that takes into account pending spends and pending funds 394 func (a *account) availableHostBalance() types.Currency { 395 return availableBalance(a.hostBalance, a.hostBalanceNegative, a.pendingDeposits, a.pendingWithdrawals) 396 } 397 398 // callForcedSyncScheduled returns whether or not a forced account sync was 399 // scheduled. 400 func (a *account) callForcedSyncScheduled() bool { 401 a.mu.Lock() 402 defer a.mu.Unlock() 403 return !a.forcedSyncAt.IsZero() 404 } 405 406 // callNeedsToSync returns whether or not the account needs to sync to the host. 407 func (a *account) callNeedsToSync() bool { 408 a.mu.Lock() 409 defer a.mu.Unlock() 410 return a.syncAt.Before(time.Now()) 411 } 412 413 // callSpendingDetails returns the spending details for the account 414 func (a *account) callSpendingDetails() spendingDetails { 415 a.mu.Lock() 416 defer a.mu.Unlock() 417 return a.spending 418 } 419 420 // deposit commits a pending deposit, either after success or failure. 421 // Depending on the outcome the given amount will be added to the balance or 422 // not. If the pending delta is zero, and we altered the account balance, we 423 // update the account. 424 func (a *account) deposit(amount types.Currency) { 425 // reflect the deposit in the balance field 426 if amount.Cmp(a.negativeBalance) <= 0 { 427 a.negativeBalance = a.negativeBalance.Sub(amount) 428 } else { 429 amount = amount.Sub(a.negativeBalance) 430 a.negativeBalance = types.ZeroCurrency 431 a.balance = a.balance.Add(amount) 432 } 433 434 // mirror the balance updates on the host balance, the host balance 435 // fields are kept purely for tracking the drift 436 if amount.Cmp(a.hostBalanceNegative) <= 0 { 437 a.hostBalanceNegative = a.hostBalanceNegative.Sub(amount) 438 } else { 439 amount = amount.Sub(a.hostBalanceNegative) 440 a.hostBalanceNegative = types.ZeroCurrency 441 a.hostBalance = a.hostBalance.Add(amount) 442 } 443 } 444 445 // managedAvailableBalance returns the amount of money that is available to 446 // spend. It is calculated by taking into account pending spends and pending 447 // funds. 448 func (a *account) managedAvailableBalance() types.Currency { 449 a.mu.Lock() 450 defer a.mu.Unlock() 451 return a.availableBalance() 452 } 453 454 // managedMaxExpectedBalance returns the max amount of money that this 455 // account is expected to contain after the renter has shut down. 456 func (a *account) managedMaxExpectedBalance() types.Currency { 457 a.mu.Lock() 458 defer a.mu.Unlock() 459 return a.maxExpectedBalance() 460 } 461 462 // managedResetSpending is called when the contract corresponding to this 463 // account gets renewed, when that happens the FundAccountCost is zero again, 464 // and thus the spending details have to be reset. In order to properly reflect 465 // the spending breakdown, we have to keep track of the current balance we carry 466 // over into the next period, which we call the residue. 467 func (a *account) managedResetSpending() { 468 a.mu.Lock() 469 defer a.mu.Unlock() 470 471 // set the residue to the current balance and reset the spending details 472 a.residue = a.balance 473 a.spending = spendingDetails{} 474 475 // persist the account upon reset 476 err := a.persist() 477 if err != nil { 478 a.staticLog.Critical(fmt.Sprintf("failed to persist account, err: %v", err)) 479 } 480 } 481 482 // maxExpectedBalance returns the max amount of money that this account is 483 // expected to contain after the renter has shut down. 484 func (a *account) maxExpectedBalance() types.Currency { 485 // NOTE: there is an edge case where an EA has some drift, positive drift 486 // where the host balance is higher than the renter balance, if the account 487 // is prevented from being refilled, the negative balance can be higher than 488 // the expected balance so we have to safe sub here 489 expectedBalance := a.balance.Add(a.pendingDeposits) 490 if a.negativeBalance.Cmp(expectedBalance) > 0 { 491 return types.ZeroCurrency 492 } 493 return expectedBalance.Sub(a.negativeBalance) 494 } 495 496 // managedMinExpectedBalance returns the min amount of money that this 497 // account is expected to contain after the renter has shut down. 498 func (a *account) managedMinExpectedBalance() types.Currency { 499 a.mu.Lock() 500 defer a.mu.Unlock() 501 return a.minExpectedBalance() 502 } 503 504 // minExpectedBalance returns the min amount of money that this account is 505 // expected to contain after the renter has shut down. 506 func (a *account) minExpectedBalance() types.Currency { 507 return minExpectedBalance(a.balance, a.negativeBalance, a.pendingWithdrawals) 508 } 509 510 // managedCommitDeposit commits a pending deposit, either after success or 511 // failure. Depending on the outcome the given amount will be added to the 512 // balance or not. If the pending delta is zero, and we altered the account 513 // balance, we update the account. 514 func (a *account) managedCommitDeposit(amount types.Currency, success bool) { 515 a.mu.Lock() 516 defer a.mu.Unlock() 517 518 // (no need to sanity check - the implementation of 'Sub' does this for us) 519 a.pendingDeposits = a.pendingDeposits.Sub(amount) 520 521 // deposit the money in case of success 522 if success { 523 a.deposit(amount) 524 } 525 } 526 527 // managedCommitRefund commits a refund for the given spending category, it's 528 // treated as a simple deposit and subtracted from the corresponding field in 529 // the spending details 530 func (a *account) managedCommitRefund(category spendingCategory, refund types.Currency) { 531 a.mu.Lock() 532 defer a.mu.Unlock() 533 a.deposit(refund) 534 a.trackSpending(category, refund, true) 535 } 536 537 // managedCommitWithdrawal commits a pending withdrawal, either after success or 538 // failure. Depending on the outcome the given withdrawal amount will be 539 // deducted from the balance or not. If the pending delta is zero, and we 540 // altered the account balance, we update the account. The refund is given 541 // because both the refund and the withdrawal amount need to be subtracted from 542 // the pending withdrawals, seeing is it is no longer 'pending'. Only the 543 // withdrawal amount has to be subtracted from the balance because the refund 544 // got refunded by the host already. 545 func (a *account) managedCommitWithdrawal(category spendingCategory, withdrawal, refund types.Currency, withdrawalErr error) { 546 a.mu.Lock() 547 defer a.mu.Unlock() 548 549 // conventience variables 550 alerter := a.staticAlerter 551 balanceTarget := a.staticBalanceTarget 552 workerHostKey := a.staticHostKey.String() 553 554 // (no need to sanity check - the implementation of 'Sub' does this for us) 555 a.pendingWithdrawals = a.pendingWithdrawals.Sub(withdrawal.Add(refund)) 556 557 // reflect the successful withdrawal in the balance field 558 if withdrawalErr == nil { 559 if a.balance.Cmp(withdrawal) >= 0 { 560 a.balance = a.balance.Sub(withdrawal) 561 } else { 562 remainder := withdrawal.Sub(a.balance) 563 a.balance = types.ZeroCurrency 564 a.negativeBalance = a.negativeBalance.Add(remainder) 565 } 566 567 // mirror the balance updates on the host balance, the host balance 568 // fields are kept purely for tracking the drift 569 if a.hostBalance.Cmp(withdrawal) >= 0 { 570 a.hostBalance = a.hostBalance.Sub(withdrawal) 571 } else { 572 remainder := withdrawal.Sub(a.hostBalance) 573 a.hostBalance = types.ZeroCurrency 574 a.hostBalanceNegative = a.hostBalanceNegative.Add(remainder) 575 } 576 577 // only in case of success we track the spend and what it was spent on 578 a.trackSpending(category, withdrawal, false) 579 } 580 581 // register an alert if the host indicates our EA is out of funds, but the 582 // renter's account has not even reached the refill threshold, unregister it 583 // if a withdrawal was successful 584 if isInsufficientBalanceError(withdrawalErr) && !a.needsToRefill(balanceTarget.Div64(2)) { 585 msg := fmt.Sprintf("host %v indicates the ephemeral account is out of balance while the account has not exceeded the refill threshold", workerHostKey) 586 alerter.RegisterAlert(skymodules.AlertIDWorkerAccountOutOfFunds(workerHostKey), msg, workerHostKey, modules.SeverityError) 587 } else if withdrawalErr == nil { 588 alerter.UnregisterAlert(skymodules.AlertIDWorkerAccountOutOfFunds(workerHostKey)) 589 } 590 } 591 592 // managedNeedsToRefill returns whether or not the account needs to be refilled. 593 func (a *account) managedNeedsToRefill(target types.Currency) bool { 594 a.mu.Lock() 595 defer a.mu.Unlock() 596 return a.needsToRefill(target) 597 } 598 599 // managedSyncBalance updates the account's balance related fields to "sync" 600 // with the given balance, which was returned by the host. If the given balance 601 // is higher or lower than the account's available balance, we update the drift 602 // fields in the positive or negative direction. The method returns the drift 603 // between the given host balance and the renter balance. 604 func (a *account) managedSyncBalance(balance types.Currency, forced bool) *big.Int { 605 a.mu.Lock() 606 defer a.mu.Unlock() 607 608 // Reset the host balance on every sync 609 defer a.resetHostBalance(balance) 610 611 // Reset the forced sync at if this sync was forced 612 if forced { 613 a.forcedSyncAt = time.Time{} 614 } 615 616 // Determine how long to wait before attempting to sync again, and then 617 // update the syncAt time. There is significant randomness in the 618 // waiting because syncing with the host requires freezing up the 619 // worker. We do not want to freeze up a large number of workers at 620 // once, nor do we want to freeze them frequently. 621 defer func() { 622 randWait := fastrand.Intn(accountSyncRandWaitMilliseconds) 623 waitTime := time.Duration(randWait) * time.Millisecond 624 waitTime += accountSyncMinWaitTime 625 a.syncAt = time.Now().Add(waitTime) 626 }() 627 628 // If our balance is equal to what the host communicated, we're done. 629 drift := big.NewInt(0) 630 currBalance := a.availableBalance() 631 if currBalance.Equals(balance) { 632 return drift 633 } 634 635 // However, if it is lower, or if we want to force sync, we want to reset 636 // our account balance 637 if forced || currBalance.Cmp(balance) < 0 { 638 a.resetBalance(balance) 639 } 640 641 // Calculate the drift, which is negative if the host reports a lower balance 642 if currBalance.Cmp(balance) < 0 { 643 drift = balance.Sub(currBalance).Big() 644 } else { 645 drift = drift.Neg(currBalance.Sub(balance).Big()) 646 } 647 648 // To avoid an exponential increase of the positive and negative drift 649 // values, we keep track of the host's balance and reset it on every sync. 650 // That way, the positive and negative deltas values measure the drift since 651 // the last sync, as opposed to the drift since the account was created 652 currBalance = a.availableHostBalance() 653 if currBalance.Cmp(balance) < 0 { 654 delta := balance.Sub(currBalance) 655 a.balanceDriftPositive = a.balanceDriftPositive.Add(delta) 656 } 657 658 // If it's higher we only track the amount we drifted. 659 if currBalance.Cmp(balance) > 0 { 660 delta := currBalance.Sub(balance) 661 a.balanceDriftNegative = a.balanceDriftNegative.Add(delta) 662 } 663 664 // Persist the account 665 err := a.persist() 666 if err != nil { 667 a.staticLog.Printf("could not persist account, err: %v\n", err) 668 } 669 670 return drift 671 } 672 673 // managedStatus returns the status of the account 674 func (a *account) managedStatus() skymodules.WorkerAccountStatus { 675 a.mu.Lock() 676 defer a.mu.Unlock() 677 678 var recentErrStr string 679 if a.recentErr != nil { 680 recentErrStr = a.recentErr.Error() 681 } 682 683 return skymodules.WorkerAccountStatus{ 684 AvailableBalance: a.availableBalance(), 685 NegativeBalance: a.negativeBalance, 686 HostBalance: a.hostBalance, 687 688 SyncAt: a.syncAt, 689 ForcedSyncAt: a.forcedSyncAt, 690 691 RecentErr: recentErrStr, 692 RecentErrTime: a.recentErrTime, 693 RecentSuccessTime: a.recentSuccessTime, 694 } 695 } 696 697 // managedTrackDeposit keeps track of pending deposits by adding the given 698 // amount to the 'pendingDeposits' field. 699 func (a *account) managedTrackDeposit(amount types.Currency) { 700 a.mu.Lock() 701 defer a.mu.Unlock() 702 a.pendingDeposits = a.pendingDeposits.Add(amount) 703 } 704 705 // managedTrackWithdrawal keeps track of pending withdrawals by adding the given 706 // amount to the 'pendingWithdrawals' field. 707 func (a *account) managedTrackWithdrawal(amount types.Currency) { 708 a.mu.Lock() 709 defer a.mu.Unlock() 710 a.pendingWithdrawals = a.pendingWithdrawals.Add(amount) 711 } 712 713 // needsToRefill returns whether or not the account needs to be refilled. 714 func (a *account) needsToRefill(target types.Currency) bool { 715 return a.availableBalance().Cmp(target) < 0 716 } 717 718 // resetBalance sets the given balance and resets the account's balance 719 // delta state variables. This happens when we have performanced a balance 720 // inquiry on the host and we decide to trust his version of the balance. 721 func (a *account) resetBalance(balance types.Currency) { 722 a.balance = balance 723 a.pendingDeposits = types.ZeroCurrency 724 a.pendingWithdrawals = types.ZeroCurrency 725 a.negativeBalance = types.ZeroCurrency 726 } 727 728 // resetHostBalance sets the given balance and resets the account's host balance 729 // delta state variables. This happens every time we get a balance from the host. 730 func (a *account) resetHostBalance(balance types.Currency) { 731 a.hostBalance = balance 732 a.hostBalanceNegative = types.ZeroCurrency 733 } 734 735 // trackSpending will keep track of the amount spent for the given spend category 736 func (a *account) trackSpending(category spendingCategory, amount types.Currency, refund bool) { 737 // sanity check the category was set 738 if category == categoryErr { 739 build.Critical("tracked a spend using an uninitialized category, this is prevented as we want to track all money that is being spent without exception") 740 return 741 } 742 743 // update the spending metrics 744 if refund { 745 a.spending.sub(category, amount) 746 } else { 747 a.spending.add(category, amount) 748 } 749 750 // every time we update we write the account to disk 751 err := a.persist() 752 if err != nil { 753 a.staticLog.Critical(fmt.Sprintf("failed to persist account, err: %v", err)) 754 } 755 } 756 757 // newWithdrawalMessage is a helper function that takes a set of parameters and 758 // a returns a new WithdrawalMessage. 759 func newWithdrawalMessage(id modules.AccountID, amount types.Currency, blockHeight types.BlockHeight) modules.WithdrawalMessage { 760 expiry := blockHeight + withdrawalValidityPeriod 761 var nonce [modules.WithdrawalNonceSize]byte 762 fastrand.Read(nonce[:]) 763 return modules.WithdrawalMessage{ 764 Account: id, 765 Expiry: expiry, 766 Amount: amount, 767 Nonce: nonce, 768 } 769 } 770 771 // callScheduleForcedAccountSync schedules a forced sync of the worker's 772 // ephemeral account with the host's balance. 773 func (w *worker) callScheduleForcedAccountSync() { 774 w.staticAccount.mu.Lock() 775 defer w.staticAccount.mu.Unlock() 776 w.staticAccount.forcedSyncAt = time.Now() 777 } 778 779 // externSyncAccountBalanceToHost is executed before the worker loop and 780 // corrects the account balance in case of an unclean renter shutdown. It does 781 // so by performing the AccountBalanceRPC and resetting the account to the 782 // balance communicated by the host. This only happens if our account balance is 783 // zero, which indicates an unclean shutdown. 784 // 785 // NOTE: it is important this function is only used when the worker has no 786 // in-progress jobs, neither serial nor async, to ensure the account balance 787 // sync does not leave the account in an undesired state. The worker should not 788 // be launching new jobs while this function is running. To achieve this, we 789 // ensure that this thread is only run from the primary work loop, which is also 790 // the only thread that is allowed to launch jobs. As long as this function is 791 // only called by that thread, and no other thread launches jobs, this function 792 // is threadsafe. 793 func (w *worker) externSyncAccountBalanceToHost(forced bool) error { 794 // Return if the renter's shutting down 795 if w.staticIsShuttingDown() { 796 return nil 797 } 798 799 // Spin/block until the worker has no jobs in motion. This should only be 800 // called from the primary loop of the worker, meaning that no new jobs will 801 // be launched while we spin. 802 isIdle := func() bool { 803 sls := w.staticLoopState 804 a := atomic.LoadUint64(&sls.atomicSerialJobRunning) == 0 805 b := atomic.LoadUint64(&sls.atomicAsyncJobsRunning) == 0 806 return a && b 807 } 808 start := time.Now() 809 for !isIdle() { 810 if time.Since(start) > accountIdleMaxWait { 811 // The worker failed to go idle for too long. Print the loop state, 812 // so we know what kind of task is keeping it busy. 813 w.staticRenter.staticLog.Printf("Worker static loop state: %+v\n\n", w.staticLoopState) 814 // Get the stack traces of all running goroutines. 815 buf := make([]byte, skymodules.StackSize) // 64MB 816 n := runtime.Stack(buf, true) 817 w.staticRenter.staticLog.Println(string(buf[:n])) 818 w.staticRenter.staticLog.Critical(fmt.Sprintf("worker has taken more than %v minutes to go idle", accountIdleMaxWait.Minutes())) 819 return nil 820 } 821 awake := w.staticTG.Sleep(accountIdleCheckFrequency) 822 if !awake { 823 return nil 824 } 825 } 826 827 // Grab the account sync lock to prevent the subscription loop from 828 // starting new pending deposits or withdrawals. We do this after the 829 // idle check since it might take a while for all jobs to finish and we 830 // don't want to unnecessarily block subscriptions. 831 w.accountSyncMu.Lock() 832 defer w.accountSyncMu.Unlock() 833 834 // Do a check to ensure that the worker is still idle after the function is 835 // complete. This should help to catch any situation where the worker is 836 // spinning up new jobs, even though it is not supposed to be spinning up 837 // new jobs while it is performing the sync operation. 838 defer func() { 839 if !isIdle() { 840 w.staticRenter.staticLog.Critical("worker appears to be spinning up new jobs during managedSyncAccountBalanceToHost") 841 } 842 }() 843 844 // Sanity check the account's deltas are zero, indicating there are no 845 // in-progress jobs 846 w.staticAccount.mu.Lock() 847 deltasAreZero := w.staticAccount.pendingDeposits.IsZero() && w.staticAccount.pendingWithdrawals.IsZero() 848 w.staticAccount.mu.Unlock() 849 if !deltasAreZero { 850 build.Critical("managedSyncAccountBalanceToHost is called on a worker with an account that has non-zero deltas, indicating in-progress jobs") 851 } 852 853 // Track the outcome of the account sync - this ensures a proper working of 854 // the maintenance cooldown mechanism. 855 balance, err := w.managedHostAccountBalance() 856 w.managedTrackAccountSyncErr(err) 857 if err != nil { 858 w.staticRenter.staticLog.Debugf("ERROR: failed to check account balance on host %v failed, err: %v\n", w.staticHostPubKeyStr, err) 859 return err 860 } 861 862 // Sync the account with the host's version of our balance. This will update 863 // our balance in case the host tells us we actually have more money, and it 864 // will keep track of drift in both directions. 865 drift := w.staticAccount.managedSyncBalance(balance, forced) 866 if drift.Sign() == -1 { 867 // if we have negative drift, we want to register an alert if it exceeds a certain threshold 868 driftCurr := types.NewCurrency(drift.Abs(drift)) 869 threshold := w.staticRenter.staticAccountBalanceTarget.MulFloat(accountBalanceDriftPercentageThreshold) 870 if driftCurr.Cmp(threshold) > 0 { 871 msg := fmt.Sprintf("host %v, host balance has drifted away from the renter balance by %v", w.staticHostPubKeyStr, driftCurr.HumanString()) 872 w.staticRenter.staticAlerter.RegisterAlert(skymodules.AlertIDWorkerAccountBalanceDrift(w.staticHostPubKeyStr), msg, "", modules.SeverityWarning) 873 } 874 } else { 875 // if we're in sync with the host balance, unregister the drift alert 876 w.staticRenter.staticAlerter.UnregisterAlert(skymodules.AlertIDWorkerAccountBalanceDrift(w.staticHostPubKeyStr)) 877 } 878 879 // TODO perform a thorough balance comparison to decide whether the drift in 880 // the account balance is warranted. If not the host needs to be penalized 881 // accordingly. Perform this check at startup and periodically. 882 883 return nil 884 } 885 886 // managedForcedAccountSyncScheduled returns true if a forced sync was 887 // scheduled. A forced sync means we will adopt the host's balance regardless of 888 // the renter's account balance. 889 func (w *worker) managedForcedAccountSyncScheduled() bool { 890 w.staticAccount.mu.Lock() 891 defer w.staticAccount.mu.Unlock() 892 return !w.staticAccount.forcedSyncAt.IsZero() 893 } 894 895 // managedNeedsToRefillAccount will check whether the worker's account needs to 896 // be refilled. This function will return false if any conditions are met which 897 // are likely to prevent the refill from being successful. 898 func (w *worker) managedNeedsToRefillAccount() (refill bool) { 899 // No need to refill the account if the worker is on maintenance cooldown. 900 if w.managedOnMaintenanceCooldown() { 901 return false 902 } 903 904 // No need to refill if the price table is not valid, as it would only 905 // result in failure anyway. 906 if !w.staticPriceTable().staticValid() { 907 return false 908 } 909 910 return w.staticAccount.managedNeedsToRefill(w.staticRenter.staticAccountBalanceTarget.Div64(2)) 911 } 912 913 // managedNeedsToSyncAccountBalanceToHost returns whether the renter needs to 914 // sync the renter's account balance with the host's version of the account. The 915 // second return value indicates whether the sync was forced and should adopt 916 // the host balance. 917 func (w *worker) managedNeedsToSyncAccountBalanceToHost() (bool, bool) { 918 // No need to sync if the price table is not valid, as it would only 919 // result in failure anyway. 920 if !w.staticPriceTable().staticValid() { 921 return false, false 922 } 923 924 // Check whether we want to force sync the account 925 if w.managedForcedAccountSyncScheduled() { 926 return true, true 927 } 928 929 // No need to sync the account if the worker's RHP3 is on cooldown. 930 if w.managedOnMaintenanceCooldown() { 931 return false, false 932 } 933 934 return w.staticAccount.callNeedsToSync(), false 935 } 936 937 // managedRefillAccount will refill the account if it needs to be refilled 938 func (w *worker) managedRefillAccount() (err error) { 939 // Disrupt the refill 940 deps := w.staticRenter.staticDeps 941 if deps.Disrupt("DisableFunding") || deps.Disrupt("DisableRefill") { 942 return 943 } 944 945 // Sanity check we have a valid price table 946 if !w.staticPriceTable().staticValid() { 947 return errors.New("can not refill account with an invalid price table") 948 } 949 950 // The account balance dropped to below half the balance target, refill. Use 951 // the max expected balance when refilling to avoid exceeding any host 952 // maximums. 953 balance := w.staticAccount.managedMaxExpectedBalance() 954 amount := types.ZeroCurrency 955 if w.staticRenter.staticAccountBalanceTarget.Cmp(balance) > 0 { 956 amount = w.staticRenter.staticAccountBalanceTarget.Sub(balance) 957 } 958 if amount.IsZero() { 959 return // nothing to do 960 } 961 pt := w.staticPriceTable().staticPriceTable 962 963 // If the target amount is larger than the remaining money, adjust the 964 // target. Make sure it can still cover the funding cost. 965 if contract, ok := w.staticRenter.staticHostContractor.ContractByPublicKey(w.staticHostPubKey); ok { 966 if amount.Add(pt.FundAccountCost).Cmp(contract.RenterFunds) > 0 && contract.RenterFunds.Cmp(pt.FundAccountCost) > 0 { 967 amount = contract.RenterFunds.Sub(pt.FundAccountCost) 968 } 969 } 970 971 // We track that there is a deposit in progress. Because filling an account 972 // is an interactive protocol with another machine, we are never sure of the 973 // exact moment that the deposit has reached our account. Instead, we track 974 // the deposit as a "maybe" until we know for sure that the deposit has 975 // either reached the remote machine or failed. 976 // 977 // At the same time that we track the deposit, we defer a function to check 978 // the error on the deposit 979 w.staticAccount.managedTrackDeposit(amount) 980 defer func() { 981 // If there was no error, the account should now be full, and will not 982 // need to be refilled until the worker has spent up the funds in the 983 // account. 984 w.staticAccount.managedCommitDeposit(amount, err == nil) 985 986 // Track the outcome of the account refill - this ensures a proper 987 // working of the maintenance cooldown mechanism. 988 cd := w.managedTrackAccountRefillErr(err) 989 990 // If the error is nil, return. 991 if err == nil { 992 w.staticAccount.mu.Lock() 993 w.staticAccount.recentSuccessTime = time.Now() 994 w.staticAccount.mu.Unlock() 995 return 996 } 997 998 // Track the error on the account for debugging purposes. 999 w.staticAccount.mu.Lock() 1000 w.staticAccount.recentErr = err 1001 w.staticAccount.recentErrTime = time.Now() 1002 w.staticAccount.mu.Unlock() 1003 1004 // Have the threadgroup wake the worker when the account comes off of 1005 // cooldown. 1006 w.staticTG.AfterFunc(time.Until(cd), func() { 1007 w.staticWake() 1008 }) 1009 }() 1010 1011 // check the current price table for gouging errors 1012 err = checkFundAccountGouging(w.staticPriceTable().staticPriceTable, w.staticCache().staticRenterAllowance, w.staticRenter.staticAccountBalanceTarget) 1013 if err != nil { 1014 return 1015 } 1016 1017 // create a new stream 1018 var stream net.Conn 1019 stream, err = w.staticNewStream() 1020 if err != nil { 1021 err = errors.AddContext(err, "Unable to create a new stream") 1022 return 1023 } 1024 defer func() { 1025 closeErr := stream.Close() 1026 if closeErr != nil { 1027 w.staticRenter.staticLog.Println("ERROR: failed to close stream", closeErr) 1028 } 1029 }() 1030 1031 // prepare a buffer so we can optimize our writes 1032 buffer := bytes.NewBuffer(nil) 1033 1034 // write the specifier 1035 err = modules.RPCWrite(buffer, modules.RPCFundAccount) 1036 if err != nil { 1037 err = errors.AddContext(err, "could not write fund account specifier") 1038 return 1039 } 1040 1041 // send price table uid 1042 err = modules.RPCWrite(buffer, pt.UID) 1043 if err != nil { 1044 err = errors.AddContext(err, "could not write price table uid") 1045 return 1046 } 1047 1048 // send fund account request 1049 err = modules.RPCWrite(buffer, modules.FundAccountRequest{Account: w.staticAccount.staticID}) 1050 if err != nil { 1051 err = errors.AddContext(err, "could not write the fund account request") 1052 return 1053 } 1054 1055 // write contents of the buffer to the stream 1056 _, err = stream.Write(buffer.Bytes()) 1057 if err != nil { 1058 err = errors.AddContext(err, "could not write the buffer contents") 1059 return 1060 } 1061 1062 // build payment details 1063 details := contractor.PaymentDetails{ 1064 Host: w.staticHostPubKey, 1065 Amount: amount.Add(pt.FundAccountCost), 1066 RefundAccount: modules.ZeroAccountID, 1067 SpendingDetails: skymodules.SpendingDetails{ 1068 FundAccountSpending: amount, 1069 MaintenanceSpending: skymodules.MaintenanceSpending{ 1070 FundAccountCost: pt.FundAccountCost, 1071 }, 1072 }, 1073 } 1074 1075 // provide payment 1076 err = w.staticRenter.staticHostContractor.ProvidePayment(stream, &pt, details) 1077 if err != nil && strings.Contains(err.Error(), "balance exceeded") { 1078 // The host reporting that the balance has been exceeded suggests that 1079 // the host believes that we have more money than we believe that we 1080 // have. 1081 if !w.staticRenter.staticDeps.Disrupt("DisableCriticalOnMaxBalance") { 1082 // Log a critical in testing as this is very unlikely to happen due 1083 // to the order of events in the worker loop, seeing as we just 1084 // synced our account balance with the host if that was necessary 1085 if build.Release == "testing" { 1086 msg := fmt.Sprintf("amount: %v, estimatedBalance: %v, target: %v", amount, balance, w.staticRenter.staticAccountBalanceTarget) 1087 build.Critical("worker account refill failed with a max balance - ", msg, " - are the host max balance settings lower than the threshold balance?", err) 1088 } 1089 w.staticRenter.staticLog.Println("worker account refill failed", err) 1090 } 1091 w.staticAccount.mu.Lock() 1092 w.staticAccount.syncAt = time.Time{} 1093 w.staticAccount.mu.Unlock() 1094 } 1095 if err != nil { 1096 err = errors.AddContext(err, "could not provide payment for the account") 1097 return 1098 } 1099 1100 // receive FundAccountResponse. The response contains a receipt and a 1101 // signature, which is useful for places where accountability is required, 1102 // but no accountability is required in this case, so we ignore the 1103 // response. 1104 var resp modules.FundAccountResponse 1105 err = modules.RPCRead(stream, &resp) 1106 if err != nil { 1107 err = errors.AddContext(err, "could not read the account response") 1108 } 1109 1110 // Wake the worker so that any jobs potentially blocking on getting more 1111 // money in the account can be activated. 1112 w.staticWake() 1113 1114 return nil 1115 } 1116 1117 // managedHostAccountBalance performs the AccountBalanceRPC on the host 1118 func (w *worker) managedHostAccountBalance() (_ types.Currency, err error) { 1119 // sanity check - only one account balance check should be running at a 1120 // time. 1121 if !atomic.CompareAndSwapUint64(&w.atomicAccountBalanceCheckRunning, 0, 1) { 1122 w.staticRenter.staticLog.Critical("account balance is being checked in two threads concurrently") 1123 } 1124 defer atomic.StoreUint64(&w.atomicAccountBalanceCheckRunning, 0) 1125 1126 // Get a stream. 1127 stream, err := w.staticNewStream() 1128 if err != nil { 1129 return types.ZeroCurrency, err 1130 } 1131 defer func() { 1132 if err := stream.Close(); err != nil { 1133 w.staticRenter.staticLog.Println("ERROR: failed to close stream", err) 1134 } 1135 }() 1136 1137 // try and pay by EA if possible 1138 if w.managedMaintenancePayByEA() { 1139 return w.managedHostAccountBalancePayByEA(stream) 1140 } 1141 return w.managedHostAccountBalancePayByContract(stream) 1142 } 1143 1144 // managedHostAccountBalancePayByEA performs the AccountBalanceRPC on the host 1145 // and performs a payment using the renter's ephemeral account. 1146 func (w *worker) managedHostAccountBalancePayByEA(stream siamux.Stream) (_ types.Currency, err error) { 1147 // prepare a buffer so we can optimize our writes 1148 buffer := staticPoolCheckAccountBalanceBuffers.Get() 1149 defer staticPoolCheckAccountBalanceBuffers.Put(buffer) 1150 1151 // write the specifier 1152 err = modules.RPCWrite(buffer, modules.RPCAccountBalance) 1153 if err != nil { 1154 return types.ZeroCurrency, err 1155 } 1156 1157 // send price table uid 1158 pt := w.staticPriceTable().staticPriceTable 1159 err = modules.RPCWrite(buffer, pt.UID) 1160 if err != nil { 1161 return types.ZeroCurrency, err 1162 } 1163 cost := pt.AccountBalanceCost 1164 1165 // track the withdrawal 1166 w.staticAccount.managedTrackWithdrawal(cost) 1167 1168 // provide payment 1169 bh, _ := w.managedSyncInfo() 1170 err = w.staticAccount.ProvidePayment(buffer, cost, bh) 1171 if err != nil { 1172 // commit the withdrawal 1173 w.staticAccount.managedCommitWithdrawal(categoryAccountBalance, cost, types.ZeroCurrency, err) 1174 err = errors.AddContext(err, "unable to provide payment by ephemeral account") 1175 return 1176 } 1177 1178 // commit the withdrawal 1179 w.staticAccount.managedCommitWithdrawal(categoryAccountBalance, cost, types.ZeroCurrency, nil) 1180 1181 // prepare the request. 1182 abr := modules.AccountBalanceRequest{Account: w.staticAccount.staticID} 1183 err = modules.RPCWrite(buffer, abr) 1184 if err != nil { 1185 return types.ZeroCurrency, err 1186 } 1187 1188 // write contents of the buffer to the stream 1189 _, err = buffer.WriteTo(stream) 1190 if err != nil { 1191 err = errors.AddContext(err, "Failed to write buffer to stream") 1192 return 1193 } 1194 1195 // read the response 1196 var resp modules.AccountBalanceResponse 1197 err = modules.RPCRead(stream, &resp) 1198 if err != nil { 1199 return types.ZeroCurrency, err 1200 } 1201 1202 // disrupt if necessary 1203 if w.staticRenter.staticDeps.Disrupt("ZeroBalance") { 1204 return types.ZeroCurrency, nil 1205 } 1206 return resp.Balance, nil 1207 } 1208 1209 // managedHostAccountBalancePayByContract performs the AccountBalanceRPC on the 1210 // host and performs a payment using a contract. 1211 func (w *worker) managedHostAccountBalancePayByContract(stream siamux.Stream) (_ types.Currency, err error) { 1212 // write the specifier 1213 err = modules.RPCWrite(stream, modules.RPCAccountBalance) 1214 if err != nil { 1215 return types.ZeroCurrency, err 1216 } 1217 1218 // send price table uid 1219 pt := w.staticPriceTable().staticPriceTable 1220 err = modules.RPCWrite(stream, pt.UID) 1221 if err != nil { 1222 return types.ZeroCurrency, err 1223 } 1224 1225 // build payment details 1226 details := contractor.PaymentDetails{ 1227 Host: w.staticHostPubKey, 1228 Amount: pt.AccountBalanceCost, 1229 RefundAccount: w.staticAccount.staticID, 1230 SpendingDetails: skymodules.SpendingDetails{ 1231 MaintenanceSpending: skymodules.MaintenanceSpending{ 1232 AccountBalanceCost: pt.AccountBalanceCost, 1233 }, 1234 }, 1235 } 1236 1237 // provide payment 1238 err = w.staticRenter.staticHostContractor.ProvidePayment(stream, &pt, details) 1239 if err != nil { 1240 return types.ZeroCurrency, err 1241 } 1242 1243 // prepare the request. 1244 abr := modules.AccountBalanceRequest{Account: w.staticAccount.staticID} 1245 err = modules.RPCWrite(stream, abr) 1246 if err != nil { 1247 return types.ZeroCurrency, err 1248 } 1249 1250 // read the response 1251 var resp modules.AccountBalanceResponse 1252 err = modules.RPCRead(stream, &resp) 1253 if err != nil { 1254 return types.ZeroCurrency, err 1255 } 1256 1257 // disrupt if necessary 1258 if w.staticRenter.staticDeps.Disrupt("ZeroBalance") { 1259 return types.ZeroCurrency, nil 1260 } 1261 return resp.Balance, nil 1262 } 1263 1264 // checkFundAccountGouging verifies the cost of funding an ephemeral account on 1265 // the host is reasonable, if deemed unreasonable we will block the refill and 1266 // the worker will eventually be put into cooldown. 1267 func checkFundAccountGouging(pt modules.RPCPriceTable, allowance skymodules.Allowance, targetBalance types.Currency) error { 1268 // If there is no allowance, price gouging checks have to be disabled, 1269 // because there is no baseline for understanding what might count as price 1270 // gouging. 1271 if allowance.Funds.IsZero() { 1272 return nil 1273 } 1274 1275 // In order to decide whether or not the fund account cost is too expensive, 1276 // we first calculate how many times we can refill the account, taking into 1277 // account the refill amount and the cost to effectively fund the account. 1278 // 1279 // Note: we divide the target balance by two because more often than not the 1280 // refill happens the moment we drop below half of the target, this means 1281 // that we actually refill half the target amount most of the time. 1282 costOfRefill := targetBalance.Div64(2).Add(pt.FundAccountCost) 1283 numRefills, err := allowance.Funds.Div(costOfRefill).Uint64() 1284 if err != nil { 1285 return errors.AddContext(err, "unable to check fund account gouging, could not calculate the amount of refills") 1286 } 1287 1288 // The cost of funding is considered too expensive if the total cost is 1289 // above a certain % of the allowance. 1290 totalFundAccountCost := pt.FundAccountCost.Mul64(numRefills) 1291 if totalFundAccountCost.Cmp(allowance.Funds.MulFloat(fundAccountGougingPercentageThreshold)) > 0 { 1292 return fmt.Errorf("fund account cost %v is considered too high, the total cost of refilling the account to spend the total allowance exceeds %v%% of the allowance - price gouging protection enabled", pt.FundAccountCost, fundAccountGougingPercentageThreshold) 1293 } 1294 1295 return nil 1296 } 1297 1298 // availableBalance takes a balance, its negative counter part, pending deposits 1299 // and withdrawals and returns a currency that reflects the resulting available 1300 // balance 1301 func availableBalance(balance, balanceNeg, pendingDeposits, pendingWithdrawals types.Currency) types.Currency { 1302 total := balance.Add(pendingDeposits) 1303 if total.Cmp(balanceNeg) <= 0 { 1304 return types.ZeroCurrency 1305 } 1306 total = total.Sub(balanceNeg) 1307 if pendingWithdrawals.Cmp(total) < 0 { 1308 return total.Sub(pendingWithdrawals) 1309 } 1310 return types.ZeroCurrency 1311 } 1312 1313 // minExpectedBalance returns the min amount of money that this account is 1314 // expected to contain after the renter has shut down. 1315 func minExpectedBalance(balance, balanceNegative, pendingWithdrawals types.Currency) types.Currency { 1316 // subtract all pending withdrawals 1317 if balance.Cmp(pendingWithdrawals) <= 0 { 1318 return types.ZeroCurrency 1319 } 1320 balance = balance.Sub(pendingWithdrawals) 1321 1322 // subtract the negative balance 1323 if balanceNegative.Cmp(balance) > 0 { 1324 return types.ZeroCurrency 1325 } 1326 balance = balance.Sub(balanceNegative) 1327 return balance 1328 } 1329 1330 // isInsufficientBalanceError is a helper function that verifies whether the 1331 // given error indicates the ephemeral account on the host is out of balance. 1332 // 1333 // NOTE: the host only returns this error after blocking the withdrawal for a 1334 // certain amount of time, awaiting a potential deposit 1335 func isInsufficientBalanceError(err error) bool { 1336 return err != nil && strings.Contains(err.Error(), host.ErrBalanceInsufficient.Error()) 1337 }