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}