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 optional MinProtocolVersion setting to autopilot config #1143

Merged
merged 6 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/autopilot.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package api

import (
"errors"
"fmt"

"go.sia.tech/core/types"
"go.sia.tech/siad/build"
)

const (
Expand Down Expand Up @@ -55,6 +57,7 @@ type (
HostsConfig struct {
AllowRedundantIPs bool `json:"allowRedundantIPs"`
MaxDowntimeHours uint64 `json:"maxDowntimeHours"`
MinProtocolVersion string `json:"minProtocolVersion"`
MinRecentScanFailures uint64 `json:"minRecentScanFailures"`
ScoreOverrides map[types.PublicKey]float64 `json:"scoreOverrides"`
}
Expand Down Expand Up @@ -128,6 +131,8 @@ type (
func (c AutopilotConfig) Validate() error {
if c.Hosts.MaxDowntimeHours > 99*365*24 {
return ErrMaxDowntimeHoursTooHigh
} else if c.Hosts.MinProtocolVersion != "" && !build.IsVersion(c.Hosts.MinProtocolVersion) {
return fmt.Errorf("invalid min protocol version '%s'", c.Hosts.MinProtocolVersion)
}
return nil
}
2 changes: 1 addition & 1 deletion autopilot/autopilot.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func (ap *Autopilot) configHandlerPOST(jc jape.Context) {
}

// evaluate the config
jc.Encode(contractor.EvaluateConfig(reqCfg.Contracts, cs, fee, cfg.CurrentPeriod, rs, gs, hosts))
jc.Encode(contractor.EvaluateConfig(reqCfg, cs, fee, cfg.CurrentPeriod, rs, gs, hosts))
}

func (ap *Autopilot) Run() error {
Expand Down
10 changes: 5 additions & 5 deletions autopilot/contractor/contractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ func (c *Contractor) performContractMaintenance(ctx *mCtx, w Worker) (bool, erro
}

// fetch candidate hosts
candidates, unusableHosts, err := c.candidateHosts(mCtx, hosts, usedHosts, hostData, smallestValidScore) // avoid 0 score hosts
candidates, unusableHosts, err := c.candidateHosts(mCtx, hosts, usedHosts, hostData, minValidScore) // avoid 0 score hosts
if err != nil {
return false, err
}
Expand Down Expand Up @@ -756,7 +756,7 @@ func (c *Contractor) runHostChecks(ctx *mCtx, hosts []api.Host, hostData map[typ
checks := make(map[types.PublicKey]*api.HostCheck)
for _, h := range hosts {
h.PriceTable.HostBlockHeight = cs.BlockHeight // ignore HostBlockHeight
checks[h.PublicKey] = checkHost(ctx.ContractsConfig(), ctx.state.RS, gc, h, minScore, hostData[h.PublicKey])
checks[h.PublicKey] = checkHost(ctx.AutopilotConfig(), ctx.state.RS, gc, h, minScore, hostData[h.PublicKey])
}
return checks, nil
}
Expand Down Expand Up @@ -1173,7 +1173,7 @@ func (c *Contractor) calculateMinScore(candidates []scoredHost, numContracts uin
// return early if there's no hosts
if len(candidates) == 0 {
c.logger.Warn("min host score is set to the smallest non-zero float because there are no candidate hosts")
return smallestValidScore
return minValidScore
}

// determine the number of random hosts we fetch per iteration when
Expand Down Expand Up @@ -1207,7 +1207,7 @@ func (c *Contractor) calculateMinScore(candidates []scoredHost, numContracts uin
return candidates[i].score > candidates[j].score
})
if len(candidates) < int(numContracts) {
return smallestValidScore
return minValidScore
} else if cutoff := candidates[numContracts-1].score; minScore > cutoff {
minScore = cutoff
}
Expand Down Expand Up @@ -1273,7 +1273,7 @@ func (c *Contractor) candidateHosts(ctx *mCtx, hosts []api.Host, usedHosts map[t
// NOTE: ignore the pricetable's HostBlockHeight by setting it to our
// own blockheight
h.PriceTable.HostBlockHeight = cs.BlockHeight
hc := checkHost(ctx.ContractsConfig(), ctx.state.RS, gc, h, minScore, storedData[h.PublicKey])
hc := checkHost(ctx.AutopilotConfig(), ctx.state.RS, gc, h, minScore, storedData[h.PublicKey])
if hc.Usability.IsUsable() {
candidates = append(candidates, scoredHost{h, hc.Score.Score()})
continue
Expand Down
18 changes: 9 additions & 9 deletions autopilot/contractor/evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import (
"go.sia.tech/renterd/worker"
)

func countUsableHosts(cfg api.ContractsConfig, cs api.ConsensusState, fee types.Currency, currentPeriod uint64, rs api.RedundancySettings, gs api.GougingSettings, hosts []api.Host) (usables uint64) {
gc := worker.NewGougingChecker(gs, cs, fee, currentPeriod, cfg.RenewWindow)
func countUsableHosts(cfg api.AutopilotConfig, cs api.ConsensusState, fee types.Currency, currentPeriod uint64, rs api.RedundancySettings, gs api.GougingSettings, hosts []api.Host) (usables uint64) {
gc := worker.NewGougingChecker(gs, cs, fee, currentPeriod, cfg.Contracts.RenewWindow)
for _, host := range hosts {
hc := checkHost(cfg, rs, gc, host, smallestValidScore, 0)
hc := checkHost(cfg, rs, gc, host, minValidScore, 0)
if hc.Usability.IsUsable() {
usables++
}
Expand All @@ -20,8 +20,8 @@ func countUsableHosts(cfg api.ContractsConfig, cs api.ConsensusState, fee types.
// EvaluateConfig evaluates the given configuration and if the gouging settings
// are too strict for the number of contracts required by 'cfg', it will provide
// a recommendation on how to loosen it.
func EvaluateConfig(cfg api.ContractsConfig, cs api.ConsensusState, fee types.Currency, currentPeriod uint64, rs api.RedundancySettings, gs api.GougingSettings, hosts []api.Host) (resp api.ConfigEvaluationResponse) {
gc := worker.NewGougingChecker(gs, cs, fee, currentPeriod, cfg.RenewWindow)
func EvaluateConfig(cfg api.AutopilotConfig, cs api.ConsensusState, fee types.Currency, currentPeriod uint64, rs api.RedundancySettings, gs api.GougingSettings, hosts []api.Host) (resp api.ConfigEvaluationResponse) {
gc := worker.NewGougingChecker(gs, cs, fee, currentPeriod, cfg.Contracts.RenewWindow)

resp.Hosts = uint64(len(hosts))
for _, host := range hosts {
Expand Down Expand Up @@ -56,7 +56,7 @@ func EvaluateConfig(cfg api.ContractsConfig, cs api.ConsensusState, fee types.Cu
}
}

if resp.Usable >= cfg.Amount {
if resp.Usable >= cfg.Contracts.Amount {
return // no recommendation needed
}

Expand Down Expand Up @@ -132,8 +132,8 @@ func EvaluateConfig(cfg api.ContractsConfig, cs api.ConsensusState, fee types.Cu

// optimiseGougingSetting tries to optimise one field of the gouging settings to
// try and hit the target number of contracts.
func optimiseGougingSetting(gs *api.GougingSettings, field *types.Currency, cfg api.ContractsConfig, cs api.ConsensusState, fee types.Currency, currentPeriod uint64, rs api.RedundancySettings, hosts []api.Host) bool {
if cfg.Amount == 0 {
func optimiseGougingSetting(gs *api.GougingSettings, field *types.Currency, cfg api.AutopilotConfig, cs api.ConsensusState, fee types.Currency, currentPeriod uint64, rs api.RedundancySettings, hosts []api.Host) bool {
if cfg.Contracts.Amount == 0 {
return true // nothing to do
}
stepSize := []uint64{200, 150, 125, 110, 105}
Expand All @@ -144,7 +144,7 @@ func optimiseGougingSetting(gs *api.GougingSettings, field *types.Currency, cfg
prevVal := *field // to keep accurate value
for {
nUsable := countUsableHosts(cfg, cs, fee, currentPeriod, rs, *gs, hosts)
targetHit := nUsable >= cfg.Amount
targetHit := nUsable >= cfg.Contracts.Amount

if targetHit && nSteps == 0 {
return true // target already hit without optimising
Expand Down
8 changes: 5 additions & 3 deletions autopilot/contractor/evaluate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ func TestOptimiseGougingSetting(t *testing.T) {
}

// prepare settings that result in all hosts being usable
cfg := api.ContractsConfig{
Allowance: types.Siacoins(100000),
Amount: 10,
cfg := api.AutopilotConfig{
Contracts: api.ContractsConfig{
Allowance: types.Siacoins(100000),
Amount: 10,
},
}
cs := api.ConsensusState{
BlockHeight: 100,
Expand Down
2 changes: 1 addition & 1 deletion autopilot/contractor/hostfilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ func isUpForRenewal(cfg api.AutopilotConfig, r types.FileContractRevision, block
}

// checkHost performs a series of checks on the host.
func checkHost(cfg api.ContractsConfig, rs api.RedundancySettings, gc worker.GougingChecker, h api.Host, minScore float64, storedData uint64) *api.HostCheck {
func checkHost(cfg api.AutopilotConfig, rs api.RedundancySettings, gc worker.GougingChecker, h api.Host, minScore float64, storedData uint64) *api.HostCheck {
if rs.Validate() != nil {
panic("invalid redundancy settings were supplied - developer error")
}
Expand Down
38 changes: 28 additions & 10 deletions autopilot/contractor/hostscore.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,22 @@ import (
"go.sia.tech/siad/build"
)

const smallestValidScore = math.SmallestNonzeroFloat64
const (
// MinProtocolVersion is the minimum protocol version of a host that we
// accept.
minProtocolVersion = "1.5.9"

// minValidScore is the smallest score that a host can have before
// being ignored.
minValidScore = math.SmallestNonzeroFloat64
)

func hostScore(cfg api.ContractsConfig, h api.Host, storedData uint64, expectedRedundancy float64) api.HostScoreBreakdown {
func hostScore(cfg api.AutopilotConfig, h api.Host, storedData uint64, expectedRedundancy float64) api.HostScoreBreakdown {
cCfg := cfg.Contracts
// idealDataPerHost is the amount of data that we would have to put on each
// host assuming that our storage requirements were spread evenly across
// every single host.
idealDataPerHost := float64(cfg.Storage) * expectedRedundancy / float64(cfg.Amount)
idealDataPerHost := float64(cCfg.Storage) * expectedRedundancy / float64(cCfg.Amount)
// allocationPerHost is the amount of data that we would like to be able to
// put on each host, because data is not always spread evenly across the
// hosts during upload. Slower hosts may get very little data, more
Expand All @@ -29,15 +38,15 @@ func hostScore(cfg api.ContractsConfig, h api.Host, storedData uint64, expectedR
// data will store twice the expectation
allocationPerHost := idealDataPerHost * 2
// hostPeriodCost is the amount of money we expect to spend on a host in a period.
hostPeriodCost := hostPeriodCostForScore(h, cfg, expectedRedundancy)
hostPeriodCost := hostPeriodCostForScore(h, cCfg, expectedRedundancy)
return api.HostScoreBreakdown{
Age: ageScore(h),
Collateral: collateralScore(cfg, h.PriceTable.HostPriceTable, uint64(allocationPerHost)),
Collateral: collateralScore(cCfg, h.PriceTable.HostPriceTable, uint64(allocationPerHost)),
Interactions: interactionScore(h),
Prices: priceAdjustmentScore(hostPeriodCost, cfg),
Prices: priceAdjustmentScore(hostPeriodCost, cCfg),
StorageRemaining: storageRemainingScore(h.Settings, storedData, allocationPerHost),
Uptime: uptimeScore(h),
Version: versionScore(h.Settings),
Version: versionScore(h.Settings, cfg.Hosts.MinProtocolVersion),
}
}

Expand Down Expand Up @@ -237,13 +246,22 @@ func uptimeScore(h api.Host) float64 {
return math.Pow(ratio, 200*math.Min(1-ratio, 0.30))
}

func versionScore(settings rhpv2.HostSettings) float64 {
func versionScore(settings rhpv2.HostSettings, minVersion string) float64 {
if minVersion == "" {
minVersion = minProtocolVersion
}
versions := []struct {
version string
penalty float64
}{
{"1.6.0", 0.99},
{"1.5.9", 0.00},
// latest protocol version
{"1.6.0", 0.10},

// user-defined minimum
{minVersion, 0.00},

// absolute minimum
{minProtocolVersion, 0.00},
}
weight := 1.0
for _, v := range versions {
Expand Down
1 change: 0 additions & 1 deletion autopilot/contractor/hostscore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ func TestHostScore(t *testing.T) {
}
h1 := newHost(test.NewHostSettings())
h2 := newHost(test.NewHostSettings())
cfg := cfg.Contracts

// assert both hosts score equal
redundancy := 3.0
Expand Down
30 changes: 30 additions & 0 deletions internal/test/e2e/gouging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,33 @@ func TestGouging(t *testing.T) {
return err
})
}

func TestHostMinVersion(t *testing.T) {
if testing.Short() {
t.SkipNow()
}

// create a new test cluster
cluster := newTestCluster(t, testClusterOptions{
hosts: int(test.AutopilotConfig.Contracts.Amount),
})
defer cluster.Shutdown()
tt := cluster.tt

// set min version to a high value
cfg := test.AutopilotConfig
cfg.Hosts.MinProtocolVersion = "99.99.99"
cluster.UpdateAutopilotConfig(context.Background(), cfg)

// contracts in set should drop to 0
ChrisSchinnerl marked this conversation as resolved.
Show resolved Hide resolved
tt.Retry(100, 100*time.Millisecond, func() error {
contracts, err := cluster.Bus.Contracts(context.Background(), api.ContractsOpts{
ContractSet: test.AutopilotConfig.Contracts.Set,
})
tt.OK(err)
if len(contracts) != 0 {
return fmt.Errorf("expected 0 contracts, got %v", len(contracts))
}
return nil
})
}
Loading