Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for recording V2 scans and use V2 settings for gouging #1670

Draft
wants to merge 8 commits into
base: chris/v2-host-announcements
Choose a base branch
from
Draft
1 change: 1 addition & 0 deletions api/autopilot.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ type (
Pruning uint64 `json:"pruning"`
Upload uint64 `json:"upload"`
} `json:"gouging"`
LowMaxDuration uint64 `json:"lowMaxDuration"`
NotAcceptingContracts uint64 `json:"notAcceptingContracts"`
NotScanned uint64 `json:"notScanned"`
} `json:"unusable"`
Expand Down
20 changes: 10 additions & 10 deletions api/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -108,7 +107,8 @@ type (
PublicKey types.PublicKey `json:"publicKey"`
NetAddress string `json:"netAddress"`
PriceTable HostPriceTable `json:"priceTable"`
Settings rhpv2.HostSettings `json:"settings"`
Settings rhpv2.HostSettings `json:"settings,omitempty"`
V2Settings rhpv4.HostSettings `json:"v2Settings,omitempty"`
Interactions HostInteractions `json:"interactions"`
Scanned bool `json:"scanned"`
Blocked bool `json:"blocked"`
Expand Down Expand Up @@ -164,7 +164,6 @@ type (
}

HostGougingBreakdown struct {
ContractErr string `json:"contractErr"`
DownloadErr string `json:"downloadErr"`
GougingErr string `json:"gougingErr"`
PruneErr string `json:"pruneErr"`
Expand All @@ -184,6 +183,7 @@ type (
HostUsabilityBreakdown struct {
Blocked bool `json:"blocked"`
Offline bool `json:"offline"`
LowMaxDuration bool `json:"lowMaxDuration"`
LowScore bool `json:"lowScore"`
RedundantIP bool `json:"redundantIP"`
Gouging bool `json:"gouging"`
Expand Down Expand Up @@ -221,12 +221,14 @@ func (h Host) IsOnline() bool {
return h.Interactions.LastScanSuccess || h.Interactions.SecondToLastScanSuccess
}

func (h Host) IsV2() bool {
// consider a host to be v2 if it has announced a v2 address
return len(h.V2SiamuxAddresses) > 0
}

func (h Host) V2SiamuxAddr() string {
// NOTE: eventually this can be smarter about picking an address but right now
// we just prioritize IPv4 over IPv6
sort.Slice(h.V2SiamuxAddresses, func(i, j int) bool {
return len(h.V2SiamuxAddresses[i]) < len(h.V2SiamuxAddresses[j])
})
// NOTE: eventually we can improve this by implementing a dialer wrapper that
// can be created from a slice of addresses and tries them in order.
if len(h.V2SiamuxAddresses) > 0 {
return h.V2SiamuxAddresses[0]
}
Expand All @@ -239,7 +241,6 @@ func (sb HostScoreBreakdown) String() string {

func (hgb HostGougingBreakdown) Gouging() bool {
for _, err := range []string{
hgb.ContractErr,
hgb.DownloadErr,
hgb.GougingErr,
hgb.PruneErr,
Expand All @@ -255,7 +256,6 @@ func (hgb HostGougingBreakdown) Gouging() bool {
func (hgb HostGougingBreakdown) String() string {
var reasons []string
for _, errStr := range []string{
hgb.ContractErr,
hgb.DownloadErr,
hgb.GougingErr,
hgb.PruneErr,
Expand Down
2 changes: 1 addition & 1 deletion autopilot/contractor/contractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,7 @@ func performHostChecks(ctx *mCtx, bus Bus, logger *zap.SugaredLogger) error {
}
for _, h := range scoredHosts {
h.host.PriceTable.HostBlockHeight = cs.BlockHeight // ignore HostBlockHeight
hc := checkHost(ctx.GougingChecker(cs), h, minScore)
hc := checkHost(ctx.GougingChecker(cs), h, minScore, ctx.Period())
if err := bus.UpdateHostCheck(ctx, ctx.ApID(), h.host.PublicKey, *hc); err != nil {
return fmt.Errorf("failed to update host check for host %v: %w", h.host.PublicKey, err)
}
Expand Down
10 changes: 5 additions & 5 deletions autopilot/contractor/evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ var ErrMissingRequiredFields = errors.New("missing required fields in configurat
func countUsableHosts(cfg api.AutopilotConfig, cs api.ConsensusState, period uint64, rs api.RedundancySettings, gs api.GougingSettings, hosts []api.Host) (usables uint64) {
gc := gouging.NewChecker(gs, cs, &period, &cfg.Contracts.RenewWindow)
for _, host := range hosts {
hc := checkHost(gc, scoreHost(host, cfg, gs, rs.Redundancy()), minValidScore)
hc := checkHost(gc, scoreHost(host, cfg, gs, rs.Redundancy()), minValidScore, period)
if hc.UsabilityBreakdown.IsUsable() {
usables++
}
Expand All @@ -36,23 +36,23 @@ func EvaluateConfig(cfg api.AutopilotConfig, cs api.ConsensusState, rs api.Redun
resp.Hosts = uint64(len(hosts))
for i, host := range hosts {
hosts[i].PriceTable.HostBlockHeight = cs.BlockHeight // ignore block height
hc := checkHost(gc, scoreHost(host, cfg, gs, rs.Redundancy()), minValidScore)
hc := checkHost(gc, scoreHost(host, cfg, gs, rs.Redundancy()), minValidScore, cfg.Contracts.Period)
if hc.UsabilityBreakdown.IsUsable() {
resp.Usable++
continue
}
if hc.UsabilityBreakdown.Blocked {
resp.Unusable.Blocked++
}
if hc.UsabilityBreakdown.LowMaxDuration {
resp.Unusable.LowMaxDuration++
}
if hc.UsabilityBreakdown.NotAcceptingContracts {
resp.Unusable.NotAcceptingContracts++
}
if hc.UsabilityBreakdown.NotCompletingScan {
resp.Unusable.NotScanned++
}
if hc.GougingBreakdown.ContractErr != "" {
resp.Unusable.Gouging.Contract++
}
if hc.GougingBreakdown.DownloadErr != "" {
resp.Unusable.Gouging.Download++
}
Expand Down
21 changes: 19 additions & 2 deletions autopilot/contractor/hostfilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func isUpForRenewal(cfg api.AutopilotConfig, r api.Revision, blockHeight uint64)
}

// checkHost performs a series of checks on the host.
func checkHost(gc gouging.Checker, sh scoredHost, minScore float64) *api.HostCheck {
func checkHost(gc gouging.Checker, sh scoredHost, minScore float64, period uint64) *api.HostCheck {
h := sh.host

// prepare host breakdown fields
Expand Down Expand Up @@ -249,8 +249,25 @@ func checkHost(gc gouging.Checker, sh scoredHost, minScore float64) *api.HostChe
ub.NotAcceptingContracts = true
}

// max duration check
checkContractGouging := func(maxDuration uint64) {
if period > maxDuration {
ub.LowMaxDuration = true
}
}
if h.IsV2() {
checkContractGouging(h.V2Settings.MaxContractDuration)
} else {
checkContractGouging(h.Settings.MaxDuration)
checkContractGouging(h.PriceTable.MaxDuration)
}

// perform gouging and score checks
gb = gc.Check(&h.Settings, &h.PriceTable.HostPriceTable)
if h.IsV2() {
gb = gc.CheckV2(h.V2Settings)
} else {
gb = gc.CheckV1(&h.Settings, &h.PriceTable.HostPriceTable)
}
if gb.Gouging() {
ub.Gouging = true
} else if minScore > 0 && !(sh.score > minScore) {
Expand Down
1 change: 0 additions & 1 deletion bus/bus.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@ type (
HostBlocklist(ctx context.Context) ([]string, error)
Hosts(ctx context.Context, opts api.HostOptions) ([]api.Host, error)
RecordHostScans(ctx context.Context, scans []api.HostScan) error
RecordPriceTables(ctx context.Context, priceTableUpdate []api.HostPriceTableUpdate) error
RemoveOfflineHosts(ctx context.Context, maxConsecutiveScanFailures uint64, maxDowntime time.Duration) (uint64, error)
ResetLostSectors(ctx context.Context, hk types.PublicKey) error
UpdateHostAllowlistEntries(ctx context.Context, add, remove []types.PublicKey, clear bool) error
Expand Down
3 changes: 2 additions & 1 deletion bus/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,8 @@ func (b *Bus) hostsHandlerGET(jc jape.Context) {

var infos []api.HostInfo
for _, h := range hosts {
if !gc.Check(&h.HS, &h.PT).Gouging() {
if (len(h.V2SiamuxAddresses) > 0 && !gc.CheckV2(h.V2HS).Gouging()) ||
(len(h.V2SiamuxAddresses) == 0 && !gc.CheckV1(&h.HS, &h.PT).Gouging()) {
infos = append(infos, h.HostInfo)
}
}
Expand Down
112 changes: 58 additions & 54 deletions internal/gouging/gouging.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

rhpv2 "go.sia.tech/core/rhp/v2"
rhpv3 "go.sia.tech/core/rhp/v3"
rhpv4 "go.sia.tech/core/rhp/v4"
"go.sia.tech/core/types"
"go.sia.tech/renterd/api"
)
Expand Down Expand Up @@ -40,7 +41,8 @@ type (
}

Checker interface {
Check(_ *rhpv2.HostSettings, _ *rhpv3.HostPriceTable) api.HostGougingBreakdown
CheckV1(*rhpv2.HostSettings, *rhpv3.HostPriceTable) api.HostGougingBreakdown
CheckV2(rhpv4.HostSettings) api.HostGougingBreakdown
CheckSettings(rhpv2.HostSettings) api.HostGougingBreakdown
CheckUnusedDefaults(rhpv3.HostPriceTable) error
BlocksUntilBlockHeightGouging(hostHeight uint64) int64
Expand All @@ -49,9 +51,6 @@ type (
checker struct {
consensusState api.ConsensusState
settings api.GougingSettings

period *uint64
renewWindow *uint64
}
)

Expand All @@ -61,9 +60,6 @@ func NewChecker(gs api.GougingSettings, cs api.ConsensusState, period, renewWind
return checker{
consensusState: cs,
settings: gs,

period: period,
renewWindow: renewWindow,
}
}

Expand Down Expand Up @@ -93,16 +89,12 @@ func (gc checker) BlocksUntilBlockHeightGouging(hostHeight uint64) int64 {
return int64(hostHeight) - int64(minHeight)
}

func (gc checker) Check(hs *rhpv2.HostSettings, pt *rhpv3.HostPriceTable) api.HostGougingBreakdown {
func (gc checker) CheckV1(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, pt),
Expand All @@ -113,8 +105,61 @@ func (gc checker) Check(hs *rhpv2.HostSettings, pt *rhpv3.HostPriceTable) api.Ho
}
}

// TODO: write tests
func (gc checker) CheckV2(hs rhpv4.HostSettings) (gb api.HostGougingBreakdown) {
prices := hs.Prices
gs := gc.settings

// upload gouging
var uploadErrs []error
if prices.StoragePrice.Cmp(gs.MaxStoragePrice) > 0 {
uploadErrs = append(uploadErrs, fmt.Errorf("%v: storage price exceeds max storage price: %v > %v", ErrPriceTableGouging, prices.StoragePrice, gs.MaxStoragePrice))
}
if prices.IngressPrice.Cmp(gs.MaxUploadPrice) > 0 {
uploadErrs = append(uploadErrs, fmt.Errorf("%v: ingress price exceeds max upload price: %v > %v", ErrPriceTableGouging, prices.IngressPrice, gs.MaxUploadPrice))
}
gb.UploadErr = errsToStr(uploadErrs...)
if gougingErr := errsToStr(uploadErrs...); gougingErr != "" {
gb.UploadErr = fmt.Sprintf("%v: %s", ErrPriceTableGouging, gougingErr)
}

// download gouging
if prices.EgressPrice.Cmp(gs.MaxDownloadPrice) > 0 {
gb.DownloadErr = fmt.Sprintf("%v: egress price exceeds max download price: %v > %v", ErrPriceTableGouging, prices.EgressPrice, gs.MaxDownloadPrice)
}

// prune gouging
maxFreeSectorCost := types.Siacoins(1).Div64((1 << 40) / rhpv4.SectorSize) // 1 SC / TiB
if prices.FreeSectorPrice.Cmp(maxFreeSectorCost) > 0 {
gb.PruneErr = fmt.Sprintf("%v: cost to free a sector exceeds max free sector cost: %v > %v", ErrPriceTableGouging, prices.FreeSectorPrice, maxFreeSectorCost)
}

// general gouging
const sectorDuration = 144 * 3
const sectorBatchSize = 25600

var errs []error
if prices.ContractPrice.Cmp(gs.MaxContractPrice) > 0 {
errs = append(errs, fmt.Errorf("contract price exceeds max contract price: %v > %v", prices.ContractPrice, gs.MaxContractPrice))
}
if hs.MaxCollateral.IsZero() {
errs = append(errs, errors.New("max collateral is zero"))
}
if hs.MaxSectorDuration < sectorDuration {
errs = append(errs, fmt.Errorf("max sector duration is less than %v: %v", sectorDuration, hs.MaxSectorDuration))
}
if hs.MaxSectorBatchSize < sectorBatchSize {
errs = append(errs, fmt.Errorf("max sector batch size is less than %v: %v", sectorBatchSize, hs.MaxSectorBatchSize))
}
if gougingErr := errsToStr(errs...); gougingErr != "" {
gb.GougingErr = fmt.Sprintf("%v: %s", ErrPriceTableGouging, gougingErr)
}
gb.GougingErr = errsToStr(errs...)
return
}

func (gc checker) CheckSettings(hs rhpv2.HostSettings) api.HostGougingBreakdown {
return gc.Check(&hs, nil)
return gc.CheckV1(&hs, nil)
}

func (gc checker) CheckUnusedDefaults(pt rhpv3.HostPriceTable) error {
Expand Down Expand Up @@ -250,47 +295,6 @@ func checkPriceGougingPT(gs api.GougingSettings, cs api.ConsensusState, pt *rhpv
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
Expand Down
2 changes: 1 addition & 1 deletion internal/rhp/v3/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ func rpcRenew(ctx context.Context, t *transportV3, gc gouging.Checker, rev types
}

// Perform gouging checks.
if breakdown := gc.Check(nil, &pt); breakdown.Gouging() {
if breakdown := gc.CheckV1(nil, &pt); breakdown.Gouging() {
return rhpv2.ContractRevision{}, nil, types.Currency{}, types.Currency{}, fmt.Errorf("host gouging during renew: %v", breakdown)
}

Expand Down
10 changes: 5 additions & 5 deletions internal/test/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ var (
ContractSet = "testset"

GougingSettings = api.GougingSettings{
MaxRPCPrice: types.Siacoins(1).Div64(1000), // 1mS per RPC
MaxContractPrice: types.Siacoins(10), // 10 SC per contract
MaxDownloadPrice: types.Siacoins(1).Mul64(1000), // 1000 SC per 1 TiB
MaxUploadPrice: types.Siacoins(1).Mul64(1000), // 1000 SC per 1 TiB
MaxStoragePrice: types.Siacoins(1000).Div64(144 * 30), // 1000 SC per month
MaxRPCPrice: types.Siacoins(1).Div64(1000), // 1mS per RPC
MaxContractPrice: types.Siacoins(10), // 10 SC per contract
MaxDownloadPrice: types.Siacoins(1).Mul64(1000).Div64(1e12), // 1000 SC per 1 TB
MaxUploadPrice: types.Siacoins(1).Mul64(1000).Div64(1e12), // 1000 SC per 1 TB
MaxStoragePrice: types.Siacoins(1000).Div64(1e12).Div64(144 * 30), // 1000 SC per TB per month

HostBlockHeightLeeway: 240, // amount of leeway given to host block height

Expand Down
6 changes: 0 additions & 6 deletions stores/hostdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,6 @@ func (s *SQLStore) RecordHostScans(ctx context.Context, scans []api.HostScan) er
})
}

func (s *SQLStore) RecordPriceTables(ctx context.Context, priceTableUpdate []api.HostPriceTableUpdate) error {
return s.db.Transaction(ctx, func(tx sql.DatabaseTx) error {
return tx.RecordPriceTables(ctx, priceTableUpdate)
})
}

func (s *SQLStore) UsableHosts(ctx context.Context) (hosts []sql.HostInfo, err error) {
err = s.db.Transaction(ctx, func(tx sql.DatabaseTx) error {
hosts, err = tx.UsableHosts(ctx)
Expand Down
Loading
Loading