BesuLightClient, with the client type besu, for both protocols. They differ only in the form of the header their validators sign, and the relayer submits each header already in its chain’s form, so everything below applies to both. Under the wrong form no seal recovers to a validator, so the counterparty’s own sealed headers tell the CLI and the relayer which form the chain signs. A header converted to the other form hashes differently, so its seals recover to unrelated signers and the client rejects it.
What the client trusts
The client state fixes what it verifies against for its whole life:What an update proves
An update carries a Besu header in the form its validators signed, the header’s commit seals, the height the client already trusts, and that height’s consensus state. Besu validators seal a block by signing a digest of its header, with the seals themselves left out: QBFT replaces them with an empty list, IBFT 2.0 drops them from the header’s extra data. The relayer submits the header in that signing form with the seals alongside, so the digest is the hash of the submitted header. The client recovers the signer of every seal against it and applies two rules:- Trusted overlap. At least
trustLevelof the validators trusted at the trusted height, rounded up, must be among the signers, capped at two thirds of them rounded down, and always more than a third of them. This is what stops a new validator set the client has never heard of from feeding it headers. The contract accepts a trust level from one third to two thirds, fixed at deployment;ibc deploy client besuuses two thirds unless--trust-levelsets another (see Deploying). The cap keeps a single validator vote from stalling the client: after one removal or addition, every quorum of the new set still holds that many trusted validators. It lowers the requirement only when two thirds of the trusted set is not a whole number: at the default, two of four trusted validators must sign rather than three. The more-than-a-third floor takes precedence over the cap, and only raises the requirement at a trust level of exactly one third with a trusted set whose size is a multiple of three: of six trusted validators, three must sign rather than two. - Quorum. At least two thirds of the header’s own validator set must be among the signers, which is the same threshold Besu needs to produce the block.
maxClockDrift seconds ahead of this chain’s clock, and the trusted state it builds on must be younger than trustingPeriod. An expired consensus state can no longer anchor an update or be used for packet proofs. Advancing an expired client requires redeployment; packets pending on it stay pending, and the relayer reports the expiry on every attempt until the connection is redeployed. Updates are submitted only alongside packets: keeping the relayer online does not refresh an idle client or prevent its expiry.
The header must be above the trusted height. Heights need not be consecutive. Every QBFT and IBFT 2.0 block is final. Each update must satisfy the quorum and overlap rules against the state it builds on, which can be one installed by an earlier update in the same transaction; the light client enforces those rules when the transaction is simulated or mined.
Misbehaviour
The client freezes itself when it sees two validly sealed consensus states that cannot both be honest:- Double sign. Two different consensus states at the same height.
- Time non-monotonicity. A consensus state whose timestamp is not below that of a higher one.
- Sub-second block time. Two consensus states whose timestamps differ by fewer seconds than their heights. Header timestamps are whole seconds, so the client does not support sub-second block periods.
- Invalid validator set change. Two consensus states whose validator sets differ by more validators than their heights do, counting validators in one set but not the other. Header voting adds or removes at most one validator a block, so a larger change cannot come from one chain. A validator-list override in Besu’s genesis
transitions, which can replace several validators in one block, is outside what the client supports and makes it freezable.
submitMisbehaviour: either two stored heights that conflict in one of those ways, or two headers that each pass the same seal checks as an update. A header that conflicts with its own trusted state, or with the state already stored at its height, is enough on its own. Evidence that proves nothing reverts, and evidence is accepted while the client is paused.
A frozen client rejects every update, proof and further evidence with FrozenClientState, and cannot be unfrozen, so every relay attempt fails with that error. Deploy a new client to restore the connection. The relayer does not watch for or submit misbehaviour evidence itself.
Membership and non-membership
The router stores every packet commitment, receipt and acknowledgement in one mapping. The client computes the storage slot of a path from the path itself and the mapping’s fixed slot, and expects an Ethereum account proof for the router against the trusted state root followed by a storage proof for that slot against the proven storage root.
Both take the consensus state for the proof height, the account proof nodes and the storage proof nodes, which the relayer fetches with a single
eth_getProof call per batch. The contract caches the proven storage root for the rest of the transaction, so only the first packet in a batch carries the account proof. The router hands the client the packet’s path behind the counterparty’s Merkle prefix, which for a Besu client must be [counterpartyRouter, IBCSTORE_STORAGE_SLOT, ""]: the counterparty router’s 20-byte address, the 32-byte slot of the router’s IBC store, and an empty element the router appends the path to. The client rejects a path naming any other router than its client state’s ibcRouter, or any other slot. ibc deploy client besu registers that prefix, and ibc deploy status checks it.
Deploying
evm.ics26Router configuration. A local counterparty deployment manifest is not required. Set TRUSTING_PERIOD to a positive duration of at most 30 days, chosen for the counterparty’s validator governance and key-retirement policy: the client relies on historical validators remaining trustworthy for that period. There is no universally safe finite default. The CLI truncates it to whole seconds and leaves the bounds to the contract, whose error it reports by name, such as InvalidTrustingPeriod. The deployment also fails if the trusted header is already older than the trusting period.
--trust-level is the fraction of the trusted validators that must seal an update, given as n/d from 1/3 to 2/3. It defaults to 2/3. Header voting changes one validator at a time, and the cap on the requirement keeps every header sealed by a bare quorum verifiable from its parent at any trust level, so the client follows header voting however far behind it is (see Relaying). A lower level never requires more trusted validators to seal an update, and for most set sizes requires fewer.
The client is bound to this chain’s router at deployment, so only calls routed through ICS26Router reach it, and that router’s access manager decides who may pause it. A paused client rejects updates and packet proofs but still accepts misbehaviour evidence. Rerunning the command with the same router, trusting period and trust level reports the client as already deployed. A different router, trusting period or trust level reports a conflict, so use a new client ID to deploy with different settings. If the manifest records the client but the chain no longer has it (for example after a devnet reset), the rerun deploys it again from the counterparty’s current head. This needs a working host router: after a full reset, rerun ibc deploy core first.
Relaying
A client end of typebesu takes no parameters, for either consensus:
remoteProver, the ProverService is responsible for both. A client deployed from ibc-contracts before dfba365f has no router binding, takes updates in a form this relayer no longer sends, and fails the first check; replace it with a new client through the router’s migrateClient. The relayer relays at the counterparty’s latest height, or, when that header is further ahead than the client’s maxClockDrift of this chain’s latest block, at an earlier header chosen the same way as at deployment, so a counterparty clock running ahead delays relaying rather than stalling it. Gas is estimated against the latest block, so a chain that stops producing blocks holds relaying back until it produces another. The prover returns the updates from the client’s latest height to that height, or none when that height is not above the client’s latest. When too few trusted validators sealed that header, the prover steps to the furthest header it finds it can verify, by bisecting on whether enough trusted validators sealed each header, counting the signers it recovers from the commit seals. It repeats this from each step, returning one update per step, then the update to the target. When there is more than one, the relayer sends all but the last in transactions of their own, as Relayer describes. It sends an update even when simulation shows the client holds it, since that may be a stalled transaction still pending, which only a resend replaces with one that carries the update; a duplicate lands as a no-op. Every update transaction leaves the client further along, so when a later one fails, the next attempt starts from the height the client reached. Header voting changes at most one validator a block, which at any trust level leaves every header verifiable from its parent, so the prover always reaches the target. Relaying runs concurrently, within one relayer as well as across several, so the client may already be past that height when the update is built. The client accepts updates only above its trusted height, so the prover then sends no update if the client stores a consensus state at that height, and otherwise fails the attempt before submitting; the next attempt relays at a newer height. Outside that guarantee the search is best effort and can stop at a header the client cannot verify from the one before, such as after a validator-list override in Besu’s genesis transitions replaces several validators at once. The relayer then relays the packets sent up to the furthest height its updates reach, holds the rest, and logs once where the search stopped. A later header may still carry the client across, but the prover does not try every header; if the client stays stopped, replace it by deploying one from a fresh trusted header and pointing the client ID at it with the router’s admin-only migrateClient. If the client’s latest consensus state is older than the trusting period, the update fails simulation; an expired client can only be replaced by a new one. Packet proofs share one eth_getProof response across packets at the same height. No attestors are involved. Everything the relayer submits is verified by the light client when the transaction is simulated.
Visit the Besu light client contract page for more information on the Solidity implementation.