Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

client_verify.gno

9.51 Kb · 276 lines
  1package cometbls
  2
  3import (
  4	"bytes"
  5	"crypto/cometblszk"
  6	"encoding/hex"
  7	"time"
  8
  9	aibtypes "gno.land/p/aib/ibc/types"
 10	"gno.land/p/aib/ics23"
 11	"gno.land/p/nt/ufmt/v0"
 12)
 13
 14// verifyHeader verifies a CometBLS Header against the client's state and the
 15// trusted ConsensusState at header.TrustedHeight. It mirrors Union's
 16// cometbls verify_header in client.rs, preserving the order of checks and their
 17// error semantics:
 18//   - the trusted consensus state must exist for header.TrustedHeight
 19//   - header revision must match the trusted height revision
 20//   - the trusted timestamp must be strictly less than the header timestamp
 21//   - header height must be greater than the trusted height
 22//   - header time must be within the max clock drift of the current block time
 23//   - for an adjacent block, the header validators hash must match the trusted
 24//     next validators hash
 25//   - the Groth16 proof must verify against the trusted next validators hash
 26//
 27// now is the current block time, used for the clock-drift bound.
 28// Union reference:
 29// https://github.com/unionlabs/union/blob/1bb07590230e7c4d071f32ad7185be021a1a1789/cosmwasm/lightclient/cometbls/src/client.rs#L185-L254
 30func (c *CometblsLightClient) verifyHeader(clientState *ClientState, trustedConsensusState *ConsensusState, header *Header, now time.Time) error {
 31	// The untrusted (signed) header height. CometBLS carries a single revision,
 32	// so heights are compared against the trusted height's revision height.
 33	untrustedHeightNumber := header.SignedHeader.Height
 34	trustedHeightNumber := header.TrustedHeight.RevisionHeight
 35
 36	if untrustedHeightNumber <= int64(trustedHeightNumber) {
 37		return errorWithDetails(
 38			ErrInvalidHeader,
 39			ufmt.Sprintf("header height <= consensus state height (%d <= %d)", untrustedHeightNumber, trustedHeightNumber),
 40		)
 41	}
 42
 43	trustedTimestamp := trustedConsensusState.GetTimestamp()
 44	// Normalize to nanoseconds to follow tendermint convention
 45	untrustedTimestamp := uint64(header.GetTime().UnixNano())
 46
 47	if untrustedTimestamp <= trustedTimestamp {
 48		return errorWithDetails(
 49			ErrInvalidHeaderTimestamp,
 50			ufmt.Sprintf(
 51				"trusted header timestamp %d is greater than or equal to the new header timestamp %d",
 52				trustedTimestamp, untrustedTimestamp,
 53			),
 54		)
 55	}
 56
 57	currentBlockTime := uint64(now.UnixNano())
 58	if isClientExpiredAt(trustedTimestamp, clientState.TrustingPeriod, currentBlockTime) {
 59		return errorWithDetails(
 60			ErrTrustingPeriodExpired,
 61			ufmt.Sprintf("trusted consensus state at TrustedHeight %s has expired", header.TrustedHeight),
 62		)
 63	}
 64
 65	maxClockDriftTimestamp, overflow := checkedAddUint64(currentBlockTime, clientState.MaxClockDrift)
 66	if overflow {
 67		return errorWithDetails(ErrMathOverflow)
 68	}
 69
 70	if untrustedTimestamp >= maxClockDriftTimestamp {
 71		return errorWithDetails(
 72			ErrInvalidHeader,
 73			ufmt.Sprintf("header time >= max drift (%d >= currentTime + %d)", untrustedTimestamp, clientState.MaxClockDrift),
 74		)
 75	}
 76
 77	trustedValidatorsHash := trustedConsensusState.GetNextValidatorsHash()
 78	// For an adjacent block, the header validators hash must match the trusted
 79	// next validators hash.
 80	if untrustedHeightNumber == int64(trustedHeightNumber)+1 &&
 81		!bytes.Equal(header.SignedHeader.ValidatorsHash, trustedValidatorsHash) {
 82		return errorWithDetails(
 83			ErrInvalidHeader,
 84			ufmt.Sprintf(
 85				"the validators hash %s doesn't match the trusted validators hash %s for an adjacent block",
 86				hex.EncodeToString(header.SignedHeader.ValidatorsHash), hex.EncodeToString(trustedValidatorsHash),
 87			),
 88		)
 89	}
 90
 91	// The trusted next validators hash commits to the validator set that signed
 92	// the untrusted header; the Groth16 proof attests that they did. SignedHeader
 93	// is a crypto/cometblszk.LightHeader, so it feeds VerifyZKP directly.
 94	if err := cometblszk.VerifyZKP(
 95		clientState.ChainID,
 96		trustedValidatorsHash,
 97		*header.SignedHeader,
 98		header.ZeroKnowledgeProof,
 99	); err != nil {
100		return errorWithDetails(ErrInvalidHeader, "zero-knowledge proof verification failed: "+err.Error())
101	}
102
103	return nil
104}
105
106func isClientExpiredAt(consensusStateTimestamp uint64, trustingPeriod uint64, now uint64) bool {
107	expiresAt, overflow := checkedAddUint64(consensusStateTimestamp, trustingPeriod)
108	if overflow {
109		return true
110	}
111
112	return expiresAt < now
113}
114
115func checkedAddUint64(a uint64, b uint64) (uint64, bool) {
116	sum := a + b
117	if sum < a {
118		return 0, true
119	}
120
121	return sum, false
122}
123
124// verifyMisbehaviour mirrors Union's cometbls verify_misbehaviour in client.rs:
125// both headers must independently verify, and the pair is accepted only when it
126// matches Union's same-height or non-increasing-time conditions.
127// Union reference:
128// https://github.com/unionlabs/union/blob/1bb07590230e7c4d071f32ad7185be021a1a1789/cosmwasm/lightclient/cometbls/src/client.rs#L256-L281
129func (c *CometblsLightClient) verifyMisbehaviour(misbehaviour *Misbehaviour, now time.Time) error {
130	headerA := misbehaviour.Header1
131	headerB := misbehaviour.Header2
132
133	if headerA.SignedHeader.Height < headerB.SignedHeader.Height {
134		return errorWithDetails(ErrInvalidMisbehaviourHeaderSequence)
135	}
136
137	// Regardless of the type of misbehaviour, ensure that both headers are valid
138	// and would have been accepted by the light client.
139	if err := c.verifyMisbehaviourHeader("Header_1", headerA, now); err != nil {
140		return err
141	}
142
143	if err := c.verifyMisbehaviourHeader("Header_2", headerB, now); err != nil {
144		return err
145	}
146
147	if !isMisbehaviourPair(headerA, headerB) {
148		return errorWithDetails(ErrMisbehaviourNotFound)
149	}
150
151	return nil
152}
153
154// isMisbehaviourPair reports whether two independently-valid headers constitute
155// accepted misbehaviour, mirroring Union's conditions: at the same height the
156// signed headers must be identical; at different heights the earlier-listed
157// header must not be strictly newer than the later one.
158func isMisbehaviourPair(headerA, headerB *Header) bool {
159	if headerA.SignedHeader.Height == headerB.SignedHeader.Height {
160		return signedHeadersEqual(headerA.SignedHeader, headerB.SignedHeader)
161	}
162
163	return headerA.GetTime().UnixNano() <= headerB.GetTime().UnixNano()
164}
165
166func (c *CometblsLightClient) verifyMisbehaviourHeader(label string, header *Header, now time.Time) error {
167	prefix := ufmt.Sprintf("verifying %s in Misbehaviour failed: ", label)
168
169	consensusState, found := c.getConsensusState(*header.TrustedHeight)
170	if !found {
171		return errorWithDetails(
172			ErrInvalidMisbehaviour,
173			prefix+ufmt.Sprintf("could not get trusted consensus state for Header at TrustedHeight: %s", header.TrustedHeight),
174		)
175	}
176
177	if err := c.verifyHeader(c.clientState, consensusState, header, now); err != nil {
178		return errorWithDetails(ErrInvalidMisbehaviour, prefix+err.Error())
179	}
180
181	return nil
182}
183
184// signedHeadersEqual reports whether two light headers have identical content.
185func signedHeadersEqual(a, b *LightHeader) bool {
186	if a == nil || b == nil {
187		return a == b
188	}
189
190	if a.Height != b.Height || a.TimeSeconds != b.TimeSeconds || a.TimeNanos != b.TimeNanos {
191		return false
192	}
193
194	return bytes.Equal(a.ValidatorsHash, b.ValidatorsHash) &&
195		bytes.Equal(a.NextValidatorsHash, b.NextValidatorsHash) &&
196		bytes.Equal(a.AppHash, b.AppHash)
197}
198
199// verifyChainedMembershipProof verifies a chain of ICS23 membership proofs.
200// Proofs/specs are ordered from the lowest subtree to the root, while keys
201// are ordered from the root to the lowest subtree.
202//
203// Starting at index, each proof must commit to the subroot produced by the
204// previous proof (or the initial value). The final derived root must match
205// the expected root.
206//
207// index allows verification to begin after a non-membership proof, using the
208// previously computed subroot as the initial value.
209func (c *CometblsLightClient) verifyChainedMembershipProof(
210	root []byte,
211	proofs []ics23.CommitmentProof,
212	keys aibtypes.MerklePath,
213	value []byte,
214	index int,
215) error {
216	subroot := value
217	specs := ics23.GetSDKProofSpecs()
218
219	var err error
220
221	// Start with the provided value. If no proofs remain, subroot is compared
222	// directly against the expected root.
223	for i := index; i < len(proofs); i++ {
224		key := keys.KeyPath[len(keys.KeyPath)-1-i]
225
226		// verify membership of the proof at this index with appropriate key and value
227		subroot, err = c.verifyMembershipProofAt(proofs[i], specs[i], key, value, i)
228		if err != nil {
229			return err
230		}
231
232		// Use the computed subroot as the value for the next proof.
233		value = subroot
234	}
235
236	// Ensure the final chained root matches the expected root.
237	if !bytes.Equal(root, subroot) {
238		h1, h2 := hex.EncodeToString(root), hex.EncodeToString(subroot)
239
240		return errorWithDetails(
241			ErrInvalidProof,
242			ufmt.Sprintf("proof did not commit to expected root: %s, got: %s. Please ensure proof was submitted with correct proofHeight and to the correct chain.", h1, h2),
243		)
244	}
245
246	return nil
247}
248
249// verifyMembershipProofAt verifies the existence proof at a single chain index
250// and returns its calculated subroot. index is used only for error context.
251func (c *CometblsLightClient) verifyMembershipProofAt(proof ics23.CommitmentProof, spec *ics23.ProofSpec, key, value []byte, index int) ([]byte, error) {
252	exist := proof.GetExist()
253	if exist == nil {
254		return nil, errorWithDetails(
255			ErrInvalidProof,
256			"commitment proof must be existence proof foar verifying membership",
257		)
258	}
259
260	subroot, err := exist.Calculate()
261	if err != nil {
262		return nil, errorWithDetails(
263			ErrInvalidProof,
264			ufmt.Sprintf("could not calculate proof root at index %d, merkle tree may be empty. %v", index, err),
265		)
266	}
267
268	if err := exist.Verify(spec, subroot, key, value); err != nil {
269		return nil, errorWithDetails(
270			ErrInvalidProof,
271			ufmt.Sprintf("failed to verify membership proof at index %d: %v", index, err),
272		)
273	}
274
275	return subroot, nil
276}