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.40 Kb · 275 lines
  1package cometbls
  2
  3import (
  4	"bytes"
  5	"crypto/cometblszk"
  6	"encoding/hex"
  7	"time"
  8
  9	aibtypes "gno.land/p/onbloc/deps/ibc/types"
 10	"gno.land/p/onbloc/deps/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 verifies both headers and checks for same-height conflict
125// or non-increasing time across heights.
126// Union reference:
127// https://github.com/unionlabs/union/blob/3b1e861e9724601c458af278a30420cb11a72314/evm/contracts/clients/CometblsClient.sol#L280-L316
128func (c *CometblsLightClient) verifyMisbehaviour(misbehaviour *Misbehaviour, now time.Time) error {
129	headerA := misbehaviour.Header1
130	headerB := misbehaviour.Header2
131
132	if headerA.SignedHeader.Height < headerB.SignedHeader.Height {
133		return errorWithDetails(ErrInvalidMisbehaviourHeaderSequence)
134	}
135
136	// Regardless of the type of misbehaviour, ensure that both headers are valid
137	// and would have been accepted by the light client.
138	if err := c.verifyMisbehaviourHeader("Header_1", headerA, now); err != nil {
139		return err
140	}
141
142	if err := c.verifyMisbehaviourHeader("Header_2", headerB, now); err != nil {
143		return err
144	}
145
146	if !isMisbehaviourPair(headerA, headerB) {
147		return errorWithDetails(ErrMisbehaviourNotFound)
148	}
149
150	return nil
151}
152
153// isMisbehaviourPair reports whether two independently-valid headers constitute
154// accepted misbehaviour: at the same height the signed headers must differ
155// (equivocation); at different heights the earlier-listed header must not be
156// strictly newer than the later one.
157func isMisbehaviourPair(headerA, headerB *Header) bool {
158	if headerA.SignedHeader.Height == headerB.SignedHeader.Height {
159		return !signedHeadersEqual(headerA.SignedHeader, headerB.SignedHeader)
160	}
161
162	return headerA.GetTime().UnixNano() <= headerB.GetTime().UnixNano()
163}
164
165func (c *CometblsLightClient) verifyMisbehaviourHeader(label string, header *Header, now time.Time) error {
166	prefix := ufmt.Sprintf("verifying %s in Misbehaviour failed: ", label)
167
168	consensusState, found := c.getConsensusState(*header.TrustedHeight)
169	if !found {
170		return errorWithDetails(
171			ErrInvalidMisbehaviour,
172			prefix+ufmt.Sprintf("could not get trusted consensus state for Header at TrustedHeight: %s", header.TrustedHeight),
173		)
174	}
175
176	if err := c.verifyHeader(c.clientState, consensusState, header, now); err != nil {
177		return errorWithDetails(ErrInvalidMisbehaviour, prefix+err.Error())
178	}
179
180	return nil
181}
182
183// signedHeadersEqual reports whether two light headers have identical content.
184func signedHeadersEqual(a, b *LightHeader) bool {
185	if a == nil || b == nil {
186		return a == b
187	}
188
189	if a.Height != b.Height || a.TimeSeconds != b.TimeSeconds || a.TimeNanos != b.TimeNanos {
190		return false
191	}
192
193	return bytes.Equal(a.ValidatorsHash, b.ValidatorsHash) &&
194		bytes.Equal(a.NextValidatorsHash, b.NextValidatorsHash) &&
195		bytes.Equal(a.AppHash, b.AppHash)
196}
197
198// verifyChainedMembershipProof verifies a chain of ICS23 membership proofs.
199// Proofs/specs are ordered from the lowest subtree to the root, while keys
200// are ordered from the root to the lowest subtree.
201//
202// Starting at index, each proof must commit to the subroot produced by the
203// previous proof (or the initial value). The final derived root must match
204// the expected root.
205//
206// index allows verification to begin after a non-membership proof, using the
207// previously computed subroot as the initial value.
208func (c *CometblsLightClient) verifyChainedMembershipProof(
209	root []byte,
210	proofs []ics23.CommitmentProof,
211	keys aibtypes.MerklePath,
212	value []byte,
213	index int,
214) error {
215	subroot := value
216	specs := ics23.GetSDKProofSpecs()
217
218	var err error
219
220	// Start with the provided value. If no proofs remain, subroot is compared
221	// directly against the expected root.
222	for i := index; i < len(proofs); i++ {
223		key := keys.KeyPath[len(keys.KeyPath)-1-i]
224
225		// verify membership of the proof at this index with appropriate key and value
226		subroot, err = c.verifyMembershipProofAt(proofs[i], specs[i], key, value, i)
227		if err != nil {
228			return err
229		}
230
231		// Use the computed subroot as the value for the next proof.
232		value = subroot
233	}
234
235	// Ensure the final chained root matches the expected root.
236	if !bytes.Equal(root, subroot) {
237		h1, h2 := hex.EncodeToString(root), hex.EncodeToString(subroot)
238
239		return errorWithDetails(
240			ErrInvalidProof,
241			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),
242		)
243	}
244
245	return nil
246}
247
248// verifyMembershipProofAt verifies the existence proof at a single chain index
249// and returns its calculated subroot. index is used only for error context.
250func (c *CometblsLightClient) verifyMembershipProofAt(proof ics23.CommitmentProof, spec *ics23.ProofSpec, key, value []byte, index int) ([]byte, error) {
251	exist := proof.GetExist()
252	if exist == nil {
253		return nil, errorWithDetails(
254			ErrInvalidProof,
255			"commitment proof must be existence proof foar verifying membership",
256		)
257	}
258
259	subroot, err := exist.Calculate()
260	if err != nil {
261		return nil, errorWithDetails(
262			ErrInvalidProof,
263			ufmt.Sprintf("could not calculate proof root at index %d, merkle tree may be empty. %v", index, err),
264		)
265	}
266
267	if err := exist.Verify(spec, subroot, key, value); err != nil {
268		return nil, errorWithDetails(
269			ErrInvalidProof,
270			ufmt.Sprintf("failed to verify membership proof at index %d: %v", index, err),
271		)
272	}
273
274	return subroot, nil
275}