Skip to content

Commit b92086b

Browse files
crodriguezvegamergify[bot]
authored andcommitted
fix: forbid negative values for trusting period, unbonding period and max clock drift (#2555)
Co-authored-by: Carlos Rodriguez <[email protected]> (cherry picked from commit eab24e8) # Conflicts: # modules/light-clients/07-tendermint/client_state.go # modules/light-clients/07-tendermint/client_state_test.go
1 parent ba71852 commit b92086b

File tree

3 files changed

+1046
-0
lines changed

3 files changed

+1046
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
4343
### State Machine Breaking
4444

4545
* (transfer) [\#2377](https://github.com/cosmos/ibc-go/pull/2377) Adding `sequence` to `MsgTransferResponse`.
46+
* (light-clients/07-tendermint) [\#2554](https://github.com/cosmos/ibc-go/pull/2554) Forbid negative values for `TrustingPeriod`, `UnbondingPeriod` and `MaxClockDrift` (as specified in ICS-07).
4647

4748
### Improvements
4849

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
package tendermint
2+
3+
import (
4+
"strings"
5+
"time"
6+
7+
ics23 "github.com/confio/ics23/go"
8+
"github.com/cosmos/cosmos-sdk/codec"
9+
sdk "github.com/cosmos/cosmos-sdk/types"
10+
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
11+
"github.com/tendermint/tendermint/light"
12+
tmtypes "github.com/tendermint/tendermint/types"
13+
14+
clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types"
15+
commitmenttypes "github.com/cosmos/ibc-go/v6/modules/core/23-commitment/types"
16+
"github.com/cosmos/ibc-go/v6/modules/core/exported"
17+
)
18+
19+
var _ exported.ClientState = (*ClientState)(nil)
20+
21+
// NewClientState creates a new ClientState instance
22+
func NewClientState(
23+
chainID string, trustLevel Fraction,
24+
trustingPeriod, ubdPeriod, maxClockDrift time.Duration,
25+
latestHeight clienttypes.Height, specs []*ics23.ProofSpec,
26+
upgradePath []string,
27+
) *ClientState {
28+
return &ClientState{
29+
ChainId: chainID,
30+
TrustLevel: trustLevel,
31+
TrustingPeriod: trustingPeriod,
32+
UnbondingPeriod: ubdPeriod,
33+
MaxClockDrift: maxClockDrift,
34+
LatestHeight: latestHeight,
35+
FrozenHeight: clienttypes.ZeroHeight(),
36+
ProofSpecs: specs,
37+
UpgradePath: upgradePath,
38+
}
39+
}
40+
41+
// GetChainID returns the chain-id
42+
func (cs ClientState) GetChainID() string {
43+
return cs.ChainId
44+
}
45+
46+
// ClientType is tendermint.
47+
func (cs ClientState) ClientType() string {
48+
return exported.Tendermint
49+
}
50+
51+
// GetLatestHeight returns latest block height.
52+
func (cs ClientState) GetLatestHeight() exported.Height {
53+
return cs.LatestHeight
54+
}
55+
56+
// GetTimestampAtHeight returns the timestamp in nanoseconds of the consensus state at the given height.
57+
func (cs ClientState) GetTimestampAtHeight(
58+
ctx sdk.Context,
59+
clientStore sdk.KVStore,
60+
cdc codec.BinaryCodec,
61+
height exported.Height,
62+
) (uint64, error) {
63+
// get consensus state at height from clientStore to check for expiry
64+
consState, found := GetConsensusState(clientStore, cdc, height)
65+
if !found {
66+
return 0, sdkerrors.Wrapf(clienttypes.ErrConsensusStateNotFound, "height (%s)", height)
67+
}
68+
return consState.GetTimestamp(), nil
69+
}
70+
71+
// Status returns the status of the tendermint client.
72+
// The client may be:
73+
// - Active: FrozenHeight is zero and client is not expired
74+
// - Frozen: Frozen Height is not zero
75+
// - Expired: the latest consensus state timestamp + trusting period <= current time
76+
//
77+
// A frozen client will become expired, so the Frozen status
78+
// has higher precedence.
79+
func (cs ClientState) Status(
80+
ctx sdk.Context,
81+
clientStore sdk.KVStore,
82+
cdc codec.BinaryCodec,
83+
) exported.Status {
84+
if !cs.FrozenHeight.IsZero() {
85+
return exported.Frozen
86+
}
87+
88+
// get latest consensus state from clientStore to check for expiry
89+
consState, found := GetConsensusState(clientStore, cdc, cs.GetLatestHeight())
90+
if !found {
91+
// if the client state does not have an associated consensus state for its latest height
92+
// then it must be expired
93+
return exported.Expired
94+
}
95+
96+
if cs.IsExpired(consState.Timestamp, ctx.BlockTime()) {
97+
return exported.Expired
98+
}
99+
100+
return exported.Active
101+
}
102+
103+
// IsExpired returns whether or not the client has passed the trusting period since the last
104+
// update (in which case no headers are considered valid).
105+
func (cs ClientState) IsExpired(latestTimestamp, now time.Time) bool {
106+
expirationTime := latestTimestamp.Add(cs.TrustingPeriod)
107+
return !expirationTime.After(now)
108+
}
109+
110+
// Validate performs a basic validation of the client state fields.
111+
func (cs ClientState) Validate() error {
112+
if strings.TrimSpace(cs.ChainId) == "" {
113+
return sdkerrors.Wrap(ErrInvalidChainID, "chain id cannot be empty string")
114+
}
115+
116+
// NOTE: the value of tmtypes.MaxChainIDLen may change in the future.
117+
// If this occurs, the code here must account for potential difference
118+
// between the tendermint version being run by the counterparty chain
119+
// and the tendermint version used by this light client.
120+
// https://github.com/cosmos/ibc-go/issues/177
121+
if len(cs.ChainId) > tmtypes.MaxChainIDLen {
122+
return sdkerrors.Wrapf(ErrInvalidChainID, "chainID is too long; got: %d, max: %d", len(cs.ChainId), tmtypes.MaxChainIDLen)
123+
}
124+
125+
if err := light.ValidateTrustLevel(cs.TrustLevel.ToTendermint()); err != nil {
126+
return err
127+
}
128+
if cs.TrustingPeriod <= 0 {
129+
return sdkerrors.Wrap(ErrInvalidTrustingPeriod, "trusting period must be greater than zero")
130+
}
131+
if cs.UnbondingPeriod <= 0 {
132+
return sdkerrors.Wrap(ErrInvalidUnbondingPeriod, "unbonding period must be greater than zero")
133+
}
134+
if cs.MaxClockDrift <= 0 {
135+
return sdkerrors.Wrap(ErrInvalidMaxClockDrift, "max clock drift must be greater than zero")
136+
}
137+
138+
// the latest height revision number must match the chain id revision number
139+
if cs.LatestHeight.RevisionNumber != clienttypes.ParseChainID(cs.ChainId) {
140+
return sdkerrors.Wrapf(ErrInvalidHeaderHeight,
141+
"latest height revision number must match chain id revision number (%d != %d)", cs.LatestHeight.RevisionNumber, clienttypes.ParseChainID(cs.ChainId))
142+
}
143+
if cs.LatestHeight.RevisionHeight == 0 {
144+
return sdkerrors.Wrapf(ErrInvalidHeaderHeight, "tendermint client's latest height revision height cannot be zero")
145+
}
146+
if cs.TrustingPeriod >= cs.UnbondingPeriod {
147+
return sdkerrors.Wrapf(
148+
ErrInvalidTrustingPeriod,
149+
"trusting period (%s) should be < unbonding period (%s)", cs.TrustingPeriod, cs.UnbondingPeriod,
150+
)
151+
}
152+
153+
if cs.ProofSpecs == nil {
154+
return sdkerrors.Wrap(ErrInvalidProofSpecs, "proof specs cannot be nil for tm client")
155+
}
156+
for i, spec := range cs.ProofSpecs {
157+
if spec == nil {
158+
return sdkerrors.Wrapf(ErrInvalidProofSpecs, "proof spec cannot be nil at index: %d", i)
159+
}
160+
}
161+
// UpgradePath may be empty, but if it isn't, each key must be non-empty
162+
for i, k := range cs.UpgradePath {
163+
if strings.TrimSpace(k) == "" {
164+
return sdkerrors.Wrapf(clienttypes.ErrInvalidClient, "key in upgrade path at index %d cannot be empty", i)
165+
}
166+
}
167+
168+
return nil
169+
}
170+
171+
// GetProofSpecs returns the format the client expects for proof verification
172+
// as a string array specifying the proof type for each position in chained proof
173+
func (cs ClientState) GetProofSpecs() []*ics23.ProofSpec {
174+
return cs.ProofSpecs
175+
}
176+
177+
// ZeroCustomFields returns a ClientState that is a copy of the current ClientState
178+
// with all client customizable fields zeroed out
179+
func (cs ClientState) ZeroCustomFields() exported.ClientState {
180+
// copy over all chain-specified fields
181+
// and leave custom fields empty
182+
return &ClientState{
183+
ChainId: cs.ChainId,
184+
UnbondingPeriod: cs.UnbondingPeriod,
185+
LatestHeight: cs.LatestHeight,
186+
ProofSpecs: cs.ProofSpecs,
187+
UpgradePath: cs.UpgradePath,
188+
}
189+
}
190+
191+
// Initialize will check that initial consensus state is a Tendermint consensus state
192+
// and will store ProcessedTime for initial consensus state as ctx.BlockTime()
193+
func (cs ClientState) Initialize(ctx sdk.Context, _ codec.BinaryCodec, clientStore sdk.KVStore, consState exported.ConsensusState) error {
194+
if _, ok := consState.(*ConsensusState); !ok {
195+
return sdkerrors.Wrapf(clienttypes.ErrInvalidConsensus, "invalid initial consensus state. expected type: %T, got: %T",
196+
&ConsensusState{}, consState)
197+
}
198+
// set metadata for initial consensus state.
199+
setConsensusMetadata(ctx, clientStore, cs.GetLatestHeight())
200+
return nil
201+
}
202+
203+
// 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.
204+
// The caller is expected to construct the full CommitmentPath from a CommitmentPrefix and a standardized path (as defined in ICS 24).
205+
func (cs ClientState) VerifyMembership(
206+
ctx sdk.Context,
207+
clientStore sdk.KVStore,
208+
cdc codec.BinaryCodec,
209+
height exported.Height,
210+
delayTimePeriod uint64,
211+
delayBlockPeriod uint64,
212+
proof []byte,
213+
path []byte,
214+
value []byte,
215+
) error {
216+
if cs.GetLatestHeight().LT(height) {
217+
return sdkerrors.Wrapf(
218+
sdkerrors.ErrInvalidHeight,
219+
"client state height < proof height (%d < %d), please ensure the client has been updated", cs.GetLatestHeight(), height,
220+
)
221+
}
222+
223+
if err := verifyDelayPeriodPassed(ctx, clientStore, height, delayTimePeriod, delayBlockPeriod); err != nil {
224+
return err
225+
}
226+
227+
var merkleProof commitmenttypes.MerkleProof
228+
if err := cdc.Unmarshal(proof, &merkleProof); err != nil {
229+
return sdkerrors.Wrap(commitmenttypes.ErrInvalidProof, "failed to unmarshal proof into ICS 23 commitment merkle proof")
230+
}
231+
232+
var merklePath commitmenttypes.MerklePath
233+
if err := cdc.Unmarshal(path, &merklePath); err != nil {
234+
return sdkerrors.Wrap(commitmenttypes.ErrInvalidProof, "failed to unmarshal path into ICS 23 commitment merkle path")
235+
}
236+
237+
consensusState, found := GetConsensusState(clientStore, cdc, height)
238+
if !found {
239+
return sdkerrors.Wrap(clienttypes.ErrConsensusStateNotFound, "please ensure the proof was constructed against a height that exists on the client")
240+
}
241+
242+
if err := merkleProof.VerifyMembership(cs.ProofSpecs, consensusState.GetRoot(), merklePath, value); err != nil {
243+
return err
244+
}
245+
246+
return nil
247+
}
248+
249+
// VerifyNonMembership is a generic proof verification method which verifies the absence of a given CommitmentPath at a specified height.
250+
// The caller is expected to construct the full CommitmentPath from a CommitmentPrefix and a standardized path (as defined in ICS 24).
251+
func (cs ClientState) VerifyNonMembership(
252+
ctx sdk.Context,
253+
clientStore sdk.KVStore,
254+
cdc codec.BinaryCodec,
255+
height exported.Height,
256+
delayTimePeriod uint64,
257+
delayBlockPeriod uint64,
258+
proof []byte,
259+
path []byte,
260+
) error {
261+
if cs.GetLatestHeight().LT(height) {
262+
return sdkerrors.Wrapf(
263+
sdkerrors.ErrInvalidHeight,
264+
"client state height < proof height (%d < %d), please ensure the client has been updated", cs.GetLatestHeight(), height,
265+
)
266+
}
267+
268+
if err := verifyDelayPeriodPassed(ctx, clientStore, height, delayTimePeriod, delayBlockPeriod); err != nil {
269+
return err
270+
}
271+
272+
var merkleProof commitmenttypes.MerkleProof
273+
if err := cdc.Unmarshal(proof, &merkleProof); err != nil {
274+
return sdkerrors.Wrap(commitmenttypes.ErrInvalidProof, "failed to unmarshal proof into ICS 23 commitment merkle proof")
275+
}
276+
277+
var merklePath commitmenttypes.MerklePath
278+
if err := cdc.Unmarshal(path, &merklePath); err != nil {
279+
return sdkerrors.Wrap(commitmenttypes.ErrInvalidProof, "failed to unmarshal path into ICS 23 commitment merkle path")
280+
}
281+
282+
consensusState, found := GetConsensusState(clientStore, cdc, height)
283+
if !found {
284+
return sdkerrors.Wrap(clienttypes.ErrConsensusStateNotFound, "please ensure the proof was constructed against a height that exists on the client")
285+
}
286+
287+
if err := merkleProof.VerifyNonMembership(cs.ProofSpecs, consensusState.GetRoot(), merklePath); err != nil {
288+
return err
289+
}
290+
291+
return nil
292+
}
293+
294+
// verifyDelayPeriodPassed will ensure that at least delayTimePeriod amount of time and delayBlockPeriod number of blocks have passed
295+
// since consensus state was submitted before allowing verification to continue.
296+
func verifyDelayPeriodPassed(ctx sdk.Context, store sdk.KVStore, proofHeight exported.Height, delayTimePeriod, delayBlockPeriod uint64) error {
297+
if delayTimePeriod != 0 {
298+
// check that executing chain's timestamp has passed consensusState's processed time + delay time period
299+
processedTime, ok := GetProcessedTime(store, proofHeight)
300+
if !ok {
301+
return sdkerrors.Wrapf(ErrProcessedTimeNotFound, "processed time not found for height: %s", proofHeight)
302+
}
303+
304+
currentTimestamp := uint64(ctx.BlockTime().UnixNano())
305+
validTime := processedTime + delayTimePeriod
306+
307+
// NOTE: delay time period is inclusive, so if currentTimestamp is validTime, then we return no error
308+
if currentTimestamp < validTime {
309+
return sdkerrors.Wrapf(ErrDelayPeriodNotPassed, "cannot verify packet until time: %d, current time: %d",
310+
validTime, currentTimestamp)
311+
}
312+
313+
}
314+
315+
if delayBlockPeriod != 0 {
316+
// check that executing chain's height has passed consensusState's processed height + delay block period
317+
processedHeight, ok := GetProcessedHeight(store, proofHeight)
318+
if !ok {
319+
return sdkerrors.Wrapf(ErrProcessedHeightNotFound, "processed height not found for height: %s", proofHeight)
320+
}
321+
322+
currentHeight := clienttypes.GetSelfHeight(ctx)
323+
validHeight := clienttypes.NewHeight(processedHeight.GetRevisionNumber(), processedHeight.GetRevisionHeight()+delayBlockPeriod)
324+
325+
// NOTE: delay block period is inclusive, so if currentHeight is validHeight, then we return no error
326+
if currentHeight.LT(validHeight) {
327+
return sdkerrors.Wrapf(ErrDelayPeriodNotPassed, "cannot verify packet until height: %s, current height: %s",
328+
validHeight, currentHeight)
329+
}
330+
}
331+
332+
return nil
333+
}

0 commit comments

Comments
 (0)