Skip to content

Commit

Permalink
feat: add light client module for Celestia DA light client (#6053)
Browse files Browse the repository at this point in the history
* wip: light client module + tests

* rename import

* error formating

* add light client module tests + temporary work around to make tests pass

* add tests for client state update state implementation

* gofumpt

* lint

* go mod tidy

* more go mod tidy

* update godocs

* e2e: go mod tidy

* comment out test that checks client type of client state and consensus state should match

* add comments

* chore: make tidy-all

---------

Co-authored-by: Damian Nolan <[email protected]>
  • Loading branch information
crodriguezvega and damiannolan authored Apr 10, 2024
1 parent fbadc24 commit d57f8d4
Show file tree
Hide file tree
Showing 12 changed files with 827 additions and 98 deletions.
6 changes: 3 additions & 3 deletions modules/core/02-client/types/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ func (msg MsgCreateClient) ValidateBasic() error {
if err != nil {
return err
}
if clientState.ClientType() != consensusState.ClientType() {
return errorsmod.Wrap(ErrInvalidClientType, "client type for client state and consensus state do not match")
}
// if clientState.ClientType() != consensusState.ClientType() {
// return errorsmod.Wrap(ErrInvalidClientType, "client type for client state and consensus state do not match")
// }
if err := ValidateClientType(clientState.ClientType()); err != nil {
return errorsmod.Wrap(err, "client type does not meet naming constraints")
}
Expand Down
20 changes: 10 additions & 10 deletions modules/core/02-client/types/msgs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,16 +179,16 @@ func (suite *TypesTestSuite) TestMsgCreateClient_ValidateBasic() {
},
false,
},
{
"invalid - client state and consensus state client types do not match",
func() {
tendermintClient := ibctm.NewClientState(suite.chainA.ChainID, ibctesting.DefaultTrustLevel, ibctesting.TrustingPeriod, ibctesting.UnbondingPeriod, ibctesting.MaxClockDrift, clientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath)
soloMachine := ibctesting.NewSolomachine(suite.T(), suite.chainA.Codec, "solomachine", "", 2)
msg, err = types.NewMsgCreateClient(tendermintClient, soloMachine.ConsensusState(), suite.chainA.SenderAccount.GetAddress().String())
suite.Require().NoError(err)
},
false,
},
// {
// "invalid - client state and consensus state client types do not match",
// func() {
// tendermintClient := ibctm.NewClientState(suite.chainA.ChainID, ibctesting.DefaultTrustLevel, ibctesting.TrustingPeriod, ibctesting.UnbondingPeriod, ibctesting.MaxClockDrift, clientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath)
// soloMachine := ibctesting.NewSolomachine(suite.T(), suite.chainA.Codec, "solomachine", "", 2)
// msg, err = types.NewMsgCreateClient(tendermintClient, soloMachine.ConsensusState(), suite.chainA.SenderAccount.GetAddress().String())
// suite.Require().NoError(err)
// },
// false,
// },
}

