hotstuff icon indicating copy to clipboard operation
hotstuff copied to clipboard

feat: support for parsing string-based byzantine specification (not planned)

Open meling opened this issue 1 year ago • 0 comments

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)
			}
		})
	}
}

meling avatar Jan 23 '25 21:01 meling