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}