client.gno
10.84 Kb · 294 lines
1package cometbls
2
3import (
4 "time"
5
6 aibtypes "gno.land/p/onbloc/deps/ibc/types"
7 "gno.land/p/onbloc/deps/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
148// Union reference (verify_creation is a no-op there):
149// https://github.com/unionlabs/union/blob/1bb07590230e7c4d071f32ad7185be021a1a1789/cosmwasm/lightclient/cometbls/src/client.rs#L134-L141
150func (c *CometblsLightClient) VerifyCreation(caller address, relayer address) (types.ClientCreationResult, error) {
151 // Unlike Union, we validate basic invariants (ClientState.Validate), but
152 // like Union we skip bounding TrustingPeriod (see Validate). Writes nothing.
153 if err := c.clientState.Validate(); err != nil {
154 return types.ClientCreationResult{}, err
155 }
156
157 return types.ClientCreationResult{}, nil
158}
159
160func (c *CometblsLightClient) Misbehaviour(caller address, misbehaviour []byte, relayer address) ([]byte, error) {
161 m, err := DecodeMisbehaviour(misbehaviour)
162 if err != nil {
163 return nil, errorWithDetails(ErrInvalidMisbehaviour, "failed to decode misbehaviour: "+err.Error())
164 }
165
166 if err := m.ValidateBasic(); err != nil {
167 return nil, err
168 }
169
170 if err := c.verifyMisbehaviour(m, time.Now()); err != nil {
171 return nil, err
172 }
173
174 // Freeze the client. The frozen height is a sentinel boolean (non-zero).
175 c.clientState.FrozenHeight = FrozenHeight
176
177 // Return the frozen client-state bytes so the host commits them back to its
178 // client-state mirror, matching union's misbehaviour handling.
179 csBytes, err := EncodeClientState(c.clientState)
180 if err != nil {
181 return nil, err
182 }
183
184 return csBytes, nil
185}
186
187// updateState creates or updates the consensus state for a verified header and
188// advances the latest height when the header is newer. It mirrors Union's
189// update_state in Union's cometbls client.
190// Union reference:
191// https://github.com/unionlabs/union/blob/1bb07590230e7c4d071f32ad7185be021a1a1789/cosmwasm/lightclient/cometbls/src/client.rs#L283-L309
192func (c *CometblsLightClient) updateState(clientState *ClientState, trustedConsensusState *ConsensusState, header *Header) types.StateUpdate {
193 untrustedHeight := header.GetHeight()
194
195 consensusState := &ConsensusState{
196 Timestamp: trustedConsensusState.Timestamp,
197 Root: trustedConsensusState.Root,
198 NextValidatorsHash: trustedConsensusState.NextValidatorsHash,
199 }
200
201 consensusState.Root = MerkleRoot{Hash: header.SignedHeader.AppHash}
202 consensusState.NextValidatorsHash = header.SignedHeader.NextValidatorsHash
203 // Normalized to nanoseconds to follow tendermint convention
204 consensusState.Timestamp = uint64(header.GetTime().UnixNano())
205
206 c.setConsensusState(untrustedHeight, consensusState)
207
208 stateUpdate := makeStateUpdate(untrustedHeight, consensusState)
209 if untrustedHeight.GT(clientState.LatestHeight) {
210 clientState.LatestHeight = untrustedHeight
211 if csBytes, err := EncodeClientState(clientState); err == nil {
212 stateUpdate.ClientStateBytes = csBytes
213 }
214 }
215
216 return stateUpdate
217}
218
219func makeStateUpdate(height types.Height, consensusState *ConsensusState) types.StateUpdate {
220 update := types.StateUpdate{Height: height.RevisionHeight}
221 if consBytes, err := EncodeConsensusState(consensusState); err == nil {
222 update.ConsensusStateBytes = consBytes
223 }
224
225 return update
226}
227
228func (c *CometblsLightClient) GetTimestamp() types.Timestamp {
229 lastConsState, found := c.getConsensusState(c.clientState.LatestHeight)
230 if !found {
231 return 0
232 }
233
234 return types.Timestamp(lastConsState.Timestamp)
235}
236
237func (c *CometblsLightClient) GetTimestampAtHeight(height uint64) (types.Timestamp, error) {
238 consensusState, found := c.getConsensusState(types.NewHeight(height))
239 if !found {
240 return 0, errorWithDetails(ErrInvalidConsensus, "no consensus state at height")
241 }
242
243 return types.Timestamp(consensusState.Timestamp), nil
244}
245
246func (c *CometblsLightClient) GetLatestHeight() uint64 {
247 return c.clientState.GetLatestRevisionHeight()
248}
249
250func (c *CometblsLightClient) GetCounterpartyChainID() string {
251 return c.clientState.ChainID
252}
253
254func (c *CometblsLightClient) Status() lightclient.Status {
255 if !c.clientState.FrozenHeight.IsZero() {
256 return lightclient.Frozen
257 }
258
259 // get latest consensus state to check for expiry
260 lastConsState, found := c.getConsensusState(c.clientState.LatestHeight)
261 if !found {
262 // if the client state does not have an associated consensus state for its
263 // latest height then it must be expired
264 return lightclient.Expired
265 }
266
267 if c.clientState.IsExpired(lastConsState.Timestamp, uint64(time.Now().UnixNano())) {
268 return lightclient.Expired
269 }
270
271 return lightclient.Active
272}
273
274func (c *CometblsLightClient) hasConsensusState(height types.Height) bool {
275 return c.consensusStateByHeight.Has(heightKey(height))
276}
277
278func (c *CometblsLightClient) getConsensusState(height types.Height) (*ConsensusState, bool) {
279 v := c.consensusStateByHeight.Get(heightKey(height))
280 if v == nil {
281 return nil, false
282 }
283
284 consensusState, ok := v.(*ConsensusState)
285 if !ok {
286 return nil, false
287 }
288
289 return consensusState, true
290}
291
292func (c *CometblsLightClient) setConsensusState(height types.Height, consensusState *ConsensusState) {
293 c.consensusStateByHeight.Set(heightKey(height), consensusState)
294}