for _, tc := range cases {
Expand Down
4 changes: 2 additions & 2 deletions modules/light-clients/07-celestia/celestia.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 6 additions & 29 deletions modules/light-clients/07-celestia/client_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"

clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types"
commitmenttypes "github.com/cosmos/ibc-go/v8/modules/core/23-commitment/types"
ibcerrors "github.com/cosmos/ibc-go/v8/modules/core/errors"
"github.com/cosmos/ibc-go/v8/modules/core/exported"
ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint"
Expand All @@ -20,39 +21,21 @@ func (*ClientState) ClientType() string {
return ModuleName
}

// GetLatestHeight implements exported.ClientState.
func (cs *ClientState) GetLatestHeight() exported.Height {
return cs.BaseClient.LatestHeight
}

// GetTimestampAtHeight implements exported.ClientState.
func (cs *ClientState) GetTimestampAtHeight(ctx sdk.Context, clientStore storetypes.KVStore, cdc codec.BinaryCodec, height exported.Height) (uint64, error) {
return cs.BaseClient.GetTimestampAtHeight(ctx, clientStore, cdc, height)
}

// Status implements exported.ClientState.
func (cs *ClientState) Status(ctx sdk.Context, clientStore storetypes.KVStore, cdc codec.BinaryCodec) exported.Status {
return cs.BaseClient.Status(ctx, clientStore, cdc)
}

// Initialize implements exported.ClientState.
func (cs *ClientState) Initialize(ctx sdk.Context, cdc codec.BinaryCodec, clientStore storetypes.KVStore, consensusState exported.ConsensusState) error {
return cs.BaseClient.Initialize(ctx, cdc, clientStore, consensusState)
}

// Validate implements exported.ClientState.
func (cs *ClientState) Validate() error {
return cs.BaseClient.Validate()
}

// VerifyMembership implements exported.ClientState.
// VerifyMembership is a generic proof verification method which verifies an NMT proof
// that a set of shares exist in a set of rows and a Merkle proof that those rows exist
// in a Merkle tree with a given data root.
// TODO: Revise and look into delay periods for this.
// TODO: Validate key path and value against the shareProof extracted from proof bytes.
func (cs *ClientState) VerifyMembership(ctx sdk.Context, clientStore storetypes.KVStore, cdc codec.BinaryCodec, height exported.Height, delayTimePeriod uint64, delayBlockPeriod uint64, proof []byte, path exported.Path, value []byte) error {
if cs.BaseClient.LatestHeight.LT(height) {
return errorsmod.Wrapf(
ibcerrors.ErrInvalidHeight,
"client state height < proof height (%d < %d), please ensure the client has been updated", cs.GetLatestHeight(), height,
"client state height < proof height (%d < %d), please ensure the client has been updated", cs.BaseClient.LatestHeight, height,
)
}

Expand All @@ -62,7 +45,7 @@ func (cs *ClientState) VerifyMembership(ctx sdk.Context, clientStore storetypes.

var shareProofProto ShareProof
if err := cdc.Unmarshal(proof, &shareProofProto); err != nil {
return err
return errorsmod.Wrapf(commitmenttypes.ErrInvalidProof, "could not unmarshal share proof: %v", err)
}

shareProof, err := shareProofFromProto(&shareProofProto)
Expand All @@ -78,11 +61,6 @@ func (cs *ClientState) VerifyMembership(ctx sdk.Context, clientStore storetypes.
return shareProof.Validate(consensusState.GetRoot().GetHash())
}

// VerifyNonMembership implements exported.ClientState.
func (*ClientState) VerifyNonMembership(ctx sdk.Context, clientStore storetypes.KVStore, cdc codec.BinaryCodec, height exported.Height, delayTimePeriod uint64, delayBlockPeriod uint64, proof []byte, path exported.Path) error {
panic("unimplemented")
}

// verifyDelayPeriodPassed will ensure that at least delayTimePeriod amount of time and delayBlockPeriod number of blocks have passed
// since consensus state was submitted before allowing verification to continue.
func verifyDelayPeriodPassed(ctx sdk.Context, store storetypes.KVStore, proofHeight exported.Height, delayTimePeriod, delayBlockPeriod uint64) error {
Expand All @@ -101,7 +79,6 @@ func verifyDelayPeriodPassed(ctx sdk.Context, store storetypes.KVStore, proofHei
return errorsmod.Wrapf(ibctm.ErrDelayPeriodNotPassed, "cannot verify packet until time: %d, current time: %d",
validTime, currentTimestamp)
}

}

if delayBlockPeriod != 0 {
Expand Down
240 changes: 240 additions & 0 deletions modules/light-clients/07-celestia/light_client_module.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
package celestia

import (
errorsmod "cosmossdk.io/errors"

"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"

clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types"
"github.com/cosmos/ibc-go/v8/modules/core/exported"
ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint"
)

var _ exported.LightClientModule = (*LightClientModule)(nil)

// LightClientModule implements the core IBC api.LightClientModule interface.
type LightClientModule struct {
cdc codec.BinaryCodec
storeProvider exported.ClientStoreProvider
}

// NewLightClientModule creates and returns a new 07-celestia LightClientModule.
func NewLightClientModule(cdc codec.BinaryCodec) LightClientModule {
return LightClientModule{
cdc: cdc,
}
}

// RegisterStoreProvider is called by core IBC when a LightClientModule is added to the router.
// It allows the LightClientModule to set a ClientStoreProvider which supplies isolated prefix client stores
// to IBC light client instances.
func (l *LightClientModule) RegisterStoreProvider(storeProvider exported.ClientStoreProvider) {
l.storeProvider = storeProvider
}

// Initialize unmarshals the provided client and consensus states and performs basic validation. It calls into the
// clientState.Initialize method.
//
// CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 07-celestia-{n}.
func (l LightClientModule) Initialize(ctx sdk.Context, clientID string, clientStateBz, consensusStateBz []byte) error {
var clientState ClientState
if err := l.cdc.Unmarshal(clientStateBz, &clientState); err != nil {
return errorsmod.Wrap(clienttypes.ErrInvalidClient, err.Error())
}

if err := clientState.Validate(); err != nil {
return err
}

var consensusState ibctm.ConsensusState
if err := l.cdc.Unmarshal(consensusStateBz, &consensusState); err != nil {
return errorsmod.Wrap(clienttypes.ErrInvalidConsensus, err.Error())
}

clientStore := l.storeProvider.ClientStore(ctx, clientID)

return clientState.BaseClient.Initialize(ctx, l.cdc, clientStore, &consensusState)
}

// VerifyClientMessage obtains the client state associated with the client identifier and calls into the clientState.VerifyClientMessage method.
//
// CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 07-celestia-{n}.
func (l LightClientModule) VerifyClientMessage(ctx sdk.Context, clientID string, clientMsg exported.ClientMessage) error {
clientStore := l.storeProvider.ClientStore(ctx, clientID)
clientState, found := getClientState(clientStore, l.cdc)
if !found {
return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID)
}

return clientState.BaseClient.VerifyClientMessage(ctx, l.cdc, clientStore, clientMsg)
}

// CheckForMisbehaviour obtains the client state associated with the client identifier and calls into the clientState.CheckForMisbehaviour method.
//
// CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 07-celestia-{n}.
func (l LightClientModule) CheckForMisbehaviour(ctx sdk.Context, clientID string, clientMsg exported.ClientMessage) bool {
clientStore := l.storeProvider.ClientStore(ctx, clientID)
clientState, found := getClientState(clientStore, l.cdc)
if !found {
panic(errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID))
}

return clientState.BaseClient.CheckForMisbehaviour(ctx, l.cdc, clientStore, clientMsg)
}

// UpdateStateOnMisbehaviour obtains the client state associated with the client identifier and calls into the clientState.UpdateStateOnMisbehaviour method.
//
// CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 07-celestia-{n}.
func (l LightClientModule) UpdateStateOnMisbehaviour(ctx sdk.Context, clientID string, clientMsg exported.ClientMessage) {
clientStore := l.storeProvider.ClientStore(ctx, clientID)
clientState, found := getClientState(clientStore, l.cdc)
if !found {
panic(errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID))
}

clientState.BaseClient.UpdateStateOnMisbehaviour(ctx, l.cdc, clientStore, clientMsg)
}

// UpdateState obtains the client state associated with the client identifier and calls into the clientState.UpdateState method.
//
// CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 07-celestia-{n}.
func (l LightClientModule) UpdateState(ctx sdk.Context, clientID string, clientMsg exported.ClientMessage) []exported.Height {
clientStore := l.storeProvider.ClientStore(ctx, clientID)
clientState, found := getClientState(clientStore, l.cdc)
if !found {
panic(errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID))
}

// execute custom 07-celestia update state logic
return clientState.UpdateState(ctx, l.cdc, clientStore, clientMsg)
}

