From b905d75b75cfc1d5c843cc9655875a97d8881270 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Wed, 14 Aug 2024 17:36:15 +0200 Subject: [PATCH 1/7] tmp --- worker/rhpv2.go => internal/rhp/v2/rhp.go | 45 +++++------------------ worker/worker.go | 29 +++++++++++++++ 2 files changed, 38 insertions(+), 36 deletions(-) rename worker/rhpv2.go => internal/rhp/v2/rhp.go (95%) diff --git a/worker/rhpv2.go b/internal/rhp/v2/rhp.go similarity index 95% rename from worker/rhpv2.go rename to internal/rhp/v2/rhp.go index 048e42962..a5b01284f 100644 --- a/worker/rhpv2.go +++ b/internal/rhp/v2/rhp.go @@ -1,4 +1,4 @@ -package worker +package rhp import ( "context" @@ -8,7 +8,6 @@ import ( "fmt" "math" "sort" - "strings" "time" rhpv2 "go.sia.tech/core/rhp/v2" @@ -60,33 +59,7 @@ var ( ErrNoSectorsToPrune = errors.New("no sectors to prune") ) -// A HostErrorSet is a collection of errors from various hosts. -type HostErrorSet map[types.PublicKey]error - -// NumGouging returns numbers of host that errored out due to price gouging. -func (hes HostErrorSet) NumGouging() (n int) { - for _, he := range hes { - if errors.Is(he, errPriceTableGouging) { - n++ - } - } - return -} - -// Error implements error. -func (hes HostErrorSet) Error() string { - if len(hes) == 0 { - return "" - } - - var strs []string - for hk, he := range hes { - strs = append(strs, fmt.Sprintf("%x: %v", hk[:4], he.Error())) - } - - // include a leading newline so that the first error isn't printed on the - // same line as the error context - return "\n" + strings.Join(strs, "\n") +type Client struct { } func wrapErr(ctx context.Context, fnName string, err *error) { @@ -245,7 +218,7 @@ func RPCFormContract(ctx context.Context, t *rhpv2.Transport, renterKey types.Pr // FetchSignedRevision fetches the latest signed revision for a contract from a host. // TODO: stop using rhpv2 and upgrade to newer protocol when possible. -func (w *worker) FetchSignedRevision(ctx context.Context, hostIP string, hostKey types.PublicKey, renterKey types.PrivateKey, contractID types.FileContractID, timeout time.Duration) (rhpv2.ContractRevision, error) { +func (w *Client) FetchSignedRevision(ctx context.Context, hostIP string, hostKey types.PublicKey, renterKey types.PrivateKey, contractID types.FileContractID, timeout time.Duration) (rhpv2.ContractRevision, error) { var rev rhpv2.ContractRevision err := w.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { req := &rhpv2.RPCLockRequest{ @@ -289,7 +262,7 @@ func (w *worker) FetchSignedRevision(ctx context.Context, hostIP string, hostKey return rev, err } -func (w *worker) PruneContract(ctx context.Context, hostIP string, hostKey types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64) (deleted, remaining uint64, err error) { +func (w *Client) PruneContract(ctx context.Context, hostIP string, hostKey types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64) (deleted, remaining uint64, err error) { err = w.withContractLock(ctx, fcid, lockingPriorityPruning, func() error { return w.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { return w.withRevisionV2(defaultLockTimeout, t, hostKey, fcid, lastKnownRevisionNumber, func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) (err error) { @@ -347,7 +320,7 @@ func (w *worker) PruneContract(ctx context.Context, hostIP string, hostKey types return } -func (w *worker) deleteContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevision, settings rhpv2.HostSettings, indices []uint64) (deleted uint64, err error) { +func (w *Client) deleteContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevision, settings rhpv2.HostSettings, indices []uint64) (deleted uint64, err error) { id := frand.Entropy128() logger := w.logger. With("id", hex.EncodeToString(id[:])). @@ -529,7 +502,7 @@ func (w *worker) deleteContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevi return } -func (w *worker) FetchContractRoots(ctx context.Context, hostIP string, hostKey types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64) (roots []types.Hash256, err error) { +func (w *Client) FetchContractRoots(ctx context.Context, hostIP string, hostKey types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64) (roots []types.Hash256, err error) { err = w.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { return w.withRevisionV2(defaultLockTimeout, t, hostKey, fcid, lastKnownRevisionNumber, func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) (err error) { gc, err := GougingCheckerFromContext(ctx, false) @@ -546,7 +519,7 @@ func (w *worker) FetchContractRoots(ctx context.Context, hostIP string, hostKey return } -func (w *worker) fetchContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevision, settings rhpv2.HostSettings) (roots []types.Hash256, _ error) { +func (w *Client) fetchContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevision, settings rhpv2.HostSettings) (roots []types.Hash256, _ error) { // derive the renter key renterKey := w.deriveRenterKey(rev.HostKey()) @@ -634,7 +607,7 @@ func (w *worker) fetchContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevis return } -func (w *worker) withTransportV2(ctx context.Context, hostKey types.PublicKey, hostIP string, fn func(*rhpv2.Transport) error) (err error) { +func (w *Client) withTransportV2(ctx context.Context, hostKey types.PublicKey, hostIP string, fn func(*rhpv2.Transport) error) (err error) { conn, err := dial(ctx, hostIP) if err != nil { return err @@ -667,7 +640,7 @@ func (w *worker) withTransportV2(ctx context.Context, hostKey types.PublicKey, h return fn(t) } -func (w *worker) withRevisionV2(lockTimeout time.Duration, t *rhpv2.Transport, hk types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64, fn func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) error) error { +func (w *Client) withRevisionV2(lockTimeout time.Duration, t *rhpv2.Transport, hk types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64, fn func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) error) error { renterKey := w.deriveRenterKey(hk) // execute lock RPC diff --git a/worker/worker.go b/worker/worker.go index fb4f8c49d..e0c81dded 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -1766,3 +1766,32 @@ func (w *worker) prepareUploadParams(ctx context.Context, bucket string, contrac } return up, nil } + +// A HostErrorSet is a collection of errors from various hosts. +type HostErrorSet map[types.PublicKey]error + +// NumGouging returns numbers of host that errored out due to price gouging. +func (hes HostErrorSet) NumGouging() (n int) { + for _, he := range hes { + if errors.Is(he, errPriceTableGouging) { + n++ + } + } + return +} + +// Error implements error. +func (hes HostErrorSet) Error() string { + if len(hes) == 0 { + return "" + } + + var strs []string + for hk, he := range hes { + strs = append(strs, fmt.Sprintf("%x: %v", hk[:4], he.Error())) + } + + // include a leading newline so that the first error isn't printed on the + // same line as the error context + return "\n" + strings.Join(strs, "\n") +} From 033d5bf6e70810698c22b7bb52ac289f79134ebe Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Thu, 15 Aug 2024 14:06:38 +0200 Subject: [PATCH 2/7] rhp: v2 client --- internal/rhp/v2/rhp.go | 548 ++++++++++++++++++--------------------- internal/rhp/v2/rpc.go | 115 ++++++++ internal/utils/errors.go | 14 + worker/gouging.go | 63 +++-- worker/rhpv3.go | 62 ++++- worker/worker.go | 97 +++---- 6 files changed, 504 insertions(+), 395 deletions(-) create mode 100644 internal/rhp/v2/rpc.go diff --git a/internal/rhp/v2/rhp.go b/internal/rhp/v2/rhp.go index a5b01284f..d17dfa5cf 100644 --- a/internal/rhp/v2/rhp.go +++ b/internal/rhp/v2/rhp.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "math" + "net" "sort" "time" @@ -14,10 +15,17 @@ import ( "go.sia.tech/core/types" "go.sia.tech/renterd/api" "go.sia.tech/renterd/internal/utils" + "go.uber.org/zap" "lukechampine.com/frand" ) const ( + batchSizeDeleteSectors = uint64(1000) // 4GiB of contract data + batchSizeFetchSectors = uint64(25600) // 100GiB of contract data + + // default lock timeout + defaultLockTimeout = time.Minute + // minMessageSize is the minimum size of an RPC message minMessageSize = 4096 @@ -28,14 +36,14 @@ const ( ) var ( - // ErrInsufficientFunds is returned by various RPCs when the renter is - // unable to provide sufficient payment to the host. - ErrInsufficientFunds = errors.New("insufficient funds") - // ErrInsufficientCollateral is returned by various RPCs when the host is // unable to provide sufficient collateral. ErrInsufficientCollateral = errors.New("insufficient collateral") + // ErrInsufficientFunds is returned by various RPCs when the renter is + // unable to provide sufficient payment to the host. + ErrInsufficientFunds = errors.New("insufficient funds") + // ErrInvalidMerkleProof is returned by various RPCs when the host supplies // an invalid Merkle proof. ErrInvalidMerkleProof = errors.New("host supplied invalid Merkle proof") @@ -59,165 +67,36 @@ var ( ErrNoSectorsToPrune = errors.New("no sectors to prune") ) -type Client struct { -} - -func wrapErr(ctx context.Context, fnName string, err *error) { - if *err != nil { - *err = fmt.Errorf("%s: %w", fnName, *err) - if cause := context.Cause(ctx); cause != nil && !utils.IsErr(*err, cause) { - *err = fmt.Errorf("%w; %w", cause, *err) - } - } -} - -func hashRevision(rev types.FileContractRevision) types.Hash256 { - h := types.NewHasher() - rev.EncodeTo(h.E) - return h.Sum() -} - -func updateRevisionOutputs(rev *types.FileContractRevision, cost, collateral types.Currency) (valid, missed []types.Currency, err error) { - // allocate new slices; don't want to risk accidentally sharing memory - rev.ValidProofOutputs = append([]types.SiacoinOutput(nil), rev.ValidProofOutputs...) - rev.MissedProofOutputs = append([]types.SiacoinOutput(nil), rev.MissedProofOutputs...) - - // move valid payout from renter to host - var underflow, overflow bool - rev.ValidProofOutputs[0].Value, underflow = rev.ValidProofOutputs[0].Value.SubWithUnderflow(cost) - rev.ValidProofOutputs[1].Value, overflow = rev.ValidProofOutputs[1].Value.AddWithOverflow(cost) - if underflow || overflow { - err = errors.New("insufficient funds to pay host") - return - } +type ( + ContractRootsFn func(ctx context.Context, id types.FileContractID) ([]types.Hash256, []types.Hash256, error) - // move missed payout from renter to void - rev.MissedProofOutputs[0].Value, underflow = rev.MissedProofOutputs[0].Value.SubWithUnderflow(cost) - rev.MissedProofOutputs[2].Value, overflow = rev.MissedProofOutputs[2].Value.AddWithOverflow(cost) - if underflow || overflow { - err = errors.New("insufficient funds to move missed payout to void") - return - } + GougingCheckFn func(settings rhpv2.HostSettings) api.HostGougingBreakdown - // move collateral from host to void - rev.MissedProofOutputs[1].Value, underflow = rev.MissedProofOutputs[1].Value.SubWithUnderflow(collateral) - rev.MissedProofOutputs[2].Value, overflow = rev.MissedProofOutputs[2].Value.AddWithOverflow(collateral) - if underflow || overflow { - err = errors.New("insufficient collateral") - return - } + 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) +) - return []types.Currency{rev.ValidProofOutputs[0].Value, rev.ValidProofOutputs[1].Value}, - []types.Currency{rev.MissedProofOutputs[0].Value, rev.MissedProofOutputs[1].Value, rev.MissedProofOutputs[2].Value}, nil +type Client struct { + logger *zap.SugaredLogger } -// RPCSettings calls the Settings RPC, returning the host's reported settings. -func RPCSettings(ctx context.Context, t *rhpv2.Transport) (settings rhpv2.HostSettings, err error) { - defer wrapErr(ctx, "Settings", &err) - - var resp rhpv2.RPCSettingsResponse - if err := t.Call(rhpv2.RPCSettingsID, nil, &resp); err != nil { - return rhpv2.HostSettings{}, err - } else if err := json.Unmarshal(resp.Settings, &settings); err != nil { - return rhpv2.HostSettings{}, fmt.Errorf("couldn't unmarshal json: %w", err) +func New(logger *zap.Logger) *Client { + return &Client{ + logger: logger.Sugar().Named("rhp2"), } - - return settings, nil } -// RPCFormContract forms a contract with a host. -func RPCFormContract(ctx context.Context, t *rhpv2.Transport, renterKey types.PrivateKey, txnSet []types.Transaction) (_ rhpv2.ContractRevision, _ []types.Transaction, err error) { - defer wrapErr(ctx, "FormContract", &err) - - // strip our signatures before sending - parents, txn := txnSet[:len(txnSet)-1], txnSet[len(txnSet)-1] - renterContractSignatures := txn.Signatures - txnSet[len(txnSet)-1].Signatures = nil - - // create request - renterPubkey := renterKey.PublicKey() - req := &rhpv2.RPCFormContractRequest{ - Transactions: txnSet, - RenterKey: renterPubkey.UnlockKey(), - } - if err := t.WriteRequest(rhpv2.RPCFormContractID, req); err != nil { - return rhpv2.ContractRevision{}, nil, err - } - - // execute form contract RPC - var resp rhpv2.RPCFormContractAdditions - if err := t.ReadResponse(&resp, 65536); err != nil { - return rhpv2.ContractRevision{}, nil, err - } - - // merge host additions with txn - txn.SiacoinInputs = append(txn.SiacoinInputs, resp.Inputs...) - txn.SiacoinOutputs = append(txn.SiacoinOutputs, resp.Outputs...) - - // create initial (no-op) revision, transaction, and signature - fc := txn.FileContracts[0] - initRevision := types.FileContractRevision{ - ParentID: txn.FileContractID(0), - UnlockConditions: types.UnlockConditions{ - PublicKeys: []types.UnlockKey{ - renterPubkey.UnlockKey(), - t.HostKey().UnlockKey(), - }, - SignaturesRequired: 2, - }, - FileContract: types.FileContract{ - RevisionNumber: 1, - Filesize: fc.Filesize, - FileMerkleRoot: fc.FileMerkleRoot, - WindowStart: fc.WindowStart, - WindowEnd: fc.WindowEnd, - ValidProofOutputs: fc.ValidProofOutputs, - MissedProofOutputs: fc.MissedProofOutputs, - UnlockHash: fc.UnlockHash, - }, - } - revSig := renterKey.SignHash(hashRevision(initRevision)) - renterRevisionSig := types.TransactionSignature{ - ParentID: types.Hash256(initRevision.ParentID), - CoveredFields: types.CoveredFields{FileContractRevisions: []uint64{0}}, - PublicKeyIndex: 0, - Signature: revSig[:], - } - - // write our signatures - renterSigs := &rhpv2.RPCFormContractSignatures{ - ContractSignatures: renterContractSignatures, - RevisionSignature: renterRevisionSig, - } - if err := t.WriteResponse(renterSigs); err != nil { - return rhpv2.ContractRevision{}, nil, err - } - - // read the host's signatures and merge them with our own - var hostSigs rhpv2.RPCFormContractSignatures - if err := t.ReadResponse(&hostSigs, minMessageSize); err != nil { - return rhpv2.ContractRevision{}, nil, err - } - - txn.Signatures = make([]types.TransactionSignature, 0, len(renterContractSignatures)+len(hostSigs.ContractSignatures)) - txn.Signatures = append(txn.Signatures, renterContractSignatures...) - txn.Signatures = append(txn.Signatures, hostSigs.ContractSignatures...) - - signedTxnSet := make([]types.Transaction, 0, len(resp.Parents)+len(parents)+1) - signedTxnSet = append(signedTxnSet, resp.Parents...) - signedTxnSet = append(signedTxnSet, parents...) - signedTxnSet = append(signedTxnSet, txn) - return rhpv2.ContractRevision{ - Revision: initRevision, - Signatures: [2]types.TransactionSignature{ - renterRevisionSig, - hostSigs.RevisionSignature, - }, - }, signedTxnSet, nil +func (w *Client) FetchContractRoots(ctx context.Context, renterKey types.PrivateKey, gougingCheck GougingCheckFn, hostIP string, hostKey types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64) (roots []types.Hash256, revision *types.FileContractRevision, cost types.Currency, err error) { + err = w.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { + return w.withRevisionV2(renterKey, gougingCheck, t, fcid, lastKnownRevisionNumber, func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) (err error) { + roots, cost, err = w.fetchContractRoots(t, renterKey, &rev, settings) + revision = &rev.Revision + return + }) + }) + return } // FetchSignedRevision fetches the latest signed revision for a contract from a host. -// TODO: stop using rhpv2 and upgrade to newer protocol when possible. func (w *Client) FetchSignedRevision(ctx context.Context, hostIP string, hostKey types.PublicKey, renterKey types.PrivateKey, contractID types.FileContractID, timeout time.Duration) (rhpv2.ContractRevision, error) { var rev rhpv2.ContractRevision err := w.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { @@ -262,67 +141,107 @@ func (w *Client) FetchSignedRevision(ctx context.Context, hostIP string, hostKey return rev, err } -func (w *Client) PruneContract(ctx context.Context, hostIP string, hostKey types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64) (deleted, remaining uint64, err error) { - err = w.withContractLock(ctx, fcid, lockingPriorityPruning, func() error { - return w.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { - return w.withRevisionV2(defaultLockTimeout, t, hostKey, fcid, lastKnownRevisionNumber, func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) (err error) { - // perform gouging checks - gc, err := GougingCheckerFromContext(ctx, false) - if err != nil { - return err - } - if breakdown := gc.Check(&settings, nil); breakdown.Gouging() { - return fmt.Errorf("failed to prune contract: %v", breakdown) - } +func (c *Client) FetchSettings(ctx context.Context, hostKey types.PublicKey, hostIP string) (settings rhpv2.HostSettings, err error) { + err = c.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { + var err error + if settings, err = rpcSettings(ctx, t); err != nil { + return err + } + // NOTE: we overwrite the NetAddress with the host address here + // since we just used it to dial the host we know it's valid + settings.NetAddress = hostIP + return nil + }) + return +} - // delete roots - got, err := w.fetchContractRoots(t, &rev, settings) - if err != nil { - return err - } +func (c *Client) FormContract(ctx context.Context, renterAddress types.Address, renterKey types.PrivateKey, hostKey types.PublicKey, hostIP string, renterFunds, hostCollateral types.Currency, endHeight uint64, checkGouging GougingCheckFn, prepareForm PrepareFormFn) (contract rhpv2.ContractRevision, txnSet []types.Transaction, err error) { + err = c.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) (err error) { + settings, err := rpcSettings(ctx, t) + if err != nil { + return err + } - // fetch the roots from the bus - want, pending, err := w.bus.ContractRoots(ctx, fcid) - if err != nil { - return err - } - keep := make(map[types.Hash256]struct{}) - for _, root := range append(want, pending...) { - keep[root] = struct{}{} - } + if breakdown := checkGouging(settings); breakdown.Gouging() { + return fmt.Errorf("failed to form contract, gouging check failed: %v", breakdown) + } - // collect indices for roots we want to prune - var indices []uint64 - for i, root := range got { - if _, wanted := keep[root]; wanted { - delete(keep, root) // prevent duplicates - continue - } - indices = append(indices, uint64(i)) - } - if len(indices) == 0 { - return fmt.Errorf("%w: database holds %d (%d pending), contract contains %d", ErrNoSectorsToPrune, len(want)+len(pending), len(pending), len(got)) - } + 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 + } + return + }) + return +} + +func (c *Client) PruneContract(ctx context.Context, renterKey types.PrivateKey, gougingCheck GougingCheckFn, hostIP string, hostKey types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64, wantedRoots ContractRootsFn) (revision *types.FileContractRevision, deleted, remaining uint64, cost types.Currency, err error) { + err = c.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { + return c.withRevisionV2(renterKey, gougingCheck, t, fcid, lastKnownRevisionNumber, func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) (err error) { + // fetch roots + got, fetchCost, err := c.fetchContractRoots(t, renterKey, &rev, settings) + if err != nil { + return err + } + + // update cost and revision + cost = cost.Add(fetchCost) + revision = &rev.Revision + + // fetch the roots from the bus + want, pending, err := wantedRoots(ctx, fcid) + if err != nil { + return err + } + keep := make(map[types.Hash256]struct{}) + for _, root := range append(want, pending...) { + keep[root] = struct{}{} + } - // delete the roots from the contract - deleted, err = w.deleteContractRoots(t, &rev, settings, indices) - if deleted < uint64(len(indices)) { - remaining = uint64(len(indices)) - deleted + // collect indices for roots we want to prune + var indices []uint64 + for i, root := range got { + if _, wanted := keep[root]; wanted { + delete(keep, root) // prevent duplicates + continue } + indices = append(indices, uint64(i)) + } + if len(indices) == 0 { + return fmt.Errorf("%w: database holds %d (%d pending), contract contains %d", ErrNoSectorsToPrune, len(want)+len(pending), len(pending), len(got)) + } - // return sizes instead of number of roots - deleted *= rhpv2.SectorSize - remaining *= rhpv2.SectorSize - return - }) + // delete the roots from the contract + var deleteCost types.Currency + deleted, deleteCost, err = c.deleteContractRoots(t, renterKey, &rev, settings, indices) + if deleted < uint64(len(indices)) { + remaining = uint64(len(indices)) - deleted + } + + // update cost and revision + if deleted > 0 { + cost = cost.Add(deleteCost) + revision = &rev.Revision + } + + // return sizes instead of number of roots + deleted *= rhpv2.SectorSize + remaining *= rhpv2.SectorSize + return }) }) return } -func (w *Client) deleteContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevision, settings rhpv2.HostSettings, indices []uint64) (deleted uint64, err error) { +func (c *Client) deleteContractRoots(t *rhpv2.Transport, renterKey types.PrivateKey, rev *rhpv2.ContractRevision, settings rhpv2.HostSettings, indices []uint64) (deleted uint64, cost types.Currency, err error) { id := frand.Entropy128() - logger := w.logger. + logger := c.logger. With("id", hex.EncodeToString(id[:])). With("hostKey", rev.HostKey()). With("hostVersion", settings.Version). @@ -333,7 +252,7 @@ func (w *Client) deleteContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevi // return early if len(indices) == 0 { - return 0, nil + return 0, types.ZeroCurrency, nil } // sort in descending order so that we can use 'range' @@ -362,17 +281,14 @@ func (w *Client) deleteContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevi } } - // derive the renter key - renterKey := w.deriveRenterKey(rev.HostKey()) - // range over the batches and delete the sectors batch per batch for i, batch := range batches { if err = func() error { - var cost types.Currency + var batchCost types.Currency start := time.Now() logger.Infow(fmt.Sprintf("starting batch %d/%d of size %d", i+1, len(batches), len(batch))) defer func() { - logger.Infow(fmt.Sprintf("processing batch %d/%d of size %d took %v", i+1, len(batches), len(batch), time.Since(start)), "cost", cost) + logger.Infow(fmt.Sprintf("processing batch %d/%d of size %d took %v", i+1, len(batches), len(batch), time.Since(start)), "cost", batchCost) }() numSectors := rev.NumSectors() @@ -402,7 +318,7 @@ func (w *Client) deleteContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevi if err != nil { return err } - cost, _ = rpcCost.Total() + batchCost, _ = rpcCost.Total() // NOTE: we currently overpay hosts by quite a large margin (~10x) // to ensure we cover both 1.5.9 and pre v0.2.1 hosts. @@ -411,11 +327,11 @@ func (w *Client) deleteContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevi // host release in the scoring and stop using old hosts proofSize := (128 + uint64(len(actions))) * rhpv2.LeafSize compatCost := settings.BaseRPCPrice.Add(settings.DownloadBandwidthPrice.Mul64(proofSize)) - if cost.Cmp(compatCost) < 0 { - cost = compatCost + if batchCost.Cmp(compatCost) < 0 { + batchCost = compatCost } - if rev.RenterFunds().Cmp(cost) < 0 { + if rev.RenterFunds().Cmp(batchCost) < 0 { return ErrInsufficientFunds } @@ -429,7 +345,7 @@ func (w *Client) deleteContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevi rev.Revision.Filesize -= rhpv2.SectorSize * actions[len(actions)-1].A // update the revision outputs - newValid, newMissed, err := updateRevisionOutputs(&rev.Revision, cost, types.ZeroCurrency) + newRevision, err := updatedRevision(rev.Revision, batchCost, types.ZeroCurrency) if err != nil { return err } @@ -439,9 +355,16 @@ func (w *Client) deleteContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevi Actions: actions, MerkleProof: true, - RevisionNumber: rev.Revision.RevisionNumber, - ValidProofValues: newValid, - MissedProofValues: newMissed, + RevisionNumber: rev.Revision.RevisionNumber, + ValidProofValues: []types.Currency{ + newRevision.ValidProofOutputs[0].Value, + newRevision.ValidProofOutputs[0].Value, + }, + MissedProofValues: []types.Currency{ + newRevision.MissedProofOutputs[0].Value, + newRevision.MissedProofOutputs[1].Value, + newRevision.MissedProofOutputs[2].Value, + }, } // send request and read merkle proof @@ -457,8 +380,8 @@ func (w *Client) deleteContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevi // verify proof proofHashes := merkleResp.OldSubtreeHashes leafHashes := merkleResp.OldLeafHashes - oldRoot, newRoot := types.Hash256(rev.Revision.FileMerkleRoot), merkleResp.NewMerkleRoot - if rev.Revision.Filesize > 0 && !rhpv2.VerifyDiffProof(actions, numSectors, proofHashes, leafHashes, oldRoot, newRoot, nil) { + oldRoot, newRoot := types.Hash256(newRevision.FileMerkleRoot), merkleResp.NewMerkleRoot + if newRevision.Filesize > 0 && !rhpv2.VerifyDiffProof(actions, numSectors, proofHashes, leafHashes, oldRoot, newRoot, nil) { err := fmt.Errorf("couldn't verify delete proof, host %v, version %v; %w", rev.HostKey(), settings.Version, ErrInvalidMerkleProof) logger.Infow(fmt.Sprintf("processing batch %d/%d failed, err %v", i+1, len(batches), err)) t.WriteResponseErr(err) @@ -466,10 +389,10 @@ func (w *Client) deleteContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevi } // update merkle root - copy(rev.Revision.FileMerkleRoot[:], newRoot[:]) + copy(newRevision.FileMerkleRoot[:], newRoot[:]) // build the write response - revisionHash := hashRevision(rev.Revision) + revisionHash := hashRevision(newRevision) renterSig := &rhpv2.RPCWriteResponse{ Signature: renterKey.SignHash(revisionHash), } @@ -492,8 +415,9 @@ func (w *Client) deleteContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevi // update deleted count deleted += uint64(len(batch)) - // record spending - w.contractSpendingRecorder.Record(rev.Revision, api.ContractSpending{Deletions: cost}) + // update revision + rev.Revision = newRevision + cost = cost.Add(batchCost) return nil }(); err != nil { return @@ -502,27 +426,7 @@ func (w *Client) deleteContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevi return } -func (w *Client) FetchContractRoots(ctx context.Context, hostIP string, hostKey types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64) (roots []types.Hash256, err error) { - err = w.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { - return w.withRevisionV2(defaultLockTimeout, t, hostKey, fcid, lastKnownRevisionNumber, func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) (err error) { - gc, err := GougingCheckerFromContext(ctx, false) - if err != nil { - return err - } - if breakdown := gc.Check(&settings, nil); breakdown.Gouging() { - return fmt.Errorf("failed to list contract roots: %v", breakdown) - } - roots, err = w.fetchContractRoots(t, &rev, settings) - return - }) - }) - return -} - -func (w *Client) fetchContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevision, settings rhpv2.HostSettings) (roots []types.Hash256, _ error) { - // derive the renter key - renterKey := w.deriveRenterKey(rev.HostKey()) - +func (c *Client) fetchContractRoots(t *rhpv2.Transport, renterKey types.PrivateKey, rev *rhpv2.ContractRevision, settings rhpv2.HostSettings) (roots []types.Hash256, cost types.Currency, _ error) { // download the full set of SectorRoots numsectors := rev.NumSectors() for offset := uint64(0); offset < numsectors; { @@ -532,7 +436,7 @@ func (w *Client) fetchContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevis } // calculate the cost - cost, _ := settings.RPCSectorRootsCost(offset, n).Total() + batchCost, _ := settings.RPCSectorRootsCost(offset, n).Total() // TODO: remove once host network is updated if utils.VersionCmp(settings.Version, "1.6.0") < 0 { @@ -542,113 +446,86 @@ func (w *Client) fetchContractRoots(t *rhpv2.Transport, rev *rhpv2.ContractRevis if responseSize < minMessageSize { responseSize = minMessageSize } - cost = settings.BaseRPCPrice.Add(settings.DownloadBandwidthPrice.Mul64(responseSize)) - cost = cost.Mul64(2) // generous leeway + batchCost = settings.BaseRPCPrice.Add(settings.DownloadBandwidthPrice.Mul64(responseSize)) + batchCost = batchCost.Mul64(2) // generous leeway } // check funds - if rev.RenterFunds().Cmp(cost) < 0 { - return nil, ErrInsufficientFunds + if rev.RenterFunds().Cmp(batchCost) < 0 { + return nil, types.ZeroCurrency, ErrInsufficientFunds } // update the revision number if rev.Revision.RevisionNumber == math.MaxUint64 { - return nil, ErrContractFinalized + return nil, types.ZeroCurrency, ErrContractFinalized } rev.Revision.RevisionNumber++ // update the revision outputs - newValid, newMissed, err := updateRevisionOutputs(&rev.Revision, cost, types.ZeroCurrency) + newRevision, err := updatedRevision(rev.Revision, batchCost, types.ZeroCurrency) if err != nil { - return nil, err + return nil, types.ZeroCurrency, err } // build the sector roots request - revisionHash := hashRevision(rev.Revision) + revisionHash := hashRevision(newRevision) req := &rhpv2.RPCSectorRootsRequest{ RootOffset: uint64(offset), NumRoots: uint64(n), - RevisionNumber: rev.Revision.RevisionNumber, - ValidProofValues: newValid, - MissedProofValues: newMissed, - Signature: renterKey.SignHash(revisionHash), + RevisionNumber: rev.Revision.RevisionNumber, + ValidProofValues: []types.Currency{ + newRevision.MissedProofOutputs[0].Value, + newRevision.MissedProofOutputs[1].Value, + }, + MissedProofValues: []types.Currency{ + newRevision.MissedProofOutputs[0].Value, + newRevision.MissedProofOutputs[1].Value, + newRevision.MissedProofOutputs[2].Value, + }, + Signature: renterKey.SignHash(revisionHash), } // execute the sector roots RPC var rootsResp rhpv2.RPCSectorRootsResponse if err := t.WriteRequest(rhpv2.RPCSectorRootsID, req); err != nil { - return nil, err + return nil, types.ZeroCurrency, err } else if err := t.ReadResponse(&rootsResp, maxMerkleProofResponseSize); err != nil { - return nil, fmt.Errorf("couldn't read sector roots response: %w", err) + return nil, types.ZeroCurrency, fmt.Errorf("couldn't read sector roots response: %w", err) } // verify the host signature if !rev.HostKey().VerifyHash(revisionHash, rootsResp.Signature) { - return nil, errors.New("host's signature is invalid") + return nil, types.ZeroCurrency, errors.New("host's signature is invalid") } rev.Signatures[0].Signature = req.Signature[:] rev.Signatures[1].Signature = rootsResp.Signature[:] // verify the proof if uint64(len(rootsResp.SectorRoots)) != n { - return nil, fmt.Errorf("couldn't verify contract roots proof, host %v, version %v, err: number of roots does not match range %d != %d (num sectors: %d rev size: %d offset: %d)", rev.HostKey(), settings.Version, len(rootsResp.SectorRoots), n, numsectors, rev.Revision.Filesize, offset) + return nil, types.ZeroCurrency, fmt.Errorf("couldn't verify contract roots proof, host %v, version %v, err: number of roots does not match range %d != %d (num sectors: %d rev size: %d offset: %d)", rev.HostKey(), settings.Version, len(rootsResp.SectorRoots), n, numsectors, rev.Revision.Filesize, offset) } else if !rhpv2.VerifySectorRangeProof(rootsResp.MerkleProof, rootsResp.SectorRoots, offset, offset+n, numsectors, rev.Revision.FileMerkleRoot) { - return nil, fmt.Errorf("couldn't verify contract roots proof, host %v, version %v; %w", rev.HostKey(), settings.Version, ErrInvalidMerkleProof) + return nil, types.ZeroCurrency, fmt.Errorf("couldn't verify contract roots proof, host %v, version %v; %w", rev.HostKey(), settings.Version, ErrInvalidMerkleProof) } // append roots roots = append(roots, rootsResp.SectorRoots...) offset += n - // record spending - w.contractSpendingRecorder.Record(rev.Revision, api.ContractSpending{SectorRoots: cost}) + // update revision + rev.Revision = newRevision + cost = cost.Add(batchCost) } return } -func (w *Client) withTransportV2(ctx context.Context, hostKey types.PublicKey, hostIP string, fn func(*rhpv2.Transport) error) (err error) { - conn, err := dial(ctx, hostIP) - if err != nil { - return err - } - done := make(chan struct{}) - go func() { - select { - case <-done: - case <-ctx.Done(): - conn.Close() - } - }() - defer func() { - close(done) - if context.Cause(ctx) != nil { - err = context.Cause(ctx) - } - }() - t, err := rhpv2.NewRenterTransport(conn, hostKey) - if err != nil { - return err - } - defer t.Close() - - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("panic (withTransportV2): %v", r) - } - }() - return fn(t) -} - -func (w *Client) withRevisionV2(lockTimeout time.Duration, t *rhpv2.Transport, hk types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64, fn func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) error) error { - renterKey := w.deriveRenterKey(hk) - +func (w *Client) withRevisionV2(renterKey types.PrivateKey, gougingCheck GougingCheckFn, t *rhpv2.Transport, fcid types.FileContractID, lastKnownRevisionNumber uint64, fn func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) error) error { // execute lock RPC var lockResp rhpv2.RPCLockResponse err := t.Call(rhpv2.RPCLockID, &rhpv2.RPCLockRequest{ ContractID: fcid, Signature: t.SignChallenge(renterKey), - Timeout: uint64(lockTimeout.Milliseconds()), + Timeout: uint64(defaultLockTimeout.Milliseconds()), }, &lockResp) if err != nil { return err @@ -693,5 +570,78 @@ func (w *Client) withRevisionV2(lockTimeout time.Duration, t *rhpv2.Transport, h return fmt.Errorf("couldn't unmarshal json: %w", err) } + // perform gouging checks on settings + if breakdown := gougingCheck(settings); breakdown.Gouging() { + return fmt.Errorf("failed to prune contract: %v", breakdown) + } + return fn(t, rev, settings) } + +func (w *Client) withTransportV2(ctx context.Context, hostKey types.PublicKey, hostIP string, fn func(*rhpv2.Transport) error) (err error) { + conn, err := (&net.Dialer{}).DialContext(ctx, "tcp", hostIP) + if err != nil { + return err + } + done := make(chan struct{}) + go func() { + select { + case <-done: + case <-ctx.Done(): + conn.Close() + } + }() + defer func() { + close(done) + if context.Cause(ctx) != nil { + err = context.Cause(ctx) + } + }() + t, err := rhpv2.NewRenterTransport(conn, hostKey) + if err != nil { + return err + } + defer t.Close() + + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic (withTransportV2): %v", r) + } + }() + return fn(t) +} + +func hashRevision(rev types.FileContractRevision) types.Hash256 { + h := types.NewHasher() + rev.EncodeTo(h.E) + return h.Sum() +} + +func updatedRevision(rev types.FileContractRevision, cost, collateral types.Currency) (types.FileContractRevision, error) { + // allocate new slices; don't want to risk accidentally sharing memory + rev.ValidProofOutputs = append([]types.SiacoinOutput(nil), rev.ValidProofOutputs...) + rev.MissedProofOutputs = append([]types.SiacoinOutput(nil), rev.MissedProofOutputs...) + + // move valid payout from renter to host + var underflow, overflow bool + rev.ValidProofOutputs[0].Value, underflow = rev.ValidProofOutputs[0].Value.SubWithUnderflow(cost) + rev.ValidProofOutputs[1].Value, overflow = rev.ValidProofOutputs[1].Value.AddWithOverflow(cost) + if underflow || overflow { + return types.FileContractRevision{}, errors.New("insufficient funds to pay host") + } + + // move missed payout from renter to void + rev.MissedProofOutputs[0].Value, underflow = rev.MissedProofOutputs[0].Value.SubWithUnderflow(cost) + rev.MissedProofOutputs[2].Value, overflow = rev.MissedProofOutputs[2].Value.AddWithOverflow(cost) + if underflow || overflow { + return types.FileContractRevision{}, errors.New("insufficient funds to move missed payout to void") + } + + // move collateral from host to void + rev.MissedProofOutputs[1].Value, underflow = rev.MissedProofOutputs[1].Value.SubWithUnderflow(collateral) + rev.MissedProofOutputs[2].Value, overflow = rev.MissedProofOutputs[2].Value.AddWithOverflow(collateral) + if underflow || overflow { + return types.FileContractRevision{}, errors.New("insufficient collateral") + } + return rev, nil +} diff --git a/internal/rhp/v2/rpc.go b/internal/rhp/v2/rpc.go new file mode 100644 index 000000000..5bd458742 --- /dev/null +++ b/internal/rhp/v2/rpc.go @@ -0,0 +1,115 @@ +package rhp + +import ( + "context" + "encoding/json" + "fmt" + + rhpv2 "go.sia.tech/core/rhp/v2" + "go.sia.tech/core/types" + "go.sia.tech/renterd/internal/utils" +) + +// rpcFormContract forms a contract with a host. +func rpcFormContract(ctx context.Context, t *rhpv2.Transport, renterKey types.PrivateKey, txnSet []types.Transaction) (_ rhpv2.ContractRevision, _ []types.Transaction, err error) { + defer utils.WrapErr(ctx, "FormContract", &err) + + // strip our signatures before sending + parents, txn := txnSet[:len(txnSet)-1], txnSet[len(txnSet)-1] + renterContractSignatures := txn.Signatures + txnSet[len(txnSet)-1].Signatures = nil + + // create request + renterPubkey := renterKey.PublicKey() + req := &rhpv2.RPCFormContractRequest{ + Transactions: txnSet, + RenterKey: renterPubkey.UnlockKey(), + } + if err := t.WriteRequest(rhpv2.RPCFormContractID, req); err != nil { + return rhpv2.ContractRevision{}, nil, err + } + + // execute form contract RPC + var resp rhpv2.RPCFormContractAdditions + if err := t.ReadResponse(&resp, 65536); err != nil { + return rhpv2.ContractRevision{}, nil, err + } + + // merge host additions with txn + txn.SiacoinInputs = append(txn.SiacoinInputs, resp.Inputs...) + txn.SiacoinOutputs = append(txn.SiacoinOutputs, resp.Outputs...) + + // create initial (no-op) revision, transaction, and signature + fc := txn.FileContracts[0] + initRevision := types.FileContractRevision{ + ParentID: txn.FileContractID(0), + UnlockConditions: types.UnlockConditions{ + PublicKeys: []types.UnlockKey{ + renterPubkey.UnlockKey(), + t.HostKey().UnlockKey(), + }, + SignaturesRequired: 2, + }, + FileContract: types.FileContract{ + RevisionNumber: 1, + Filesize: fc.Filesize, + FileMerkleRoot: fc.FileMerkleRoot, + WindowStart: fc.WindowStart, + WindowEnd: fc.WindowEnd, + ValidProofOutputs: fc.ValidProofOutputs, + MissedProofOutputs: fc.MissedProofOutputs, + UnlockHash: fc.UnlockHash, + }, + } + revSig := renterKey.SignHash(hashRevision(initRevision)) + renterRevisionSig := types.TransactionSignature{ + ParentID: types.Hash256(initRevision.ParentID), + CoveredFields: types.CoveredFields{FileContractRevisions: []uint64{0}}, + PublicKeyIndex: 0, + Signature: revSig[:], + } + + // write our signatures + renterSigs := &rhpv2.RPCFormContractSignatures{ + ContractSignatures: renterContractSignatures, + RevisionSignature: renterRevisionSig, + } + if err := t.WriteResponse(renterSigs); err != nil { + return rhpv2.ContractRevision{}, nil, err + } + + // read the host's signatures and merge them with our own + var hostSigs rhpv2.RPCFormContractSignatures + if err := t.ReadResponse(&hostSigs, minMessageSize); err != nil { + return rhpv2.ContractRevision{}, nil, err + } + + txn.Signatures = make([]types.TransactionSignature, 0, len(renterContractSignatures)+len(hostSigs.ContractSignatures)) + txn.Signatures = append(txn.Signatures, renterContractSignatures...) + txn.Signatures = append(txn.Signatures, hostSigs.ContractSignatures...) + + signedTxnSet := make([]types.Transaction, 0, len(resp.Parents)+len(parents)+1) + signedTxnSet = append(signedTxnSet, resp.Parents...) + signedTxnSet = append(signedTxnSet, parents...) + signedTxnSet = append(signedTxnSet, txn) + return rhpv2.ContractRevision{ + Revision: initRevision, + Signatures: [2]types.TransactionSignature{ + renterRevisionSig, + hostSigs.RevisionSignature, + }, + }, signedTxnSet, nil +} + +// rpcSettings calls the Settings RPC, returning the host's reported settings. +func rpcSettings(ctx context.Context, t *rhpv2.Transport) (settings rhpv2.HostSettings, err error) { + defer utils.WrapErr(ctx, "Settings", &err) + + var resp rhpv2.RPCSettingsResponse + if err := t.Call(rhpv2.RPCSettingsID, nil, &resp); err != nil { + return rhpv2.HostSettings{}, err + } else if err := json.Unmarshal(resp.Settings, &settings); err != nil { + return rhpv2.HostSettings{}, fmt.Errorf("couldn't unmarshal json: %w", err) + } + return settings, nil +} diff --git a/internal/utils/errors.go b/internal/utils/errors.go index 67696b984..22ff0e660 100644 --- a/internal/utils/errors.go +++ b/internal/utils/errors.go @@ -1,7 +1,9 @@ package utils import ( + "context" "errors" + "fmt" "strings" ) @@ -28,3 +30,15 @@ func IsErr(err error, target error) bool { // renterd/hostd use the same error messages return strings.Contains(strings.ToLower(err.Error()), strings.ToLower(target.Error())) } + +// WrapErr can be used to defer wrapping an error which is then decorated with +// the provided function name. If the context contains a cause error, it will +// also be included in the wrapping. +func WrapErr(ctx context.Context, fnName string, err *error) { + if *err != nil { + *err = fmt.Errorf("%s: %w", fnName, *err) + if cause := context.Cause(ctx); cause != nil && !IsErr(*err, cause) { + *err = fmt.Errorf("%w; %w", cause, *err) + } + } +} diff --git a/worker/gouging.go b/worker/gouging.go index 5d4a96a4e..753582664 100644 --- a/worker/gouging.go +++ b/worker/gouging.go @@ -39,6 +39,7 @@ var ( type ( GougingChecker interface { Check(_ *rhpv2.HostSettings, _ *rhpv3.HostPriceTable) api.HostGougingBreakdown + CheckSettings(rhpv2.HostSettings) api.HostGougingBreakdown CheckUnusedDefaults(rhpv3.HostPriceTable) error BlocksUntilBlockHeightGouging(hostHeight uint64) int64 } @@ -65,36 +66,40 @@ func GougingCheckerFromContext(ctx context.Context, criticalMigration bool) (Gou return gc(criticalMigration) } -func WithGougingChecker(ctx context.Context, cs ConsensusState, gp api.GougingParams) context.Context { - return context.WithValue(ctx, keyGougingChecker, func(criticalMigration bool) (GougingChecker, error) { - consensusState, err := cs.ConsensusState(ctx) - if err != nil { - return gougingChecker{}, fmt.Errorf("failed to get consensus state: %w", err) - } +func newGougingChecker(ctx context.Context, cs ConsensusState, gp api.GougingParams, criticalMigration bool) (GougingChecker, error) { + consensusState, err := cs.ConsensusState(ctx) + if err != nil { + return gougingChecker{}, fmt.Errorf("failed to get consensus state: %w", err) + } - // adjust the max download price if we are dealing with a critical - // migration that might be failing due to gouging checks - settings := gp.GougingSettings - if criticalMigration && gp.GougingSettings.MigrationSurchargeMultiplier > 0 { - if adjustedMaxDownloadPrice, overflow := gp.GougingSettings.MaxDownloadPrice.Mul64WithOverflow(gp.GougingSettings.MigrationSurchargeMultiplier); !overflow { - settings.MaxDownloadPrice = adjustedMaxDownloadPrice - } + // adjust the max download price if we are dealing with a critical + // migration that might be failing due to gouging checks + settings := gp.GougingSettings + if criticalMigration && gp.GougingSettings.MigrationSurchargeMultiplier > 0 { + if adjustedMaxDownloadPrice, overflow := gp.GougingSettings.MaxDownloadPrice.Mul64WithOverflow(gp.GougingSettings.MigrationSurchargeMultiplier); !overflow { + settings.MaxDownloadPrice = adjustedMaxDownloadPrice } + } - return gougingChecker{ - consensusState: consensusState, - settings: settings, - txFee: gp.TransactionFee, - - // NOTE: - // - // period and renew window are nil here and that's fine, gouging - // checkers in the workers don't have easy access to these settings and - // thus ignore them when perform gouging checks, the autopilot however - // does have those and will pass them when performing gouging checks - period: nil, - renewWindow: nil, - }, nil + return gougingChecker{ + consensusState: consensusState, + settings: settings, + txFee: gp.TransactionFee, + + // NOTE: + // + // period and renew window are nil here and that's fine, gouging + // checkers in the workers don't have easy access to these settings and + // thus ignore them when perform gouging checks, the autopilot however + // does have those and will pass them when performing gouging checks + period: nil, + renewWindow: nil, + }, nil +} + +func WithGougingChecker(ctx context.Context, cs ConsensusState, gp api.GougingParams) context.Context { + return context.WithValue(ctx, keyGougingChecker, func(criticalMigration bool) (GougingChecker, error) { + return newGougingChecker(ctx, cs, gp, criticalMigration) }) } @@ -139,6 +144,10 @@ func (gc gougingChecker) Check(hs *rhpv2.HostSettings, pt *rhpv3.HostPriceTable) } } +func (gc gougingChecker) CheckSettings(hs rhpv2.HostSettings) api.HostGougingBreakdown { + return gc.Check(&hs, nil) +} + func (gc gougingChecker) CheckUnusedDefaults(pt rhpv3.HostPriceTable) error { return checkUnusedDefaults(pt) } diff --git a/worker/rhpv3.go b/worker/rhpv3.go index f95f596d3..ab3ae0b36 100644 --- a/worker/rhpv3.go +++ b/worker/rhpv3.go @@ -62,6 +62,10 @@ var ( // balance over the maximum allowed ephemeral account balance. errBalanceMaxExceeded = errors.New("ephemeral account maximum balance exceeded") + // errInsufficientFunds is returned by various RPCs when the renter is + // unable to provide sufficient payment to the host. + errInsufficientFunds = errors.New("insufficient funds") + // errMaxRevisionReached occurs when trying to revise a contract that has // already reached the highest possible revision number. Usually happens // when trying to use a renewed contract. @@ -100,7 +104,7 @@ func isBalanceMaxExceeded(err error) bool { return utils.IsErr(err, errBalanceM func isClosedStream(err error) bool { return utils.IsErr(err, mux.ErrClosedStream) || utils.IsErr(err, net.ErrClosed) } -func isInsufficientFunds(err error) bool { return utils.IsErr(err, ErrInsufficientFunds) } +func isInsufficientFunds(err error) bool { return utils.IsErr(err, errInsufficientFunds) } func isPriceTableExpired(err error) bool { return utils.IsErr(err, errPriceTableExpired) } func isPriceTableGouging(err error) bool { return utils.IsErr(err, errPriceTableGouging) } func isPriceTableNotFound(err error) bool { return utils.IsErr(err, errPriceTableNotFound) } @@ -632,7 +636,7 @@ type PriceTablePaymentFunc func(pt rhpv3.HostPriceTable) (rhpv3.PaymentMethod, e // RPCPriceTable calls the UpdatePriceTable RPC. func RPCPriceTable(ctx context.Context, t *transportV3, paymentFunc PriceTablePaymentFunc) (_ api.HostPriceTable, err error) { - defer wrapErr(ctx, "PriceTable", &err) + defer utils.WrapErr(ctx, "PriceTable", &err) s, err := t.DialStream(ctx) if err != nil { @@ -669,7 +673,7 @@ func RPCPriceTable(ctx context.Context, t *transportV3, paymentFunc PriceTablePa // RPCAccountBalance calls the AccountBalance RPC. func RPCAccountBalance(ctx context.Context, t *transportV3, payment rhpv3.PaymentMethod, account rhpv3.Account, settingsID rhpv3.SettingsID) (bal types.Currency, err error) { - defer wrapErr(ctx, "AccountBalance", &err) + defer utils.WrapErr(ctx, "AccountBalance", &err) s, err := t.DialStream(ctx) if err != nil { return types.ZeroCurrency, err @@ -694,7 +698,7 @@ func RPCAccountBalance(ctx context.Context, t *transportV3, payment rhpv3.Paymen // RPCFundAccount calls the FundAccount RPC. func RPCFundAccount(ctx context.Context, t *transportV3, payment rhpv3.PaymentMethod, account rhpv3.Account, settingsID rhpv3.SettingsID) (err error) { - defer wrapErr(ctx, "FundAccount", &err) + defer utils.WrapErr(ctx, "FundAccount", &err) s, err := t.DialStream(ctx) if err != nil { return err @@ -721,7 +725,7 @@ func RPCFundAccount(ctx context.Context, t *transportV3, payment rhpv3.PaymentMe // fetching a pricetable using the fetched revision to pay for it. If // paymentFunc returns 'nil' as payment, the host is not paid. func RPCLatestRevision(ctx context.Context, t *transportV3, contractID types.FileContractID, paymentFunc func(rev *types.FileContractRevision) (rhpv3.HostPriceTable, rhpv3.PaymentMethod, error)) (_ types.FileContractRevision, err error) { - defer wrapErr(ctx, "LatestRevision", &err) + defer utils.WrapErr(ctx, "LatestRevision", &err) s, err := t.DialStream(ctx) if err != nil { return types.FileContractRevision{}, err @@ -747,7 +751,7 @@ func RPCLatestRevision(ctx context.Context, t *transportV3, contractID types.Fil // RPCReadSector calls the ExecuteProgram RPC with a ReadSector instruction. func RPCReadSector(ctx context.Context, t *transportV3, w io.Writer, pt rhpv3.HostPriceTable, payment rhpv3.PaymentMethod, offset, length uint32, merkleRoot types.Hash256) (cost, refund types.Currency, err error) { - defer wrapErr(ctx, "ReadSector", &err) + defer utils.WrapErr(ctx, "ReadSector", &err) s, err := t.DialStream(ctx) if err != nil { return types.ZeroCurrency, types.ZeroCurrency, err @@ -811,7 +815,7 @@ func RPCReadSector(ctx context.Context, t *transportV3, w io.Writer, pt rhpv3.Ho } func RPCAppendSector(ctx context.Context, t *transportV3, renterKey types.PrivateKey, pt rhpv3.HostPriceTable, rev *types.FileContractRevision, payment rhpv3.PaymentMethod, sectorRoot types.Hash256, sector *[rhpv2.SectorSize]byte) (cost types.Currency, err error) { - defer wrapErr(ctx, "AppendSector", &err) + defer utils.WrapErr(ctx, "AppendSector", &err) // sanity check revision first if rev.RevisionNumber == math.MaxUint64 { @@ -949,7 +953,7 @@ func RPCAppendSector(ctx context.Context, t *transportV3, renterKey types.Privat } func RPCRenew(ctx context.Context, rrr api.RHPRenewRequest, bus Bus, t *transportV3, pt *rhpv3.HostPriceTable, rev types.FileContractRevision, renterKey types.PrivateKey, l *zap.SugaredLogger) (_ rhpv2.ContractRevision, _ []types.Transaction, _, _ types.Currency, err error) { - defer wrapErr(ctx, "RPCRenew", &err) + defer utils.WrapErr(ctx, "RPCRenew", &err) s, err := t.DialStream(ctx) if err != nil { @@ -1097,6 +1101,12 @@ func RPCRenew(ctx context.Context, rrr api.RHPRenewRequest, bus Bus, t *transpor }, txnSet, pt.ContractPrice, wprr.FundAmount, nil } +func hashRevision(rev types.FileContractRevision) types.Hash256 { + h := types.NewHasher() + rev.EncodeTo(h.E) + return h.Sum() +} + // initialRevision returns the first revision of a file contract formation // transaction. func initialRevision(formationTxn types.Transaction, hostPubKey, renterPubKey types.UnlockKey) types.FileContractRevision { @@ -1126,7 +1136,41 @@ func payByContract(rev *types.FileContractRevision, amount types.Currency, refun } payment, ok := rhpv3.PayByContract(rev, amount, refundAcct, sk) if !ok { - return rhpv3.PayByContractRequest{}, ErrInsufficientFunds + return rhpv3.PayByContractRequest{}, errInsufficientFunds } return payment, nil } + +func updateRevisionOutputs(rev *types.FileContractRevision, cost, collateral types.Currency) (valid, missed []types.Currency, err error) { + // allocate new slices; don't want to risk accidentally sharing memory + rev.ValidProofOutputs = append([]types.SiacoinOutput(nil), rev.ValidProofOutputs...) + rev.MissedProofOutputs = append([]types.SiacoinOutput(nil), rev.MissedProofOutputs...) + + // move valid payout from renter to host + var underflow, overflow bool + rev.ValidProofOutputs[0].Value, underflow = rev.ValidProofOutputs[0].Value.SubWithUnderflow(cost) + rev.ValidProofOutputs[1].Value, overflow = rev.ValidProofOutputs[1].Value.AddWithOverflow(cost) + if underflow || overflow { + err = errors.New("insufficient funds to pay host") + return + } + + // move missed payout from renter to void + rev.MissedProofOutputs[0].Value, underflow = rev.MissedProofOutputs[0].Value.SubWithUnderflow(cost) + rev.MissedProofOutputs[2].Value, overflow = rev.MissedProofOutputs[2].Value.AddWithOverflow(cost) + if underflow || overflow { + err = errors.New("insufficient funds to move missed payout to void") + return + } + + // move collateral from host to void + rev.MissedProofOutputs[1].Value, underflow = rev.MissedProofOutputs[1].Value.SubWithUnderflow(collateral) + rev.MissedProofOutputs[2].Value, overflow = rev.MissedProofOutputs[2].Value.AddWithOverflow(collateral) + if underflow || overflow { + err = errors.New("insufficient collateral") + return + } + + return []types.Currency{rev.ValidProofOutputs[0].Value, rev.ValidProofOutputs[1].Value}, + []types.Currency{rev.MissedProofOutputs[0].Value, rev.MissedProofOutputs[1].Value, rev.MissedProofOutputs[2].Value}, nil +} diff --git a/worker/worker.go b/worker/worker.go index e0c81dded..f05f96b58 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -24,6 +24,7 @@ import ( "go.sia.tech/renterd/alerts" "go.sia.tech/renterd/api" "go.sia.tech/renterd/build" + clientV2 "go.sia.tech/renterd/internal/rhp/v2" "go.sia.tech/renterd/internal/utils" iworker "go.sia.tech/renterd/internal/worker" "go.sia.tech/renterd/object" @@ -34,10 +35,6 @@ import ( ) const ( - batchSizeDeleteSectors = uint64(1000) // 4GiB of contract data - batchSizeFetchSectors = uint64(25600) // 100GiB of contract data - - defaultLockTimeout = time.Minute defaultRevisionFetchTimeout = 30 * time.Second lockingPriorityActiveContractRevision = 100 @@ -203,6 +200,8 @@ func (w *worker) deriveRenterKey(hostKey types.PublicKey) types.PrivateKey { type worker struct { alerts alerts.Alerter + rhp2Client *clientV2.Client + allowPrivateIPs bool id string bus Bus @@ -387,10 +386,6 @@ func (w *worker) rhpPriceTableHandler(jc jape.Context) { jc.Encode(hpt) } -func (w *worker) discardTxnOnErr(txn types.Transaction, errContext string, err *error) { - discardTxnOnErr(w.shutdownCtx, w.bus, w.logger, txn, errContext, err) -} - func (w *worker) rhpFormHandler(jc jape.Context) { ctx := jc.Request.Context() @@ -414,42 +409,23 @@ func (w *worker) rhpFormHandler(jc jape.Context) { if jc.Check("could not get gouging parameters", err) != nil { return } + gc, err := newGougingChecker(ctx, w.bus, gp, false) + if jc.Check("could not create gouging checker", err) != nil { + return + } hostIP, hostKey, renterFunds := rfr.HostIP, rfr.HostKey, rfr.RenterFunds renterAddress, endHeight, hostCollateral := rfr.RenterAddress, rfr.EndHeight, rfr.HostCollateral renterKey := w.deriveRenterKey(hostKey) - var contract rhpv2.ContractRevision - var txnSet []types.Transaction - ctx = WithGougingChecker(ctx, w.bus, gp) - err = w.withTransportV2(ctx, rfr.HostKey, hostIP, func(t *rhpv2.Transport) (err error) { - hostSettings, err := RPCSettings(ctx, t) + contract, txnSet, err := w.rhp2Client.FormContract(ctx, renterAddress, renterKey, hostKey, hostIP, renterFunds, hostCollateral, endHeight, gc.CheckSettings, 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 err + return nil, nil, err } - // NOTE: we overwrite the NetAddress with the host address here since we - // just used it to dial the host we know it's valid - hostSettings.NetAddress = hostIP - - gc, err := GougingCheckerFromContext(ctx, false) - if err != nil { - return err - } - if breakdown := gc.Check(&hostSettings, nil); breakdown.Gouging() { - return fmt.Errorf("failed to form contract, gouging check failed: %v", breakdown) - } - - renterTxnSet, err := w.bus.WalletPrepareForm(ctx, renterAddress, renterKey.PublicKey(), renterFunds, hostCollateral, hostKey, hostSettings, endHeight) - if err != nil { - return err - } - defer w.discardTxnOnErr(renterTxnSet[len(renterTxnSet)-1], "rhpFormHandler", &err) - - contract, txnSet, err = RPCFormContract(ctx, t, renterKey, renterTxnSet) - if err != nil { - return err - } - return + return txns, func(txn types.Transaction) { + _ = w.bus.WalletDiscard(ctx, txn) + }, nil }) if jc.Check("couldn't form contract", err) != nil { return @@ -491,7 +467,7 @@ func (w *worker) rhpBroadcastHandler(jc jape.Context) { } rk := w.deriveRenterKey(c.HostKey) - rev, err := w.FetchSignedRevision(ctx, c.HostIP, c.HostKey, rk, fcid, time.Minute) + rev, err := w.rhp2Client.FetchSignedRevision(ctx, c.HostIP, c.HostKey, rk, fcid, time.Minute) if jc.Check("could not fetch revision", err) != nil { return } @@ -575,8 +551,17 @@ func (w *worker) rhpPruneContractHandlerPOST(jc jape.Context) { ctx = WithGougingChecker(ctx, w.bus, gp) // prune the contract - pruned, remaining, err := w.PruneContract(ctx, contract.HostIP, contract.HostKey, fcid, contract.RevisionNumber) - if err != nil && !errors.Is(err, ErrNoSectorsToPrune) && pruned == 0 { + var pruned, remaining uint64 + var rev *types.FileContractRevision + var cost types.Currency + err = w.withContractLock(ctx, contract.ID, lockingPriorityPruning, func() error { + rev, pruned, remaining, cost, err = w.rhp2Client.PruneContract(ctx, w.deriveRenterKey(contract.HostKey), nil, contract.HostIP, contract.HostKey, fcid, contract.RevisionNumber, nil) + return err + }) + if rev != nil { + w.contractSpendingRecorder.Record(*rev, api.ContractSpending{Deletions: cost}) + } + if err != nil && !errors.Is(err, clientV2.ErrNoSectorsToPrune) && pruned == 0 { err = fmt.Errorf("failed to prune contract %v; %w", fcid, err) jc.Error(err, http.StatusInternalServerError) return @@ -620,10 +605,13 @@ func (w *worker) rhpContractRootsHandlerGET(jc jape.Context) { ctx = WithGougingChecker(ctx, w.bus, gp) // fetch the roots from the host - roots, err := w.FetchContractRoots(ctx, c.HostIP, c.HostKey, id, c.RevisionNumber) - if jc.Check("couldn't fetch contract roots from host", err) == nil { - jc.Encode(roots) + roots, rev, cost, err := w.rhp2Client.FetchContractRoots(ctx, w.deriveRenterKey(c.HostKey), nil, c.HostIP, c.HostKey, id, c.RevisionNumber) + if jc.Check("couldn't fetch contract roots from host", err) != nil { + return + } else if rev != nil { + w.contractSpendingRecorder.Record(*rev, api.ContractSpending{SectorRoots: cost}) } + jc.Encode(roots) } func (w *worker) rhpRenewHandler(jc jape.Context) { @@ -1310,6 +1298,7 @@ func New(masterKey [32]byte, id string, b Bus, contractLockingDuration, busFlush bus: b, masterKey: masterKey, logger: l.Sugar(), + rhp2Client: clientV2.New(l), startTime: time.Now(), uploadingPackedSlabs: make(map[string]struct{}), shutdownCtx: shutdownCtx, @@ -1407,23 +1396,11 @@ func (w *worker) scanHost(ctx context.Context, timeout time.Duration, hostKey ty scan := func() (rhpv2.HostSettings, rhpv3.HostPriceTable, time.Duration, error) { // fetch the host settings start := time.Now() - var settings rhpv2.HostSettings - { - scanCtx, cancel := timeoutCtx() - defer cancel() - err := w.withTransportV2(scanCtx, hostKey, hostIP, func(t *rhpv2.Transport) error { - var err error - if settings, err = RPCSettings(scanCtx, t); err != nil { - return fmt.Errorf("failed to fetch host settings: %w", err) - } - // NOTE: we overwrite the NetAddress with the host address here - // since we just used it to dial the host we know it's valid - settings.NetAddress = hostIP - return nil - }) - if err != nil { - return settings, rhpv3.HostPriceTable{}, time.Since(start), err - } + scanCtx, cancel := timeoutCtx() + settings, err := w.rhp2Client.FetchSettings(scanCtx, hostKey, hostIP) + cancel() + if err != nil { + return settings, rhpv3.HostPriceTable{}, time.Since(start), err } // fetch the host pricetable From 3fb394df81a283507aa6d50c704729f00c4abbed Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Thu, 15 Aug 2024 14:49:07 +0200 Subject: [PATCH 3/7] rhp: remove contract sector fn --- internal/rhp/v2/rhp.go | 13 +++---------- worker/worker.go | 11 ++++++++++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/internal/rhp/v2/rhp.go b/internal/rhp/v2/rhp.go index d17dfa5cf..8e4be0108 100644 --- a/internal/rhp/v2/rhp.go +++ b/internal/rhp/v2/rhp.go @@ -68,8 +68,6 @@ var ( ) type ( - ContractRootsFn func(ctx context.Context, id types.FileContractID) ([]types.Hash256, []types.Hash256, error) - GougingCheckFn func(settings rhpv2.HostSettings) api.HostGougingBreakdown 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) @@ -181,7 +179,7 @@ func (c *Client) FormContract(ctx context.Context, renterAddress types.Address, return } -func (c *Client) PruneContract(ctx context.Context, renterKey types.PrivateKey, gougingCheck GougingCheckFn, hostIP string, hostKey types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64, wantedRoots ContractRootsFn) (revision *types.FileContractRevision, deleted, remaining uint64, cost types.Currency, err error) { +func (c *Client) PruneContract(ctx context.Context, renterKey types.PrivateKey, gougingCheck GougingCheckFn, 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.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { return c.withRevisionV2(renterKey, gougingCheck, t, fcid, lastKnownRevisionNumber, func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) (err error) { // fetch roots @@ -194,13 +192,8 @@ func (c *Client) PruneContract(ctx context.Context, renterKey types.PrivateKey, cost = cost.Add(fetchCost) revision = &rev.Revision - // fetch the roots from the bus - want, pending, err := wantedRoots(ctx, fcid) - if err != nil { - return err - } keep := make(map[types.Hash256]struct{}) - for _, root := range append(want, pending...) { + for _, root := range toKeep { keep[root] = struct{}{} } @@ -214,7 +207,7 @@ func (c *Client) PruneContract(ctx context.Context, renterKey types.PrivateKey, indices = append(indices, uint64(i)) } if len(indices) == 0 { - return fmt.Errorf("%w: database holds %d (%d pending), contract contains %d", ErrNoSectorsToPrune, len(want)+len(pending), len(pending), len(got)) + return fmt.Errorf("%w: database holds %d, contract contains %d", ErrNoSectorsToPrune, len(toKeep), len(got)) } // delete the roots from the contract diff --git a/worker/worker.go b/worker/worker.go index f05f96b58..dcdc8ae12 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -546,6 +546,11 @@ func (w *worker) rhpPruneContractHandlerPOST(jc jape.Context) { if jc.Check("could not fetch gouging parameters", err) != nil { return } + gc, err := newGougingChecker(ctx, w.bus, gp, false) + if err != nil { + jc.Error(err, http.StatusInternalServerError) + return + } // attach gouging checker ctx = WithGougingChecker(ctx, w.bus, gp) @@ -555,7 +560,11 @@ func (w *worker) rhpPruneContractHandlerPOST(jc jape.Context) { var rev *types.FileContractRevision var cost types.Currency err = w.withContractLock(ctx, contract.ID, lockingPriorityPruning, func() error { - rev, pruned, remaining, cost, err = w.rhp2Client.PruneContract(ctx, w.deriveRenterKey(contract.HostKey), nil, contract.HostIP, contract.HostKey, fcid, contract.RevisionNumber, nil) + stored, pending, err := w.bus.ContractRoots(ctx, contract.ID) + if err != nil { + return fmt.Errorf("failed to fetch contract roots; %w", err) + } + rev, pruned, remaining, cost, err = w.rhp2Client.PruneContract(ctx, w.deriveRenterKey(contract.HostKey), gc.CheckSettings, contract.HostIP, contract.HostKey, fcid, contract.RevisionNumber, append(stored, pending...)) return err }) if rev != nil { From c5f740dd4198df993299aaf1812976414cecba76 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Thu, 15 Aug 2024 15:11:21 +0200 Subject: [PATCH 4/7] e2e: fix TestMetrics --- internal/rhp/v2/rhp.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/rhp/v2/rhp.go b/internal/rhp/v2/rhp.go index 8e4be0108..6d86d6387 100644 --- a/internal/rhp/v2/rhp.go +++ b/internal/rhp/v2/rhp.go @@ -351,7 +351,7 @@ func (c *Client) deleteContractRoots(t *rhpv2.Transport, renterKey types.Private RevisionNumber: rev.Revision.RevisionNumber, ValidProofValues: []types.Currency{ newRevision.ValidProofOutputs[0].Value, - newRevision.ValidProofOutputs[0].Value, + newRevision.ValidProofOutputs[1].Value, }, MissedProofValues: []types.Currency{ newRevision.MissedProofOutputs[0].Value, @@ -468,8 +468,8 @@ func (c *Client) fetchContractRoots(t *rhpv2.Transport, renterKey types.PrivateK RevisionNumber: rev.Revision.RevisionNumber, ValidProofValues: []types.Currency{ - newRevision.MissedProofOutputs[0].Value, - newRevision.MissedProofOutputs[1].Value, + newRevision.ValidProofOutputs[0].Value, + newRevision.ValidProofOutputs[1].Value, }, MissedProofValues: []types.Currency{ newRevision.MissedProofOutputs[0].Value, From b3729772f0d2f56a71407513eb107cf8e78b1e8a Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Thu, 15 Aug 2024 15:49:30 +0200 Subject: [PATCH 5/7] worker: move gouging to internal package --- autopilot/contractor/evaluate.go | 6 +- autopilot/contractor/hostfilter.go | 4 +- autopilot/contractor/state.go | 6 +- internal/gouging/gouging.go | 521 +++++++++++++++++++++++++++++ worker/gouging.go | 521 +---------------------------- worker/host.go | 7 +- worker/mocks_test.go | 3 +- worker/rhpv3.go | 5 +- worker/worker.go | 25 +- 9 files changed, 555 insertions(+), 543 deletions(-) create mode 100644 internal/gouging/gouging.go diff --git a/autopilot/contractor/evaluate.go b/autopilot/contractor/evaluate.go index 14fdb3c1d..b617b307b 100644 --- a/autopilot/contractor/evaluate.go +++ b/autopilot/contractor/evaluate.go @@ -5,13 +5,13 @@ import ( "go.sia.tech/core/types" "go.sia.tech/renterd/api" - "go.sia.tech/renterd/worker" + "go.sia.tech/renterd/internal/gouging" ) var ErrMissingRequiredFields = errors.New("missing required fields in configuration, both allowance and amount must be set") func countUsableHosts(cfg api.AutopilotConfig, cs api.ConsensusState, fee types.Currency, period uint64, rs api.RedundancySettings, gs api.GougingSettings, hosts []api.Host) (usables uint64) { - gc := worker.NewGougingChecker(gs, cs, fee, period, cfg.Contracts.RenewWindow) + gc := gouging.NewChecker(gs, cs, fee, period, cfg.Contracts.RenewWindow) for _, host := range hosts { hc := checkHost(gc, scoreHost(host, cfg, rs.Redundancy()), minValidScore) if hc.Usability.IsUsable() { @@ -31,7 +31,7 @@ func EvaluateConfig(cfg api.AutopilotConfig, cs api.ConsensusState, fee types.Cu } period := cfg.Contracts.Period - gc := worker.NewGougingChecker(gs, cs, fee, period, cfg.Contracts.RenewWindow) + gc := gouging.NewChecker(gs, cs, fee, period, cfg.Contracts.RenewWindow) resp.Hosts = uint64(len(hosts)) for i, host := range hosts { diff --git a/autopilot/contractor/hostfilter.go b/autopilot/contractor/hostfilter.go index 0b358d60f..3083976ed 100644 --- a/autopilot/contractor/hostfilter.go +++ b/autopilot/contractor/hostfilter.go @@ -10,7 +10,7 @@ import ( rhpv3 "go.sia.tech/core/rhp/v3" "go.sia.tech/core/types" "go.sia.tech/renterd/api" - "go.sia.tech/renterd/worker" + "go.sia.tech/renterd/internal/gouging" ) const ( @@ -220,7 +220,7 @@ func isUpForRenewal(cfg api.AutopilotConfig, r types.FileContractRevision, block } // checkHost performs a series of checks on the host. -func checkHost(gc worker.GougingChecker, sh scoredHost, minScore float64) *api.HostCheck { +func checkHost(gc gouging.Checker, sh scoredHost, minScore float64) *api.HostCheck { h := sh.host // prepare host breakdown fields diff --git a/autopilot/contractor/state.go b/autopilot/contractor/state.go index 1a03697e7..dcbb910e8 100644 --- a/autopilot/contractor/state.go +++ b/autopilot/contractor/state.go @@ -7,7 +7,7 @@ import ( "go.sia.tech/core/types" "go.sia.tech/renterd/api" - "go.sia.tech/renterd/worker" + "go.sia.tech/renterd/internal/gouging" ) type ( @@ -73,8 +73,8 @@ func (ctx *mCtx) Err() error { return ctx.ctx.Err() } -func (ctx *mCtx) GougingChecker(cs api.ConsensusState) worker.GougingChecker { - return worker.NewGougingChecker(ctx.state.GS, cs, ctx.state.Fee, ctx.Period(), ctx.RenewWindow()) +func (ctx *mCtx) GougingChecker(cs api.ConsensusState) gouging.Checker { + return gouging.NewChecker(ctx.state.GS, cs, ctx.state.Fee, ctx.Period(), ctx.RenewWindow()) } func (ctx *mCtx) HostScore(h api.Host) (sb api.HostScoreBreakdown, err error) { diff --git a/internal/gouging/gouging.go b/internal/gouging/gouging.go new file mode 100644 index 000000000..926386dae --- /dev/null +++ b/internal/gouging/gouging.go @@ -0,0 +1,521 @@ +package gouging + +import ( + "context" + "errors" + "fmt" + "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" +) + +const ( + // maxBaseRPCPriceVsBandwidth is the max ratio for sane pricing between the + // MinBaseRPCPrice and the MinDownloadBandwidthPrice. This ensures that 1 + // million base RPC charges are at most 1% of the cost to download 4TB. This + // ratio should be used by checking that the MinBaseRPCPrice is less than or + // equal to the MinDownloadBandwidthPrice multiplied by this constant + maxBaseRPCPriceVsBandwidth = uint64(40e3) + + // maxSectorAccessPriceVsBandwidth is the max ratio for sane pricing between + // the MinSectorAccessPrice and the MinDownloadBandwidthPrice. This ensures + // that 1 million base accesses are at most 10% of the cost to download 4TB. + // This ratio should be used by checking that the MinSectorAccessPrice is + // less than or equal to the MinDownloadBandwidthPrice multiplied by this + // constant + maxSectorAccessPriceVsBandwidth = uint64(400e3) +) + +var ( + errHostSettingsGouging = errors.New("host settings gouging detected") + ErrPriceTableGouging = errors.New("price table gouging detected") +) + +type ( + ConsensusState interface { + ConsensusState(ctx context.Context) (api.ConsensusState, error) + } + + Checker interface { + Check(_ *rhpv2.HostSettings, _ *rhpv3.HostPriceTable) api.HostGougingBreakdown + CheckSettings(rhpv2.HostSettings) api.HostGougingBreakdown + CheckUnusedDefaults(rhpv3.HostPriceTable) error + BlocksUntilBlockHeightGouging(hostHeight uint64) int64 + } + + checker struct { + consensusState api.ConsensusState + settings api.GougingSettings + txFee types.Currency + + period *uint64 + renewWindow *uint64 + } +) + +var _ Checker = checker{} + +func NewWorkerGougingChecker(ctx context.Context, cs ConsensusState, gp api.GougingParams, criticalMigration bool) (Checker, error) { + consensusState, err := cs.ConsensusState(ctx) + if err != nil { + return checker{}, fmt.Errorf("failed to get consensus state: %w", err) + } + + // adjust the max download price if we are dealing with a critical + // migration that might be failing due to gouging checks + settings := gp.GougingSettings + if criticalMigration && gp.GougingSettings.MigrationSurchargeMultiplier > 0 { + if adjustedMaxDownloadPrice, overflow := gp.GougingSettings.MaxDownloadPrice.Mul64WithOverflow(gp.GougingSettings.MigrationSurchargeMultiplier); !overflow { + settings.MaxDownloadPrice = adjustedMaxDownloadPrice + } + } + + return checker{ + consensusState: consensusState, + settings: settings, + txFee: gp.TransactionFee, + + // NOTE: + // + // period and renew window are nil here and that's fine, gouging + // checkers in the workers don't have easy access to these settings and + // thus ignore them when perform gouging checks, the autopilot however + // does have those and will pass them when performing gouging checks + period: nil, + renewWindow: nil, + }, nil +} + +func NewChecker(gs api.GougingSettings, cs api.ConsensusState, txnFee types.Currency, period, renewWindow uint64) Checker { + return checker{ + consensusState: cs, + settings: gs, + txFee: txnFee, + + period: &period, + renewWindow: &renewWindow, + } +} + +func (gc checker) BlocksUntilBlockHeightGouging(hostHeight uint64) int64 { + blockHeight := gc.consensusState.BlockHeight + leeway := gc.settings.HostBlockHeightLeeway + var minHeight uint64 + if blockHeight >= uint64(leeway) { + minHeight = blockHeight - uint64(leeway) + } + return int64(hostHeight) - int64(minHeight) +} + +func (gc checker) Check(hs *rhpv2.HostSettings, pt *rhpv3.HostPriceTable) api.HostGougingBreakdown { + if hs == nil && pt == nil { + panic("gouging checker needs to be provided with at least host settings or a price table") // developer error + } + + return api.HostGougingBreakdown{ + ContractErr: errsToStr( + checkContractGougingRHPv2(gc.period, gc.renewWindow, hs), + checkContractGougingRHPv3(gc.period, gc.renewWindow, pt), + ), + DownloadErr: errsToStr(checkDownloadGougingRHPv3(gc.settings, pt)), + GougingErr: errsToStr( + checkPriceGougingPT(gc.settings, gc.consensusState, gc.txFee, pt), + checkPriceGougingHS(gc.settings, hs), + ), + PruneErr: errsToStr(checkPruneGougingRHPv2(gc.settings, hs)), + UploadErr: errsToStr(checkUploadGougingRHPv3(gc.settings, pt)), + } +} + +func (gc checker) CheckSettings(hs rhpv2.HostSettings) api.HostGougingBreakdown { + return gc.Check(&hs, nil) +} + +func (gc checker) CheckUnusedDefaults(pt rhpv3.HostPriceTable) error { + return checkUnusedDefaults(pt) +} + +func checkPriceGougingHS(gs api.GougingSettings, hs *rhpv2.HostSettings) error { + // check if we have settings + if hs == nil { + return nil + } + // check base rpc price + if !gs.MaxRPCPrice.IsZero() && hs.BaseRPCPrice.Cmp(gs.MaxRPCPrice) > 0 { + return fmt.Errorf("rpc price exceeds max: %v > %v", hs.BaseRPCPrice, gs.MaxRPCPrice) + } + maxBaseRPCPrice := hs.DownloadBandwidthPrice.Mul64(maxBaseRPCPriceVsBandwidth) + if hs.BaseRPCPrice.Cmp(maxBaseRPCPrice) > 0 { + return fmt.Errorf("rpc price too high, %v > %v", hs.BaseRPCPrice, maxBaseRPCPrice) + } + + // check sector access price + if hs.DownloadBandwidthPrice.IsZero() { + hs.DownloadBandwidthPrice = types.NewCurrency64(1) + } + maxSectorAccessPrice := hs.DownloadBandwidthPrice.Mul64(maxSectorAccessPriceVsBandwidth) + if hs.SectorAccessPrice.Cmp(maxSectorAccessPrice) > 0 { + return fmt.Errorf("sector access price too high, %v > %v", hs.SectorAccessPrice, maxSectorAccessPrice) + } + + // check max storage price + if !gs.MaxStoragePrice.IsZero() && hs.StoragePrice.Cmp(gs.MaxStoragePrice) > 0 { + return fmt.Errorf("storage price exceeds max: %v > %v", hs.StoragePrice, gs.MaxStoragePrice) + } + + // check contract price + if !gs.MaxContractPrice.IsZero() && hs.ContractPrice.Cmp(gs.MaxContractPrice) > 0 { + return fmt.Errorf("contract price exceeds max: %v > %v", hs.ContractPrice, gs.MaxContractPrice) + } + + // check max EA balance + if hs.MaxEphemeralAccountBalance.Cmp(gs.MinMaxEphemeralAccountBalance) < 0 { + return fmt.Errorf("'MaxEphemeralAccountBalance' is less than the allowed minimum value, %v < %v", hs.MaxEphemeralAccountBalance, gs.MinMaxEphemeralAccountBalance) + } + + // check EA expiry + if hs.EphemeralAccountExpiry < gs.MinAccountExpiry { + return fmt.Errorf("'EphemeralAccountExpiry' is less than the allowed minimum value, %v < %v", hs.EphemeralAccountExpiry, gs.MinAccountExpiry) + } + + return nil +} + +// TODO: if we ever stop assuming that certain prices in the pricetable are +// always set to 1H we should account for those fields in +// `hostPeriodCostForScore` as well. +func checkPriceGougingPT(gs api.GougingSettings, cs api.ConsensusState, txnFee types.Currency, pt *rhpv3.HostPriceTable) error { + // check if we have a price table + if pt == nil { + return nil + } + + // check unused defaults + if err := checkUnusedDefaults(*pt); err != nil { + return err + } + + // check base rpc price + if !gs.MaxRPCPrice.IsZero() && gs.MaxRPCPrice.Cmp(pt.InitBaseCost) < 0 { + return fmt.Errorf("init base cost exceeds max: %v > %v", pt.InitBaseCost, gs.MaxRPCPrice) + } + + // check contract price + if !gs.MaxContractPrice.IsZero() && pt.ContractPrice.Cmp(gs.MaxContractPrice) > 0 { + return fmt.Errorf("contract price exceeds max: %v > %v", pt.ContractPrice, gs.MaxContractPrice) + } + + // check max storage + if !gs.MaxStoragePrice.IsZero() && pt.WriteStoreCost.Cmp(gs.MaxStoragePrice) > 0 { + return fmt.Errorf("storage price exceeds max: %v > %v", pt.WriteStoreCost, gs.MaxStoragePrice) + } + + // check max collateral + if pt.MaxCollateral.IsZero() { + return errors.New("MaxCollateral of host is 0") + } + + // check LatestRevisionCost - expect sane value + maxRevisionCost, overflow := gs.MaxRPCPrice.AddWithOverflow(gs.MaxDownloadPrice.Div64(1 << 40).Mul64(2048)) + if overflow { + maxRevisionCost = types.MaxCurrency + } + if pt.LatestRevisionCost.Cmp(maxRevisionCost) > 0 { + return fmt.Errorf("LatestRevisionCost of %v exceeds maximum cost of %v", pt.LatestRevisionCost, maxRevisionCost) + } + + // check block height - if too much time has passed since the last block + // there is a chance we are not up-to-date anymore. So we only check whether + // the host's height is at least equal to ours. + if !cs.Synced || time.Since(cs.LastBlockTime.Std()) > time.Hour { + if pt.HostBlockHeight < cs.BlockHeight { + return fmt.Errorf("consensus not synced and host block height is lower, %v < %v", pt.HostBlockHeight, cs.BlockHeight) + } + } else { + var minHeight uint64 + if cs.BlockHeight >= uint64(gs.HostBlockHeightLeeway) { + minHeight = cs.BlockHeight - uint64(gs.HostBlockHeightLeeway) + } + maxHeight := cs.BlockHeight + uint64(gs.HostBlockHeightLeeway) + if !(minHeight <= pt.HostBlockHeight && pt.HostBlockHeight <= maxHeight) { + return fmt.Errorf("consensus is synced and host block height is not within range, %v-%v %v", minHeight, maxHeight, pt.HostBlockHeight) + } + } + + // check TxnFeeMaxRecommended - expect at most a multiple of our fee + if !txnFee.IsZero() && pt.TxnFeeMaxRecommended.Cmp(txnFee.Mul64(5)) > 0 { + return fmt.Errorf("TxnFeeMaxRecommended %v exceeds %v", pt.TxnFeeMaxRecommended, txnFee.Mul64(5)) + } + + // check TxnFeeMinRecommended - expect it to be lower or equal than the max + if pt.TxnFeeMinRecommended.Cmp(pt.TxnFeeMaxRecommended) > 0 { + return fmt.Errorf("TxnFeeMinRecommended is greater than TxnFeeMaxRecommended, %v > %v", pt.TxnFeeMinRecommended, pt.TxnFeeMaxRecommended) + } + + // check Validity + if pt.Validity < gs.MinPriceTableValidity { + return fmt.Errorf("'Validity' is less than the allowed minimum value, %v < %v", pt.Validity, gs.MinPriceTableValidity) + } + + return nil +} + +func checkContractGougingRHPv2(period, renewWindow *uint64, hs *rhpv2.HostSettings) (err error) { + // period and renew window might be nil since we don't always have access to + // these settings when performing gouging checks + if hs == nil || period == nil || renewWindow == nil { + return nil + } + + err = checkContractGouging(*period, *renewWindow, hs.MaxDuration, hs.WindowSize) + if err != nil { + err = fmt.Errorf("%w: %v", errHostSettingsGouging, err) + } + return +} + +func checkContractGougingRHPv3(period, renewWindow *uint64, pt *rhpv3.HostPriceTable) (err error) { + // period and renew window might be nil since we don't always have access to + // these settings when performing gouging checks + if pt == nil || period == nil || renewWindow == nil { + return nil + } + err = checkContractGouging(*period, *renewWindow, pt.MaxDuration, pt.WindowSize) + if err != nil { + err = fmt.Errorf("%w: %v", ErrPriceTableGouging, err) + } + return +} + +func checkContractGouging(period, renewWindow, maxDuration, windowSize uint64) error { + // check MaxDuration + if period != 0 && period > maxDuration { + return fmt.Errorf("MaxDuration %v is lower than the period %v", maxDuration, period) + } + + // check WindowSize + if renewWindow != 0 && renewWindow < windowSize { + return fmt.Errorf("minimum WindowSize %v is greater than the renew window %v", windowSize, renewWindow) + } + + return nil +} + +func checkPruneGougingRHPv2(gs api.GougingSettings, hs *rhpv2.HostSettings) error { + if hs == nil { + return nil + } + // pruning costs are similar to sector read costs in a way because they + // include base costs and download bandwidth costs, to avoid re-adding all + // RHPv2 cost calculations we reuse download gouging checks to cover pruning + sectorDownloadPrice, overflow := sectorReadCost( + types.NewCurrency64(1), // 1H + hs.SectorAccessPrice, + hs.BaseRPCPrice, + hs.DownloadBandwidthPrice, + hs.UploadBandwidthPrice, + ) + if overflow { + return fmt.Errorf("%w: overflow detected when computing sector download price", errHostSettingsGouging) + } + dpptb, overflow := sectorDownloadPrice.Mul64WithOverflow(1 << 40 / rhpv2.SectorSize) // sectors per TiB + if overflow { + return fmt.Errorf("%w: overflow detected when computing download price per TiB", errHostSettingsGouging) + } + if !gs.MaxDownloadPrice.IsZero() && dpptb.Cmp(gs.MaxDownloadPrice) > 0 { + return fmt.Errorf("%w: cost per TiB exceeds max dl price: %v > %v", errHostSettingsGouging, dpptb, gs.MaxDownloadPrice) + } + return nil +} + +func checkDownloadGougingRHPv3(gs api.GougingSettings, pt *rhpv3.HostPriceTable) error { + if pt == nil { + return nil + } + sectorDownloadPrice, overflow := sectorReadCostRHPv3(*pt) + if overflow { + return fmt.Errorf("%w: overflow detected when computing sector download price", ErrPriceTableGouging) + } + dpptb, overflow := sectorDownloadPrice.Mul64WithOverflow(1 << 40 / rhpv2.SectorSize) // sectors per TiB + if overflow { + return fmt.Errorf("%w: overflow detected when computing download price per TiB", ErrPriceTableGouging) + } + if !gs.MaxDownloadPrice.IsZero() && dpptb.Cmp(gs.MaxDownloadPrice) > 0 { + return fmt.Errorf("%w: cost per TiB exceeds max dl price: %v > %v", ErrPriceTableGouging, dpptb, gs.MaxDownloadPrice) + } + return nil +} + +func checkUploadGougingRHPv3(gs api.GougingSettings, pt *rhpv3.HostPriceTable) error { + if pt == nil { + return nil + } + sectorUploadPricePerMonth, overflow := sectorUploadCostRHPv3(*pt) + if overflow { + return fmt.Errorf("%w: overflow detected when computing sector price", ErrPriceTableGouging) + } + uploadPrice, overflow := sectorUploadPricePerMonth.Mul64WithOverflow(1 << 40 / rhpv2.SectorSize) // sectors per TiB + if overflow { + return fmt.Errorf("%w: overflow detected when computing upload price per TiB", ErrPriceTableGouging) + } + if !gs.MaxUploadPrice.IsZero() && uploadPrice.Cmp(gs.MaxUploadPrice) > 0 { + return fmt.Errorf("%w: cost per TiB exceeds max ul price: %v > %v", ErrPriceTableGouging, uploadPrice, gs.MaxUploadPrice) + } + return nil +} + +func checkUnusedDefaults(pt rhpv3.HostPriceTable) error { + // check ReadLengthCost - should be 1H as it's unused by hosts + if types.NewCurrency64(1).Cmp(pt.ReadLengthCost) < 0 { + return fmt.Errorf("ReadLengthCost of host is %v but should be %v", pt.ReadLengthCost, types.NewCurrency64(1)) + } + + // check WriteLengthCost - should be 1H as it's unused by hosts + if types.NewCurrency64(1).Cmp(pt.WriteLengthCost) < 0 { + return fmt.Errorf("WriteLengthCost of %v exceeds 1H", pt.WriteLengthCost) + } + + // check AccountBalanceCost - should be 1H as it's unused by hosts + if types.NewCurrency64(1).Cmp(pt.AccountBalanceCost) < 0 { + return fmt.Errorf("AccountBalanceCost of %v exceeds 1H", pt.AccountBalanceCost) + } + + // check FundAccountCost - should be 1H as it's unused by hosts + if types.NewCurrency64(1).Cmp(pt.FundAccountCost) < 0 { + return fmt.Errorf("FundAccountCost of %v exceeds 1H", pt.FundAccountCost) + } + + // check UpdatePriceTableCost - should be 1H as it's unused by hosts + if types.NewCurrency64(1).Cmp(pt.UpdatePriceTableCost) < 0 { + return fmt.Errorf("UpdatePriceTableCost of %v exceeds 1H", pt.UpdatePriceTableCost) + } + + // check HasSectorBaseCost - should be 1H as it's unused by hosts + if types.NewCurrency64(1).Cmp(pt.HasSectorBaseCost) < 0 { + return fmt.Errorf("HasSectorBaseCost of %v exceeds 1H", pt.HasSectorBaseCost) + } + + // check MemoryTimeCost - should be 1H as it's unused by hosts + if types.NewCurrency64(1).Cmp(pt.MemoryTimeCost) < 0 { + return fmt.Errorf("MemoryTimeCost of %v exceeds 1H", pt.MemoryTimeCost) + } + + // check DropSectorsBaseCost - should be 1H as it's unused by hosts + if types.NewCurrency64(1).Cmp(pt.DropSectorsBaseCost) < 0 { + return fmt.Errorf("DropSectorsBaseCost of %v exceeds 1H", pt.DropSectorsBaseCost) + } + + // check DropSectorsUnitCost - should be 1H as it's unused by hosts + if types.NewCurrency64(1).Cmp(pt.DropSectorsUnitCost) < 0 { + return fmt.Errorf("DropSectorsUnitCost of %v exceeds 1H", pt.DropSectorsUnitCost) + } + + // check SwapSectorBaseCost - should be 1H as it's unused by hosts + if types.NewCurrency64(1).Cmp(pt.SwapSectorBaseCost) < 0 { + return fmt.Errorf("SwapSectorBaseCost of %v exceeds 1H", pt.SwapSectorBaseCost) + } + + // check SubscriptionMemoryCost - expect 1H default + if types.NewCurrency64(1).Cmp(pt.SubscriptionMemoryCost) < 0 { + return fmt.Errorf("SubscriptionMemoryCost of %v exceeds 1H", pt.SubscriptionMemoryCost) + } + + // check SubscriptionNotificationCost - expect 1H default + if types.NewCurrency64(1).Cmp(pt.SubscriptionNotificationCost) < 0 { + return fmt.Errorf("SubscriptionNotificationCost of %v exceeds 1H", pt.SubscriptionNotificationCost) + } + + // check RenewContractCost - expect 100nS default + if types.Siacoins(1).Mul64(100).Div64(1e9).Cmp(pt.RenewContractCost) < 0 { + return fmt.Errorf("RenewContractCost of %v exceeds 100nS", pt.RenewContractCost) + } + + // check RevisionBaseCost - expect 0H default + if types.ZeroCurrency.Cmp(pt.RevisionBaseCost) < 0 { + return fmt.Errorf("RevisionBaseCost of %v exceeds 0H", pt.RevisionBaseCost) + } + + return nil +} + +func sectorReadCostRHPv3(pt rhpv3.HostPriceTable) (types.Currency, bool) { + return sectorReadCost( + pt.ReadLengthCost, + pt.ReadBaseCost, + pt.InitBaseCost, + pt.UploadBandwidthCost, + pt.DownloadBandwidthCost, + ) +} + +func sectorReadCost(readLengthCost, readBaseCost, initBaseCost, ulBWCost, dlBWCost types.Currency) (types.Currency, bool) { + // base + base, overflow := readLengthCost.Mul64WithOverflow(rhpv2.SectorSize) + if overflow { + return types.ZeroCurrency, true + } + base, overflow = base.AddWithOverflow(readBaseCost) + if overflow { + return types.ZeroCurrency, true + } + base, overflow = base.AddWithOverflow(initBaseCost) + if overflow { + return types.ZeroCurrency, true + } + // bandwidth + ingress, overflow := ulBWCost.Mul64WithOverflow(32) + if overflow { + return types.ZeroCurrency, true + } + egress, overflow := dlBWCost.Mul64WithOverflow(rhpv2.SectorSize) + if overflow { + return types.ZeroCurrency, true + } + // total + total, overflow := base.AddWithOverflow(ingress) + if overflow { + return types.ZeroCurrency, true + } + total, overflow = total.AddWithOverflow(egress) + if overflow { + return types.ZeroCurrency, true + } + return total, false +} + +func sectorUploadCostRHPv3(pt rhpv3.HostPriceTable) (types.Currency, bool) { + // write + writeCost, overflow := pt.WriteLengthCost.Mul64WithOverflow(rhpv2.SectorSize) + if overflow { + return types.ZeroCurrency, true + } + writeCost, overflow = writeCost.AddWithOverflow(pt.WriteBaseCost) + if overflow { + return types.ZeroCurrency, true + } + writeCost, overflow = writeCost.AddWithOverflow(pt.InitBaseCost) + if overflow { + return types.ZeroCurrency, true + } + // bandwidth + ingress, overflow := pt.UploadBandwidthCost.Mul64WithOverflow(rhpv2.SectorSize) + if overflow { + return types.ZeroCurrency, true + } + // total + total, overflow := writeCost.AddWithOverflow(ingress) + if overflow { + return types.ZeroCurrency, true + } + return total, false +} + +func errsToStr(errs ...error) string { + if err := errors.Join(errs...); err != nil { + return err.Error() + } + return "" +} diff --git a/worker/gouging.go b/worker/gouging.go index 753582664..d38dd55ec 100644 --- a/worker/gouging.go +++ b/worker/gouging.go @@ -2,534 +2,27 @@ package worker import ( "context" - "errors" - "fmt" - "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" + "go.sia.tech/renterd/internal/gouging" ) const ( keyGougingChecker contextKey = "GougingChecker" - - // maxBaseRPCPriceVsBandwidth is the max ratio for sane pricing between the - // MinBaseRPCPrice and the MinDownloadBandwidthPrice. This ensures that 1 - // million base RPC charges are at most 1% of the cost to download 4TB. This - // ratio should be used by checking that the MinBaseRPCPrice is less than or - // equal to the MinDownloadBandwidthPrice multiplied by this constant - maxBaseRPCPriceVsBandwidth = uint64(40e3) - - // maxSectorAccessPriceVsBandwidth is the max ratio for sane pricing between - // the MinSectorAccessPrice and the MinDownloadBandwidthPrice. This ensures - // that 1 million base accesses are at most 10% of the cost to download 4TB. - // This ratio should be used by checking that the MinSectorAccessPrice is - // less than or equal to the MinDownloadBandwidthPrice multiplied by this - // constant - maxSectorAccessPriceVsBandwidth = uint64(400e3) -) - -var ( - errHostSettingsGouging = errors.New("host settings gouging detected") - errPriceTableGouging = errors.New("price table gouging detected") -) - -type ( - GougingChecker interface { - Check(_ *rhpv2.HostSettings, _ *rhpv3.HostPriceTable) api.HostGougingBreakdown - CheckSettings(rhpv2.HostSettings) api.HostGougingBreakdown - CheckUnusedDefaults(rhpv3.HostPriceTable) error - BlocksUntilBlockHeightGouging(hostHeight uint64) int64 - } - - gougingChecker struct { - consensusState api.ConsensusState - settings api.GougingSettings - txFee types.Currency - - period *uint64 - renewWindow *uint64 - } - - contextKey string ) -var _ GougingChecker = gougingChecker{} +type contextKey string -func GougingCheckerFromContext(ctx context.Context, criticalMigration bool) (GougingChecker, error) { - gc, ok := ctx.Value(keyGougingChecker).(func(bool) (GougingChecker, error)) +func GougingCheckerFromContext(ctx context.Context, criticalMigration bool) (gouging.Checker, error) { + gc, ok := ctx.Value(keyGougingChecker).(func(bool) (gouging.Checker, error)) if !ok { panic("no gouging checker attached to the context") // developer error } return gc(criticalMigration) } -func newGougingChecker(ctx context.Context, cs ConsensusState, gp api.GougingParams, criticalMigration bool) (GougingChecker, error) { - consensusState, err := cs.ConsensusState(ctx) - if err != nil { - return gougingChecker{}, fmt.Errorf("failed to get consensus state: %w", err) - } - - // adjust the max download price if we are dealing with a critical - // migration that might be failing due to gouging checks - settings := gp.GougingSettings - if criticalMigration && gp.GougingSettings.MigrationSurchargeMultiplier > 0 { - if adjustedMaxDownloadPrice, overflow := gp.GougingSettings.MaxDownloadPrice.Mul64WithOverflow(gp.GougingSettings.MigrationSurchargeMultiplier); !overflow { - settings.MaxDownloadPrice = adjustedMaxDownloadPrice - } - } - - return gougingChecker{ - consensusState: consensusState, - settings: settings, - txFee: gp.TransactionFee, - - // NOTE: - // - // period and renew window are nil here and that's fine, gouging - // checkers in the workers don't have easy access to these settings and - // thus ignore them when perform gouging checks, the autopilot however - // does have those and will pass them when performing gouging checks - period: nil, - renewWindow: nil, - }, nil -} - -func WithGougingChecker(ctx context.Context, cs ConsensusState, gp api.GougingParams) context.Context { - return context.WithValue(ctx, keyGougingChecker, func(criticalMigration bool) (GougingChecker, error) { - return newGougingChecker(ctx, cs, gp, criticalMigration) +func WithGougingChecker(ctx context.Context, cs gouging.ConsensusState, gp api.GougingParams) context.Context { + return context.WithValue(ctx, keyGougingChecker, func(criticalMigration bool) (gouging.Checker, error) { + return gouging.NewWorkerGougingChecker(ctx, cs, gp, criticalMigration) }) } - -func NewGougingChecker(gs api.GougingSettings, cs api.ConsensusState, txnFee types.Currency, period, renewWindow uint64) GougingChecker { - return gougingChecker{ - consensusState: cs, - settings: gs, - txFee: txnFee, - - period: &period, - renewWindow: &renewWindow, - } -} - -func (gc gougingChecker) BlocksUntilBlockHeightGouging(hostHeight uint64) int64 { - blockHeight := gc.consensusState.BlockHeight - leeway := gc.settings.HostBlockHeightLeeway - var minHeight uint64 - if blockHeight >= uint64(leeway) { - minHeight = blockHeight - uint64(leeway) - } - return int64(hostHeight) - int64(minHeight) -} - -func (gc gougingChecker) Check(hs *rhpv2.HostSettings, pt *rhpv3.HostPriceTable) api.HostGougingBreakdown { - if hs == nil && pt == nil { - panic("gouging checker needs to be provided with at least host settings or a price table") // developer error - } - - return api.HostGougingBreakdown{ - ContractErr: errsToStr( - checkContractGougingRHPv2(gc.period, gc.renewWindow, hs), - checkContractGougingRHPv3(gc.period, gc.renewWindow, pt), - ), - DownloadErr: errsToStr(checkDownloadGougingRHPv3(gc.settings, pt)), - GougingErr: errsToStr( - checkPriceGougingPT(gc.settings, gc.consensusState, gc.txFee, pt), - checkPriceGougingHS(gc.settings, hs), - ), - PruneErr: errsToStr(checkPruneGougingRHPv2(gc.settings, hs)), - UploadErr: errsToStr(checkUploadGougingRHPv3(gc.settings, pt)), - } -} - -func (gc gougingChecker) CheckSettings(hs rhpv2.HostSettings) api.HostGougingBreakdown { - return gc.Check(&hs, nil) -} - -func (gc gougingChecker) CheckUnusedDefaults(pt rhpv3.HostPriceTable) error { - return checkUnusedDefaults(pt) -} - -func checkPriceGougingHS(gs api.GougingSettings, hs *rhpv2.HostSettings) error { - // check if we have settings - if hs == nil { - return nil - } - // check base rpc price - if !gs.MaxRPCPrice.IsZero() && hs.BaseRPCPrice.Cmp(gs.MaxRPCPrice) > 0 { - return fmt.Errorf("rpc price exceeds max: %v > %v", hs.BaseRPCPrice, gs.MaxRPCPrice) - } - maxBaseRPCPrice := hs.DownloadBandwidthPrice.Mul64(maxBaseRPCPriceVsBandwidth) - if hs.BaseRPCPrice.Cmp(maxBaseRPCPrice) > 0 { - return fmt.Errorf("rpc price too high, %v > %v", hs.BaseRPCPrice, maxBaseRPCPrice) - } - - // check sector access price - if hs.DownloadBandwidthPrice.IsZero() { - hs.DownloadBandwidthPrice = types.NewCurrency64(1) - } - maxSectorAccessPrice := hs.DownloadBandwidthPrice.Mul64(maxSectorAccessPriceVsBandwidth) - if hs.SectorAccessPrice.Cmp(maxSectorAccessPrice) > 0 { - return fmt.Errorf("sector access price too high, %v > %v", hs.SectorAccessPrice, maxSectorAccessPrice) - } - - // check max storage price - if !gs.MaxStoragePrice.IsZero() && hs.StoragePrice.Cmp(gs.MaxStoragePrice) > 0 { - return fmt.Errorf("storage price exceeds max: %v > %v", hs.StoragePrice, gs.MaxStoragePrice) - } - - // check contract price - if !gs.MaxContractPrice.IsZero() && hs.ContractPrice.Cmp(gs.MaxContractPrice) > 0 { - return fmt.Errorf("contract price exceeds max: %v > %v", hs.ContractPrice, gs.MaxContractPrice) - } - - // check max EA balance - if hs.MaxEphemeralAccountBalance.Cmp(gs.MinMaxEphemeralAccountBalance) < 0 { - return fmt.Errorf("'MaxEphemeralAccountBalance' is less than the allowed minimum value, %v < %v", hs.MaxEphemeralAccountBalance, gs.MinMaxEphemeralAccountBalance) - } - - // check EA expiry - if hs.EphemeralAccountExpiry < gs.MinAccountExpiry { - return fmt.Errorf("'EphemeralAccountExpiry' is less than the allowed minimum value, %v < %v", hs.EphemeralAccountExpiry, gs.MinAccountExpiry) - } - - return nil -} - -// TODO: if we ever stop assuming that certain prices in the pricetable are -// always set to 1H we should account for those fields in -// `hostPeriodCostForScore` as well. -func checkPriceGougingPT(gs api.GougingSettings, cs api.ConsensusState, txnFee types.Currency, pt *rhpv3.HostPriceTable) error { - // check if we have a price table - if pt == nil { - return nil - } - - // check unused defaults - if err := checkUnusedDefaults(*pt); err != nil { - return err - } - - // check base rpc price - if !gs.MaxRPCPrice.IsZero() && gs.MaxRPCPrice.Cmp(pt.InitBaseCost) < 0 { - return fmt.Errorf("init base cost exceeds max: %v > %v", pt.InitBaseCost, gs.MaxRPCPrice) - } - - // check contract price - if !gs.MaxContractPrice.IsZero() && pt.ContractPrice.Cmp(gs.MaxContractPrice) > 0 { - return fmt.Errorf("contract price exceeds max: %v > %v", pt.ContractPrice, gs.MaxContractPrice) - } - - // check max storage - if !gs.MaxStoragePrice.IsZero() && pt.WriteStoreCost.Cmp(gs.MaxStoragePrice) > 0 { - return fmt.Errorf("storage price exceeds max: %v > %v", pt.WriteStoreCost, gs.MaxStoragePrice) - } - - // check max collateral - if pt.MaxCollateral.IsZero() { - return errors.New("MaxCollateral of host is 0") - } - - // check LatestRevisionCost - expect sane value - maxRevisionCost, overflow := gs.MaxRPCPrice.AddWithOverflow(gs.MaxDownloadPrice.Div64(1 << 40).Mul64(2048)) - if overflow { - maxRevisionCost = types.MaxCurrency - } - if pt.LatestRevisionCost.Cmp(maxRevisionCost) > 0 { - return fmt.Errorf("LatestRevisionCost of %v exceeds maximum cost of %v", pt.LatestRevisionCost, maxRevisionCost) - } - - // check block height - if too much time has passed since the last block - // there is a chance we are not up-to-date anymore. So we only check whether - // the host's height is at least equal to ours. - if !cs.Synced || time.Since(cs.LastBlockTime.Std()) > time.Hour { - if pt.HostBlockHeight < cs.BlockHeight { - return fmt.Errorf("consensus not synced and host block height is lower, %v < %v", pt.HostBlockHeight, cs.BlockHeight) - } - } else { - var minHeight uint64 - if cs.BlockHeight >= uint64(gs.HostBlockHeightLeeway) { - minHeight = cs.BlockHeight - uint64(gs.HostBlockHeightLeeway) - } - maxHeight := cs.BlockHeight + uint64(gs.HostBlockHeightLeeway) - if !(minHeight <= pt.HostBlockHeight && pt.HostBlockHeight <= maxHeight) { - return fmt.Errorf("consensus is synced and host block height is not within range, %v-%v %v", minHeight, maxHeight, pt.HostBlockHeight) - } - } - - // check TxnFeeMaxRecommended - expect at most a multiple of our fee - if !txnFee.IsZero() && pt.TxnFeeMaxRecommended.Cmp(txnFee.Mul64(5)) > 0 { - return fmt.Errorf("TxnFeeMaxRecommended %v exceeds %v", pt.TxnFeeMaxRecommended, txnFee.Mul64(5)) - } - - // check TxnFeeMinRecommended - expect it to be lower or equal than the max - if pt.TxnFeeMinRecommended.Cmp(pt.TxnFeeMaxRecommended) > 0 { - return fmt.Errorf("TxnFeeMinRecommended is greater than TxnFeeMaxRecommended, %v > %v", pt.TxnFeeMinRecommended, pt.TxnFeeMaxRecommended) - } - - // check Validity - if pt.Validity < gs.MinPriceTableValidity { - return fmt.Errorf("'Validity' is less than the allowed minimum value, %v < %v", pt.Validity, gs.MinPriceTableValidity) - } - - return nil -} - -func checkContractGougingRHPv2(period, renewWindow *uint64, hs *rhpv2.HostSettings) (err error) { - // period and renew window might be nil since we don't always have access to - // these settings when performing gouging checks - if hs == nil || period == nil || renewWindow == nil { - return nil - } - - err = checkContractGouging(*period, *renewWindow, hs.MaxDuration, hs.WindowSize) - if err != nil { - err = fmt.Errorf("%w: %v", errHostSettingsGouging, err) - } - return -} - -func checkContractGougingRHPv3(period, renewWindow *uint64, pt *rhpv3.HostPriceTable) (err error) { - // period and renew window might be nil since we don't always have access to - // these settings when performing gouging checks - if pt == nil || period == nil || renewWindow == nil { - return nil - } - err = checkContractGouging(*period, *renewWindow, pt.MaxDuration, pt.WindowSize) - if err != nil { - err = fmt.Errorf("%w: %v", errPriceTableGouging, err) - } - return -} - -func checkContractGouging(period, renewWindow, maxDuration, windowSize uint64) error { - // check MaxDuration - if period != 0 && period > maxDuration { - return fmt.Errorf("MaxDuration %v is lower than the period %v", maxDuration, period) - } - - // check WindowSize - if renewWindow != 0 && renewWindow < windowSize { - return fmt.Errorf("minimum WindowSize %v is greater than the renew window %v", windowSize, renewWindow) - } - - return nil -} - -func checkPruneGougingRHPv2(gs api.GougingSettings, hs *rhpv2.HostSettings) error { - if hs == nil { - return nil - } - // pruning costs are similar to sector read costs in a way because they - // include base costs and download bandwidth costs, to avoid re-adding all - // RHPv2 cost calculations we reuse download gouging checks to cover pruning - sectorDownloadPrice, overflow := sectorReadCost( - types.NewCurrency64(1), // 1H - hs.SectorAccessPrice, - hs.BaseRPCPrice, - hs.DownloadBandwidthPrice, - hs.UploadBandwidthPrice, - ) - if overflow { - return fmt.Errorf("%w: overflow detected when computing sector download price", errHostSettingsGouging) - } - dpptb, overflow := sectorDownloadPrice.Mul64WithOverflow(1 << 40 / rhpv2.SectorSize) // sectors per TiB - if overflow { - return fmt.Errorf("%w: overflow detected when computing download price per TiB", errHostSettingsGouging) - } - if !gs.MaxDownloadPrice.IsZero() && dpptb.Cmp(gs.MaxDownloadPrice) > 0 { - return fmt.Errorf("%w: cost per TiB exceeds max dl price: %v > %v", errHostSettingsGouging, dpptb, gs.MaxDownloadPrice) - } - return nil -} - -func checkDownloadGougingRHPv3(gs api.GougingSettings, pt *rhpv3.HostPriceTable) error { - if pt == nil { - return nil - } - sectorDownloadPrice, overflow := sectorReadCostRHPv3(*pt) - if overflow { - return fmt.Errorf("%w: overflow detected when computing sector download price", errPriceTableGouging) - } - dpptb, overflow := sectorDownloadPrice.Mul64WithOverflow(1 << 40 / rhpv2.SectorSize) // sectors per TiB - if overflow { - return fmt.Errorf("%w: overflow detected when computing download price per TiB", errPriceTableGouging) - } - if !gs.MaxDownloadPrice.IsZero() && dpptb.Cmp(gs.MaxDownloadPrice) > 0 { - return fmt.Errorf("%w: cost per TiB exceeds max dl price: %v > %v", errPriceTableGouging, dpptb, gs.MaxDownloadPrice) - } - return nil -} - -func checkUploadGougingRHPv3(gs api.GougingSettings, pt *rhpv3.HostPriceTable) error { - if pt == nil { - return nil - } - sectorUploadPricePerMonth, overflow := sectorUploadCostRHPv3(*pt) - if overflow { - return fmt.Errorf("%w: overflow detected when computing sector price", errPriceTableGouging) - } - uploadPrice, overflow := sectorUploadPricePerMonth.Mul64WithOverflow(1 << 40 / rhpv2.SectorSize) // sectors per TiB - if overflow { - return fmt.Errorf("%w: overflow detected when computing upload price per TiB", errPriceTableGouging) - } - if !gs.MaxUploadPrice.IsZero() && uploadPrice.Cmp(gs.MaxUploadPrice) > 0 { - return fmt.Errorf("%w: cost per TiB exceeds max ul price: %v > %v", errPriceTableGouging, uploadPrice, gs.MaxUploadPrice) - } - return nil -} - -func checkUnusedDefaults(pt rhpv3.HostPriceTable) error { - // check ReadLengthCost - should be 1H as it's unused by hosts - if types.NewCurrency64(1).Cmp(pt.ReadLengthCost) < 0 { - return fmt.Errorf("ReadLengthCost of host is %v but should be %v", pt.ReadLengthCost, types.NewCurrency64(1)) - } - - // check WriteLengthCost - should be 1H as it's unused by hosts - if types.NewCurrency64(1).Cmp(pt.WriteLengthCost) < 0 { - return fmt.Errorf("WriteLengthCost of %v exceeds 1H", pt.WriteLengthCost) - } - - // check AccountBalanceCost - should be 1H as it's unused by hosts - if types.NewCurrency64(1).Cmp(pt.AccountBalanceCost) < 0 { - return fmt.Errorf("AccountBalanceCost of %v exceeds 1H", pt.AccountBalanceCost) - } - - // check FundAccountCost - should be 1H as it's unused by hosts - if types.NewCurrency64(1).Cmp(pt.FundAccountCost) < 0 { - return fmt.Errorf("FundAccountCost of %v exceeds 1H", pt.FundAccountCost) - } - - // check UpdatePriceTableCost - should be 1H as it's unused by hosts - if types.NewCurrency64(1).Cmp(pt.UpdatePriceTableCost) < 0 { - return fmt.Errorf("UpdatePriceTableCost of %v exceeds 1H", pt.UpdatePriceTableCost) - } - - // check HasSectorBaseCost - should be 1H as it's unused by hosts - if types.NewCurrency64(1).Cmp(pt.HasSectorBaseCost) < 0 { - return fmt.Errorf("HasSectorBaseCost of %v exceeds 1H", pt.HasSectorBaseCost) - } - - // check MemoryTimeCost - should be 1H as it's unused by hosts - if types.NewCurrency64(1).Cmp(pt.MemoryTimeCost) < 0 { - return fmt.Errorf("MemoryTimeCost of %v exceeds 1H", pt.MemoryTimeCost) - } - - // check DropSectorsBaseCost - should be 1H as it's unused by hosts - if types.NewCurrency64(1).Cmp(pt.DropSectorsBaseCost) < 0 { - return fmt.Errorf("DropSectorsBaseCost of %v exceeds 1H", pt.DropSectorsBaseCost) - } - - // check DropSectorsUnitCost - should be 1H as it's unused by hosts - if types.NewCurrency64(1).Cmp(pt.DropSectorsUnitCost) < 0 { - return fmt.Errorf("DropSectorsUnitCost of %v exceeds 1H", pt.DropSectorsUnitCost) - } - - // check SwapSectorBaseCost - should be 1H as it's unused by hosts - if types.NewCurrency64(1).Cmp(pt.SwapSectorBaseCost) < 0 { - return fmt.Errorf("SwapSectorBaseCost of %v exceeds 1H", pt.SwapSectorBaseCost) - } - - // check SubscriptionMemoryCost - expect 1H default - if types.NewCurrency64(1).Cmp(pt.SubscriptionMemoryCost) < 0 { - return fmt.Errorf("SubscriptionMemoryCost of %v exceeds 1H", pt.SubscriptionMemoryCost) - } - - // check SubscriptionNotificationCost - expect 1H default - if types.NewCurrency64(1).Cmp(pt.SubscriptionNotificationCost) < 0 { - return fmt.Errorf("SubscriptionNotificationCost of %v exceeds 1H", pt.SubscriptionNotificationCost) - } - - // check RenewContractCost - expect 100nS default - if types.Siacoins(1).Mul64(100).Div64(1e9).Cmp(pt.RenewContractCost) < 0 { - return fmt.Errorf("RenewContractCost of %v exceeds 100nS", pt.RenewContractCost) - } - - // check RevisionBaseCost - expect 0H default - if types.ZeroCurrency.Cmp(pt.RevisionBaseCost) < 0 { - return fmt.Errorf("RevisionBaseCost of %v exceeds 0H", pt.RevisionBaseCost) - } - - return nil -} - -func sectorReadCostRHPv3(pt rhpv3.HostPriceTable) (types.Currency, bool) { - return sectorReadCost( - pt.ReadLengthCost, - pt.ReadBaseCost, - pt.InitBaseCost, - pt.UploadBandwidthCost, - pt.DownloadBandwidthCost, - ) -} - -func sectorReadCost(readLengthCost, readBaseCost, initBaseCost, ulBWCost, dlBWCost types.Currency) (types.Currency, bool) { - // base - base, overflow := readLengthCost.Mul64WithOverflow(rhpv2.SectorSize) - if overflow { - return types.ZeroCurrency, true - } - base, overflow = base.AddWithOverflow(readBaseCost) - if overflow { - return types.ZeroCurrency, true - } - base, overflow = base.AddWithOverflow(initBaseCost) - if overflow { - return types.ZeroCurrency, true - } - // bandwidth - ingress, overflow := ulBWCost.Mul64WithOverflow(32) - if overflow { - return types.ZeroCurrency, true - } - egress, overflow := dlBWCost.Mul64WithOverflow(rhpv2.SectorSize) - if overflow { - return types.ZeroCurrency, true - } - // total - total, overflow := base.AddWithOverflow(ingress) - if overflow { - return types.ZeroCurrency, true - } - total, overflow = total.AddWithOverflow(egress) - if overflow { - return types.ZeroCurrency, true - } - return total, false -} - -func sectorUploadCostRHPv3(pt rhpv3.HostPriceTable) (types.Currency, bool) { - // write - writeCost, overflow := pt.WriteLengthCost.Mul64WithOverflow(rhpv2.SectorSize) - if overflow { - return types.ZeroCurrency, true - } - writeCost, overflow = writeCost.AddWithOverflow(pt.WriteBaseCost) - if overflow { - return types.ZeroCurrency, true - } - writeCost, overflow = writeCost.AddWithOverflow(pt.InitBaseCost) - if overflow { - return types.ZeroCurrency, true - } - // bandwidth - ingress, overflow := pt.UploadBandwidthCost.Mul64WithOverflow(rhpv2.SectorSize) - if overflow { - return types.ZeroCurrency, true - } - // total - total, overflow := writeCost.AddWithOverflow(ingress) - if overflow { - return types.ZeroCurrency, true - } - return total, false -} - -func errsToStr(errs ...error) string { - if err := errors.Join(errs...); err != nil { - return err.Error() - } - return "" -} diff --git a/worker/host.go b/worker/host.go index ceb9b24b2..5be166ead 100644 --- a/worker/host.go +++ b/worker/host.go @@ -12,6 +12,7 @@ import ( rhpv3 "go.sia.tech/core/rhp/v3" "go.sia.tech/core/types" "go.sia.tech/renterd/api" + "go.sia.tech/renterd/internal/gouging" "go.uber.org/zap" ) @@ -93,7 +94,7 @@ func (h *host) DownloadSector(ctx context.Context, w io.Writer, root types.Hash2 return err } if breakdown := gc.Check(nil, &hpt); breakdown.DownloadErr != "" { - return fmt.Errorf("%w: %v", errPriceTableGouging, breakdown.DownloadErr) + return fmt.Errorf("%w: %v", gouging.ErrPriceTableGouging, breakdown.DownloadErr) } // return errBalanceInsufficient if balance insufficient @@ -239,7 +240,7 @@ func (h *host) FundAccount(ctx context.Context, balance types.Currency, rev *typ if err != nil { return err } else if err := gc.CheckUnusedDefaults(pt.HostPriceTable); err != nil { - return fmt.Errorf("%w: %v", errPriceTableGouging, err) + return fmt.Errorf("%w: %v", gouging.ErrPriceTableGouging, err) } // check whether we have money left in the contract @@ -288,7 +289,7 @@ func (h *host) SyncAccount(ctx context.Context, rev *types.FileContractRevision) if err != nil { return err } else if err := gc.CheckUnusedDefaults(pt.HostPriceTable); err != nil { - return fmt.Errorf("%w: %v", errPriceTableGouging, err) + return fmt.Errorf("%w: %v", gouging.ErrPriceTableGouging, err) } return h.acc.WithSync(ctx, func() (types.Currency, error) { diff --git a/worker/mocks_test.go b/worker/mocks_test.go index 4c3929205..0eae14d5d 100644 --- a/worker/mocks_test.go +++ b/worker/mocks_test.go @@ -15,6 +15,7 @@ import ( "go.sia.tech/core/types" "go.sia.tech/renterd/alerts" "go.sia.tech/renterd/api" + "go.sia.tech/renterd/internal/gouging" "go.sia.tech/renterd/object" "go.sia.tech/renterd/webhooks" ) @@ -61,7 +62,7 @@ func (*alerterMock) Alerts(_ context.Context, opts alerts.AlertsOpts) (resp aler func (*alerterMock) RegisterAlert(context.Context, alerts.Alert) error { return nil } func (*alerterMock) DismissAlerts(context.Context, ...types.Hash256) error { return nil } -var _ ConsensusState = (*chainMock)(nil) +var _ gouging.ConsensusState = (*chainMock)(nil) type chainMock struct { cs api.ConsensusState diff --git a/worker/rhpv3.go b/worker/rhpv3.go index ab3ae0b36..3ba85c01f 100644 --- a/worker/rhpv3.go +++ b/worker/rhpv3.go @@ -18,6 +18,7 @@ import ( "go.sia.tech/core/types" "go.sia.tech/mux/v1" "go.sia.tech/renterd/api" + "go.sia.tech/renterd/internal/gouging" "go.sia.tech/renterd/internal/utils" "go.uber.org/zap" ) @@ -106,7 +107,7 @@ func isClosedStream(err error) bool { } func isInsufficientFunds(err error) bool { return utils.IsErr(err, errInsufficientFunds) } func isPriceTableExpired(err error) bool { return utils.IsErr(err, errPriceTableExpired) } -func isPriceTableGouging(err error) bool { return utils.IsErr(err, errPriceTableGouging) } +func isPriceTableGouging(err error) bool { return utils.IsErr(err, gouging.ErrPriceTableGouging) } func isPriceTableNotFound(err error) bool { return utils.IsErr(err, errPriceTableNotFound) } func isSectorNotFound(err error) bool { return utils.IsErr(err, errSectorNotFound) || utils.IsErr(err, errSectorNotFoundOld) @@ -549,7 +550,7 @@ func (h *host) priceTable(ctx context.Context, rev *types.FileContractRevision) return rhpv3.HostPriceTable{}, err } if breakdown := gc.Check(nil, &pt.HostPriceTable); breakdown.Gouging() { - return rhpv3.HostPriceTable{}, fmt.Errorf("%w: %v", errPriceTableGouging, breakdown) + return rhpv3.HostPriceTable{}, fmt.Errorf("%w: %v", gouging.ErrPriceTableGouging, breakdown) } return pt.HostPriceTable, nil } diff --git a/worker/worker.go b/worker/worker.go index dcdc8ae12..e54bb4fb6 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -24,6 +24,7 @@ import ( "go.sia.tech/renterd/alerts" "go.sia.tech/renterd/api" "go.sia.tech/renterd/build" + "go.sia.tech/renterd/internal/gouging" clientV2 "go.sia.tech/renterd/internal/rhp/v2" "go.sia.tech/renterd/internal/utils" iworker "go.sia.tech/renterd/internal/worker" @@ -66,7 +67,7 @@ func NewClient(address, password string) *Client { type ( Bus interface { alerts.Alerter - ConsensusState + gouging.ConsensusState webhooks.Broadcaster AccountStore @@ -156,10 +157,6 @@ type ( RegisterWebhook(ctx context.Context, webhook webhooks.Webhook) error UnregisterWebhook(ctx context.Context, webhook webhooks.Webhook) error } - - ConsensusState interface { - ConsensusState(ctx context.Context) (api.ConsensusState, error) - } ) // deriveSubKey can be used to derive a sub-masterkey from the worker's @@ -409,7 +406,7 @@ func (w *worker) rhpFormHandler(jc jape.Context) { if jc.Check("could not get gouging parameters", err) != nil { return } - gc, err := newGougingChecker(ctx, w.bus, gp, false) + gc, err := gouging.NewWorkerGougingChecker(ctx, w.bus, gp, false) if jc.Check("could not create gouging checker", err) != nil { return } @@ -546,15 +543,12 @@ func (w *worker) rhpPruneContractHandlerPOST(jc jape.Context) { if jc.Check("could not fetch gouging parameters", err) != nil { return } - gc, err := newGougingChecker(ctx, w.bus, gp, false) + gc, err := gouging.NewWorkerGougingChecker(ctx, w.bus, gp, false) if err != nil { jc.Error(err, http.StatusInternalServerError) return } - // attach gouging checker - ctx = WithGougingChecker(ctx, w.bus, gp) - // prune the contract var pruned, remaining uint64 var rev *types.FileContractRevision @@ -609,12 +603,13 @@ func (w *worker) rhpContractRootsHandlerGET(jc jape.Context) { if jc.Check("couldn't fetch gouging parameters from bus", err) != nil { return } - - // attach gouging checker to the context - ctx = WithGougingChecker(ctx, w.bus, gp) + gc, err := gouging.NewWorkerGougingChecker(ctx, w.bus, gp, false) + if jc.Check("couldn't create gouging checker", err) != nil { + return + } // fetch the roots from the host - roots, rev, cost, err := w.rhp2Client.FetchContractRoots(ctx, w.deriveRenterKey(c.HostKey), nil, c.HostIP, c.HostKey, id, c.RevisionNumber) + roots, rev, cost, err := w.rhp2Client.FetchContractRoots(ctx, w.deriveRenterKey(c.HostKey), gc.CheckSettings, c.HostIP, c.HostKey, id, c.RevisionNumber) if jc.Check("couldn't fetch contract roots from host", err) != nil { return } else if rev != nil { @@ -1759,7 +1754,7 @@ type HostErrorSet map[types.PublicKey]error // NumGouging returns numbers of host that errored out due to price gouging. func (hes HostErrorSet) NumGouging() (n int) { for _, he := range hes { - if errors.Is(he, errPriceTableGouging) { + if errors.Is(he, gouging.ErrPriceTableGouging) { n++ } } From 1a4864a800126e95ee19a2ed3a23d13529582887 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Thu, 15 Aug 2024 16:01:20 +0200 Subject: [PATCH 6/7] worker: newGougingChecker --- autopilot/contractor/evaluate.go | 4 ++-- autopilot/contractor/state.go | 3 ++- internal/gouging/gouging.go | 37 +++----------------------------- internal/rhp/v2/rhp.go | 8 +++---- worker/gouging.go | 19 +++++++++++++++- worker/worker.go | 22 ++++++------------- 6 files changed, 35 insertions(+), 58 deletions(-) diff --git a/autopilot/contractor/evaluate.go b/autopilot/contractor/evaluate.go index b617b307b..e947009cb 100644 --- a/autopilot/contractor/evaluate.go +++ b/autopilot/contractor/evaluate.go @@ -11,7 +11,7 @@ import ( var ErrMissingRequiredFields = errors.New("missing required fields in configuration, both allowance and amount must be set") func countUsableHosts(cfg api.AutopilotConfig, cs api.ConsensusState, fee types.Currency, period uint64, rs api.RedundancySettings, gs api.GougingSettings, hosts []api.Host) (usables uint64) { - gc := gouging.NewChecker(gs, cs, fee, period, cfg.Contracts.RenewWindow) + gc := gouging.NewChecker(gs, cs, fee, &period, &cfg.Contracts.RenewWindow) for _, host := range hosts { hc := checkHost(gc, scoreHost(host, cfg, rs.Redundancy()), minValidScore) if hc.Usability.IsUsable() { @@ -31,7 +31,7 @@ func EvaluateConfig(cfg api.AutopilotConfig, cs api.ConsensusState, fee types.Cu } period := cfg.Contracts.Period - gc := gouging.NewChecker(gs, cs, fee, period, cfg.Contracts.RenewWindow) + gc := gouging.NewChecker(gs, cs, fee, &period, &cfg.Contracts.RenewWindow) resp.Hosts = uint64(len(hosts)) for i, host := range hosts { diff --git a/autopilot/contractor/state.go b/autopilot/contractor/state.go index dcbb910e8..0b786adf1 100644 --- a/autopilot/contractor/state.go +++ b/autopilot/contractor/state.go @@ -74,7 +74,8 @@ func (ctx *mCtx) Err() error { } func (ctx *mCtx) GougingChecker(cs api.ConsensusState) gouging.Checker { - return gouging.NewChecker(ctx.state.GS, cs, ctx.state.Fee, ctx.Period(), ctx.RenewWindow()) + period, renewWindow := ctx.Period(), ctx.RenewWindow() + return gouging.NewChecker(ctx.state.GS, cs, ctx.state.Fee, &period, &renewWindow) } func (ctx *mCtx) HostScore(h api.Host) (sb api.HostScoreBreakdown, err error) { diff --git a/internal/gouging/gouging.go b/internal/gouging/gouging.go index 926386dae..b56de19a4 100644 --- a/internal/gouging/gouging.go +++ b/internal/gouging/gouging.go @@ -58,45 +58,14 @@ type ( var _ Checker = checker{} -func NewWorkerGougingChecker(ctx context.Context, cs ConsensusState, gp api.GougingParams, criticalMigration bool) (Checker, error) { - consensusState, err := cs.ConsensusState(ctx) - if err != nil { - return checker{}, fmt.Errorf("failed to get consensus state: %w", err) - } - - // adjust the max download price if we are dealing with a critical - // migration that might be failing due to gouging checks - settings := gp.GougingSettings - if criticalMigration && gp.GougingSettings.MigrationSurchargeMultiplier > 0 { - if adjustedMaxDownloadPrice, overflow := gp.GougingSettings.MaxDownloadPrice.Mul64WithOverflow(gp.GougingSettings.MigrationSurchargeMultiplier); !overflow { - settings.MaxDownloadPrice = adjustedMaxDownloadPrice - } - } - - return checker{ - consensusState: consensusState, - settings: settings, - txFee: gp.TransactionFee, - - // NOTE: - // - // period and renew window are nil here and that's fine, gouging - // checkers in the workers don't have easy access to these settings and - // thus ignore them when perform gouging checks, the autopilot however - // does have those and will pass them when performing gouging checks - period: nil, - renewWindow: nil, - }, nil -} - -func NewChecker(gs api.GougingSettings, cs api.ConsensusState, txnFee types.Currency, period, renewWindow uint64) Checker { +func NewChecker(gs api.GougingSettings, cs api.ConsensusState, txnFee types.Currency, period, renewWindow *uint64) Checker { return checker{ consensusState: cs, settings: gs, txFee: txnFee, - period: &period, - renewWindow: &renewWindow, + period: period, + renewWindow: renewWindow, } } diff --git a/internal/rhp/v2/rhp.go b/internal/rhp/v2/rhp.go index 6d86d6387..071f3b141 100644 --- a/internal/rhp/v2/rhp.go +++ b/internal/rhp/v2/rhp.go @@ -83,7 +83,7 @@ func New(logger *zap.Logger) *Client { } } -func (w *Client) FetchContractRoots(ctx context.Context, renterKey types.PrivateKey, gougingCheck GougingCheckFn, hostIP string, hostKey types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64) (roots []types.Hash256, revision *types.FileContractRevision, cost types.Currency, err error) { +func (w *Client) ContractRoots(ctx context.Context, renterKey types.PrivateKey, gougingCheck GougingCheckFn, hostIP string, hostKey types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64) (roots []types.Hash256, revision *types.FileContractRevision, cost types.Currency, err error) { err = w.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { return w.withRevisionV2(renterKey, gougingCheck, t, fcid, lastKnownRevisionNumber, func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) (err error) { roots, cost, err = w.fetchContractRoots(t, renterKey, &rev, settings) @@ -94,8 +94,8 @@ func (w *Client) FetchContractRoots(ctx context.Context, renterKey types.Private return } -// FetchSignedRevision fetches the latest signed revision for a contract from a host. -func (w *Client) FetchSignedRevision(ctx context.Context, hostIP string, hostKey types.PublicKey, renterKey types.PrivateKey, contractID types.FileContractID, timeout time.Duration) (rhpv2.ContractRevision, error) { +// SignedRevision fetches the latest signed revision for a contract from a host. +func (w *Client) SignedRevision(ctx context.Context, hostIP string, hostKey types.PublicKey, renterKey types.PrivateKey, contractID types.FileContractID, timeout time.Duration) (rhpv2.ContractRevision, error) { var rev rhpv2.ContractRevision err := w.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { req := &rhpv2.RPCLockRequest{ @@ -139,7 +139,7 @@ func (w *Client) FetchSignedRevision(ctx context.Context, hostIP string, hostKey return rev, err } -func (c *Client) FetchSettings(ctx context.Context, hostKey types.PublicKey, hostIP string) (settings rhpv2.HostSettings, err error) { +func (c *Client) Settings(ctx context.Context, hostKey types.PublicKey, hostIP string) (settings rhpv2.HostSettings, err error) { err = c.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { var err error if settings, err = rpcSettings(ctx, t); err != nil { diff --git a/worker/gouging.go b/worker/gouging.go index d38dd55ec..eccc5785e 100644 --- a/worker/gouging.go +++ b/worker/gouging.go @@ -2,7 +2,9 @@ package worker import ( "context" + "fmt" + "go.sia.tech/core/types" "go.sia.tech/renterd/api" "go.sia.tech/renterd/internal/gouging" ) @@ -23,6 +25,21 @@ func GougingCheckerFromContext(ctx context.Context, criticalMigration bool) (gou func WithGougingChecker(ctx context.Context, cs gouging.ConsensusState, gp api.GougingParams) context.Context { return context.WithValue(ctx, keyGougingChecker, func(criticalMigration bool) (gouging.Checker, error) { - return gouging.NewWorkerGougingChecker(ctx, cs, gp, criticalMigration) + cs, err := cs.ConsensusState(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get consensus state: %w", err) + } + return newGougingChecker(gp.GougingSettings, cs, gp.TransactionFee, criticalMigration), nil }) } + +func newGougingChecker(settings api.GougingSettings, cs api.ConsensusState, txnFee types.Currency, criticalMigration bool) gouging.Checker { + // adjust the max download price if we are dealing with a critical + // migration that might be failing due to gouging checks + if criticalMigration && settings.MigrationSurchargeMultiplier > 0 { + if adjustedMaxDownloadPrice, overflow := settings.MaxDownloadPrice.Mul64WithOverflow(settings.MigrationSurchargeMultiplier); !overflow { + settings.MaxDownloadPrice = adjustedMaxDownloadPrice + } + } + return gouging.NewChecker(settings, cs, txnFee, nil, nil) +} diff --git a/worker/worker.go b/worker/worker.go index e54bb4fb6..2ecb9db37 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -406,10 +406,7 @@ func (w *worker) rhpFormHandler(jc jape.Context) { if jc.Check("could not get gouging parameters", err) != nil { return } - gc, err := gouging.NewWorkerGougingChecker(ctx, w.bus, gp, false) - if jc.Check("could not create gouging checker", 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 @@ -464,7 +461,7 @@ func (w *worker) rhpBroadcastHandler(jc jape.Context) { } rk := w.deriveRenterKey(c.HostKey) - rev, err := w.rhp2Client.FetchSignedRevision(ctx, c.HostIP, c.HostKey, rk, fcid, time.Minute) + rev, err := w.rhp2Client.SignedRevision(ctx, c.HostIP, c.HostKey, rk, fcid, time.Minute) if jc.Check("could not fetch revision", err) != nil { return } @@ -543,11 +540,7 @@ func (w *worker) rhpPruneContractHandlerPOST(jc jape.Context) { if jc.Check("could not fetch gouging parameters", err) != nil { return } - gc, err := gouging.NewWorkerGougingChecker(ctx, w.bus, gp, false) - if err != nil { - jc.Error(err, http.StatusInternalServerError) - return - } + gc := newGougingChecker(gp.GougingSettings, gp.ConsensusState, gp.TransactionFee, false) // prune the contract var pruned, remaining uint64 @@ -603,13 +596,10 @@ func (w *worker) rhpContractRootsHandlerGET(jc jape.Context) { if jc.Check("couldn't fetch gouging parameters from bus", err) != nil { return } - gc, err := gouging.NewWorkerGougingChecker(ctx, w.bus, gp, false) - if jc.Check("couldn't create gouging checker", err) != nil { - return - } + gc := newGougingChecker(gp.GougingSettings, gp.ConsensusState, gp.TransactionFee, false) // fetch the roots from the host - roots, rev, cost, err := w.rhp2Client.FetchContractRoots(ctx, w.deriveRenterKey(c.HostKey), gc.CheckSettings, c.HostIP, c.HostKey, id, c.RevisionNumber) + roots, rev, cost, err := w.rhp2Client.ContractRoots(ctx, w.deriveRenterKey(c.HostKey), gc.CheckSettings, c.HostIP, c.HostKey, id, c.RevisionNumber) if jc.Check("couldn't fetch contract roots from host", err) != nil { return } else if rev != nil { @@ -1401,7 +1391,7 @@ func (w *worker) scanHost(ctx context.Context, timeout time.Duration, hostKey ty // fetch the host settings start := time.Now() scanCtx, cancel := timeoutCtx() - settings, err := w.rhp2Client.FetchSettings(scanCtx, hostKey, hostIP) + settings, err := w.rhp2Client.Settings(scanCtx, hostKey, hostIP) cancel() if err != nil { return settings, rhpv3.HostPriceTable{}, time.Since(start), err From e1067e414e209cea9f970e81f8a07d57804e18cd Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Thu, 15 Aug 2024 16:10:11 +0200 Subject: [PATCH 7/7] rhp: remove GougingCheckFn --- internal/rhp/v2/rhp.go | 20 +++++++++----------- worker/worker.go | 6 +++--- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/internal/rhp/v2/rhp.go b/internal/rhp/v2/rhp.go index 071f3b141..cb8ce5f31 100644 --- a/internal/rhp/v2/rhp.go +++ b/internal/rhp/v2/rhp.go @@ -13,7 +13,7 @@ import ( rhpv2 "go.sia.tech/core/rhp/v2" "go.sia.tech/core/types" - "go.sia.tech/renterd/api" + "go.sia.tech/renterd/internal/gouging" "go.sia.tech/renterd/internal/utils" "go.uber.org/zap" "lukechampine.com/frand" @@ -68,8 +68,6 @@ var ( ) type ( - GougingCheckFn func(settings rhpv2.HostSettings) api.HostGougingBreakdown - 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) ) @@ -83,9 +81,9 @@ func New(logger *zap.Logger) *Client { } } -func (w *Client) ContractRoots(ctx context.Context, renterKey types.PrivateKey, gougingCheck GougingCheckFn, hostIP string, hostKey types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64) (roots []types.Hash256, revision *types.FileContractRevision, cost types.Currency, err error) { +func (w *Client) ContractRoots(ctx context.Context, renterKey types.PrivateKey, gougingChecker gouging.Checker, hostIP string, hostKey types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64) (roots []types.Hash256, revision *types.FileContractRevision, cost types.Currency, err error) { err = w.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { - return w.withRevisionV2(renterKey, gougingCheck, t, fcid, lastKnownRevisionNumber, func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) (err error) { + return w.withRevisionV2(renterKey, gougingChecker, t, fcid, lastKnownRevisionNumber, func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) (err error) { roots, cost, err = w.fetchContractRoots(t, renterKey, &rev, settings) revision = &rev.Revision return @@ -153,14 +151,14 @@ 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, checkGouging GougingCheckFn, prepareForm PrepareFormFn) (contract rhpv2.ContractRevision, txnSet []types.Transaction, err error) { +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) { err = c.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) (err error) { settings, err := rpcSettings(ctx, t) if err != nil { return err } - if breakdown := checkGouging(settings); breakdown.Gouging() { + if breakdown := gougingChecker.CheckSettings(settings); breakdown.Gouging() { return fmt.Errorf("failed to form contract, gouging check failed: %v", breakdown) } @@ -179,9 +177,9 @@ func (c *Client) FormContract(ctx context.Context, renterAddress types.Address, return } -func (c *Client) PruneContract(ctx context.Context, renterKey types.PrivateKey, gougingCheck GougingCheckFn, hostIP string, hostKey types.PublicKey, fcid types.FileContractID, lastKnownRevisionNumber uint64, toKeep []types.Hash256) (revision *types.FileContractRevision, deleted, remaining uint64, cost types.Currency, err error) { +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.withTransportV2(ctx, hostKey, hostIP, func(t *rhpv2.Transport) error { - return c.withRevisionV2(renterKey, gougingCheck, t, fcid, lastKnownRevisionNumber, func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) (err error) { + return c.withRevisionV2(renterKey, gougingChecker, t, fcid, lastKnownRevisionNumber, func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) (err error) { // fetch roots got, fetchCost, err := c.fetchContractRoots(t, renterKey, &rev, settings) if err != nil { @@ -512,7 +510,7 @@ func (c *Client) fetchContractRoots(t *rhpv2.Transport, renterKey types.PrivateK return } -func (w *Client) withRevisionV2(renterKey types.PrivateKey, gougingCheck GougingCheckFn, t *rhpv2.Transport, fcid types.FileContractID, lastKnownRevisionNumber uint64, fn func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) error) error { +func (w *Client) withRevisionV2(renterKey types.PrivateKey, gougingChecker gouging.Checker, t *rhpv2.Transport, fcid types.FileContractID, lastKnownRevisionNumber uint64, fn func(t *rhpv2.Transport, rev rhpv2.ContractRevision, settings rhpv2.HostSettings) error) error { // execute lock RPC var lockResp rhpv2.RPCLockResponse err := t.Call(rhpv2.RPCLockID, &rhpv2.RPCLockRequest{ @@ -564,7 +562,7 @@ func (w *Client) withRevisionV2(renterKey types.PrivateKey, gougingCheck Gouging } // perform gouging checks on settings - if breakdown := gougingCheck(settings); breakdown.Gouging() { + if breakdown := gougingChecker.CheckSettings(settings); breakdown.Gouging() { return fmt.Errorf("failed to prune contract: %v", breakdown) } diff --git a/worker/worker.go b/worker/worker.go index 2ecb9db37..0b9267041 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -412,7 +412,7 @@ func (w *worker) rhpFormHandler(jc jape.Context) { 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.CheckSettings, 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) { + 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 @@ -551,7 +551,7 @@ func (w *worker) rhpPruneContractHandlerPOST(jc jape.Context) { if err != nil { return fmt.Errorf("failed to fetch contract roots; %w", err) } - rev, pruned, remaining, cost, err = w.rhp2Client.PruneContract(ctx, w.deriveRenterKey(contract.HostKey), gc.CheckSettings, contract.HostIP, contract.HostKey, fcid, contract.RevisionNumber, append(stored, pending...)) + rev, pruned, remaining, cost, err = w.rhp2Client.PruneContract(ctx, w.deriveRenterKey(contract.HostKey), gc, contract.HostIP, contract.HostKey, fcid, contract.RevisionNumber, append(stored, pending...)) return err }) if rev != nil { @@ -599,7 +599,7 @@ func (w *worker) rhpContractRootsHandlerGET(jc jape.Context) { gc := newGougingChecker(gp.GougingSettings, gp.ConsensusState, gp.TransactionFee, false) // fetch the roots from the host - roots, rev, cost, err := w.rhp2Client.ContractRoots(ctx, w.deriveRenterKey(c.HostKey), gc.CheckSettings, c.HostIP, c.HostKey, id, c.RevisionNumber) + roots, rev, cost, err := w.rhp2Client.ContractRoots(ctx, w.deriveRenterKey(c.HostKey), gc, c.HostIP, c.HostKey, id, c.RevisionNumber) if jc.Check("couldn't fetch contract roots from host", err) != nil { return } else if rev != nil {