Skip to content

Commit

Permalink
worker: only check unused defaults when funding account
Browse files Browse the repository at this point in the history
  • Loading branch information
peterjan authored and ChrisSchinnerl committed Jul 29, 2024
1 parent 5148653 commit 9ecf0ef
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 70 deletions.
56 changes: 56 additions & 0 deletions internal/test/e2e/gouging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"go.sia.tech/core/types"
"go.sia.tech/renterd/api"
"go.sia.tech/renterd/internal/test"
"go.uber.org/zap/zapcore"
"lukechampine.com/frand"
)

Expand Down Expand Up @@ -72,6 +73,7 @@ func TestGouging(t *testing.T) {
if err := b.UpdateSetting(context.Background(), api.SettingGouging, gs); err != nil {
t.Fatal(err)
}

// fetch current contract set
contracts, err := b.Contracts(context.Background(), api.ContractsOpts{ContractSet: cfg.Set})
tt.OK(err)
Expand Down Expand Up @@ -134,6 +136,60 @@ func TestGouging(t *testing.T) {
})
}

// TestAccountFunding is a regression tests that verify we can fund an account
// even if the host is considered gouging, this protects us from not being able
// to download from certain critical hosts when we migrate away from them.
func TestAccountFunding(t *testing.T) {
if testing.Short() {
t.SkipNow()
}

// run without autopilot
opts := clusterOptsDefault
opts.skipRunningAutopilot = true
opts.logger = newTestLoggerCustom(zapcore.ErrorLevel)

// create a new test cluster
cluster := newTestCluster(t, opts)
defer cluster.Shutdown()

// convenience variables
b := cluster.Bus
w := cluster.Worker
tt := cluster.tt

// add a host
hosts := cluster.AddHosts(1)
h, err := b.Host(context.Background(), hosts[0].PublicKey())
tt.OK(err)

// scan the host
_, err = w.RHPScan(context.Background(), h.PublicKey, h.NetAddress, 10*time.Second)
tt.OK(err)

// manually form a contract with the host
cs, _ := b.ConsensusState(context.Background())
wallet, _ := b.Wallet(context.Background())
rev, _, err := w.RHPForm(context.Background(), cs.BlockHeight+test.AutopilotConfig.Contracts.Period+test.AutopilotConfig.Contracts.RenewWindow, h.PublicKey, h.NetAddress, wallet.Address, types.Siacoins(1), types.Siacoins(1))
tt.OK(err)
c, err := b.AddContract(context.Background(), rev, rev.Revision.MissedHostPayout().Sub(types.Siacoins(1)), types.Siacoins(1), cs.BlockHeight, api.ContractStatePending)
tt.OK(err)

// fund the account
tt.OK(w.RHPFund(context.Background(), c.ID, c.HostKey, c.HostIP, c.SiamuxAddr, types.Siacoins(1).Div64(2)))

// update host so it's gouging
settings := hosts[0].settings.Settings()
settings.StoragePrice = types.Siacoins(1)
tt.OK(hosts[0].UpdateSettings(settings))

// ensure the price table expires so the worker is forced to fetch it
time.Sleep(defaultHostSettings.PriceTableValidity)

// fund the account again
tt.OK(w.RHPFund(context.Background(), c.ID, c.HostKey, c.HostIP, c.SiamuxAddr, types.Siacoins(1)))
}

func TestHostMinVersion(t *testing.T) {
if testing.Short() {
t.SkipNow()
Expand Down
154 changes: 85 additions & 69 deletions worker/gouging.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var (
type (
GougingChecker interface {
Check(_ *rhpv2.HostSettings, _ *rhpv3.HostPriceTable) api.HostGougingBreakdown
CheckUnusedDefaults(rhpv3.HostPriceTable) error
BlocksUntilBlockHeightGouging(hostHeight uint64) int64
}

Expand Down Expand Up @@ -138,6 +139,10 @@ func (gc gougingChecker) Check(hs *rhpv2.HostSettings, pt *rhpv3.HostPriceTable)
}
}

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 {
Expand Down Expand Up @@ -192,6 +197,12 @@ func checkPriceGougingPT(gs api.GougingSettings, cs api.ConsensusState, txnFee t
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)
Expand All @@ -211,65 +222,6 @@ func checkPriceGougingPT(gs api.GougingSettings, cs api.ConsensusState, txnFee t
if pt.MaxCollateral.IsZero() {
return errors.New("MaxCollateral of host is 0")
}
// 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 LatestRevisionCost - expect sane value
maxRevisionCost, overflow := gs.MaxRPCPrice.AddWithOverflow(gs.MaxDownloadPrice.Div64(1 << 40).Mul64(2048))
Expand All @@ -280,16 +232,6 @@ func checkPriceGougingPT(gs api.GougingSettings, cs api.ConsensusState, txnFee t
return fmt.Errorf("LatestRevisionCost of %v exceeds maximum cost of %v", pt.LatestRevisionCost, maxRevisionCost)
}

// 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)
}

// 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.
Expand Down Expand Up @@ -430,6 +372,80 @@ func checkUploadGougingRHPv3(gs api.GougingSettings, pt *rhpv3.HostPriceTable) e
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,
Expand Down
10 changes: 9 additions & 1 deletion worker/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,19 @@ func (h *host) FundAccount(ctx context.Context, balance types.Currency, rev *typ
return h.acc.WithDeposit(ctx, func() (types.Currency, error) {
if err := h.transportPool.withTransportV3(ctx, h.hk, h.siamuxAddr, func(ctx context.Context, t *transportV3) error {
// fetch pricetable
pt, err := h.priceTable(ctx, rev)
pt, err := h.priceTables.fetch(ctx, h.hk, rev)
if err != nil {
return err
}

// check only the unused defaults
gc, err := GougingCheckerFromContext(ctx, false)
if err != nil {
return err
} else if err := gc.CheckUnusedDefaults(pt.HostPriceTable); err != nil {
return fmt.Errorf("%w: %v", errPriceTableGouging, err)
}

// check whether we have money left in the contract
cost := types.NewCurrency64(1)
if cost.Cmp(rev.ValidRenterPayout()) >= 0 {
Expand Down

0 comments on commit 9ecf0ef

Please sign in to comment.