// VerifyMembership obtains the client state associated with the client identifier and calls into the clientState.VerifyMembership method.
//
// CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 07-celestia-{n}.
func (l LightClientModule) VerifyMembership(
ctx sdk.Context,
clientID string,
height exported.Height,
delayTimePeriod uint64,
delayBlockPeriod uint64,
proof []byte,
path exported.Path,
value []byte,
) error {
clientStore := l.storeProvider.ClientStore(ctx, clientID)
clientState, found := getClientState(clientStore, l.cdc)
if !found {
return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID)
}

// execute custom 07-celestia verify membership logic
return clientState.VerifyMembership(ctx, clientStore, l.cdc, height, delayTimePeriod, delayBlockPeriod, proof, path, value)
}

// VerifyNonMembership obtains the client state associated with the client identifier and calls into the clientState.VerifyNonMembership method.
//
// CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 07-celestia-{n}.
func (LightClientModule) VerifyNonMembership(
ctx sdk.Context,
clientID string,
height exported.Height,
delayTimePeriod uint64,
delayBlockPeriod uint64,
proof []byte,
path exported.Path,
) error {
panic("07-celestia light clients do not verify non-membership proofs")
}

// Status obtains the client state associated with the client identifier and calls into the clientState.Status method.
//
// CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 07-celestia-{n}.
func (l LightClientModule) Status(ctx sdk.Context, clientID string) exported.Status {
clientStore := l.storeProvider.ClientStore(ctx, clientID)
clientState, found := getClientState(clientStore, l.cdc)
if !found {
return exported.Unknown
}

return clientState.BaseClient.Status(ctx, clientStore, l.cdc)
}

