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
344 lines
14 KiB
Go
344 lines
14 KiB
Go
package tendermint
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
|
|
"cosmossdk.io/store/prefix"
|
|
storetypes "cosmossdk.io/store/types"
|
|
|
|
"git.cw.tr/mukan-network/mukan-sdk/codec"
|
|
sdk "git.cw.tr/mukan-network/mukan-sdk/types"
|
|
|
|
clienttypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/02-client/types"
|
|
host "git.cw.tr/mukan-network/mukan-ibc/modules/core/24-host"
|
|
"git.cw.tr/mukan-network/mukan-ibc/modules/core/exported"
|
|
)
|
|
|
|
/*
|
|
This file contains the logic for storage and iteration over `IterationKey` metadata that is stored
|
|
for each consensus state. The consensus state key specified in ICS-24 and expected by counterparty chains
|
|
stores the consensus state under the key: `consensusStates/{revision_number}-{revision_height}`, with each number
|
|
represented as a string.
|
|
While this works fine for IBC proof verification, it makes efficient iteration difficult since the lexicographic order
|
|
of the consensus state keys do not match the height order of consensus states. This makes consensus state pruning and
|
|
monotonic time enforcement difficult since it is inefficient to find the earliest consensus state or to find the neighboring
|
|
consensus states given a consensus state height.
|
|
Changing the ICS-24 representation will be a major breaking change that requires counterparty chains to accept a new key format.
|
|
Thus to avoid breaking IBC, we can store a lookup from a more efficiently formatted key: `iterationKey` to the consensus state key which
|
|
stores the underlying consensus state. This efficient iteration key will be formatted like so: `iterateConsensusStates{BigEndianRevisionBytes}{BigEndianHeightBytes}`.
|
|
This ensures that the lexicographic order of iteration keys match the height order of the consensus states. Thus, we can use the SDK store's
|
|
Iterators to iterate over the consensus states in ascending/descending order by providing a mapping from `iterationKey -> consensusStateKey -> ConsensusState`.
|
|
A future version of IBC may choose to replace the ICS24 ConsensusState path with the more efficient format and make this indirection unnecessary.
|
|
*/
|
|
|
|
const KeyIterateConsensusStatePrefix = "iterateConsensusStates"
|
|
|
|
var (
|
|
// KeyProcessedTime is appended to consensus state key to store the processed time
|
|
KeyProcessedTime = []byte("/processedTime")
|
|
// KeyProcessedHeight is appended to consensus state key to store the processed height
|
|
KeyProcessedHeight = []byte("/processedHeight")
|
|
// KeyIteration stores the key mapping to consensus state key for efficient iteration
|
|
KeyIteration = []byte("/iterationKey")
|
|
)
|
|
|
|
// setClientState stores the client state
|
|
func setClientState(clientStore storetypes.KVStore, cdc codec.BinaryCodec, clientState *ClientState) {
|
|
key := host.ClientStateKey()
|
|
val := clienttypes.MustMarshalClientState(cdc, clientState)
|
|
clientStore.Set(key, val)
|
|
}
|
|
|
|
// getClientState retrieves the client state from the store using the provided KVStore and codec.
|
|
// It returns the unmarshaled ClientState and a boolean indicating if the state was found.
|
|
func getClientState(store storetypes.KVStore, cdc codec.BinaryCodec) (*ClientState, bool) {
|
|
bz := store.Get(host.ClientStateKey())
|
|
if len(bz) == 0 {
|
|
return nil, false
|
|
}
|
|
|
|
clientStateI := clienttypes.MustUnmarshalClientState(cdc, bz)
|
|
var clientState *ClientState
|
|
clientState, ok := clientStateI.(*ClientState)
|
|
if !ok {
|
|
panic(fmt.Errorf("cannot convert %T into %T", clientStateI, clientState))
|
|
}
|
|
return clientState, true
|
|
}
|
|
|
|
// setConsensusState stores the consensus state at the given height.
|
|
func setConsensusState(clientStore storetypes.KVStore, cdc codec.BinaryCodec, consensusState *ConsensusState, height exported.Height) {
|
|
key := host.ConsensusStateKey(height)
|
|
val := clienttypes.MustMarshalConsensusState(cdc, consensusState)
|
|
clientStore.Set(key, val)
|
|
}
|
|
|
|
// GetConsensusState retrieves the consensus state from the client prefixed store.
|
|
// If the ConsensusState does not exist in state for the provided height a nil value and false boolean flag is returned
|
|
func GetConsensusState(store storetypes.KVStore, cdc codec.BinaryCodec, height exported.Height) (*ConsensusState, bool) {
|
|
bz := store.Get(host.ConsensusStateKey(height))
|
|
if len(bz) == 0 {
|
|
return nil, false
|
|
}
|
|
|
|
consensusStateI := clienttypes.MustUnmarshalConsensusState(cdc, bz)
|
|
var consensusState *ConsensusState
|
|
consensusState, ok := consensusStateI.(*ConsensusState)
|
|
if !ok {
|
|
panic(fmt.Errorf("cannot convert %T into %T", consensusStateI, consensusState))
|
|
}
|
|
|
|
return consensusState, true
|
|
}
|
|
|
|
// deleteConsensusState deletes the consensus state at the given height
|
|
func deleteConsensusState(clientStore storetypes.KVStore, height exported.Height) {
|
|
key := host.ConsensusStateKey(height)
|
|
clientStore.Delete(key)
|
|
}
|
|
|
|
// ProcessedTimeKey returns the key under which the processed time will be stored in the client store.
|
|
func ProcessedTimeKey(height exported.Height) []byte {
|
|
return append(host.ConsensusStateKey(height), KeyProcessedTime...)
|
|
}
|
|
|
|
// SetProcessedTime stores the time at which a header was processed and the corresponding consensus state was created.
|
|
// This is useful when validating whether a packet has reached the time specified delay period in the tendermint client's
|
|
// verification functions
|
|
func SetProcessedTime(clientStore storetypes.KVStore, height exported.Height, timeNs uint64) {
|
|
key := ProcessedTimeKey(height)
|
|
val := sdk.Uint64ToBigEndian(timeNs)
|
|
clientStore.Set(key, val)
|
|
}
|
|
|
|
// GetProcessedTime gets the time (in nanoseconds) at which this chain received and processed a tendermint header.
|
|
// This is used to validate that a received packet has passed the time delay period.
|
|
func GetProcessedTime(clientStore storetypes.KVStore, height exported.Height) (uint64, bool) {
|
|
key := ProcessedTimeKey(height)
|
|
bz := clientStore.Get(key)
|
|
if len(bz) == 0 {
|
|
return 0, false
|
|
}
|
|
return sdk.BigEndianToUint64(bz), true
|
|
}
|
|
|
|
// deleteProcessedTime deletes the processedTime for a given height
|
|
func deleteProcessedTime(clientStore storetypes.KVStore, height exported.Height) {
|
|
key := ProcessedTimeKey(height)
|
|
clientStore.Delete(key)
|
|
}
|
|
|
|
// ProcessedHeightKey returns the key under which the processed height will be stored in the client store.
|
|
func ProcessedHeightKey(height exported.Height) []byte {
|
|
return append(host.ConsensusStateKey(height), KeyProcessedHeight...)
|
|
}
|
|
|
|
// SetProcessedHeight stores the height at which a header was processed and the corresponding consensus state was created.
|
|
// This is useful when validating whether a packet has reached the specified block delay period in the tendermint client's
|
|
// verification functions
|
|
func SetProcessedHeight(clientStore storetypes.KVStore, consHeight, processedHeight exported.Height) {
|
|
key := ProcessedHeightKey(consHeight)
|
|
val := []byte(processedHeight.String())
|
|
clientStore.Set(key, val)
|
|
}
|
|
|
|
// GetProcessedHeight gets the height at which this chain received and processed a tendermint header.
|
|
// This is used to validate that a received packet has passed the block delay period.
|
|
func GetProcessedHeight(clientStore storetypes.KVStore, height exported.Height) (exported.Height, bool) {
|
|
key := ProcessedHeightKey(height)
|
|
bz := clientStore.Get(key)
|
|
if len(bz) == 0 {
|
|
return nil, false
|
|
}
|
|
processedHeight, err := clienttypes.ParseHeight(string(bz))
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
return processedHeight, true
|
|
}
|
|
|
|
// deleteProcessedHeight deletes the processedHeight for a given height
|
|
func deleteProcessedHeight(clientStore storetypes.KVStore, height exported.Height) {
|
|
key := ProcessedHeightKey(height)
|
|
clientStore.Delete(key)
|
|
}
|
|
|
|
// IterationKey returns the key under which the consensus state key will be stored.
|
|
// The iteration key is a BigEndian representation of the consensus state key to support efficient iteration.
|
|
func IterationKey(height exported.Height) []byte {
|
|
heightBytes := bigEndianHeightBytes(height)
|
|
return append([]byte(KeyIterateConsensusStatePrefix), heightBytes...)
|
|
}
|
|
|
|
// SetIterationKey stores the consensus state key under a key that is more efficient for ordered iteration
|
|
func SetIterationKey(clientStore storetypes.KVStore, height exported.Height) {
|
|
key := IterationKey(height)
|
|
val := host.ConsensusStateKey(height)
|
|
clientStore.Set(key, val)
|
|
}
|
|
|
|
// GetIterationKey returns the consensus state key stored under the efficient iteration key.
|
|
// NOTE: This function is currently only used for testing purposes
|
|
func GetIterationKey(clientStore storetypes.KVStore, height exported.Height) []byte {
|
|
key := IterationKey(height)
|
|
return clientStore.Get(key)
|
|
}
|
|
|
|
// deleteIterationKey deletes the iteration key for a given height
|
|
func deleteIterationKey(clientStore storetypes.KVStore, height exported.Height) {
|
|
key := IterationKey(height)
|
|
clientStore.Delete(key)
|
|
}
|
|
|
|
// GetHeightFromIterationKey takes an iteration key and returns the height that it references
|
|
func GetHeightFromIterationKey(iterKey []byte) exported.Height {
|
|
bigEndianBytes := iterKey[len([]byte(KeyIterateConsensusStatePrefix)):]
|
|
revisionBytes := bigEndianBytes[0:8]
|
|
heightBytes := bigEndianBytes[8:]
|
|
revision := binary.BigEndian.Uint64(revisionBytes)
|
|
height := binary.BigEndian.Uint64(heightBytes)
|
|
return clienttypes.NewHeight(revision, height)
|
|
}
|
|
|
|
// IterateConsensusStateAscending iterates through the consensus states in ascending order. It calls the provided
|
|
// callback on each height, until stop=true is returned.
|
|
func IterateConsensusStateAscending(clientStore storetypes.KVStore, cb func(height exported.Height) (stop bool)) {
|
|
iterator := storetypes.KVStorePrefixIterator(clientStore, []byte(KeyIterateConsensusStatePrefix))
|
|
defer iterator.Close()
|
|
|
|
for ; iterator.Valid(); iterator.Next() {
|
|
iterKey := iterator.Key()
|
|
height := GetHeightFromIterationKey(iterKey)
|
|
if cb(height) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetNextConsensusState returns the lowest consensus state that is larger than the given height.
|
|
// The Iterator returns a storetypes.Iterator which iterates from start (inclusive) to end (exclusive).
|
|
// If the starting height exists in store, we need to call iterator.Next() to get the next consensus state.
|
|
// Otherwise, the iterator is already at the next consensus state so we can call iterator.Value() immediately.
|
|
func GetNextConsensusState(clientStore storetypes.KVStore, cdc codec.BinaryCodec, height exported.Height) (*ConsensusState, bool) {
|
|
iterateStore := prefix.NewStore(clientStore, []byte(KeyIterateConsensusStatePrefix))
|
|
iterator := iterateStore.Iterator(bigEndianHeightBytes(height), nil)
|
|
defer iterator.Close()
|
|
if !iterator.Valid() {
|
|
return nil, false
|
|
}
|
|
|
|
// if iterator is at current height, ignore the consensus state at current height and get next height
|
|
// if iterator value is not at current height, it is already at next height.
|
|
if bytes.Equal(iterator.Value(), host.ConsensusStateKey(height)) {
|
|
iterator.Next()
|
|
if !iterator.Valid() {
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
csKey := iterator.Value()
|
|
|
|
return getTmConsensusState(clientStore, cdc, csKey)
|
|
}
|
|
|
|
// GetPreviousConsensusState returns the highest consensus state that is lower than the given height.
|
|
// The Iterator returns a storetypes.Iterator which iterates from the end (exclusive) to start (inclusive).
|
|
// Thus to get previous consensus state we call iterator.Value() immediately.
|
|
func GetPreviousConsensusState(clientStore storetypes.KVStore, cdc codec.BinaryCodec, height exported.Height) (*ConsensusState, bool) {
|
|
iterateStore := prefix.NewStore(clientStore, []byte(KeyIterateConsensusStatePrefix))
|
|
iterator := iterateStore.ReverseIterator(nil, bigEndianHeightBytes(height))
|
|
defer iterator.Close()
|
|
|
|
if !iterator.Valid() {
|
|
return nil, false
|
|
}
|
|
|
|
csKey := iterator.Value()
|
|
|
|
return getTmConsensusState(clientStore, cdc, csKey)
|
|
}
|
|
|
|
// PruneAllExpiredConsensusStates iterates over all consensus states for a given
|
|
// client store. If a consensus state is expired, it is deleted and its metadata
|
|
// is deleted. The number of consensus states pruned is returned.
|
|
func PruneAllExpiredConsensusStates(
|
|
ctx sdk.Context, clientStore storetypes.KVStore,
|
|
cdc codec.BinaryCodec, clientState *ClientState,
|
|
) int {
|
|
var heights []exported.Height
|
|
|
|
pruneCb := func(height exported.Height) bool {
|
|
consState, found := GetConsensusState(clientStore, cdc, height)
|
|
if !found { // consensus state should always be found
|
|
return true
|
|
}
|
|
if clientState.IsExpired(consState.Timestamp, ctx.BlockTime()) {
|
|
heights = append(heights, height)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
IterateConsensusStateAscending(clientStore, pruneCb)
|
|
|
|
for _, height := range heights {
|
|
deleteConsensusState(clientStore, height)
|
|
deleteConsensusMetadata(clientStore, height)
|
|
}
|
|
|
|
return len(heights)
|
|
}
|
|
|
|
// Helper function for GetNextConsensusState and GetPreviousConsensusState
|
|
func getTmConsensusState(clientStore storetypes.KVStore, cdc codec.BinaryCodec, key []byte) (*ConsensusState, bool) {
|
|
bz := clientStore.Get(key)
|
|
if len(bz) == 0 {
|
|
return nil, false
|
|
}
|
|
|
|
consensusStateI, err := clienttypes.UnmarshalConsensusState(cdc, bz)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
|
|
consensusState, ok := consensusStateI.(*ConsensusState)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
return consensusState, true
|
|
}
|
|
|
|
func bigEndianHeightBytes(height exported.Height) []byte {
|
|
heightBytes := make([]byte, 16)
|
|
binary.BigEndian.PutUint64(heightBytes, height.GetRevisionNumber())
|
|
binary.BigEndian.PutUint64(heightBytes[8:], height.GetRevisionHeight())
|
|
return heightBytes
|
|
}
|
|
|
|
// setConsensusMetadata sets context time as processed time and set context height as processed height
|
|
// as this is internal tendermint light client logic.
|
|
// client state and consensus state will be set by client keeper
|
|
// set iteration key to provide ability for efficient ordered iteration of consensus states.
|
|
func setConsensusMetadata(ctx sdk.Context, clientStore storetypes.KVStore, height exported.Height) {
|
|
setConsensusMetadataWithValues(clientStore, height, clienttypes.GetSelfHeight(ctx), uint64(ctx.BlockTime().UnixNano()))
|
|
}
|
|
|
|
// setConsensusMetadataWithValues sets the consensus metadata with the provided values
|
|
func setConsensusMetadataWithValues(
|
|
clientStore storetypes.KVStore, height,
|
|
processedHeight exported.Height,
|
|
processedTime uint64,
|
|
) {
|
|
SetProcessedTime(clientStore, height, processedTime)
|
|
SetProcessedHeight(clientStore, height, processedHeight)
|
|
SetIterationKey(clientStore, height)
|
|
}
|
|
|
|
// deleteConsensusMetadata deletes the metadata stored for a particular consensus state.
|
|
func deleteConsensusMetadata(clientStore storetypes.KVStore, height exported.Height) {
|
|
deleteProcessedTime(clientStore, height)
|
|
deleteProcessedHeight(clientStore, height)
|
|
deleteIterationKey(clientStore, height)
|
|
}
|