Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}