From bb2be05b7c907271782369e166dbe67d007e6c81 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Mon, 11 Dec 2023 11:37:25 +0100 Subject: [PATCH] worker: take expectedStorage and minNewCollateral as params instead of newCollateral --- api/worker.go | 20 +++++++++-------- autopilot/contractor.go | 28 +++++++---------------- autopilot/hostfilter.go | 13 +++++++---- autopilot/workerpool.go | 2 +- bus/bus.go | 4 ++-- worker/client/rhp.go | 27 +++++++++++----------- worker/rhpv3.go | 50 +++++++++++++++++++++++++++++++++++++---- worker/worker.go | 6 +++-- 8 files changed, 94 insertions(+), 56 deletions(-) diff --git a/api/worker.go b/api/worker.go index d41b2c46a..8aeb90b2d 100644 --- a/api/worker.go +++ b/api/worker.go @@ -115,15 +115,16 @@ type ( // RHPRenewRequest is the request type for the /rhp/renew endpoint. RHPRenewRequest struct { - ContractID types.FileContractID `json:"contractID"` - EndHeight uint64 `json:"endHeight"` - HostAddress types.Address `json:"hostAddress"` - HostKey types.PublicKey `json:"hostKey"` - SiamuxAddr string `json:"siamuxAddr"` - NewCollateral types.Currency `json:"newCollateral"` - RenterAddress types.Address `json:"renterAddress"` - RenterFunds types.Currency `json:"renterFunds"` - WindowSize uint64 `json:"windowSize"` + ContractID types.FileContractID `json:"contractID"` + EndHeight uint64 `json:"endHeight"` + ExpectedStorage uint64 `json:"expectedStorage"` + HostAddress types.Address `json:"hostAddress"` + HostKey types.PublicKey `json:"hostKey"` + MinNewCollateral types.Currency `json:"minNewCollateral"` + SiamuxAddr string `json:"siamuxAddr"` + RenterAddress types.Address `json:"renterAddress"` + RenterFunds types.Currency `json:"renterFunds"` + WindowSize uint64 `json:"windowSize"` } // RHPRenewResponse is the response type for the /rhp/renew endpoint. @@ -131,6 +132,7 @@ type ( Error string `json:"error"` ContractID types.FileContractID `json:"contractID"` Contract rhpv2.ContractRevision `json:"contract"` + ContractPrice types.Currency `json:"contractPrice"` TransactionSet []types.Transaction `json:"transactionSet"` } diff --git a/autopilot/contractor.go b/autopilot/contractor.go index d6b86c7c0..3145e5e64 100644 --- a/autopilot/contractor.go +++ b/autopilot/contractor.go @@ -1371,10 +1371,9 @@ func (c *contractor) renewContract(ctx context.Context, w Worker, ci contractInf // calculate the host collateral expectedStorage := renterFundsToExpectedStorage(renterFunds, endHeight-cs.BlockHeight, ci.priceTable) - newCollateral := rhpv2.ContractRenewalCollateral(rev.FileContract, expectedStorage, settings, cs.BlockHeight, endHeight) // renew the contract - newRevision, _, err := w.RHPRenew(ctx, fcid, endHeight, hk, contract.SiamuxAddr, settings.Address, state.address, renterFunds, newCollateral, settings.WindowSize) + resp, err := w.RHPRenew(ctx, fcid, endHeight, hk, contract.SiamuxAddr, settings.Address, state.address, renterFunds, types.ZeroCurrency, expectedStorage, settings.WindowSize) if err != nil { c.logger.Errorw(fmt.Sprintf("renewal failed, err: %v", err), "hk", hk, "fcid", fcid) if strings.Contains(err.Error(), wallet.ErrInsufficientBalance.Error()) { @@ -1387,8 +1386,8 @@ func (c *contractor) renewContract(ctx context.Context, w Worker, ci contractInf *budget = budget.Sub(renterFunds) // persist the contract - contractPrice := newRevision.Revision.MissedHostPayout().Sub(newCollateral) - renewedContract, err := c.ap.bus.AddRenewedContract(ctx, newRevision, contractPrice, renterFunds, cs.BlockHeight, fcid, api.ContractStatePending) + newCollateral := resp.Contract.Revision.MissedHostPayout().Sub(resp.ContractPrice) + renewedContract, err := c.ap.bus.AddRenewedContract(ctx, resp.Contract, resp.ContractPrice, renterFunds, cs.BlockHeight, fcid, api.ContractStatePending) if err != nil { c.logger.Errorw(fmt.Sprintf("renewal failed to persist, err: %v", err), "hk", hk, "fcid", fcid) return api.ContractMetadata{}, false, err @@ -1448,22 +1447,11 @@ func (c *contractor) refreshContract(ctx context.Context, w Worker, ci contractI // calculate the new collateral expectedStorage := renterFundsToExpectedStorage(renterFunds, contract.EndHeight()-cs.BlockHeight, ci.priceTable) - newCollateral := rhpv2.ContractRenewalCollateral(rev.FileContract, expectedStorage, settings, cs.BlockHeight, contract.EndHeight()) - - // do not refresh if the contract's updated collateral will fall below the threshold anyway - _, hostMissedPayout, _, _ := rhpv2.CalculateHostPayouts(rev.FileContract, newCollateral, settings, contract.EndHeight()) - var newRemainingCollateral types.Currency - if hostMissedPayout.Cmp(settings.ContractPrice) > 0 { - newRemainingCollateral = hostMissedPayout.Sub(settings.ContractPrice) - } - if isBelowCollateralThreshold(newCollateral, newRemainingCollateral) { - err := errors.New("refresh failed, new collateral is below the threshold") - c.logger.Errorw(err.Error(), "hk", hk, "fcid", fcid, "expectedCollateral", newCollateral.String(), "actualCollateral", newRemainingCollateral.String(), "maxCollateral", settings.MaxCollateral) - return api.ContractMetadata{}, true, err - } + unallocatedCollateral := rev.MissedHostPayout().Sub(contract.ContractPrice) + minNewCollateral := minNewCollateral(unallocatedCollateral) // renew the contract - newRevision, _, err := w.RHPRenew(ctx, contract.ID, contract.EndHeight(), hk, contract.SiamuxAddr, settings.Address, state.address, renterFunds, newCollateral, settings.WindowSize) + resp, err := w.RHPRenew(ctx, contract.ID, contract.EndHeight(), hk, contract.SiamuxAddr, settings.Address, state.address, renterFunds, minNewCollateral, expectedStorage, settings.WindowSize) if err != nil { c.logger.Errorw(fmt.Sprintf("refresh failed, err: %v", err), "hk", hk, "fcid", fcid) if strings.Contains(err.Error(), wallet.ErrInsufficientBalance.Error()) { @@ -1476,8 +1464,8 @@ func (c *contractor) refreshContract(ctx context.Context, w Worker, ci contractI *budget = budget.Sub(renterFunds) // persist the contract - contractPrice := newRevision.Revision.MissedHostPayout().Sub(newCollateral) - refreshedContract, err := c.ap.bus.AddRenewedContract(ctx, newRevision, contractPrice, renterFunds, cs.BlockHeight, contract.ID, api.ContractStatePending) + newCollateral := resp.Contract.Revision.MissedHostPayout().Sub(resp.ContractPrice) + refreshedContract, err := c.ap.bus.AddRenewedContract(ctx, resp.Contract, resp.ContractPrice, renterFunds, cs.BlockHeight, contract.ID, api.ContractStatePending) if err != nil { c.logger.Errorw(fmt.Sprintf("refresh failed, err: %v", err), "hk", hk, "fcid", fcid) return api.ContractMetadata{}, false, err diff --git a/autopilot/hostfilter.go b/autopilot/hostfilter.go index c9ffc8fd2..c8f228f47 100644 --- a/autopilot/hostfilter.go +++ b/autopilot/hostfilter.go @@ -312,7 +312,7 @@ func isOutOfCollateral(c api.Contract, s rhpv2.HostSettings, pt rhpv3.HostPriceT if expectedStorage > s.RemainingStorage { expectedStorage = s.RemainingStorage } - newCollateral := rhpv2.ContractRenewalCollateral(c.Revision.FileContract, expectedStorage, s, blockHeight, c.EndHeight()) + newCollateral := worker.ContractRenewalCollateral(c.Revision.FileContract, expectedStorage, pt, blockHeight, c.EndHeight()) return isBelowCollateralThreshold(newCollateral, c.RemainingCollateral(s)) } @@ -335,9 +335,14 @@ func isBelowCollateralThreshold(newCollateral, actualCollateral types.Currency) // contracts out eventually. return false } - collateral := big.NewRat(0, 1).SetFrac(actualCollateral.Big(), newCollateral.Big()) - threshold := big.NewRat(minContractCollateralThresholdNumerator, minContractCollateralThresholdDenominator) - return collateral.Cmp(threshold) < 0 + return newCollateral.Cmp(minNewCollateral(actualCollateral)) < 0 +} + +// minNewCollateral returns the minimum amount of unallocated collateral that a +// contract should contain after a refresh given the current amount of +// unallocated collateral. +func minNewCollateral(unallocatedCollateral types.Currency) types.Currency { + return unallocatedCollateral.Mul64(minContractCollateralThresholdDenominator).Div64(minContractCollateralThresholdNumerator) } func isUpForRenewal(cfg api.AutopilotConfig, r types.FileContractRevision, blockHeight uint64) (shouldRenew, secondHalf bool) { diff --git a/autopilot/workerpool.go b/autopilot/workerpool.go index ce42854c2..d8c821354 100644 --- a/autopilot/workerpool.go +++ b/autopilot/workerpool.go @@ -25,7 +25,7 @@ type Worker interface { RHPFund(ctx context.Context, contractID types.FileContractID, hostKey types.PublicKey, hostIP, siamuxAddr string, balance types.Currency) (err error) RHPPriceTable(ctx context.Context, hostKey types.PublicKey, siamuxAddr string, timeout time.Duration) (hostdb.HostPriceTable, error) RHPPruneContract(ctx context.Context, fcid types.FileContractID, timeout time.Duration) (pruned, remaining uint64, err error) - RHPRenew(ctx context.Context, fcid types.FileContractID, endHeight uint64, hk types.PublicKey, hostIP string, hostAddress, renterAddress types.Address, renterFunds, newCollateral types.Currency, windowSize uint64) (rhpv2.ContractRevision, []types.Transaction, error) + RHPRenew(ctx context.Context, fcid types.FileContractID, endHeight uint64, hk types.PublicKey, hostIP string, hostAddress, renterAddress types.Address, renterFunds, minNewCollateral types.Currency, expectedStorage, windowSize uint64) (api.RHPRenewResponse, error) RHPScan(ctx context.Context, hostKey types.PublicKey, hostIP string, timeout time.Duration) (api.RHPScanResponse, error) RHPSync(ctx context.Context, contractID types.FileContractID, hostKey types.PublicKey, hostIP, siamuxAddr string) (err error) } diff --git a/bus/bus.go b/bus/bus.go index 305ac839a..e51c080ea 100644 --- a/bus/bus.go +++ b/bus/bus.go @@ -298,8 +298,8 @@ func (b *bus) Handler() http.Handler { "GET /host/:hostkey": b.hostsPubkeyHandlerGET, "POST /host/:hostkey/resetlostsectors": b.hostsResetLostSectorsPOST, - "PUT /metric/:key": b.metricsHandlerPUT, - "GET /metric/:key": b.metricsHandlerGET, + "PUT /metric/:key": b.metricsHandlerPUT, + "GET /metric/:key": b.metricsHandlerGET, "POST /multipart/create": b.multipartHandlerCreatePOST, "POST /multipart/abort": b.multipartHandlerAbortPOST, diff --git a/worker/client/rhp.go b/worker/client/rhp.go index acace7d36..4e164b809 100644 --- a/worker/client/rhp.go +++ b/worker/client/rhp.go @@ -80,22 +80,21 @@ func (c *Client) RHPPruneContract(ctx context.Context, contractID types.FileCont } // RHPRenew renews an existing contract with a host. -func (c *Client) RHPRenew(ctx context.Context, contractID types.FileContractID, endHeight uint64, hostKey types.PublicKey, siamuxAddr string, hostAddress, renterAddress types.Address, renterFunds, newCollateral types.Currency, windowSize uint64) (rhpv2.ContractRevision, []types.Transaction, error) { +func (c *Client) RHPRenew(ctx context.Context, contractID types.FileContractID, endHeight uint64, hostKey types.PublicKey, siamuxAddr string, hostAddress, renterAddress types.Address, renterFunds, minNewCollateral types.Currency, expectedStorage, windowSize uint64) (resp api.RHPRenewResponse, err error) { req := api.RHPRenewRequest{ - ContractID: contractID, - EndHeight: endHeight, - HostAddress: hostAddress, - HostKey: hostKey, - NewCollateral: newCollateral, - RenterAddress: renterAddress, - RenterFunds: renterFunds, - SiamuxAddr: siamuxAddr, - WindowSize: windowSize, + ContractID: contractID, + EndHeight: endHeight, + ExpectedStorage: expectedStorage, + HostAddress: hostAddress, + HostKey: hostKey, + MinNewCollateral: minNewCollateral, + RenterAddress: renterAddress, + RenterFunds: renterFunds, + SiamuxAddr: siamuxAddr, + WindowSize: windowSize, } - - var resp api.RHPRenewResponse - err := c.c.WithContext(ctx).POST("/rhp/renew", req, &resp) - return resp.Contract, resp.TransactionSet, err + err = c.c.WithContext(ctx).POST("/rhp/renew", req, &resp) + return } // RHPScan scans a host, returning its current settings. diff --git a/worker/rhpv3.go b/worker/rhpv3.go index 635cc5c5c..910d77789 100644 --- a/worker/rhpv3.go +++ b/worker/rhpv3.go @@ -923,7 +923,7 @@ type PriceTablePaymentFunc func(pt rhpv3.HostPriceTable) (rhpv3.PaymentMethod, e // Renew renews a contract with a host. To avoid an edge case where the contract // is drained and can therefore not be used to pay for the revision, we simply // don't pay for it. -func (h *host) Renew(ctx context.Context, rrr api.RHPRenewRequest) (_ rhpv2.ContractRevision, _ []types.Transaction, err error) { +func (h *host) Renew(ctx context.Context, rrr api.RHPRenewRequest) (_ rhpv2.ContractRevision, _ []types.Transaction, _ types.Currency, err error) { // Try to get a valid pricetable. ptCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() @@ -935,21 +935,23 @@ func (h *host) Renew(ctx context.Context, rrr api.RHPRenewRequest) (_ rhpv2.Cont h.logger.Debugf("unable to fetch price table for renew: %v", err) } + var contractPrice types.Currency var rev rhpv2.ContractRevision var txnSet []types.Transaction var renewErr error err = h.transportPool.withTransportV3(ctx, h.HostKey(), h.siamuxAddr, func(ctx context.Context, t *transportV3) (err error) { _, err = RPCLatestRevision(ctx, t, h.fcid, func(revision *types.FileContractRevision) (rhpv3.HostPriceTable, rhpv3.PaymentMethod, error) { // Renew contract. + contractPrice = pt.ContractPrice rev, txnSet, renewErr = RPCRenew(ctx, rrr, h.bus, t, pt, *revision, h.renterKey, h.logger) return rhpv3.HostPriceTable{}, nil, nil }) return err }) if err != nil { - return rhpv2.ContractRevision{}, nil, err + return rhpv2.ContractRevision{}, nil, contractPrice, err } - return rev, txnSet, renewErr + return rev, txnSet, contractPrice, renewErr } func (h *host) FetchPriceTable(ctx context.Context, rev *types.FileContractRevision) (hpt hostdb.HostPriceTable, err error) { @@ -1386,9 +1388,15 @@ func RPCRenew(ctx context.Context, rrr api.RHPRenewRequest, bus Bus, t *transpor return rhpv2.ContractRevision{}, nil, fmt.Errorf("host gouging during renew: %v", breakdown.Reasons()) } + // Compute the additional collateral we can put into the contract. + newCollateral := ContractRenewalCollateral(rev.FileContract, rrr.ExpectedStorage, *pt, pt.HostBlockHeight, rrr.EndHeight) + if newCollateral.Cmp(rrr.MinNewCollateral) < 0 { + return rhpv2.ContractRevision{}, nil, fmt.Errorf("newCollateral < minNewCollateral: %v < %v", newCollateral, rrr.MinNewCollateral) + } + // Prepare the signed transaction that contains the final revision as well // as the new contract - wprr, err := bus.WalletPrepareRenew(ctx, rev, rrr.HostAddress, rrr.RenterAddress, renterKey, rrr.RenterFunds, rrr.NewCollateral, *pt, rrr.EndHeight, rrr.WindowSize) + wprr, err := bus.WalletPrepareRenew(ctx, rev, rrr.HostAddress, rrr.RenterAddress, renterKey, rrr.RenterFunds, newCollateral, *pt, rrr.EndHeight, rrr.WindowSize) if err != nil { return rhpv2.ContractRevision{}, nil, fmt.Errorf("failed to prepare renew: %w", err) } @@ -1571,3 +1579,37 @@ func payByContract(rev *types.FileContractRevision, amount types.Currency, refun } return payment, nil } + +// TODO: get this into core and eventually use that instead of this function +func ContractRenewalCollateral(fc types.FileContract, expectedNewStorage uint64, pt rhpv3.HostPriceTable, blockHeight, endHeight uint64) types.Currency { + if endHeight < fc.EndHeight() { + panic("endHeight should be at least the current end height of the contract") + } + extension := endHeight - fc.EndHeight() + if endHeight < blockHeight { + panic("current blockHeight should be lower than the endHeight") + } + duration := endHeight - blockHeight + + // calculate the base collateral - if it exceeds MaxCollateral we can't add more collateral + baseCollateral := pt.CollateralCost.Mul64(fc.Filesize).Mul64(extension) + if baseCollateral.Cmp(pt.MaxCollateral) >= 0 { + return types.ZeroCurrency + } + + // calculate the new collateral + newCollateral := pt.CollateralCost.Mul64(expectedNewStorage).Mul64(duration) + + // if the total collateral is more than the MaxCollateral subtract the + // delta. + totalCollateral := baseCollateral.Add(newCollateral) + if totalCollateral.Cmp(pt.MaxCollateral) > 0 { + delta := totalCollateral.Sub(pt.MaxCollateral) + if delta.Cmp(newCollateral) > 0 { + newCollateral = types.ZeroCurrency + } else { + newCollateral = newCollateral.Sub(delta) + } + } + return newCollateral +} diff --git a/worker/worker.go b/worker/worker.go index ba52735f6..e4a8d0434 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -218,7 +218,7 @@ type hostV3 interface { FetchPriceTable(ctx context.Context, rev *types.FileContractRevision) (hpt hostdb.HostPriceTable, err error) FetchRevision(ctx context.Context, fetchTimeout time.Duration, blockHeight uint64) (types.FileContractRevision, error) FundAccount(ctx context.Context, balance types.Currency, rev *types.FileContractRevision) error - Renew(ctx context.Context, rrr api.RHPRenewRequest) (_ rhpv2.ContractRevision, _ []types.Transaction, err error) + Renew(ctx context.Context, rrr api.RHPRenewRequest) (_ rhpv2.ContractRevision, _ []types.Transaction, _ types.Currency, err error) SyncAccount(ctx context.Context, rev *types.FileContractRevision) error UploadSector(ctx context.Context, sector *[rhpv2.SectorSize]byte, rev types.FileContractRevision) (types.Hash256, error) } @@ -742,9 +742,10 @@ func (w *worker) rhpRenewHandler(jc jape.Context) { // renew the contract var renewed rhpv2.ContractRevision var txnSet []types.Transaction + var contractPrice types.Currency if jc.Check("couldn't renew contract", w.withRevision(ctx, defaultRevisionFetchTimeout, rrr.ContractID, rrr.HostKey, rrr.SiamuxAddr, lockingPriorityRenew, cs.BlockHeight, func(_ types.FileContractRevision) (err error) { h := w.newHostV3(rrr.ContractID, rrr.HostKey, rrr.SiamuxAddr) - renewed, txnSet, err = h.Renew(ctx, rrr) + renewed, txnSet, contractPrice, err = h.Renew(ctx, rrr) return err })) != nil { return @@ -760,6 +761,7 @@ func (w *worker) rhpRenewHandler(jc jape.Context) { jc.Encode(api.RHPRenewResponse{ ContractID: renewed.ID(), Contract: renewed, + ContractPrice: contractPrice, TransactionSet: txnSet, }) }