diff --git a/x/committee/genesis.go b/x/committee/genesis.go index fef015c9..cc0b9409 100644 --- a/x/committee/genesis.go +++ b/x/committee/genesis.go @@ -4,6 +4,7 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/committee/types" ) // InitGenesis initializes the store state from a genesis state. @@ -14,11 +15,44 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, gs GenesisState) { keeper.SetNextProposalID(ctx, gs.NextProposalID) - // TODO set votes, committee, proposals + for _, com := range gs.Committees { + keeper.SetCommittee(ctx, com) + } + for _, p := range gs.Proposals { + keeper.SetProposal(ctx, p) + } + for _, v := range gs.Votes { + keeper.SetVote(ctx, v) + } } // ExportGenesis returns a GenesisState for a given context and keeper. func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { - // TODO - return GenesisState{} + + nextID, err := keeper.GetNextProposalID(ctx) + if err != nil { + panic(err) + } + committees := []types.Committee{} + keeper.IterateCommittees(ctx, func(com types.Committee) bool { + committees = append(committees, com) + return false + }) + proposals := []types.Proposal{} + votes := []types.Vote{} + keeper.IterateProposals(ctx, func(p types.Proposal) bool { + proposals = append(proposals, p) + keeper.IterateVotes(ctx, p.ID, func(v types.Vote) bool { + votes = append(votes, v) + return false + }) + return false + }) + + return types.NewGenesisState( + nextID, + committees, + proposals, + votes, + ) } diff --git a/x/committee/genesis_test.go b/x/committee/genesis_test.go new file mode 100644 index 00000000..c64664eb --- /dev/null +++ b/x/committee/genesis_test.go @@ -0,0 +1,75 @@ +package committee_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/committee" + "github.com/kava-labs/kava/x/committee/types" +) + +type GenesisTestSuite struct { + suite.Suite + + app app.TestApp + ctx sdk.Context + keeper committee.Keeper +} + +func (suite *GenesisTestSuite) TestGenesis() { + testCases := []struct { + name string + genState types.GenesisState + expectPass bool + }{ + { + name: "normal", + genState: types.DefaultGenesisState(), + expectPass: true, + }, + { + name: "invalid", + genState: types.NewGenesisState( + 2, + []types.Committee{}, + []types.Proposal{{ID: 1, CommitteeID: 57}}, + []types.Vote{}, + ), + expectPass: false, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + // Setup (note: suite.SetupTest is not run before every suite.Run) + suite.app = app.NewTestApp() + suite.keeper = suite.app.GetCommitteeKeeper() + suite.ctx = suite.app.NewContext(true, abci.Header{}) + + // Run + var exportedGenState types.GenesisState + run := func() { + committee.InitGenesis(suite.ctx, suite.keeper, tc.genState) + exportedGenState = committee.ExportGenesis(suite.ctx, suite.keeper) + } + if tc.expectPass { + suite.NotPanics(run) + } else { + suite.Panics(run) + } + + // Check + if tc.expectPass { + suite.Equal(tc.genState, exportedGenState) + } + }) + } +} + +func TestGenesisTestSuite(t *testing.T) { + suite.Run(t, new(GenesisTestSuite)) +} diff --git a/x/committee/types/genesis.go b/x/committee/types/genesis.go index c44b14c6..a259f228 100644 --- a/x/committee/types/genesis.go +++ b/x/committee/types/genesis.go @@ -2,6 +2,7 @@ package types import ( "bytes" + "fmt" ) // DefaultNextProposalID is the starting poiint for proposal IDs. @@ -10,18 +11,18 @@ const DefaultNextProposalID uint64 = 1 // GenesisState is state that must be provided at chain genesis. type GenesisState struct { NextProposalID uint64 - Votes []Vote - Proposals []Proposal Committees []Committee + Proposals []Proposal + Votes []Vote } // NewGenesisState returns a new genesis state object for the module. -func NewGenesisState(nextProposalID uint64, votes []Vote, proposals []Proposal, committees []Committee) GenesisState { +func NewGenesisState(nextProposalID uint64, committees []Committee, proposals []Proposal, votes []Vote) GenesisState { return GenesisState{ NextProposalID: nextProposalID, - Votes: votes, - Proposals: proposals, Committees: committees, + Proposals: proposals, + Votes: votes, } } @@ -29,9 +30,9 @@ func NewGenesisState(nextProposalID uint64, votes []Vote, proposals []Proposal, func DefaultGenesisState() GenesisState { return NewGenesisState( DefaultNextProposalID, - []Vote{}, - []Proposal{}, []Committee{}, + []Proposal{}, + []Vote{}, ) } @@ -48,4 +49,62 @@ func (data GenesisState) IsEmpty() bool { } // Validate performs basic validation of genesis data. -func (gs GenesisState) Validate() error { return nil } +func (gs GenesisState) Validate() error { + // validate committees + committeeMap := make(map[uint64]bool, len(gs.Committees)) + for _, com := range gs.Committees { + // check there are no duplicate IDs + if _, ok := committeeMap[com.ID]; ok { + return fmt.Errorf("duplicate committee ID found in genesis state; id: %d", com.ID) + } + committeeMap[com.ID] = true + + // validate committee + if len(com.Members) == 0 { + return fmt.Errorf("committee %d invalid: cannot have zero members", com.ID) + } + for _, m := range com.Members { + if m.Empty() { + return fmt.Errorf("committee %d invalid: found empty member address", com.ID) + } + } + } + + // validate proposals - pp.Val, no duplicate IDs, no ids >= nextID, committee needs to exist + proposalMap := make(map[uint64]bool, len(gs.Proposals)) + for _, p := range gs.Proposals { + // check there are no duplicate IDs + if _, ok := proposalMap[p.ID]; ok { + return fmt.Errorf("duplicate proposal ID found in genesis state; id: %d", p.ID) + } + proposalMap[p.ID] = true + + // validate next proposal ID + if p.ID >= gs.NextProposalID { + return fmt.Errorf("NextProposalID is not greater than all proposal IDs; id: %d", p.ID) + } + + // check committee exists + if !committeeMap[p.CommitteeID] { + return fmt.Errorf("proposal refers to non existant committee; proposal: %+v", p) + } + + // validate pubProposal + if err := p.PubProposal.ValidateBasic(); err != nil { + return fmt.Errorf("proposal %d invalid: %w", p.ID, err) + } + } + + // validate votes + for _, v := range gs.Votes { + // check proposal exists + if !proposalMap[v.ProposalID] { + return fmt.Errorf("vote refers to non existant proposal; vote: %+v", v) + } + // validate address + if v.Voter.Empty() { + return fmt.Errorf("found empty voter address; vote: %+v", v) + } + } + return nil +} diff --git a/x/committee/types/genesis_test.go b/x/committee/types/genesis_test.go new file mode 100644 index 00000000..3a9835ee --- /dev/null +++ b/x/committee/types/genesis_test.go @@ -0,0 +1,36 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGenesisState_Validate(t *testing.T) { + testCases := []struct { + name string + genState GenesisState + expectPass bool + }{ + { + name: "normal", + genState: DefaultGenesisState(), + expectPass: true, + }, + // TODO test failure cases + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + err := tc.genState.Validate() + + if tc.expectPass { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } + +}