Documentation Index
Fetch the complete documentation index at: https://docs.cosmos.network/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The attestation light client is an IBC v2 light client that verifies packets using quorum-signed ECDSA attestations from a trusted set of off-chain signers, rather than cryptographic verification of block headers. Instead of tracking a chain’s consensus state through header proofs, it trusts a fixed set of known attestors to sign statements about packet state on demand. In production, the light client is designed to be used in a multi-attestor deployment, where multiple attestors are used to sign attestations for the same packet. There are two implementations:- Cosmos (Go):
ibc-go/modules/light-clients/attestations - EVM (Solidity):
solidity-ibc-eureka/contracts/light-clients/attestation/AttestationLightClient.sol
Trust Model
The client trusts a fixed set of Ethereum-style ECDSA addresses (secp256k1 EOAs) configured at creation time. Proof verification requires at leastminRequiredSigs unique valid signatures from that set.
Key properties:
- Each signer can only sign once per proof — duplicates are rejected
- Only addresses in the registered attestor set are accepted
- Signatures are domain-separated by attestation type to prevent cross-protocol replay
- Conflicting attestations at the same height (different timestamps) trigger misbehaviour and freeze the client.
State
Client State
| Field | Type | Description |
|---|---|---|
attestorAddresses | []string (Go) / address[] (Solidity) | Fixed set of trusted attestor EOA addresses |
minRequiredSigs | uint32 (Go) / uint8 (Solidity) | Minimum unique signatures required to accept a proof |
latestHeight | uint64 | Highest trusted height |
isFrozen | bool | When true, the client rejects all proofs — set on misbehaviour detection |
Consensus State
A consensus state is stored per height. It contains one field:| Field | Type | Description |
|---|---|---|
timestamp | uint64 | Trusted block timestamp at this height. Stored in nanoseconds (Go) / seconds (Solidity). |
proofHeight requires a consensus state to exist at that exact height. Until state attestations are submitted for additional heights, only the initial height is trusted. If the relayer attempts to verify a proof at a height for which no state attestation has been submitted, verification fails.
Runtime Flow
The light client is never called directly by users. It is invoked by the IBC core layer as part of processing a relay transaction. Here is the full flow for a packet relay:- Packet sent: a user submits a send transaction on the source chain. The IBC core module writes a packet commitment to chain state.
-
Relayer detects the packet: the relayer monitors source chain events, sees the packet, and calls the Proof API’s
RelayByTxRPC with the source transaction hash. -
Proof API queries attestors: the Proof API calls each attestor’s gRPC endpoint. Each attestor reads the source chain state at the relevant height and returns a signed
PacketAttestation(containing the packet path and commitment) and a signedStateAttestation(containing the block height and timestamp). -
Proof API assembles the relay transaction: it wraps the collected signatures into an
AttestationProofand builds two messages:updateClient: submits theStateAttestationproof to write a consensus state at the packet’s heightRecvPacket(Cosmos) orrecvPacket(EVM): submits thePacketAttestationproof to deliver the packet
-
IBC core calls the light client: when processing
RecvPacket/recvPacket, the IBC core layer computes the packet commitment path and value, then callsVerifyMembershipon the light client, passing theAttestationProofas theproofargument:- Cosmos (
packet.go):k.ClientKeeper.VerifyMembership(ctx, clientID, proofHeight, 0, 0, proof, merklePath, commitment) - EVM (
ICS26Router.sol):getClient(msg_.packet.destClient).verifyMembership(membershipMsg)
- Cosmos (
-
Light client verifies the proof: checks that a consensus state exists at
proofHeight, verifies the quorum of signatures, and confirms the packet commitment is present in the attested packets array. If all checks pass, the packet is accepted.
updateClient step must succeed before RecvPacket can be verified, because VerifyMembership requires a consensus state at proofHeight. In practice, the Proof API bundles both into a single relay submission.
Wire Format
This is the proof format the attestor must produce and the light client expects to receive. All attestation data is ABI-encoded. The proof envelope is the same for both client updates and packet membership proofs:attestationData payload depends on the operation:
State attestation — used for client updates (updateClient):
Signature Verification
Signatures are domain-separated by attestation type to prevent a packet attestation from being replayed as a state attestation or vice versa:typeTag is:
0x01— state attestation (client update)0x02— packet attestation (membership / non-membership proof)
r || s || v). The signer address is recovered from the signature and checked against the registered attestor set. The Cosmos implementation normalizes v from Ethereum format (27/28) to raw format (0/1) before recovery.
Client Updates
To advance the client’s view of the counterparty chain, anAttestationProof is submitted containing an ABI-encoded StateAttestation over a new height and timestamp. The client:
- Verifies signatures meet quorum against the
0x01-tagged digest - Checks whether a consensus state already exists at that height:
- If yes and the timestamp matches: no-op (idempotent)
- If yes and the timestamp differs: freeze the client (misbehaviour). On the Solidity side, this happens inline; on the Go side, the IBC framework detects it via
CheckForMisbehaviourand callsUpdateStateOnMisbehaviourto set the frozen flag. - If no: stores the new consensus state and updates
latestHeightif the new height is greater
Proof Verification
Both membership and non-membership use anAttestationProof containing an ABI-encoded PacketAttestation.
The path field must have exactly one element — both implementations reject proofs where path.length != 1.
Membership
Verifies that a packet commitment exists at a given path and height:- Checks
path.length == 1andvalueis non-empty - Checks a consensus state exists at the claimed
proofHeight - Verifies signatures against the
0x02-tagged digest - Confirms
packetAttestation.height == proofHeight - Computes
keccak256(path[0])and scans the attestedpacketsarray for a matchingpathandcommitment
Non-Membership
Verifies that a path has no commitment (was deleted or never existed):- Checks
path.length == 1 - Checks a consensus state exists at the claimed
proofHeight - Verifies signatures against the
0x02-tagged digest - Confirms
packetAttestation.height == proofHeight - Computes
keccak256(path[0])and finds a matching entry in thepacketsarray withcommitment == bytes32(0)
Integration
For a working end-to-end example of how the light client is deployed and configured, see the Create Attestation Light Clients page in the walkthrough of the IBC demo tutorial.Cosmos Side
Register the light client module in your app. The following example is fromapp/app.go in the sandbox-ledger example repo:
MsgCreateClient with a serialized ClientState and ConsensusState:
attestations-N.
Attestor addresses must be in EIP-55 checksummed format.
EVM Side
DeployAttestationLightClient.sol and register it with the ICS26Router:
client-N, where N is the next client sequence from ICS26Router.getNextClientSeq().
Access Control
AttestationLightClient uses OpenZeppelin AccessControl with a PROOF_SUBMITTER_ROLE:
- If
roleManager == address(0)at construction: anyone can submit proofs (suitable for demos and permissionless setups) - Otherwise: grant
PROOF_SUBMITTER_ROLEto theICS26Routeraddress so the router can callupdateClient,verifyMembership, andverifyNonMembership
ICS26Router proxy address as roleManager (or grant it the role after deployment).