|
1 | 1 | package types
|
2 | 2 |
|
3 | 3 | import (
|
| 4 | + "encoding/binary" |
4 | 5 | "strings"
|
5 | 6 |
|
6 | 7 | "github.com/cosmos/cosmos-sdk/codec"
|
| 8 | + "github.com/cosmos/cosmos-sdk/store/prefix" |
7 | 9 | sdk "github.com/cosmos/cosmos-sdk/types"
|
8 | 10 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
9 | 11 | clienttypes "github.com/cosmos/ibc-go/modules/core/02-client/types"
|
10 | 12 | host "github.com/cosmos/ibc-go/modules/core/24-host"
|
11 | 13 | "github.com/cosmos/ibc-go/modules/core/exported"
|
12 | 14 | )
|
13 | 15 |
|
14 |
| -// KeyProcessedTime is appended to consensus state key to store the processed time |
15 |
| -var KeyProcessedTime = []byte("/processedTime") |
| 16 | +/* |
| 17 | +This file contains the logic for storage and iteration over `IterationKey` metadata that is stored |
| 18 | +for each consensus state. The consensus state key specified in ICS-24 and expected by counterparty chains |
| 19 | +stores the consensus state under the key: `consensusStates/{revision_number}-{revision_height}`, with each number |
| 20 | +represented as a string. |
| 21 | +While this works fine for IBC proof verification, it makes efficient iteration difficult since the lexicographic order |
| 22 | +of the consensus state keys do not match the height order of consensus states. This makes consensus state pruning and |
| 23 | +monotonic time enforcement difficult since it is inefficient to find the earliest consensus state or to find the neigboring |
| 24 | +consensus states given a consensus state height. |
| 25 | +Changing the ICS-24 representation will be a major breaking change that requires counterparty chains to accept a new key format. |
| 26 | +Thus to avoid breaking IBC, we can store a lookup from a more efficiently formatted key: `iterationKey` to the consensus state key which |
| 27 | +stores the underlying consensus state. This efficient iteration key will be formatted like so: `iterateConsensusStates{BigEndianRevisionBytes}{BigEndianHeightBytes}`. |
| 28 | +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 |
| 29 | +Iterators to iterate over the consensus states in ascending/descending order by providing a mapping from `iterationKey -> consensusStateKey -> ConsensusState`. |
| 30 | +A future version of IBC may choose to replace the ICS24 ConsensusState path with the more efficient format and make this indirection unnecessary. |
| 31 | +*/ |
| 32 | + |
| 33 | +const KeyIterateConsensusStatePrefix = "iterateConsensusStates" |
| 34 | + |
| 35 | +var ( |
| 36 | + // KeyProcessedTime is appended to consensus state key to store the processed time |
| 37 | + KeyProcessedTime = []byte("/processedTime") |
| 38 | + KeyIteration = []byte("/iterationKey") |
| 39 | +) |
16 | 40 |
|
17 | 41 | // SetConsensusState stores the consensus state at the given height.
|
18 | 42 | func SetConsensusState(clientStore sdk.KVStore, cdc codec.BinaryMarshaler, consensusState *ConsensusState, height exported.Height) {
|
@@ -48,6 +72,12 @@ func GetConsensusState(store sdk.KVStore, cdc codec.BinaryMarshaler, height expo
|
48 | 72 | return consensusState, nil
|
49 | 73 | }
|
50 | 74 |
|
| 75 | +// deleteConsensusState deletes the consensus state at the given height |
| 76 | +func deleteConsensusState(clientStore sdk.KVStore, height exported.Height) { |
| 77 | + key := host.ConsensusStateKey(height) |
| 78 | + clientStore.Delete(key) |
| 79 | +} |
| 80 | + |
51 | 81 | // IterateProcessedTime iterates through the prefix store and applies the callback.
|
52 | 82 | // If the cb returns true, then iterator will close and stop.
|
53 | 83 | func IterateProcessedTime(store sdk.KVStore, cb func(key, val []byte) bool) {
|
@@ -94,3 +124,123 @@ func GetProcessedTime(clientStore sdk.KVStore, height exported.Height) (uint64,
|
94 | 124 | }
|
95 | 125 | return sdk.BigEndianToUint64(bz), true
|
96 | 126 | }
|
| 127 | + |
| 128 | +// deleteProcessedTime deletes the processedTime for a given height |
| 129 | +func deleteProcessedTime(clientStore sdk.KVStore, height exported.Height) { |
| 130 | + key := ProcessedTimeKey(height) |
| 131 | + clientStore.Delete(key) |
| 132 | +} |
| 133 | + |
| 134 | +// Iteration Code |
| 135 | + |
| 136 | +// IterationKey returns the key under which the consensus state key will be stored. |
| 137 | +// The iteration key is a BigEndian representation of the consensus state key to support efficient iteration. |
| 138 | +func IterationKey(height exported.Height) []byte { |
| 139 | + heightBytes := bigEndianHeightBytes(height) |
| 140 | + return append([]byte(KeyIterateConsensusStatePrefix), heightBytes...) |
| 141 | +} |
| 142 | + |
| 143 | +// SetIterationKey stores the consensus state key under a key that is more efficient for ordered iteration |
| 144 | +func SetIterationKey(clientStore sdk.KVStore, height exported.Height) { |
| 145 | + key := IterationKey(height) |
| 146 | + val := host.ConsensusStateKey(height) |
| 147 | + clientStore.Set(key, val) |
| 148 | +} |
| 149 | + |
| 150 | +// GetIterationKey returns the consensus state key stored under the efficient iteration key. |
| 151 | +// NOTE: This function is currently only used for testing purposes |
| 152 | +func GetIterationKey(clientStore sdk.KVStore, height exported.Height) []byte { |
| 153 | + key := IterationKey(height) |
| 154 | + return clientStore.Get(key) |
| 155 | +} |
| 156 | + |
| 157 | +// deleteIterationKey deletes the iteration key for a given height |
| 158 | +func deleteIterationKey(clientStore sdk.KVStore, height exported.Height) { |
| 159 | + key := IterationKey(height) |
| 160 | + clientStore.Delete(key) |
| 161 | +} |
| 162 | + |
| 163 | +// GetHeightFromIterationKey takes an iteration key and returns the height that it references |
| 164 | +func GetHeightFromIterationKey(iterKey []byte) exported.Height { |
| 165 | + bigEndianBytes := iterKey[len([]byte(KeyIterateConsensusStatePrefix)):] |
| 166 | + revisionBytes := bigEndianBytes[0:8] |
| 167 | + heightBytes := bigEndianBytes[8:] |
| 168 | + revision := binary.BigEndian.Uint64(revisionBytes) |
| 169 | + height := binary.BigEndian.Uint64(heightBytes) |
| 170 | + return clienttypes.NewHeight(revision, height) |
| 171 | +} |
| 172 | + |
| 173 | +func IterateConsensusStateAscending(clientStore sdk.KVStore, cb func(height exported.Height) (stop bool)) error { |
| 174 | + iterator := sdk.KVStorePrefixIterator(clientStore, []byte(KeyIterateConsensusStatePrefix)) |
| 175 | + defer iterator.Close() |
| 176 | + |
| 177 | + for ; iterator.Valid(); iterator.Next() { |
| 178 | + iterKey := iterator.Key() |
| 179 | + height := GetHeightFromIterationKey(iterKey) |
| 180 | + if cb(height) { |
| 181 | + return nil |
| 182 | + } |
| 183 | + } |
| 184 | + return nil |
| 185 | +} |
| 186 | + |
| 187 | +// GetNextConsensusState returns the lowest consensus state that is larger than the given height. |
| 188 | +// The Iterator returns a storetypes.Iterator which iterates from start (inclusive) to end (exclusive). |
| 189 | +// Thus, to get the next consensus state, we must first call iterator.Next() and then get the value. |
| 190 | +func GetNextConsensusState(clientStore sdk.KVStore, cdc codec.BinaryMarshaler, height exported.Height) (*ConsensusState, bool) { |
| 191 | + iterateStore := prefix.NewStore(clientStore, []byte(KeyIterateConsensusStatePrefix)) |
| 192 | + iterator := iterateStore.Iterator(bigEndianHeightBytes(height), nil) |
| 193 | + defer iterator.Close() |
| 194 | + // ignore the consensus state at current height and get next height |
| 195 | + iterator.Next() |
| 196 | + if !iterator.Valid() { |
| 197 | + return nil, false |
| 198 | + } |
| 199 | + |
| 200 | + csKey := iterator.Value() |
| 201 | + |
| 202 | + return getTmConsensusState(clientStore, cdc, csKey) |
| 203 | +} |
| 204 | + |
| 205 | +// GetPreviousConsensusState returns the highest consensus state that is lower than the given height. |
| 206 | +// The Iterator returns a storetypes.Iterator which iterates from the end (exclusive) to start (inclusive). |
| 207 | +// Thus to get previous consensus state we call iterator.Value() immediately. |
| 208 | +func GetPreviousConsensusState(clientStore sdk.KVStore, cdc codec.BinaryMarshaler, height exported.Height) (*ConsensusState, bool) { |
| 209 | + iterateStore := prefix.NewStore(clientStore, []byte(KeyIterateConsensusStatePrefix)) |
| 210 | + iterator := iterateStore.ReverseIterator(nil, bigEndianHeightBytes(height)) |
| 211 | + defer iterator.Close() |
| 212 | + |
| 213 | + if !iterator.Valid() { |
| 214 | + return nil, false |
| 215 | + } |
| 216 | + |
| 217 | + csKey := iterator.Value() |
| 218 | + |
| 219 | + return getTmConsensusState(clientStore, cdc, csKey) |
| 220 | +} |
| 221 | + |
| 222 | +// Helper function for GetNextConsensusState and GetPreviousConsensusState |
| 223 | +func getTmConsensusState(clientStore sdk.KVStore, cdc codec.BinaryMarshaler, key []byte) (*ConsensusState, bool) { |
| 224 | + bz := clientStore.Get(key) |
| 225 | + if bz == nil { |
| 226 | + return nil, false |
| 227 | + } |
| 228 | + |
| 229 | + consensusStateI, err := clienttypes.UnmarshalConsensusState(cdc, bz) |
| 230 | + if err != nil { |
| 231 | + return nil, false |
| 232 | + } |
| 233 | + |
| 234 | + consensusState, ok := consensusStateI.(*ConsensusState) |
| 235 | + if !ok { |
| 236 | + return nil, false |
| 237 | + } |
| 238 | + return consensusState, true |
| 239 | +} |
| 240 | + |
| 241 | +func bigEndianHeightBytes(height exported.Height) []byte { |
| 242 | + heightBytes := make([]byte, 16) |
| 243 | + binary.BigEndian.PutUint64(heightBytes, height.GetRevisionNumber()) |
| 244 | + binary.BigEndian.PutUint64(heightBytes[8:], height.GetRevisionHeight()) |
| 245 | + return heightBytes |
| 246 | +} |
0 commit comments