client.gno
12.12 Kb · 302 lines
1package ics23_mpt
2
3import (
4 "bytes"
5 "crypto/keccak256"
6 "errors"
7 "strconv"
8 "strings"
9
10 "gno.land/p/nt/bptree/v0"
11 "gno.land/p/nt/ufmt/v0"
12 "gno.land/p/onbloc/ibc/union/lightclient"
13 "gno.land/p/onbloc/ibc/union/types"
14 "gno.land/p/onbloc/verifier/evm/storage"
15)
16
17// Sentinel errors for the object-form client. Proof-shape errors reuse the
18// errors declared in consensus.gno (ErrConsensusStateTooShort,
19// ErrInvalidCommitmentKey).
20var (
21 ErrNoL1Client = errors.New("ics23mpt: l1 client not configured")
22 ErrInvalidHeight = errors.New("ics23mpt: proof height above latest height")
23 ErrConsensusStateNotFound = errors.New("ics23mpt: consensus state not found at height")
24 ErrInvalidCommitmentValue = errors.New("ics23mpt: commitment value must be 32 bytes")
25 ErrStoredValueMismatch = errors.New("ics23mpt: stored value mismatch")
26 ErrMisbehaviourUnsupported = errors.New("ics23mpt: misbehaviour is not supported for state-lens clients")
27)
28
29// StateLensIcs23MptLightClient is the stateful object implementing
30// lightclient.Interface for the state-lens/ics23/mpt client.
31//
32// It holds the client state and the L2 consensus states keyed by height.
33// The L1 client dependency is resolved lazily on every use via getL1Client,
34// mirroring Union's ctx-based approach where the light client struct is
35// stateless and the host provides L1 access through the execution context.
36//
37// A state-lens client does not track its own chain directly. It trusts the
38// counterparty's opaque L2 consensus blob, proven to exist in the L1 chain's
39// state via the L1 light client. Membership of application state is then
40// verified against the L2 storage root with EVM MPT storage proofs.
41//
42// This mirrors Union's StateLensIcs23MptLightClient
43// (cosmwasm/lightclient/state-lens-ics23-mpt).
44type StateLensIcs23MptLightClient struct {
45 clientState *ClientState
46 consensusStateByHeight *bptree.BPTree // height:*ConsensusState
47 // getL1ClientFn resolves the settlement-chain light client from the host store
48 // on every call, so the state-lens always sees the current L1 state without
49 // needing an explicit refresh step.
50 getL1ClientFn func(types.ClientId) (lightclient.Interface, bool)
51}
52
53var _ lightclient.Interface = (*StateLensIcs23MptLightClient)(nil)
54
55// NewStateLensIcs23MptLightClient builds the Gno object-form light client from
56// decoded client and consensus state. getL1ClientFn is a live store getter
57// called on every L1 lookup, matching Union's ctx-based approach where the
58// light client resolves the L1 dependency lazily from the host store.
59func NewStateLensIcs23MptLightClient(clientState *ClientState, consensusState *ConsensusState, getL1ClientFn func(types.ClientId) (lightclient.Interface, bool)) (*StateLensIcs23MptLightClient, error) {
60 c := &StateLensIcs23MptLightClient{
61 clientState: clientState,
62 consensusStateByHeight: bptree.NewBPTree32(),
63 getL1ClientFn: getL1ClientFn,
64 }
65 c.setConsensusState(clientState.L2LatestHeight, consensusState)
66 return c, nil
67}
68
69// VerifyMembership verifies that key stores value under the L2 storage root at
70// the given height using an EVM MPT storage proof. Status gating is the caller's
71// responsibility, mirroring the cometbls object form.
72// Union reference:
73// https://github.com/unionlabs/union/blob/1bb07590230e7c4d071f32ad7185be021a1a1789/cosmwasm/lightclient/state-lens-ics23-mpt/src/client.rs#L39-L50
74// https://github.com/unionlabs/union/blob/1bb07590230e7c4d071f32ad7185be021a1a1789/cosmwasm/lightclient/state-lens-ics23-mpt/src/client.rs#L189-L218
75func (c *StateLensIcs23MptLightClient) VerifyMembership(height uint64, key []byte, proof []byte, value []byte) error {
76 consensusState, storageProof, err := c.prepareProof(height, key, proof)
77 if err != nil {
78 return err
79 }
80
81 if len(value) != 32 {
82 return ErrInvalidCommitmentValue
83 }
84
85 if !bytes.Equal(storageProof.Value, value) {
86 return ErrStoredValueMismatch
87 }
88
89 // storage.VerifyProof only reads the storage root (it hashes the proof key
90 // and walks the trie), so the root slice can be passed without copying.
91 if err := storage.VerifyProof(consensusState.StorageRoot, storage.Proof{
92 Key: storageProof.Key,
93 Value: storageProof.Value,
94 Proof: storageProof.Proof,
95 }); err != nil {
96 return ufmt.Errorf("ics23mpt: storage membership verification failed: %v", err)
97 }
98
99 return nil
100}
101
102// VerifyNonMembership verifies the absence of key under the L2 storage root at
103// the given height using an EVM MPT absence proof.
104// Union reference:
105// https://github.com/unionlabs/union/blob/1bb07590230e7c4d071f32ad7185be021a1a1789/cosmwasm/lightclient/state-lens-ics23-mpt/src/client.rs#L53-L63
106// https://github.com/unionlabs/union/blob/1bb07590230e7c4d071f32ad7185be021a1a1789/cosmwasm/lightclient/state-lens-ics23-mpt/src/client.rs#L233-L254
107func (c *StateLensIcs23MptLightClient) VerifyNonMembership(height uint64, key []byte, proof []byte) error {
108 consensusState, storageProof, err := c.prepareProof(height, key, proof)
109 if err != nil {
110 return err
111 }
112
113 if err := storage.VerifyAbsence(consensusState.StorageRoot, storage.Proof{
114 Key: storageProof.Key,
115 Proof: storageProof.Proof,
116 }); err != nil {
117 return ufmt.Errorf("ics23mpt: storage non-membership verification failed: %v", err)
118 }
119
120 return nil
121}
122
123// prepareProof performs the validation shared by VerifyMembership and
124// VerifyNonMembership: the proof height must not exceed the latest height, a
125// consensus state must exist at the height, the storage proof must decode, and
126// its key must match the IBC commitment key derived from path.
127// Union reference:
128// https://github.com/unionlabs/union/blob/1bb07590230e7c4d071f32ad7185be021a1a1789/cosmwasm/lightclient/state-lens-ics23-mpt/src/client.rs#L189-L231
129// https://github.com/unionlabs/union/blob/1bb07590230e7c4d071f32ad7185be021a1a1789/cosmwasm/lightclient/state-lens-ics23-mpt/src/client.rs#L233-L254
130func (c *StateLensIcs23MptLightClient) prepareProof(height uint64, key []byte, proof []byte) (*ConsensusState, storage.Proof, error) {
131 if height > c.clientState.L2LatestHeight {
132 return nil, storage.Proof{}, ErrInvalidHeight
133 }
134
135 consensusState, found := c.getConsensusState(height)
136 if !found {
137 return nil, storage.Proof{}, ErrConsensusStateNotFound
138 }
139
140 storageProof, err := storage.DecodeProof(proof)
141 if err != nil {
142 return nil, storage.Proof{}, ufmt.Errorf("ics23mpt: invalid storage proof: %v", err)
143 }
144
145 if err := VerifyCommitmentKey(key, storageProof.Key); err != nil {
146 return nil, storage.Proof{}, err
147 }
148
149 return consensusState, storageProof, nil
150}
151
152// VerifyHeader verifies a state-lens header and updates the client. It proves
153// that the submitted L2 consensus state is committed in the L1 state at
154// L1Height (via the L1 client), then extracts and stores the L2 consensus state
155// at L2Height. Mirrors Union's verify_header.
156// Union reference:
157// https://github.com/unionlabs/union/blob/1bb07590230e7c4d071f32ad7185be021a1a1789/cosmwasm/lightclient/state-lens-ics23-mpt/src/client.rs#L98-L134
158func (c *StateLensIcs23MptLightClient) VerifyHeader(caller address, headerBytes []byte, relayer address) (types.StateUpdate, error) {
159 l1, ok := c.getL1ClientFn(types.ClientId(c.clientState.L1ClientID))
160 if !ok {
161 return types.StateUpdate{}, ErrNoL1Client
162 }
163
164 header, err := DecodeHeader(headerBytes)
165 if err != nil {
166 return types.StateUpdate{}, ufmt.Errorf("ics23mpt: failed to decode header: %v", err)
167 }
168
169 // The L2 consensus state must be committed under the consensus-state path of
170 // the L2 client, in the L1 chain's state, as a keccak256 hash.
171 consensusPath := types.ConsensusStatePath(types.ClientId(c.clientState.L2ClientID), types.NewHeight(header.L2Height))
172
173 consensusHash := keccak256.Sum256(header.L2ConsensusState)
174 if err := l1.VerifyMembership(header.L1Height, consensusPath[:], header.L2ConsensusStateProof, consensusHash[:]); err != nil {
175 return types.StateUpdate{}, ufmt.Errorf("ics23mpt: l1 consensus state membership verification failed: %v", err)
176 }
177
178 consensusState, err := c.clientState.ExtractConsensusState(header.L2ConsensusState)
179 if err != nil {
180 return types.StateUpdate{}, err
181 }
182
183 return c.updateState(header, &consensusState), nil
184}
185
186// updateState stores the extracted L2 consensus state at L2Height and advances
187// the latest height when the header is newer. Mirrors Union's StateUpdate flow.
188func (c *StateLensIcs23MptLightClient) updateState(header Header, consensusState *ConsensusState) types.StateUpdate {
189 height := header.L2Height
190
191 c.setConsensusState(height, consensusState)
192
193 stateUpdate := types.StateUpdate{Height: height}
194 // Mirror the new consensus state bytes so the host (core) can persist them
195 // for queries and counterparty proofs. Encoding cannot fail here (roots are
196 // fixed 32-byte words), but stay defensive and leave the mirror empty on error.
197 if consensusStateBytes, err := EncodeConsensusState(*consensusState); err == nil {
198 stateUpdate.ConsensusStateBytes = consensusStateBytes
199 }
200
201 if height > c.clientState.L2LatestHeight {
202 c.clientState.L2LatestHeight = height
203 if clientStateBytes, err := EncodeClientState(*c.clientState); err == nil {
204 stateUpdate.ClientStateBytes = clientStateBytes
205 }
206 }
207
208 return stateUpdate
209}
210
211// VerifyCreation performs no creation-time state mutation; the state-lens client
212// trusts its initial consensus state. Mirrors Union's verify_creation, which
213// only emits an event.
214// Union reference:
215// https://github.com/unionlabs/union/blob/1bb07590230e7c4d071f32ad7185be021a1a1789/cosmwasm/lightclient/state-lens-ics23-mpt/src/client.rs#L83-L96
216func (c *StateLensIcs23MptLightClient) VerifyCreation(caller address, relayer address) (types.ClientCreationResult, error) {
217 return types.ClientCreationResult{}, nil
218}
219
220// Misbehaviour is unsupported for state-lens clients, mirroring Union's
221// `unimplemented!()` for this client type.
222func (c *StateLensIcs23MptLightClient) Misbehaviour(caller address, misbehaviour []byte, relayer address) ([]byte, error) {
223 return nil, ErrMisbehaviourUnsupported
224}
225
226// GetTimestamp returns the timestamp of the consensus state at the latest
227// height, or zero when none exists.
228func (c *StateLensIcs23MptLightClient) GetTimestamp() types.Timestamp {
229 consState, found := c.getConsensusState(c.clientState.L2LatestHeight)
230 if !found {
231 return 0
232 }
233
234 return types.Timestamp(consState.Timestamp)
235}
236
237func (c *StateLensIcs23MptLightClient) GetTimestampAtHeight(height uint64) (types.Timestamp, error) {
238 consState, found := c.getConsensusState(height)
239 if !found {
240 return 0, ErrConsensusStateNotFound
241 }
242
243 return types.Timestamp(consState.Timestamp), nil
244}
245
246func (c *StateLensIcs23MptLightClient) GetLatestHeight() uint64 {
247 return c.clientState.L2LatestHeight
248}
249
250func (c *StateLensIcs23MptLightClient) GetCounterpartyChainID() string {
251 return c.clientState.L2ChainID
252}
253
254// Status mirrors the L1 client's status. A state-lens client has no liveness of
255// its own: it is only as live as the L1 client it depends on. A missing or
256// unknown L1 status is treated as Frozen, matching Union's unwrap_or(Frozen).
257func (c *StateLensIcs23MptLightClient) Status() lightclient.Status {
258 l1, ok := c.getL1ClientFn(types.ClientId(c.clientState.L1ClientID))
259 if !ok {
260 return lightclient.Frozen
261 }
262
263 switch status := l1.Status(); status {
264 case lightclient.Active, lightclient.Expired, lightclient.Frozen:
265 return status
266 default:
267 return lightclient.Frozen
268 }
269}
270
271func (c *StateLensIcs23MptLightClient) getConsensusState(height uint64) (*ConsensusState, bool) {
272 v := c.consensusStateByHeight.Get(heightKey(height))
273 if v == nil {
274 return nil, false
275 }
276
277 consState, ok := v.(*ConsensusState)
278 if !ok {
279 return nil, false
280 }
281
282 return consState, true
283}
284
285func (c *StateLensIcs23MptLightClient) setConsensusState(height uint64, consState *ConsensusState) {
286 c.consensusStateByHeight.Set(heightKey(height), consState)
287}
288
289// heightKey renders a height as a fixed-width, lexicographically sortable bptree
290// key. ibc-union heights carry a single revision, so the numeric ordering of the
291// formatted height matches height ordering.
292func heightKey(height uint64) string {
293 return zeroPadDecimalUint64(height)
294}
295
296// zeroPadDecimalUint64 converts a uint64 number into a zero-padded 20-character string.
297func zeroPadDecimalUint64(num uint64) string {
298 s := strconv.FormatUint(num, 10)
299 zerosNeeded := 20 - len(s)
300
301 return strings.Repeat("0", zerosNeeded) + s
302}