diff --git a/api/contract.go b/api/contract.go index ae0cf2956..29f75ea28 100644 --- a/api/contract.go +++ b/api/contract.go @@ -206,9 +206,9 @@ func (c Contract) RenterFunds() types.Currency { } // RemainingCollateral returns the remaining collateral in the contract. -func (c Contract) RemainingCollateral(s rhpv2.HostSettings) types.Currency { - if c.Revision.MissedHostPayout().Cmp(s.ContractPrice) < 0 { +func (c Contract) RemainingCollateral() types.Currency { + if c.Revision.MissedHostPayout().Cmp(c.ContractPrice) < 0 { return types.ZeroCurrency } - return c.Revision.MissedHostPayout().Sub(s.ContractPrice) + return c.Revision.MissedHostPayout().Sub(c.ContractPrice) } diff --git a/autopilot/contractor.go b/autopilot/contractor.go index eb2b66763..16666d16c 100644 --- a/autopilot/contractor.go +++ b/autopilot/contractor.go @@ -1441,10 +1441,15 @@ func (c *contractor) refreshContract(ctx context.Context, w Worker, ci contractI } // calculate the renter funds - renterFunds, err := c.refreshFundingEstimate(ctx, state.cfg, ci, state.fee) - if err != nil { - c.logger.Errorw(fmt.Sprintf("could not get refresh funding estimate, err: %v", err), "hk", hk, "fcid", fcid) - return api.ContractMetadata{}, true, err + var renterFunds types.Currency + if isOutOfFunds(state.cfg, ci.settings, ci.contract) { + renterFunds, err = c.refreshFundingEstimate(ctx, state.cfg, ci, state.fee) + if err != nil { + c.logger.Errorw(fmt.Sprintf("could not get refresh funding estimate, err: %v", err), "hk", hk, "fcid", fcid) + return api.ContractMetadata{}, true, err + } + } else { + renterFunds = rev.ValidRenterPayout() // don't increase funds } // check our budget @@ -1453,14 +1458,30 @@ func (c *contractor) refreshContract(ctx context.Context, w Worker, ci contractI return api.ContractMetadata{}, false, fmt.Errorf("insufficient budget: %s < %s", budget.String(), renterFunds.String()) } - // calculate the new collateral expectedStorage := renterFundsToExpectedStorage(renterFunds, contract.EndHeight()-cs.BlockHeight, ci.priceTable) unallocatedCollateral := rev.MissedHostPayout().Sub(contract.ContractPrice) - minNewCollateral := minNewCollateral(unallocatedCollateral) + + // calculate the expected new collateral to determine the minNewCollateral. + // If the contract isn't below the min collateral, we don't enforce a + // minimum. + var minNewColl types.Currency + _, _, expectedNewCollateral := rhpv3.RenewalCosts(contract.Revision.FileContract, ci.priceTable, expectedStorage, contract.EndHeight()) + if isBelowCollateralThreshold(expectedNewCollateral, unallocatedCollateral) { + minNewColl = minNewCollateral(unallocatedCollateral) + } // renew the contract - resp, err := w.RHPRenew(ctx, contract.ID, contract.EndHeight(), hk, contract.SiamuxAddr, settings.Address, state.address, renterFunds, minNewCollateral, expectedStorage, settings.WindowSize) + resp, err := w.RHPRenew(ctx, contract.ID, contract.EndHeight(), hk, contract.SiamuxAddr, settings.Address, state.address, renterFunds, minNewColl, expectedStorage, settings.WindowSize) if err != nil { + if strings.Contains(err.Error(), "new collateral is too low") { + c.logger.Debugw("refresh failed: contract wouldn't have enough collateral after refresh", + "hk", hk, + "fcid", fcid, + "unallocatedCollateral", unallocatedCollateral.String(), + "minNewCollateral", minNewColl.String(), + ) + return api.ContractMetadata{}, true, err + } c.logger.Errorw("refresh failed", zap.Error(err), "hk", hk, "fcid", fcid) if strings.Contains(err.Error(), wallet.ErrInsufficientBalance.Error()) { return api.ContractMetadata{}, false, err @@ -1484,6 +1505,7 @@ func (c *contractor) refreshContract(ctx context.Context, w Worker, ci contractI "fcid", refreshedContract.ID, "renewedFrom", contract.ID, "renterFunds", renterFunds.String(), + "minNewCollateral", minNewColl.String(), "newCollateral", newCollateral.String(), ) return refreshedContract, true, nil diff --git a/autopilot/hostfilter.go b/autopilot/hostfilter.go index 22ddb9bae..a8a674e0a 100644 --- a/autopilot/hostfilter.go +++ b/autopilot/hostfilter.go @@ -313,7 +313,7 @@ func isOutOfCollateral(c api.Contract, s rhpv2.HostSettings, pt rhpv3.HostPriceT expectedStorage = s.RemainingStorage } _, _, newCollateral := rhpv3.RenewalCosts(c.Revision.FileContract, pt, expectedStorage, c.EndHeight()) - return isBelowCollateralThreshold(newCollateral, c.RemainingCollateral(s)) + return isBelowCollateralThreshold(newCollateral, c.RemainingCollateral()) } // isBelowCollateralThreshold returns true if the remainingCollateral is below a diff --git a/cmd/renterd/main.go b/cmd/renterd/main.go index 9a7004999..a884a3c42 100644 --- a/cmd/renterd/main.go +++ b/cmd/renterd/main.go @@ -296,7 +296,7 @@ func main() { flag.Parse() - log.Println("renterd v0.6.0") + log.Println("renterd v0.7.1") log.Println("Network", build.NetworkName()) if flag.Arg(0) == "version" { log.Println("Commit:", githash) diff --git a/worker/host.go b/worker/host.go index b5e4b7429..33f905a00 100644 --- a/worker/host.go +++ b/worker/host.go @@ -172,8 +172,7 @@ func (h *host) RenewContract(ctx context.Context, rrr api.RHPRenewRequest) (_ rh // for it. _, 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) + rev, txnSet, contractPrice, renewErr = RPCRenew(ctx, rrr, h.bus, t, pt, *revision, h.renterKey, h.logger) return rhpv3.HostPriceTable{}, nil, nil }) return err diff --git a/worker/rhpv3.go b/worker/rhpv3.go index ff45390f9..effc9a63e 100644 --- a/worker/rhpv3.go +++ b/worker/rhpv3.go @@ -1121,11 +1121,11 @@ func RPCAppendSector(ctx context.Context, t *transportV3, renterKey types.Privat return } -func RPCRenew(ctx context.Context, rrr api.RHPRenewRequest, bus Bus, t *transportV3, pt *rhpv3.HostPriceTable, rev types.FileContractRevision, renterKey types.PrivateKey, l *zap.SugaredLogger) (_ rhpv2.ContractRevision, _ []types.Transaction, err error) { +func RPCRenew(ctx context.Context, rrr api.RHPRenewRequest, bus Bus, t *transportV3, pt *rhpv3.HostPriceTable, rev types.FileContractRevision, renterKey types.PrivateKey, l *zap.SugaredLogger) (_ rhpv2.ContractRevision, _ []types.Transaction, _ types.Currency, err error) { defer wrapErr(&err, "RPCRenew") s, err := t.DialStream(ctx) if err != nil { - return rhpv2.ContractRevision{}, nil, fmt.Errorf("failed to dial stream: %w", err) + return rhpv2.ContractRevision{}, nil, types.ZeroCurrency, fmt.Errorf("failed to dial stream: %w", err) } defer s.Close() @@ -1135,7 +1135,7 @@ func RPCRenew(ctx context.Context, rrr api.RHPRenewRequest, bus Bus, t *transpor ptUID = pt.UID } if err = s.WriteRequest(rhpv3.RPCRenewContractID, &ptUID); err != nil { - return rhpv2.ContractRevision{}, nil, fmt.Errorf("failed to send ptUID: %w", err) + return rhpv2.ContractRevision{}, nil, types.Currency{}, fmt.Errorf("failed to send ptUID: %w", err) } // If we didn't have a valid pricetable, read the temporary one from the @@ -1143,28 +1143,28 @@ func RPCRenew(ctx context.Context, rrr api.RHPRenewRequest, bus Bus, t *transpor if ptUID == (rhpv3.SettingsID{}) { var ptResp rhpv3.RPCUpdatePriceTableResponse if err = s.ReadResponse(&ptResp, defaultRPCResponseMaxSize); err != nil { - return rhpv2.ContractRevision{}, nil, fmt.Errorf("failed to read RPCUpdatePriceTableResponse: %w", err) + return rhpv2.ContractRevision{}, nil, types.Currency{}, fmt.Errorf("failed to read RPCUpdatePriceTableResponse: %w", err) } pt = new(rhpv3.HostPriceTable) if err = json.Unmarshal(ptResp.PriceTableJSON, pt); err != nil { - return rhpv2.ContractRevision{}, nil, fmt.Errorf("failed to unmarshal price table: %w", err) + return rhpv2.ContractRevision{}, nil, types.Currency{}, fmt.Errorf("failed to unmarshal price table: %w", err) } } // Perform gouging checks. gc, err := GougingCheckerFromContext(ctx, false) if err != nil { - return rhpv2.ContractRevision{}, nil, fmt.Errorf("failed to get gouging checker: %w", err) + return rhpv2.ContractRevision{}, nil, types.Currency{}, fmt.Errorf("failed to get gouging checker: %w", err) } if breakdown := gc.Check(nil, pt); breakdown.Gouging() { - return rhpv2.ContractRevision{}, nil, fmt.Errorf("host gouging during renew: %v", breakdown) + return rhpv2.ContractRevision{}, nil, types.Currency{}, fmt.Errorf("host gouging during renew: %v", breakdown) } // 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.MinNewCollateral, *pt, rrr.EndHeight, rrr.WindowSize, rrr.ExpectedNewStorage) if err != nil { - return rhpv2.ContractRevision{}, nil, fmt.Errorf("failed to prepare renew: %w", err) + return rhpv2.ContractRevision{}, nil, types.Currency{}, fmt.Errorf("failed to prepare renew: %w", err) } // Starting from here, we need to make sure to release the txn on error. @@ -1187,13 +1187,13 @@ func RPCRenew(ctx context.Context, rrr api.RHPRenewRequest, bus Bus, t *transpor FinalRevisionSignature: finalRevisionSignature, } if err = s.WriteResponse(&req); err != nil { - return rhpv2.ContractRevision{}, nil, fmt.Errorf("failed to send RPCRenewContractRequest: %w", err) + return rhpv2.ContractRevision{}, nil, types.Currency{}, fmt.Errorf("failed to send RPCRenewContractRequest: %w", err) } // Incorporate the host's additions. var hostAdditions rhpv3.RPCRenewContractHostAdditions if err = s.ReadResponse(&hostAdditions, defaultRPCResponseMaxSize); err != nil { - return rhpv2.ContractRevision{}, nil, fmt.Errorf("failed to read RPCRenewContractHostAdditions: %w", err) + return rhpv2.ContractRevision{}, nil, types.Currency{}, fmt.Errorf("failed to read RPCRenewContractHostAdditions: %w", err) } parents = append(parents, hostAdditions.Parents...) txn.SiacoinInputs = append(txn.SiacoinInputs, hostAdditions.SiacoinInputs...) @@ -1225,7 +1225,7 @@ func RPCRenew(ctx context.Context, rrr api.RHPRenewRequest, bus Bus, t *transpor Signatures: []uint64{0, 1}, } if err := bus.WalletSign(ctx, &txn, wprr.ToSign, cf); err != nil { - return rhpv2.ContractRevision{}, nil, fmt.Errorf("failed to sign transaction: %w", err) + return rhpv2.ContractRevision{}, nil, types.Currency{}, fmt.Errorf("failed to sign transaction: %w", err) } // Create a new no-op revision and sign it. @@ -1249,13 +1249,13 @@ func RPCRenew(ctx context.Context, rrr api.RHPRenewRequest, bus Bus, t *transpor RevisionSignature: renterNoOpRevisionSignature, } if err = s.WriteResponse(&rs); err != nil { - return rhpv2.ContractRevision{}, nil, fmt.Errorf("failed to send RPCRenewSignatures: %w", err) + return rhpv2.ContractRevision{}, nil, types.Currency{}, fmt.Errorf("failed to send RPCRenewSignatures: %w", err) } // Receive the host's signatures. var hostSigs rhpv3.RPCRenewSignatures if err = s.ReadResponse(&hostSigs, defaultRPCResponseMaxSize); err != nil { - return rhpv2.ContractRevision{}, nil, fmt.Errorf("failed to read RPCRenewSignatures: %w", err) + return rhpv2.ContractRevision{}, nil, types.Currency{}, fmt.Errorf("failed to read RPCRenewSignatures: %w", err) } txn.Signatures = append(txn.Signatures, hostSigs.TransactionSignatures...) @@ -1265,7 +1265,7 @@ func RPCRenew(ctx context.Context, rrr api.RHPRenewRequest, bus Bus, t *transpor return rhpv2.ContractRevision{ Revision: noOpRevision, Signatures: [2]types.TransactionSignature{renterNoOpRevisionSignature, hostSigs.RevisionSignature}, - }, txnSet, nil + }, txnSet, pt.ContractPrice, nil } // initialRevision returns the first revision of a file contract formation