diff --git a/consensus/polybft/helpers_test.go b/consensus/polybft/helpers_test.go index 87d764789c..d578794c08 100644 --- a/consensus/polybft/helpers_test.go +++ b/consensus/polybft/helpers_test.go @@ -143,35 +143,35 @@ func getEpochNumber(t *testing.T, blockNumber, epochSize uint64) uint64 { } // newTestState creates new instance of state used by tests. -func newTestState(t *testing.T) *State { - t.Helper() +func newTestState(tb testing.TB) *State { + tb.Helper() dir := fmt.Sprintf("/tmp/consensus-temp_%v", time.Now().UTC().Format(time.RFC3339Nano)) err := os.Mkdir(dir, 0775) if err != nil { - t.Fatal(err) + tb.Fatal(err) } state, err := newState(path.Join(dir, "my.db"), hclog.NewNullLogger(), make(chan struct{})) if err != nil { - t.Fatal(err) + tb.Fatal(err) } - t.Cleanup(func() { + tb.Cleanup(func() { if err := os.RemoveAll(dir); err != nil { - t.Fatal(err) + tb.Fatal(err) } }) return state } -func generateTestAccount(t *testing.T) *wallet.Account { - t.Helper() +func generateTestAccount(tb testing.TB) *wallet.Account { + tb.Helper() acc, err := wallet.GenerateAccount() - require.NoError(t, err) + require.NoError(tb, err) return acc } diff --git a/consensus/polybft/stake_manager_fuzz_test.go b/consensus/polybft/stake_manager_fuzz_test.go new file mode 100644 index 0000000000..81ceb883fa --- /dev/null +++ b/consensus/polybft/stake_manager_fuzz_test.go @@ -0,0 +1,284 @@ +package polybft + +import ( + "encoding/json" + "errors" + "fmt" + "math/big" + "reflect" + "testing" + + "github.com/0xPolygon/polygon-edge/consensus/polybft/validator" + "github.com/0xPolygon/polygon-edge/consensus/polybft/wallet" + "github.com/0xPolygon/polygon-edge/types" + "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/require" +) + +type epochIDValidatorsF struct { + EpochID uint64 + Validators []*validator.ValidatorMetadata +} + +type postBlockStructF struct { + EpochID uint64 + ValidatorID uint64 + BlockID uint64 + StakeValue uint64 +} + +type updateValidatorSetF struct { + EpochID uint64 + Index uint64 + VotingPower int64 +} + +func FuzzTestStakeManagerPostEpoch(f *testing.F) { + state := newTestState(f) + + seeds := []epochIDValidatorsF{ + { + EpochID: 0, + Validators: validator.NewTestValidators(f, 6).GetPublicIdentities(), + }, + { + EpochID: 1, + Validators: validator.NewTestValidators(f, 42).GetPublicIdentities(), + }, + { + EpochID: 42, + Validators: validator.NewTestValidators(f, 6).GetPublicIdentities(), + }, + } + + for _, seed := range seeds { + data, err := json.Marshal(seed) + if err != nil { + return + } + + f.Add(data) + } + + f.Fuzz(func(t *testing.T, input []byte) { + stakeManager := &stakeManager{ + logger: hclog.NewNullLogger(), + state: state, + maxValidatorSetSize: 10, + } + + var data epochIDValidatorsF + if err := json.Unmarshal(input, &data); err != nil { + t.Skip(err) + } + + invalidDataFormat := false + for _, v := range data.Validators { + if err := ValidateStruct(*v); err != nil { + invalidDataFormat = true + } + } + if invalidDataFormat { + t.Skip() + } + + err := stakeManager.PostEpoch(&PostEpochRequest{ + NewEpochID: data.EpochID, + ValidatorSet: validator.NewValidatorSet( + data.Validators, + stakeManager.logger, + ), + }) + require.NoError(t, err) + }) +} + +func FuzzTestStakeManagerPostBlock(f *testing.F) { + var ( + allAliases = []string{"A", "B", "C", "D", "E", "F"} + initialSetAliases = []string{"A", "B", "C", "D", "E"} + validators = validator.NewTestValidatorsWithAliases(f, allAliases) + state = newTestState(f) + ) + + seeds := []postBlockStructF{ + { + EpochID: 0, + ValidatorID: 1, + BlockID: 1, + StakeValue: 30, + }, + { + EpochID: 5, + ValidatorID: 30, + BlockID: 4, + StakeValue: 60, + }, + { + EpochID: 1, + ValidatorID: 42, + BlockID: 11, + StakeValue: 70, + }, + } + + for _, seed := range seeds { + data, err := json.Marshal(seed) + if err != nil { + return + } + + f.Add(data) + } + + f.Fuzz(func(t *testing.T, input []byte) { + t.Parallel() + + var data postBlockStructF + if err := json.Unmarshal(input, &data); err != nil { + t.Skip(err) + } + + if err := ValidateStruct(data); err != nil { + t.Skip(err) + } + + if data.ValidatorID > uint64(len(initialSetAliases)-1) { + t.Skip() + } + + stakeManager := newStakeManager( + hclog.NewNullLogger(), + state, + nil, + wallet.NewEcdsaSigner(validators.GetValidator("A").Key()), + types.StringToAddress("0x0001"), + types.StringToAddress("0x0002"), + 5, + ) + + // insert initial full validator set + require.NoError(t, state.StakeStore.insertFullValidatorSet(validatorSetState{ + Validators: newValidatorStakeMap(validators.GetPublicIdentities(initialSetAliases...)), + })) + + receipt := &types.Receipt{ + Logs: []*types.Log{ + createTestLogForTransferEvent( + t, + stakeManager.validatorSetContract, + validators.GetValidator(initialSetAliases[data.ValidatorID]).Address(), + types.ZeroAddress, + data.StakeValue, + ), + }, + } + + req := &PostBlockRequest{ + FullBlock: &types.FullBlock{Block: &types.Block{Header: &types.Header{Number: data.BlockID}}, + Receipts: []*types.Receipt{receipt}, + }, + Epoch: data.EpochID, + } + err := stakeManager.PostBlock(req) + require.NoError(t, err) + }) +} + +func FuzzTestStakeManagerUpdateValidatorSet(f *testing.F) { + var ( + aliases = []string{"A", "B", "C", "D", "E"} + stakes = []uint64{10, 10, 10, 10, 10} + ) + + validators := validator.NewTestValidatorsWithAliases(f, aliases, stakes) + state := newTestState(f) + + stakeManager := newStakeManager( + hclog.NewNullLogger(), + state, + nil, + wallet.NewEcdsaSigner(validators.GetValidator("A").Key()), + types.StringToAddress("0x0001"), types.StringToAddress("0x0002"), + 10, + ) + + seeds := []updateValidatorSetF{ + { + EpochID: 0, + Index: 1, + VotingPower: 30, + }, + { + EpochID: 1, + Index: 4, + VotingPower: 1, + }, + { + EpochID: 2, + Index: 3, + VotingPower: -2, + }, + } + + for _, seed := range seeds { + data, err := json.Marshal(seed) + if err != nil { + return + } + + f.Add(data) + } + + f.Fuzz(func(t *testing.T, input []byte) { + var data updateValidatorSetF + if err := json.Unmarshal(input, &data); err != nil { + t.Skip(err) + } + + if err := ValidateStruct(data); err != nil { + t.Skip(err) + } + + if data.Index > uint64(len(aliases)-1) { + t.Skip() + } + + err := state.StakeStore.insertFullValidatorSet(validatorSetState{ + Validators: newValidatorStakeMap(validators.GetPublicIdentities())}) + require.NoError(t, err) + + _, err = stakeManager.UpdateValidatorSet(data.EpochID, validators.GetPublicIdentities(aliases[data.Index:]...)) + require.NoError(t, err) + + fullValidatorSet := validators.GetPublicIdentities().Copy() + validatorToUpdate := fullValidatorSet[data.Index] + validatorToUpdate.VotingPower = big.NewInt(data.VotingPower) + + _, err = stakeManager.UpdateValidatorSet(data.EpochID, validators.GetPublicIdentities()) + require.NoError(t, err) + }) +} + +func ValidateStruct(s interface{}) (err error) { + structType := reflect.TypeOf(s) + if structType.Kind() != reflect.Struct { + return errors.New("input param should be a struct") + } + + structVal := reflect.ValueOf(s) + fieldNum := structVal.NumField() + + for i := 0; i < fieldNum; i++ { + field := structVal.Field(i) + fieldName := structType.Field(i).Name + + isSet := field.IsValid() && !field.IsZero() + + if !isSet { + err = fmt.Errorf("%w%s is not set; ", err, fieldName) + } + } + + return err +} diff --git a/consensus/polybft/validator/test_helpers.go b/consensus/polybft/validator/test_helpers.go index fbe34037c3..8a6c719d21 100644 --- a/consensus/polybft/validator/test_helpers.go +++ b/consensus/polybft/validator/test_helpers.go @@ -20,19 +20,19 @@ type TestValidators struct { Validators map[string]*TestValidator } -func NewTestValidators(t *testing.T, validatorsCount int) *TestValidators { - t.Helper() +func NewTestValidators(tb testing.TB, validatorsCount int) *TestValidators { + tb.Helper() aliases := make([]string, validatorsCount) for i := 0; i < validatorsCount; i++ { aliases[i] = strconv.Itoa(i) } - return NewTestValidatorsWithAliases(t, aliases) + return NewTestValidatorsWithAliases(tb, aliases) } -func NewTestValidatorsWithAliases(t *testing.T, aliases []string, votingPowers ...[]uint64) *TestValidators { - t.Helper() +func NewTestValidatorsWithAliases(tb testing.TB, aliases []string, votingPowers ...[]uint64) *TestValidators { + tb.Helper() validators := map[string]*TestValidator{} @@ -42,7 +42,7 @@ func NewTestValidatorsWithAliases(t *testing.T, aliases []string, votingPowers . votingPower = votingPowers[0][i] } - validators[alias] = NewTestValidator(t, alias, votingPower) + validators[alias] = NewTestValidator(tb, alias, votingPower) } return &TestValidators{Validators: validators} @@ -139,13 +139,13 @@ type TestValidator struct { VotingPower uint64 } -func NewTestValidator(t *testing.T, alias string, votingPower uint64) *TestValidator { - t.Helper() +func NewTestValidator(tb testing.TB, alias string, votingPower uint64) *TestValidator { + tb.Helper() return &TestValidator{ Alias: alias, VotingPower: votingPower, - Account: generateTestAccount(t), + Account: generateTestAccount(tb), } } @@ -185,11 +185,11 @@ func (v *TestValidator) MustSign(hash, domain []byte) *bls.Signature { return signature } -func generateTestAccount(t *testing.T) *wallet.Account { - t.Helper() +func generateTestAccount(tb testing.TB) *wallet.Account { + tb.Helper() acc, err := wallet.GenerateAccount() - require.NoError(t, err) + require.NoError(tb, err) return acc }