diff --git a/config/config.go b/config/config.go index 4c98b6a47fda..6cb209827d5d 100644 --- a/config/config.go +++ b/config/config.go @@ -90,8 +90,19 @@ var ( errFileDoesNotExist = errors.New("file does not exist") ) -func getConsensusConfig(v *viper.Viper) snowball.Parameters { +func getConsensusConfig(v *viper.Viper) (snowball.Parameters, error) { + var terminationCriteria []snowball.TerminationCriteria + + tcText := v.GetString(SnowTerminationCriteriaJSONKey) + if v.IsSet(SnowTerminationCriteriaJSONKey) && tcText != "" { + if err := json.Unmarshal([]byte(tcText), &terminationCriteria); err != nil { + return snowball.Parameters{}, fmt.Errorf("%w: failed parsing %s: %s is not a valid JSON array of the form "+ + "{\"consecutiveSuccesses\":x,\"voteThreshold\":y}", snowball.ErrParametersInvalid, SnowTerminationCriteriaJSONKey, tcText) + } + } + p := snowball.Parameters{ + TerminationCriteria: terminationCriteria, K: v.GetInt(SnowSampleSizeKey), AlphaPreference: v.GetInt(SnowPreferenceQuorumSizeKey), AlphaConfidence: v.GetInt(SnowConfidenceQuorumSizeKey), @@ -105,7 +116,7 @@ func getConsensusConfig(v *viper.Viper) snowball.Parameters { p.AlphaPreference = v.GetInt(SnowQuorumSizeKey) p.AlphaConfidence = p.AlphaPreference } - return p + return p, nil } func getLoggingConfig(v *viper.Viper) (logging.Config, error) { @@ -1063,7 +1074,10 @@ func getSubnetConfigsFromFlags(v *viper.Viper, subnetIDs []ids.ID) (map[ids.ID]s res := make(map[ids.ID]subnets.Config) for _, subnetID := range subnetIDs { if rawSubnetConfigBytes, ok := subnetConfigs[subnetID]; ok { - config := getDefaultSubnetConfig(v) + config, err := getDefaultSubnetConfig(v) + if err != nil { + return nil, err + } if err := json.Unmarshal(rawSubnetConfigBytes, &config); err != nil { return nil, err } @@ -1116,7 +1130,10 @@ func getSubnetConfigsFromDir(v *viper.Viper, subnetIDs []ids.ID) (map[ids.ID]sub return nil, err } - config := getDefaultSubnetConfig(v) + config, err := getDefaultSubnetConfig(v) + if err != nil { + return nil, err + } if err := json.Unmarshal(file, &config); err != nil { return nil, fmt.Errorf("%w: %w", errUnmarshalling, err) } @@ -1136,13 +1153,17 @@ func getSubnetConfigsFromDir(v *viper.Viper, subnetIDs []ids.ID) (map[ids.ID]sub return subnetConfigs, nil } -func getDefaultSubnetConfig(v *viper.Viper) subnets.Config { +func getDefaultSubnetConfig(v *viper.Viper) (subnets.Config, error) { + cc, err := getConsensusConfig(v) + if err != nil { + return subnets.Config{}, err + } return subnets.Config{ - ConsensusParameters: getConsensusConfig(v), + ConsensusParameters: cc, ValidatorOnly: false, ProposerMinBlockDelay: proposervm.DefaultMinBlockDelay, ProposerNumHistoricalBlocks: proposervm.DefaultNumHistoricalBlocks, - } + }, nil } func getCPUTargeterConfig(v *viper.Viper) (tracker.TargeterConfig, error) { @@ -1373,7 +1394,10 @@ func GetNodeConfig(v *viper.Viper) (node.Config, error) { return node.Config{}, fmt.Errorf("couldn't read subnet configs: %w", err) } - primaryNetworkConfig := getDefaultSubnetConfig(v) + primaryNetworkConfig, err := getDefaultSubnetConfig(v) + if err != nil { + return node.Config{}, err + } if err := primaryNetworkConfig.Valid(); err != nil { return node.Config{}, fmt.Errorf("invalid consensus parameters: %w", err) } diff --git a/config/config_test.go b/config/config_test.go index 68847ca4f6d5..fcd97897160f 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -446,6 +446,32 @@ func TestGetSubnetConfigsFromFile(t *testing.T) { } } +func TestGetSubnetConfigsFromFlagsInvalidJSON(t *testing.T) { + subnetID, err := ids.FromString("2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i") + require.NoError(t, err) + + require := require.New(t) + + encodedFileContent := base64.StdEncoding.EncodeToString([]byte(`{ + "2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i": { + "consensusParameters": { + "terminationCriteria":{"consecutiveSuccesses":5,"voteThreshold":10}, + "k": 30, + "alphaPreference": 16, + "alphaConfidence": 15 + }, + "validatorOnly": true + } + }`)) + + // build viper config + v := setupViperFlags() + v.Set(SubnetConfigContentKey, encodedFileContent) + + _, err = getSubnetConfigs(v, []ids.ID{subnetID}) + require.Errorf(err, "json: cannot unmarshal object into Go struct field Parameters.consensusParameters.terminationCriteria of type []snowball.TerminationCriteria") +} + func TestGetSubnetConfigsFromFlags(t *testing.T) { subnetID, err := ids.FromString("2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i") require.NoError(t, err) @@ -519,6 +545,58 @@ func TestGetSubnetConfigsFromFlags(t *testing.T) { }, expectedErr: nil, }, + "correct config with termination criteria": { + givenJSON: `{ + "2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i": { + "consensusParameters": { + "terminationCriteria":[{"consecutiveSuccesses":5,"voteThreshold":10},{"consecutiveSuccesses":4,"voteThreshold":11}], + "k": 30, + "alphaPreference": 16, + "alphaConfidence": 0 + }, + "validatorOnly": true + } + }`, + testF: func(require *require.Assertions, given map[ids.ID]subnets.Config) { + id, _ := ids.FromString("2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i") + config, ok := given[id] + require.True(ok) + require.True(config.ValidatorOnly) + require.Equal([]snowball.TerminationCriteria{{ConsecutiveSuccesses: 5, VoteThreshold: 10}, {ConsecutiveSuccesses: 4, VoteThreshold: 11}}, + config.ConsensusParameters.TerminationCriteria) + require.Equal(16, config.ConsensusParameters.AlphaPreference) + require.Equal(30, config.ConsensusParameters.K) + // must still respect defaults + require.Equal(256, config.ConsensusParameters.MaxOutstandingItems) + }, + expectedErr: nil, + }, + "conflict between alpha confidence and termination criteria": { + givenJSON: `{ + "2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i": { + "consensusParameters": { + "terminationCriteria":[{"consecutiveSuccesses":5,"voteThreshold":10},{"consecutiveSuccesses":4,"voteThreshold":11}], + "k": 30, + "alphaPreference": 16, + "alphaConfidence": 15 + }, + "validatorOnly": true + } + }`, + testF: func(require *require.Assertions, given map[ids.ID]subnets.Config) { + id, _ := ids.FromString("2Ctt6eGAeo4MLqTmGa7AdRecuVMPGWEX9wSsCLBYrLhX4a394i") + config, ok := given[id] + require.True(ok) + require.True(config.ValidatorOnly) + require.Equal([]snowball.TerminationCriteria{{ConsecutiveSuccesses: 5, VoteThreshold: 10}, {ConsecutiveSuccesses: 4, VoteThreshold: 11}}, + config.ConsensusParameters.TerminationCriteria) + require.Equal(16, config.ConsensusParameters.AlphaPreference) + require.Equal(30, config.ConsensusParameters.K) + // must still respect defaults + require.Equal(256, config.ConsensusParameters.MaxOutstandingItems) + }, + expectedErr: snowball.ErrParametersInvalid, + }, } for name, test := range tests { diff --git a/config/flags.go b/config/flags.go index 6351161e69a2..f360da81f717 100644 --- a/config/flags.go +++ b/config/flags.go @@ -304,6 +304,7 @@ func addNodeFlags(fs *pflag.FlagSet) { fs.Uint(BootstrapAncestorsMaxContainersReceivedKey, 2000, "This node reads at most this many containers from an incoming Ancestors message") // Consensus + fs.String(SnowTerminationCriteriaJSONKey, "", "Termination criteria for poll") fs.Int(SnowSampleSizeKey, snowball.DefaultParameters.K, "Number of nodes to query for each network poll") fs.Int(SnowQuorumSizeKey, snowball.DefaultParameters.AlphaConfidence, "Threshold of nodes required to update this node's preference and increase its confidence in a network poll") fs.Int(SnowPreferenceQuorumSizeKey, snowball.DefaultParameters.AlphaPreference, fmt.Sprintf("Threshold of nodes required to update this node's preference in a network poll. Ignored if %s is provided", SnowQuorumSizeKey)) diff --git a/config/keys.go b/config/keys.go index 714136a74073..05d7f67aaf40 100644 --- a/config/keys.go +++ b/config/keys.go @@ -135,6 +135,7 @@ const ( LogRotaterMaxAgeKey = "log-rotater-max-age" LogRotaterCompressEnabledKey = "log-rotater-compress-enabled" LogDisableDisplayPluginLogsKey = "log-disable-display-plugin-logs" + SnowTerminationCriteriaJSONKey = "snow-termination-criteria" SnowSampleSizeKey = "snow-sample-size" SnowQuorumSizeKey = "snow-quorum-size" SnowPreferenceQuorumSizeKey = "snow-preference-quorum-size" diff --git a/snow/consensus/snowball/binary_snowball_test.go b/snow/consensus/snowball/binary_snowball_test.go index 968743ef36a8..155052d96d47 100644 --- a/snow/consensus/snowball/binary_snowball_test.go +++ b/snow/consensus/snowball/binary_snowball_test.go @@ -17,7 +17,7 @@ func TestBinarySnowball(t *testing.T) { alphaPreference, alphaConfidence := 2, 3 beta := 2 - terminationConditions := newSingleTerminationCondition(alphaConfidence, beta) + terminationConditions := newTerminationCondition([]TerminationCriteria{{VoteThreshold: alphaConfidence, ConsecutiveSuccesses: beta}}) sb := newBinarySnowball(alphaPreference, terminationConditions, red) require.Equal(red, sb.Preference()) @@ -48,7 +48,7 @@ func TestBinarySnowballRecordPollPreference(t *testing.T) { alphaPreference, alphaConfidence := 1, 2 beta := 2 - terminationConditions := newSingleTerminationCondition(alphaConfidence, beta) + terminationConditions := newTerminationCondition([]TerminationCriteria{{VoteThreshold: alphaConfidence, ConsecutiveSuccesses: beta}}) sb := newBinarySnowball(alphaPreference, terminationConditions, red) require.Equal(red, sb.Preference()) @@ -86,7 +86,7 @@ func TestBinarySnowballRecordUnsuccessfulPoll(t *testing.T) { alphaPreference, alphaConfidence := 1, 2 beta := 2 - terminationConditions := newSingleTerminationCondition(alphaConfidence, beta) + terminationConditions := newTerminationCondition([]TerminationCriteria{{VoteThreshold: alphaConfidence, ConsecutiveSuccesses: beta}}) sb := newBinarySnowball(alphaPreference, terminationConditions, red) require.Equal(red, sb.Preference()) @@ -118,7 +118,7 @@ func TestBinarySnowballAcceptWeirdColor(t *testing.T) { alphaPreference, alphaConfidence := 1, 2 beta := 2 - terminationConditions := newSingleTerminationCondition(alphaConfidence, beta) + terminationConditions := newTerminationCondition([]TerminationCriteria{{VoteThreshold: alphaConfidence, ConsecutiveSuccesses: beta}}) sb := newBinarySnowball(alphaPreference, terminationConditions, red) @@ -160,7 +160,7 @@ func TestBinarySnowballLockColor(t *testing.T) { alphaPreference, alphaConfidence := 1, 2 beta := 1 - terminationConditions := newSingleTerminationCondition(alphaConfidence, beta) + terminationConditions := newTerminationCondition([]TerminationCriteria{{VoteThreshold: alphaConfidence, ConsecutiveSuccesses: beta}}) sb := newBinarySnowball(alphaPreference, terminationConditions, red) diff --git a/snow/consensus/snowball/binary_snowflake_test.go b/snow/consensus/snowball/binary_snowflake_test.go index ca2347aa086d..d916c34d444c 100644 --- a/snow/consensus/snowball/binary_snowflake_test.go +++ b/snow/consensus/snowball/binary_snowflake_test.go @@ -17,7 +17,7 @@ func TestBinarySnowflake(t *testing.T) { alphaPreference, alphaConfidence := 1, 2 beta := 2 - terminationConditions := newSingleTerminationCondition(alphaConfidence, beta) + terminationConditions := newTerminationCondition([]TerminationCriteria{{VoteThreshold: alphaConfidence, ConsecutiveSuccesses: beta}}) sf := newBinarySnowflake(alphaPreference, terminationConditions, red) diff --git a/snow/consensus/snowball/factory.go b/snow/consensus/snowball/factory.go index e9ae98180e6e..c02444520342 100644 --- a/snow/consensus/snowball/factory.go +++ b/snow/consensus/snowball/factory.go @@ -13,23 +13,23 @@ var ( type snowballFactory struct{} func (snowballFactory) NewNnary(params Parameters, choice ids.ID) Nnary { - sb := newNnarySnowball(params.AlphaPreference, newSingleTerminationCondition(params.AlphaConfidence, params.Beta), choice) + sb := newNnarySnowball(params.AlphaPreference, params.terminationConditions(), choice) return &sb } func (snowballFactory) NewUnary(params Parameters) Unary { - sb := newUnarySnowball(params.AlphaPreference, newSingleTerminationCondition(params.AlphaConfidence, params.Beta)) + sb := newUnarySnowball(params.AlphaPreference, params.terminationConditions()) return &sb } type snowflakeFactory struct{} func (snowflakeFactory) NewNnary(params Parameters, choice ids.ID) Nnary { - sf := newNnarySnowflake(params.AlphaPreference, newSingleTerminationCondition(params.AlphaConfidence, params.Beta), choice) + sf := newNnarySnowflake(params.AlphaPreference, params.terminationConditions(), choice) return &sf } func (snowflakeFactory) NewUnary(params Parameters) Unary { - sf := newUnarySnowflake(params.AlphaPreference, newSingleTerminationCondition(params.AlphaConfidence, params.Beta)) + sf := newUnarySnowflake(params.AlphaPreference, params.terminationConditions()) return &sf } diff --git a/snow/consensus/snowball/nnary_snowball_test.go b/snow/consensus/snowball/nnary_snowball_test.go index 8a5e66143db2..e6644327a2dc 100644 --- a/snow/consensus/snowball/nnary_snowball_test.go +++ b/snow/consensus/snowball/nnary_snowball_test.go @@ -14,7 +14,7 @@ func TestNnarySnowball(t *testing.T) { alphaPreference, alphaConfidence := 1, 2 beta := 2 - terminationConditions := newSingleTerminationCondition(alphaConfidence, beta) + terminationConditions := newTerminationCondition([]TerminationCriteria{{VoteThreshold: alphaConfidence, ConsecutiveSuccesses: beta}}) sb := newNnarySnowball(alphaPreference, terminationConditions, Red) sb.Add(Blue) @@ -57,7 +57,7 @@ func TestVirtuousNnarySnowball(t *testing.T) { alphaPreference, alphaConfidence := 1, 2 beta := 1 - terminationConditions := newSingleTerminationCondition(alphaConfidence, beta) + terminationConditions := newTerminationCondition([]TerminationCriteria{{VoteThreshold: alphaConfidence, ConsecutiveSuccesses: beta}}) sb := newNnarySnowball(alphaPreference, terminationConditions, Red) @@ -74,7 +74,7 @@ func TestNarySnowballRecordUnsuccessfulPoll(t *testing.T) { alphaPreference, alphaConfidence := 1, 2 beta := 2 - terminationConditions := newSingleTerminationCondition(alphaConfidence, beta) + terminationConditions := newTerminationCondition([]TerminationCriteria{{VoteThreshold: alphaConfidence, ConsecutiveSuccesses: beta}}) sb := newNnarySnowball(alphaPreference, terminationConditions, Red) sb.Add(Blue) @@ -114,7 +114,7 @@ func TestNarySnowballDifferentSnowflakeColor(t *testing.T) { alphaPreference, alphaConfidence := 1, 2 beta := 2 - terminationConditions := newSingleTerminationCondition(alphaConfidence, beta) + terminationConditions := newTerminationCondition([]TerminationCriteria{{VoteThreshold: alphaConfidence, ConsecutiveSuccesses: beta}}) sb := newNnarySnowball(alphaPreference, terminationConditions, Red) sb.Add(Blue) diff --git a/snow/consensus/snowball/nnary_snowflake_test.go b/snow/consensus/snowball/nnary_snowflake_test.go index ad090ee0f0df..9777fe3a2014 100644 --- a/snow/consensus/snowball/nnary_snowflake_test.go +++ b/snow/consensus/snowball/nnary_snowflake_test.go @@ -16,7 +16,8 @@ func TestNnarySnowflake(t *testing.T) { alphaPreference, alphaConfidence := 1, 2 beta := 2 - terminationConditions := newSingleTerminationCondition(alphaConfidence, beta) + + terminationConditions := newTerminationCondition([]TerminationCriteria{{VoteThreshold: alphaConfidence, ConsecutiveSuccesses: beta}}) sf := newNnarySnowflake(alphaPreference, terminationConditions, Red) sf.Add(Blue) @@ -55,7 +56,7 @@ func TestNnarySnowflakeConfidenceReset(t *testing.T) { alphaPreference, alphaConfidence := 1, 2 beta := 4 - terminationConditions := newSingleTerminationCondition(alphaConfidence, beta) + terminationConditions := newTerminationCondition([]TerminationCriteria{{VoteThreshold: alphaConfidence, ConsecutiveSuccesses: beta}}) sf := newNnarySnowflake(alphaPreference, terminationConditions, Red) sf.Add(Blue) @@ -89,7 +90,7 @@ func TestVirtuousNnarySnowflake(t *testing.T) { alphaPreference, alphaConfidence := 1, 2 beta := 2 - terminationConditions := newSingleTerminationCondition(alphaConfidence, beta) + terminationConditions := newTerminationCondition([]TerminationCriteria{{VoteThreshold: alphaConfidence, ConsecutiveSuccesses: beta}}) sb := newNnarySnowflake(alphaPreference, terminationConditions, Red) require.Equal(Red, sb.Preference()) diff --git a/snow/consensus/snowball/parameters.go b/snow/consensus/snowball/parameters.go index 3e2ba03f2081..97987f5e5d76 100644 --- a/snow/consensus/snowball/parameters.go +++ b/snow/consensus/snowball/parameters.go @@ -63,6 +63,9 @@ type Parameters struct { // Beta is the number of consecutive successful queries required for // finalization. Beta int `json:"beta" yaml:"beta"` + // TerminationCriteria is the criteria that if any of them is satisfied, the protocol is finalized. + // The VoteThresholds should be arranged in ascending order. + TerminationCriteria []TerminationCriteria `json:"terminationCriteria" yaml:"terminationCriteria"` // ConcurrentRepolls is the number of outstanding polls the engine will // target to have while there is something processing. ConcurrentRepolls int `json:"concurrentRepolls" yaml:"concurrentRepolls"` @@ -78,23 +81,37 @@ type Parameters struct { MaxItemProcessingTime time.Duration `json:"maxItemProcessingTime" yaml:"maxItemProcessingTime"` } +// TerminationCriteria is a condition that is satisfied, the protocol is finalized. +type TerminationCriteria struct { + // ConsecutiveSuccesses is the number of consecutive successful queries with a given vote threshold, + // also known as β. + ConsecutiveSuccesses int `json:"consecutiveSuccesses" yaml:"consecutiveSuccesses"` + // VoteThreshold is the threshold to increase the confidence, also known as α. + VoteThreshold int `json:"voteThreshold" yaml:"voteThreshold"` +} + // Verify returns nil if the parameters describe a valid initialization. // // An initialization is valid if the following conditions are met: // -// - K/2 < AlphaPreference <= AlphaConfidence <= K -// - 0 < ConcurrentRepolls <= Beta -// - 0 < OptimalProcessing -// - 0 < MaxOutstandingItems -// - 0 < MaxItemProcessingTime -// -// Note: K/2 < K implies that 0 <= K/2, so we don't need an explicit check that -// AlphaPreference is positive. +// - 0 <= K/2 < AlphaPreference <= AlphaConfidence <= K +// - 0 < ConcurrentRepolls <= Beta +// - 0 < OptimalProcessing +// - 0 < MaxOutstandingItems +// - 0 < MaxItemProcessingTime +// - ∀i⋹{0,1,...,|TerminationCriteria|-2}: +// TerminationCriteria[i].VoteThreshold < TerminationCriteria[i+1].VoteThreshold +// TerminationCriteria[i].ConsecutiveSuccesses >= TerminationCriteria[i+1].ConsecutiveSuccesses +// - If |TerminationCriteria| > 0 then AlphaConfidence == 0 func (p Parameters) Verify() error { + if err := p.verifyTerminationCriteria(); err != nil { + return err + } + switch { case p.AlphaPreference <= p.K/2: return fmt.Errorf("%w: k = %d, alphaPreference = %d: fails the condition that: k/2 < alphaPreference", ErrParametersInvalid, p.K, p.AlphaPreference) - case p.AlphaConfidence < p.AlphaPreference: + case p.AlphaConfidence < p.AlphaPreference && len(p.TerminationCriteria) == 0: return fmt.Errorf("%w: alphaPreference = %d, alphaConfidence = %d: fails the condition that: alphaPreference <= alphaConfidence", ErrParametersInvalid, p.AlphaPreference, p.AlphaConfidence) case p.K < p.AlphaConfidence: return fmt.Errorf("%w: k = %d, alphaConfidence = %d: fails the condition that: alphaConfidence <= k", ErrParametersInvalid, p.K, p.AlphaConfidence) @@ -115,6 +132,28 @@ func (p Parameters) Verify() error { } } +func (p Parameters) verifyTerminationCriteria() error { + if len(p.TerminationCriteria) > 0 && p.AlphaConfidence != 0 { + return fmt.Errorf("%w: termination criteria cannot be configured together with alpha confidence (%d), "+ + "they are mutually exclusive", ErrParametersInvalid, p.AlphaConfidence) + } + for i := range p.TerminationCriteria { + if i+1 == len(p.TerminationCriteria) { + continue + } + if p.TerminationCriteria[i].VoteThreshold >= p.TerminationCriteria[i+1].VoteThreshold { + return fmt.Errorf("%w: TerminationCriteria[%d].VoteThreshold (%d) should be less than "+ + "TerminationCriteria[%d].VoteThreshold (%d)", ErrParametersInvalid, i, p.TerminationCriteria[i].VoteThreshold, i+1, p.TerminationCriteria[i+1].VoteThreshold) + } + if p.TerminationCriteria[i].ConsecutiveSuccesses < p.TerminationCriteria[i+1].ConsecutiveSuccesses { + return fmt.Errorf("%w: TerminationCriteria[%d].ConsecutiveSuccesses (%d) should be bigger or equal than "+ + "TerminationCriteria[%d].ConsecutiveSuccesses (%d)", + ErrParametersInvalid, i, p.TerminationCriteria[i].ConsecutiveSuccesses, i+1, p.TerminationCriteria[i+1].ConsecutiveSuccesses) + } + } + return nil +} + func (p Parameters) MinPercentConnectedHealthy() float64 { // AlphaConfidence is used here to ensure that the node can still feasibly // accept operations. If AlphaPreference were used, committing could be @@ -123,16 +162,27 @@ func (p Parameters) MinPercentConnectedHealthy() float64 { return alphaRatio*(1-MinPercentConnectedBuffer) + MinPercentConnectedBuffer } +func (p Parameters) terminationConditions() []terminationCondition { + if len(p.TerminationCriteria) == 0 { + return []terminationCondition{{alphaConfidence: p.AlphaConfidence, beta: p.Beta}} + } + + return newTerminationCondition(p.TerminationCriteria) +} + type terminationCondition struct { alphaConfidence int beta int } -func newSingleTerminationCondition(alphaConfidence int, beta int) []terminationCondition { - return []terminationCondition{ - { - alphaConfidence: alphaConfidence, - beta: beta, - }, +func newTerminationCondition(tc []TerminationCriteria) []terminationCondition { + result := make([]terminationCondition, len(tc)) + for i := 0; i < len(tc); i++ { + result[i] = terminationCondition{ + alphaConfidence: tc[i].VoteThreshold, + beta: tc[i].ConsecutiveSuccesses, + } } + + return result } diff --git a/snow/consensus/snowball/parameters_test.go b/snow/consensus/snowball/parameters_test.go index 60a9612ef6ee..a5ea480b4675 100644 --- a/snow/consensus/snowball/parameters_test.go +++ b/snow/consensus/snowball/parameters_test.go @@ -211,6 +211,68 @@ func TestParametersVerify(t *testing.T) { }, expectedError: ErrParametersInvalid, }, + { + name: "termination criteria same vote threshold", + params: Parameters{ + TerminationCriteria: []TerminationCriteria{ + {VoteThreshold: 10, ConsecutiveSuccesses: 5}, + {VoteThreshold: 10, ConsecutiveSuccesses: 4}, + }, + }, + expectedError: ErrParametersInvalid, + }, + { + name: "termination criteria descending vote threshold", + params: Parameters{ + TerminationCriteria: []TerminationCriteria{ + {VoteThreshold: 10, ConsecutiveSuccesses: 5}, + {VoteThreshold: 9, ConsecutiveSuccesses: 4}, + }, + }, + expectedError: ErrParametersInvalid, + }, + { + name: "termination criteria ascending consecutive successes", + params: Parameters{ + TerminationCriteria: []TerminationCriteria{ + {VoteThreshold: 10, ConsecutiveSuccesses: 5}, + {VoteThreshold: 11, ConsecutiveSuccesses: 6}, + }, + }, + expectedError: ErrParametersInvalid, + }, + { + name: "termination criteria single criteria", + params: Parameters{ + K: 1, + AlphaPreference: 1, + Beta: 1, + ConcurrentRepolls: 1, + OptimalProcessing: 1, + MaxOutstandingItems: 1, + MaxItemProcessingTime: 1, + TerminationCriteria: []TerminationCriteria{ + {VoteThreshold: 10, ConsecutiveSuccesses: 5}, + }, + }, + }, + { + name: "termination criteria multiple criteria", + params: Parameters{ + K: 1, + AlphaPreference: 1, + Beta: 1, + ConcurrentRepolls: 1, + OptimalProcessing: 1, + MaxOutstandingItems: 1, + MaxItemProcessingTime: 1, + TerminationCriteria: []TerminationCriteria{ + {VoteThreshold: 10, ConsecutiveSuccesses: 5}, + {VoteThreshold: 11, ConsecutiveSuccesses: 5}, + {VoteThreshold: 12, ConsecutiveSuccesses: 4}, + }, + }, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/snow/consensus/snowball/unary_snowball_test.go b/snow/consensus/snowball/unary_snowball_test.go index 007d2ab53090..faf20b7d7315 100644 --- a/snow/consensus/snowball/unary_snowball_test.go +++ b/snow/consensus/snowball/unary_snowball_test.go @@ -22,7 +22,7 @@ func TestUnarySnowball(t *testing.T) { alphaPreference, alphaConfidence := 1, 2 beta := 2 - terminationConditions := newSingleTerminationCondition(alphaConfidence, beta) + terminationConditions := newTerminationCondition([]TerminationCriteria{{VoteThreshold: alphaConfidence, ConsecutiveSuccesses: beta}}) sb := newUnarySnowball(alphaPreference, terminationConditions) diff --git a/snow/consensus/snowball/unary_snowflake_test.go b/snow/consensus/snowball/unary_snowflake_test.go index ee099460e52b..4b625e7ffdfb 100644 --- a/snow/consensus/snowball/unary_snowflake_test.go +++ b/snow/consensus/snowball/unary_snowflake_test.go @@ -21,7 +21,7 @@ func TestUnarySnowflake(t *testing.T) { alphaPreference, alphaConfidence := 1, 2 beta := 2 - terminationConditions := newSingleTerminationCondition(alphaConfidence, beta) + terminationConditions := newTerminationCondition([]TerminationCriteria{{VoteThreshold: alphaConfidence, ConsecutiveSuccesses: beta}}) sf := newUnarySnowflake(alphaPreference, terminationConditions) diff --git a/snow/consensus/snowman/poll/early_term_no_traversal.go b/snow/consensus/snowman/poll/early_term_no_traversal.go index df09157b04df..1e552e109f5c 100644 --- a/snow/consensus/snowman/poll/early_term_no_traversal.go +++ b/snow/consensus/snowman/poll/early_term_no_traversal.go @@ -99,10 +99,10 @@ func (m *earlyTermNoTraversalMetrics) observeEarlyAlphaConf(duration time.Durati } type earlyTermNoTraversalFactory struct { - alphaPreference int - alphaConfidence int - - metrics *earlyTermNoTraversalMetrics + alphaPreference int + alphaConfidence int + alphaConfidences []int + metrics *earlyTermNoTraversalMetrics } // NewEarlyTermNoTraversalFactory returns a factory that returns polls with @@ -111,6 +111,7 @@ func NewEarlyTermNoTraversalFactory( alphaPreference int, alphaConfidence int, reg prometheus.Registerer, + alphaConfidences []int, ) (Factory, error) { metrics, err := newEarlyTermNoTraversalMetrics(reg) if err != nil { @@ -118,9 +119,10 @@ func NewEarlyTermNoTraversalFactory( } return &earlyTermNoTraversalFactory{ - alphaPreference: alphaPreference, - alphaConfidence: alphaConfidence, - metrics: metrics, + alphaPreference: alphaPreference, + alphaConfidence: alphaConfidence, + metrics: metrics, + alphaConfidences: alphaConfidences, }, nil } @@ -131,6 +133,7 @@ func (f *earlyTermNoTraversalFactory) New(vdrs bag.Bag[ids.NodeID]) Poll { alphaConfidence: f.alphaConfidence, metrics: f.metrics, start: time.Now(), + confidences: f.alphaConfidences, } } @@ -142,6 +145,7 @@ type earlyTermNoTraversalPoll struct { polled bag.Bag[ids.NodeID] alphaPreference int alphaConfidence int + confidences []int metrics *earlyTermNoTraversalMetrics start time.Time @@ -193,6 +197,15 @@ func (p *earlyTermNoTraversalPoll) Finished() bool { } _, freq := p.votes.Mode() + + if len(p.confidences) > 0 { + if p.shouldTerminateEarlyErrDriven(freq, maxPossibleVotes) { + p.finished = true + return true + } + return false + } + if freq >= p.alphaPreference && maxPossibleVotes < p.alphaConfidence { p.finished = true p.metrics.observeEarlyAlphaPref(time.Since(p.start)) @@ -208,6 +221,36 @@ func (p *earlyTermNoTraversalPoll) Finished() bool { return false } +func (p *earlyTermNoTraversalPoll) shouldTerminateEarlyErrDriven(freq, maxPossibleVotes int) bool { + // Case 4 - First check if we collected the highest alpha confidence + if freq >= p.confidences[len(p.confidences)-1] { + p.metrics.observeEarlyAlphaConf(time.Since(p.start)) + return true + } + + if freq < p.alphaPreference { + return false + } + + // Case 3a: We have collected the maximum votes, but it is below any of the confidence thresholds. + if maxPossibleVotes < p.confidences[0] { + p.metrics.observeEarlyAlphaPref(time.Since(p.start)) + return true + } + + // Case 3b - We don't have an outstanding query response that improves our confidence, + // because we have collected a threshold below a threshold we cannot pass due to reaching maximum possible votes. + + for i := 0; i < len(p.confidences)-1; i++ { + if freq >= p.confidences[i] && maxPossibleVotes < p.confidences[i+1] { + p.metrics.observeEarlyAlphaPref(time.Since(p.start)) + return true + } + } + + return false +} + // Result returns the result of this poll func (p *earlyTermNoTraversalPoll) Result() bag.Bag[ids.ID] { return p.votes diff --git a/snow/consensus/snowman/poll/early_term_no_traversal_test.go b/snow/consensus/snowman/poll/early_term_no_traversal_test.go index 232169c01d41..5148ae0d24e9 100644 --- a/snow/consensus/snowman/poll/early_term_no_traversal_test.go +++ b/snow/consensus/snowman/poll/early_term_no_traversal_test.go @@ -13,7 +13,7 @@ import ( ) func newEarlyTermNoTraversalTestFactory(require *require.Assertions, alpha int) Factory { - factory, err := NewEarlyTermNoTraversalFactory(alpha, alpha, prometheus.NewRegistry()) + factory, err := NewEarlyTermNoTraversalFactory(alpha, alpha, prometheus.NewRegistry(), nil) require.NoError(err) return factory } @@ -99,7 +99,7 @@ func TestEarlyTermNoTraversalTerminatesEarlyWithAlphaPreference(t *testing.T) { alphaPreference := 3 alphaConfidence := 5 - factory, err := NewEarlyTermNoTraversalFactory(alphaPreference, alphaConfidence, prometheus.NewRegistry()) + factory, err := NewEarlyTermNoTraversalFactory(alphaPreference, alphaConfidence, prometheus.NewRegistry(), nil) require.NoError(err) poll := factory.New(vdrs) @@ -116,6 +116,59 @@ func TestEarlyTermNoTraversalTerminatesEarlyWithAlphaPreference(t *testing.T) { require.True(poll.Finished()) } +// Tests case 3a with multiple termination criteria +func TestEarlyMultiTermATerminatesEarlyWithAlphaPreference(t *testing.T) { + require := require.New(t) + + vdrs := bag.Of(vdr1, vdr2, vdr3, vdr4, vdr5) // k = 5 + alphaPreference := 2 + terminationCriteria := []int{2, 3, 5} + + factory, err := NewEarlyTermNoTraversalFactory(alphaPreference, 0, prometheus.NewRegistry(), terminationCriteria) + require.NoError(err) + poll := factory.New(vdrs) + + poll.Drop(vdr1) + require.False(poll.Finished()) + + poll.Vote(vdr2, blkID1) + require.False(poll.Finished()) + + poll.Vote(vdr3, blkID1) + require.False(poll.Finished()) + + poll.Vote(vdr4, blkID1) + require.True(poll.Finished()) +} + +// Tests case 3b with multiple termination criteria +func TestEarlyMultiTermBTerminatesEarlyWithAlphaPreference(t *testing.T) { + require := require.New(t) + + vdrs := bag.Of(vdr1, vdr2, vdr3, vdr4, vdr5) // k = 5 + alphaPreference := 2 + terminationCriteria := []int{4, 5} + + factory, err := NewEarlyTermNoTraversalFactory(alphaPreference, 0, prometheus.NewRegistry(), terminationCriteria) + require.NoError(err) + poll := factory.New(vdrs) + + poll.Drop(vdr1) + require.False(poll.Finished()) + + poll.Vote(vdr2, blkID1) + require.False(poll.Finished()) + + poll.Vote(vdr3, blkID1) + require.False(poll.Finished()) + + poll.Drop(vdr4) + require.True(poll.Finished()) + + poll.Drop(vdr5) // Last poll doesn't do anything + require.True(poll.Finished()) +} + // Tests case 4 func TestEarlyTermNoTraversalTerminatesEarlyWithAlphaConfidence(t *testing.T) { require := require.New(t) @@ -124,7 +177,29 @@ func TestEarlyTermNoTraversalTerminatesEarlyWithAlphaConfidence(t *testing.T) { alphaPreference := 3 alphaConfidence := 3 - factory, err := NewEarlyTermNoTraversalFactory(alphaPreference, alphaConfidence, prometheus.NewRegistry()) + factory, err := NewEarlyTermNoTraversalFactory(alphaPreference, alphaConfidence, prometheus.NewRegistry(), nil) + require.NoError(err) + poll := factory.New(vdrs) + + poll.Vote(vdr1, blkID1) + require.False(poll.Finished()) + + poll.Vote(vdr2, blkID1) + require.False(poll.Finished()) + + poll.Vote(vdr3, blkID1) + require.True(poll.Finished()) +} + +// Tests case 4 with multiple termination criteria +func TestEarlyMultiTermNoTerminatesEarlyWithAlphaConfidence(t *testing.T) { + require := require.New(t) + + vdrs := bag.Of(vdr1, vdr2, vdr3, vdr4, vdr5) // k = 5 + alphaPreference := 3 + terminationCriteria := []int{2, 3} + + factory, err := NewEarlyTermNoTraversalFactory(alphaPreference, 0, prometheus.NewRegistry(), terminationCriteria) require.NoError(err) poll := factory.New(vdrs) diff --git a/snow/engine/snowman/engine.go b/snow/engine/snowman/engine.go index 1b2cfb5c11d4..49effd616b45 100644 --- a/snow/engine/snowman/engine.go +++ b/snow/engine/snowman/engine.go @@ -107,10 +107,16 @@ func New(config Config) (*Engine, error) { acceptedFrontiers := tracker.NewAccepted() config.Validators.RegisterSetCallbackListener(config.Ctx.SubnetID, acceptedFrontiers) + confidences := make([]int, 0, len(config.Params.TerminationCriteria)) + for _, confidence := range config.Params.TerminationCriteria { + confidences = append(confidences, confidence.VoteThreshold) + } + factory, err := poll.NewEarlyTermNoTraversalFactory( config.Params.AlphaPreference, config.Params.AlphaConfidence, config.Ctx.Registerer, + confidences, ) if err != nil { return nil, err