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}