Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ require (
github.com/cometbft/cometbft v0.38.17
github.com/cosmos/cosmos-db v1.1.1
github.com/cosmos/cosmos-proto v1.0.0-beta.5
github.com/cosmos/cosmos-sdk v0.53.0
github.com/cosmos/cosmos-sdk v0.53.4
github.com/cosmos/go-bip39 v1.0.0
github.com/cosmos/gogoproto v1.7.0
github.com/cosmos/ibc-go/v10 v10.2.0
Expand All @@ -46,14 +46,20 @@ require (
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
golang.org/x/sync v0.14.0
golang.org/x/sync v0.16.0
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e
google.golang.org/grpc v1.72.0
google.golang.org/protobuf v1.36.6
gotest.tools/v3 v3.5.2
pgregory.net/rapid v1.2.0
)

require (
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
github.com/btcsuite/btcd/btcutil v1.1.6 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
)

require (
cel.dev/expr v0.20.0 // indirect
cloud.google.com/go v0.116.0 // indirect
Expand Down Expand Up @@ -121,6 +127,7 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/getsentry/sentry-go v0.32.0 // indirect
github.com/gnolang/gno v0.0.0-20251010155040-3f1cc0962a5f
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
Expand Down Expand Up @@ -231,12 +238,12 @@ require (
go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.15.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/term v0.33.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.10.0 // indirect
google.golang.org/api v0.222.0 // indirect
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
Expand Down
51 changes: 51 additions & 0 deletions go.sum

Large diffs are not rendered by default.

326 changes: 326 additions & 0 deletions modules/10-gno/client_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
package gno

import (
"strings"
"time"

ics23 "github.com/cosmos/ics23/go"

errorsmod "cosmossdk.io/errors"
storetypes "cosmossdk.io/store/types"

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

"github.com/cometbft/cometbft/light"
bfttypes "github.com/gnolang/gno/tm2/pkg/bft/types"

clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types"
commitmenttypes "github.com/cosmos/ibc-go/v10/modules/core/23-commitment/types"
commitmenttypesv2 "github.com/cosmos/ibc-go/v10/modules/core/23-commitment/types/v2"
ibcerrors "github.com/cosmos/ibc-go/v10/modules/core/errors"
"github.com/cosmos/ibc-go/v10/modules/core/exported"
)

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

// NewClientState creates a new ClientState instance
func NewClientState(
chainID string, trustLevel Fraction,
trustingPeriod, ubdPeriod, maxClockDrift time.Duration,
latestHeight clienttypes.Height, specs []*ics23.ProofSpec,
upgradePath []string,
) *ClientState {
return &ClientState{
ChainId: chainID,
TrustLevel: trustLevel,
TrustingPeriod: trustingPeriod,
UnbondingPeriod: ubdPeriod,
MaxClockDrift: maxClockDrift,
LatestHeight: latestHeight,
FrozenHeight: clienttypes.ZeroHeight(),
ProofSpecs: specs,
UpgradePath: upgradePath,
}
}

// GetChainID returns the chain-id
func (cs ClientState) GetChainID() string {
return cs.ChainId
}

// ClientType is gno.
func (ClientState) ClientType() string {
return Gno
}

// getTimestampAtHeight returns the timestamp in nanoseconds of the consensus state at the given height.
func (ClientState) getTimestampAtHeight(
clientStore storetypes.KVStore,
cdc codec.BinaryCodec,
height exported.Height,
) (uint64, error) {
// get consensus state at height from clientStore to check for expiry
consState, found := GetConsensusState(clientStore, cdc, height)
if !found {
return 0, errorsmod.Wrapf(clienttypes.ErrConsensusStateNotFound, "height (%s)", height)
}
return consState.GetTimestamp(), nil
}

// status returns the status of the gno client.
// The client may be:
// - Active: FrozenHeight is zero and client is not expired
// - Frozen: Frozen Height is not zero
// - Expired: the latest consensus state timestamp + trusting period <= current time
//
// A frozen client will become expired, so the Frozen status
// has higher precedence.
func (cs ClientState) status(
ctx sdk.Context,
clientStore storetypes.KVStore,
cdc codec.BinaryCodec,
) exported.Status {
if !cs.FrozenHeight.IsZero() {
return exported.Frozen
}

// get latest consensus state from clientStore to check for expiry
consState, found := GetConsensusState(clientStore, cdc, cs.LatestHeight)
if !found {
// if the client state does not have an associated consensus state for its latest height
// then it must be expired
return exported.Expired
}

if cs.IsExpired(consState.Timestamp, ctx.BlockTime()) {
return exported.Expired
}

return exported.Active
}

// IsExpired returns whether or not the client has passed the trusting period since the last
// update (in which case no headers are considered valid).
func (cs ClientState) IsExpired(latestTimestamp, now time.Time) bool {
expirationTime := latestTimestamp.Add(cs.TrustingPeriod)
return !expirationTime.After(now)
}

// Validate performs a basic validation of the client state fields.
func (cs ClientState) Validate() error {
if strings.TrimSpace(cs.ChainId) == "" {
return errorsmod.Wrap(ErrInvalidChainID, "chain id cannot be empty string")
}

// NOTE: the value of bfttypes.MaxChainIDLen may change in the future.
// If this occurs, the code here must account for potential difference
// between the gno/tm2 version being run by the counterparty chain
// and the gno/tm2 version used by this light client.
// https://github.com/cosmos/ibc-go/issues/177
if len(cs.ChainId) > bfttypes.MaxChainIDLen {
return errorsmod.Wrapf(ErrInvalidChainID, "chainID is too long; got: %d, max: %d", len(cs.ChainId), bfttypes.MaxChainIDLen)
}

if err := light.ValidateTrustLevel(cs.TrustLevel.ToTendermint()); err != nil {
return errorsmod.Wrap(ErrInvalidTrustLevel, err.Error())
}
if cs.TrustingPeriod <= 0 {
return errorsmod.Wrap(ErrInvalidTrustingPeriod, "trusting period must be greater than zero")
}
if cs.UnbondingPeriod <= 0 {
return errorsmod.Wrap(ErrInvalidUnbondingPeriod, "unbonding period must be greater than zero")
}
if cs.MaxClockDrift <= 0 {
return errorsmod.Wrap(ErrInvalidMaxClockDrift, "max clock drift must be greater than zero")
}

// the latest height revision number must match the chain id revision number
if cs.LatestHeight.RevisionNumber != clienttypes.ParseChainID(cs.ChainId) {
return errorsmod.Wrapf(ErrInvalidHeaderHeight,
"latest height revision number must match chain id revision number (%d != %d)", cs.LatestHeight.RevisionNumber, clienttypes.ParseChainID(cs.ChainId))
}
if cs.LatestHeight.RevisionHeight == 0 {
return errorsmod.Wrap(ErrInvalidHeaderHeight, "tendermint client's latest height revision height cannot be zero")
}
if cs.TrustingPeriod >= cs.UnbondingPeriod {
return errorsmod.Wrapf(
ErrInvalidTrustingPeriod,
"trusting period (%s) should be < unbonding period (%s)", cs.TrustingPeriod, cs.UnbondingPeriod,
)
}

if cs.ProofSpecs == nil {
return errorsmod.Wrap(ErrInvalidProofSpecs, "proof specs cannot be nil for gno client")
}
for i, spec := range cs.ProofSpecs {
if spec == nil {
return errorsmod.Wrapf(ErrInvalidProofSpecs, "proof spec cannot be nil at index: %d", i)
}
}
// UpgradePath may be empty, but if it isn't, each key must be non-empty
for i, k := range cs.UpgradePath {
if strings.TrimSpace(k) == "" {
return errorsmod.Wrapf(clienttypes.ErrInvalidClient, "key in upgrade path at index %d cannot be empty", i)
}
}

return nil
}

// ZeroCustomFields returns a ClientState that is a copy of the current ClientState
// with all client customizable fields zeroed out. All chain specific fields must
// remain unchanged. This client state will be used to verify chain upgrades when a
// chain breaks a light client verification parameter such as chainID.
func (cs ClientState) ZeroCustomFields() *ClientState {
// copy over all chain-specified fields
// and leave custom fields empty
return &ClientState{
ChainId: cs.ChainId,
UnbondingPeriod: cs.UnbondingPeriod,
LatestHeight: cs.LatestHeight,
ProofSpecs: cs.ProofSpecs,
UpgradePath: cs.UpgradePath,
}
}

// initialize checks that the initial consensus state is an 10-gno consensus state and
// sets the client state, consensus state and associated metadata in the provided client store.
func (cs ClientState) initialize(ctx sdk.Context, cdc codec.BinaryCodec, clientStore storetypes.KVStore, consState exported.ConsensusState) error {
consensusState, ok := consState.(*ConsensusState)
if !ok {
return errorsmod.Wrapf(clienttypes.ErrInvalidConsensus, "invalid initial consensus state. expected type: %T, got: %T",
&ConsensusState{}, consState)
}

setClientState(clientStore, cdc, &cs)
setConsensusState(clientStore, cdc, consensusState, cs.LatestHeight)
setConsensusMetadata(ctx, clientStore, cs.LatestHeight)

return nil
}

// verifyMembership is a generic proof verification method which verifies a proof of the existence of a value at a given CommitmentPath at the specified height.
// The caller is expected to construct the full CommitmentPath from a CommitmentPrefix and a standardized path (as defined in ICS 24).
// If a zero proof height is passed in, it will fail to retrieve the associated consensus state.
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.LatestHeight.LT(height) {
return errorsmod.Wrapf(
ibcerrors.ErrInvalidHeight,
"client state height < proof height (%d < %d), please ensure the client has been updated", cs.LatestHeight, height,
)
}

if err := verifyDelayPeriodPassed(ctx, clientStore, height, delayTimePeriod, delayBlockPeriod); err != nil {
return err
}

var merkleProof commitmenttypes.MerkleProof
if err := cdc.Unmarshal(proof, &merkleProof); err != nil {
return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "failed to unmarshal proof into ICS 23 commitment merkle proof")
}

merklePath, ok := path.(commitmenttypesv2.MerklePath)
if !ok {
return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", commitmenttypesv2.MerklePath{}, path)
}

consensusState, found := GetConsensusState(clientStore, cdc, height)
if !found {
return errorsmod.Wrap(clienttypes.ErrConsensusStateNotFound, "please ensure the proof was constructed against a height that exists on the client")
}

return merkleProof.VerifyMembership(cs.ProofSpecs, consensusState.GetRoot(), merklePath, value)
}

// verifyNonMembership is a generic proof verification method which verifies the absence of a given CommitmentPath at a specified height.
// The caller is expected to construct the full CommitmentPath from a CommitmentPrefix and a standardized path (as defined in ICS 24).
// If a zero proof height is passed in, it will fail to retrieve the associated consensus state.
func (cs ClientState) verifyNonMembership(
ctx sdk.Context,
clientStore storetypes.KVStore,
cdc codec.BinaryCodec,
height exported.Height,
delayTimePeriod uint64,
delayBlockPeriod uint64,
proof []byte,
path exported.Path,
) error {
if cs.LatestHeight.LT(height) {
return errorsmod.Wrapf(
ibcerrors.ErrInvalidHeight,
"client state height < proof height (%d < %d), please ensure the client has been updated", cs.LatestHeight, height,
)
}

if err := verifyDelayPeriodPassed(ctx, clientStore, height, delayTimePeriod, delayBlockPeriod); err != nil {
return err
}

var merkleProof commitmenttypes.MerkleProof
if err := cdc.Unmarshal(proof, &merkleProof); err != nil {
return errorsmod.Wrap(commitmenttypes.ErrInvalidProof, "failed to unmarshal proof into ICS 23 commitment merkle proof")
}

merklePath, ok := path.(commitmenttypesv2.MerklePath)
if !ok {
return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", commitmenttypesv2.MerklePath{}, path)
}

consensusState, found := GetConsensusState(clientStore, cdc, height)
if !found {
return errorsmod.Wrap(clienttypes.ErrConsensusStateNotFound, "please ensure the proof was constructed against a height that exists on the client")
}

return merkleProof.VerifyNonMembership(cs.ProofSpecs, consensusState.GetRoot(), merklePath)
}

// 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 {
if delayTimePeriod != 0 {
// check that executing chain's timestamp has passed consensusState's processed time + delay time period
processedTime, ok := GetProcessedTime(store, proofHeight)
if !ok {
return errorsmod.Wrapf(ErrProcessedTimeNotFound, "processed time not found for height: %s", proofHeight)
}

currentTimestamp := uint64(ctx.BlockTime().UnixNano())
validTime := processedTime + delayTimePeriod

// NOTE: delay time period is inclusive, so if currentTimestamp is validTime, then we return no error
if currentTimestamp < validTime {
return errorsmod.Wrapf(ErrDelayPeriodNotPassed, "cannot verify packet until time: %d, current time: %d",
validTime, currentTimestamp)
}
}

if delayBlockPeriod != 0 {
// check that executing chain's height has passed consensusState's processed height + delay block period
processedHeight, ok := GetProcessedHeight(store, proofHeight)
if !ok {
return errorsmod.Wrapf(ErrProcessedHeightNotFound, "processed height not found for height: %s", proofHeight)
}

currentHeight := clienttypes.GetSelfHeight(ctx)
validHeight := clienttypes.NewHeight(processedHeight.GetRevisionNumber(), processedHeight.GetRevisionHeight()+delayBlockPeriod)

// NOTE: delay block period is inclusive, so if currentHeight is validHeight, then we return no error
if currentHeight.LT(validHeight) {
return errorsmod.Wrapf(ErrDelayPeriodNotPassed, "cannot verify packet until height: %s, current height: %s",
validHeight, currentHeight)
}
}

return nil
}
Loading