Search Apps Documentation Source Content File Folder Download Copy Actions Download

tendermint.gno

21.77 Kb · 580 lines
  1package tendermint
  2
  3import (
  4	"bytes"
  5	"errors"
  6	"time"
  7
  8	"gno.land/p/aib/encoding/proto"
  9	"gno.land/p/aib/ibc/lightclient"
 10	"gno.land/p/aib/ibc/types"
 11	"gno.land/p/aib/ics23"
 12	"gno.land/p/nt/bptree/v0"
 13	"gno.land/p/nt/ufmt/v0"
 14)
 15
 16type TMLightClient struct {
 17	ClientState            *ClientState
 18	ConsensusStateByHeight *bptree.BPTree // height:*ConsensusState
 19}
 20
 21func NewTMLightClient() *TMLightClient {
 22	return &TMLightClient{
 23		ConsensusStateByHeight: bptree.NewBPTree32(),
 24	}
 25}
 26
 27var _ lightclient.Interface = (*TMLightClient)(nil)
 28
 29// Implements lightclient.Interface
 30func (tm *TMLightClient) Initialize(clientState lightclient.ClientState, consensusState lightclient.ConsensusState) error {
 31	cs := clientState.(ClientState)
 32	tm.ClientState = &cs
 33	in := consensusState.(ConsensusState)
 34	// Construct a fresh ConsensusState in our own (caller's) realm context
 35	// rather than mutating the inbound value: the inbound value carries the
 36	// constructor realm's PkgID stamp, and borrow rule #2 makes writes to
 37	// foreign-stamped fields panic with "readonly tainted object".
 38	consState := ConsensusState{
 39		Timestamp:          in.Timestamp,
 40		Root:               in.Root,
 41		NextValidatorsHash: in.NextValidatorsHash,
 42		processedTime:      time.Now(),
 43		processedHeight:    types.GetSelfHeight(),
 44	}
 45	tm.SetConsensusState(cs.LatestHeight, &consState)
 46	return nil
 47}
 48
 49// Implements lightclient.Interface
 50func (tm *TMLightClient) VerifyClientMessage(clientMsg lightclient.ClientMessage) error {
 51	switch msg := clientMsg.(type) {
 52	case *MsgHeader:
 53		return tm.verifyHeader(msg)
 54	case *Misbehaviour:
 55		return tm.verifyMisbehavior(msg)
 56	default:
 57		return errors.New("unknown client message type")
 58	}
 59}
 60
 61// Implements lightclient.Interface
 62func (tm *TMLightClient) CheckForMisbehaviour(clientMsg lightclient.ClientMessage) bool {
 63	switch msg := clientMsg.(type) {
 64	case *MsgHeader:
 65		consState := msg.ConsensusState()
 66		// Check if the Client store already has a consensus state for the header's
 67		// height.
 68		// If the consensus state exists, and it matches the header then we return
 69		// early since header has already been submitted in a previous
 70		// UpdateClient.
 71		if existingConsState, found := tm.GetConsensusState(msg.GetHeight()); found {
 72			// This header has already been submitted and the necessary state is
 73			// already stored in client store, thus we can return early without
 74			// further validation.
 75			if existingConsState.Equal(consState) {
 76				return false
 77			}
 78			// A consensus state already exists for this height, but it does not
 79			// match the provided header. The assumption is that Header has already
 80			// been validated. Thus we can return true as misbehaviour is present
 81			return true
 82		}
 83
 84		// Check that consensus state timestamps are monotonic
 85		prevCons, prevOk := tm.GetPreviousConsensusState(msg.GetHeight())
 86		nextCons, nextOk := tm.GetNextConsensusState(msg.GetHeight())
 87		// if previous consensus state exists, check consensus state time is
 88		// greater than previous consensus state time if previous consensus state
 89		// is not before current consensus state return true
 90		if prevOk && !prevCons.Timestamp.Before(consState.Timestamp) {
 91			return true
 92		}
 93		// if next consensus state exists, check consensus state time is less than
 94		// next consensus state time if next consensus state is not after current
 95		// consensus state return true
 96		if nextOk && !nextCons.Timestamp.After(consState.Timestamp) {
 97			return true
 98		}
 99
100	case *Misbehaviour:
101		// if heights are equal check that this is valid misbehaviour of a fork
102		// otherwise if heights are unequal check that this is valid misbehavior of
103		// BFT time violation.
104		if msg.Header1.GetHeight().EQ(msg.Header2.GetHeight()) {
105			// Ensure that Commit Hashes are different
106			if !bytes.Equal(
107				msg.Header1.Commit.BlockID.Hash,
108				msg.Header2.Commit.BlockID.Hash,
109			) {
110				return true
111			}
112		} else if !msg.Header1.Header.Time.After(msg.Header2.Header.Time) {
113			// Header1 is at greater height than Header2 (ensured by
114			// Misbehaviour.ValidateBasic()), therefore Header1 time must be less
115			// than or equal to Header2 time in order to be valid misbehaviour
116			// (violation of monotonic time).
117			return true
118		}
119	}
120	return false
121}
122
123// FrozenHeight is same for all misbehaviour
124var FrozenHeight = types.NewHeight(0, 1)
125
126// Implements lightclient.Interface
127func (tm *TMLightClient) UpdateStateOnMisbehaviour(clientMsg lightclient.ClientMessage) {
128	// Whole-slot replace instead of field-write: tm.ClientState was
129	// constructed in the caller's realm (the relayer's MsgCreateClient
130	// payload) so its fields are foreign-stamped and v2's borrow rule
131	// rejects direct writes. tm.ClientState (the slot) lives on tm,
132	// which is core-owned, so reassigning the pointer is fine.
133	tm.ClientState = cloneClientStateWithFrozenHeight(tm.ClientState, FrozenHeight)
134}
135
136// Implements lightclient.Interface
137func (tm *TMLightClient) UpdateState(clientMsg lightclient.ClientMessage) []types.Height {
138	msg, ok := clientMsg.(*MsgHeader)
139	if !ok {
140		// clientMsg is an invalid Misbehaviour, no update necessary
141		return []types.Height{}
142	}
143
144	// check for duplicate update
145	msgHeight := msg.GetHeight()
146	if tm.HasConsensusState(msgHeight) {
147		// perform no-op
148		return []types.Height{msgHeight}
149	}
150	// Update latestHeight if required. Whole-slot replace; see
151	// UpdateStateOnMisbehaviour for the v2 borrow-rule rationale.
152	if msgHeight.GT(tm.ClientState.LatestHeight) {
153		tm.ClientState = cloneClientStateWithLatestHeight(tm.ClientState, msgHeight)
154	}
155	// Build and store new consensus state from clientMsg
156	consState := &ConsensusState{
157		Timestamp:          msg.Header.Time,
158		Root:               NewMerkleRoot(msg.Header.AppHash),
159		NextValidatorsHash: msg.Header.NextValidatorsHash,
160		processedTime:      time.Now(),
161		processedHeight:    types.GetSelfHeight(),
162	}
163	tm.SetConsensusState(msgHeight, consState)
164	return []types.Height{msgHeight}
165}
166
167// Implements lightclient.Interface
168func (tm *TMLightClient) VerifyMembership(height types.Height,
169	proofs []ics23.CommitmentProof, path types.MerklePath,
170	value []byte) error {
171	if tm.ClientState.LatestHeight.LT(height) {
172		return ufmt.Errorf(
173			"client state height < proof height (%s < %s), please ensure the client has been updated", tm.ClientState.LatestHeight, height,
174		)
175	}
176	if len(value) == 0 {
177		return ufmt.Errorf("empty value in membership proof")
178	}
179	if len(tm.ClientState.ProofSpecs) != len(proofs) {
180		return ufmt.Errorf(
181			"length of specs: %d not equal to length of proof: %d",
182			len(tm.ClientState.ProofSpecs), len(proofs),
183		)
184	}
185	if len(path.KeyPath) != len(proofs) {
186		return ufmt.Errorf(
187			"path length %d not same as proof %d", len(path.KeyPath), len(proofs),
188		)
189	}
190	consState, found := tm.GetConsensusState(height)
191	if !found {
192		return ufmt.Errorf("please ensure the proof was constructed against a height that exists on the client")
193	}
194	return tm.verifyChainedMembershipProof(consState.Root.Hash, proofs, path, value, 0)
195}
196
197// Implements lightclient.Interface
198func (tm *TMLightClient) VerifyNonMembership(height types.Height,
199	proofs []ics23.CommitmentProof, path types.MerklePath) error {
200	if tm.ClientState.LatestHeight.LT(height) {
201		return ufmt.Errorf(
202			"client state height < proof height (%d < %d), please ensure the client has been updated", tm.ClientState.LatestHeight, height,
203		)
204	}
205	if len(tm.ClientState.ProofSpecs) != len(proofs) {
206		return ufmt.Errorf(
207			"length of specs: %d not equal to length of proof: %d",
208			len(tm.ClientState.ProofSpecs), len(proofs),
209		)
210	}
211	if len(path.KeyPath) != len(proofs) {
212		return ufmt.Errorf(
213			"path length %d not same as proof %d", len(path.KeyPath), len(proofs),
214		)
215	}
216
217	consState, found := tm.GetConsensusState(height)
218	if !found {
219		return ufmt.Errorf("please ensure the proof was constructed against a height that exists on the client")
220	}
221	// VerifyNonMembership will verify the absence of key in lowest subtree, and
222	// then chain inclusion proofs of all subroots up to final root.
223	nonexist := proofs[0].GetNonexist()
224	if nonexist == nil {
225		return ufmt.Errorf("commitment proof must be non-existence proof for verifying non-membership")
226	}
227	subroot, err := nonexist.Calculate()
228	if err != nil {
229		return ufmt.Errorf("could not calculate root for proof index 0, merkle tree is likely empty. %v", err)
230	}
231
232	key := path.KeyPath[len(path.KeyPath)-1]
233	if err := nonexist.Verify(tm.ClientState.ProofSpecs[0], subroot, key); err != nil {
234		return ufmt.Errorf("failed to verify non-membership proof with key %s: %v", string(key), err)
235	}
236
237	// Verify chained membership proof starting from index 1 with value = subroot
238	return tm.verifyChainedMembershipProof(consState.Root.Hash, proofs, path, subroot, 1)
239}
240
241// Implements lightclient.Interface
242func (tm *TMLightClient) Status() lightclient.Status {
243	if !tm.ClientState.FrozenHeight.IsZero() {
244		return lightclient.Frozen
245	}
246	// get latest consensus state to check for expiry
247	lastConsState, found := tm.GetConsensusState(tm.LatestHeight())
248	if !found {
249		// if the client state does not have an associated consensus state for its
250		// latest height then it must be expired
251		return lightclient.Expired
252	}
253	if tm.IsExpired(lastConsState.Timestamp, time.Now()) {
254		return lightclient.Expired
255	}
256	return lightclient.Active
257}
258
259// Implements lightclient.Interface
260func (tm *TMLightClient) LatestHeight() types.Height {
261	return tm.ClientState.LatestHeight
262}
263
264// Implements lightclient.Interface
265func (tm *TMLightClient) TimestampAtHeight(height types.Height) (uint64, error) {
266	cs, found := tm.GetConsensusState(height)
267	if !found {
268		return 0, ufmt.Errorf("no consensus state found for height %s", height.String())
269	}
270	return uint64(cs.Timestamp.Unix()), nil
271}
272
273// Implements lightclient.Interface.
274//
275// RecoverClient copies the substitute's consensus state at its latest height
276// into the subject (tm), updates the subject's ChainID, LatestHeight and
277// TrustingPeriod to match the substitute, and un-freezes the subject by
278// resetting FrozenHeight. The substitute and subject client states must match
279// in TrustLevel, UnbondingPeriod, MaxClockDrift, ProofSpecs and UpgradePath.
280func (tm *TMLightClient) RecoverClient(substitute lightclient.Interface) error {
281	if substitute == nil {
282		return errors.New("substitute must not be nil")
283	}
284	sub, ok := substitute.(*TMLightClient)
285	if !ok {
286		return ufmt.Errorf("substitute client type mismatch: expected *TMLightClient, got %T", substitute)
287	}
288	if field := mismatchingClientStateField(tm.ClientState, sub.ClientState); field != "" {
289		return ufmt.Errorf("subject and substitute client states differ on %s", field)
290	}
291	subLatest := sub.ClientState.LatestHeight
292	subConsState, found := sub.GetConsensusState(subLatest)
293	if !found {
294		return ufmt.Errorf("substitute consensus state not found at height %s", subLatest)
295	}
296
297	// Whole-slot replace, single allocation. See UpdateStateOnMisbehaviour
298	// for the v2 borrow-rule rationale on why per-field writes don't work.
299	tm.ClientState = &ClientState{
300		ChainID:         sub.ClientState.ChainID,
301		TrustLevel:      tm.ClientState.TrustLevel,
302		TrustingPeriod:  sub.ClientState.TrustingPeriod,
303		UnbondingPeriod: tm.ClientState.UnbondingPeriod,
304		MaxClockDrift:   tm.ClientState.MaxClockDrift,
305		FrozenHeight:    types.ZeroHeight(),
306		LatestHeight:    subLatest,
307		ProofSpecs:      tm.ClientState.ProofSpecs,
308		UpgradePath:     tm.ClientState.UpgradePath,
309	}
310	tm.SetConsensusState(subLatest, subConsState)
311	return nil
312}
313
314// cloneClientStateWithFrozenHeight returns a fresh *ClientState in the
315// caller's realm with FrozenHeight replaced. Used to whole-slot replace
316// tm.ClientState when a per-field write would violate borrow rules.
317func cloneClientStateWithFrozenHeight(cs *ClientState, h types.Height) *ClientState {
318	return &ClientState{
319		ChainID:         cs.ChainID,
320		TrustLevel:      cs.TrustLevel,
321		TrustingPeriod:  cs.TrustingPeriod,
322		UnbondingPeriod: cs.UnbondingPeriod,
323		MaxClockDrift:   cs.MaxClockDrift,
324		FrozenHeight:    h,
325		LatestHeight:    cs.LatestHeight,
326		ProofSpecs:      cs.ProofSpecs,
327		UpgradePath:     cs.UpgradePath,
328	}
329}
330
331// cloneClientStateWithLatestHeight is the LatestHeight counterpart of
332// cloneClientStateWithFrozenHeight.
333func cloneClientStateWithLatestHeight(cs *ClientState, h types.Height) *ClientState {
334	return &ClientState{
335		ChainID:         cs.ChainID,
336		TrustLevel:      cs.TrustLevel,
337		TrustingPeriod:  cs.TrustingPeriod,
338		UnbondingPeriod: cs.UnbondingPeriod,
339		MaxClockDrift:   cs.MaxClockDrift,
340		FrozenHeight:    cs.FrozenHeight,
341		LatestHeight:    h,
342		ProofSpecs:      cs.ProofSpecs,
343		UpgradePath:     cs.UpgradePath,
344	}
345}
346
347// mismatchingClientStateField returns the name of the first client-state field
348// that differs between subject and substitute, or an empty string if all
349// parameters that must not change during a client recovery match. ChainID,
350// LatestHeight, FrozenHeight and TrustingPeriod are allowed to differ — the
351// substitute's TrustingPeriod is adopted by the subject.
352func mismatchingClientStateField(subject, substitute *ClientState) string {
353	if subject.TrustLevel != substitute.TrustLevel {
354		return "TrustLevel"
355	}
356	if subject.UnbondingPeriod != substitute.UnbondingPeriod {
357		return "UnbondingPeriod"
358	}
359	if subject.MaxClockDrift != substitute.MaxClockDrift {
360		return "MaxClockDrift"
361	}
362	if len(subject.ProofSpecs) != len(substitute.ProofSpecs) {
363		return "ProofSpecs"
364	}
365	for i := range subject.ProofSpecs {
366		if !subject.ProofSpecs[i].Equal(substitute.ProofSpecs[i]) {
367			return "ProofSpecs"
368		}
369	}
370	if len(subject.UpgradePath) != len(substitute.UpgradePath) {
371		return "UpgradePath"
372	}
373	for i := range subject.UpgradePath {
374		if subject.UpgradePath[i] != substitute.UpgradePath[i] {
375			return "UpgradePath"
376		}
377	}
378	return ""
379}
380
381// Implements lightclient.Interface
382func (tm *TMLightClient) VerifyUpgradeAndUpdateState(newClient, newConsState,
383	upgradeClientProof, upgradeConsensusStateProof any) error {
384	upgradedClientState, ok := newClient.(ClientState)
385	if !ok {
386		return ufmt.Errorf("upgraded client state must be tendermint.ClientState, got %T", newClient)
387	}
388	if err := upgradedClientState.ValidateBasic(); err != nil {
389		return ufmt.Errorf("invalid upgraded client state: %v", err)
390	}
391
392	upgradedConsensusState, ok := newConsState.(ConsensusState)
393	if !ok {
394		return ufmt.Errorf("upgraded consensus state must be tendermint.ConsensusState, got %T", newConsState)
395	}
396	if err := upgradedConsensusState.ValidateBasic(); err != nil {
397		return ufmt.Errorf("invalid upgraded consensus state: %v", err)
398	}
399
400	if len(tm.ClientState.UpgradePath) == 0 {
401		return errors.New("cannot upgrade client: no upgrade path set")
402	}
403	if !upgradedClientState.LatestHeight.GT(tm.ClientState.LatestHeight) {
404		return ufmt.Errorf(
405			"upgraded client latest height (%s) must be greater than current latest height (%s)",
406			upgradedClientState.LatestHeight, tm.ClientState.LatestHeight,
407		)
408	}
409
410	lastHeight := tm.ClientState.LatestHeight
411
412	clientProofs, ok := upgradeClientProof.([]ics23.CommitmentProof)
413	if !ok {
414		return ufmt.Errorf("upgradeClientProof must be []ics23.CommitmentProof, got %T", upgradeClientProof)
415	}
416	if len(clientProofs) == 0 {
417		return errors.New("upgradeClientProof cannot be empty")
418	}
419
420	consensusProofs, ok := upgradeConsensusStateProof.([]ics23.CommitmentProof)
421	if !ok {
422		return ufmt.Errorf("upgradeConsensusStateProof must be []ics23.CommitmentProof, got %T", upgradeConsensusStateProof)
423	}
424	if len(consensusProofs) == 0 {
425		return errors.New("upgradeConsensusStateProof cannot be empty")
426	}
427
428	path := tm.ClientState.UpgradePath
429	clientPath := buildUpgradeMerklePath(path, "upgradedClient", lastHeight)
430	// Counterparties commit the upgraded client/consensus states wrapped in
431	// google.protobuf.Any (cdc.MarshalInterface) and with the client's
432	// customizable fields zeroed, so we verify against the same shape.
433	clientValue := proto.MarshalAny(
434		ClientStateTypeURL,
435		upgradedClientState.ZeroCustomFields().ProtoMarshal(),
436	)
437	if err := tm.VerifyMembership(lastHeight, clientProofs, clientPath, clientValue); err != nil {
438		return ufmt.Errorf("failed to verify upgrade client proof: %v", err)
439	}
440
441	// "upgradedConsState" matches the SDK upgrade module's KeyUpgradedConsState
442	// (note: no "ensus" — must match what the counterparty stored).
443	consensusPath := buildUpgradeMerklePath(path, "upgradedConsState", lastHeight)
444	consensusValue := proto.MarshalAny(
445		ConsensusStateTypeURL,
446		upgradedConsensusState.ProtoMarshal(),
447	)
448	if err := tm.VerifyMembership(lastHeight, consensusProofs, consensusPath, consensusValue); err != nil {
449		return ufmt.Errorf("failed to verify upgrade consensus state proof: %v", err)
450	}
451
452	// Construct the new client by combining chain-specified fields from the
453	// upgraded client (ChainID, UnbondingPeriod, LatestHeight, ProofSpecs,
454	// UpgradePath) with the customizable fields preserved from the current
455	// client (TrustLevel, TrustingPeriod, MaxClockDrift). FrozenHeight is
456	// reset on a successful upgrade. If the unbonding period shrinks, the
457	// trusting period is scaled proportionally to maintain the security
458	// ratio.
459	trustingPeriod := tm.ClientState.TrustingPeriod
460	if upgradedClientState.UnbondingPeriod < tm.ClientState.UnbondingPeriod {
461		trustingPeriod = calculateNewTrustingPeriod(
462			trustingPeriod, tm.ClientState.UnbondingPeriod, upgradedClientState.UnbondingPeriod,
463		)
464	}
465	newClientState := &ClientState{
466		ChainID:         upgradedClientState.ChainID,
467		TrustLevel:      tm.ClientState.TrustLevel,
468		TrustingPeriod:  trustingPeriod,
469		UnbondingPeriod: upgradedClientState.UnbondingPeriod,
470		MaxClockDrift:   tm.ClientState.MaxClockDrift,
471		FrozenHeight:    types.ZeroHeight(),
472		LatestHeight:    upgradedClientState.LatestHeight,
473		ProofSpecs:      upgradedClientState.ProofSpecs,
474		UpgradePath:     upgradedClientState.UpgradePath,
475	}
476	if err := newClientState.ValidateBasic(); err != nil {
477		return ufmt.Errorf("upgraded client state failed basic validation: %v", err)
478	}
479
480	tm.ClientState = newClientState
481	// The upgraded consensus state is committed before the new chain
482	// produces any blocks, so its Root is a placeholder. Construct a fresh
483	// copy in our realm context with the sentinel Root so this consensus
484	// state cannot be used to verify packet proofs — real Roots arrive
485	// later via UpdateClient. Constructing a new value avoids the borrow
486	// rule #2 readonly write that hits an inbound foreign-stamped value.
487	newConsensusState := ConsensusState{
488		Timestamp:          upgradedConsensusState.Timestamp,
489		Root:               NewMerkleRoot([]byte(SentinelRoot)),
490		NextValidatorsHash: upgradedConsensusState.NextValidatorsHash,
491		processedTime:      time.Now(),
492		processedHeight:    types.GetSelfHeight(),
493	}
494	tm.SetConsensusState(newClientState.LatestHeight, &newConsensusState)
495
496	return nil
497}
498
499// calculateNewTrustingPeriod scales the trusting period proportionally to a
500// shorter unbonding period: newTrusting = trusting * newUnbonding /
501// currentUnbonding (with truncation). The math is performed in seconds to
502// stay within int64 range for realistic chain values; sub-second precision
503// is irrelevant for IBC trusting periods, which are measured in days.
504func calculateNewTrustingPeriod(trustingPeriod, currentUnbondingPeriod, newUnbondingPeriod time.Duration) time.Duration {
505	currentSec := int64(currentUnbondingPeriod / time.Second)
506	if currentSec == 0 {
507		return 0
508	}
509	trustingSec := int64(trustingPeriod / time.Second)
510	newUnbondingSec := int64(newUnbondingPeriod / time.Second)
511	return time.Duration(trustingSec*newUnbondingSec/currentSec) * time.Second
512}
513
514// buildUpgradeMerklePath constructs the merkle path the counterparty chain
515// stores the upgraded client/consensus state at. Each prefix element of
516// UpgradePath is kept as a separate KeyPath segment; the final element is
517// suffixed with the upgrade height and the leaf key (e.g. "upgradedClient"),
518// matching ibc-go's constructUpgrade*MerklePath. Caller must ensure
519// len(path) > 0.
520func buildUpgradeMerklePath(path []string, lastKey string, height types.Height) types.MerklePath {
521	keyPath := make([][]byte, 0, len(path))
522	for _, k := range path[:len(path)-1] {
523		keyPath = append(keyPath, []byte(k))
524	}
525	appendedKey := ufmt.Sprintf("%s/%d/%s", path[len(path)-1], height.RevisionHeight, lastKey)
526	keyPath = append(keyPath, []byte(appendedKey))
527	return types.MerklePath{KeyPath: keyPath}
528}
529
530func (tm *TMLightClient) SetConsensusState(height types.Height, consState *ConsensusState) {
531	tm.ConsensusStateByHeight.Set(height.StringNatSort(), consState)
532}
533
534func (tm *TMLightClient) HasConsensusState(height types.Height) bool {
535	return tm.ConsensusStateByHeight.Has(height.StringNatSort())
536}
537
538// GetConsensusState returns the consensus state mapped at height, if any.
539func (tm *TMLightClient) GetConsensusState(height types.Height) (*ConsensusState, bool) {
540	x, found := tm.ConsensusStateByHeight.Get(height.StringNatSort())
541	if !found {
542		return nil, false
543	}
544	return x.(*ConsensusState), true
545}
546
547// GetNextConsensusState returns the lowest consensus state that is larger than
548// the given height.
549func (tm *TMLightClient) GetNextConsensusState(height types.Height) (*ConsensusState, bool) {
550	var cs *ConsensusState
551	tm.ConsensusStateByHeight.Iterate(height.StringNatSort(), "", func(k string, v any) bool {
552		if k == height.StringNatSort() {
553			return false // ignore passed height
554		}
555		cs = v.(*ConsensusState)
556		return true
557	})
558	return cs, cs != nil
559}
560
561// GetPreviousConsensusState returns the highest consensus state that is lower
562// than the given height.
563func (tm *TMLightClient) GetPreviousConsensusState(height types.Height) (*ConsensusState, bool) {
564	var cs *ConsensusState
565	tm.ConsensusStateByHeight.ReverseIterate("", height.StringNatSort(), func(k string, v any) bool {
566		if k == height.StringNatSort() {
567			return false // ignore passed height
568		}
569		cs = v.(*ConsensusState)
570		return true
571	})
572	return cs, cs != nil
573}
574
575// IsExpired returns whether or not the client has passed the trusting period
576// since the last update (in which case no headers are considered valid).
577func (tm *TMLightClient) IsExpired(latestTimestamp, now time.Time) bool {
578	expirationTime := latestTimestamp.Add(tm.ClientState.TrustingPeriod)
579	return !expirationTime.After(now)
580}