From e88ff05c95f8e8b38241aff84a394dc15cbd918a Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Sat, 10 Dec 2022 13:51:01 +0000 Subject: [PATCH] x/interchainstaking/keeper: add fuzzers Adds fuzzers that we've written to help find vulnerabilities. While here, using go test -short to skip fuzzing being run in regular tests per `make test`. Updates #90 --- Makefile | 14 +- x/interchainstaking/keeper/fuzz_test.go | 350 ++++++++++++++++++++++++ 2 files changed, 357 insertions(+), 7 deletions(-) create mode 100644 x/interchainstaking/keeper/fuzz_test.go diff --git a/Makefile b/Makefile index 59ed8d6a8..edc20b2d4 100755 --- a/Makefile +++ b/Makefile @@ -316,13 +316,13 @@ test-unit-cover: TEST_PACKAGES=$(PACKAGES_UNIT) run-tests: ifneq (,$(shell which tparse 2>/dev/null)) - go test -mod=readonly -json $(ARGS) $(EXTRA_ARGS) $(TEST_PACKAGES) | tparse + go test -short -mod=readonly -json $(ARGS) $(EXTRA_ARGS) $(TEST_PACKAGES) | tparse else - go test -mod=readonly $(ARGS) $(EXTRA_ARGS) $(TEST_PACKAGES) + go test -short -mod=readonly $(ARGS) $(EXTRA_ARGS) $(TEST_PACKAGES) endif test-import: - @go test ./tests/importer -v --vet=off --run=TestImportBlocks --datadir tmp \ + @go test -short ./tests/importer -v --vet=off --run=TestImportBlocks --datadir tmp \ --blockchain blockchain rm -rf tests/importer/tmp @@ -336,13 +336,13 @@ test-rpc-pending: test-sim-nondeterminism: @echo "Running non-determinism test..." - @go test -mod=readonly $(SIMAPP) -run TestAppStateDeterminism -Enabled=true \ + @go test -short -mod=readonly $(SIMAPP) -run TestAppStateDeterminism -Enabled=true \ -NumBlocks=100 -BlockSize=200 -Commit=true -Period=0 -v -timeout 24h test-sim-custom-genesis-fast: @echo "Running custom genesis simulation..." @echo "By default, ${HOME}/.$(QS_DIR)/config/genesis.json will be used." - @go test -mod=readonly $(SIMAPP) -run TestFullAppSimulation -Genesis=${HOME}/.$(QS_DIR)/config/genesis.json \ + @go test -short -mod=readonly $(SIMAPP) -run TestFullAppSimulation -Genesis=${HOME}/.$(QS_DIR)/config/genesis.json \ -Enabled=true -NumBlocks=100 -BlockSize=200 -Commit=true -Seed=99 -Period=5 -v -timeout 24h test-sim-import-export: runsim @@ -368,7 +368,7 @@ test-sim-multi-seed-short: runsim test-sim-benchmark-invariants: @echo "Running simulation invariant benchmarks..." - @go test -mod=readonly $(SIMAPP) -benchmem -bench=BenchmarkInvariants -run=^$ \ + @go test -short -mod=readonly $(SIMAPP) -benchmem -bench=BenchmarkInvariants -run=^$ \ -Enabled=true -NumBlocks=1000 -BlockSize=200 \ -Period=1 -Commit=true -Seed=57 -v -timeout 24h @@ -383,7 +383,7 @@ test-sim-multi-seed-long \ test-sim-benchmark-invariants benchmark: - @go test -mod=readonly -bench=. $(PACKAGES_NOSIMULATION) + @go test -short -mod=readonly -bench=. $(PACKAGES_NOSIMULATION) .PHONY: benchmark ############################################################################### diff --git a/x/interchainstaking/keeper/fuzz_test.go b/x/interchainstaking/keeper/fuzz_test.go new file mode 100644 index 000000000..de03f769a --- /dev/null +++ b/x/interchainstaking/keeper/fuzz_test.go @@ -0,0 +1,350 @@ +package keeper_test + +import ( + "bytes" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/ingenuity-build/quicksilver/utils" + icqtypes "github.com/ingenuity-build/quicksilver/x/interchainquery/types" + "github.com/ingenuity-build/quicksilver/x/interchainstaking/keeper" + icstypes "github.com/ingenuity-build/quicksilver/x/interchainstaking/types" +) + +type FuzzingTestSuite struct { + KeeperTestSuite +} + +func FuzzZoneInfos(f *testing.F) { + if testing.Short() { + f.Skip("In -short mode") + } + + // 1. Generate the seeds. + suite := new(FuzzingTestSuite) + suite.SetT(new(testing.T)) + suite.SetupTest() + + suite.setupTestZones() + app := suite.GetQuicksilverApp(suite.chainA) + app.InterchainstakingKeeper.CallbackHandler().RegisterCallbacks() + + seeds := []*icstypes.QueryZonesInfoRequest{ + &icstypes.QueryZonesInfoRequest{}, + nil, + &icstypes.QueryZonesInfoRequest{ + Pagination: &query.PageRequest{}, + }, + &icstypes.QueryZonesInfoRequest{ + Pagination: &query.PageRequest{}, + }, + &icstypes.QueryZonesInfoRequest{ + Pagination: &query.PageRequest{ + Key: []byte("key"), + Offset: 10, + Reverse: true, + }, + }, + } + + for _, seed := range seeds { + bz, err := app.AppCodec().Marshal(seed) + suite.Require().NoError(err) + f.Add(bz) + } + + // 2. Now fuzz the code. + icsKeeper := suite.GetQuicksilverApp(suite.chainA).InterchainstakingKeeper + ctx := suite.chainA.GetContext() + + f.Fuzz(func(t *testing.T, reqBz []byte) { + switch str := string(reqBz); str { + // Manually skipping over known and reported vectors + // as we know they cause crashes. + case "\n\t\n\x01000 0(0", "\n\t\n\x03000 0(0": // https://github.com/ingenuity-build/quicksilver-incognito/issues/88 + return + case "\n\t\n\x01K\x10\x0000(0", "\n\t\n\x030D0 0(0", "\n\t\n\x0301000(0": + return + } + + suite := new(FuzzingTestSuite) + suite.SetT(new(testing.T)) + suite.SetupTest() + suite.setupTestZones() + app := suite.GetQuicksilverApp(suite.chainA) + app.InterchainstakingKeeper.CallbackHandler().RegisterCallbacks() + + req := new(icstypes.QueryZonesInfoRequest) + if err := app.AppCodec().Unmarshal(reqBz, req); err != nil { + // Do nothing with an invalid ZonesInfoRequest. + return + } + + if pag := req.Pagination; pag != nil && bytes.Count(pag.Key, []byte("0")) == len(pag.Key) { + // A case already seen. + return + } + _, _ = icsKeeper.ZoneInfos(ctx, req) + }) +} + +func TestInvalidPaginationForQueryZones(t *testing.T) { + t.Skip("Not yet fixed per https://github.com/ingenuity-build/quicksilver-incognito/issues/88") + + suite := new(FuzzingTestSuite) + suite.SetT(t) + suite.SetupTest() + suite.setupTestZones() + app := suite.GetQuicksilverApp(suite.chainA) + app.InterchainstakingKeeper.CallbackHandler().RegisterCallbacks() + icsKeeper := suite.GetQuicksilverApp(suite.chainA).InterchainstakingKeeper + ctx := suite.chainA.GetContext() + + reqBz := []byte("\n\t\n\x03000 0(0") + req := new(icstypes.QueryZonesInfoRequest) + if err := app.AppCodec().Unmarshal(reqBz, req); err != nil { + // Do nothing with an invalid ZonesInfoRequest. + return + } + + _, _ = icsKeeper.ZoneInfos(ctx, req) +} + +func FuzzValsetCallback(f *testing.F) { + // 1. Generate the seeds. + newVal := utils.GenerateValAddressForTest() + valSetFuncs := []func(in stakingtypes.Validators) stakingtypes.QueryValidatorsResponse{ + func(in stakingtypes.Validators) stakingtypes.QueryValidatorsResponse { + new := in[0] + new.OperatorAddress = newVal.String() + in = append(in, new) + return stakingtypes.QueryValidatorsResponse{Validators: in} + }, + func(in stakingtypes.Validators) stakingtypes.QueryValidatorsResponse { + in[1].DelegatorShares = in[1].DelegatorShares.Add(sdk.NewDec(1000)) + in[2].DelegatorShares = in[2].DelegatorShares.Add(sdk.NewDec(2000)) + return stakingtypes.QueryValidatorsResponse{Validators: in} + }, + func(in stakingtypes.Validators) stakingtypes.QueryValidatorsResponse { + in[0].Tokens = in[0].Tokens.Add(sdk.NewInt(1000)) + return stakingtypes.QueryValidatorsResponse{Validators: in} + }, + func(in stakingtypes.Validators) stakingtypes.QueryValidatorsResponse { + in[1].Tokens = in[1].Tokens.Add(sdk.NewInt(1000)) + in[2].Tokens = in[2].Tokens.Add(sdk.NewInt(2000)) + return stakingtypes.QueryValidatorsResponse{Validators: in} + }, + func(in stakingtypes.Validators) stakingtypes.QueryValidatorsResponse { + in[1].Tokens = in[1].Tokens.Sub(sdk.NewInt(10)) + in[2].Tokens = in[2].Tokens.Sub(sdk.NewInt(20)) + return stakingtypes.QueryValidatorsResponse{Validators: in} + }, + func(in stakingtypes.Validators) stakingtypes.QueryValidatorsResponse { + in[0].Commission.CommissionRates.Rate = sdk.NewDecWithPrec(5, 1) + in[2].Commission.CommissionRates.Rate = sdk.NewDecWithPrec(5, 2) + return stakingtypes.QueryValidatorsResponse{Validators: in} + }, + func(in stakingtypes.Validators) stakingtypes.QueryValidatorsResponse { + new := in[0] + new.OperatorAddress = newVal.String() + in = append(in, new) + return stakingtypes.QueryValidatorsResponse{Validators: in} + }, + } + + suite := new(FuzzingTestSuite) + suite.SetT(new(testing.T)) + suite.SetupTest() + suite.setupTestZones() + + for _, valFunc := range valSetFuncs { + // 1.5. Set up a fresh test suite given that valFunc can mutate inputs. + chainBVals := suite.GetQuicksilverApp(suite.chainB).StakingKeeper.GetValidators(suite.chainB.GetContext(), 300) + query := valFunc(chainBVals) + app := suite.GetQuicksilverApp(suite.chainA) + bz, err := app.AppCodec().Marshal(&query) + suite.Require().NoError(err) + f.Add(bz) + } + + // 2. Now fuzz. + f.Fuzz(func(t *testing.T, args []byte) { + suite.SetT(t) + suite.FuzzValsetCallback(t, args) + }) +} + +func FuzzDelegationsCallback(f *testing.F) { + // 1. Add the samples firstly. + suite := new(FuzzingTestSuite) + suite.SetT(new(testing.T)) + suite.SetupTest() + suite.setupTestZones() + + app := suite.GetQuicksilverApp(suite.chainA) + app.InterchainstakingKeeper.CallbackHandler().RegisterCallbacks() + + // 1.5. Create the queries. + zone, ok := app.InterchainstakingKeeper.GetZone(suite.chainA.GetContext(), suite.chainB.ChainID) + if !ok { + f.Fatalf("Could not retrieve zone for chainB: %q", suite.chainB.ChainID) + } + var queries []*stakingtypes.QueryDelegatorDelegationsRequest + for _, addr := range []string{zone.DepositAddress.Address, zone.WithdrawalAddress.Address} { + accAddr, err := sdk.AccAddressFromBech32(addr) + suite.Require().NoError(err) + queries = append(queries, &stakingtypes.QueryDelegatorDelegationsRequest{ + DelegatorAddr: accAddr.String(), + }) + } + + for _, query := range queries { + bz, err := app.AppCodec().Marshal(query) + suite.Require().NoError(err) + f.Add(bz) + } + + f.Fuzz(func(t *testing.T, args []byte) { + suite.SetT(t) + suite.FuzzDelegationsCallback(t, args) + }) +} + +func FuzzAccountBalanceCallback(f *testing.F) { + // 1. Add the samples firstly. + suite := new(FuzzingTestSuite) + suite.SetT(new(testing.T)) + suite.SetupTest() + suite.setupTestZones() + + app := suite.GetQuicksilverApp(suite.chainA) + + values := []int64{10, 0, 100, 1_000, 1_000_000} + for _, val := range values { + response := sdk.NewCoin("qck", sdk.NewInt(val)) + respbz, err := app.AppCodec().Marshal(&response) + suite.Require().NoError(err) + f.Add(respbz) + } + + // 2. Now fuzz. + f.Fuzz(func(t *testing.T, respbz []byte) { + suite.SetT(t) + suite.FuzzAccountBalanceCallback(t, respbz) + }) +} + +func FuzzAllBalancesCallback(f *testing.F) { + // 1. Add the samples firstly. + suite := new(FuzzingTestSuite) + suite.SetT(new(testing.T)) + suite.SetupTest() + suite.setupTestZones() + + // 1. Add corpus from chainA. + app := suite.GetQuicksilverApp(suite.chainA) + zone, ok := app.InterchainstakingKeeper.GetZone(suite.chainA.GetContext(), suite.chainB.ChainID) + if !ok { + f.Fatalf("Could not retrieve zone for chainB: %q", suite.chainB.ChainID) + } + reqbz, err := app.AppCodec().Marshal(&banktypes.QueryAllBalancesRequest{ + Address: zone.DepositAddress.Address, + }) + suite.Require().NoError(err) + f.Add(reqbz) + + if false { + // 2. Add corpus from chainB. + app = suite.GetQuicksilverApp(suite.chainB) + zone, ok = app.InterchainstakingKeeper.GetZone(suite.chainB.GetContext(), suite.chainA.ChainID) + if !ok { + f.Fatalf("Could not retrieve zone for chainA: %q", suite.chainA.ChainID) + } + reqbz, err = app.AppCodec().Marshal(&banktypes.QueryAllBalancesRequest{ + Address: zone.DepositAddress.Address, + }) + suite.Require().NoError(err) + f.Add(reqbz) + } + + // 3. Now fuzz. + f.Fuzz(func(t *testing.T, respbz []byte) { + suite.SetT(t) + suite.FuzzAllBalancesCallback(t, respbz) + }) +} + +func (s *FuzzingTestSuite) FuzzAccountBalanceCallback(t *testing.T, respbz []byte) { + if testing.Short() { + t.Skip("In -short mode") + } + + app := s.GetQuicksilverApp(s.chainA) + app.InterchainstakingKeeper.CallbackHandler().RegisterCallbacks() + ctx := s.chainA.GetContext() + + zone, _ := app.InterchainstakingKeeper.GetZone(ctx, s.chainB.ChainID) + zone.DepositAddress.BalanceWaitgroup++ + zone.WithdrawalAddress.BalanceWaitgroup++ + app.InterchainstakingKeeper.SetZone(ctx, &zone) + + for _, addr := range []string{zone.DepositAddress.Address, zone.WithdrawalAddress.Address} { + accAddr, err := sdk.AccAddressFromBech32(addr) + s.Require().NoError(err) + data := append(banktypes.CreateAccountBalancesPrefix(accAddr), []byte("stake")...) + + _ = keeper.AccountBalanceCallback(app.InterchainstakingKeeper, ctx, respbz, icqtypes.Query{ChainId: s.chainB.ChainID, Request: data}) + } +} + +func (s *FuzzingTestSuite) FuzzDelegationsCallback(t *testing.T, respbz []byte) { + if testing.Short() { + t.Skip("In -short mode") + } + + app := s.GetQuicksilverApp(s.chainA) + app.InterchainstakingKeeper.CallbackHandler().RegisterCallbacks() + ctx := s.chainA.GetContext() + + zone, _ := app.InterchainstakingKeeper.GetZone(ctx, s.chainB.ChainID) + query := stakingtypes.QueryDelegatorDelegationsRequest{ + DelegatorAddr: zone.DelegationAddress.Address, + } + reqbz, err := app.AppCodec().Marshal(&query) + s.Require().NoError(err) + + _ = keeper.DelegationsCallback(app.InterchainstakingKeeper, ctx, respbz, icqtypes.Query{ChainId: s.chainB.ChainID, Request: reqbz}) +} + +func (s *FuzzingTestSuite) FuzzAllBalancesCallback(t *testing.T, respbz []byte) { + if testing.Short() { + t.Skip("In -short mode") + } + + app := s.GetQuicksilverApp(s.chainA) + app.InterchainstakingKeeper.CallbackHandler().RegisterCallbacks() + ctx := s.chainA.GetContext() + + zone, _ := app.InterchainstakingKeeper.GetZone(ctx, s.chainB.ChainID) + + query := banktypes.QueryAllBalancesRequest{ + Address: zone.DepositAddress.Address, + } + reqbz, err := app.AppCodec().Marshal(&query) + s.Require().NoError(err) + + err = keeper.AllBalancesCallback(app.InterchainstakingKeeper, ctx, respbz, icqtypes.Query{ChainId: s.chainB.ChainID, Request: reqbz}) + s.Require().NoError(err) +} + +func (s *FuzzingTestSuite) FuzzValsetCallback(t *testing.T, args []byte) { + app := s.GetQuicksilverApp(s.chainA) + app.InterchainstakingKeeper.CallbackHandler().RegisterCallbacks() + ctx := s.chainA.GetContext() + + _ = keeper.ValsetCallback(app.InterchainstakingKeeper, ctx, args, icqtypes.Query{ChainId: s.chainB.ChainID}) +}