// LatestHeight returns the latest height for the client state for the given client identifier.
// If no client is present for the provided client identifier a zero value height is returned.
//
// CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 07-celestia-{n}.
func (l LightClientModule) LatestHeight(ctx sdk.Context, clientID string) exported.Height {
clientStore := l.storeProvider.ClientStore(ctx, clientID)

clientState, found := getClientState(clientStore, l.cdc)
if !found {
return clienttypes.ZeroHeight()
}

return clientState.BaseClient.LatestHeight
}

// TimestampAtHeight obtains the client state associated with the client identifier and calls into the clientState.GetTimestampAtHeight method.
//
// CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 07-celestia-{n}.
func (l LightClientModule) TimestampAtHeight(
ctx sdk.Context,
clientID string,
height exported.Height,
) (uint64, error) {
clientStore := l.storeProvider.ClientStore(ctx, clientID)
clientState, found := getClientState(clientStore, l.cdc)
if !found {
return 0, errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID)
}

return clientState.BaseClient.GetTimestampAtHeight(ctx, clientStore, l.cdc, height)
}

// RecoverClient asserts that the substitute client is a celestia client. It obtains the client state associated with the
// subject client and calls into the subjectClientState.CheckSubstituteAndUpdateState method.
//
// CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 07-celestia-{n}.
func (l LightClientModule) RecoverClient(ctx sdk.Context, clientID, substituteClientID string) error {
substituteClientType, _, err := clienttypes.ParseClientIdentifier(substituteClientID)
if err != nil {
return err
}

if substituteClientType != ModuleName {
return errorsmod.Wrapf(clienttypes.ErrInvalidClientType, "expected: %s, got: %s", ModuleName, substituteClientType)
}

clientStore := l.storeProvider.ClientStore(ctx, clientID)
clientState, found := getClientState(clientStore, l.cdc)
if !found {
return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID)
}

substituteClientStore := l.storeProvider.ClientStore(ctx, substituteClientID)
substituteClient, found := getClientState(substituteClientStore, l.cdc)
if !found {
return errorsmod.Wrap(clienttypes.ErrClientNotFound, substituteClientID)
}

return clientState.BaseClient.CheckSubstituteAndUpdateState(ctx, l.cdc, clientStore, substituteClientStore, substituteClient.BaseClient)
}

// VerifyUpgradeAndUpdateState obtains the client state associated with the client identifier and calls into the clientState.VerifyUpgradeAndUpdateState method.
// The new client and consensus states will be unmarshaled and an error is returned if the new client state is not at a height greater
// than the existing client.
//
// CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 07-celestia-{n}.
func (LightClientModule) VerifyUpgradeAndUpdateState(
ctx sdk.Context,
clientID string,
newClient []byte,
newConsState []byte,
upgradeClientProof,
upgradeConsensusStateProof []byte,
) error {
// TODO: do we need to implement this?
panic("unimplemented")
}
Loading

0 comments on commit d57f8d4

Please sign in to comment.