diff --git a/api/contract.go b/api/contract.go index b012582e2..ea90c722f 100644 --- a/api/contract.go +++ b/api/contract.go @@ -167,6 +167,16 @@ type ( LockID uint64 `json:"lockID"` } + // ContractRenewRequest is the request type for the /contract/:id/renew + // endpoint. + ContractRenewRequest struct { + EndHeight uint64 `json:"endHeight"` + ExpectedNewStorage uint64 `json:"expectedNewStorage"` + MaxFundAmount types.Currency `json:"maxFundAmount"` + MinNewCollateral types.Currency `json:"minNewCollateral"` + RenterFunds types.Currency `json:"renterFunds"` + } + // ContractRenewedRequest is the request type for the /contract/:id/renewed // endpoint. ContractRenewedRequest struct { diff --git a/api/wallet.go b/api/wallet.go index d2ddbc857..ad8acb56d 100644 --- a/api/wallet.go +++ b/api/wallet.go @@ -5,7 +5,6 @@ import ( "net/url" "time" - rhpv3 "go.sia.tech/core/rhp/v3" "go.sia.tech/core/types" ) @@ -44,30 +43,6 @@ type ( DependsOn []types.Transaction `json:"dependsOn"` } - // WalletPrepareRenewRequest is the request type for the /wallet/prepare/renew - // endpoint. - WalletPrepareRenewRequest struct { - Revision types.FileContractRevision `json:"revision"` - EndHeight uint64 `json:"endHeight"` - ExpectedNewStorage uint64 `json:"expectedNewStorage"` - HostAddress types.Address `json:"hostAddress"` - PriceTable rhpv3.HostPriceTable `json:"priceTable"` - MaxFundAmount types.Currency `json:"maxFundAmount"` - MinNewCollateral types.Currency `json:"minNewCollateral"` - RenterAddress types.Address `json:"renterAddress"` - RenterFunds types.Currency `json:"renterFunds"` - RenterKey types.PrivateKey `json:"renterKey"` - WindowSize uint64 `json:"windowSize"` - } - - // WalletPrepareRenewResponse is the response type for the /wallet/prepare/renew - // endpoint. - WalletPrepareRenewResponse struct { - FundAmount types.Currency `json:"fundAmount"` - ToSign []types.Hash256 `json:"toSign"` - TransactionSet []types.Transaction `json:"transactionSet"` - } - // WalletRedistributeRequest is the request type for the /wallet/redistribute // endpoint. WalletRedistributeRequest struct { diff --git a/api/worker.go b/api/worker.go index b8b2b9a93..ab4aec5dd 100644 --- a/api/worker.go +++ b/api/worker.go @@ -108,31 +108,6 @@ type ( Timeout DurationMS `json:"timeout"` } - // RHPRenewRequest is the request type for the /rhp/renew endpoint. - RHPRenewRequest struct { - ContractID types.FileContractID `json:"contractID"` - EndHeight uint64 `json:"endHeight"` - ExpectedNewStorage uint64 `json:"expectedNewStorage"` - HostAddress types.Address `json:"hostAddress"` - HostKey types.PublicKey `json:"hostKey"` - MaxFundAmount types.Currency `json:"maxFundAmount"` - 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. - RHPRenewResponse struct { - Error string `json:"error"` - ContractID types.FileContractID `json:"contractID"` - Contract rhpv2.ContractRevision `json:"contract"` - ContractPrice types.Currency `json:"contractPrice"` - FundAmount types.Currency `json:"fundAmount"` - TransactionSet []types.Transaction `json:"transactionSet"` - } - // RHPScanRequest is the request type for the /rhp/scan endpoint. RHPScanRequest struct { HostKey types.PublicKey `json:"hostKey"` diff --git a/autopilot/autopilot.go b/autopilot/autopilot.go index 43814fb83..c54fefa6c 100644 --- a/autopilot/autopilot.go +++ b/autopilot/autopilot.go @@ -10,7 +10,6 @@ import ( "sync" "time" - rhpv2 "go.sia.tech/core/rhp/v2" "go.sia.tech/core/types" "go.sia.tech/jape" "go.sia.tech/renterd/alerts" @@ -40,13 +39,13 @@ type Bus interface { ConsensusState(ctx context.Context) (api.ConsensusState, error) // contracts - AddRenewedContract(ctx context.Context, c rhpv2.ContractRevision, contractPrice, totalCost types.Currency, startHeight uint64, renewedFrom types.FileContractID, state string) (api.ContractMetadata, error) AncestorContracts(ctx context.Context, id types.FileContractID, minStartHeight uint64) ([]api.ArchivedContract, error) ArchiveContracts(ctx context.Context, toArchive map[types.FileContractID]string) error Contract(ctx context.Context, id types.FileContractID) (api.ContractMetadata, error) Contracts(ctx context.Context, opts api.ContractsOpts) (contracts []api.ContractMetadata, err error) FileContractTax(ctx context.Context, payout types.Currency) (types.Currency, error) FormContract(ctx context.Context, renterAddress types.Address, renterFunds types.Currency, hostKey types.PublicKey, hostIP string, hostCollateral types.Currency, endHeight uint64) (api.ContractMetadata, error) + RenewContract(ctx context.Context, fcid types.FileContractID, endHeight uint64, renterFunds, minNewCollateral, maxFundAmount types.Currency, expectedNewStorage uint64) (api.ContractMetadata, error) SetContractSet(ctx context.Context, set string, contracts []types.FileContractID) error PrunableData(ctx context.Context) (prunableData api.ContractsPrunableDataResponse, err error) diff --git a/autopilot/contractor/contractor.go b/autopilot/contractor/contractor.go index d384c88f3..a886f7cc2 100644 --- a/autopilot/contractor/contractor.go +++ b/autopilot/contractor/contractor.go @@ -81,7 +81,6 @@ const ( ) type Bus interface { - AddRenewedContract(ctx context.Context, c rhpv2.ContractRevision, contractPrice, totalCost types.Currency, startHeight uint64, renewedFrom types.FileContractID, state string) (api.ContractMetadata, error) AncestorContracts(ctx context.Context, id types.FileContractID, minStartHeight uint64) ([]api.ArchivedContract, error) ArchiveContracts(ctx context.Context, toArchive map[types.FileContractID]string) error ConsensusState(ctx context.Context) (api.ConsensusState, error) @@ -89,6 +88,7 @@ type Bus interface { Contracts(ctx context.Context, opts api.ContractsOpts) (contracts []api.ContractMetadata, err error) FileContractTax(ctx context.Context, payout types.Currency) (types.Currency, error) FormContract(ctx context.Context, renterAddress types.Address, renterFunds types.Currency, hostKey types.PublicKey, hostIP string, hostCollateral types.Currency, endHeight uint64) (api.ContractMetadata, error) + RenewContract(ctx context.Context, fcid types.FileContractID, endHeight uint64, renterFunds, minNewCollateral, maxFundAmount types.Currency, expectedNewStorage uint64) (api.ContractMetadata, error) Host(ctx context.Context, hostKey types.PublicKey) (api.Host, error) RecordContractSetChurnMetric(ctx context.Context, metrics ...api.ContractSetChurnMetric) error SearchHosts(ctx context.Context, opts api.SearchHostOptions) ([]api.Host, error) @@ -100,7 +100,6 @@ type Worker interface { Contracts(ctx context.Context, hostTimeout time.Duration) (api.ContractsResponse, error) RHPBroadcast(ctx context.Context, fcid types.FileContractID) (err error) RHPPriceTable(ctx context.Context, hostKey types.PublicKey, siamuxAddr string, timeout time.Duration) (api.HostPriceTable, error) - RHPRenew(ctx context.Context, fcid types.FileContractID, endHeight uint64, hk types.PublicKey, hostIP string, hostAddress, renterAddress types.Address, renterFunds, minNewCollateral, maxFundAmount types.Currency, expectedNewStorage, windowSize uint64) (api.RHPRenewResponse, error) RHPScan(ctx context.Context, hostKey types.PublicKey, hostIP string, timeout time.Duration) (api.RHPScanResponse, error) } @@ -307,7 +306,7 @@ func (c *Contractor) refreshContract(ctx *mCtx, w Worker, contract api.Contract, maxFundAmount := budget.Add(rev.ValidRenterPayout()) // renew the contract - resp, err := w.RHPRenew(ctx, contract.ID, contract.EndHeight(), hk, contract.SiamuxAddr, settings.Address, ctx.state.Address, renterFunds, minNewCollateral, maxFundAmount, expectedNewStorage, settings.WindowSize) + renewal, err := c.bus.RenewContract(ctx, contract.ID, contract.EndHeight(), renterFunds, minNewCollateral, maxFundAmount, expectedNewStorage) if err != nil { if strings.Contains(err.Error(), "new collateral is too low") { logger.Infow("refresh failed: contract wouldn't have enough collateral after refresh", @@ -326,25 +325,16 @@ func (c *Contractor) refreshContract(ctx *mCtx, w Worker, contract api.Contract, } // update the budget - *budget = budget.Sub(resp.FundAmount) - - // persist the contract - refreshedContract, err := c.bus.AddRenewedContract(ctx, resp.Contract, resp.ContractPrice, renterFunds, cs.BlockHeight, contract.ID, api.ContractStatePending) - if err != nil { - logger.Errorw("adding refreshed contract failed", zap.Error(err), "hk", hk, "fcid", fcid) - return api.ContractMetadata{}, false, err - } + *budget = budget.Sub(renewal.TotalCost) // add to renewed set - newCollateral := resp.Contract.Revision.MissedHostPayout().Sub(resp.ContractPrice) logger.Infow("refresh succeeded", - "fcid", refreshedContract.ID, - "renewedFrom", contract.ID, + "fcid", renewal.ID, + "renewedFrom", renewal.RenewedFrom, "renterFunds", renterFunds.String(), "minNewCollateral", minNewCollateral.String(), - "newCollateral", newCollateral.String(), ) - return refreshedContract, true, nil + return renewal, true, nil } func (c *Contractor) renewContract(ctx *mCtx, w Worker, contract api.Contract, host api.Host, budget *types.Currency, logger *zap.SugaredLogger) (cm api.ContractMetadata, proceed bool, err error) { @@ -354,11 +344,9 @@ func (c *Contractor) renewContract(ctx *mCtx, w Worker, contract api.Contract, h logger = logger.With("to_renew", contract.ID, "hk", contract.HostKey, "hostVersion", host.Settings.Version, "hostRelease", host.Settings.Release) // convenience variables - settings := host.Settings pt := host.PriceTable.HostPriceTable fcid := contract.ID rev := contract.Revision - hk := contract.HostKey // fetch consensus state cs, err := c.bus.ConsensusState(ctx) @@ -388,7 +376,7 @@ func (c *Contractor) renewContract(ctx *mCtx, w Worker, contract api.Contract, h expectedNewStorage := renterFundsToExpectedStorage(renterFunds, endHeight-cs.BlockHeight, pt) // renew the contract - resp, err := w.RHPRenew(ctx, fcid, endHeight, hk, contract.SiamuxAddr, settings.Address, ctx.state.Address, renterFunds, types.ZeroCurrency, *budget, expectedNewStorage, settings.WindowSize) + renewal, err := c.bus.RenewContract(ctx, fcid, endHeight, renterFunds, types.ZeroCurrency, *budget, expectedNewStorage) if err != nil { logger.Errorw( "renewal failed", @@ -404,24 +392,15 @@ func (c *Contractor) renewContract(ctx *mCtx, w Worker, contract api.Contract, h } // update the budget - *budget = budget.Sub(resp.FundAmount) - - // persist the contract - renewedContract, err := c.bus.AddRenewedContract(ctx, resp.Contract, resp.ContractPrice, renterFunds, cs.BlockHeight, fcid, api.ContractStatePending) - if err != nil { - logger.Errorw(fmt.Sprintf("renewal failed to persist, err: %v", err)) - return api.ContractMetadata{}, false, err - } + *budget = budget.Sub(renewal.TotalCost) - newCollateral := resp.Contract.Revision.MissedHostPayout().Sub(resp.ContractPrice) logger.Infow( "renewal succeeded", - "fcid", renewedContract.ID, - "renewedFrom", fcid, + "fcid", renewal.ID, + "renewedFrom", renewal.RenewedFrom, "renterFunds", renterFunds.String(), - "newCollateral", newCollateral.String(), ) - return renewedContract, true, nil + return renewal, true, nil } // broadcastRevisions broadcasts contract revisions from the current set of diff --git a/autopilot/workerpool.go b/autopilot/workerpool.go index 1f23a3cb6..7220f7f4a 100644 --- a/autopilot/workerpool.go +++ b/autopilot/workerpool.go @@ -21,7 +21,6 @@ type Worker interface { RHPBroadcast(ctx context.Context, fcid types.FileContractID) (err error) RHPPriceTable(ctx context.Context, hostKey types.PublicKey, siamuxAddr string, timeout time.Duration) (api.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, minNewCollateral, maxFundAmount types.Currency, expectedStorage, windowSize uint64) (api.RHPRenewResponse, error) RHPScan(ctx context.Context, hostKey types.PublicKey, hostIP string, timeout time.Duration) (api.RHPScanResponse, error) } diff --git a/bus/bus.go b/bus/bus.go index 1807575e9..838628a59 100644 --- a/bus/bus.go +++ b/bus/bus.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "net" "net/http" "strings" @@ -16,6 +17,7 @@ import ( "go.sia.tech/core/consensus" "go.sia.tech/core/gateway" rhpv2 "go.sia.tech/core/rhp/v2" + rhpv3 "go.sia.tech/core/rhp/v3" "go.sia.tech/core/types" "go.sia.tech/coreutils/chain" "go.sia.tech/coreutils/syncer" @@ -25,8 +27,10 @@ import ( "go.sia.tech/renterd/api" "go.sia.tech/renterd/bus/client" ibus "go.sia.tech/renterd/internal/bus" + "go.sia.tech/renterd/internal/gouging" "go.sia.tech/renterd/internal/rhp" rhp2 "go.sia.tech/renterd/internal/rhp/v2" + rhp3 "go.sia.tech/renterd/internal/rhp/v3" "go.sia.tech/renterd/object" "go.sia.tech/renterd/stores/sql" "go.sia.tech/renterd/webhooks" @@ -38,6 +42,7 @@ const ( defaultWalletRecordMetricInterval = 5 * time.Minute defaultPinUpdateInterval = 5 * time.Minute defaultPinRateWindow = 6 * time.Hour + lockingPriorityRenew = 80 stdTxnSize = 1200 // bytes ) @@ -311,6 +316,7 @@ type Bus struct { ss SettingStore rhp2 *rhp2.Client + rhp3 *rhp3.Client contractLocker ContractLocker sectors UploadingSectorsCache @@ -343,6 +349,7 @@ func New(ctx context.Context, masterKey [32]byte, am AlertManager, wm WebhooksMa logger: l.Sugar(), rhp2: rhp2.New(rhp.NewFallbackDialer(store, net.Dialer{}, l), l), + rhp3: rhp3.New(rhp.NewFallbackDialer(store, net.Dialer{}, l), l), } // init settings @@ -411,6 +418,7 @@ func (b *Bus) Handler() http.Handler { "POST /contract/:id/acquire": b.contractAcquireHandlerPOST, "GET /contract/:id/ancestors": b.contractIDAncestorsHandler, "POST /contract/:id/keepalive": b.contractKeepaliveHandlerPOST, + "POST /contract/:id/renew": b.contractIDRenewHandlerPOST, "POST /contract/:id/renewed": b.contractIDRenewedHandlerPOST, "POST /contract/:id/release": b.contractReleaseHandlerPOST, "GET /contract/:id/roots": b.contractIDRootsHandlerGET, @@ -487,16 +495,15 @@ func (b *Bus) Handler() http.Handler { "DELETE /upload/:id": b.uploadFinishedHandlerDELETE, "POST /upload/:id/sector": b.uploadAddSectorHandlerPOST, - "GET /wallet": b.walletHandler, - "POST /wallet/discard": b.walletDiscardHandler, - "POST /wallet/fund": b.walletFundHandler, - "GET /wallet/outputs": b.walletOutputsHandler, - "GET /wallet/pending": b.walletPendingHandler, - "POST /wallet/prepare/renew": b.walletPrepareRenewHandler, - "POST /wallet/redistribute": b.walletRedistributeHandler, - "POST /wallet/send": b.walletSendSiacoinsHandler, - "POST /wallet/sign": b.walletSignHandler, - "GET /wallet/transactions": b.walletTransactionsHandler, + "GET /wallet": b.walletHandler, + "POST /wallet/discard": b.walletDiscardHandler, + "POST /wallet/fund": b.walletFundHandler, + "GET /wallet/outputs": b.walletOutputsHandler, + "GET /wallet/pending": b.walletPendingHandler, + "POST /wallet/redistribute": b.walletRedistributeHandler, + "POST /wallet/send": b.walletSendSiacoinsHandler, + "POST /wallet/sign": b.walletSignHandler, + "GET /wallet/transactions": b.walletTransactionsHandler, "GET /webhooks": b.webhookHandlerGet, "POST /webhooks": b.webhookHandlerPost, @@ -532,9 +539,22 @@ func (b *Bus) addContract(ctx context.Context, rev rhpv2.ContractRevision, contr return c, nil } -func (b *Bus) isPassedV2AllowHeight() bool { - cs := b.cm.TipState() - return cs.Index.Height >= cs.Network.HardforkV2.AllowHeight +func (b *Bus) addRenewedContract(ctx context.Context, renewedFrom types.FileContractID, rev rhpv2.ContractRevision, contractPrice, totalCost types.Currency, startHeight uint64, state string) (api.ContractMetadata, error) { + r, err := b.ms.AddRenewedContract(ctx, rev, contractPrice, totalCost, startHeight, renewedFrom, state) + if err != nil { + return api.ContractMetadata{}, err + } + + b.sectors.HandleRenewal(r.ID, r.RenewedFrom) + b.broadcastAction(webhooks.Event{ + Module: api.ModuleContract, + Event: api.EventRenew, + Payload: api.EventContractRenew{ + Renewal: r, + Timestamp: time.Now().UTC(), + }, + }) + return r, nil } func (b *Bus) formContract(ctx context.Context, hostSettings rhpv2.HostSettings, renterAddress types.Address, renterFunds, hostCollateral types.Currency, hostKey types.PublicKey, hostIP string, endHeight uint64) (rhpv2.ContractRevision, error) { @@ -694,6 +714,11 @@ func (b *Bus) initSettings(ctx context.Context) error { return nil } +func (b *Bus) isPassedV2AllowHeight() bool { + cs := b.cm.TipState() + return cs.Index.Height >= cs.Network.HardforkV2.AllowHeight +} + func (b *Bus) deriveRenterKey(hostKey types.PublicKey) types.PrivateKey { seed := blake2b.Sum256(append(b.deriveSubKey("renterkey"), hostKey[:]...)) pk := types.NewPrivateKeyFromSeed(seed[:]) @@ -711,3 +736,83 @@ func (b *Bus) deriveSubKey(purpose string) types.PrivateKey { } return pk } + +func (b *Bus) prepareRenew(cs consensus.State, revision types.FileContractRevision, hostAddress, renterAddress types.Address, renterFunds, minNewCollateral, maxFundAmount types.Currency, endHeight, expectedStorage uint64) rhp3.PrepareRenewFn { + return func(pt rhpv3.HostPriceTable) ([]types.Hash256, []types.Transaction, types.Currency, rhp3.DiscardTxnFn, error) { + // create the final revision from the provided revision + finalRevision := revision + finalRevision.MissedProofOutputs = finalRevision.ValidProofOutputs + finalRevision.Filesize = 0 + finalRevision.FileMerkleRoot = types.Hash256{} + finalRevision.RevisionNumber = math.MaxUint64 + + // prepare the new contract + fc, basePrice, err := rhpv3.PrepareContractRenewal(revision, hostAddress, renterAddress, renterFunds, minNewCollateral, pt, expectedStorage, endHeight) + if err != nil { + return nil, nil, types.ZeroCurrency, nil, fmt.Errorf("couldn't prepare contract renewal: %w", err) + } + + // prepare the transaction + txn := types.Transaction{ + FileContracts: []types.FileContract{fc}, + FileContractRevisions: []types.FileContractRevision{finalRevision}, + MinerFees: []types.Currency{pt.TxnFeeMaxRecommended.Mul64(4096)}, + } + + // compute how much renter funds to put into the new contract + fundAmount := rhpv3.ContractRenewalCost(cs, pt, fc, txn.MinerFees[0], basePrice) + + // make sure we don't exceed the max fund amount. + if maxFundAmount.Cmp(fundAmount) < 0 { + return nil, nil, types.ZeroCurrency, nil, fmt.Errorf("%w: %v > %v", api.ErrMaxFundAmountExceeded, fundAmount, maxFundAmount) + } + + // fund the transaction, we are not signing it yet since it's not + // complete. The host still needs to complete it and the revision + + // contract are signed with the renter key by the worker. + toSign, err := b.w.FundTransaction(&txn, fundAmount, true) + if err != nil { + return nil, nil, types.ZeroCurrency, nil, fmt.Errorf("couldn't fund transaction: %w", err) + } + + return toSign, append(b.cm.UnconfirmedParents(txn), txn), fundAmount, func(err *error) { + if *err == nil { + return + } + b.w.ReleaseInputs([]types.Transaction{txn}, nil) + }, nil + } +} + +func (b *Bus) renewContract(ctx context.Context, cs consensus.State, gp api.GougingParams, c api.ContractMetadata, hs rhpv2.HostSettings, renterFunds, minNewCollateral, maxFundAmount types.Currency, endHeight, expectedNewStorage uint64) (rhpv2.ContractRevision, types.Currency, types.Currency, error) { + // acquire contract lock indefinitely and defer the release + lockID, err := b.contractLocker.Acquire(ctx, lockingPriorityRenew, c.ID, time.Duration(math.MaxInt64)) + if err != nil { + return rhpv2.ContractRevision{}, types.ZeroCurrency, types.ZeroCurrency, fmt.Errorf("couldn't acquire contract lock; %w", err) + } + defer func() { + if err := b.contractLocker.Release(c.ID, lockID); err != nil { + b.logger.Error("failed to release contract lock", zap.Error(err)) + } + }() + + // fetch the revision + rev, err := b.rhp3.Revision(ctx, c.ID, c.HostKey, c.SiamuxAddr) + if err != nil { + return rhpv2.ContractRevision{}, types.ZeroCurrency, types.ZeroCurrency, fmt.Errorf("couldn't fetch revision; %w", err) + } + + // renew contract + gc := gouging.NewChecker(gp.GougingSettings, gp.ConsensusState, gp.TransactionFee, nil, nil) + renterKey := b.deriveRenterKey(c.HostKey) + prepareRenew := b.prepareRenew(cs, rev, hs.Address, b.w.Address(), renterFunds, minNewCollateral, maxFundAmount, endHeight, expectedNewStorage) + newRevision, txnSet, contractPrice, fundAmount, err := b.rhp3.Renew(ctx, gc, rev, renterKey, c.HostKey, c.SiamuxAddr, prepareRenew, b.w.SignTransaction) + if err != nil { + return rhpv2.ContractRevision{}, types.ZeroCurrency, types.ZeroCurrency, fmt.Errorf("couldn't renew contract; %w", err) + } + + // broadcast the transaction set + b.s.BroadcastTransactionSet(txnSet) + + return newRevision, contractPrice, fundAmount, nil +} diff --git a/bus/client/contracts.go b/bus/client/contracts.go index bb3b16b4c..a831cb8e7 100644 --- a/bus/client/contracts.go +++ b/bus/client/contracts.go @@ -160,6 +160,19 @@ func (c *Client) PrunableData(ctx context.Context) (prunableData api.ContractsPr return } +// RenewContract renews an existing contract with a host and adds it to the bus. +func (c *Client) RenewContract(ctx context.Context, contractID types.FileContractID, endHeight uint64, renterFunds, minNewCollateral, maxFundAmount types.Currency, expectedStorage uint64) (renewal api.ContractMetadata, err error) { + req := api.ContractRenewRequest{ + EndHeight: endHeight, + ExpectedNewStorage: expectedStorage, + MaxFundAmount: maxFundAmount, + MinNewCollateral: minNewCollateral, + RenterFunds: renterFunds, + } + err = c.c.WithContext(ctx).POST(fmt.Sprintf("/contract/%s/renew", contractID), req, &renewal) + return +} + // RenewedContract returns the renewed contract for the given ID. func (c *Client) RenewedContract(ctx context.Context, renewedFrom types.FileContractID) (contract api.ContractMetadata, err error) { err = c.c.WithContext(ctx).GET(fmt.Sprintf("/contracts/renewed/%s", renewedFrom), &contract) diff --git a/bus/client/wallet.go b/bus/client/wallet.go index 0fcc8d0b5..d91289b56 100644 --- a/bus/client/wallet.go +++ b/bus/client/wallet.go @@ -6,7 +6,6 @@ import ( "net/http" "net/url" - rhpv3 "go.sia.tech/core/rhp/v3" "go.sia.tech/core/types" "go.sia.tech/renterd/api" ) @@ -63,26 +62,6 @@ func (c *Client) WalletPending(ctx context.Context) (resp []types.Transaction, e return } -// WalletPrepareRenew funds and signs a contract renewal transaction. -func (c *Client) WalletPrepareRenew(ctx context.Context, revision types.FileContractRevision, hostAddress, renterAddress types.Address, renterKey types.PrivateKey, renterFunds, minNewCollateral, maxFundAmount types.Currency, pt rhpv3.HostPriceTable, endHeight, windowSize, expectedStorage uint64) (api.WalletPrepareRenewResponse, error) { - req := api.WalletPrepareRenewRequest{ - Revision: revision, - EndHeight: endHeight, - ExpectedNewStorage: expectedStorage, - HostAddress: hostAddress, - PriceTable: pt, - MaxFundAmount: maxFundAmount, - MinNewCollateral: minNewCollateral, - RenterAddress: renterAddress, - RenterFunds: renterFunds, - RenterKey: renterKey, - WindowSize: windowSize, - } - var resp api.WalletPrepareRenewResponse - err := c.c.WithContext(ctx).POST("/wallet/prepare/renew", req, &resp) - return resp, err -} - // WalletRedistribute broadcasts a transaction that redistributes the money in // the wallet in the desired number of outputs of given amount. If the // transaction was successfully broadcasted it will return the transaction ID. diff --git a/bus/routes.go b/bus/routes.go index 4eee7ce44..f1302fef1 100644 --- a/bus/routes.go +++ b/bus/routes.go @@ -14,7 +14,6 @@ import ( "time" rhpv2 "go.sia.tech/core/rhp/v2" - rhpv3 "go.sia.tech/core/rhp/v3" ibus "go.sia.tech/renterd/internal/bus" "go.sia.tech/renterd/internal/gouging" @@ -481,63 +480,6 @@ func (b *Bus) walletDiscardHandler(jc jape.Context) { } } -func (b *Bus) walletPrepareRenewHandler(jc jape.Context) { - var wprr api.WalletPrepareRenewRequest - if jc.Decode(&wprr) != nil { - return - } - if wprr.RenterKey == nil { - jc.Error(errors.New("no renter key provided"), http.StatusBadRequest) - return - } - cs := b.cm.TipState() - - // Create the final revision from the provided revision. - finalRevision := wprr.Revision - finalRevision.MissedProofOutputs = finalRevision.ValidProofOutputs - finalRevision.Filesize = 0 - finalRevision.FileMerkleRoot = types.Hash256{} - finalRevision.RevisionNumber = math.MaxUint64 - - // Prepare the new contract. - fc, basePrice, err := rhpv3.PrepareContractRenewal(wprr.Revision, wprr.HostAddress, wprr.RenterAddress, wprr.RenterFunds, wprr.MinNewCollateral, wprr.PriceTable, wprr.ExpectedNewStorage, wprr.EndHeight) - if jc.Check("couldn't prepare contract renewal", err) != nil { - return - } - - // Create the transaction containing both the final revision and new - // contract. - txn := types.Transaction{ - FileContracts: []types.FileContract{fc}, - FileContractRevisions: []types.FileContractRevision{finalRevision}, - MinerFees: []types.Currency{wprr.PriceTable.TxnFeeMaxRecommended.Mul64(4096)}, - } - - // Compute how much renter funds to put into the new contract. - cost := rhpv3.ContractRenewalCost(cs, wprr.PriceTable, fc, txn.MinerFees[0], basePrice) - - // Make sure we don't exceed the max fund amount. - // TODO: remove the IsZero check for the v2 change - if /*!wprr.MaxFundAmount.IsZero() &&*/ wprr.MaxFundAmount.Cmp(cost) < 0 { - jc.Error(fmt.Errorf("%w: %v > %v", api.ErrMaxFundAmountExceeded, cost, wprr.MaxFundAmount), http.StatusBadRequest) - return - } - - // Fund the txn. We are not signing it yet since it's not complete. The host - // still needs to complete it and the revision + contract are signed with - // the renter key by the worker. - toSign, err := b.w.FundTransaction(&txn, cost, true) - if jc.Check("couldn't fund transaction", err) != nil { - return - } - - jc.Encode(api.WalletPrepareRenewResponse{ - FundAmount: cost, - ToSign: toSign, - TransactionSet: append(b.cm.UnconfirmedParents(txn), txn), - }) -} - func (b *Bus) walletPendingHandler(jc jape.Context) { isRelevant := func(txn types.Transaction) bool { addr := b.w.Address() @@ -955,6 +897,86 @@ func (b *Bus) contractIDHandlerPOST(jc jape.Context) { jc.Encode(a) } +func (b *Bus) contractIDRenewHandlerPOST(jc jape.Context) { + // apply pessimistic timeout + ctx, cancel := context.WithTimeout(jc.Request.Context(), 15*time.Minute) + defer cancel() + + // decode contract id + var fcid types.FileContractID + if jc.DecodeParam("id", &fcid) != nil { + return + } + + // decode request + var rrr api.ContractRenewRequest + if jc.Decode(&rrr) != nil { + return + } + + // validate the request + if rrr.EndHeight == 0 { + http.Error(jc.ResponseWriter, "EndHeight can not be zero", http.StatusBadRequest) + } else if rrr.ExpectedNewStorage == 0 { + http.Error(jc.ResponseWriter, "ExpectedNewStorage can not be zero", http.StatusBadRequest) + } else if rrr.MaxFundAmount.IsZero() { + http.Error(jc.ResponseWriter, "MaxFundAmount can not be zero", http.StatusBadRequest) + } else if rrr.MinNewCollateral.IsZero() { + http.Error(jc.ResponseWriter, "MinNewCollateral can not be zero", http.StatusBadRequest) + } else if rrr.RenterFunds.IsZero() { + http.Error(jc.ResponseWriter, "RenterFunds can not be zero", http.StatusBadRequest) + return + } + + // fetch the contract + c, err := b.ms.Contract(ctx, fcid) + if errors.Is(err, api.ErrContractNotFound) { + jc.Error(err, http.StatusNotFound) + return + } else if jc.Check("couldn't fetch contract", err) != nil { + return + } + + // fetch the host + h, err := b.hs.Host(ctx, c.HostKey) + if jc.Check("couldn't fetch host", err) != nil { + return + } + + // fetch consensus state + cs := b.cm.TipState() + + // fetch gouging parameters + gp, err := b.gougingParams(ctx) + if jc.Check("could not get gouging parameters", err) != nil { + return + } + + // send V2 transaction if we're passed the V2 hardfork allow height + var newRevision rhpv2.ContractRevision + var contractPrice, fundAmount types.Currency + if b.isPassedV2AllowHeight() { + panic("not implemented") + } else { + newRevision, contractPrice, fundAmount, err = b.renewContract(ctx, cs, gp, c, h.Settings, rrr.RenterFunds, rrr.MinNewCollateral, rrr.MaxFundAmount, rrr.EndHeight, rrr.ExpectedNewStorage) + if errors.Is(err, api.ErrMaxFundAmountExceeded) { + jc.Error(err, http.StatusBadRequest) + return + } else if jc.Check("couldn't renew contract", err) != nil { + return + } + } + + // add renewal contract to store + metadata, err := b.addRenewedContract(ctx, fcid, newRevision, contractPrice, fundAmount, cs.Index.Height, api.ContractStatePending) + if jc.Check("couldn't store contract", err) != nil { + return + } + + // send the response + jc.Encode(metadata) +} + func (b *Bus) contractIDRenewedHandlerPOST(jc jape.Context) { var id types.FileContractID var req api.ContractRenewedRequest @@ -972,21 +994,11 @@ func (b *Bus) contractIDRenewedHandlerPOST(jc jape.Context) { if req.State == "" { req.State = api.ContractStatePending } - r, err := b.ms.AddRenewedContract(jc.Request.Context(), req.Contract, req.ContractPrice, req.TotalCost, req.StartHeight, req.RenewedFrom, req.State) + r, err := b.addRenewedContract(jc.Request.Context(), req.RenewedFrom, req.Contract, req.ContractPrice, req.TotalCost, req.StartHeight, req.State) if jc.Check("couldn't store contract", err) != nil { return } - b.sectors.HandleRenewal(req.Contract.ID(), req.RenewedFrom) - b.broadcastAction(webhooks.Event{ - Module: api.ModuleContract, - Event: api.EventRenew, - Payload: api.EventContractRenew{ - Renewal: r, - Timestamp: time.Now().UTC(), - }, - }) - jc.Encode(r) } diff --git a/internal/rhp/v3/rhp.go b/internal/rhp/v3/rhp.go index 5ae5d9972..44d4c91bc 100644 --- a/internal/rhp/v3/rhp.go +++ b/internal/rhp/v3/rhp.go @@ -159,9 +159,9 @@ func (c *Client) FundAccount(ctx context.Context, rev *types.FileContractRevisio }) } -func (c *Client) Renew(ctx context.Context, rrr api.RHPRenewRequest, gougingChecker gouging.Checker, renewer PrepareRenewFunc, signer SignFunc, rev types.FileContractRevision, renterKey types.PrivateKey) (newRev rhpv2.ContractRevision, txnSet []types.Transaction, contractPrice, fundAmount types.Currency, err error) { - err = c.tpool.withTransport(ctx, rrr.HostKey, rrr.SiamuxAddr, func(ctx context.Context, t *transportV3) error { - newRev, txnSet, contractPrice, fundAmount, err = rpcRenew(ctx, rrr, gougingChecker, renewer, signer, t, rev, renterKey) +func (c *Client) Renew(ctx context.Context, gc gouging.Checker, rev types.FileContractRevision, renterKey types.PrivateKey, hostKey types.PublicKey, hostSiamuxAddr string, renewTxnFn PrepareRenewFn, signTxnFn SignTxnFn) (newRev rhpv2.ContractRevision, txnSet []types.Transaction, contractPrice, fundAmount types.Currency, err error) { + err = c.tpool.withTransport(ctx, hostKey, hostSiamuxAddr, func(ctx context.Context, t *transportV3) error { + newRev, txnSet, contractPrice, fundAmount, err = rpcRenew(ctx, t, gc, rev, renterKey, renewTxnFn, signTxnFn) return err }) return diff --git a/internal/rhp/v3/rpc.go b/internal/rhp/v3/rpc.go index 746cac5fe..db9c57469 100644 --- a/internal/rhp/v3/rpc.go +++ b/internal/rhp/v3/rpc.go @@ -26,9 +26,9 @@ type ( // gouging checks before paying for the table. PriceTablePaymentFunc func(pt rhpv3.HostPriceTable) (rhpv3.PaymentMethod, error) - PrepareRenewFunc func(ctx context.Context, revision types.FileContractRevision, hostAddress, renterAddress types.Address, renterKey types.PrivateKey, renterFunds, minNewCollateral, maxFundAmount types.Currency, pt rhpv3.HostPriceTable, endHeight, windowSize, expectedStorage uint64) (resp api.WalletPrepareRenewResponse, discard func(context.Context, types.Transaction, *error), err error) - - SignFunc func(ctx context.Context, txn *types.Transaction, toSign []types.Hash256, cf types.CoveredFields) error + DiscardTxnFn func(err *error) + PrepareRenewFn func(pt rhpv3.HostPriceTable) (toSign []types.Hash256, txnSet []types.Transaction, fundAmount types.Currency, discard DiscardTxnFn, err error) + SignTxnFn func(txn *types.Transaction, toSign []types.Hash256, cf types.CoveredFields) ) // rpcPriceTable calls the UpdatePriceTable RPC. @@ -343,7 +343,7 @@ func rpcAppendSector(ctx context.Context, t *transportV3, renterKey types.Privat return } -func rpcRenew(ctx context.Context, rrr api.RHPRenewRequest, gougingChecker gouging.Checker, prepareRenew PrepareRenewFunc, signTxn SignFunc, t *transportV3, rev types.FileContractRevision, renterKey types.PrivateKey) (_ rhpv2.ContractRevision, _ []types.Transaction, _, _ types.Currency, err error) { +func rpcRenew(ctx context.Context, t *transportV3, gc gouging.Checker, rev types.FileContractRevision, renterKey types.PrivateKey, prepareTxnFn PrepareRenewFn, signTxnFn SignTxnFn) (_ rhpv2.ContractRevision, _ []types.Transaction, _, _ types.Currency, err error) { defer utils.WrapErr(ctx, "RPCRenew", &err) s, err := t.DialStream(ctx) @@ -368,21 +368,20 @@ func rpcRenew(ctx context.Context, rrr api.RHPRenewRequest, gougingChecker gougi } // Perform gouging checks. - if breakdown := gougingChecker.Check(nil, &pt); breakdown.Gouging() { + if breakdown := gc.Check(nil, &pt); breakdown.Gouging() { return rhpv2.ContractRevision{}, nil, types.Currency{}, 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, discard, err := prepareRenew(ctx, rev, rrr.HostAddress, rrr.RenterAddress, renterKey, rrr.RenterFunds, rrr.MinNewCollateral, rrr.MaxFundAmount, pt, rrr.EndHeight, rrr.WindowSize, rrr.ExpectedNewStorage) + toSign, txnSet, fundAmount, discard, err := prepareTxnFn(pt) if err != nil { return rhpv2.ContractRevision{}, nil, types.Currency{}, types.Currency{}, fmt.Errorf("failed to prepare renew: %w", err) } // Starting from here, we need to make sure to release the txn on error. - defer discard(ctx, wprr.TransactionSet[len(wprr.TransactionSet)-1], &err) + defer discard(&err) - txnSet := wprr.TransactionSet parents, txn := txnSet[:len(txnSet)-1], txnSet[len(txnSet)-1] // Sign only the revision and contract. We can't sign everything because @@ -436,9 +435,7 @@ func rpcRenew(ctx context.Context, rrr api.RHPRenewRequest, gougingChecker gougi WholeTransaction: true, Signatures: []uint64{0, 1}, } - if err := signTxn(ctx, &txn, wprr.ToSign, cf); err != nil { - return rhpv2.ContractRevision{}, nil, types.Currency{}, types.Currency{}, fmt.Errorf("failed to sign transaction: %w", err) - } + signTxnFn(&txn, toSign, cf) // Create a new no-op revision and sign it. noOpRevision := initialRevision(txn, rev.UnlockConditions.PublicKeys[1], renterKey.PublicKey().UnlockKey()) @@ -478,7 +475,7 @@ func rpcRenew(ctx context.Context, rrr api.RHPRenewRequest, gougingChecker gougi return rhpv2.ContractRevision{ Revision: noOpRevision, Signatures: [2]types.TransactionSignature{renterNoOpRevisionSignature, hostSigs.RevisionSignature}, - }, txnSet, pt.ContractPrice, wprr.FundAmount, nil + }, txnSet, pt.ContractPrice, fundAmount, nil } // wrapRPCErr extracts the innermost error, wraps it in either a errHost or diff --git a/internal/test/e2e/contracts_test.go b/internal/test/e2e/contracts_test.go index aec472b57..82a8f043b 100644 --- a/internal/test/e2e/contracts_test.go +++ b/internal/test/e2e/contracts_test.go @@ -56,7 +56,7 @@ func TestFormContract(t *testing.T) { // assert the contract gets renewed and thus maintained var renewalID types.FileContractID - tt.Retry(100, 100*time.Millisecond, func() error { + tt.Retry(300, 100*time.Millisecond, func() error { contracts, err := cluster.Bus.Contracts(context.Background(), api.ContractsOpts{}) if err != nil { return err @@ -72,7 +72,7 @@ func TestFormContract(t *testing.T) { }) // assert the contract is part of the contract set - tt.Retry(100, 100*time.Millisecond, func() error { + tt.Retry(300, 100*time.Millisecond, func() error { contracts, err := b.Contracts(context.Background(), api.ContractsOpts{ContractSet: test.ContractSet}) tt.OK(err) if len(contracts) != 1 { diff --git a/worker/client/rhp.go b/worker/client/rhp.go index 47c177e61..8352f54ad 100644 --- a/worker/client/rhp.go +++ b/worker/client/rhp.go @@ -49,25 +49,6 @@ func (c *Client) RHPPruneContract(ctx context.Context, contractID types.FileCont return } -// 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, minNewCollateral, maxFundAmount types.Currency, expectedStorage, windowSize uint64) (resp api.RHPRenewResponse, err error) { - req := api.RHPRenewRequest{ - ContractID: contractID, - EndHeight: endHeight, - ExpectedNewStorage: expectedStorage, - HostAddress: hostAddress, - HostKey: hostKey, - MaxFundAmount: maxFundAmount, - MinNewCollateral: minNewCollateral, - RenterAddress: renterAddress, - RenterFunds: renterFunds, - SiamuxAddr: siamuxAddr, - WindowSize: windowSize, - } - err = c.c.WithContext(ctx).POST("/rhp/renew", req, &resp) - return -} - // RHPScan scans a host, returning its current settings. func (c *Client) RHPScan(ctx context.Context, hostKey types.PublicKey, hostIP string, timeout time.Duration) (resp api.RHPScanResponse, err error) { err = c.c.WithContext(ctx).POST("/rhp/scan", api.RHPScanRequest{ diff --git a/worker/host.go b/worker/host.go index 730dffd59..b5bbb71b7 100644 --- a/worker/host.go +++ b/worker/host.go @@ -29,8 +29,6 @@ type ( FundAccount(ctx context.Context, balance types.Currency, rev *types.FileContractRevision) error SyncAccount(ctx context.Context, rev *types.FileContractRevision) error - - RenewContract(ctx context.Context, rrr api.RHPRenewRequest) (_ rhpv2.ContractRevision, _ []types.Transaction, _, _ types.Currency, err error) } HostManager interface { @@ -122,52 +120,6 @@ func (h *host) UploadSector(ctx context.Context, sectorRoot types.Hash256, secto return nil } -func (h *host) RenewContract(ctx context.Context, rrr api.RHPRenewRequest) (_ rhpv2.ContractRevision, _ []types.Transaction, _, _ types.Currency, err error) { - gc, err := h.gougingChecker(ctx, false) - if err != nil { - return rhpv2.ContractRevision{}, nil, types.ZeroCurrency, types.ZeroCurrency, err - } - revision, err := h.client.Revision(ctx, h.fcid, h.hk, h.siamuxAddr) - if err != nil { - return rhpv2.ContractRevision{}, nil, types.ZeroCurrency, types.ZeroCurrency, err - } - - // helper to discard txn on error - discardTxn := func(ctx context.Context, txn types.Transaction, err *error) { - if *err == nil { - return - } - - ctx, cancel := context.WithTimeout(ctx, 30*time.Second) - if dErr := h.bus.WalletDiscard(ctx, txn); dErr != nil { - h.logger.Errorf("%v: %s, failed to discard txn: %v", *err, dErr) - } - cancel() - } - - // helper to sign txn - signTxn := func(ctx context.Context, txn *types.Transaction, toSign []types.Hash256, cf types.CoveredFields) error { - // sign txn - return h.bus.WalletSign(ctx, txn, toSign, cf) - } - - // helper to prepare contract renewal - prepareRenew := func(ctx context.Context, revision types.FileContractRevision, hostAddress, renterAddress types.Address, renterKey types.PrivateKey, renterFunds, minNewCollateral, maxFundAmount types.Currency, pt rhpv3.HostPriceTable, endHeight, windowSize, expectedStorage uint64) (api.WalletPrepareRenewResponse, func(context.Context, types.Transaction, *error), error) { - resp, err := h.bus.WalletPrepareRenew(ctx, revision, hostAddress, renterAddress, renterKey, renterFunds, minNewCollateral, maxFundAmount, pt, endHeight, windowSize, expectedStorage) - if err != nil { - return api.WalletPrepareRenewResponse{}, nil, err - } - return resp, discardTxn, nil - } - - // renew contract - rev, txnSet, contractPrice, fundAmount, err := h.client.Renew(ctx, rrr, gc, prepareRenew, signTxn, revision, h.renterKey) - if err != nil { - return rhpv2.ContractRevision{}, nil, contractPrice, fundAmount, err - } - return rev, txnSet, contractPrice, fundAmount, err -} - func (h *host) PriceTableUnpaid(ctx context.Context) (api.HostPriceTable, error) { return h.client.PriceTableUnpaid(ctx, h.hk, h.siamuxAddr) } @@ -275,14 +227,6 @@ func (h *host) SyncAccount(ctx context.Context, rev *types.FileContractRevision) }) } -func (h *host) gougingChecker(ctx context.Context, criticalMigration bool) (gouging.Checker, error) { - gp, err := h.bus.GougingParams(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get gouging params: %w", err) - } - return newGougingChecker(gp.GougingSettings, gp.ConsensusState, gp.TransactionFee, criticalMigration), nil -} - // priceTable fetches a price table from the host. If a revision is provided, it // will be used to pay for the price table. The returned price table is // guaranteed to be safe to use. diff --git a/worker/host_test.go b/worker/host_test.go index 8bbecaeff..f6ea236cd 100644 --- a/worker/host_test.go +++ b/worker/host_test.go @@ -123,10 +123,6 @@ func (h *testHost) FundAccount(ctx context.Context, balance types.Currency, rev return nil } -func (h *testHost) RenewContract(ctx context.Context, rrr api.RHPRenewRequest) (_ rhpv2.ContractRevision, _ []types.Transaction, _, _ types.Currency, err error) { - return rhpv2.ContractRevision{}, nil, types.Currency{}, types.Currency{}, nil -} - func (h *testHost) SyncAccount(ctx context.Context, rev *types.FileContractRevision) error { return nil } diff --git a/worker/mocks_test.go b/worker/mocks_test.go index 0b0d53351..29bf576f2 100644 --- a/worker/mocks_test.go +++ b/worker/mocks_test.go @@ -10,7 +10,6 @@ import ( "time" rhpv2 "go.sia.tech/core/rhp/v2" - rhpv3 "go.sia.tech/core/rhp/v3" "go.sia.tech/core/types" "go.sia.tech/renterd/alerts" "go.sia.tech/renterd/api" @@ -694,10 +693,6 @@ func (*walletMock) WalletFund(context.Context, *types.Transaction, types.Currenc return nil, nil, nil } -func (*walletMock) WalletPrepareRenew(context.Context, types.FileContractRevision, types.Address, types.Address, types.PrivateKey, types.Currency, types.Currency, types.Currency, rhpv3.HostPriceTable, uint64, uint64, uint64) (api.WalletPrepareRenewResponse, error) { - return api.WalletPrepareRenewResponse{}, nil -} - func (*walletMock) WalletSign(context.Context, *types.Transaction, []types.Hash256, types.CoveredFields) error { return nil } diff --git a/worker/worker.go b/worker/worker.go index 7502f646b..09f24073e 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -141,7 +141,6 @@ type ( Wallet interface { WalletDiscard(ctx context.Context, txn types.Transaction) error WalletFund(ctx context.Context, txn *types.Transaction, amount types.Currency, useUnconfirmedTxns bool) ([]types.Hash256, []types.Transaction, error) - WalletPrepareRenew(ctx context.Context, revision types.FileContractRevision, hostAddress, renterAddress types.Address, renterKey types.PrivateKey, renterFunds, minNewCollateral, maxFundAmount types.Currency, pt rhpv3.HostPriceTable, endHeight, windowSize, expectedStorage uint64) (api.WalletPrepareRenewResponse, error) WalletSign(ctx context.Context, txn *types.Transaction, toSign []types.Hash256, cf types.CoveredFields) error } @@ -542,56 +541,6 @@ func (w *Worker) rhpContractRootsHandlerGET(jc jape.Context) { jc.Encode(roots) } -func (w *Worker) rhpRenewHandler(jc jape.Context) { - ctx := jc.Request.Context() - - // decode request - var rrr api.RHPRenewRequest - if jc.Decode(&rrr) != nil { - return - } - - // check renter funds is not zero - if rrr.RenterFunds.IsZero() { - http.Error(jc.ResponseWriter, "RenterFunds can not be zero", http.StatusBadRequest) - return - } - - // attach gouging checker - gp, err := w.bus.GougingParams(ctx) - if jc.Check("could not get gouging parameters", err) != nil { - return - } - ctx = WithGougingChecker(ctx, w.bus, gp) - - // renew the contract - var renewed rhpv2.ContractRevision - var txnSet []types.Transaction - var contractPrice, fundAmount types.Currency - if jc.Check("couldn't renew contract", w.withContractLock(ctx, rrr.ContractID, lockingPriorityRenew, func() (err error) { - h := w.Host(rrr.HostKey, rrr.ContractID, rrr.SiamuxAddr) - renewed, txnSet, contractPrice, fundAmount, err = h.RenewContract(ctx, rrr) - return err - })) != nil { - return - } - - // broadcast the transaction set - err = w.bus.BroadcastTransaction(ctx, txnSet) - if err != nil { - w.logger.Errorf("failed to broadcast renewal txn set: %v", err) - } - - // send the response - jc.Encode(api.RHPRenewResponse{ - ContractID: renewed.ID(), - Contract: renewed, - ContractPrice: contractPrice, - FundAmount: fundAmount, - TransactionSet: txnSet, - }) -} - func (w *Worker) slabMigrateHandler(jc jape.Context) { ctx := jc.Request.Context() @@ -1227,9 +1176,8 @@ func (w *Worker) Handler() http.Handler { "GET /rhp/contracts": w.rhpContractsHandlerGET, "POST /rhp/contract/:id/broadcast": w.rhpBroadcastHandler, "POST /rhp/contract/:id/prune": w.rhpPruneContractHandlerPOST, - "GET /rhp/contract/:id/roots": w.rhpContractRootsHandlerGET, + "GET /rhp/contract/:id/roots": w.rhpContractRootsHandlerGET, "POST /rhp/scan": w.rhpScanHandler, - "POST /rhp/renew": w.rhpRenewHandler, "POST /rhp/pricetable": w.rhpPriceTableHandler, "GET /stats/downloads": w.downloadsStatsHandlerGET,