From aba8759383e064b6e464de967f657277df84e64b Mon Sep 17 00:00:00 2001 From: PJ Date: Tue, 20 Aug 2024 13:57:29 +0200 Subject: [PATCH 1/9] bus: add /rhp/form endpoint --- bus/bus.go | 34 +++- bus/client/rhp.go | 21 +++ bus/routes.go | 268 +++++++++++++++++++++++++---- cmd/renterd/node.go | 6 +- internal/{worker => rhp}/dialer.go | 2 +- internal/rhp/v2/rhp.go | 8 +- internal/test/e2e/cluster.go | 6 +- internal/test/e2e/rhp_test.go | 81 +++++++++ internal/utils/errors.go | 5 + worker/worker.go | 5 +- 10 files changed, 391 insertions(+), 45 deletions(-) create mode 100644 bus/client/rhp.go rename internal/{worker => rhp}/dialer.go (99%) create mode 100644 internal/test/e2e/rhp_test.go diff --git a/bus/bus.go b/bus/bus.go index 431d5abd5..4bc41fbcd 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 @@ -469,6 +479,8 @@ func (b *Bus) Handler() http.Handler { "POST /slabbuffer/done": b.packedSlabsHandlerDonePOST, "POST /slabbuffer/fetch": b.packedSlabsHandlerFetchPOST, + "POST /rhp/form": b.rhpFormHandler, + "POST /search/hosts": b.searchHostsHandlerPOST, "GET /search/objects": b.searchObjectsHandlerGET, @@ -645,3 +657,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/rhp.go b/bus/client/rhp.go new file mode 100644 index 000000000..52c82cd4c --- /dev/null +++ b/bus/client/rhp.go @@ -0,0 +1,21 @@ +package client + +import ( + "context" + + "go.sia.tech/core/types" + "go.sia.tech/renterd/api" +) + +// RHPForm forms a contract with a host and adds it to the bus. +func (c *Client) RHPForm(ctx context.Context, endHeight uint64, hostKey types.PublicKey, hostIP string, renterAddress types.Address, renterFunds types.Currency, hostCollateral types.Currency) (contractID types.FileContractID, err error) { + err = c.c.WithContext(ctx).POST("/rhp/form", api.RHPFormRequest{ + EndHeight: endHeight, + HostCollateral: hostCollateral, + HostKey: hostKey, + HostIP: hostIP, + RenterFunds: renterFunds, + RenterAddress: renterAddress, + }, &contractID) + return +} diff --git a/bus/routes.go b/bus/routes.go index 9feb747e4..2b6a63688 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()) } } @@ -495,22 +494,22 @@ func (b *Bus) walletPrepareFormHandler(jc jape.Context) { 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 { + if txns, _, err := b.prepareForm( + jc.Request.Context(), + wpfr.RenterAddress, + wpfr.RenterKey, + wpfr.RenterFunds, + wpfr.HostCollateral, + wpfr.HostKey, + wpfr.HostSettings, + wpfr.EndHeight, + ); err != nil { + jc.Error(err, http.StatusInternalServerError) return + } else { + jc.Encode(txns) } - - b.w.SignTransaction(&txn, toSign, wallet.ExplicitCoveredFields(txn)) - - jc.Encode(append(b.cm.UnconfirmedParents(txn), txn)) } func (b *Bus) walletPrepareRenewHandler(jc jape.Context) { @@ -2315,3 +2314,198 @@ func (b *Bus) multipartHandlerListPartsPOST(jc jape.Context) { } jc.Encode(resp) } + +func (b *Bus) rhpFormHandler(jc jape.Context) { + // apply pessimistic timeout + ctx, cancel := context.WithTimeout(jc.Request.Context(), 15*time.Minute) + defer cancel() + + // decode the request + var rfr api.RHPFormRequest + 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) + + // send V2 transaction if we're passed the V2 hardfork allow height + var contract rhpv2.ContractRevision + if b.isPassedV2AllowHeight() { + // form the contract + var txnSet []types.V2Transaction + contract, txnSet, err = b.rhp2.FormV2Contract( + ctx, + rfr.RenterAddress, + b.deriveRenterKey(rfr.HostKey), + rfr.HostKey, + rfr.HostIP, + rfr.RenterFunds, + rfr.HostCollateral, + rfr.EndHeight, + gc, + b.prepareFormV2, + ) + if errors.Is(err, utils.ErrNotImplemented) { + jc.Error(err, http.StatusNotImplemented) // TODO: remove once rhp4 is implemented + return + } else if jc.Check("couldn't form contract", err) != nil { + return + } + + // fetch state + state := b.cm.TipState() + + // add transaction set to the pool + _, err := b.cm.AddV2PoolTransactions(state.Index, txnSet) + if jc.Check("couldn't broadcast transaction set", err) != nil { + b.w.ReleaseInputs(nil, txnSet) + return + } + + // broadcast the transaction set + b.s.BroadcastV2TransactionSet(state.Index, txnSet) + } else { + // form the contract + var txnSet []types.Transaction + contract, txnSet, err = b.rhp2.FormContract( + ctx, + rfr.RenterAddress, + b.deriveRenterKey(rfr.HostKey), + rfr.HostKey, + rfr.HostIP, + rfr.RenterFunds, + rfr.HostCollateral, + rfr.EndHeight, + gc, + b.prepareForm, + ) + if jc.Check("couldn't form contract", err) != nil { + return + } + + // add transaction set to the pool + _, err := b.cm.AddPoolTransactions(txnSet) + if jc.Check("couldn't broadcast transaction set", err) != nil { + b.w.ReleaseInputs(txnSet, nil) + return + } + + // broadcast the transaction set + b.s.BroadcastTransactionSet(txnSet) + } + + // store the contract + _, err = b.ms.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 ID + jc.Encode(contract.ID()) +} + +func (b *Bus) prepareForm(ctx context.Context, renterAddress types.Address, renterKey types.PublicKey, renterFunds, hostCollateral types.Currency, hostKey types.PublicKey, hostSettings rhpv2.HostSettings, endHeight uint64) ([]types.Transaction, func(types.Transaction), error) { + // prepare the transaction + cs := b.cm.TipState() + fc := rhpv2.PrepareContractFormation(renterKey, 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 nil, nil, fmt.Errorf("couldn't fund transaction: %w", err) + } + + // sign the transaction + b.w.SignTransaction(&txn, toSign, wallet.ExplicitCoveredFields(txn)) + + txns := append(b.cm.UnconfirmedParents(txn), txn) + return txns, func(txn types.Transaction) { b.w.ReleaseInputs(txns, nil) }, nil +} + +func (b *Bus) prepareFormV2(ctx context.Context, renterAddress types.Address, renterKey types.PublicKey, renterFunds, hostCollateral types.Currency, hostKey types.PublicKey, hostSettings rhpv2.HostSettings, endHeight uint64) ([]types.V2Transaction, func(types.V2Transaction), error) { + hostFunds := hostSettings.ContractPrice.Add(hostCollateral) + + // prepare the transaction + cs := b.cm.TipState() + fc := types.V2FileContract{ + RevisionNumber: 0, + Filesize: 0, + FileMerkleRoot: types.Hash256{}, + ProofHeight: endHeight + hostSettings.WindowSize, + ExpirationHeight: endHeight + hostSettings.WindowSize + 10, + RenterOutput: types.SiacoinOutput{ + Value: renterFunds, + Address: renterAddress, + }, + HostOutput: types.SiacoinOutput{ + Value: hostFunds, + Address: hostSettings.Address, + }, + MissedHostValue: hostFunds, + TotalCollateral: hostFunds, + RenterPublicKey: renterKey, + HostPublicKey: hostKey, + } + txn := types.V2Transaction{FileContracts: []types.V2FileContract{fc}} + + // calculate the miner fee + fee := b.cm.RecommendedFee().Mul64(cs.V2TransactionWeight(txn)) + txn.MinerFee = fee + + // fund the transaction + fundAmount := cs.V2FileContractTax(fc).Add(hostFunds).Add(renterFunds).Add(fee) + cs, toSign, err := b.w.FundV2Transaction(&txn, fundAmount, false) + if err != nil { + return nil, nil, fmt.Errorf("couldn't fund transaction: %w", err) + } + + // sign the transaction + b.w.SignV2Inputs(cs, &txn, toSign) + + txns := append(b.cm.V2UnconfirmedParents(txn), txn) + return txns, func(txn types.V2Transaction) { b.w.ReleaseInputs(nil, txns) }, nil +} + +func (b *Bus) isPassedV2AllowHeight() bool { + cs := b.cm.TipState() + return cs.Index.Height >= cs.Network.HardforkV2.AllowHeight +} 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..01b0a56cc 100644 --- a/internal/rhp/v2/rhp.go +++ b/internal/rhp/v2/rhp.go @@ -72,7 +72,8 @@ type ( 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) + 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) + PrepareV2FormFn func(ctx context.Context, renterAddress types.Address, renterKey types.PublicKey, renterFunds, hostCollateral types.Currency, hostKey types.PublicKey, hostSettings rhpv2.HostSettings, endHeight uint64) (txns []types.V2Transaction, discard func(types.V2Transaction), err error) ) type Client struct { @@ -183,6 +184,11 @@ func (c *Client) FormContract(ctx context.Context, renterAddress types.Address, return } +func (c *Client) FormV2Contract(ctx context.Context, renterAddress types.Address, renterKey types.PrivateKey, hostKey types.PublicKey, hostIP string, renterFunds, hostCollateral types.Currency, endHeight uint64, gougingChecker gouging.Checker, prepareForm PrepareV2FormFn) (contract rhpv2.ContractRevision, txnSet []types.V2Transaction, err error) { + err = fmt.Errorf("%w; forming contracts using V2 transactions is not supported yet", utils.ErrNotImplemented) + return +} + func (c *Client) PruneContract(ctx context.Context, renterKey types.PrivateKey, gougingChecker gouging.Checker, hostIP string, hostKey types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64, toKeep []types.Hash256) (revision *types.FileContractRevision, deleted, remaining uint64, cost types.Currency, err error) { err = c.withTransport(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { return c.withRevisionV2(renterKey, gougingChecker, t, fcid, lastKnownRevisionNumber, func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) (err error) { diff --git a/internal/test/e2e/cluster.go b/internal/test/e2e/cluster.go index 3e01e8ae7..08bc6866a 100644 --- a/internal/test/e2e/cluster.go +++ b/internal/test/e2e/cluster.go @@ -572,9 +572,13 @@ 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 } diff --git a/internal/test/e2e/rhp_test.go b/internal/test/e2e/rhp_test.go new file mode 100644 index 000000000..d6530998f --- /dev/null +++ b/internal/test/e2e/rhp_test.go @@ -0,0 +1,81 @@ +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 TestRHPForm(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 + cs, _ := b.ConsensusState(context.Background()) + wallet, _ := b.Wallet(context.Background()) + fcid, err := b.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) + + // assert the contract is added to the bus + _, err = b.Contract(context.Background(), fcid) + 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 != fcid { + return fmt.Errorf("contract wasn't renewed %v != %v", contracts[0].RenewedFrom, fcid) + } + 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", fcid, contracts[0].ID) + } +} diff --git a/internal/utils/errors.go b/internal/utils/errors.go index 22ff0e660..30e1c767c 100644 --- a/internal/utils/errors.go +++ b/internal/utils/errors.go @@ -17,6 +17,11 @@ var ( ErrIOTimeout = errors.New("i/o timeout") ) +var ( + // ErrNotImplemented is returned when a function is not implemented. + ErrNotImplemented = errors.New("not implemented") +) + // IsErr can be used to compare an error to a target and also works when used on // errors that haven't been wrapped since it will fall back to a string // comparison. Useful to check errors returned over the network. diff --git a/worker/worker.go b/worker/worker.go index 7073e0c63..3082f7842 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" @@ -216,7 +217,7 @@ type Worker struct { uploadManager *uploadManager accounts *accounts - dialer *iworker.FallbackDialer + dialer *rhp.FallbackDialer cache iworker.WorkerCache priceTables *priceTables @@ -1270,7 +1271,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, From 33bd92bb313d4ba9efa467534ab9e210583af2ae Mon Sep 17 00:00:00 2001 From: PJ Date: Wed, 21 Aug 2024 12:12:44 +0200 Subject: [PATCH 2/9] internal: panic when forming v2 contracts --- bus/routes.go | 37 +++---------------------------------- internal/rhp/v2/rhp.go | 5 ----- internal/utils/errors.go | 5 ----- 3 files changed, 3 insertions(+), 44 deletions(-) diff --git a/bus/routes.go b/bus/routes.go index 2b6a63688..c7cf0b1a1 100644 --- a/bus/routes.go +++ b/bus/routes.go @@ -2357,39 +2357,7 @@ func (b *Bus) rhpFormHandler(jc jape.Context) { // send V2 transaction if we're passed the V2 hardfork allow height var contract rhpv2.ContractRevision if b.isPassedV2AllowHeight() { - // form the contract - var txnSet []types.V2Transaction - contract, txnSet, err = b.rhp2.FormV2Contract( - ctx, - rfr.RenterAddress, - b.deriveRenterKey(rfr.HostKey), - rfr.HostKey, - rfr.HostIP, - rfr.RenterFunds, - rfr.HostCollateral, - rfr.EndHeight, - gc, - b.prepareFormV2, - ) - if errors.Is(err, utils.ErrNotImplemented) { - jc.Error(err, http.StatusNotImplemented) // TODO: remove once rhp4 is implemented - return - } else if jc.Check("couldn't form contract", err) != nil { - return - } - - // fetch state - state := b.cm.TipState() - - // add transaction set to the pool - _, err := b.cm.AddV2PoolTransactions(state.Index, txnSet) - if jc.Check("couldn't broadcast transaction set", err) != nil { - b.w.ReleaseInputs(nil, txnSet) - return - } - - // broadcast the transaction set - b.s.BroadcastV2TransactionSet(state.Index, txnSet) + panic("not implemented") } else { // form the contract var txnSet []types.Transaction @@ -2461,7 +2429,8 @@ func (b *Bus) prepareForm(ctx context.Context, renterAddress types.Address, rent return txns, func(txn types.Transaction) { b.w.ReleaseInputs(txns, nil) }, nil } -func (b *Bus) prepareFormV2(ctx context.Context, renterAddress types.Address, renterKey types.PublicKey, renterFunds, hostCollateral types.Currency, hostKey types.PublicKey, hostSettings rhpv2.HostSettings, endHeight uint64) ([]types.V2Transaction, func(types.V2Transaction), error) { +// nolint: unused +func (b *Bus) prepareFormV2(_ context.Context, renterAddress types.Address, renterKey types.PublicKey, renterFunds, hostCollateral types.Currency, hostKey types.PublicKey, hostSettings rhpv2.HostSettings, endHeight uint64) ([]types.V2Transaction, func(types.V2Transaction), error) { hostFunds := hostSettings.ContractPrice.Add(hostCollateral) // prepare the transaction diff --git a/internal/rhp/v2/rhp.go b/internal/rhp/v2/rhp.go index 01b0a56cc..8b2c1da6e 100644 --- a/internal/rhp/v2/rhp.go +++ b/internal/rhp/v2/rhp.go @@ -184,11 +184,6 @@ func (c *Client) FormContract(ctx context.Context, renterAddress types.Address, return } -func (c *Client) FormV2Contract(ctx context.Context, renterAddress types.Address, renterKey types.PrivateKey, hostKey types.PublicKey, hostIP string, renterFunds, hostCollateral types.Currency, endHeight uint64, gougingChecker gouging.Checker, prepareForm PrepareV2FormFn) (contract rhpv2.ContractRevision, txnSet []types.V2Transaction, err error) { - err = fmt.Errorf("%w; forming contracts using V2 transactions is not supported yet", utils.ErrNotImplemented) - return -} - func (c *Client) PruneContract(ctx context.Context, renterKey types.PrivateKey, gougingChecker gouging.Checker, hostIP string, hostKey types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64, toKeep []types.Hash256) (revision *types.FileContractRevision, deleted, remaining uint64, cost types.Currency, err error) { err = c.withTransport(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { return c.withRevisionV2(renterKey, gougingChecker, t, fcid, lastKnownRevisionNumber, func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) (err error) { diff --git a/internal/utils/errors.go b/internal/utils/errors.go index 30e1c767c..22ff0e660 100644 --- a/internal/utils/errors.go +++ b/internal/utils/errors.go @@ -17,11 +17,6 @@ var ( ErrIOTimeout = errors.New("i/o timeout") ) -var ( - // ErrNotImplemented is returned when a function is not implemented. - ErrNotImplemented = errors.New("not implemented") -) - // IsErr can be used to compare an error to a target and also works when used on // errors that haven't been wrapped since it will fall back to a string // comparison. Useful to check errors returned over the network. From b7197abaac4fae2c91e2d33ac43e5933d0fbe567 Mon Sep 17 00:00:00 2001 From: PJ Date: Wed, 21 Aug 2024 13:18:39 +0200 Subject: [PATCH 3/9] bus,worker: remove legacy formation endpoints --- api/wallet.go | 13 --- autopilot/autopilot.go | 2 +- autopilot/contractor/contractor.go | 17 +--- autopilot/workerpool.go | 2 - bus/bus.go | 1 - bus/client/contracts.go | 25 +++--- bus/client/rhp.go | 21 ----- bus/client/wallet.go | 16 ---- bus/routes.go | 127 ++++++++--------------------- internal/rhp/v2/rhp.go | 25 +----- internal/test/e2e/cluster.go | 26 +++--- internal/test/e2e/cluster_test.go | 12 ++- internal/test/e2e/gouging_test.go | 5 +- internal/test/e2e/rhp_test.go | 13 +-- worker/client/rhp.go | 17 ---- worker/mocks_test.go | 4 - worker/worker.go | 57 ------------- 17 files changed, 81 insertions(+), 302 deletions(-) delete mode 100644 bus/client/rhp.go 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/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 4bc41fbcd..f4082d3dd 100644 --- a/bus/bus.go +++ b/bus/bus.go @@ -519,7 +519,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, diff --git a/bus/client/contracts.go b/bus/client/contracts.go index 84cd7dc88..d1c2d8006 100644 --- a/bus/client/contracts.go +++ b/bus/client/contracts.go @@ -11,18 +11,6 @@ import ( "go.sia.tech/renterd/api" ) -// AddContract adds the provided contract to the metadata store. -func (c *Client) AddContract(ctx context.Context, contract rhpv2.ContractRevision, contractPrice, totalCost types.Currency, startHeight uint64, state string) (added api.ContractMetadata, err error) { - err = c.c.WithContext(ctx).POST(fmt.Sprintf("/contract/%s", contract.ID()), api.ContractAddRequest{ - Contract: contract, - StartHeight: startHeight, - ContractPrice: contractPrice, - State: state, - TotalCost: totalCost, - }, &added) - return -} - // AddRenewedContract adds the provided contract to the metadata store. func (c *Client) AddRenewedContract(ctx context.Context, contract rhpv2.ContractRevision, contractPrice, totalCost types.Currency, startHeight uint64, renewedFrom types.FileContractID, state string) (renewed api.ContractMetadata, err error) { err = c.c.WithContext(ctx).POST(fmt.Sprintf("/contract/%s/renewed", contract.ID()), api.ContractRenewedRequest{ @@ -130,6 +118,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("/rhp/form", api.RHPFormRequest{ + 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/rhp.go b/bus/client/rhp.go deleted file mode 100644 index 52c82cd4c..000000000 --- a/bus/client/rhp.go +++ /dev/null @@ -1,21 +0,0 @@ -package client - -import ( - "context" - - "go.sia.tech/core/types" - "go.sia.tech/renterd/api" -) - -// RHPForm forms a contract with a host and adds it to the bus. -func (c *Client) RHPForm(ctx context.Context, endHeight uint64, hostKey types.PublicKey, hostIP string, renterAddress types.Address, renterFunds types.Currency, hostCollateral types.Currency) (contractID types.FileContractID, err error) { - err = c.c.WithContext(ctx).POST("/rhp/form", api.RHPFormRequest{ - EndHeight: endHeight, - HostCollateral: hostCollateral, - HostKey: hostKey, - HostIP: hostIP, - RenterFunds: renterFunds, - RenterAddress: renterAddress, - }, &contractID) - return -} 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 c7cf0b1a1..c59e2faf7 100644 --- a/bus/routes.go +++ b/bus/routes.go @@ -481,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 - } - - if txns, _, err := b.prepareForm( - jc.Request.Context(), - wpfr.RenterAddress, - wpfr.RenterKey, - wpfr.RenterFunds, - wpfr.HostCollateral, - wpfr.HostKey, - wpfr.HostSettings, - wpfr.EndHeight, - ); err != nil { - jc.Error(err, http.StatusInternalServerError) - return - } else { - jc.Encode(txns) - } -} - func (b *Bus) walletPrepareRenewHandler(jc jape.Context) { var wprr api.WalletPrepareRenewRequest if jc.Decode(&wprr) != nil { @@ -2354,26 +2323,37 @@ func (b *Bus) rhpFormHandler(jc jape.Context) { } 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 + var revision rhpv2.ContractRevision if b.isPassedV2AllowHeight() { panic("not implemented") } else { - // form the contract var txnSet []types.Transaction - contract, txnSet, err = b.rhp2.FormContract( + revision, txnSet, err = b.formContract( ctx, + settings, rfr.RenterAddress, - b.deriveRenterKey(rfr.HostKey), - rfr.HostKey, - rfr.HostIP, rfr.RenterFunds, rfr.HostCollateral, + rfr.HostKey, + rfr.HostIP, rfr.EndHeight, - gc, - b.prepareForm, ) if jc.Check("couldn't form contract", err) != nil { + b.w.ReleaseInputs(txnSet, nil) return } @@ -2389,10 +2369,10 @@ func (b *Bus) rhpFormHandler(jc jape.Context) { } // store the contract - _, err = b.ms.AddContract( + contract, err := b.ms.AddContract( ctx, - contract, - contract.Revision.MissedHostPayout().Sub(rfr.HostCollateral), + revision, + revision.Revision.MissedHostPayout().Sub(rfr.HostCollateral), rfr.RenterFunds, b.cm.Tip().Height, api.ContractStatePending, @@ -2401,14 +2381,17 @@ func (b *Bus) rhpFormHandler(jc jape.Context) { return } - // return the contract ID - jc.Encode(contract.ID()) + // return the contract + jc.Encode(contract) } -func (b *Bus) prepareForm(ctx context.Context, renterAddress types.Address, renterKey types.PublicKey, renterFunds, hostCollateral types.Currency, hostKey types.PublicKey, hostSettings rhpv2.HostSettings, endHeight uint64) ([]types.Transaction, func(types.Transaction), error) { +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, []types.Transaction, error) { + // derive the renter key + renterKey := b.deriveRenterKey(hostKey) + // prepare the transaction cs := b.cm.TipState() - fc := rhpv2.PrepareContractFormation(renterKey, hostKey, renterFunds, hostCollateral, endHeight, hostSettings, renterAddress) + fc := rhpv2.PrepareContractFormation(renterKey.PublicKey(), hostKey, renterFunds, hostCollateral, endHeight, hostSettings, renterAddress) txn := types.Transaction{FileContracts: []types.FileContract{fc}} // calculate the miner fee @@ -2419,59 +2402,15 @@ func (b *Bus) prepareForm(ctx context.Context, renterAddress types.Address, rent cost := rhpv2.ContractFormationCost(cs, fc, hostSettings.ContractPrice).Add(fee) toSign, err := b.w.FundTransaction(&txn, cost, true) if err != nil { - return nil, nil, fmt.Errorf("couldn't fund transaction: %w", err) + return rhpv2.ContractRevision{}, nil, fmt.Errorf("couldn't fund transaction: %w", err) } // sign the transaction b.w.SignTransaction(&txn, toSign, wallet.ExplicitCoveredFields(txn)) - txns := append(b.cm.UnconfirmedParents(txn), txn) - return txns, func(txn types.Transaction) { b.w.ReleaseInputs(txns, nil) }, nil -} - -// nolint: unused -func (b *Bus) prepareFormV2(_ context.Context, renterAddress types.Address, renterKey types.PublicKey, renterFunds, hostCollateral types.Currency, hostKey types.PublicKey, hostSettings rhpv2.HostSettings, endHeight uint64) ([]types.V2Transaction, func(types.V2Transaction), error) { - hostFunds := hostSettings.ContractPrice.Add(hostCollateral) - - // prepare the transaction - cs := b.cm.TipState() - fc := types.V2FileContract{ - RevisionNumber: 0, - Filesize: 0, - FileMerkleRoot: types.Hash256{}, - ProofHeight: endHeight + hostSettings.WindowSize, - ExpirationHeight: endHeight + hostSettings.WindowSize + 10, - RenterOutput: types.SiacoinOutput{ - Value: renterFunds, - Address: renterAddress, - }, - HostOutput: types.SiacoinOutput{ - Value: hostFunds, - Address: hostSettings.Address, - }, - MissedHostValue: hostFunds, - TotalCollateral: hostFunds, - RenterPublicKey: renterKey, - HostPublicKey: hostKey, - } - txn := types.V2Transaction{FileContracts: []types.V2FileContract{fc}} - - // calculate the miner fee - fee := b.cm.RecommendedFee().Mul64(cs.V2TransactionWeight(txn)) - txn.MinerFee = fee - - // fund the transaction - fundAmount := cs.V2FileContractTax(fc).Add(hostFunds).Add(renterFunds).Add(fee) - cs, toSign, err := b.w.FundV2Transaction(&txn, fundAmount, false) - if err != nil { - return nil, nil, fmt.Errorf("couldn't fund transaction: %w", err) - } - - // sign the transaction - b.w.SignV2Inputs(cs, &txn, toSign) - - txns := append(b.cm.V2UnconfirmedParents(txn), txn) - return txns, func(txn types.V2Transaction) { b.w.ReleaseInputs(nil, txns) }, nil + // form the contract + txnSet := append(b.cm.UnconfirmedParents(txn), txn) + return b.rhp2.FormContract(ctx, hostKey, hostIP, renterKey, txnSet) } func (b *Bus) isPassedV2AllowHeight() bool { diff --git a/internal/rhp/v2/rhp.go b/internal/rhp/v2/rhp.go index 8b2c1da6e..c2454c13d 100644 --- a/internal/rhp/v2/rhp.go +++ b/internal/rhp/v2/rhp.go @@ -71,9 +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) - PrepareV2FormFn func(ctx context.Context, renterAddress types.Address, renterKey types.PublicKey, renterFunds, hostCollateral types.Currency, hostKey types.PublicKey, hostSettings rhpv2.HostSettings, endHeight uint64) (txns []types.V2Transaction, discard func(types.V2Transaction), err error) ) type Client struct { @@ -158,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 08bc6866a..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) @@ -580,7 +582,7 @@ func newTestBus(ctx context.Context, dir string, cfg config.Bus, cfgDb dbConfig, announcementMaxAgeHours := time.Duration(cfg.AnnouncementMaxAgeHours) * time.Hour 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 { @@ -593,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..fb9217cc0 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) @@ -1589,7 +1587,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/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/internal/test/e2e/rhp_test.go b/internal/test/e2e/rhp_test.go index d6530998f..08f97b5e1 100644 --- a/internal/test/e2e/rhp_test.go +++ b/internal/test/e2e/rhp_test.go @@ -37,11 +37,12 @@ func TestRHPForm(t *testing.T) { // form a contract using the bus cs, _ := b.ConsensusState(context.Background()) wallet, _ := b.Wallet(context.Background()) - fcid, err := b.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)) + 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 the contract is added to the bus - _, err = b.Contract(context.Background(), fcid) + // assert the contract was added to the bus + _, err = b.Contract(context.Background(), contract.ID) tt.OK(err) // mine to the renew window @@ -63,8 +64,8 @@ func TestRHPForm(t *testing.T) { if len(contracts) != 1 { return fmt.Errorf("unexpected number of contracts %d != 1", len(contracts)) } - if contracts[0].RenewedFrom != fcid { - return fmt.Errorf("contract wasn't renewed %v != %v", contracts[0].RenewedFrom, fcid) + 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 @@ -76,6 +77,6 @@ func TestRHPForm(t *testing.T) { 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", fcid, contracts[0].ID) + t.Fatalf("expected contract %v, got %v", contract.ID, contracts[0].ID) } } 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 3082f7842..111300ff2 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -154,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 } @@ -386,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() @@ -1316,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, From a00ac2b96344db4027fbb45d5f7906a12d1631f9 Mon Sep 17 00:00:00 2001 From: PJ Date: Wed, 21 Aug 2024 13:43:52 +0200 Subject: [PATCH 4/9] bus: update form contract route --- api/contract.go | 10 ++++++++ api/worker.go | 10 -------- bus/bus.go | 3 +-- bus/client/contracts.go | 2 +- bus/routes.go | 51 ++++++++++++++++++++++------------------- 5 files changed, 39 insertions(+), 37 deletions(-) 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/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/bus/bus.go b/bus/bus.go index f4082d3dd..c4deb1c98 100644 --- a/bus/bus.go +++ b/bus/bus.go @@ -421,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, @@ -479,8 +480,6 @@ func (b *Bus) Handler() http.Handler { "POST /slabbuffer/done": b.packedSlabsHandlerDonePOST, "POST /slabbuffer/fetch": b.packedSlabsHandlerFetchPOST, - "POST /rhp/form": b.rhpFormHandler, - "POST /search/hosts": b.searchHostsHandlerPOST, "GET /search/objects": b.searchObjectsHandlerGET, diff --git a/bus/client/contracts.go b/bus/client/contracts.go index d1c2d8006..57245afd3 100644 --- a/bus/client/contracts.go +++ b/bus/client/contracts.go @@ -120,7 +120,7 @@ func (c *Client) DeleteContractSet(ctx context.Context, set string) (err error) // 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("/rhp/form", api.RHPFormRequest{ + err = c.c.WithContext(ctx).POST("/contracts", api.ContractFormRequest{ EndHeight: endHeight, HostCollateral: hostCollateral, HostKey: hostKey, diff --git a/bus/routes.go b/bus/routes.go index c59e2faf7..a76d862e5 100644 --- a/bus/routes.go +++ b/bus/routes.go @@ -2284,13 +2284,13 @@ func (b *Bus) multipartHandlerListPartsPOST(jc jape.Context) { jc.Encode(resp) } -func (b *Bus) rhpFormHandler(jc jape.Context) { +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.RHPFormRequest + var rfr api.ContractFormRequest if jc.Decode(&rfr) != nil { return } @@ -2337,12 +2337,11 @@ func (b *Bus) rhpFormHandler(jc jape.Context) { } // send V2 transaction if we're passed the V2 hardfork allow height - var revision rhpv2.ContractRevision + var contract rhpv2.ContractRevision if b.isPassedV2AllowHeight() { panic("not implemented") } else { - var txnSet []types.Transaction - revision, txnSet, err = b.formContract( + contract, err = b.formContract( ctx, settings, rfr.RenterAddress, @@ -2353,26 +2352,15 @@ func (b *Bus) rhpFormHandler(jc jape.Context) { rfr.EndHeight, ) if jc.Check("couldn't form contract", err) != nil { - b.w.ReleaseInputs(txnSet, nil) return } - - // add transaction set to the pool - _, err := b.cm.AddPoolTransactions(txnSet) - if jc.Check("couldn't broadcast transaction set", err) != nil { - b.w.ReleaseInputs(txnSet, nil) - return - } - - // broadcast the transaction set - b.s.BroadcastTransactionSet(txnSet) } // store the contract - contract, err := b.ms.AddContract( + metadata, err := b.ms.AddContract( ctx, - revision, - revision.Revision.MissedHostPayout().Sub(rfr.HostCollateral), + contract, + contract.Revision.MissedHostPayout().Sub(rfr.HostCollateral), rfr.RenterFunds, b.cm.Tip().Height, api.ContractStatePending, @@ -2382,10 +2370,10 @@ func (b *Bus) rhpFormHandler(jc jape.Context) { } // return the contract - jc.Encode(contract) + jc.Encode(metadata) } -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, []types.Transaction, error) { +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) @@ -2402,15 +2390,30 @@ func (b *Bus) formContract(ctx context.Context, hostSettings rhpv2.HostSettings, cost := rhpv2.ContractFormationCost(cs, fc, hostSettings.ContractPrice).Add(fee) toSign, err := b.w.FundTransaction(&txn, cost, true) if err != nil { - return rhpv2.ContractRevision{}, nil, fmt.Errorf("couldn't fund transaction: %w", err) + 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 - txnSet := append(b.cm.UnconfirmedParents(txn), txn) - return b.rhp2.FormContract(ctx, hostKey, hostIP, renterKey, txnSet) + 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 } func (b *Bus) isPassedV2AllowHeight() bool { From 5886648ef58933aba7cff809ebb0b69b30274126 Mon Sep 17 00:00:00 2001 From: PJ Date: Wed, 21 Aug 2024 13:46:33 +0200 Subject: [PATCH 5/9] bus: re-add AddContract to bus client --- bus/client/contracts.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bus/client/contracts.go b/bus/client/contracts.go index 57245afd3..bb3b16b4c 100644 --- a/bus/client/contracts.go +++ b/bus/client/contracts.go @@ -11,6 +11,18 @@ import ( "go.sia.tech/renterd/api" ) +// AddContract adds the provided contract to the metadata store. +func (c *Client) AddContract(ctx context.Context, contract rhpv2.ContractRevision, contractPrice, totalCost types.Currency, startHeight uint64, state string) (added api.ContractMetadata, err error) { + err = c.c.WithContext(ctx).POST(fmt.Sprintf("/contract/%s", contract.ID()), api.ContractAddRequest{ + Contract: contract, + StartHeight: startHeight, + ContractPrice: contractPrice, + State: state, + TotalCost: totalCost, + }, &added) + return +} + // AddRenewedContract adds the provided contract to the metadata store. func (c *Client) AddRenewedContract(ctx context.Context, contract rhpv2.ContractRevision, contractPrice, totalCost types.Currency, startHeight uint64, renewedFrom types.FileContractID, state string) (renewed api.ContractMetadata, err error) { err = c.c.WithContext(ctx).POST(fmt.Sprintf("/contract/%s/renewed", contract.ID()), api.ContractRenewedRequest{ From f0fdce44226c07d74b71772166a6f2d9319c4c68 Mon Sep 17 00:00:00 2001 From: PJ Date: Wed, 21 Aug 2024 13:51:11 +0200 Subject: [PATCH 6/9] testing: move and rename TestRHPForm --- internal/test/e2e/{rhp_test.go => contracts_test.go} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename internal/test/e2e/{rhp_test.go => contracts_test.go} (98%) diff --git a/internal/test/e2e/rhp_test.go b/internal/test/e2e/contracts_test.go similarity index 98% rename from internal/test/e2e/rhp_test.go rename to internal/test/e2e/contracts_test.go index 08f97b5e1..fcafdd2ac 100644 --- a/internal/test/e2e/rhp_test.go +++ b/internal/test/e2e/contracts_test.go @@ -12,7 +12,7 @@ import ( "go.uber.org/zap/zapcore" ) -func TestRHPForm(t *testing.T) { +func TestFormContract(t *testing.T) { // configure the autopilot not to form any contracts apSettings := test.AutopilotConfig apSettings.Contracts.Amount = 0 From e225505df09442988720e2c3a8be2c33b7fea0b4 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Thu, 22 Aug 2024 11:34:53 +0200 Subject: [PATCH 7/9] e2e: fix TestEphemeralAccounts --- internal/test/e2e/cluster_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/test/e2e/cluster_test.go b/internal/test/e2e/cluster_test.go index fb9217cc0..255999911 100644 --- a/internal/test/e2e/cluster_test.go +++ b/internal/test/e2e/cluster_test.go @@ -1157,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})) From 59b04ddac0b695e701557bedc0ef76e0a8fed0b9 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Thu, 22 Aug 2024 10:42:42 +0200 Subject: [PATCH 8/9] bus: make sure formed contracts are added to the worker cache --- bus/bus.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ bus/routes.go | 62 ++---------------------------------------------- 2 files changed, 67 insertions(+), 60 deletions(-) diff --git a/bus/bus.go b/bus/bus.go index c4deb1c98..c5ae1113e 100644 --- a/bus/bus.go +++ b/bus/bus.go @@ -542,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 { diff --git a/bus/routes.go b/bus/routes.go index a76d862e5..f020c5944 100644 --- a/bus/routes.go +++ b/bus/routes.go @@ -948,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) } @@ -2357,7 +2347,7 @@ func (b *Bus) contractsFormHandler(jc jape.Context) { } // store the contract - metadata, err := b.ms.AddContract( + metadata, err := b.addContract( ctx, contract, contract.Revision.MissedHostPayout().Sub(rfr.HostCollateral), @@ -2372,51 +2362,3 @@ func (b *Bus) contractsFormHandler(jc jape.Context) { // return the contract jc.Encode(metadata) } - -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 -} - -func (b *Bus) isPassedV2AllowHeight() bool { - cs := b.cm.TipState() - return cs.Index.Height >= cs.Network.HardforkV2.AllowHeight -} From 6668d90d31393641c99a135dd9981ad13c8f5007 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Thu, 22 Aug 2024 15:16:25 +0200 Subject: [PATCH 9/9] e2e: fix TestFormContract --- internal/test/e2e/contracts_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/test/e2e/contracts_test.go b/internal/test/e2e/contracts_test.go index fcafdd2ac..25f74fa8d 100644 --- a/internal/test/e2e/contracts_test.go +++ b/internal/test/e2e/contracts_test.go @@ -35,10 +35,10 @@ func TestFormContract(t *testing.T) { tt.OK(err) // form a contract using the bus - cs, _ := b.ConsensusState(context.Background()) wallet, _ := b.Wallet(context.Background()) - 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) + 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