Skip to content

Commit

Permalink
ibc/02-client: import export GenesisState (#6073)
Browse files Browse the repository at this point in the history
* ibc/02-client: import export GenesisState

* client validation

* genesis validation

* ibc genesis tests

* GetAllConsensusStates test

* fix non-determinism

* lint

* fix test
  • Loading branch information
fedekunze committed Apr 27, 2020
1 parent f0b72b9 commit 9b51908
Show file tree
Hide file tree
Showing 18 changed files with 616 additions and 37 deletions.
10 changes: 8 additions & 2 deletions x/ibc/02-client/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ var (
ErrRootNotFound = types.ErrRootNotFound
ErrInvalidHeader = types.ErrInvalidHeader
ErrInvalidEvidence = types.ErrInvalidEvidence
DefaultGenesisState = types.DefaultGenesisState
NewGenesisState = types.NewGenesisState
NewClientConsensusStates = types.NewClientConsensusStates

// variable aliases
SubModuleCdc = types.SubModuleCdc
Expand All @@ -45,7 +48,10 @@ var (
AttributeValueCategory = types.AttributeValueCategory
)

// nolint
type (
Keeper = keeper.Keeper
StakingKeeper = types.StakingKeeper
Keeper = keeper.Keeper
StakingKeeper = types.StakingKeeper
GenesisState = types.GenesisState
ClientConsensusStates = types.ClientConsensusStates
)
1 change: 1 addition & 0 deletions x/ibc/02-client/exported/exported.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type ClientState interface {
ClientType() ClientType
GetLatestHeight() uint64
IsFrozen() bool
Validate() error

// State verification functions

Expand Down
27 changes: 27 additions & 0 deletions x/ibc/02-client/genesis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package client

import (
sdk "github.com/cosmos/cosmos-sdk/types"
)

// InitGenesis initializes the ibc client submodule's state from a provided genesis
// state.
func InitGenesis(ctx sdk.Context, k Keeper, gs GenesisState) {
for _, client := range gs.Clients {
k.SetClientState(ctx, client)
k.SetClientType(ctx, client.GetID(), client.ClientType())
}
for _, cs := range gs.ClientsConsensus {
for _, consState := range cs.ConsensusStates {
k.SetClientConsensusState(ctx, cs.ClientID, consState.GetHeight(), consState)
}
}
}

// ExportGenesis returns the ibc client submodule's exported genesis.
func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState {
return GenesisState{
Clients: k.GetAllClients(ctx),
ClientsConsensus: k.GetAllConsensusStates(ctx),
}
}
61 changes: 61 additions & 0 deletions x/ibc/02-client/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,67 @@ func (k Keeper) SetClientConsensusState(ctx sdk.Context, clientID string, height
store.Set(ibctypes.KeyConsensusState(height), bz)
}

// IterateConsensusStates provides an iterator over all stored consensus states.
// objects. For each State object, cb will be called. If the cb returns true,
// the iterator will close and stop.
func (k Keeper) IterateConsensusStates(ctx sdk.Context, cb func(clientID string, cs exported.ConsensusState) bool) {
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStorePrefixIterator(store, ibctypes.KeyClientStorePrefix)

defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
keySplit := strings.Split(string(iterator.Key()), "/")
// consensus key is in the format "clients/<clientID>/consensusState/<height>"
if len(keySplit) != 4 || keySplit[2] != "consensusState" {
continue
}
clientID := keySplit[1]
var consensusState exported.ConsensusState
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &consensusState)

if cb(clientID, consensusState) {
break
}
}
}

// GetAllConsensusStates returns all stored client consensus states.
// NOTE: non deterministic.
func (k Keeper) GetAllConsensusStates(ctx sdk.Context) (clientConsStates []types.ClientConsensusStates) {
var clientIDs []string
// create map to add consensus states to the existing clients
cons := make(map[string][]exported.ConsensusState)

k.IterateConsensusStates(ctx, func(clientID string, cs exported.ConsensusState) bool {
consensusStates, ok := cons[clientID]
if !ok {
clientIDs = append(clientIDs, clientID)
cons[clientID] = []exported.ConsensusState{cs}
return false
}

cons[clientID] = append(consensusStates, cs)
return false
})

// create ClientConsensusStates in the same order of iteration to prevent non-determinism
for len(clientIDs) > 0 {
id := clientIDs[len(clientIDs)-1]
consensusStates, ok := cons[id]
if !ok {
panic(fmt.Sprintf("consensus states from client id %s not found", id))
}

clientConsState := types.NewClientConsensusStates(id, consensusStates)
clientConsStates = append(clientConsStates, clientConsState)

// remove the last element
clientIDs = clientIDs[:len(clientIDs)-1]
}

return clientConsStates
}

// HasClientConsensusState returns if keeper has a ConsensusState for a particular
// client at the given height
func (k Keeper) HasClientConsensusState(ctx sdk.Context, clientID string, height uint64) bool {
Expand Down
39 changes: 38 additions & 1 deletion x/ibc/02-client/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported"
"github.com/cosmos/cosmos-sdk/x/ibc/02-client/keeper"
"github.com/cosmos/cosmos-sdk/x/ibc/02-client/types"
ibctmtypes "github.com/cosmos/cosmos-sdk/x/ibc/07-tendermint/types"
commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types"
"github.com/cosmos/cosmos-sdk/x/staking"
Expand Down Expand Up @@ -161,7 +162,9 @@ func (suite KeeperTestSuite) TestGetConsensusState() {

func (suite KeeperTestSuite) TestConsensusStateHelpers() {
// initial setup
clientState, _ := ibctmtypes.Initialize(testClientID, trustingPeriod, ubdPeriod, maxClockDrift, suite.header)
clientState, err := ibctmtypes.Initialize(testClientID, trustingPeriod, ubdPeriod, maxClockDrift, suite.header)
suite.Require().NoError(err)

suite.keeper.SetClientState(suite.ctx, clientState)
suite.keeper.SetClientConsensusState(suite.ctx, testClientID, testClientHeight, suite.consensusState)

Expand Down Expand Up @@ -192,3 +195,37 @@ func (suite KeeperTestSuite) TestConsensusStateHelpers() {
suite.Require().True(ok)
suite.Require().Equal(suite.consensusState, lte, "LTE helper function did not return latest client state below height: %d", testClientHeight+3)
}

func (suite KeeperTestSuite) TestGetAllConsensusStates() {
expConsensus := []types.ClientConsensusStates{
types.NewClientConsensusStates(
testClientID,
[]exported.ConsensusState{
ibctmtypes.NewConsensusState(
suite.consensusState.Timestamp, commitmenttypes.NewMerkleRoot([]byte("hash")), suite.consensusState.GetHeight(), &tmtypes.ValidatorSet{},
),
ibctmtypes.NewConsensusState(
suite.consensusState.Timestamp.Add(time.Minute), commitmenttypes.NewMerkleRoot([]byte("app_hash")), suite.consensusState.GetHeight()+1, &tmtypes.ValidatorSet{},
),
},
),
types.NewClientConsensusStates(
testClientID2,
[]exported.ConsensusState{
ibctmtypes.NewConsensusState(
suite.consensusState.Timestamp.Add(2*time.Minute), commitmenttypes.NewMerkleRoot([]byte("app_hash_2")), suite.consensusState.GetHeight()+2, &tmtypes.ValidatorSet{},
),
},
),
}

for i := range expConsensus {
for _, cons := range expConsensus[i].ConsensusStates {
suite.keeper.SetClientConsensusState(suite.ctx, expConsensus[i].ClientID, cons.GetHeight(), cons)
}
}

consStates := suite.keeper.GetAllConsensusStates(suite.ctx)
suite.Require().Len(consStates, len(expConsensus))
suite.Require().Equal(expConsensus, consStates)
}
69 changes: 69 additions & 0 deletions x/ibc/02-client/types/genesis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package types

import (
"fmt"

"github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported"
host "github.com/cosmos/cosmos-sdk/x/ibc/24-host"
)

// ClientConsensusStates defines all the stored consensus states for a given client.
type ClientConsensusStates struct {
ClientID string `json:"client_id" yaml:"client_id"`
ConsensusStates []exported.ConsensusState `json:"consensus_states" yaml:"consensus_states"`
}

// NewClientConsensusStates creates a new ClientConsensusStates instance.
func NewClientConsensusStates(id string, states []exported.ConsensusState) ClientConsensusStates {
return ClientConsensusStates{
ClientID: id,
ConsensusStates: states,
}
}

// GenesisState defines the ibc client submodule's genesis state.
type GenesisState struct {
Clients []exported.ClientState `json:"clients" yaml:"clients"`
ClientsConsensus []ClientConsensusStates `json:"clients_consensus" yaml:"clients_consensus"`
}

// NewGenesisState creates a GenesisState instance.
func NewGenesisState(
clients []exported.ClientState, clientsConsensus []ClientConsensusStates,
) GenesisState {
return GenesisState{
Clients: clients,
ClientsConsensus: clientsConsensus,
}
}

// DefaultGenesisState returns the ibc client submodule's default genesis state.
func DefaultGenesisState() GenesisState {
return GenesisState{
Clients: []exported.ClientState{},
ClientsConsensus: []ClientConsensusStates{},
}
}

// Validate performs basic genesis state validation returning an error upon any
// failure.
func (gs GenesisState) Validate() error {
for i, client := range gs.Clients {
if err := client.Validate(); err != nil {
return fmt.Errorf("invalid client %d: %w", i, err)
}
}

for i, cs := range gs.ClientsConsensus {
if err := host.DefaultClientIdentifierValidator(cs.ClientID); err != nil {
return fmt.Errorf("invalid client consensus state %d: %w", i, err)
}
for _, consensusState := range cs.ConsensusStates {
if err := consensusState.ValidateBasic(); err != nil {
return fmt.Errorf("invalid client consensus state %d: %w", i, err)
}
}
}

return nil
}
135 changes: 135 additions & 0 deletions x/ibc/02-client/types/genesis_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package types_test

import (
"testing"
"time"

"github.com/stretchr/testify/require"

tmtypes "github.com/tendermint/tendermint/types"
dbm "github.com/tendermint/tm-db"

"github.com/cosmos/cosmos-sdk/store/cachekv"
"github.com/cosmos/cosmos-sdk/store/dbadapter"
"github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported"
"github.com/cosmos/cosmos-sdk/x/ibc/02-client/types"
ibctmtypes "github.com/cosmos/cosmos-sdk/x/ibc/07-tendermint/types"
localhosttypes "github.com/cosmos/cosmos-sdk/x/ibc/09-localhost/types"
commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types"
)

const (
clientID = "ethbridge"

trustingPeriod time.Duration = time.Hour * 24 * 7 * 2
ubdPeriod time.Duration = time.Hour * 24 * 7 * 3
maxClockDrift time.Duration = time.Second * 10
)

func TestValidateGenesis(t *testing.T) {
privVal := tmtypes.NewMockPV()
pubKey, err := privVal.GetPubKey()
require.NoError(t, err)

now := time.Now().UTC()

val := tmtypes.NewValidator(pubKey, 10)
valSet := tmtypes.NewValidatorSet([]*tmtypes.Validator{val})

mem := dbadapter.Store{DB: dbm.NewMemDB()}
store := cachekv.NewStore(mem)
header := ibctmtypes.CreateTestHeader("chainID", 10, now, valSet, []tmtypes.PrivValidator{privVal})

testCases := []struct {
name string
genState types.GenesisState
expPass bool
}{
{
name: "default",
genState: types.DefaultGenesisState(),
expPass: true,
},
{
name: "valid genesis",
genState: types.NewGenesisState(
[]exported.ClientState{
ibctmtypes.NewClientState(clientID, trustingPeriod, ubdPeriod, maxClockDrift, header),
localhosttypes.NewClientState(store, "chaindID", 10),
},
[]types.ClientConsensusStates{
{
clientID,
[]exported.ConsensusState{
ibctmtypes.NewConsensusState(
header.Time, commitmenttypes.NewMerkleRoot(header.AppHash), header.GetHeight(), header.ValidatorSet,
),
},
},
},
),
expPass: true,
},
{
name: "invalid client",
genState: types.NewGenesisState(
[]exported.ClientState{
ibctmtypes.NewClientState(clientID, trustingPeriod, ubdPeriod, maxClockDrift, header),
localhosttypes.NewClientState(store, "chaindID", 0),
},
nil,
),
expPass: false,
},
{
name: "invalid consensus state",
genState: types.NewGenesisState(
[]exported.ClientState{
ibctmtypes.NewClientState(clientID, trustingPeriod, ubdPeriod, maxClockDrift, header),
localhosttypes.NewClientState(store, "chaindID", 10),
},
[]types.ClientConsensusStates{
{
"CLIENTID2",
[]exported.ConsensusState{
ibctmtypes.NewConsensusState(
header.Time, commitmenttypes.NewMerkleRoot(header.AppHash), 0, header.ValidatorSet,
),
},
},
},
),
expPass: false,
},
{
name: "invalid consensus state",
genState: types.NewGenesisState(
[]exported.ClientState{
ibctmtypes.NewClientState(clientID, trustingPeriod, ubdPeriod, maxClockDrift, header),
localhosttypes.NewClientState(store, "chaindID", 10),
},
[]types.ClientConsensusStates{
types.NewClientConsensusStates(
clientID,
[]exported.ConsensusState{
ibctmtypes.NewConsensusState(
header.Time, commitmenttypes.NewMerkleRoot(header.AppHash), 0, header.ValidatorSet,
),
},
),
},
),
expPass: false,
},
}

for _, tc := range testCases {
tc := tc
err := tc.genState.Validate()
if tc.expPass {
require.NoError(t, err, tc.name)
} else {
require.Error(t, err, tc.name)
}
}
}
Loading

0 comments on commit 9b51908

Please sign in to comment.