This step creates an attestation light client on each chain and registers each client’s counterparty. Each client is initialized with the attestor address and an initial trusted height and timestamp from the counterparty chain. Once both clients exist and their counterparties are registered, the two chains can verify each other’s packets.
Run the following:
./setup.sh create-clients
The logic for this command is in lib/ibc.sh.
Attestation light client
An attestation light client verifies IBC packets using ECDSA signatures from a registered set of off-chain attestors. There are two implementations: Go (cosmos/ibc-go) for the Cosmos side and Solidity (cosmos/solidity-ibc-eureka) for the EVM side.
Each client is initialized with a list of attestor Ethereum addresses and a quorum threshold. When a packet arrives with an attestation proof, the light client:
- Validates that the proof value is non-empty and the path has exactly one element.
- Looks up the trusted consensus timestamp stored at
proofHeight.
- Decodes the proof into
attestationData and signatures.
- Recomputes the expected digest:
sha256(0x02 || sha256(attested_data)).
- For each signature: recovers the signer address via ECDSA, checks it against the registered attestor set, and rejects duplicates. Verifies that
signatures.length >= minRequiredSigs.
- Decodes
attestationData as a PacketAttestation and verifies the attested height matches proofHeight.
- Checks that the packet commitment is present in the attested packets array, matching on both the keccak256 hash of the path and the commitment value.
What the script does
1. Generate or read the attestor keystore
Both light clients are initialized with the attestor’s Ethereum address. The script first ensures the keystore exists, generating a new one if needed, then reads the address from it:
The keystore is written to ibc/local/.ibc-attestor/ibc-attestor-keystore and reused by the attestor services started in a later step.
Ethereum addresses configured in the light client must be in EIP-55 checksummed format.
2. Create the Cosmos-side client
The Cosmos-side client verifies EVM packets on the Cosmos chain. It is initialized with:
- The attestor’s Ethereum address
- The current EVM block height and timestamp as the initial trusted state
The script renders two JSON files from templates and submits them:
sandboxd tx ibc client create client-state.json consensus-state.json
The client state holds the client’s configuration: which attestor addresses are trusted, the quorum threshold, the latest known height, and whether the client is frozen:
{
"@type": "/ibc.lightclients.attestations.v1.ClientState",
"attestor_addresses": ["<ATTESTOR_ETH_ADDR>"],
"min_required_sigs": 1,
"latest_height": <EVM_BLOCK_HEIGHT>,
"is_frozen": false
}
The consensus state records the block timestamp at the initial trusted height, which anchors proof verification:
{
"@type": "/ibc.lightclients.attestations.v1.ConsensusState",
"timestamp": "<EVM_BLOCK_TIMESTAMP_NANOSECONDS>"
}
Output: COSMOS_CLIENT_ID in the format attestations-N.
3. Create and register the EVM-side client
The EVM-side client verifies Cosmos packets on the EVM chain. It is a Solidity contract (AttestationLightClient) deployed from prebuilt bytecode and registered with the ICS26Router.
The script initializes it with:
- The attestor’s Ethereum address
- The current Cosmos block height and timestamp as the initial trusted state
After deployment, the contract is registered with the ICS26Router on the EVM chain. The CounterpartyInfo passed to addClient includes the Cosmos client ID, so the EVM client knows its counterparty at registration time:
ICS26Router.addClient((COSMOS_CLIENT_ID, [0x]), lcAddress)
The EVM client ID (EVM_CLIENT_ID, in the format client-N) is assigned by the router on registration.
Output: EVM_CLIENT_ID in the format client-N.
4. Register the Cosmos-side counterparty
With both client IDs now known, the script registers the Cosmos-side counterparty. This is the on-chain record that maps the Cosmos attestation client to its EVM peer:
sandboxd tx ibc client add-counterparty $COSMOS_CLIENT_ID $EVM_CLIENT_ID ""
The EVM client registers its counterparty at addClient time (the Cosmos client ID is passed in the CounterpartyInfo). The Cosmos client registers its counterparty here after the EVM client ID is known.
Configuration reference
Cosmos client state
| Field | Description |
|---|
attestor_addresses | List of registered attestor Ethereum addresses |
min_required_sigs | Quorum threshold: minimum signatures required to accept a proof |
latest_height | EVM block height at time of client creation (initial trusted state) |
is_frozen | If true, the client rejects all proofs — used as an emergency stop |
Cosmos consensus state
| Field | Description |
|---|
timestamp | EVM block timestamp at latest_height, in nanoseconds |
EVM client constructor
| Argument | Description |
|---|
attestorAddresses | List of registered attestor Ethereum addresses |
minRequiredSigs | Minimum signatures required to accept a proof |
initialHeight | Cosmos block height at time of deployment (initial trusted state) |
initialTimestampSeconds | Cosmos block timestamp at initialHeight, in Unix seconds |
roleManager | Address granted DEFAULT_ADMIN_ROLE (full role administration) and PROOF_SUBMITTER_ROLE on the contract. Use address(0) to grant PROOF_SUBMITTER_ROLE to everyone, allowing any caller to submit proofs (demo only) |
Applying this to your own setup
Initial trusted state
The initial height and timestamp anchor the light client to a specific point in the counterparty chain’s history.
Quorum threshold
The demo uses min_required_sigs: 1 because there is a single attestor. For production, set this to the threshold of your attestor set. It is recommended to use a threshold of greater than 1.
roleManager in production
The demo passes address(0) as the roleManager, which allows anyone to submit proofs. For production, pass the ICS26Router proxy address so only the router can submit proofs to the light client.
Next steps
With both light clients created, counterparties registered, and their IDs written to state, the next step is to register the IFT bridges.