Integrations

Karatum certificate status over Hyperlane (testnets)

Testnet pilotSource: docs/integrations/hyperlane.md

Status: testnet pilot. Ethereum Sepolia → Base Sepolia, Arbitrum Sepolia, OP Sepolia. Contracts, addresses and the recorded end-to-end runs are in contracts/deployments/hyperlane.json.

What it does

When Karatum revokes (or reinstates) an agent's certificate, the change used to reach other chains only through Karatum's own direct transactions on each chain (Guard KaratumCertRegistry.revokeCert, KaratumIncidentLog, the reputation feed). That needs a funded Karatum key on every chain and makes every chain trust that key.

With Hyperlane, Karatum signs one transaction on the home chain. KaratumStatusHub records the new status and dispatches it through Hyperlane's canonical Mailbox to a KaratumStatusMirror on each destination. Hyperlane's public relayer delivers the message; the destination Mailbox verifies it with its ISM and calls the mirror. A contract on the destination reads isRevoked(agent) or statusOf(agent) from the mirror. No Karatum key exists on the destination for this path.

incident key ──broadcast()──▶ KaratumStatusHub (Sepolia) ──dispatch()──▶ Mailbox (Sepolia)
                                                                            │  Hyperlane validators sign the
                                                                            │  merkle checkpoint; relayer delivers
                                                                            ▼
consumer ──isRevoked()──▶ KaratumStatusMirror ◀──handle()── Mailbox (Base / Arbitrum / OP Sepolia) + default ISM

It is additive: the direct transactions still go out. The Hyperlane path is a second, independent route.

Contracts (contracts/src/hyperlane/)

ContractWhereRole
KaratumStatusHubEthereum SepoliaAuthoritative record. broadcast(agent, status, reason, drill, handleGas) (broadcaster only) bumps the agent's nonce, stores the record and dispatches one message per configured route, paying each message's Mailbox.quoteDispatch fee and refunding any excess in the same transaction. rebroadcast re-sends the current record (same nonce) to chosen routes, for a newly added mirror or a message that has to be paid again. quoteBroadcast returns the total fee.
KaratumStatusMirroreach destinationhandle() accepts a message only when the caller is the Mailbox, the origin is the home domain (11155111) and the sender is the hub. Mailbox, home domain and hub are immutable. Within an agent's lane only a strictly higher nonce is applied. Older or replayed updates are ignored with StaleUpdateIgnored; they are not reverted, so a late message can't be retried forever and can't roll the state back. Malformed messages revert. ETH is refused.
KaratumStatusCodeclibraryWire format v1: abi.encode(uint8 version, address agent, uint8 status, bool drill, uint64 nonce, uint64 updatedAt, bytes32 reason), 224 bytes.

Status codes: 0 unknown (never broadcast), 1 VALID, 2 REVOKED. reason is the incident hash keccak256("KAR-YYYY-NNNN") (or 0). updatedAt is the hub block time and receivedAt is the mirror's delivery block time, so receivedAt - updatedAt is the bridge latency.

Drill lane. Every agent has two independent records with separate nonces: live and drill. A drill broadcast is a rehearsal of the recall mechanism. It writes only drillStatusOf, so it can't overwrite or clear a real revocation, and isRevoked never reports it.

Ownership. All contracts use Ownable2Step with owner 0xa4AB…Af43, and renouncing is disabled. On the hub the owner can rotate the broadcaster and the routes. On a mirror the owner can only change the ISM. The broadcaster is the incident key (0x6358…b508).

Gas. Each message carries StandardHookMetadata (variant 1, msg.value 0, gas limit, refund address = caller). The default handle gas limit is 150,000; a first write measures about 70k. The fee is whatever the Mailbox quotes for that metadata (default hook plus the IGP payment). On 2026-10-27 three routes cost about 4.6e-5 ETH in total.

Reading it from a consumer contract

solidity
interface IKaratumAgentStatus {
    function isRevoked(address agent) external view returns (bool);
    function statusOf(address agent) external view returns (uint8 status, uint64 nonce, uint64 updatedAt, uint64 receivedAt, bytes32 reason);
}

require(!IKaratumAgentStatus(KARATUM_MIRROR).isRevoked(agent), "Karatum: certificate revoked");

Reading isRevoked from the hub on Sepolia gives the same answer as reading it from any mirror once delivery completes. A mirror can't speed up a revocation: until the relayer delivers, a mirror shows the previous status. Consumers that need a freshness bound should check updatedAt.

Security model and switching to a stricter ISM

The mirrors start on the Mailbox default ISM (interchainSecurityModule() returns address(0)): Hyperlane's own testnet validator set for the Sepolia origin. A message reaches handle() only if that ISM verified it. The mirror then also checks the origin domain and the hub address, so a valid message from any other sender is rejected.

On top of that, Karatum can pin its own ISM on each mirror. Only the owner can do it:

  1. Run validators for the Sepolia origin. Run one or more Hyperlane validator agents watching the Sepolia MerkleTreeHook (0x4917a974…2d0d). Each one announces its checkpoint storage location (S3 or GCS) on the Sepolia ValidatorAnnounce (0xE6105C59…CfCD9), which is how relayers find its signatures.
  2. Deploy a multisig ISM on each destination with Hyperlane's static factory, which needs no code of our own. For example on Base Sepolia (staticMessageIdMultisigIsmFactory 0xfc6e5465…B213):
    cast send 0xfc6e546510dC9d76057F1f76633FCFfC188CB213 "deploy(address[],uint8)" "[0xVAL1,0xVAL2,0xVAL3]" 2 --rpc-url $BASE_SEPOLIA_RPC
    # the ISM address is the return value / the factory's deploy event; `cast call <factory> "getAddress(address[],uint8)(address)" ...` predicts it

    For defence in depth, require both Hyperlane's default ISM and Karatum's multisig. To do that, build an aggregation ISM with staticAggregationIsmFactory.deploy([defaultIsm, karatumMultisigIsm], 2). The default ISM addresses are listed under mirrors.*.defaultIsm in the deployment file.

  3. Pin it: KaratumStatusMirror.setInterchainSecurityModule(ism) from the owner on that chain. To revert to the Mailbox default, pin address(0). Check the result with pnpm --filter @karatum/incidents-service hyperlane config.
  4. Relaying. Hyperlane's public relayer delivers to a recipient with a custom ISM only if it can fetch the signatures that ISM needs. Validators announced on-chain (step 1) are usually enough. If not, run a Hyperlane relayer restricted to the hub as sender (whitelist {"originDomain": 11155111, "senderAddress": "<hub>"}).

Important: pin the new ISM only after its validators are signing. If you pin it earlier, every message stays undeliverable until they do. Messages queued in that window are not lost: the relayer retries, or the broadcaster sends rebroadcast.

Limitations (pilot)

  • One status per agent per lane. The hub doesn't keep a set of active incidents per agent the way KaratumIncidentLog does. If two incidents recall the same agent and the later one is fixed first, its lift reinstates the agent in the mirror even though the earlier incident is still active. The lift rule above covers the common order; the full fix is to have the hub derive status from per-incident recalls.
  • Delivery depends on Hyperlane's public testnet relayer. It has no SLA. Undelivered messages stay pending and can be re-paid with rebroadcast.
  • The OP Sepolia mirror was deployed by the feed-updater key. The owner key holds no OP Sepolia ETH; ownership was set in the constructor. The owner needs OP Sepolia ETH before it can pin an ISM there.
  • Testnet only. A mainnet hub needs a mainnet ISM decision first (a Karatum multisig aggregated with the default ISM) and a separate broadcaster key.