client.gno
10.52 Kb · 288 lines
1package cometbls
2
3import (
4 "time"
5
6 aibtypes "gno.land/p/aib/ibc/types"
7 "gno.land/p/aib/ics23"
8 "gno.land/p/nt/bptree/v0"
9 "gno.land/p/nt/ufmt/v0"
10 "gno.land/p/onbloc/ibc/union/lightclient"
11 "gno.land/p/onbloc/ibc/union/types"
12)
13
14// CometblsLightClient is the stateful object implementing lightclient.Interface.
15//
16// It holds the client state and the consensus states keyed by height, mirroring
17// the host store's per-client entry as a self-contained pure object. The core
18// host routes by clientId to the stored light-client object; this object owns its
19// own state and is verified directly against lightclient.Interface.
20type CometblsLightClient struct {
21 clientState *ClientState
22 consensusStateByHeight *bptree.BPTree // height:*ConsensusState
23}
24
25var _ lightclient.Interface = (*CometblsLightClient)(nil)
26
27// NewCometblsLightClient builds the Gno object-form light client from decoded
28// client and consensus state. Core ClientImpl adapters decode MsgCreateClient
29// bytes before calling this constructor.
30func NewCometblsLightClient(clientState *ClientState, consensusState *ConsensusState) (*CometblsLightClient, error) {
31 client := &CometblsLightClient{
32 clientState: clientState,
33 consensusStateByHeight: bptree.NewBPTree32(),
34 }
35 client.setConsensusState(clientState.LatestHeight, consensusState)
36 return client, nil
37}
38
39// Union reference:
40// https://github.com/unionlabs/union/blob/1bb07590230e7c4d071f32ad7185be021a1a1789/cosmwasm/lightclient/cometbls/src/client.rs#L48-L72
41func (c *CometblsLightClient) VerifyMembership(height uint64, key []byte, proof []byte, value []byte) error {
42 clientState := c.clientState
43
44 consensusState, storageProof, merklePath, err := c.prepareProof(clientState, height, key, proof)
45 if err != nil {
46 return err
47 }
48
49 return c.verifyChainedMembershipProof(consensusState.GetRoot().Hash, storageProof, merklePath, value, 0)
50}
51
52// Union reference:
53// https://github.com/unionlabs/union/blob/1bb07590230e7c4d071f32ad7185be021a1a1789/cosmwasm/lightclient/cometbls/src/client.rs#L75-L99
54func (c *CometblsLightClient) VerifyNonMembership(height uint64, key []byte, proof []byte) error {
55 clientState := c.clientState
56
57 consensusState, storageProof, merklePath, err := c.prepareProof(clientState, height, key, proof)
58 if err != nil {
59 return err
60 }
61
62 // VerifyNonMembership verifies the absence of the key in the lowest subtree
63 // and then chains inclusion proofs of all subroots up to the final root.
64 nonexist := storageProof[0].GetNonexist()
65 if nonexist == nil {
66 return errorWithDetails(ErrInvalidProof, "commitment proof must be non-existence proof for verifying non-membership")
67 }
68
69 subroot, err := nonexist.Calculate()
70 if err != nil {
71 return errorWithDetails(ErrInvalidProof, "could not calculate root for proof index 0, merkle tree is likely empty: "+err.Error())
72 }
73
74 key0 := merklePath.KeyPath[len(merklePath.KeyPath)-1]
75 if err := nonexist.Verify(ics23.GetSDKProofSpecs()[0], subroot, key0); err != nil {
76 return errorWithDetails(ErrInvalidProof, "failed to verify non-membership proof: "+err.Error())
77 }
78
79 // Verify the chained membership proof starting from index 1 with value =
80 // subroot.
81 return c.verifyChainedMembershipProof(consensusState.GetRoot().Hash, storageProof, merklePath, subroot, 1)
82}
83
84// prepareProof performs the validation shared by VerifyMembership and
85// VerifyNonMembership: the proof height must not exceed the latest height, the
86// proof must decode, its length must match the SDK proof specs, and a consensus
87// state must exist at the height. It returns the consensus state, decoded
88// storage proof and the membership path for the key.
89func (c *CometblsLightClient) prepareProof(clientState *ClientState, height uint64, key []byte, proof []byte) (*ConsensusState, []ics23.CommitmentProof, aibtypes.MerklePath, error) {
90 if clientState.GetLatestRevisionHeight() < height {
91 return nil, nil, aibtypes.MerklePath{}, errorWithDetails(
92 ErrInvalidHeight,
93 ufmt.Sprintf("client state height < proof height (%d < %d), please ensure the client has been updated", clientState.GetLatestRevisionHeight(), height),
94 )
95 }
96
97 storageProof, err := DecodeProofs(proof)
98 if err != nil {
99 return nil, nil, aibtypes.MerklePath{}, errorWithDetails(ErrInvalidProof, "failed to decode proof: "+err.Error())
100 }
101
102 specs := ics23.GetSDKProofSpecs()
103 if len(storageProof) != len(specs) {
104 return nil, nil, aibtypes.MerklePath{}, errorWithDetails(
105 ErrInvalidProof,
106 ufmt.Sprintf("length of specs: %d not equal to length of proof: %d", len(specs), len(storageProof)),
107 )
108 }
109
110 consensusState, found := c.getConsensusState(types.NewHeight(height))
111 if !found {
112 return nil, nil, aibtypes.MerklePath{}, errorWithDetails(ErrInvalidConsensus, "please ensure the proof was constructed against a height that exists on the client")
113 }
114
115 merklePath := aibtypes.MerklePath{KeyPath: [][]byte{moduleKey, makeStoreKey(clientState.ContractAddress, key)}}
116
117 return consensusState, storageProof, merklePath, nil
118}
119
120// Union reference:
121// https://github.com/unionlabs/union/blob/1bb07590230e7c4d071f32ad7185be021a1a1789/cosmwasm/lightclient/cometbls/src/client.rs#L143-L160
122func (c *CometblsLightClient) VerifyHeader(caller address, headerBytes []byte, relayer address) (types.StateUpdate, error) {
123 header, err := DecodeHeader(headerBytes)
124 if err != nil {
125 return types.StateUpdate{}, errorWithDetails(ErrInvalidHeader, "failed to decode header: "+err.Error())
126 }
127
128 consensusState, found := c.getConsensusState(*header.TrustedHeight)
129 if !found {
130 return types.StateUpdate{}, errorWithDetails(
131 ErrInvalidConsensus,
132 ufmt.Sprintf("could not get trusted consensus state for Header at TrustedHeight: %s", header.TrustedHeight),
133 )
134 }
135
136 headerHeight := header.GetHeight()
137 if consensusState, found := c.getConsensusState(headerHeight); found {
138 return makeStateUpdate(headerHeight, consensusState), nil
139 }
140
141 if err := c.verifyHeader(c.clientState, consensusState, header, time.Now()); err != nil {
142 return types.StateUpdate{}, err
143 }
144
145 return c.updateState(c.clientState, consensusState, header), nil
146}
147
148func (c *CometblsLightClient) VerifyCreation(caller address, relayer address) (types.ClientCreationResult, error) {
149 // CometBLS performs no creation-time validation and writes nothing, so the
150 // result is empty (mirrors Union's verify_creation).
151 return types.ClientCreationResult{}, nil
152}
153
154func (c *CometblsLightClient) Misbehaviour(caller address, misbehaviour []byte, relayer address) ([]byte, error) {
155 m, err := DecodeMisbehaviour(misbehaviour)
156 if err != nil {
157 return nil, errorWithDetails(ErrInvalidMisbehaviour, "failed to decode misbehaviour: "+err.Error())
158 }
159
160 if err := m.ValidateBasic(); err != nil {
161 return nil, err
162 }
163
164 if err := c.verifyMisbehaviour(m, time.Now()); err != nil {
165 return nil, err
166 }
167
168 // Freeze the client. The frozen height is a sentinel boolean (non-zero).
169 c.clientState.FrozenHeight = FrozenHeight
170
171 // Return the frozen client-state bytes so the host commits them back to its
172 // client-state mirror, matching union's misbehaviour handling.
173 csBytes, err := EncodeClientState(c.clientState)
174 if err != nil {
175 return nil, err
176 }
177
178 return csBytes, nil
179}
180
181// updateState creates or updates the consensus state for a verified header and
182// advances the latest height when the header is newer. It mirrors Union's
183// update_state in Union's cometbls client.
184// Union reference:
185// https://github.com/unionlabs/union/blob/1bb07590230e7c4d071f32ad7185be021a1a1789/cosmwasm/lightclient/cometbls/src/client.rs#L283-L309
186func (c *CometblsLightClient) updateState(clientState *ClientState, trustedConsensusState *ConsensusState, header *Header) types.StateUpdate {
187 untrustedHeight := header.GetHeight()
188
189 consensusState := &ConsensusState{
190 Timestamp: trustedConsensusState.Timestamp,
191 Root: trustedConsensusState.Root,
192 NextValidatorsHash: trustedConsensusState.NextValidatorsHash,
193 }
194
195 consensusState.Root = MerkleRoot{Hash: header.SignedHeader.AppHash}
196 consensusState.NextValidatorsHash = header.SignedHeader.NextValidatorsHash
197 // Normalized to nanoseconds to follow tendermint convention
198 consensusState.Timestamp = uint64(header.GetTime().UnixNano())
199
200 c.setConsensusState(untrustedHeight, consensusState)
201
202 stateUpdate := makeStateUpdate(untrustedHeight, consensusState)
203 if untrustedHeight.GT(clientState.LatestHeight) {
204 clientState.LatestHeight = untrustedHeight
205 if csBytes, err := EncodeClientState(clientState); err == nil {
206 stateUpdate.ClientStateBytes = csBytes
207 }
208 }
209
210 return stateUpdate
211}
212
213func makeStateUpdate(height types.Height, consensusState *ConsensusState) types.StateUpdate {
214 update := types.StateUpdate{Height: height.RevisionHeight}
215 if consBytes, err := EncodeConsensusState(consensusState); err == nil {
216 update.ConsensusStateBytes = consBytes
217 }
218
219 return update
220}
221
222func (c *CometblsLightClient) GetTimestamp() types.Timestamp {
223 lastConsState, found := c.getConsensusState(c.clientState.LatestHeight)
224 if !found {
225 return 0
226 }
227
228 return types.Timestamp(lastConsState.Timestamp)
229}
230
231func (c *CometblsLightClient) GetTimestampAtHeight(height uint64) (types.Timestamp, error) {
232 consensusState, found := c.getConsensusState(types.NewHeight(height))
233 if !found {
234 return 0, errorWithDetails(ErrInvalidConsensus, "no consensus state at height")
235 }
236
237 return types.Timestamp(consensusState.Timestamp), nil
238}
239
240func (c *CometblsLightClient) GetLatestHeight() uint64 {
241 return c.clientState.GetLatestRevisionHeight()
242}
243
244func (c *CometblsLightClient) GetCounterpartyChainID() string {
245 return c.clientState.ChainID
246}
247
248func (c *CometblsLightClient) Status() lightclient.Status {
249 if !c.clientState.FrozenHeight.IsZero() {
250 return lightclient.Frozen
251 }
252
253 // get latest consensus state to check for expiry
254 lastConsState, found := c.getConsensusState(c.clientState.LatestHeight)
255 if !found {
256 // if the client state does not have an associated consensus state for its
257 // latest height then it must be expired
258 return lightclient.Expired
259 }
260
261 if c.clientState.IsExpired(lastConsState.Timestamp, uint64(time.Now().UnixNano())) {
262 return lightclient.Expired
263 }
264
265 return lightclient.Active
266}
267
268func (c *CometblsLightClient) hasConsensusState(height types.Height) bool {
269 return c.consensusStateByHeight.Has(heightKey(height))
270}
271
272func (c *CometblsLightClient) getConsensusState(height types.Height) (*ConsensusState, bool) {
273 v := c.consensusStateByHeight.Get(heightKey(height))
274 if v == nil {
275 return nil, false
276 }
277
278 consensusState, ok := v.(*ConsensusState)
279 if !ok {
280 return nil, false
281 }
282
283 return consensusState, true
284}
285
286func (c *CometblsLightClient) setConsensusState(height types.Height, consensusState *ConsensusState) {
287 c.consensusStateByHeight.Set(heightKey(height), consensusState)
288}