From 783fdfc9d52267865300acee56b10e918a6e80b6 Mon Sep 17 00:00:00 2001 From: aaronbuchwald Date: Thu, 6 Jun 2024 11:59:05 -0400 Subject: [PATCH] Implement error driven snowflake hardcoded with a single beta (#2978) --- snow/consensus/snowball/binary_snowflake.go | 69 ++++++++++------- snow/consensus/snowball/nnary_snowflake.go | 69 ++++++++++------- snow/consensus/snowball/parameters.go | 5 ++ snow/consensus/snowball/unary_snowball.go | 17 +++-- .../consensus/snowball/unary_snowball_test.go | 14 ++-- snow/consensus/snowball/unary_snowflake.go | 74 ++++++++++++------- .../snowball/unary_snowflake_test.go | 16 ++-- 7 files changed, 164 insertions(+), 100 deletions(-) diff --git a/snow/consensus/snowball/binary_snowflake.go b/snow/consensus/snowball/binary_snowflake.go index 6349fd3975a6..81cca1ce8501 100644 --- a/snow/consensus/snowball/binary_snowflake.go +++ b/snow/consensus/snowball/binary_snowflake.go @@ -11,29 +11,38 @@ func newBinarySnowflake(alphaPreference, alphaConfidence, beta, choice int) bina return binarySnowflake{ binarySlush: newBinarySlush(choice), alphaPreference: alphaPreference, - alphaConfidence: alphaConfidence, - beta: beta, + terminationConditions: []terminationCondition{ + { + alphaConfidence: alphaConfidence, + beta: beta, + }, + }, + confidence: make([]int, 1), } } // binarySnowflake is the implementation of a binary snowflake instance +// Invariant: +// len(terminationConditions) == len(confidence) +// terminationConditions[i].alphaConfidence < terminationConditions[i+1].alphaConfidence +// terminationConditions[i].beta <= terminationConditions[i+1].beta +// confidence[i] >= confidence[i+1] (except after finalizing due to early termination) type binarySnowflake struct { // wrap the binary slush logic binarySlush - // confidence tracks the number of successful polls in a row that have - // returned the preference - confidence int - // alphaPreference is the threshold required to update the preference alphaPreference int - // alphaConfidence is the threshold required to increment the confidence counter - alphaConfidence int + // terminationConditions gives the ascending ordered list of alphaConfidence values + // required to increment the corresponding confidence counter. + // The corresponding beta values give the threshold required to finalize this instance. + terminationConditions []terminationCondition - // beta is the number of consecutive successful queries required for - // finalization. - beta int + // confidence is the number of consecutive succcessful polls for a given + // alphaConfidence threshold. + // This instance finalizes when confidence[i] >= terminationConditions[i].beta for any i + confidence []int // finalized prevents the state from changing after the required number of // consecutive polls has been reached @@ -50,26 +59,34 @@ func (sf *binarySnowflake) RecordPoll(count, choice int) { return } - if count < sf.alphaConfidence { - sf.confidence = 0 - sf.binarySlush.RecordSuccessfulPoll(choice) - return + // If I am changing my preference, reset confidence counters + // before recording a successful poll on the slush instance. + if choice != sf.Preference() { + clear(sf.confidence) } + sf.binarySlush.RecordSuccessfulPoll(choice) - if preference := sf.Preference(); preference == choice { - sf.confidence++ - } else { - // confidence is set to 1 because there has already been 1 successful - // poll, namely this poll. - sf.confidence = 1 + for i, terminationCondition := range sf.terminationConditions { + // If I did not reach this alpha threshold, I did not + // reach any more alpha thresholds. + // Clear the remaining confidence counters. + if count < terminationCondition.alphaConfidence { + clear(sf.confidence[i:]) + return + } + + // I reached this alpha threshold, increment the confidence counter + // and check if I can finalize. + sf.confidence[i]++ + if sf.confidence[i] >= terminationCondition.beta { + sf.finalized = true + return + } } - - sf.finalized = sf.confidence >= sf.beta - sf.binarySlush.RecordSuccessfulPoll(choice) } func (sf *binarySnowflake) RecordUnsuccessfulPoll() { - sf.confidence = 0 + clear(sf.confidence) } func (sf *binarySnowflake) Finalized() bool { @@ -78,7 +95,7 @@ func (sf *binarySnowflake) Finalized() bool { func (sf *binarySnowflake) String() string { return fmt.Sprintf("SF(Confidence = %d, Finalized = %v, %s)", - sf.confidence, + sf.confidence[0], sf.finalized, &sf.binarySlush) } diff --git a/snow/consensus/snowball/nnary_snowflake.go b/snow/consensus/snowball/nnary_snowflake.go index 9433078e36b8..ab3c4f462c29 100644 --- a/snow/consensus/snowball/nnary_snowflake.go +++ b/snow/consensus/snowball/nnary_snowflake.go @@ -15,30 +15,39 @@ func newNnarySnowflake(alphaPreference, alphaConfidence, beta int, choice ids.ID return nnarySnowflake{ nnarySlush: newNnarySlush(choice), alphaPreference: alphaPreference, - alphaConfidence: alphaConfidence, - beta: beta, + terminationConditions: []terminationCondition{ + { + alphaConfidence: alphaConfidence, + beta: beta, + }, + }, + confidence: make([]int, 1), } } // nnarySnowflake is the implementation of a snowflake instance with an // unbounded number of choices +// Invariant: +// len(terminationConditions) == len(confidence) +// terminationConditions[i].alphaConfidence < terminationConditions[i+1].alphaConfidence +// terminationConditions[i].beta <= terminationConditions[i+1].beta +// confidence[i] >= confidence[i+1] (except after finalizing due to early termination) type nnarySnowflake struct { // wrap the n-nary slush logic nnarySlush - // beta is the number of consecutive successful queries required for - // finalization. - beta int - // alphaPreference is the threshold required to update the preference alphaPreference int - // alphaConfidence is the threshold required to increment the confidence counter - alphaConfidence int + // terminationConditions gives the ascending ordered list of alphaConfidence values + // required to increment the corresponding confidence counter. + // The corresponding beta values give the threshold required to finalize this instance. + terminationConditions []terminationCondition - // confidence tracks the number of successful polls in a row that have - // returned the preference - confidence int + // confidence is the number of consecutive succcessful polls for a given + // alphaConfidence threshold. + // This instance finalizes when confidence[i] >= terminationConditions[i].beta for any i + confidence []int // finalized prevents the state from changing after the required number of // consecutive polls has been reached @@ -57,26 +66,34 @@ func (sf *nnarySnowflake) RecordPoll(count int, choice ids.ID) { return } - if count < sf.alphaConfidence { - sf.confidence = 0 - sf.nnarySlush.RecordSuccessfulPoll(choice) - return + // If I am changing my preference, reset confidence counters + // before recording a successful poll on the slush instance. + if choice != sf.Preference() { + clear(sf.confidence) } + sf.nnarySlush.RecordSuccessfulPoll(choice) - if preference := sf.Preference(); preference == choice { - sf.confidence++ - } else { - // confidence is set to 1 because there has already been 1 successful - // poll, namely this poll. - sf.confidence = 1 + for i, terminationCondition := range sf.terminationConditions { + // If I did not reach this alpha threshold, I did not + // reach any more alpha thresholds. + // Clear the remaining confidence counters. + if count < terminationCondition.alphaConfidence { + clear(sf.confidence[i:]) + return + } + + // I reached this alpha threshold, increment the confidence counter + // and check if I can finalize. + sf.confidence[i]++ + if sf.confidence[i] >= terminationCondition.beta { + sf.finalized = true + return + } } - - sf.finalized = sf.confidence >= sf.beta - sf.nnarySlush.RecordSuccessfulPoll(choice) } func (sf *nnarySnowflake) RecordUnsuccessfulPoll() { - sf.confidence = 0 + clear(sf.confidence) } func (sf *nnarySnowflake) Finalized() bool { @@ -85,7 +102,7 @@ func (sf *nnarySnowflake) Finalized() bool { func (sf *nnarySnowflake) String() string { return fmt.Sprintf("SF(Confidence = %d, Finalized = %v, %s)", - sf.confidence, + sf.confidence[0], sf.finalized, &sf.nnarySlush) } diff --git a/snow/consensus/snowball/parameters.go b/snow/consensus/snowball/parameters.go index 9a63a3316e6e..a13d99c27565 100644 --- a/snow/consensus/snowball/parameters.go +++ b/snow/consensus/snowball/parameters.go @@ -122,3 +122,8 @@ func (p Parameters) MinPercentConnectedHealthy() float64 { alphaRatio := float64(p.AlphaConfidence) / float64(p.K) return alphaRatio*(1-MinPercentConnectedBuffer) + MinPercentConnectedBuffer } + +type terminationCondition struct { + alphaConfidence int + beta int +} diff --git a/snow/consensus/snowball/unary_snowball.go b/snow/consensus/snowball/unary_snowball.go index 2c15a58cf971..24ed78cee43b 100644 --- a/snow/consensus/snowball/unary_snowball.go +++ b/snow/consensus/snowball/unary_snowball.go @@ -3,7 +3,10 @@ package snowball -import "fmt" +import ( + "fmt" + "slices" +) var _ Unary = (*unarySnowball)(nil) @@ -32,12 +35,11 @@ func (sb *unarySnowball) RecordPoll(count int) { func (sb *unarySnowball) Extend(choice int) Binary { bs := &binarySnowball{ binarySnowflake: binarySnowflake{ - binarySlush: binarySlush{preference: choice}, - confidence: sb.confidence, - alphaPreference: sb.alphaPreference, - alphaConfidence: sb.alphaConfidence, - beta: sb.beta, - finalized: sb.Finalized(), + binarySlush: binarySlush{preference: choice}, + confidence: slices.Clone(sb.confidence), + alphaPreference: sb.alphaPreference, + terminationConditions: sb.terminationConditions, + finalized: sb.Finalized(), }, preference: choice, } @@ -47,6 +49,7 @@ func (sb *unarySnowball) Extend(choice int) Binary { func (sb *unarySnowball) Clone() Unary { newSnowball := *sb + newSnowball.confidence = slices.Clone(sb.confidence) return &newSnowball } diff --git a/snow/consensus/snowball/unary_snowball_test.go b/snow/consensus/snowball/unary_snowball_test.go index 1178ca6bdaa9..4bea0458d95f 100644 --- a/snow/consensus/snowball/unary_snowball_test.go +++ b/snow/consensus/snowball/unary_snowball_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" ) -func UnarySnowballStateTest(t *testing.T, sb *unarySnowball, expectedPreferenceStrength, expectedConfidence int, expectedFinalized bool) { +func UnarySnowballStateTest(t *testing.T, sb *unarySnowball, expectedPreferenceStrength int, expectedConfidence []int, expectedFinalized bool) { require := require.New(t) require.Equal(expectedPreferenceStrength, sb.preferenceStrength) @@ -26,25 +26,25 @@ func TestUnarySnowball(t *testing.T) { sb := newUnarySnowball(alphaPreference, alphaConfidence, beta) sb.RecordPoll(alphaConfidence) - UnarySnowballStateTest(t, &sb, 1, 1, false) + UnarySnowballStateTest(t, &sb, 1, []int{1}, false) sb.RecordPoll(alphaPreference) - UnarySnowballStateTest(t, &sb, 2, 0, false) + UnarySnowballStateTest(t, &sb, 2, []int{0}, false) sb.RecordPoll(alphaConfidence) - UnarySnowballStateTest(t, &sb, 3, 1, false) + UnarySnowballStateTest(t, &sb, 3, []int{1}, false) sb.RecordUnsuccessfulPoll() - UnarySnowballStateTest(t, &sb, 3, 0, false) + UnarySnowballStateTest(t, &sb, 3, []int{0}, false) sb.RecordPoll(alphaConfidence) - UnarySnowballStateTest(t, &sb, 4, 1, false) + UnarySnowballStateTest(t, &sb, 4, []int{1}, false) sbCloneIntf := sb.Clone() require.IsType(&unarySnowball{}, sbCloneIntf) sbClone := sbCloneIntf.(*unarySnowball) - UnarySnowballStateTest(t, sbClone, 4, 1, false) + UnarySnowballStateTest(t, sbClone, 4, []int{1}, false) binarySnowball := sbClone.Extend(0) diff --git a/snow/consensus/snowball/unary_snowflake.go b/snow/consensus/snowball/unary_snowflake.go index edf5fdbd256f..3e21316dc370 100644 --- a/snow/consensus/snowball/unary_snowflake.go +++ b/snow/consensus/snowball/unary_snowflake.go @@ -3,33 +3,45 @@ package snowball -import "fmt" +import ( + "fmt" + "slices" +) var _ Unary = (*unarySnowflake)(nil) func newUnarySnowflake(alphaPreference, alphaConfidence, beta int) unarySnowflake { return unarySnowflake{ alphaPreference: alphaPreference, - alphaConfidence: alphaConfidence, - beta: beta, + terminationConditions: []terminationCondition{ + { + alphaConfidence: alphaConfidence, + beta: beta, + }, + }, + confidence: make([]int, 1), } } // unarySnowflake is the implementation of a unary snowflake instance +// Invariant: +// len(terminationConditions) == len(confidence) +// terminationConditions[i].alphaConfidence < terminationConditions[i+1].alphaConfidence +// terminationConditions[i].beta <= terminationConditions[i+1].beta +// confidence[i] >= confidence[i+1] (except after finalizing due to early termination) type unarySnowflake struct { - // beta is the number of consecutive successful queries required for - // finalization. - beta int - // alphaPreference is the threshold required to update the preference alphaPreference int - // alphaConfidence is the threshold required to increment the confidence counter - alphaConfidence int + // terminationConditions gives the ascending ordered list of alphaConfidence values + // required to increment the corresponding confidence counter. + // The corresponding beta values give the threshold required to finalize this instance. + terminationConditions []terminationCondition - // confidence tracks the number of successful polls in a row that have - // returned the preference - confidence int + // confidence is the number of consecutive succcessful polls for a given + // alphaConfidence threshold. + // This instance finalizes when confidence[i] >= terminationConditions[i].beta for any i + confidence []int // finalized prevents the state from changing after the required number of // consecutive polls has been reached @@ -37,17 +49,27 @@ type unarySnowflake struct { } func (sf *unarySnowflake) RecordPoll(count int) { - if count < sf.alphaConfidence { - sf.RecordUnsuccessfulPoll() - return + for i, terminationCondition := range sf.terminationConditions { + // If I did not reach this alpha threshold, I did not + // reach any more alpha thresholds. + // Clear the remaining confidence counters. + if count < terminationCondition.alphaConfidence { + clear(sf.confidence[i:]) + return + } + + // I reached this alpha threshold, increment the confidence counter + // and check if I can finalize. + sf.confidence[i]++ + if sf.confidence[i] >= terminationCondition.beta { + sf.finalized = true + return + } } - - sf.confidence++ - sf.finalized = sf.finalized || sf.confidence >= sf.beta } func (sf *unarySnowflake) RecordUnsuccessfulPoll() { - sf.confidence = 0 + clear(sf.confidence) } func (sf *unarySnowflake) Finalized() bool { @@ -56,22 +78,22 @@ func (sf *unarySnowflake) Finalized() bool { func (sf *unarySnowflake) Extend(choice int) Binary { return &binarySnowflake{ - binarySlush: binarySlush{preference: choice}, - confidence: sf.confidence, - alphaPreference: sf.alphaPreference, - alphaConfidence: sf.alphaConfidence, - beta: sf.beta, - finalized: sf.finalized, + binarySlush: binarySlush{preference: choice}, + confidence: slices.Clone(sf.confidence), + alphaPreference: sf.alphaPreference, + terminationConditions: sf.terminationConditions, + finalized: sf.finalized, } } func (sf *unarySnowflake) Clone() Unary { newSnowflake := *sf + newSnowflake.confidence = slices.Clone(sf.confidence) return &newSnowflake } func (sf *unarySnowflake) String() string { return fmt.Sprintf("SF(Confidence = %d, Finalized = %v)", - sf.confidence, + sf.confidence[0], sf.finalized) } diff --git a/snow/consensus/snowball/unary_snowflake_test.go b/snow/consensus/snowball/unary_snowflake_test.go index 6a3348f53502..0c6282060b42 100644 --- a/snow/consensus/snowball/unary_snowflake_test.go +++ b/snow/consensus/snowball/unary_snowflake_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" ) -func UnarySnowflakeStateTest(t *testing.T, sf *unarySnowflake, expectedConfidence int, expectedFinalized bool) { +func UnarySnowflakeStateTest(t *testing.T, sf *unarySnowflake, expectedConfidence []int, expectedFinalized bool) { require := require.New(t) require.Equal(expectedConfidence, sf.confidence) @@ -25,19 +25,19 @@ func TestUnarySnowflake(t *testing.T) { sf := newUnarySnowflake(alphaPreference, alphaConfidence, beta) sf.RecordPoll(alphaConfidence) - UnarySnowflakeStateTest(t, &sf, 1, false) + UnarySnowflakeStateTest(t, &sf, []int{1}, false) sf.RecordUnsuccessfulPoll() - UnarySnowflakeStateTest(t, &sf, 0, false) + UnarySnowflakeStateTest(t, &sf, []int{0}, false) sf.RecordPoll(alphaConfidence) - UnarySnowflakeStateTest(t, &sf, 1, false) + UnarySnowflakeStateTest(t, &sf, []int{1}, false) sfCloneIntf := sf.Clone() require.IsType(&unarySnowflake{}, sfCloneIntf) sfClone := sfCloneIntf.(*unarySnowflake) - UnarySnowflakeStateTest(t, sfClone, 1, false) + UnarySnowflakeStateTest(t, sfClone, []int{1}, false) binarySnowflake := sfClone.Extend(0) @@ -53,11 +53,11 @@ func TestUnarySnowflake(t *testing.T) { require.True(binarySnowflake.Finalized()) sf.RecordPoll(alphaConfidence) - UnarySnowflakeStateTest(t, &sf, 2, true) + UnarySnowflakeStateTest(t, &sf, []int{2}, true) sf.RecordUnsuccessfulPoll() - UnarySnowflakeStateTest(t, &sf, 0, true) + UnarySnowflakeStateTest(t, &sf, []int{0}, true) sf.RecordPoll(alphaConfidence) - UnarySnowflakeStateTest(t, &sf, 1, true) + UnarySnowflakeStateTest(t, &sf, []int{1}, true) }