diff --git a/api/contract.go b/api/contract.go index b7d43b6a7..b012582e2 100644 --- a/api/contract.go +++ b/api/contract.go @@ -144,6 +144,16 @@ type ( TotalCost types.Currency `json:"totalCost"` } + // ContractFormRequest is the request type for the POST /contracts endpoint. + ContractFormRequest struct { + EndHeight uint64 `json:"endHeight"` + HostCollateral types.Currency `json:"hostCollateral"` + HostKey types.PublicKey `json:"hostKey"` + HostIP string `json:"hostIP"` + RenterFunds types.Currency `json:"renterFunds"` + RenterAddress types.Address `json:"renterAddress"` + } + // ContractKeepaliveRequest is the request type for the /contract/:id/keepalive // endpoint. ContractKeepaliveRequest struct { diff --git a/api/wallet.go b/api/wallet.go index 510e7b95b..d2ddbc857 100644 --- a/api/wallet.go +++ b/api/wallet.go @@ -5,7 +5,6 @@ import ( "net/url" "time" - rhpv2 "go.sia.tech/core/rhp/v2" rhpv3 "go.sia.tech/core/rhp/v3" "go.sia.tech/core/types" ) @@ -45,18 +44,6 @@ type ( DependsOn []types.Transaction `json:"dependsOn"` } - // WalletPrepareFormRequest is the request type for the /wallet/prepare/form - // endpoint. - WalletPrepareFormRequest struct { - EndHeight uint64 `json:"endHeight"` - HostCollateral types.Currency `json:"hostCollateral"` - HostKey types.PublicKey `json:"hostKey"` - HostSettings rhpv2.HostSettings `json:"hostSettings"` - RenterAddress types.Address `json:"renterAddress"` - RenterFunds types.Currency `json:"renterFunds"` - RenterKey types.PublicKey `json:"renterKey"` - } - // WalletPrepareRenewRequest is the request type for the /wallet/prepare/renew // endpoint. WalletPrepareRenewRequest struct { diff --git a/api/worker.go b/api/worker.go index 894fd0c60..9bce3386f 100644 --- a/api/worker.go +++ b/api/worker.go @@ -80,16 +80,6 @@ type ( Error string `json:"error,omitempty"` } - // RHPFormRequest is the request type for the /rhp/form endpoint. - RHPFormRequest struct { - EndHeight uint64 `json:"endHeight"` - HostCollateral types.Currency `json:"hostCollateral"` - HostKey types.PublicKey `json:"hostKey"` - HostIP string `json:"hostIP"` - RenterFunds types.Currency `json:"renterFunds"` - RenterAddress types.Address `json:"renterAddress"` - } - // RHPFormResponse is the response type for the /rhp/form endpoint. RHPFormResponse struct { ContractID types.FileContractID `json:"contractID"` diff --git a/autopilot/autopilot.go b/autopilot/autopilot.go index 58fb0a9ec..9ea235a11 100644 --- a/autopilot/autopilot.go +++ b/autopilot/autopilot.go @@ -42,13 +42,13 @@ type Bus interface { ConsensusState(ctx context.Context) (api.ConsensusState, error) // contracts - AddContract(ctx context.Context, c rhpv2.ContractRevision, contractPrice, totalCost types.Currency, startHeight uint64, state string) (api.ContractMetadata, error) 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) 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 e82253d43..4e1b87d3b 100644 --- a/autopilot/contractor/contractor.go +++ b/autopilot/contractor/contractor.go @@ -81,7 +81,6 @@ const ( ) type Bus interface { - AddContract(ctx context.Context, c rhpv2.ContractRevision, contractPrice, totalCost types.Currency, startHeight uint64, state string) (api.ContractMetadata, error) 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 @@ -89,6 +88,7 @@ type Bus interface { 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) 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) @@ -99,7 +99,6 @@ type Bus interface { type Worker interface { Contracts(ctx context.Context, hostTimeout time.Duration) (api.ContractsResponse, error) RHPBroadcast(ctx context.Context, fcid types.FileContractID) (err error) - RHPForm(ctx context.Context, endHeight uint64, hk types.PublicKey, hostIP string, renterAddress types.Address, renterFunds types.Currency, hostCollateral types.Currency) (rhpv2.ContractRevision, []types.Transaction, 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) @@ -228,7 +227,7 @@ func (c *Contractor) formContract(ctx *mCtx, w Worker, host api.Host, minInitial hostCollateral := rhpv2.ContractFormationCollateral(ctx.Period(), expectedStorage, scan.Settings) // form contract - contract, _, err := w.RHPForm(ctx, endHeight, hk, host.NetAddress, ctx.state.Address, renterFunds, hostCollateral) + contract, err := c.bus.FormContract(ctx, ctx.state.Address, renterFunds, hk, host.NetAddress, hostCollateral, endHeight) if err != nil { // TODO: keep track of consecutive failures and break at some point logger.Errorw(fmt.Sprintf("contract formation failed, err: %v", err), "hk", hk) @@ -241,20 +240,12 @@ func (c *Contractor) formContract(ctx *mCtx, w Worker, host api.Host, minInitial // update the budget *budget = budget.Sub(renterFunds) - // persist contract in store - contractPrice := contract.Revision.MissedHostPayout().Sub(hostCollateral) - formedContract, err := c.bus.AddContract(ctx, contract, contractPrice, renterFunds, cs.BlockHeight, api.ContractStatePending) - if err != nil { - logger.Errorw(fmt.Sprintf("contract formation failed, err: %v", err), "hk", hk) - return api.ContractMetadata{}, true, err - } - logger.Infow("formation succeeded", - "fcid", formedContract.ID, + "fcid", contract.ID, "renterFunds", renterFunds.String(), "collateral", hostCollateral.String(), ) - return formedContract, true, nil + return contract, true, nil } func (c *Contractor) initialContractFunding(settings rhpv2.HostSettings, txnFee, minFunding, maxFunding types.Currency) types.Currency { diff --git a/autopilot/workerpool.go b/autopilot/workerpool.go index 990498e62..acc6d22e2 100644 --- a/autopilot/workerpool.go +++ b/autopilot/workerpool.go @@ -5,7 +5,6 @@ import ( "sync" "time" - rhpv2 "go.sia.tech/core/rhp/v2" rhpv3 "go.sia.tech/core/rhp/v3" "go.sia.tech/core/types" "go.sia.tech/renterd/api" @@ -20,7 +19,6 @@ type Worker interface { MigrateSlab(ctx context.Context, s object.Slab, set string) (api.MigrateSlabResponse, error) RHPBroadcast(ctx context.Context, fcid types.FileContractID) (err error) - RHPForm(ctx context.Context, endHeight uint64, hk types.PublicKey, hostIP string, renterAddress types.Address, renterFunds types.Currency, hostCollateral types.Currency) (rhpv2.ContractRevision, []types.Transaction, error) 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) (api.HostPriceTable, error) RHPPruneContract(ctx context.Context, fcid types.FileContractID, timeout time.Duration) (pruned, remaining uint64, err error) diff --git a/bus/bus.go b/bus/bus.go index 431d5abd5..c5ae1113e 100644 --- a/bus/bus.go +++ b/bus/bus.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "math/big" + "net" "net/http" "strings" "time" @@ -26,10 +27,13 @@ import ( "go.sia.tech/renterd/api" "go.sia.tech/renterd/bus/client" ibus "go.sia.tech/renterd/internal/bus" + "go.sia.tech/renterd/internal/rhp" + rhp2 "go.sia.tech/renterd/internal/rhp/v2" "go.sia.tech/renterd/object" "go.sia.tech/renterd/stores/sql" "go.sia.tech/renterd/webhooks" "go.uber.org/zap" + "golang.org/x/crypto/blake2b" ) const ( @@ -303,6 +307,7 @@ type ( type Bus struct { startTime time.Time + masterKey [32]byte accountsMgr AccountManager alerts alerts.Alerter @@ -320,6 +325,8 @@ type Bus struct { mtrcs MetricsStore ss SettingStore + rhp2 *rhp2.Client + contractLocker ContractLocker sectors UploadingSectorsCache walletMetricsRecorder WalletMetricsRecorder @@ -328,10 +335,13 @@ type Bus struct { } // New returns a new Bus -func New(ctx context.Context, am AlertManager, wm WebhooksManager, cm ChainManager, s Syncer, w Wallet, store Store, announcementMaxAge time.Duration, l *zap.Logger) (_ *Bus, err error) { +func New(ctx context.Context, masterKey [32]byte, am AlertManager, wm WebhooksManager, cm ChainManager, s Syncer, w Wallet, store Store, announcementMaxAge time.Duration, l *zap.Logger) (_ *Bus, err error) { l = l.Named("bus") b := &Bus{ + startTime: time.Now(), + masterKey: masterKey, + s: s, cm: cm, w: w, @@ -346,7 +356,7 @@ func New(ctx context.Context, am AlertManager, wm WebhooksManager, cm ChainManag webhooksMgr: wm, logger: l.Sugar(), - startTime: time.Now(), + rhp2: rhp2.New(rhp.NewFallbackDialer(store, net.Dialer{}, l), l), } // init settings @@ -411,6 +421,7 @@ func (b *Bus) Handler() http.Handler { "GET /consensus/siafundfee/:payout": b.contractTaxHandlerGET, "GET /consensus/state": b.consensusStateHandler, + "POST /contracts": b.contractsFormHandler, "GET /contracts": b.contractsHandlerGET, "DELETE /contracts/all": b.contractsAllHandlerDELETE, "POST /contracts/archive": b.contractsArchiveHandlerPOST, @@ -507,7 +518,6 @@ func (b *Bus) Handler() http.Handler { "POST /wallet/fund": b.walletFundHandler, "GET /wallet/outputs": b.walletOutputsHandler, "GET /wallet/pending": b.walletPendingHandler, - "POST /wallet/prepare/form": b.walletPrepareFormHandler, "POST /wallet/prepare/renew": b.walletPrepareRenewHandler, "POST /wallet/redistribute": b.walletRedistributeHandler, "POST /wallet/send": b.walletSendSiacoinsHandler, @@ -532,6 +542,71 @@ func (b *Bus) Shutdown(ctx context.Context) error { ) } +func (b *Bus) addContract(ctx context.Context, rev rhpv2.ContractRevision, contractPrice, totalCost types.Currency, startHeight uint64, state string) (api.ContractMetadata, error) { + c, err := b.ms.AddContract(ctx, rev, contractPrice, totalCost, startHeight, state) + if err != nil { + return api.ContractMetadata{}, err + } + + b.broadcastAction(webhooks.Event{ + Module: api.ModuleContract, + Event: api.EventAdd, + Payload: api.EventContractAdd{ + Added: c, + Timestamp: time.Now().UTC(), + }, + }) + return c, nil +} + +func (b *Bus) isPassedV2AllowHeight() bool { + cs := b.cm.TipState() + return cs.Index.Height >= cs.Network.HardforkV2.AllowHeight +} + +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) { + // derive the renter key + renterKey := b.deriveRenterKey(hostKey) + + // prepare the transaction + cs := b.cm.TipState() + fc := rhpv2.PrepareContractFormation(renterKey.PublicKey(), hostKey, renterFunds, hostCollateral, endHeight, hostSettings, renterAddress) + txn := types.Transaction{FileContracts: []types.FileContract{fc}} + + // calculate the miner fee + fee := b.cm.RecommendedFee().Mul64(cs.TransactionWeight(txn)) + txn.MinerFees = []types.Currency{fee} + + // fund the transaction + cost := rhpv2.ContractFormationCost(cs, fc, hostSettings.ContractPrice).Add(fee) + toSign, err := b.w.FundTransaction(&txn, cost, true) + if err != nil { + return rhpv2.ContractRevision{}, fmt.Errorf("couldn't fund transaction: %w", err) + } + + // sign the transaction + b.w.SignTransaction(&txn, toSign, wallet.ExplicitCoveredFields(txn)) + + // form the contract + contract, txnSet, err := b.rhp2.FormContract(ctx, hostKey, hostIP, renterKey, append(b.cm.UnconfirmedParents(txn), txn)) + if err != nil { + b.w.ReleaseInputs([]types.Transaction{txn}, nil) + return rhpv2.ContractRevision{}, err + } + + // add transaction set to the pool + _, err = b.cm.AddPoolTransactions(txnSet) + if err != nil { + b.w.ReleaseInputs([]types.Transaction{txn}, nil) + return rhpv2.ContractRevision{}, fmt.Errorf("couldn't add transaction set to the pool: %w", err) + } + + // broadcast the transaction set + go b.s.BroadcastTransactionSet(txnSet) + + return contract, nil +} + // initSettings loads the default settings if the setting is not already set and // ensures the settings are valid func (b *Bus) initSettings(ctx context.Context) error { @@ -645,3 +720,21 @@ func (b *Bus) initSettings(ctx context.Context) error { return nil } + +func (b *Bus) deriveRenterKey(hostKey types.PublicKey) types.PrivateKey { + seed := blake2b.Sum256(append(b.deriveSubKey("renterkey"), hostKey[:]...)) + pk := types.NewPrivateKeyFromSeed(seed[:]) + for i := range seed { + seed[i] = 0 + } + return pk +} + +func (b *Bus) deriveSubKey(purpose string) types.PrivateKey { + seed := blake2b.Sum256(append(b.masterKey[:], []byte(purpose)...)) + pk := types.NewPrivateKeyFromSeed(seed[:]) + for i := range seed { + seed[i] = 0 + } + return pk +} diff --git a/bus/client/contracts.go b/bus/client/contracts.go index 84cd7dc88..bb3b16b4c 100644 --- a/bus/client/contracts.go +++ b/bus/client/contracts.go @@ -130,6 +130,19 @@ func (c *Client) DeleteContractSet(ctx context.Context, set string) (err error) return } +// FormContract forms a contract with a host and adds it to the bus. +func (c *Client) FormContract(ctx context.Context, renterAddress types.Address, renterFunds types.Currency, hostKey types.PublicKey, hostIP string, hostCollateral types.Currency, endHeight uint64) (contract api.ContractMetadata, err error) { + err = c.c.WithContext(ctx).POST("/contracts", api.ContractFormRequest{ + EndHeight: endHeight, + HostCollateral: hostCollateral, + HostKey: hostKey, + HostIP: hostIP, + RenterFunds: renterFunds, + RenterAddress: renterAddress, + }, &contract) + return +} + // KeepaliveContract extends the duration on an already acquired lock on a // contract. func (c *Client) KeepaliveContract(ctx context.Context, contractID types.FileContractID, lockID uint64, d time.Duration) (err error) { diff --git a/bus/client/wallet.go b/bus/client/wallet.go index 9733ed335..0fcc8d0b5 100644 --- a/bus/client/wallet.go +++ b/bus/client/wallet.go @@ -6,7 +6,6 @@ import ( "net/http" "net/url" - rhpv2 "go.sia.tech/core/rhp/v2" rhpv3 "go.sia.tech/core/rhp/v3" "go.sia.tech/core/types" "go.sia.tech/renterd/api" @@ -64,21 +63,6 @@ func (c *Client) WalletPending(ctx context.Context) (resp []types.Transaction, e return } -// WalletPrepareForm funds and signs a contract transaction. -func (c *Client) WalletPrepareForm(ctx context.Context, renterAddress types.Address, renterKey types.PublicKey, renterFunds, hostCollateral types.Currency, hostKey types.PublicKey, hostSettings rhpv2.HostSettings, endHeight uint64) (txns []types.Transaction, err error) { - req := api.WalletPrepareFormRequest{ - EndHeight: endHeight, - HostCollateral: hostCollateral, - HostKey: hostKey, - HostSettings: hostSettings, - RenterAddress: renterAddress, - RenterFunds: renterFunds, - RenterKey: renterKey, - } - err = c.c.WithContext(ctx).POST("/wallet/prepare/form", req, &txns) - 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{ diff --git a/bus/routes.go b/bus/routes.go index 9feb747e4..f020c5944 100644 --- a/bus/routes.go +++ b/bus/routes.go @@ -17,6 +17,7 @@ import ( rhpv3 "go.sia.tech/core/rhp/v3" ibus "go.sia.tech/renterd/internal/bus" + "go.sia.tech/renterd/internal/gouging" "go.sia.tech/core/gateway" "go.sia.tech/core/types" @@ -354,54 +355,52 @@ func (b *Bus) walletSendSiacoinsHandler(jc jape.Context) { } } - state := b.cm.TipState() - // if the current height is below the v2 hardfork height, send a v1 - // transaction - if state.Index.Height < state.Network.HardforkV2.AllowHeight { - // build transaction - txn := types.Transaction{ - MinerFees: []types.Currency{minerFee}, + // send V2 transaction if we're passed the V2 hardfork allow height + if b.isPassedV2AllowHeight() { + txn := types.V2Transaction{ + MinerFee: minerFee, SiacoinOutputs: []types.SiacoinOutput{ {Address: req.Address, Value: req.Amount}, }, } - toSign, err := b.w.FundTransaction(&txn, req.Amount.Add(minerFee), req.UseUnconfirmed) + // fund and sign transaction + state, toSign, err := b.w.FundV2Transaction(&txn, req.Amount.Add(minerFee), req.UseUnconfirmed) if jc.Check("failed to fund transaction", err) != nil { return } - b.w.SignTransaction(&txn, toSign, types.CoveredFields{WholeTransaction: true}) - // shouldn't be necessary to get parents since the transaction is - // not using unconfirmed outputs, but good practice - txnset := append(b.cm.UnconfirmedParents(txn), txn) + b.w.SignV2Inputs(state, &txn, toSign) + txnset := append(b.cm.V2UnconfirmedParents(txn), txn) // verify the transaction and add it to the transaction pool - if _, err := b.cm.AddPoolTransactions(txnset); jc.Check("failed to add transaction set", err) != nil { - b.w.ReleaseInputs([]types.Transaction{txn}, nil) + if _, err := b.cm.AddV2PoolTransactions(state.Index, txnset); jc.Check("failed to add v2 transaction set", err) != nil { + b.w.ReleaseInputs(nil, []types.V2Transaction{txn}) return } // broadcast the transaction - b.s.BroadcastTransactionSet(txnset) + b.s.BroadcastV2TransactionSet(state.Index, txnset) jc.Encode(txn.ID()) } else { - txn := types.V2Transaction{ - MinerFee: minerFee, + // build transaction + txn := types.Transaction{ + MinerFees: []types.Currency{minerFee}, SiacoinOutputs: []types.SiacoinOutput{ {Address: req.Address, Value: req.Amount}, }, } - // fund and sign transaction - state, toSign, err := b.w.FundV2Transaction(&txn, req.Amount.Add(minerFee), req.UseUnconfirmed) + toSign, err := b.w.FundTransaction(&txn, req.Amount.Add(minerFee), req.UseUnconfirmed) if jc.Check("failed to fund transaction", err) != nil { return } - b.w.SignV2Inputs(state, &txn, toSign) - txnset := append(b.cm.V2UnconfirmedParents(txn), txn) + b.w.SignTransaction(&txn, toSign, types.CoveredFields{WholeTransaction: true}) + // shouldn't be necessary to get parents since the transaction is + // not using unconfirmed outputs, but good practice + txnset := append(b.cm.UnconfirmedParents(txn), txn) // verify the transaction and add it to the transaction pool - if _, err := b.cm.AddV2PoolTransactions(state.Index, txnset); jc.Check("failed to add v2 transaction set", err) != nil { - b.w.ReleaseInputs(nil, []types.V2Transaction{txn}) + if _, err := b.cm.AddPoolTransactions(txnset); jc.Check("failed to add transaction set", err) != nil { + b.w.ReleaseInputs([]types.Transaction{txn}, nil) return } // broadcast the transaction - b.s.BroadcastV2TransactionSet(state.Index, txnset) + b.s.BroadcastTransactionSet(txnset) jc.Encode(txn.ID()) } } @@ -482,37 +481,6 @@ func (b *Bus) walletDiscardHandler(jc jape.Context) { } } -func (b *Bus) walletPrepareFormHandler(jc jape.Context) { - var wpfr api.WalletPrepareFormRequest - if jc.Decode(&wpfr) != nil { - return - } - if wpfr.HostKey == (types.PublicKey{}) { - jc.Error(errors.New("no host key provided"), http.StatusBadRequest) - return - } - if wpfr.RenterKey == (types.PublicKey{}) { - jc.Error(errors.New("no renter key provided"), http.StatusBadRequest) - return - } - cs := b.cm.TipState() - - fc := rhpv2.PrepareContractFormation(wpfr.RenterKey, wpfr.HostKey, wpfr.RenterFunds, wpfr.HostCollateral, wpfr.EndHeight, wpfr.HostSettings, wpfr.RenterAddress) - cost := rhpv2.ContractFormationCost(cs, fc, wpfr.HostSettings.ContractPrice) - txn := types.Transaction{ - FileContracts: []types.FileContract{fc}, - } - txn.MinerFees = []types.Currency{b.cm.RecommendedFee().Mul64(cs.TransactionWeight(txn))} - toSign, err := b.w.FundTransaction(&txn, cost.Add(txn.MinerFees[0]), true) - if jc.Check("couldn't fund transaction", err) != nil { - return - } - - b.w.SignTransaction(&txn, toSign, wallet.ExplicitCoveredFields(txn)) - - jc.Encode(append(b.cm.UnconfirmedParents(txn), txn)) -} - func (b *Bus) walletPrepareRenewHandler(jc jape.Context) { var wprr api.WalletPrepareRenewRequest if jc.Decode(&wprr) != nil { @@ -980,20 +948,10 @@ func (b *Bus) contractIDHandlerPOST(jc jape.Context) { return } - a, err := b.ms.AddContract(jc.Request.Context(), req.Contract, req.ContractPrice, req.TotalCost, req.StartHeight, req.State) + a, err := b.addContract(jc.Request.Context(), req.Contract, req.ContractPrice, req.TotalCost, req.StartHeight, req.State) if jc.Check("couldn't store contract", err) != nil { return } - - b.broadcastAction(webhooks.Event{ - Module: api.ModuleContract, - Event: api.EventAdd, - Payload: api.EventContractAdd{ - Added: a, - Timestamp: time.Now().UTC(), - }, - }) - jc.Encode(a) } @@ -2315,3 +2273,92 @@ func (b *Bus) multipartHandlerListPartsPOST(jc jape.Context) { } jc.Encode(resp) } + +func (b *Bus) contractsFormHandler(jc jape.Context) { + // apply pessimistic timeout + ctx, cancel := context.WithTimeout(jc.Request.Context(), 15*time.Minute) + defer cancel() + + // decode the request + var rfr api.ContractFormRequest + if jc.Decode(&rfr) != nil { + return + } + + // validate the request + if rfr.EndHeight == 0 { + http.Error(jc.ResponseWriter, "EndHeight can not be zero", http.StatusBadRequest) + return + } else if rfr.HostKey == (types.PublicKey{}) { + http.Error(jc.ResponseWriter, "HostKey must be provided", http.StatusBadRequest) + return + } else if rfr.HostCollateral.IsZero() { + http.Error(jc.ResponseWriter, "HostCollateral can not be zero", http.StatusBadRequest) + return + } else if rfr.HostIP == "" { + http.Error(jc.ResponseWriter, "HostIP must be provided", http.StatusBadRequest) + return + } else if rfr.RenterFunds.IsZero() { + http.Error(jc.ResponseWriter, "RenterFunds can not be zero", http.StatusBadRequest) + return + } else if rfr.RenterAddress == (types.Address{}) { + http.Error(jc.ResponseWriter, "RenterAddress must be provided", http.StatusBadRequest) + return + } + + // fetch gouging parameters + gp, err := b.gougingParams(ctx) + if jc.Check("could not get gouging parameters", err) != nil { + return + } + gc := gouging.NewChecker(gp.GougingSettings, gp.ConsensusState, gp.TransactionFee, nil, nil) + + // fetch host settings + settings, err := b.rhp2.Settings(ctx, rfr.HostKey, rfr.HostIP) + if jc.Check("couldn't fetch host settings", err) != nil { + return + } + + // check gouging + breakdown := gc.CheckSettings(settings) + if breakdown.Gouging() { + jc.Error(fmt.Errorf("failed to form contract, gouging check failed: %v", breakdown), http.StatusBadRequest) + return + } + + // send V2 transaction if we're passed the V2 hardfork allow height + var contract rhpv2.ContractRevision + if b.isPassedV2AllowHeight() { + panic("not implemented") + } else { + contract, err = b.formContract( + ctx, + settings, + rfr.RenterAddress, + rfr.RenterFunds, + rfr.HostCollateral, + rfr.HostKey, + rfr.HostIP, + rfr.EndHeight, + ) + if jc.Check("couldn't form contract", err) != nil { + return + } + } + + // store the contract + metadata, err := b.addContract( + ctx, + contract, + contract.Revision.MissedHostPayout().Sub(rfr.HostCollateral), + rfr.RenterFunds, + b.cm.Tip().Height, + api.ContractStatePending, + ) + if jc.Check("couldn't store contract", err) != nil { + return + } + + // return the contract + jc.Encode(metadata) +} diff --git a/cmd/renterd/node.go b/cmd/renterd/node.go index 89dd75ab0..9defbc127 100644 --- a/cmd/renterd/node.go +++ b/cmd/renterd/node.go @@ -376,9 +376,13 @@ func newBus(ctx context.Context, cfg config.Config, pk types.PrivateKey, network } } + // create master key - we currently derive the same key used by the workers + // to ensure contracts formed by the bus can be renewed by the autopilot + masterKey := blake2b.Sum256(append([]byte("worker"), pk...)) + // create bus announcementMaxAgeHours := time.Duration(cfg.Bus.AnnouncementMaxAgeHours) * time.Hour - b, err := bus.New(ctx, alertsMgr, wh, cm, s, w, sqlStore, announcementMaxAgeHours, logger) + b, err := bus.New(ctx, masterKey, alertsMgr, wh, cm, s, w, sqlStore, announcementMaxAgeHours, logger) if err != nil { return nil, nil, fmt.Errorf("failed to create bus: %w", err) } diff --git a/internal/worker/dialer.go b/internal/rhp/dialer.go similarity index 99% rename from internal/worker/dialer.go rename to internal/rhp/dialer.go index 56e51ce42..b2f87b32e 100644 --- a/internal/worker/dialer.go +++ b/internal/rhp/dialer.go @@ -1,4 +1,4 @@ -package worker +package rhp import ( "context" diff --git a/internal/rhp/v2/rhp.go b/internal/rhp/v2/rhp.go index 9786bf4c8..c2454c13d 100644 --- a/internal/rhp/v2/rhp.go +++ b/internal/rhp/v2/rhp.go @@ -71,8 +71,6 @@ type ( Dialer interface { Dial(ctx context.Context, hk types.PublicKey, address string) (net.Conn, error) } - - PrepareFormFn func(ctx context.Context, renterAddress types.Address, renterKey types.PublicKey, renterFunds, hostCollateral types.Currency, hostKey types.PublicKey, hostSettings rhpv2.HostSettings, endHeight uint64) (txns []types.Transaction, discard func(types.Transaction), err error) ) type Client struct { @@ -157,27 +155,9 @@ func (c *Client) Settings(ctx context.Context, hostKey types.PublicKey, hostIP s return } -func (c *Client) FormContract(ctx context.Context, renterAddress types.Address, renterKey types.PrivateKey, hostKey types.PublicKey, hostIP string, renterFunds, hostCollateral types.Currency, endHeight uint64, gougingChecker gouging.Checker, prepareForm PrepareFormFn) (contract rhpv2.ContractRevision, txnSet []types.Transaction, err error) { +func (c *Client) FormContract(ctx context.Context, hostKey types.PublicKey, hostIP string, renterKey types.PrivateKey, txnSet []types.Transaction) (contract rhpv2.ContractRevision, fullTxnSet []types.Transaction, err error) { err = c.withTransport(ctx, hostKey, hostIP, func(t *rhpv2.Transport) (err error) { - settings, err := rpcSettings(ctx, t) - if err != nil { - return err - } - - if breakdown := gougingChecker.CheckSettings(settings); breakdown.Gouging() { - return fmt.Errorf("failed to form contract, gouging check failed: %v", breakdown) - } - - renterTxnSet, discardTxn, err := prepareForm(ctx, renterAddress, renterKey.PublicKey(), renterFunds, hostCollateral, hostKey, settings, endHeight) - if err != nil { - return err - } - - contract, txnSet, err = rpcFormContract(ctx, t, renterKey, renterTxnSet) - if err != nil { - discardTxn(renterTxnSet[len(renterTxnSet)-1]) - return err - } + contract, fullTxnSet, err = rpcFormContract(ctx, t, renterKey, txnSet) return }) return diff --git a/internal/test/e2e/cluster.go b/internal/test/e2e/cluster.go index 3e01e8ae7..b500643d3 100644 --- a/internal/test/e2e/cluster.go +++ b/internal/test/e2e/cluster.go @@ -71,6 +71,7 @@ type TestCluster struct { network *consensus.Network genesisBlock types.Block + bs bus.Store cm *chain.Manager apID string dbName string @@ -313,7 +314,7 @@ func newTestCluster(t *testing.T, opts testClusterOptions) *TestCluster { // Create bus. busDir := filepath.Join(dir, "bus") - b, bShutdownFn, cm, err := newTestBus(ctx, busDir, busCfg, dbCfg, wk, logger) + b, bShutdownFn, cm, bs, err := newTestBus(ctx, busDir, busCfg, dbCfg, wk, logger) tt.OK(err) busAuth := jape.BasicAuth(busPassword) @@ -371,6 +372,7 @@ func newTestCluster(t *testing.T, opts testClusterOptions) *TestCluster { logger: logger, network: network, genesisBlock: genesis, + bs: bs, cm: cm, tt: tt, wk: wk, @@ -484,23 +486,23 @@ func newTestCluster(t *testing.T, opts testClusterOptions) *TestCluster { return cluster } -func newTestBus(ctx context.Context, dir string, cfg config.Bus, cfgDb dbConfig, pk types.PrivateKey, logger *zap.Logger) (*bus.Bus, func(ctx context.Context) error, *chain.Manager, error) { +func newTestBus(ctx context.Context, dir string, cfg config.Bus, cfgDb dbConfig, pk types.PrivateKey, logger *zap.Logger) (*bus.Bus, func(ctx context.Context) error, *chain.Manager, bus.Store, error) { // create store alertsMgr := alerts.NewManager() storeCfg, err := buildStoreConfig(alertsMgr, dir, cfg.SlabBufferCompletionThreshold, cfgDb, pk, logger) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } sqlStore, err := stores.NewSQLStore(storeCfg) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } // create webhooks manager wh, err := webhooks.NewManager(sqlStore, logger) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } // hookup webhooks <-> alerts @@ -509,35 +511,35 @@ func newTestBus(ctx context.Context, dir string, cfg config.Bus, cfgDb dbConfig, // create consensus directory consensusDir := filepath.Join(dir, "consensus") if err := os.MkdirAll(consensusDir, 0700); err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } // create chain database chainPath := filepath.Join(consensusDir, "blockchain.db") bdb, err := coreutils.OpenBoltChainDB(chainPath) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } // create chain manager network, genesis := testNetwork() store, state, err := chain.NewDBStore(bdb, network, genesis) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } cm := chain.NewManager(store, state) // create wallet w, err := wallet.NewSingleAddressWallet(pk, cm, sqlStore, wallet.WithReservationDuration(cfg.UsedUTXOExpiry)) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } // create syncer, peers will reject us if our hostname is empty or // unspecified, so use loopback l, err := net.Listen("tcp", cfg.GatewayAddr) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } syncerAddr := l.Addr().String() host, port, _ := net.SplitHostPort(syncerAddr) @@ -572,11 +574,15 @@ func newTestBus(ctx context.Context, dir string, cfg config.Bus, cfgDb dbConfig, } } + // create master key - we currently derive the same key used by the workers + // to ensure contracts formed by the bus can be renewed by the autopilot + masterKey := blake2b.Sum256(append([]byte("worker"), pk...)) + // create bus announcementMaxAgeHours := time.Duration(cfg.AnnouncementMaxAgeHours) * time.Hour - b, err := bus.New(ctx, alertsMgr, wh, cm, s, w, sqlStore, announcementMaxAgeHours, logger) + b, err := bus.New(ctx, masterKey, alertsMgr, wh, cm, s, w, sqlStore, announcementMaxAgeHours, logger) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } shutdownFn := func(ctx context.Context) error { @@ -589,7 +595,7 @@ func newTestBus(ctx context.Context, dir string, cfg config.Bus, cfgDb dbConfig, syncerShutdown(ctx), ) } - return b, shutdownFn, cm, nil + return b, shutdownFn, cm, sqlStore, nil } // addStorageFolderToHosts adds a single storage folder to each host. diff --git a/internal/test/e2e/cluster_test.go b/internal/test/e2e/cluster_test.go index f9ba9e018..255999911 100644 --- a/internal/test/e2e/cluster_test.go +++ b/internal/test/e2e/cluster_test.go @@ -1099,9 +1099,8 @@ func TestContractApplyChainUpdates(t *testing.T) { // manually form a contract with the host cs, _ := b.ConsensusState(context.Background()) wallet, _ := b.Wallet(context.Background()) - rev, _, err := w.RHPForm(context.Background(), cs.BlockHeight+test.AutopilotConfig.Contracts.Period+test.AutopilotConfig.Contracts.RenewWindow, h.PublicKey, h.NetAddress, wallet.Address, types.Siacoins(1), types.Siacoins(1)) - tt.OK(err) - contract, err := b.AddContract(context.Background(), rev, rev.Revision.MissedHostPayout().Sub(types.Siacoins(1)), types.Siacoins(1), cs.BlockHeight, api.ContractStatePending) + endHeight := cs.BlockHeight + test.AutopilotConfig.Contracts.Period + test.AutopilotConfig.Contracts.RenewWindow + contract, err := b.FormContract(context.Background(), wallet.Address, types.Siacoins(1), h.PublicKey, h.NetAddress, types.Siacoins(1), endHeight) tt.OK(err) // assert revision height is 0 @@ -1110,13 +1109,12 @@ func TestContractApplyChainUpdates(t *testing.T) { } // broadcast the revision for each contract - fcid := contract.ID - tt.OK(w.RHPBroadcast(context.Background(), fcid)) + tt.OK(w.RHPBroadcast(context.Background(), contract.ID)) cluster.MineBlocks(1) // check the revision height was updated. tt.Retry(100, 100*time.Millisecond, func() error { - c, err := cluster.Bus.Contract(context.Background(), fcid) + c, err := cluster.Bus.Contract(context.Background(), contract.ID) tt.OK(err) if c.RevisionHeight == 0 { return fmt.Errorf("contract %v should have been revised", c.ID) @@ -1159,9 +1157,7 @@ func TestEphemeralAccounts(t *testing.T) { // manually form a contract with the host cs, _ := b.ConsensusState(context.Background()) wallet, _ := b.Wallet(context.Background()) - rev, _, err := w.RHPForm(context.Background(), cs.BlockHeight+test.AutopilotConfig.Contracts.Period+test.AutopilotConfig.Contracts.RenewWindow, h.PublicKey, h.NetAddress, wallet.Address, types.Siacoins(10), types.Siacoins(1)) - tt.OK(err) - c, err := b.AddContract(context.Background(), rev, rev.Revision.MissedHostPayout().Sub(types.Siacoins(1)), types.Siacoins(1), cs.BlockHeight, api.ContractStatePending) + c, err := b.FormContract(context.Background(), wallet.Address, types.Siacoins(2), h.PublicKey, h.NetAddress, types.Siacoins(1), cs.BlockHeight+10) tt.OK(err) tt.OK(b.SetContractSet(context.Background(), test.ContractSet, []types.FileContractID{c.ID})) @@ -1589,7 +1585,7 @@ func TestUnconfirmedContractArchival(t *testing.T) { c := contracts[0] // add a contract to the bus - _, err = cluster.Bus.AddContract(context.Background(), rhpv2.ContractRevision{ + _, err = cluster.bs.AddContract(context.Background(), rhpv2.ContractRevision{ Revision: types.FileContractRevision{ ParentID: types.FileContractID{1}, UnlockConditions: types.UnlockConditions{ diff --git a/internal/test/e2e/contracts_test.go b/internal/test/e2e/contracts_test.go new file mode 100644 index 000000000..25f74fa8d --- /dev/null +++ b/internal/test/e2e/contracts_test.go @@ -0,0 +1,82 @@ +package e2e + +import ( + "context" + "fmt" + "testing" + "time" + + "go.sia.tech/core/types" + "go.sia.tech/renterd/api" + "go.sia.tech/renterd/internal/test" + "go.uber.org/zap/zapcore" +) + +func TestFormContract(t *testing.T) { + // configure the autopilot not to form any contracts + apSettings := test.AutopilotConfig + apSettings.Contracts.Amount = 0 + + // create cluster + opts := clusterOptsDefault + opts.autopilotSettings = &apSettings + opts.logger = newTestLoggerCustom(zapcore.DebugLevel) + cluster := newTestCluster(t, opts) + defer cluster.Shutdown() + + // convenience variables + b := cluster.Bus + a := cluster.Autopilot + tt := cluster.tt + + // add a host + hosts := cluster.AddHosts(1) + h, err := b.Host(context.Background(), hosts[0].PublicKey()) + tt.OK(err) + + // form a contract using the bus + wallet, _ := b.Wallet(context.Background()) + ap, err := b.Autopilot(context.Background(), api.DefaultAutopilotID) + tt.OK(err) + contract, err := b.FormContract(context.Background(), wallet.Address, types.Siacoins(1), h.PublicKey, h.NetAddress, types.Siacoins(1), ap.EndHeight()) + tt.OK(err) + + // assert the contract was added to the bus + _, err = b.Contract(context.Background(), contract.ID) + tt.OK(err) + + // mine to the renew window + cluster.MineToRenewWindow() + + // update autopilot config to allow for 1 contract, this won't form a + // contract but will ensure we don't skip contract maintenance, which should + // renew the contract we formed + apSettings.Contracts.Amount = 1 + tt.OK(a.UpdateConfig(apSettings)) + + // assert the contract gets renewed and thus maintained + var renewalID types.FileContractID + tt.Retry(100, 100*time.Millisecond, func() error { + contracts, err := cluster.Bus.Contracts(context.Background(), api.ContractsOpts{}) + if err != nil { + return err + } + if len(contracts) != 1 { + return fmt.Errorf("unexpected number of contracts %d != 1", len(contracts)) + } + if contracts[0].RenewedFrom != contract.ID { + return fmt.Errorf("contract wasn't renewed %v != %v", contracts[0].RenewedFrom, contract.ID) + } + renewalID = contracts[0].ID + return nil + }) + + // assert the contract is part of the contract set + contracts, err := b.Contracts(context.Background(), api.ContractsOpts{ContractSet: test.ContractSet}) + tt.OK(err) + if len(contracts) != 1 { + t.Fatalf("expected 1 contract, got %v", len(contracts)) + } else if contracts[0].ID != renewalID { + t.Fatalf("expected contract %v, got %v", contract.ID, contracts[0].ID) + } +} diff --git a/internal/test/e2e/gouging_test.go b/internal/test/e2e/gouging_test.go index a40fe0024..5be1784cb 100644 --- a/internal/test/e2e/gouging_test.go +++ b/internal/test/e2e/gouging_test.go @@ -170,9 +170,8 @@ func TestAccountFunding(t *testing.T) { // manually form a contract with the host cs, _ := b.ConsensusState(context.Background()) wallet, _ := b.Wallet(context.Background()) - rev, _, err := w.RHPForm(context.Background(), cs.BlockHeight+test.AutopilotConfig.Contracts.Period+test.AutopilotConfig.Contracts.RenewWindow, h.PublicKey, h.NetAddress, wallet.Address, types.Siacoins(1), types.Siacoins(1)) - tt.OK(err) - c, err := b.AddContract(context.Background(), rev, rev.Revision.MissedHostPayout().Sub(types.Siacoins(1)), types.Siacoins(1), cs.BlockHeight, api.ContractStatePending) + endHeight := cs.BlockHeight + test.AutopilotConfig.Contracts.Period + test.AutopilotConfig.Contracts.RenewWindow + c, err := b.FormContract(context.Background(), wallet.Address, types.Siacoins(1), h.PublicKey, h.NetAddress, types.Siacoins(1), endHeight) tt.OK(err) // fund the account diff --git a/worker/client/rhp.go b/worker/client/rhp.go index d1fb2d9e8..65b939f47 100644 --- a/worker/client/rhp.go +++ b/worker/client/rhp.go @@ -8,8 +8,6 @@ import ( "go.sia.tech/core/types" "go.sia.tech/renterd/api" - - rhpv2 "go.sia.tech/core/rhp/v2" ) // RHPBroadcast broadcasts the latest revision for a contract. @@ -24,21 +22,6 @@ func (c *Client) RHPContractRoots(ctx context.Context, contractID types.FileCont return } -// RHPForm forms a contract with a host. -func (c *Client) RHPForm(ctx context.Context, endHeight uint64, hostKey types.PublicKey, hostIP string, renterAddress types.Address, renterFunds types.Currency, hostCollateral types.Currency) (rhpv2.ContractRevision, []types.Transaction, error) { - req := api.RHPFormRequest{ - EndHeight: endHeight, - HostCollateral: hostCollateral, - HostKey: hostKey, - HostIP: hostIP, - RenterFunds: renterFunds, - RenterAddress: renterAddress, - } - var resp api.RHPFormResponse - err := c.c.WithContext(ctx).POST("/rhp/form", req, &resp) - return resp.Contract, resp.TransactionSet, err -} - // RHPFund funds an ephemeral account using the supplied contract. func (c *Client) RHPFund(ctx context.Context, contractID types.FileContractID, hostKey types.PublicKey, hostIP, siamuxAddr string, balance types.Currency) (err error) { req := api.RHPFundRequest{ diff --git a/worker/mocks_test.go b/worker/mocks_test.go index f982437a7..13e5fd733 100644 --- a/worker/mocks_test.go +++ b/worker/mocks_test.go @@ -722,10 +722,6 @@ func (*walletMock) WalletFund(context.Context, *types.Transaction, types.Currenc return nil, nil, nil } -func (*walletMock) WalletPrepareForm(context.Context, types.Address, types.PublicKey, types.Currency, types.Currency, types.PublicKey, rhpv2.HostSettings, uint64) ([]types.Transaction, error) { - return 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 } diff --git a/worker/worker.go b/worker/worker.go index 7073e0c63..111300ff2 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -27,6 +27,7 @@ import ( "go.sia.tech/renterd/build" "go.sia.tech/renterd/config" "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/internal/utils" @@ -153,7 +154,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) - WalletPrepareForm(ctx context.Context, renterAddress types.Address, renterKey types.PublicKey, renterFunds, hostCollateral types.Currency, hostKey types.PublicKey, hostSettings rhpv2.HostSettings, endHeight uint64) (txns []types.Transaction, err 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 } @@ -216,7 +216,7 @@ type Worker struct { uploadManager *uploadManager accounts *accounts - dialer *iworker.FallbackDialer + dialer *rhp.FallbackDialer cache iworker.WorkerCache priceTables *priceTables @@ -385,61 +385,6 @@ func (w *Worker) rhpPriceTableHandler(jc jape.Context) { jc.Encode(hpt) } -func (w *Worker) rhpFormHandler(jc jape.Context) { - ctx := jc.Request.Context() - - // decode the request - var rfr api.RHPFormRequest - if jc.Decode(&rfr) != nil { - return - } - - // check renter funds is not zero - if rfr.RenterFunds.IsZero() { - http.Error(jc.ResponseWriter, "RenterFunds can not be zero", http.StatusBadRequest) - return - } - - // apply a pessimistic timeout on contract formations - ctx, cancel := context.WithTimeout(ctx, 15*time.Minute) - defer cancel() - - gp, err := w.bus.GougingParams(ctx) - if jc.Check("could not get gouging parameters", err) != nil { - return - } - gc := newGougingChecker(gp.GougingSettings, gp.ConsensusState, gp.TransactionFee, false) - - hostIP, hostKey, renterFunds := rfr.HostIP, rfr.HostKey, rfr.RenterFunds - renterAddress, endHeight, hostCollateral := rfr.RenterAddress, rfr.EndHeight, rfr.HostCollateral - renterKey := w.deriveRenterKey(hostKey) - - contract, txnSet, err := w.rhp2Client.FormContract(ctx, renterAddress, renterKey, hostKey, hostIP, renterFunds, hostCollateral, endHeight, gc, func(ctx context.Context, renterAddress types.Address, renterKey types.PublicKey, renterFunds, hostCollateral types.Currency, hostKey types.PublicKey, hostSettings rhpv2.HostSettings, endHeight uint64) (txns []types.Transaction, discard func(types.Transaction), err error) { - txns, err = w.bus.WalletPrepareForm(ctx, renterAddress, renterKey, renterFunds, hostCollateral, hostKey, hostSettings, endHeight) - if err != nil { - return nil, nil, err - } - return txns, func(txn types.Transaction) { - _ = w.bus.WalletDiscard(ctx, txn) - }, nil - }) - if jc.Check("couldn't form contract", err) != nil { - return - } - - // broadcast the transaction set - err = w.bus.BroadcastTransaction(ctx, txnSet) - if err != nil { - w.logger.Errorf("failed to broadcast formation txn set: %v", err) - } - - jc.Encode(api.RHPFormResponse{ - ContractID: contract.ID(), - Contract: contract, - TransactionSet: txnSet, - }) -} - func (w *Worker) rhpBroadcastHandler(jc jape.Context) { ctx := jc.Request.Context() @@ -1270,7 +1215,7 @@ func New(cfg config.Worker, masterKey [32]byte, b Bus, l *zap.Logger) (*Worker, a := alerts.WithOrigin(b, fmt.Sprintf("worker.%s", cfg.ID)) shutdownCtx, shutdownCancel := context.WithCancel(context.Background()) - dialer := iworker.NewFallbackDialer(b, net.Dialer{}, l) + dialer := rhp.NewFallbackDialer(b, net.Dialer{}, l) w := &Worker{ alerts: a, allowPrivateIPs: cfg.AllowPrivateIPs, @@ -1315,7 +1260,6 @@ func (w *Worker) Handler() http.Handler { "POST /rhp/contract/:id/prune": w.rhpPruneContractHandlerPOST, "GET /rhp/contract/:id/roots": w.rhpContractRootsHandlerGET, "POST /rhp/scan": w.rhpScanHandler, - "POST /rhp/form": w.rhpFormHandler, "POST /rhp/renew": w.rhpRenewHandler, "POST /rhp/fund": w.rhpFundHandler, "POST /rhp/sync": w.rhpSyncHandler,