hotstuff
hotstuff copied to clipboard
feat: support for parsing string-based byzantine specification (not planned)
The following parsing code was written for use in tests, but a better solution was found for the test case. It is also not needed for Cue-based configurations, since the Cue parser handles errors for us. Hence, I decided not to include it in the code base but instead post it here if we, in the future, decide that we want to support flag-based parsing of the byzantine string.
I made this as a method on ExperimentConfig (in config.go):
// SetByzantineStrategy sets the Byzantine strategy for the experiment based on
// the string format: "strategy1:id1,strategy2:id2,..."
func (c *ExperimentConfig) SetByzantineStrategy(byzantine string) error {
byzStrategy := make(map[string][]uint32)
for _, entry := range strings.Split(byzantine, ",") {
if strategy, replicaID, ok := strings.Cut(entry, ":"); ok {
if strategy == "" || replicaID == "" {
return errors.New("missing strategy or ID")
}
id, err := strconv.ParseUint(replicaID, 10, 32)
if err != nil {
return err
}
byzStrategy[strategy] = append(byzStrategy[strategy], uint32(id))
}
}
c.ByzantineStrategy = byzStrategy
return nil
}
Here is a test for it (in config_test.go):
func TestSetByzantineStrategy(t *testing.T) {
tests := []struct {
name string
byz string
want map[string][]uint32
wantErr bool
}{
{name: "Empty", byz: "", want: map[string][]uint32{}},
{name: "Silent", byz: "silent:1,silent:3", want: map[string][]uint32{"silent": {1, 3}}},
{name: "Slow", byz: "slow:2", want: map[string][]uint32{"slow": {2}}},
{name: "SilentSlow", byz: "silent:1,silent:3,slow:2", want: map[string][]uint32{"silent": {1, 3}, "slow": {2}}},
{name: "SlowSilent", byz: "slow:2,silent:1,silent:3", want: map[string][]uint32{"silent": {1, 3}, "slow": {2}}},
{name: "Fork", byz: "fork:1,fork:2", want: map[string][]uint32{"fork": {1, 2}}},
{name: "ForkSilent", byz: "fork:1,fork:2,silent:3", want: map[string][]uint32{"fork": {1, 2}, "silent": {3}}},
{name: "ForkSilentSlow", byz: "fork:1,fork:2,silent:3,slow:4", want: map[string][]uint32{"fork": {1, 2}, "silent": {3}, "slow": {4}}},
{name: "MissingStrategy", byz: ":1", want: nil, wantErr: true},
{name: "MissingStrategy2", byz: "fork:1,", want: map[string][]uint32{"fork": {1}}, wantErr: false}, // this is okay
{name: "MissingID", byz: "fork:", want: nil, wantErr: true},
{name: "MissingID2", byz: "fork:,", want: nil, wantErr: true},
{name: "InvalidID", byz: "fork:1.5", want: nil, wantErr: true},
{name: "InvalidID2", byz: "fork:a,2", want: nil, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &config.ExperimentConfig{}
if err := cfg.SetByzantineStrategy(tt.byz); (err != nil) != tt.wantErr {
t.Errorf("SetByzantineStrategy() error = %v, want nil", err)
}
if diff := cmp.Diff(cfg.ByzantineStrategy, tt.want); diff != "" {
t.Errorf("SetByzantineStrategy() mismatch (-want +got):\n%s", diff)
}
})
}
}