Some checks failed
CodeQL / Analyze (push) Waiting to run
golangci-lint / lint (push) Waiting to run
Tests / Code Coverage / build (amd64) (push) Waiting to run
Tests / Code Coverage / build (arm64) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[additional-args:-tags="test_e2e" name:e2e path:./e2e]) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[name:08-wasm path:./modules/light-clients/08-wasm]) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[name:ibc-go path:.]) (push) Waiting to run
Docker Build & Push Simapp (main) / docker-build (push) Has been cancelled
326 lines
12 KiB
Go
326 lines
12 KiB
Go
package tendermint
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
|
|
ics23 "github.com/cosmos/ics23/go"
|
|
|
|
errorsmod "cosmossdk.io/errors"
|
|
storetypes "cosmossdk.io/store/types"
|
|
|
|
"git.cw.tr/mukan-network/mukan-sdk/codec"
|
|
sdk "git.cw.tr/mukan-network/mukan-sdk/types"
|
|
|
|
"git.cw.tr/mukan-network/mukan-consensus/light"
|
|
cmttypes "git.cw.tr/mukan-network/mukan-consensus/types"
|
|
|
|
clienttypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/02-client/types"
|
|
commitmenttypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/23-commitment/types"
|
|
commitmenttypesv2 "git.cw.tr/mukan-network/mukan-ibc/modules/core/23-commitment/types/v2"
|
|
ibcerrors "git.cw.tr/mukan-network/mukan-ibc/modules/core/errors"
|
|
"git.cw.tr/mukan-network/mukan-ibc/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 tendermint.
|
|
func (ClientState) ClientType() string {
|
|
return exported.Tendermint
|
|
}
|
|
|
|
// 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 tendermint 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 cmttypes.MaxChainIDLen may change in the future.
|
|
// If this occurs, the code here must account for potential difference
|
|
// between the tendermint version being run by the counterparty chain
|
|
// and the tendermint version used by this light client.
|
|
// https://github.com/cosmos/ibc-go/issues/177
|
|
if len(cs.ChainId) > cmttypes.MaxChainIDLen {
|
|
return errorsmod.Wrapf(ErrInvalidChainID, "chainID is too long; got: %d, max: %d", len(cs.ChainId), cmttypes.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 tm 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 07-tendermint 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
|
|
}
|