An explainer · for developers · no prior knowledge of CLPR assumed
CLPR for developers
From zero to the full protocol: what you can build, the API, one message end to end, then the machinery underneath it, trust and money, operating and extending, and the reference material. Comfort with ledgers, hashes and contracts is assumed; nothing about CLPR is.
Act I · Foundations
What it is for, and the mental model
What CLPR delivers, what people build on it, whom an application trusts and why, and the four layers the rest of the page walks down.
Section 1of 23
What CLPR is for, and what you can build
Source: design §1 · design §3.4 · design §2.2 (clpr-spec@945082d)
CLPR is a transport primitive: reliable, asynchronous, in-order delivery of arbitrary byte payloads from an application on one ledger to an application on another, with exactly one response per data message. It is defined by what it refuses to introduce. No new token. No intermediary consensus layer. No federated signer set. No pooled liquidity. The destination ledger accepts a message only after verifying, in its own execution environment, a state proof produced by the source ledger’s own consensus. When both ledgers provide asynchronous Byzantine fault tolerant finality, cross-ledger communication inherits those guarantees and “requires only a single honest endpoint node per ledger for correct operation”, provided the channel’s verifier and both CLPR Services are correct; that honest endpoint is needed for liveness, not for safety.
In one paragraph: an application calls sendMessage. The message is appended to an ordered, per-channel outbound queue and folded into a SHA-256 running hash. An off-chain endpoint fetches (or constructs) a proof that the queue state is committed on the source ledger, hands it to a peer endpoint over gRPC, and the peer submits it as a native transaction. The destination CLPR Service passes the opaque proof to the channel’s verifier contract, then independently checks progress, size, sequence and the running hash, dispatches to the application, charges the connector for the gas used, and enqueues a response. The response travels back the same way. The simulation below is the same twenty-eight-step round trip as the general page’s, with the protocol wording under each step and a source line per step.
Four application patterns are outlined so far, each just messages with replies:
Remote smart contract call
A contract on one ledger calls a proxy, which records the call. An endpoint carries it across, the proxy on the other ledger invokes the target contract, and the result comes back the same way. Applications have to be written for answers that arrive later, not in the same call.
Escrow, mint, then burn
Assets are locked on the source ledger, then minted on the destination. If minting succeeds the locked assets may be burned; if it fails they are released. Needs atomic semantics and a common asset standard.
N-ledger atomic swap
Several ledgers make state changes tied together by two-phase commit: each enters a ready-or-roll-back state, and if any one fails, all roll back. Every participating ledger must understand the others' commit messages.
Native Hiero asset management
Hiero tokens, fungible and non-fungible, handled across Hiero networks by the messaging layer directly, with no smart-contract proxies in between. The design outlines this in one sentence; it is not yet specified.
When several channels exist between the same two ledgers, for example one per Ethereum commitment level, the application picks one at send time by its own needs: a price feed may accept the risk of a reorganisation for speed, a high-value transfer waits for finality. CLPR imposes no default.
Source: design §3.4.1 · design §3.4.2 · design §3.4.3 · design §3.4.4 · design §3.4 (clpr-spec@945082d)
Choosing a channel is part of the application’s design, not the protocol’s: several channels may join the same two ledgers with different verifiers, and the application names one on every send.
Section 2of 23
The mental model: the trust chain, safety and liveness
Source: design §1 · design §2.3 · spec §8.2 (clpr-spec@945082d)
Removing the intermediary does not remove trust; it shortens the list. An application on Ledger A sending to Ledger B trusts, in order:
Wider than a phone; scroll it sideways if you need to.
- Its own ledger’s consensus: Ledger A orders and finalizes correctly.
- The local CLPR Service: the implementation on A executes the protocol faithfully.
- The channel’s verifier: the contract that validates B’s proofs. “A buggy or compromised verifier could accept fabricated proofs.”
- The remote ledger’s consensus and CLPR Service.
- The remote application.
Two infrastructure roles sit outside the chain: the CLPR Service Admin, with purely protective powers (configure, close) and no economic incentive; and the Endpoint Operator, who moves bundles for every channel on a ledger without choosing among them.
Endpoints are untrusted for safety, required for liveness. “Even with dishonest endpoints, no fabricated messages can be accepted — but availability depends on at least one honest endpoint per side relaying bundles.” A malicious connector, likewise, “cannot forge or tamper with messages” but can refuse to pay.
Wider than a phone; scroll it sideways if you need to.
Section 3of 23
The four layers
Source: design §3 · organization profile (clpr-spec@945082d, org-profile@528e9c0)
| Layer | Responsibility | Key abstractions | Capability gained |
|---|---|---|---|
| Network | Transport between ledger endpoints; channel establishment (permissionless, commit-reveal); trust anchor updates; throttle negotiation | Channel, endpoint, verifier contract, gRPC stream, protobuf encoding | Two ledgers can connect. |
| Messaging | Ordered, reliable, state-proven queuing and delivery | Message queues, bundles, running hashes, state proofs | Two ledgers can pass messages. |
| Payment & Routing | Connector authorization and payment; dispatch to applications; response generation; slashing | Connector contracts, application interfaces, slashing | Messages are paid for, submitters reimbursed where a connector can pay, misbehaving connectors punishable. |
| Application | Cross-ledger applications | Remote contract calls, asset movement, atomic swaps | Applications address a destination ledger, application and connector. |
Wider than a phone; scroll it sideways if you need to.
Network communication uses gRPC; every protocol type on the wire is protobuf. All state-proof verification is delegated to verifier contracts registered per channel; the CLPR Service itself is proof-system-agnostic and never interprets proof bytes. That single decision is what makes the protocol extensible to ledgers with different consensus and data formats without changing the service on ledgers already connected.
Act II · Your first message
One message through the API
The three calls an application sees, the twenty-eight steps one message takes, and the queue and running hash that make the steps checkable.
Section 4of 23
Send, and two callbacks
Source: design §3.4 · spec §6.4 · spec §6.5 Application Delivery · IClprApplication.sol (clpr-spec@945082d, clpr-smart-contracts@276c8d5)
The application surface is deliberately tiny: one call to send, two callbacks to receive.
Wider than a phone; scroll it sideways if you need to.
sendMessage([auth] caller, channel_id: bytes(32), connector_id: bytes(32),
target_application: bytes, message_data: bytes) → { message_id: uint64 }interface IClprApplication {
/// Called when a cross-ledger Data Message arrives. Return bytes become the reply.
function onClprMessage(bytes32 channelId, bytes calldata sender, bytes calldata messageData)
external returns (bytes memory responseData);
/// Called, best-effort, when the response to a message this contract sent arrives.
function onClprResponse(bytes32 channelId, uint64 messageId, uint8 status, bytes calldata responseData)
external;
}Platform specifications define the gas budget, whether callbacks are synchronous, how success is signalled and what happens to externally owned senders: an EOA cannot receive a callback, so responses to it “SHOULD be recorded in the transaction receipt/record but no callback is made”. In the EVM implementation the callback is synchronous within the bundle transaction, capped at max_gas_per_message, and a revert becomes APPLICATION_ERROR with the revert data as the reply.
A minimal echo application, with the caller check that any real deployment needs:
contract PingPong is IClprApplication {
address public immutable clprService;
event MessageReceived(bytes32 indexed channelId, bytes sender, bytes messageData);
event ResponseReceived(bytes32 indexed channelId, uint64 messageId, uint8 status, bytes responseData);
constructor(address _clprService) { clprService = _clprService; }
function serve(bytes32 channelId, bytes32 connectorId, bytes calldata target, bytes calldata data)
external returns (uint64 messageId)
{ return IClprService(clprService).sendMessage(channelId, connectorId, target, data); }
function onClprMessage(bytes32 channelId, bytes calldata sender, bytes calldata messageData)
external returns (bytes memory)
{
require(msg.sender == clprService, "only CLPR service"); // both callbacks are public
emit MessageReceived(channelId, sender, messageData);
return messageData; // the returned bytes go in the reply
}
function onClprResponse(bytes32 channelId, uint64 messageId, uint8 status, bytes calldata responseData)
external
{
require(msg.sender == clprService, "only CLPR service");
emit ResponseReceived(channelId, messageId, status, responseData);
}
}Reply statuses. There are four: SUCCESS, APPLICATION_ERROR, CONNECTOR_NOT_FOUND and CONNECTOR_UNDERFUNDED. The last two are connector-attributable and slash, and so does a connector charge that fails after the call. The EVM ClprTypes.ReplyStatus adds REDACTED (the reply to a redacted message), CHANNEL_CLOSED and NOT_HANDLED; the pinned code defines the last two but never sets them. Treat any value beyond the four as implementation-specific.
Section 5of 23
The 28 steps, in protocol terms
Source: design §2.2 (clpr-spec@945082d)
One message, end to end, in protocol terms: twenty-eight steps, two state proofs, one reply, between a Hiero ledger on the left and a QBFT ledger on the right. Each step names the algorithm or structure it runs through; the plain narration sits above the protocol narration. The switch runs the trip the other way, where the proofs change and the rules do not.
One message, there and back: the 28 steps
Wider than a phone; scroll it sideways if you need to.
What you are looking at
Two ledgers. Each has an application, a CLPR Service with an outbound queue, a connector that pays, and a verifier that checks the other ledger's proofs. Endpoints carry messages between them: on the Hiero ledger every consensus node is one; on the EVM ledger it is a separate program. Press Play or step through with Next.
The application calls sendMessage on the CLPR Service
One function call on the application. It hands over four things: which channel to use, which connector is paying, who the message is for, and the message itself. The application does no cross-chain work of its own; it passes all of that to the CLPR Service and gets back a message id.
In protocol terms. The application calls IClprService.sendMessage(channelId, connectorId, targetApplication, messageData). The service stamps msg.sender as the message's sender and, on success, returns the new message id.
Source: design §3.3.2 · spec §4.3
Channel ACTIVE; payload and queue-depth checks
Before accepting anything, the service checks the obvious. Does this channel exist and is it switched on? Is the message small enough for the other side to accept? Is the outbound queue already full? Any "no" and the whole transaction is rejected.
In protocol terms. Enqueue algorithm steps 1, 4 and 5: the Channel must be ACTIVE; len(message_data) must not exceed the peer's advertised max_message_payload_bytes; next_message_id − acked_message_id must be below max_queue_depth. Any failure rejects the send.
Source: spec §4.3 · design §3.1.1
Per-connector queue quota
One busy payer could fill a shared outbound queue and starve everyone else on the channel. Giving each payer its own slice of the queue is one candidate fix; the EVM contracts already enforce one.
In protocol terms. Queue monopolization is an open issue, and per-Connector quotas are one candidate mitigation rather than a protocol requirement. The EVM contracts enforce a quota of connectorQueueQuotaPct percent of maxQueueDepth per connector, checked before the connector is asked to authorize.
Source: design §3.3.5 · spec §8.11 · MessagingLogic.sol
The connector authorizes the message
The connector is the account that pays. The service asks it, in effect: are you willing to cover this one? If it says no, nothing is sent.
In protocol terms. Enqueue step 3: the service calls the Connector's authorization contract. By returning true the source Connector commits that its counterpart on the destination has funds to pay for execution, and that it can pay for the response. That commitment is what slashing later enforces.
Source: spec §3.3 · design §3.3.1
A ConfigUpdate is queued first if this ledger's settings changed
If anything about this ledger's published limits changed since the last time this channel told the other side, a small housekeeping message is slipped into the queue ahead of yours. On a normal day nothing has changed and this step does nothing.
In protocol terms. Enqueue step 1a, lazy config propagation: if Channel.last_config_timestamp is behind the current configuration's consensus timestamp, a CONTROL ConfigUpdate message is enqueued before the Data Message. Total ordering means the peer applies the new limits at a consensus-determined point in the stream.
Message stored, running hash chained, next id advanced
The message is written into the outbound queue and given the next number in line. It is also folded into a running hash of everything ever sent on this channel, so the other side can later prove that nothing was inserted, removed or reordered.
In protocol terms. Enqueue steps 6–9: build the ClprMessage (connector_id, target_application, stamped sender, message_data); compute running_hash = SHA-256(sent_running_hash ‖ serialized_payload); store it under (channel_id, next_message_id); set sent_running_hash and increment next_message_id.
Source: spec §4.1 · design §3.2.2
The Hiero network signs the block root
The sending ledger's validators jointly sign the block the message landed in. It is a single aggregate signature produced by many nodes together.
In protocol terms. Consensus signs the block root with an aggregate BLS12-381 threshold signature (hinTS). The endpoint does not construct this: it fetches the finished StateProof, which carries that signature over the SHA-384 block root together with a Merkle path for each proven leaf.
Source: verifiers/hiero/state_proof.md
A sync stream opens and BundleRequests are exchanged
The two endpoints keep a connection open between them. The receiving side says which message number it has reached, so the sending side knows exactly what to send.
In protocol terms. A sync cycle is one bidirectional gRPC stream of ClprSyncPayload messages under mutual TLS. Each side's BundleRequest carries its own received_message_id, status, trust_anchor_id and endpoint manifest version. The request is unproven and never changes state; it only shapes a minimal bundle. Delivery is pulled, not pushed.
Source: spec §1.5 · design §3.1.4
The Hiero endpoint fetches the StateProof
The endpoint collects the receipt proving the message really is in the ledger's signed state. It only fetches this proof. It does not create it, and it could not forge it if it wanted to.
In protocol terms. The endpoint retrieves a StateProof covering the Channel leaf (tag 482) and one ClprMessageValue leaf (tag 498) per message, each with a SHA-384 Merkle path to the signed block root. It parses and validates the structure but does not reassemble it.
Source: verifiers/hiero/state_proof.md
The BundleResponse travels down the stream
The message, together with its proof, travels down the open line to the other ledger's endpoint.
In protocol terms. A BundleResponse carries one opaque bundle_payload: the ordered batch of messages plus the state proof over the last of them, the anchor. Proving the anchor plus recomputing the running hash chain proves every message in the batch.
Source: spec §1.5 · design §3.2.3
The EVM endpoint dry-runs the submission
Before spending money submitting anything, the receiving endpoint runs the whole verification locally as a dry run. If it would fail, nobody pays for it.
In protocol terms. Endpoints MUST verify bundle proofs locally before submitting a transaction, otherwise they pay for payloads that fail post-consensus. The EVM relay simulates submitBundle with eth_call and skips the submission if it reverts.
Source: design §3.1.4 · verifiers/qbft/state_proof.md
The bundle lands on the EVM CLPR Service
The endpoint sends an ordinary transaction on the destination ledger carrying the message and its proof. Everything from here until the reply is queued happens inside this single transaction.
In protocol terms. Any account may call submitBundle(channelId, proofBytes); submission is permissionless. The verifier checks the proof, and the service's own replay and running-hash checks still hold if it is buggy. msg.sender, the submitter, is paid the connector charges for this bundle; if that transfer fails, the EVM contracts pay the CLPR Service owner instead.
Source: spec §6.4 · IClprService.sol · design §3.2.5 · ConnectorLib.sol
The verifier checks the threshold signature and the Merkle paths
The destination ledger checks the source ledger's signature itself, against the identity it already trusts, and walks the proof back to the signed block. This is the step where the destination checks the source ledger's own signature itself, rather than relying on a relay's say-so.
In protocol terms. Bundle verification step 1: the service calls the Channel's verifier with (bundle_payload, Channel.trust_anchor, channel_context). For a Hiero peer the trust anchor is the ledger_id: the verifier recomputes the SHA-384 Merkle root from each leaf proof, checks the hinTS aggregate signature against the stored ledger_id, and returns sync data, the ordered messages, and the anchor id and running hash. The EVM HieroVerifier checks against its constructor's ledgerId instead, using Channel.trust_anchor as a cached keccak256 of the hinTS verification key.
Source: spec §4.2 · verifiers/hiero/verifier_contract.md · HieroVerifier.sol
Progress, size, replay and running-hash checks
Then the bookkeeping. Is this the next message in sequence? Have we seen it before? Does recomputing the running hash give the same answer as the proven one? Any mismatch and it stops here.
In protocol terms. Steps 1a–4: reject if no progress criterion holds; reject if the bundle exceeds max_messages_per_bundle or any payload exceeds max_message_payload_bytes; the first id must equal received_message_id + 1 with the rest contiguous (replay defence); recomputing SHA-256(prev ‖ payload) from received_running_hash must reproduce anchor_running_hash. These checks are the service's own and hold even if the verifier is buggy. The EVM contracts skip leading messages they already hold instead of rejecting the bundle, and fold the hash as sha256(prev ‖ sha256(payload)).
Source: spec §4.2 · spec §2.1.2 · BundleLib.sol
Connector resolved and the gas budget computed
The destination looks up the connector that agreed to pay and works out how much execution it can afford.
In protocol terms. The message's connector_id is resolved via (channel_id, connector_id) to the local Connector. A missing Connector yields a CONNECTOR_NOT_FOUND response; an unfunded one is slashed and yields CONNECTOR_UNDERFUNDED. Both are answers, not bundle failures.
Source: design §3.3.3 · spec §4.6
The application receives the message and answers it
The application on the other ledger finally receives the message, does its work and returns its answer. This is the moment the message has actually crossed; everything before it was transport.
In protocol terms. The service dispatches IClprApplication.onClprMessage(channelId, sender, messageData) under a gas cap. The returned bytes become the reply and the status is SUCCESS; a revert becomes APPLICATION_ERROR with the revert data as the reply. An application failing does not fail the bundle.
Source: IClprApplication.sol · design §3.3.3
The connector is charged and the submitter reimbursed
The connector pays for the execution that just happened, plus a little extra, and the whole amount goes to whoever submitted the transaction. That margin is why anyone bothers running an endpoint.
In protocol terms. The Connector is charged actual_gas_used × (1 + margin_percent/100). The entire charge is paid to the submitting account; there is no protocol treasury and no payment to anyone on the sending side.
Source: spec §4.6
The reply is enqueued as a REPLY message
The answer is placed in the destination ledger's own outbound queue, numbered and folded into the running hash the same way the original was. Every application message that gets accepted produces exactly one reply.
In protocol terms. A ClprMessageReply carrying the original message_id, a status and the reply bytes is appended to the destination Channel's outbound queue and chained into its sent_running_hash. Every Data Message produces exactly one Response Message, in order.
Source: spec §1.4 · design §3.2.8
The destination announces how far it has got
The destination records how far it has now received and acknowledged. The other side reads those numbers next time they talk and uses them to know what it can safely forget.
In protocol terms. The service's own ChannelSyncData (received_message_id, status, next_message_id) is updated; the EVM implementation also emits a BundleProcessed event once per accepted bundle. The peer learns the new received_message_id on the next sync and treats it as its acked_message_id.
Source: design §3.2.1 · docs/bundle-flow.md
The EVM endpoint builds a QBFT state proof
Coming back, the proof works differently. This ledger's validators sign block headers directly, so the endpoint takes a header carrying those signatures plus a proof of the exact storage holding the reply.
In protocol terms. The relay assembles an RLP 5-tuple: the current block header (with QBFT committed seals in extraData), any epoch-boundary headers needed for validator-set rotation, the EIP-1186 account proof for the CLPR Service, storage proofs for five Channel slots and the anchor message's running hash, and the serialized bundle content.
Source: verifiers/qbft/state_proof.md
Delivery to the Hiero endpoint, then pre-verification
The same discipline in reverse, on a later sync: the reply goes back down the line, and the receiving endpoint dry-runs it before paying to submit it.
In protocol terms. The reply exists only after the destination's consensus, so it travels back in a later sync cycle. The receiving endpoint verifies locally before paying to submit.
Source: design §3.1.4 · design §2.2
Bundle submission on the Hiero ledger
The Hiero-side endpoint hands the reply to its ledger using that ledger's own transaction format rather than an EVM one.
In protocol terms. On Hiero the bundle is submitted as a HAPI transaction and processed post-consensus by the native CLPR Service.
Source: design §3.2.3
QBFT committed seals checked against the trust anchor
The Hiero ledger checks the other ledger's validator signatures on that block header against the validators it has on record for this channel, and follows any change of validators since.
In protocol terms. The QBFT verifier recovers the ECDSA signers from the header's committed seals and requires a threshold to match the validator set hash in the trust anchor. Epoch headers in the bundle advance the set; each must be sealed by the previous set, so history cannot be skipped. The reference QBFTVerifier tracks one validator address instead and needs MIN_COMMITTED_SEALS seals, one of them from that validator.
Account and storage proofs, then replay and running-hash checks
Then it checks the proof tying the reply to that exact signed block, and repeats the same sequence and running hash checks as the outbound leg. The proof technology differs; the rules do not.
In protocol terms. The account proof ties the CLPR Service to the header's stateRoot; the storage proofs tie the queue state and anchor hash to that account. Then the service runs the same steps 1a–4 as on the outbound leg. Verification is symmetric; only the proof system changes.
Source: verifiers/qbft/state_proof.md · spec §4.2
Response ordering verified against the retained message
Replies have to come back in the same order the messages went out. If one arrives out of turn, the channel pauses rather than delivering it.
In protocol terms. Response ordering verification: the incoming reply's message_id must match the oldest unresponded Data Message still retained in the outbound queue. A mismatch sets the Channel to PAUSED and rejects the bundle; the channel auto-resumes when a correctly ordered bundle arrives.
Source: spec §4.5
The original application is called back
The original application is called back with the message id, a status, and whatever the other side sent back.
In protocol terms. IClprApplication.onClprResponse(channelId, messageId, status, responseData) is invoked best-effort; the status is a ClprMessageReplyStatus: SUCCESS, APPLICATION_ERROR, CONNECTOR_NOT_FOUND or CONNECTOR_UNDERFUNDED. The EVM contracts' ReplyStatus adds REDACTED, CHANNEL_CLOSED and NOT_HANDLED.
Source: IClprApplication.sol · spec §1.4 · ClprTypes.sol
Reply delivered; the original message deleted
Only now is the original message deleted from the outbound queue. It was deliberately kept until its reply arrived, so the ordering check just before had something to compare against.
In protocol terms. Data Messages are retained after acknowledgement because they are the ordering reference for responses; they are deleted only when their response has been received and matched. Response and Control messages, by contrast, are deleted as soon as they are acknowledged.
Source: spec §4.4
Round trip complete
A message went to another ledger, ran there, and its answer came back. Each leg carried a proof of the other ledger's own consensus, checked on arrival by the channel's verifier, with no intermediary signer set in the middle.
In protocol terms. Two bundles, two proofs, two verifications, one reply. For correctness, the only parties trusted are the two ledgers' own consensus, their CLPR Services, the verifier each channel was created with, and the applications themselves. Delivery also depended on at least one honest endpoint per side and a connector that pays.
Source: design §2.3
The EVM application calls sendMessage on its CLPR Service
The same call, started from the other side. The application on the EVM ledger hands the four things (channel, connector, recipient, message) to its CLPR Service contract and gets back a message id.
In protocol terms. The EVM application calls ClprService.sendMessage with the Hiero application's address as the target application. The service stamps msg.sender as the sender and returns the message id.
Source: design §3.3.2 · spec §4.3
Channel ACTIVE; payload and queue-depth checks
The same checks as the other direction, this time written in Solidity rather than living inside the node. Channel switched on, message small enough, outbound queue not full.
In protocol terms. Enqueue steps 1, 4 and 5 in the Solidity service: Channel ACTIVE, payload within the peer's advertised max_message_payload_bytes, queue depth below the cap. The EVM contracts read the peer's limit from the throttles stored when the channel was completed.
Source: spec §4.3 · docs/bundle-flow.md · MessagingLogic.sol
Connector queue quota checked
The connector's slice of the outbound queue is checked before it is asked to pay for anything.
In protocol terms. The EVM service enforces connectorQueueQuotaPct × maxQueueDepth per connector and reverts with ClprQueueQuotaExceeded above it; per-Connector quotas are one candidate mitigation for queue monopolization, not a protocol requirement.
Source: docs/evm-platform-spec.md E-5 · spec §8.11
The connector authorizes the message
The connector on this side approves the message. The same connector is registered on both ledgers under one identity, and that is what makes a channel work in both directions.
In protocol terms. ClprService calls IClprConnector.authorizeOutboundMessage(channelId, targetApplication, sender, messageData). The connector id is keccak256(channel_id ‖ public_key ‖ salt), identical on both ledgers, so the destination will resolve the same connector to pay.
Source: IClprConnector.sol · spec §2.2
A ConfigUpdate is queued first if this ledger's settings changed
A housekeeping message carrying changed limits goes into the queue ahead of yours when something has changed, and is skipped when nothing has.
In protocol terms. Lazy config propagation (enqueue step 1a): if the Channel's last_config_timestamp is behind the current configuration's timestamp, a CONTROL ConfigUpdate is enqueued before the Data Message.
Message stored and MessageQueued(DATA) emitted
The message is written into the outbound queue and folded into the running hash. The service here is an ordinary contract, so storing your message also emits a log that anyone can read.
In protocol terms. The EVM service stores the ClprMessage under (channel_id, next_message_id), folds sent_running_hash as sha256(prev ‖ sha256(payload)) rather than SHA-256(prev ‖ payload), advances next_message_id, and emits MessageQueued.
Source: spec §4.1 · MessagingLogic.sol
The EVM endpoint builds a QBFT state proof
This ledger's validators sign block headers directly, so the endpoint pairs a signed header with a proof of the exact storage slots holding your message.
In protocol terms. The relay assembles the RLP 5-tuple: the current block header with QBFT committed seals, any epoch-boundary headers needed for validator-set rotation, the EIP-1186 account proof for the CLPR Service, storage proofs for five Channel slots and the anchor message's running hash, and the serialized bundle content.
Source: verifiers/qbft/state_proof.md
A sync stream opens and BundleRequests are exchanged
One connection per sync, with the receiving side saying what it still needs. The transport does not care which way the message is going.
In protocol terms. One bidirectional gRPC stream per channel under mutual TLS; each side's BundleRequest carries its received_message_id, status, trust_anchor_id and manifest version. The request is unproven and shapes the bundle; it never changes state.
Source: spec §1.5 · design §3.1.4
The BundleResponse with the QBFT proof travels to the Hiero endpoint
The bundle and its proof travel down the open line to the Hiero-side endpoint.
In protocol terms. A BundleResponse carries one opaque bundle_payload: the ordered messages plus the state proof over the anchor message.
Source: spec §1.5 · design §3.2.3
The Hiero endpoint pre-verifies the bundle locally
The receiving endpoint verifies everything locally first, so it never pays to submit something that will be rejected.
In protocol terms. Endpoints MUST verify bundle proofs before submitting, otherwise they pay for payloads that fail post-consensus.
Source: design §3.1.4
Bundle submission on the Hiero ledger
The Hiero-side endpoint hands the bundle to its ledger. From here to the application call, everything runs inside that one transaction.
In protocol terms. The bundle is submitted as a HAPI transaction and processed post-consensus by the native CLPR Service.
Source: design §3.2.3
QBFT committed seals verified against the trust anchor
The Hiero ledger checks the signatures in the block header against the validator list it holds for this channel.
In protocol terms. The QBFT verifier recovers the ECDSA signers of the committed seals and requires a threshold to match the validator set hash stored in the Channel's trust anchor; epoch headers, if present, advance the set. The reference QBFTVerifier tracks one validator address instead and needs MIN_COMMITTED_SEALS seals, one of them from that validator.
Source: verifiers/qbft/trust_anchor.md · verifiers/qbft/verifier_contract.md · QBFTVerifier.sol
EIP-1186 account and storage proofs verified
Then it checks the proofs that tie the contract, and the queue inside it, to that exact sealed block.
In protocol terms. The account proof ties the CLPR Service contract to the header's stateRoot; the storage proofs tie the five queue slots and the anchor hash to that account's storageRoot.
Source: verifiers/qbft/state_proof.md
Progress, replay and running-hash checks
Sequence and running hash checks run exactly as on the other leg. The proof system changes; the rules about message integrity do not.
In protocol terms. Verification steps 1a–4: a progress criterion must hold; sizes within limits; first id equals received_message_id + 1 and the rest are contiguous; recomputing SHA-256(prev ‖ payload) from received_running_hash must equal anchor_running_hash.
Source: spec §4.2
Connector resolved and the gas budget computed
The Hiero-side connector's balance sets the budget for the application call it is about to pay for.
In protocol terms. The message's connector_id resolves via (channel_id, connector_id) to the local Connector; a missing one yields CONNECTOR_NOT_FOUND, an unfunded one is slashed and yields CONNECTOR_UNDERFUNDED.
Source: design §3.3.3 · spec §4.6
The Hiero application receives the message and answers it
The Hiero application receives the message and returns its answer. This is the crossing point for this direction.
In protocol terms. The native service dispatches onClprMessage(channelId, sender, messageData) to the contract under a gas cap; the returned bytes become the reply with status SUCCESS, a revert becomes APPLICATION_ERROR.
Source: IClprApplication.sol · design §3.3.3
The connector is charged and the submitter reimbursed
The connector pays for the execution and the endpoint that submitted it is reimbursed, with a margin.
In protocol terms. The Connector is charged actual_gas_used × (1 + margin_percent/100), all of it paid to the submitting account.
Source: spec §4.6
The reply is enqueued in the Hiero outbound queue
The answer goes into the Hiero ledger's outbound queue for this channel, numbered and folded into its running hash the same way the original was.
In protocol terms. A ClprMessageReply with the original message_id, status and reply bytes is appended to the Hiero Channel's outbound queue and chained into its sent_running_hash. Every Data Message produces exactly one Response Message, in order.
Source: spec §1.4 · design §3.2.8
The Hiero side records how far it has received
The Hiero side records how far it has received and acknowledged; the other ledger reads those numbers on the next sync and uses them to know what it can forget.
In protocol terms. The native service updates its ChannelSyncData (received_message_id, status, next_message_id) and received_running_hash. The EVM peer will read received_message_id on the next sync and treat it as its acked_message_id.
Source: design §3.2.1
The Hiero network signs the block root
The return leg is proved with the validators' joint signature over the block, the same mechanism the other direction used on its way out.
In protocol terms. Consensus signs the block root with the hinTS aggregate BLS12-381 threshold signature; the endpoint fetches the finished StateProof.
Source: verifiers/hiero/state_proof.md
The Hiero endpoint fetches the StateProof for the reply
The endpoint fetches the proof tying the reply to that signed block, then sends it across; the destination dry-runs it before submitting.
In protocol terms. The endpoint retrieves the Channel leaf and one ClprMessageValue leaf per message, with their SHA-384 Merkle paths to the signed root, sends the bundle across the stream, and the EVM endpoint simulates submitBundle with eth_call before paying.
Source: verifiers/hiero/state_proof.md · design §3.1.4
The reply bundle lands on the EVM CLPR Service
The reply is submitted back on this ledger as an ordinary transaction, and its acceptance is announced on-chain.
In protocol terms. Any account may call submitBundle(channelId, proofBytes). The EVM implementation emits BundleProcessed once the bundle is accepted.
Source: spec §6.4 · docs/bundle-flow.md
The verifier checks the threshold signature and the Merkle paths
The verifier checks the joint signature and the proof paths against the identity on record for this channel.
In protocol terms. A Hiero peer's trust anchor is its ledger_id, and the verifier checks the hinTS aggregate signature against it. The EVM HieroVerifier decodes the StateProof, derives the SHA-384 block root, has TSSVerifier check the signature through the EIP-2537 BLS precompiles against its constructor's ledgerId, and walks the leaf paths; Channel.trust_anchor serves only as a cached keccak256 of the hinTS verification key.
Source: HieroVerifier.sol · verifiers/hiero/verifier_contract.md
Progress, replay and running-hash checks
Sequence and running hash are checked before any reply is handed to an application.
In protocol terms. The same steps 1a–4 of the verification algorithm run on the EVM side, where the contracts skip leading messages they already hold and fold the running hash as sha256(prev ‖ sha256(payload)) rather than SHA-256(prev ‖ payload).
Source: spec §4.2 · BundleLib.sol
Response ordering verified against the retained message
The reply is matched against the oldest message still waiting for one. Out-of-order delivery pauses the channel instead.
In protocol terms. The reply's message_id must match the oldest unresponded Data Message retained in the outbound queue; a mismatch sets PAUSED and rejects the bundle.
Source: spec §4.5
The EVM application is called back
Your original contract is called back with the status and the other side's answer, closing the loop.
In protocol terms. IClprApplication.onClprResponse(channelId, messageId, status, responseData) is invoked best-effort on the sender, which must be a contract; a revert in the callback is caught and ignored.
Source: IClprApplication.sol
ReplyDelivered emitted and the original message deleted
One transaction does all of it: the service removes your original message from the queue, calls your contract back, then announces the delivery on-chain.
In protocol terms. The matched Data Message, retained past acknowledgement as the ordering reference, is deleted. The EVM service does this in the same transaction as the callback, in this order: it deletes the queue slot, calls onClprResponse, then emits ReplyDelivered.
Source: spec §4.4 · docs/bundle-flow.md · BundleLib.sol
Round trip complete
The reverse journey closes the same way the forward one does, proved against each ledger's own consensus.
In protocol terms. Two bundles, two proofs, two verifications, one reply; on the EVM side, the service contract's MessageQueued and ReplyDelivered events record the send and the delivery.
Source: design §2.3
- Getting in line
- 1sendMessage()
- 2channel checks
- 3quota
- 4authorize()
- 5config sync
- 6enqueue
- Proving it happened
- 7TSS signature
- 8gRPC open
- 9fetch proof
- 10gRPC send
- 11pre-verify
- Crossing
- 12submitBundle()
- 13verifyBundle()
- 14hash chain
- 15gas budget
- 16onClprMessage()
- 17settle gas
- 18reply queued
- 19BundleProcessed
- Coming home
- 20QBFT proof
- 21gRPC back
- 22HAPI submit
- 23verify seals
- 24verify state
- 25ordering
- Closing the loop
- 26onClprResponse()
- 27delivered
- 28complete
- Getting in line
- 1sendMessage()
- 2channel checks
- 3quota
- 4authorize()
- 5config sync
- 6MessageQueued
- Proving it happened
- 7QBFT proof
- 8gRPC open
- 9gRPC send
- 10pre-verify
- Crossing
- 11HAPI submit
- 12verify seals
- 13verify proofs
- 14hash chain
- 15gas budget
- 16onClprMessage()
- 17settle gas
- 18reply queued
- 19sync data
- Coming home
- 20TSS signature
- 21fetch proof
- 22submitBundle()
- 23verifyBundle()
- 24hash chain
- 25ordering
- Closing the loop
- 26onClprResponse()
- 27ReplyDelivered
- 28complete
Keep two things from it for the sections that follow. Steps 12 to 19 are one transaction on the receiving ledger: submitBundle, the verification algorithm, acknowledgement, the connector charge, dispatch and the queued response. A rejected bundle applies none of it; in an accepted one, a failed application call or connector charge becomes that message’s reply status and the rest of the bundle still commits. And steps 7 to 11 and 20 to 21 are the endpoints’ own work, re-checked the moment it comes back on chain.
Section 6of 23
The message queue and the running hash
Source: spec §1.4 · spec §4.1 · spec §4.3 · design §3.2.2 (clpr-spec@945082d)
One ordered queue per channel per direction carries three message classes in a single intermixed stream, sharing one running hash chain, one proof mechanism and one transport:
message ClprMessagePayload {
oneof payload {
ClprMessage message = 1; // DATA: application content
ClprMessageReply message_reply = 2; // RESPONSE: outcome of a Data Message
ClprControlMessage control = 3; // CONTROL: protocol housekeeping (ConfigUpdate)
}
}
message ClprMessage {
bytes connector_id = 1; // keccak256(channel_id ‖ public_key ‖ salt); same on both ledgers
bytes target_application = 2; // destination application address
bytes sender = 3; // stamped by the service from the caller; not caller-supplied
bytes message_data = 4; // opaque
}
message ClprMessageReply {
uint64 message_id = 1; // the Data Message this answers
ClprMessageReplyStatus status = 2; // SUCCESS | APPLICATION_ERROR | CONNECTOR_NOT_FOUND | CONNECTOR_UNDERFUNDED
bytes message_reply_data = 3;
}
message ClprMessageValue {
ClprMessagePayload payload = 1;
bytes running_hash_after_processing = 2;
}Queue entries are stored separately from the channel, keyed by (channel_id, message_id), because they are read by id range during bundle construction and deleted after acknowledgement. On Hiero they live in a Merkle state map (directly provable); on EVM ledgers in contract storage, where gas cost makes aggressive deletion important.
Wider than a phone; scroll it sideways if you need to.
The running hash
running_hash = SHA-256( previous_running_hash ‖ serialized_payload ) H0 = 0x00 × 32serialized_payload is the canonical protobuf encoding of the ClprMessagePayload. SHA-256 was chosen for universal availability (EVM precompile, Hiero native, Solana syscall) and retains 128-bit preimage resistance under Grover’s algorithm; the fields are opaque bytes, so an upgrade needs a protocol version bump and channel renegotiation but no wire format change.
Five messages, one chain of running hashes
Hn = SHA-256( Hn−1 ‖ payloadn ), with H0 = 32 zero bytes. Only H5, the anchor, is covered by the state proof. This is the specification's form; the pinned EVM contracts hash the payload first: sha256( Hn−1 ‖ sha256(payloadn) ). The receiver recomputes the chain from its own stored hash and compares.
| # | message | running hash after it | proven? | check |
|---|---|---|---|---|
| 1 | ping | …chain breaks here | - | |
| 2 | pay Bo 5 | …chain breaks here | - | |
| 3 | config update | …chain breaks here | - | |
| 4 | ping | …chain breaks here | - | |
| 5 | pay Cy 2 | …chain breaks here | 🔏 anchor |
🔏 anchor: …
Enqueue (sendMessage)
- Look up the channel; reject unless ACTIVE. 1a. If
last_config_timestampis behind the current configuration’s consensus timestamp, enqueue aConfigUpdatecontrol message first and update the timestamp. - Look up the connector by
(channel_id, connector_id); reject if absent. - Call the connector’s authorization contract; reject if not authorized.
- Check
len(message_data) ≤ peer_config.throttles.max_message_payload_bytes. A stricter local limit is allowed; exceeding the peer’s is not. - Check
next_message_id − acked_message_id < max_queue_depth. - Build the
ClprMessage, stampingsenderfrom the transaction caller. - Compute the running hash. 8. Store under
(channel_id, next_message_id). 9. Setsent_running_hash; incrementnext_message_id.
The sender pays only the native transaction fee; there is no protocol fee on the sending side. If anything fails, “the whole thing reverts and the user pays only for the failed attempt”.
Act III · Under the hood
Bundles, verification, channels, endpoints, sync
How a bundle is built and verified, how replies are ordered, how ledgers identify each other, and how channels and endpoints are set up and kept talking.
Section 7of 23
Bundles and progress
Source: design §3.2.3 · spec §2.1.2 · design §3.2.6 · ADR 2026-07-17 · ClprEvmBundleVerifier.sol (clpr-spec@945082d, clpr-smart-contracts@276c8d5)
Messages never travel individually. A bundle is an ordered batch of payloads plus one state proof over the last of them, the anchor. The proof covers the running hash at the anchor, so the receiver can verify that the whole batch exists in the sender’s state, in order, with nothing omitted, by recomputing the chain. The endpoint reads unacknowledged messages starting after the peer’s current_received_message_id; if received + 1 ≥ next_message_id the bundle carries no messages. A bundle may be a proper prefix of the backlog, so a queue larger than max_messages_per_bundle or max_sync_bytes can still drain in slices.
Wider than a phone; scroll it sideways if you need to.
Progress criteria
The service rejects any bundle that satisfies none of these (NoProgress), and endpoints MUST NOT submit one:
| # | Criterion | Holds when… |
|---|---|---|
| 1 | New messages | anchor message id is greater than this side’s stored received_message_id |
| 2 | Trust anchor advancement | verifier returns new_trust_anchor_id ≠ Channel.trust_anchor_id |
| 3 | Endpoint manifest advancement | new_endpoint_manifest.version > Channel.endpoint_manifest.version |
| 4 | Acknowledgement progress | sync_data.received_message_id > Channel.acked_message_id |
| 5 | Channel state transition | (a) peer status CLOSING/DRAINED/CLOSED while ours is ACTIVE/PAUSED → CLOSING; (b) ours is CLOSING and no unacked Data Messages remain → DRAINED; (c) ours is DRAINED, peer DRAINED/CLOSED and all our responses acknowledged → CLOSED |
Endpoints evaluate these against their cached view before building a bundle (the pre-check); the service re-evaluates against on-chain values after the verifier returns (step 1a). A rotation-only or manifest-only bundle with no messages is valid.
Sizing. On a ledger with a 30M-gas block the worked example is ten messages per bundle at 2M gas each, leaving room for the service’s own logic. On Hiero, limits are expressed as operations-per-second so that oversized bundles are rejected at ingest: “once a bundle starts execution, it must be able to finish”. max_message_payload_bytes is enforced on both ends, the destination’s check being authoritative.
Section 8of 23
Verifying a bundle
Source: spec §4.2 · design §3.2.5 · contracts docs/bundle-flow.md · BundleLib.sol (clpr-spec@945082d, clpr-smart-contracts@276c8d5)
This is the heart of the protocol: what a CLPR Service does, post-consensus, when submitBundle(channel_id, bundle_payload) lands. Step through it on a concrete bundle, then inject faults and watch which step rejects.
Wider than a phone; scroll it sideways if you need to.
One inbound bundle, checked step by step (spec §4.2)
Our channel (Ledger B) stored state, updated as the steps run
- status
- ACTIVE
- received_message_id
- 42
- received_running_hash
computing…- acked_message_id
- 17
- next_message_id
- 21
- trust_anchor_id
- epoch 7
- endpoint_manifest.version
- 3
- throttles we advertise
max_messages_per_bundle 10·max_message_payload_bytes 64
The bundle from Ledger A as the verifier returns it
- sync_data
received_message_id 19· ACTIVE ·next_message_id 47- anchor_message_id
- 46
- anchor_running_hash
computing…- new_trust_anchor_id
- (empty)
- new_endpoint_manifest
- (empty)
- messages
- 4
Our outbound queue (to Ledger A) what we sent and have not finished with
The division of labour matters. The verifier answers one question: is this payload authentically the peer’s state under the current trust anchor, and if so, what sync data, messages, anchor id and anchor hash does it contain? Everything else, progress, size, contiguity, the running hash, acknowledgements, status transitions and dispatch, is the service’s own logic and runs identically regardless of proof system. What this buys is precise: the service’s checks “catch verifier bugs but not a compromised verifier, which would return fabricated data with matching hashes”.
Two ordering rules inside the algorithm are easy to miss. The manifest update (1b) and trust anchor update (1c) are applied before any message is processed, so every message in a bundle is attributed to the authority that produced it, with no window in which two anchors apply. And the lazy ConfigUpdate (5c) is enqueued after the acknowledgement update and before any response generated by this bundle’s dispatch, pinning it to a deterministic point in the stream.
A rejected bundle changes no state and costs the submitter its fee; the one exception is a bundle with out-of-order responses, which is rejected but sets the channel to PAUSED. “Critically, a failure on one message does not stop processing of the remaining messages in the bundle.”
Section 9of 23
Response ordering and correlation
Source: design §3.2.8 · spec §4.5 · spec §4.4 (clpr-spec@945082d)
CLPR distinguishes two confirmations. An acknowledgement is transport-level: the peer’s received_message_id, reported on every sync, becomes the sender’s acked_message_id. A response is application-level: the ClprMessageReply generated when the destination processed a Data Message. Because the destination processes each inbound message sequentially, responses are generated in exactly the order the Data Messages arrived, “regardless of whether each individual response indicates success or failure”.
Wider than a phone; scroll it sideways if you need to.
That ordering guarantee is what lets the source verify correlation without a separate counter. It walks its retained Data Messages in order and matches each incoming response to the next expected one. Hence the lifecycle rules: Response and Control messages in the outbound queue are deleted as soon as they are acknowledged; Data Messages are retained past acknowledgement and deleted only when their response has arrived and matched.
Acks, replies, and what may be deleted when
Two different confirmations flow back to a sender. An acknowledgement is transport-level: the peer's received_message_id, reported on every sync, says which queue entries have been delivered. A response is application-level: the ClprMessageReply the peer generates after processing a Data Message. Only the first lets a sender delete replies and control messages; a Data Message stays until its response has arrived in the right order.
Ledger A's outbound queue (to Ledger B)
acked_message_id 0 · next_message_id 5 · next expected response: Rb1
Latest bundle from Ledger B none yet
Two kinds of rejection. A bundle that fails verification (bad hash chain, replay, oversized payload) is simply rejected: no pause, no state change, and the channel stays ACTIVE ready for the next valid bundle. PAUSED is reserved exclusively for response-ordering violations, because they indicate corruption in the peer's own outbound queue state, not a bad endpoint. Nobody is slashed either way: a peer ledger cannot be slashed.
Source: design §3.2.8 (the worked example is taken from there) · §3.2.7 · spec §4.5 · §4.4 · §4.2 step 5 · §2.1.1.
A mismatch is a protocol violation by the peer ledger. The service sets the channel to PAUSED, rejects the bundle outright (nothing dispatched, no acks, no hash advance), and blocks new sendMessage calls; syncs continue, and the channel auto-resumes when a correctly ordered bundle arrives. It does not slash: “you cannot slash a peer ledger, only individual endpoints”, and the out-of-order responses are already committed in the peer’s queue. PAUSED is reserved exclusively for this case; bundles that fail hashing, replay or size checks are simply rejected with no state change.
Section 10of 23
Ledger identity and configuration
Source: design §3.1.1 · spec §1.1 · spec §7 (clpr-spec@945082d)
Each ledger publishes one ClprLedgerConfiguration: its identity and the limits it will enforce on peers.
message ClprLedgerConfiguration {
uint32 protocol_version = 1; // both sides MUST agree; unknown → reject
string chain_id = 2; // CAIP-2, e.g. "hedera:mainnet", "eip155:1"
bytes service_address = 3; // this ledger's CLPR Service (contract addr / well-known constant)
Timestamp timestamp = 4; // consensus time of the last change; monotonic
ClprThrottles throttles = 5;
bytes initial_trust_anchor = 6; // e.g. Hiero ledger_id; empty if derived from a separate proof
bytes initial_trust_anchor_id = 7; // non-empty iff initial_trust_anchor is non-empty
}Identity is a compound key. chain_id is a CAIP-2 string, but it does not uniquely identify a CLPR Service: two competing service contracts on Ethereum can both claim eip155:1. A peer is therefore identified by (ChainID, ClprServiceAddress). Private networks may self-assign an unregistered namespace such as hashsphere:acme-prod.
Wider than a phone; scroll it sideways if you need to.
Protocol version is bumped on any backward-incompatible change: new control message variants, running hash algorithm, signature schemes, wire format. Cross-version messaging is not supported; a peer that receives a ConfigUpdate with an unrecognized version rejects it, halting the channel until it upgrades.
Throttles are ledger-wide values advertised to all peers and enforced per channel:
| Field | Meaning | Enforced by |
|---|---|---|
max_messages_per_bundle | Hard cap on messages in one bundle, sized to this ledger’s per-transaction execution budget | Sending endpoints must respect it; destination rejects the whole bundle at verification step 2 |
max_message_payload_bytes | Largest single payload this ledger will accept | Source at enqueue (against the peer’s advertised value); destination at bundle processing (authoritative) |
max_gas_per_message | Compute budget for one message; bounds worst-case execution time | Destination, as the gas cap on the application callback |
max_queue_depth | Maximum unacknowledged messages in the outbound queue per channel: natural backpressure | Source at enqueue |
max_sync_bytes | Largest serialized ClprSyncPayload accepted at the gRPC level | Endpoints; must exceed one payload plus proof overhead or the channel deadlocks |
max_local_endpoints | Cap on this service’s live endpoint manifest | Service, at admission |
max_peer_endpoints | How many of a peer’s endpoints to cache per channel; the first N in manifest order are kept | Service, when storing a peer manifest |
Strictness. Every limit is published, so exceeding one is “a measurable, attributable violation”. Implementations MUST reject unrecognized fields, unknown message types and malformed metadata outright rather than skipping them. Defaults for the main throttle values are still to be fixed (the spec marks them TBD); clprEnabled defaults to false and is checked dynamically on every call, giving each deployment a runtime kill switch.
Section 11of 23
Channels
Source: spec §2.1 · spec §5.1 · design §3.1.3 · contracts docs/lifecycle.md (clpr-spec@945082d, clpr-smart-contracts@276c8d5)
A channel is the on-ledger record of one peer relationship. Its state is presented as a single structure, while allowing implementations to split it for efficient state proofs:
Channel {
channel_id : bytes(32) // primary key; same value on both ledgers
chain_id : string // CAIP-2 id of the peer
peer_config_timestamp : Timestamp // last known peer configuration
verifier_contract : bytes // immutable after registration
verifier_fingerprint : bytes // code hash at registration (informational)
trust_anchor : bytes // peer signing authority; only verifyBundle may change it
trust_anchor_id : bytes // opaque id for trust_anchor
channel_context : ChannelContext { channel_id, service_address } // immutable
endpoint_manifest : ClprEndpointManifest // cached peer endpoints (+ version)
last_config_timestamp : Timestamp // for lazy ConfigUpdate propagation
acked_message_id : uint64 // highest own message confirmed received by peer
sent_running_hash : bytes(32)
received_running_hash : bytes(32)
}Three fields are deliberately not on the Channel: status, received_message_id and next_message_id. Each side keeps them in its own ChannelSyncData, which is state-proven and exchanged on every bundle. Wherever a rule says Channel.status, read it as this side’s own ChannelSyncData.status. An ADR moved these fields out so that proofs carry the minimum, not the whole record.
Wider than a phone; scroll it sideways if you need to.
Initial values. acked_message_id = 0; both running hashes are 32 zero bytes; received_message_id = 0; next_message_id = 1. The first Data Message ever enqueued has ID 1 and the first Response Message received must reference it.
Registration: commit, then reveal
Because the same channel_id must be claimed on both ledgers, an attacker who watches a registration on A could race to claim the id on B. The commit-reveal scheme hides both the id and the key until both claims exist. The ownership commitment is keccak256(channel_id ‖ public_key); keccak-256 is fixed cross-platform, while the signature scheme and key encoding are platform-specific (ECDSA secp256k1 or Ed25519 today; “extensible for post-quantum”). Different keypairs may be used on each ledger; the channel id, not the key, ties the two sides together.
Wider than a phone; scroll it sideways if you need to.
Claiming a channel on two ledgers without being front-run
Wider than a phone; scroll it sideways if you need to.
Pick a channel id and a keypair
The same channel id must be claimed on both ledgers, and claiming is permissionless, so whoever claimed it first on the second ledger would win. Commit-reveal is the fix. First the registrant picks the channel id, any 32 bytes (here 32 random ones), and generates a keypair for this ledger. Nothing has touched a ledger yet. Every hash you will see is a real Keccak-256 computed by this page; "New random id / keypair" rerolls both inputs.
In protocol terms. channel_id is bytes(32), an arbitrary value chosen by the registrant and used unchanged on both ledgers. The keypair uses whatever signature scheme the platform supports (ECDSA secp256k1 on Ethereum, Ed25519 on other platforms). Because the id is not derived from a key, a different keypair may be used on each ledger.
Source: spec §5.1.2 · spec §2.1
Commit: the ownership commitment
Hash the channel id and the public key together with Keccak-256. The 32-byte result is the ownership commitment. Anyone may see it; nobody can read the id or the key back out of it.
In protocol terms. ownership_commitment = keccak256(channel_id ‖ public_key). The hash function is fixed cross-platform; the public-key encoding is platform-specific. The value below is a genuine Keccak-256 computed by this page.
Source: spec §5.1.2, step 3 · design §3.1.3
registerChannel on Ledger A → PENDING
The registrant sends only the commitment to Ledger A. The CLPR Service checks that this exact commitment has not been claimed before and creates a channel in PENDING state, filed under the commitment. A PENDING channel can do nothing: no messages, no bundles, no syncing.
In protocol terms. registerChannel(caller, ownership_commitment: bytes(32)), permissionless. The Service (1) verifies the commitment is not already claimed and (2) creates the Channel in PENDING, keyed internally by the commitment until reveal. In PENDING every bundle submission and every sendMessage is rejected.
Source: spec §5.1.2 · spec §6.2 · spec §2.1.1
registerChannel on Ledger B → PENDING
The same claim goes to Ledger B, independently. Both commitments are now on file, and neither shows the name or the key.
In protocol terms. The registrant repeats the claim on both ledgers before proceeding to the reveal phase, possibly with different keypairs (which would give a different commitment on each side; this page reuses one keypair so there is a single hash to follow).
Source: spec §5.1.2 · design §3.1.3, sequence diagram
Someone is watching Ledger A
An attacker reads Ledger A and sees the registerChannel transaction. All it carries is the 32-byte hash. To claim the same id on Ledger B they would need the channel id; to ever complete a claim they would need the private key. The hash yields neither. Copying the hash onto Ledger B does not help either: completing it requires the preimage and a signature from the key.
In protocol terms. Anti-squatting rationale: the commitment hides both channel_id and public_key. An attacker who sees it cannot derive either value, so they cannot complete the reveal phase on either ledger. completeChannel checks keccak256(channel_id ‖ public_key) against the stored commitment and verifies a signature from the keypair.
Source: spec §5.1.1 · design §3.1.3, "Anti-squatting"
completeChannel on Ledger A → ACTIVE
Now the registrant reveals on Ledger A: the channel id, the public key, a signature proving they hold the matching private key, the address of a verifier contract they deployed on A, and two proofs fetched from Ledger B (its CLPR configuration and its endpoint list). The Service recomputes the hash, checks the signature, and hands both proofs to the verifier. If everything holds, the channel becomes ACTIVE.
In protocol terms. completeChannel(caller, channel_id, public_key, signature, verifier_contract, config_proof_bytes, endpoint_manifest_proof_bytes), where the signature is over keccak256(channel_id). The Service: (1) keccak256(channel_id ‖ public_key) matches a stored commitment; (2) the signature verifies, scheme platform-specific; (3) verifyConfig(config_proof_bytes, channel_id, endpoint_manifest_proof_bytes) on verifier_contract returns ctx, chain_id, peer_config_nanos, throttles, the initial trust anchor and its id, and the peer endpoint_manifest; (4–5) it stores channel_context, the trust anchor and the manifest (version ≥ 1, truncated to max_peer_endpoints); (6) PENDING → ACTIVE, and the commitment is removed.
Source: spec §5.1.3 · spec §6.2 · spec §3.1, verifyConfig
completeChannel on Ledger B → ACTIVE
The same reveal on Ledger B, with a verifier deployed on B and proofs fetched from A. Each verifier understands the other ledger's proofs, which is why the two are different contracts.
In protocol terms. The same inputs work on both ledgers. Note the order: commitments on both ledgers first, then reveals. The verifier is a locally deployed contract; the Service only requires that it exists on this ledger.
Source: design §3.1.3 · spec §5.1.3
Both ACTIVE; the verifier is now fixed for life
Both sides are ACTIVE and syncing may begin. The verifier each side named cannot be changed later; if the other ledger changes how its proofs look, you register a new channel with a new verifier. The commitment was deleted at reveal, and the channel keeps no registration key.
In protocol terms. Verifier immutability: verifier_contract is immutable after completeChannel; a proof-format upgrade means a new Channel. Initial values on each side: acked_message_id = 0, both running hashes 32 zero bytes, own ChannelSyncData with received_message_id = 0 and next_message_id = 1. An abandoned PENDING claim can be deleted by the admin with closeChannel.
Source: spec §5.1.5 · spec §2.1 · spec §5.1.4 · design §3.1.3, "Who can close"
… 32 random bytes… 64 random bytes standing in for an uncompressed secp256k1 key… = keccak256(channel_id ‖ public_key)… what the reveal signature is over(placeholder) no real ECDSA runs on this pageFull hex, to check with your own keccak256
A Connector is registered with the same commit-reveal, with one extra input (a 32-byte salt) and an id that is derived rather than chosen. Everything below is computed live from the channel id and public key of the other tab.
……… a label for operators who need several Connectors on one Channel; defaults to 32 zero bytes… = keccak256(channel_id ‖ public_key ‖ salt); the same on every ledger, so no address translation is ever needed… = keccak256(connector_id ‖ public_key), the value passed to registerConnector(channel_id, commitment)… = keccak256(connector_id ‖ service_address); here with the Hiero CLPR system contract address 0x…016e. On EVM it is address(this) of the CLPR Service. Binding the signature to the deployment stops it being replayed elsewhere.Reveal. completeConnector(channel_id, public_key, salt, signature, connector_contract, initial_balance, stake) re-derives the id, checks the commitment and the signature, and creates the Connector keyed by (channel_id, connector_id); the caller becomes its admin. A Connector must be registered on both ledgers with the same public_key and salt, so the ids match: the source side authorises, the destination side pays. Registered on one side only, messages fail with CONNECTOR_NOT_FOUND.
Source: spec §2.2 · spec §6.3 · design §3.3.1.
Source: spec §5.1 · §6.2 · §2.1 · design §3.1.3 · lifecycle.md (EVM). Keccak-256 is implemented in this page and self-tested against the standard vectors at load.
At the reveal (completeChannel) the service verifies the commitment preimage and a signature over keccak256(channel_id), then calls the verifier’s verifyConfig(config_proof_bytes, channel_id, endpoint_manifest_proof_bytes). The verifier returns the peer’s verified chain_id, config timestamp and throttles, the immutable ChannelContext, the initial_trust_anchor and its id, and the peer’s initial endpoint manifest (version ≥ 1; an empty endpoint list is valid). All of it is stored and the channel becomes ACTIVE. The registrant has no privileges afterwards; “the keypair used for registration exists solely to coordinate cross-ledger registration”.
Wider than a phone; scroll it sideways if you need to.
The simulation’s “reference implementation” note lists where the EVM contracts bind these rules to the platform (a derived channel id, 64-byte keys, an EIP-191 signature domain).
Lifecycle
The life of a channel
Wider than a phone; scroll it sideways if you need to.
The subtleties are in the closing states. From ACTIVE or PAUSED, closeChannel (admin only) moves to CLOSING: sendMessage is rejected, but inbound Data Messages are still dispatched and answered, and inbound responses are still delivered. CLOSING becomes DRAINED when no Data Message with an id above acked_message_id remains in the outbound queue; responses generated during CLOSING may still be in flight and drain through DRAINED. DRAINED becomes CLOSED when an inbound bundle carries the peer’s status DRAINED or CLOSED and acked_message_id ≥ next_message_id − 1. The peer’s status travels in ChannelSyncData.status on every sync, which is how one side’s close propagates to the other. PAUSED is different in kind: it is entered automatically on a response-ordering violation and exits automatically when a correctly ordered bundle arrives; the admin cannot resume it, only close it.
Wider than a phone; scroll it sideways if you need to.
Verifier immutability. Fixed for the channel’s lifetime so that “applications can evaluate a Channel’s verifier once and know it will not change underneath them”. If a source ledger upgrades its proof format, a new channel is registered with a new verifier and applications migrate. One caveat: the verifier address being fixed does not stop the verifier being an upgradeable proxy: check who holds the upgrade key.
Multiple channels per pair. Channels are keyed by id, not by peer, so several can target the same service: for instance one per Ethereum commitment level (latest, safe, finalized), each with its own verifier and its own independent queue. Applications choose at send time.
Section 12of 23
Endpoints and manifests
Source: design §3.1.2 · spec §1.2 · spec §2.4 · spec §6.5 · ADR 2026-07-03 (clpr-spec@945082d)
An endpoint is an off-chain node that runs the sync protocol, obtains proofs, pre-verifies and submits bundles. Two manifests describe them.
The local manifest (ClprEndpointManifest) is service-level state: an ordered list of ClprEndpoint entries, a version counter starting at 1 and incremented on every change to the set, and the service_address it belongs to. An entry holds an optional ServiceEndpoint (numeric IP and port; omitted by private networks that only dial out) and a tls_certificate: a DER-encoded, self-signed ECDSA P-384 CA certificate, minimized (single-RDN DN, minimal serial, only basicConstraints and keyUsage) and at most 512 bytes, rejected at admission otherwise. That CA is never presented on the wire; each endpoint mints an in-memory Ed25519 leaf signed by it at process start, presents the leaf at every mTLS handshake, and regenerates it on restart or after leaf_certificate_validity_seconds (default 86 400).
How endpoints get into the local manifest depends on the platform:
- Hiero: every consensus node is automatically an endpoint. The manifest mirrors the roster; a node joining, leaving or rotating its identity bumps the version, stake-weight-only changes do not. No manual administration is required or permitted.
- Permissionless ledgers (EVM): the admin decides. Anyone may call
registerEndpoint(endpoint_data, bond)and enterpending, with a bond whose minimum the platform specification defines; the admin callsaddEndpointto admit it to the live manifest, or to add an endpoint directly with no bond (updateEndpointManifestdoes either for a batch, one version bump for the whole batch).removeEndpointby the endpoint or the admin returns the bond in full. Endpoint bonds are never slashed; misbehavior means eviction with a refund. Slashing applies only to connectors.
The peer manifest is stored per channel as Channel.endpoint_manifest: the authoritative directory of remote endpoints to dial. It is populated at completeChannel from the manifest proof passed to verifyConfig, truncated to max_peer_endpoints, and refreshed whenever verifyBundle returns a new_endpoint_manifest with a higher version, replacing the whole manifest atomically (verification step 1b).
Wider than a phone; scroll it sideways if you need to.
Wider than a phone; scroll it sideways if you need to.
Bundle submission is permissionless and orthogonal to manifest membership: anyone may submit a bundle that makes progress, whether or not they registered, and the bond plays no role in reimbursement. Sybil resistance on permissionless ledgers is “provided primarily by admin curation”; the bond is “commitment friction”, not a threshold calibrated against attack cost. Peer selection should incorporate randomization, and endpoints SHOULD prefer peers that reciprocate with messages of their own, which “cannot be faked — providing a valid bundle with messages requires having actual messages to send”.
Seen from the endpoint’s side, the whole job is a loop: watch the home queue and the peer manifest, open a stream per channel, ask for what is missing, dry-run what arrives, submit only what would pass.
Wider than a phone; scroll it sideways if you need to.
Wider than a phone; scroll it sideways if you need to.
Section 13of 23
The sync protocol
Source: spec §1.5 · design §3.1.4 · ADR 2026-07-14 (clpr-spec@945082d)
In the specification, every endpoint exposes one gRPC service with one method:
service ClprEndpointService {
rpc sync(stream ClprSyncPayload) returns (stream ClprSyncPayload);
}
message ClprSyncPayload {
bytes channel_id = 1; // exactly 32 bytes
BundleRequest bundle_request = 2; // absent when nothing more to request
BundleResponse bundle_response = 3; // absent when carrying no bundle
}
message BundleRequest { // UNPROVEN: shapes the peer's bundle, never changes state
uint64 current_received_message_id = 1;
ClprChannelStatus current_status = 2;
bytes current_trust_anchor_id = 3;
uint64 current_endpoint_manifest_version = 4;
}
message BundleResponse { bytes bundle_payload = 1; } // opaque; for the verifierA sync cycle is one bidirectional stream scoped to a single channel. Each side sends a BundleRequest describing its own state; the responder builds a minimal bundle starting at current_received_message_id + 1, or at its own acked_message_id + 1 if the claim names a message it has not sent yet; a payload with both fields absent closes the stream. The connection and its TLS session may persist across cycles.
One sync cycle as specified: four messages on one stream
Wider than a phone; scroll it sideways if you need to.
Dashed arrow: an unproven request. Solid arrow: a bundle whose contents are state-proven by the sending ledger; sync(r, s, n) abbreviates its ChannelSyncData(received_message_id, status, next_message_id). Both channels are ACTIVE and the trust anchors do not rotate in this cycle; those values are illustrative constants.
One stream, one channel, mutual TLS
Endpoint A wants to sync one channel. It picks one of Ledger B's endpoints from the peer manifest that Ledger A holds for the channel and opens a gRPC stream to it. The two authenticate each other with mutual TLS: each endpoint's root of trust is the CA certificate published on-chain in its manifest, and what it actually presents is an in-memory Ed25519 leaf certificate its process minted at start-up. One stream is one sync cycle. Nothing is pushed unasked: each side asks for a bundle by describing its own state, and the peer shapes the bundle from that.
In protocol terms. rpc sync(stream ClprSyncPayload) returns (stream ClprSyncPayload); every payload carries the 32-byte channel_id. ClprEndpoint.tls_certificate is a DER-encoded self-signed ECDSA P-384 CA certificate (at most 512 bytes; not presented on the wire); each endpoint holds an Ed25519 leaf signed by that CA and presents it at the mTLS handshake. The connection and its TLS session MAY persist across cycles.
Source: spec §1.5, gRPC Endpoint Service · spec §1.2 · design §3.1.4 · design §3.1.2, TLS certificate · ADR 2026-07-14 §3.4
A describes its own state
A sends the first message: a BundleRequest with four values saying where A stands on this channel: how far it has got with B's messages, its channel status, the trust anchor it currently holds for B, and the version of B's endpoint list it has cached. It is a hint, not a proof. B will use it to build the smallest bundle that still makes progress.
In protocol terms. ClprSyncPayload(channel_id, bundle_request, no bundle_response). BundleRequest fields: current_received_message_id, current_status, current_trust_anchor_id, current_endpoint_manifest_version. The request is unproven: it "informs bundle creation only and MUST NOT cause a state change"; verifyBundle's replay defence, hash-chain check and trust-anchor update remain the sole authorities on state.
Source: spec §1.5, BundleRequest · design §3.1.4 · ADR 2026-07-14 §3.2
B answers with a bundle for A, and its own request
B reads the request and takes from Ledger B exactly the messages A says it is missing: from the next id after A's number, up to the per-bundle cap. It wraps them in a state proof of Ledger B that covers B's own sync data and the last message in the batch (the earlier ones ride on the running hash), and sends them back together with B's own BundleRequest, so that A can do the same for B.
In protocol terms. ClprSyncPayload(bundle_request: B's state, bundle_response: bundle_payload). If the claim is at or above B's next_message_id, it names a message B has not sent, and B starts from its own acked_message_id + 1 instead; no messages if current_received_message_id + 1 equals B's next_message_id; otherwise the range starts at current_received_message_id + 1. State-proven inside the payload: B's ChannelSyncData(received_message_id, status, next_message_id), the anchor message id and running hash, plus any trust-anchor or manifest update. The bundle may be a proper prefix of the backlog: max_messages_per_bundle is a hard cap and the receiving Service rejects anything above it.
Source: design §3.2.3 · ADR 2026-07-14 §3.3 · ADR 2026-07-17 · spec §1.1, ClprThrottles · spec §4.2, step 2
A verifies, submits to Ledger A, then sends B's bundle
A does not trust the bytes. It runs the verifier locally first, so it does not pay for a bundle whose proof would fail, then submits the bundle as an ordinary transaction to Ledger A. There the CLPR Service checks that the bundle makes progress, that the message ids continue exactly where A's record ends, and that the running hash matches the proven anchor; it records B's acknowledgement and dispatches the messages. Then A builds B's bundle from Ledger A, shaped by B's request, and sends it. A has nothing more to ask, so bundle_request is absent.
In protocol terms. Endpoints MUST verify bundle proofs before submitting. On-chain (spec §4.2): verifyBundle → NoProgress check (spec §2.1.2) → size check → replay defence (first id = received_message_id + 1, then contiguous) → running-hash check → acked_message_id := sync_data.received_message_id → status checks → dispatch. Then ClprSyncPayload(no bundle_request, bundle_response: bundle for B).
Source: design §3.1.4 · spec §4.2 · spec §2.1.2 · ADR 2026-07-14 §3.3
B verifies, submits to Ledger B, and closes the stream
B does the same on its side. With nothing left to ask or send, it ends the cycle with an empty payload: both fields absent. The stream closes; the connection beneath it may stay open for the next cycle, with no new handshake. How often to sync is left to each endpoint. Endpoints also keep score: a peer that only ever asks and never brings messages of its own gets deprioritised.
In protocol terms. ClprSyncPayload(no bundle_request, no bundle_response) closes the stream. Reciprocity-based peer selection: endpoints SHOULD prefer peers that reciprocate by providing messages in return; one that only requests is deprioritised, and this cannot be faked, since a valid bundle with messages requires real messages to send. Sync cadence is deliberately unspecified.
Source: spec §1.5, gRPC Endpoint Service · spec §5.2, reciprocity · design §3.1.2, reciprocity · ADR 2026-07-14 §3.1
Source: spec §1.5 · design §3.1.4 · design §3.2.3 · ADR 2026-07-14 (why a bidirectional stream) · ADR 2026-07-17 (prefix bundles).
Three properties to internalize. The request is unproven and “MUST NOT cause a state change”: a stale or under-claiming request at worst produces duplicates that the replay defence rejects, costing bandwidth and a fee, never correctness, and an over-claim only delays messages the requester still needs. Each bundle is shaped by the receiver’s request: the responder starts from the state the receiver reports in the same stream, not from a cached view of it. And endpoints MUST pre-verify by executing the verifier locally before submitting, “otherwise the endpoint will have to pay for invalid payloads that fail verification post-consensus”.
The reason: the earlier single-call sync computed bundles from a cached view of the peer’s acknowledgements one cycle stale, so in steady state delivered messages were sent again, roughly halving effective bundle throughput. Sync cadence is intentionally unspecified: “an empirical tuning question, not a protocol-level constant”.
Private networks simply omit service_endpoint from their manifest entries; their endpoints are then the only ones that can initiate. Close-notification: when a channel reaches CLOSED locally, the endpoint’s terminal obligation is to keep submitting one final bundle carrying status = CLOSED to the remote until it is accepted or rejected as already closed.
Wider than a phone; scroll it sideways if you need to.
Act IV · Trust and money
Verifiers, connectors, recovery, security
Where trust actually rests, who pays and who is fined, what recovers from what, and the security considerations that shape implementations.
Section 14of 23
Verifiers and trust anchors
Source: spec §3.1 · spec §3.2 · design §3.1.5 · verifiers/ · HieroVerifier.sol · QBFTVerifier.sol · ADR 2026-07-02 (clpr-spec@945082d, clpr-smart-contracts@276c8d5)
A verifier implements two read-only methods. It holds no per-channel state: mutable trust material comes in and goes out through the trust_anchor parameters, and constant identity through ChannelContext.
struct ChannelContext { bytes channel_id; bytes service_address } // immutable per channel
interface IClprVerifier {
function verifyConfig(bytes config_proof_bytes, bytes channel_id, bytes endpoint_manifest_proof_bytes)
returns (ChannelContext ctx, string chain_id, uint96 peer_config_nanos, ClprThrottles throttles,
bytes initial_trust_anchor, bytes initial_trust_anchor_id, ClprEndpointManifest endpoint_manifest)
function verifyBundle(bytes bundle_payload, bytes trust_anchor, ChannelContext ctx)
returns (ChannelSyncData sync_data, ClprMessagePayload[] messages,
uint64 anchor_message_id, bytes anchor_running_hash,
bytes new_trust_anchor, bytes new_trust_anchor_id, ClprEndpointManifest new_endpoint_manifest)
}verifyConfig runs once, at completeChannel; it MUST revert if any proof fails, if the manifest’s service_address does not match, or if the manifest version is 0, and MUST NOT revert merely because the endpoint list is empty. verifyBundle runs on every submission and “is responsible for validating that the bundle data belongs to the (trust_anchor, ctx.service_address, ctx.channel_id) triple”; hash chain, ordering and replay are the service’s job. EVM verifiers SHOULD fail fast on malformed input before expensive cryptography. ChannelContext exists for two reasons: EVM and QBFT verifiers need the channel id and peer service address to derive the storage slots they prove, and without them a valid Hiero proof for one channel could be passed off as another’s when both trust the same ledger_id; the running hash catches that except in a narrow window at the start of a channel’s life, which ADR 2026-07-02 addresses. It is kept apart from the trust anchor because identity must not live in a field designed to rotate.
Wider than a phone; scroll it sideways if you need to.
Trust anchors and rotation
The trust anchor is opaque to the service: “it stores, passes, and replaces the anchor, but never inspects its contents.” It is initialized from verifyConfig and changes through exactly one path, a non-empty new_trust_anchor returned by verifyBundle, applied atomically before any message in that bundle. No admin key, governance call or ConfigUpdate can set it; only the channel’s verifier, by accepting a proof, can move it, and a verifier behind an upgradeable proxy can change what it accepts. A verifier that returns a new anchor without a state-proven commitment in the same proof MUST revert; that requirement is what prevents anchor injection by a compromised endpoint.
Trust anchors: keeping up with the source's validators
Wider than a phone; scroll it sideways if you need to.
Concrete encodings
QBFT (EVM ledger) verifier. The anchor is abi.encode(…) of four fields, 128 bytes in all:
bytes32 validatorSetHashkeccak256 of the sorted validator addressesbytes32 codeHashkeccak256 of the ClprService bytecodeuint64 epochLengthblocks per epochuint64 epochNumberepoch of this validator settrust_anchor_id=epochNumberas big-endian bytes, no leading zeros
Rotation evidence is the list of epoch-boundary headers (epochBlockHeaders) in the proof: each is sealed by the previous set and lists the next set in extraData; the verifier walks them in order and returns only the final anchor. The relay includes at most maxEpochBlockHeadersPerBundle (default 5) per bundle and catches up over several bundles if more epochs passed. Implementation note: the reference QBFTVerifier.sol currently stores a single validator address in the first slot, reverts with MultiValidatorNotSupported for larger sets, and returns the id as a fixed 8-byte uint64.
Hiero (TSS) verifier. The anchor is the raw ledger_id bytes, the genesis-rooted identity of the network's threshold signing key, with no framing and no separate id (verifyConfig returns an empty initialTrustAnchorId). Rotation is not implemented yet: the native VerifyBundleCall returns an empty newTrustAnchor, so there the anchor is immutable for the Channel's lifetime. The Solidity HieroVerifier checks signatures against the ledger_id fixed at its deployment and returns the hash of the hinTS verification key as newTrustAnchor, a cache hint. In the specification one generic Hiero verifier can serve Channels to many Hiero networks, the anchor saying which network is verified; the pinned Solidity verifier is fixed to one ledger_id at deployment.
What can and cannot change the anchor
- The anchor is written by
verifyConfigatcompleteChanneland afterwards changes only whenverifyBundlereturns a non-emptynew_trust_anchor. There is no admin key, governance call orConfigUpdatepath; a verifier behind an upgradeable proxy can still change what it accepts. - Succession requirement: the successor must be committed to inside the same proof that was verified against the current anchor; a verifier that returns a new anchor without that proven commitment must revert. This is what stops a compromised endpoint from injecting an authority.
- The Service stores
new_trust_anchorandnew_trust_anchor_idatomically before processing any message in the bundle (spec §4.2 Step 1c); the id must be non-empty exactly when the anchor is. - A bundle with zero application messages that advances the anchor is valid: Bundle Progress Criterion 2.
ChannelContext(channel_id,service_address) is stored separately and never changes. The ADR rejected putting identity inside the anchor because a field designed to rotate cannot also carry something that must not: it would make a legitimate rotation indistinguishable from an identity substitution.
Source: design §3.1.5 · spec §3.1 · spec §3.2 · spec §4.2 · QBFT trust anchor · QBFTVerifier.sol · relay configuration · Hiero trust anchor · ADR 2026-07-02.
The endpoint carries the obligation to keep the remote anchor current. Every sync payload reports the peer’s trust_anchor_id; the endpoint keeps a local ordered record of authorities it has seen and, when it cannot produce proofs the remote would accept, submits transition bundles until it can. Whether timing is tight depends on the anchor’s encoding: a single-authority anchor must be advanced exactly at the boundary, while a window anchor (current, next) decouples submission from the rotation and is recommended for proof systems with frequent, predictable rotations such as Ethereum’s roughly 27-hour sync committee. A proof system that cannot produce a proof verifiable by the old authority and committing to the new one “cannot support the CLPR trust anchor model”.
The two production verifiers
| Hiero (TSS / hinTS) | QBFT (EVM ledger) | |
|---|---|---|
| Trust anchor | ledger_id: the genesis-rooted identity of the network’s threshold signing key. No secondary id. | abi.encode(validator, codeHash, epochLength, epochNumber), 128 bytes; id = big-endian epoch number. The reference verifier tracks one validator address and rejects multi-validator sets; verifiers/qbft describes a validator-set hash in that first word. |
| Proof bytes | A StateProof protobuf: BlockStateProof (ledger_id, BLS12-381 aggregate TSS signature over a 48-byte SHA-384 block root) plus one MerkleLeafProof per proven leaf | RLP 5-tuple (7 items with an endpoint manifest proof): current block header with QBFT committed seals in extraData, epoch-boundary headers for rotation, EIP-1186 account proof for the CLPR Service, storage proofs, serialized bundle content |
| What is proven | The Channel leaf (tag 482) and one ClprMessageValue leaf (tag 498) per message; natively, a ClprLedgerConfiguration leaf for verifyConfig. The Solidity verifier decodes the configuration unproven, takes the ledger_id fixed at its deployment, and proves only the endpoint manifest. | Five Channel slots: status + nextMessageId, ackedMessageId + receivedMessageId, sentRunningHash, receivedRunningHash and endpointManifestVersion; plus the running hash of the sender’s latest message when the bundle carries messages |
| Verification | Recompute SHA-384 Merkle roots with domain prefixes (leaf 0x00, unary 0x01, binary 0x02), extend to the block root, check the TSS signature, extract and sanity-check leaves. The native path, through the system contract at 0x16e, checks the signature against the channel’s stored ledger_id (not one inside the proof). The Solidity HieroVerifier + TSSVerifier check the hinTS aggregate with the EIP-2537 BLS12-381 precompiles and a WRAPS proof against the ledger_id fixed when the verifier was deployed. | Recover ECDSA signers from the committed seals and require a configured minimum number of seals, one of them from the anchor’s validator; keccak the header; walk the account proof to stateRoot, pinning the service’s code hash, then each storage proof; process epoch headers in sequence to advance the validator |
| Who builds proofs | The Hiero node itself; the relay fetches the finished proof and only parses it | The relay, from eth_getBlockByNumber and eth_getProof |
| Rotation | Not yet implemented. The native VerifyBundleCall returns an empty newTrustAnchor; the planned path is a WRAPS recursive proof binding the new verification key to the same ledger_id. The Solidity verifier returns the hash of the hinTS verification key as newTrustAnchor, a cache hint the service stores as the channel’s anchor. | Epoch-driven; each epoch header must be sealed by the previous validator, so history cannot be skipped; catch-up bounded per bundle by a relay setting, and the relay sends a bundle with more than one epoch header without messages |
Both are documented under verifiers/ in the specification repository, which also prescribes the three files (state_proof.md, verifier_contract.md, trust_anchor.md) any new chain must add. The contracts repository additionally carries verifier code for other proof families (Ethereum beacon BLS, Sei/CometBFT, an Avalanche mock) which have READMEs in the contracts repository but no specification in clpr-spec and no stated maturity.
Wider than a phone; scroll it sideways if you need to.
Wider than a phone; scroll it sideways if you need to.
Section 15of 23
Connectors, payment and slashing
Source: design §3.3 · spec §2.2 · spec §3.3 · spec §4.6 · spec §6.3 · BundleLib.sol (clpr-spec@945082d, clpr-smart-contracts@276c8d5)
The messaging layer moves bytes; the payment layer decides who pays. A connector is a contract registered on a specific channel with a counterpart on the peer ledger under the same derived identity:
connector_id = keccak256( channel_id ‖ public_key ‖ salt ) // salt defaults to 32 zero bytes
Connector {
connector_id : bytes(32) // primary key with channel_id
channel_id : bytes(32)
connector_contract : bytes // authorization contract on this ledger
admin : bytes // can top up, adjust, shut down
balance : uint // pays for inbound execution
locked_stake : uint // slashable
}Registration mirrors channels: commit keccak256(connector_id ‖ public_key), then reveal (public_key, salt, signature over keccak256(connector_id ‖ service_address), connector_contract, initial_balance, stake); the service re-derives the id and checks the commitment. On Hiero, service_address is the system contract 0x…016e. Storage is keyed by (channel_id, connector_id), so a squatter on some third ledger affects only channels there. The relationship is many-to-many, and the requirement is bilateral: a connector registered only on the source side authorizes messages that then fail on the destination with CONNECTOR_NOT_FOUND.
Wider than a phone; scroll it sideways if you need to.
Authorizing
interface IClprConnectorAuth {
function authorizeMessage(bytes sender, bytes target_application,
uint64 message_size, bytes message_data) returns (bool authorized)
}Returning true is a commitment: “it asserts that its counterpart on the destination ledger has sufficient funds to pay for execution, and that it itself can cover the cost of handling the response. This commitment is the contractual basis for slashing.” A pass-through connector accepts everything; a sophisticated one enforces allow-lists, rate limits or payment from the application. The call MUST NOT modify service state.
Paying
On the destination, each Data Message’s connector_id is resolved to the local connector. If it exists and can pay, it is charged actual_gas_used × (1 + margin_percent / 100), and the entire amount goes to the account that submitted the bundle: no treasury, no owner cut, nothing to the sending side. Response Messages are charged the same way on their way home. Then the application is called.
A message arrives on Ledger B. Who pays whom?
Amounts are in "units" of Ledger B's native token. One unit buys 100,000 gas.
charge = gas × (1 + margin/100)
Happy path
charge 4.4 · balance 20
Slashing
| Outcome on the destination | Destination-side action | Source-side action when the reply arrives |
|---|---|---|
SUCCESS | Charge connector; pay submitter | Deliver response; no penalty |
APPLICATION_ERROR | Charge connector; pay submitter | Deliver error; no penalty (the connector paid; the app reverted) |
CONNECTOR_NOT_FOUND | No connector to slash; submitter absorbs the cost | Slash source connector’s locked_stake; reimburse the source submitter |
CONNECTOR_UNDERFUNDED | Slash destination connector’s stake; reimburse submitter | Slash source connector’s stake; reimburse the source submitter |
Penalties may escalate from a fine to a ban with forfeited stake; the schedule is left to platform specifications. Two acknowledged gaps: the destination submitter is uncompensated when no connector exists at all, and endpoints are paid only on the receiving side, so pure senders face a free-rider problem mitigated by peer preference rather than payment.
Section 16of 23
Recovery scenarios
Source: design §5 · spec §9 (clpr-spec@945082d)
Pick a failure. What does the protocol do?
The application on the other side crashes
The connector was never registered on the other side
The connector on the other side has run out of funds
An endpoint changes a message in transit
An endpoint re-submits an old bundle
All the endpoints on one side go offline
The other ledger goes down entirely
Replies come back in the wrong order
The message is larger than the other side accepts
Every endpoint on the other side was replaced and nobody knows the new addresses
The verifier turns out to be buggy or compromised
A ledger changes how its proofs look
A ledger's own validators are compromised
| # | Scenario | Recovery path | Status |
|---|---|---|---|
| R1 | Endpoints rotated during a partition; none of the new ones known | Anyone proves the remote manifest and submits it as a bundle on-chain; no gRPC needed | Works |
| R2 | Source upgrades its proof format | New channel with a new verifier; applications migrate; admin closes the old channel | Works (new channel) |
| R3 | Endpoints rotated, proof format unchanged | Manifest-update bundle as R1 | Works |
| R4 | Endpoints rotated and proof format changed | R2 then R1 on the new channel | Works (new channel) |
| R5 | Verifier compromised or broken | Admin closes; new channel with a correct verifier | Works; in-flight messages may be unresolved |
| R6 | Peer queue state corrupted; cannot order responses | Channel PAUSED; auto-resumes if the peer fixes it; admin may close | Works (requires peer fix) |
| R7 | Network partition, endpoints unchanged | Syncs resume; ids and hashes verify integrity | Works |
| R8 | Peer ledger down | Queue fills to max_queue_depth, then backpressure; resumes where it stopped | Works |
| R9 | Both sides’ endpoints change at once | Either side submits a manifest-update bundle directly | Works |
R1, R3 and R9 share one mechanism, the proven manifest submitted directly as a bundle, which the figure in section 12 draws as the third refresh path. R5 and R6 may leave in-flight messages UNRESOLVED, requiring application-level, out-of-band reconciliation; tooling for that is one of the items in the options to consider.
Section 17of 23
Security considerations
Source: spec §8 (clpr-spec@945082d)
- Strictness (8.1). Published limits make violations attributable; unrecognized fields, unknown message types and malformed metadata are rejected outright, never skipped.
- Trust model (8.2). Verifier, peer consensus, both services. No verifier allow-list exists.
- Reorg risk (8.3). Commitment level is the verifier’s property; use
finalizedfor anything irreversible. - Sybil resistance (8.4). Admin curation of the endpoint manifest; the bond is friction, not a threshold; no reputation system for endpoints; the admin is fully trusted not to empty the manifest.
- Reentrancy (8.5). Dispatching hands control to arbitrary code. Implementations MUST use reentrancy guards and checks-effects-interactions: all channel state updated before the application is called.
Wider than a phone; scroll it sideways if you need to.
- Untrusted payloads (8.6) and no confidentiality (8.7).
- Upgradeable verifiers (8.8) are valid but shift trust to whoever holds the upgrade key.
- Upgradeable services (8.9). Endpoint proof construction depends on storage layout; uncoordinated upgrades halt all syncs. The EVM contracts are not upgradeable; their CI still diffs a committed storage-layout snapshot, because every logic module writes into the router’s layout by DELEGATECALL.
- Endpoint pre-funding (8.10). Out-of-funds endpoints are effectively offline.
- Queue monopolization (8.11). Open; platform specs SHOULD define quotas, escrow or pricing.
Misbehavior detection is strictly local: “there is no cross-ledger misbehavior reporting protocol”.
Act V · Running and extending
Operating it, adding a ledger, the code
The roles and their scenarios, what a new kind of ledger needs, and what the reference implementations actually contain.
Section 18of 23
Roles, operations and four scenarios
Source: design §2.4 · design §4 · design §4.7 (clpr-spec@945082d)
| Role | What they do | What they risk |
|---|---|---|
| End user | Uses applications. Evaluates an application before using it and watches for degraded service; never touches the protocol directly. | Application bugs, poor channel or connector choices, a channel closed while messages are in flight, and finality risk if a verifier accepts proofs below finality. Escrowed assets should be settled before stopping. |
| Application developer | Sends messages and handles the responses in callbacks, naming a channel and a connector on every send. Keeps both configurable, watches connector health and channel status, and migrates when a proof format changes. | Every cross-ledger payload is untrusted input; payloads sit on chain in plaintext unless the application encrypts them; a compromised verifier can mean direct loss of assets. |
| Connector operator | Often also creates the channel: deploys a verifier, registers the channel, then registers as a connector on both ledgers. Keeps a balance to pay for execution and a locked stake that covers fines; may run allow-lists and rate limits. | An empty balance fails messages and slashes the stake with escalating penalties; a lapsed registration fails them too; removal is blocked while messages are in flight. |
| Verifier developer | An off-chain role with no protocol identity: writes the contract that checks one ledger's proofs. Ledger implementers, connector operators, application developers with high-value flows and auditors all have reasons to. | Proof format changes need new implementations; an upgrade key on a verifier is a single point of compromise; the protocol pays nothing for maintenance. |
| Endpoint operator | On Hiero, nothing: every consensus node is an endpoint automatically. Elsewhere, registers with a bond, is admitted by the service admin, serves every channel on the service, and pre-funds the account that submits bundles. | Invalid, failed or duplicate submissions cost unreimbursed fees; misbehaviour means eviction, with the bond returned in full. |
| CLPR Service admin | Broad but only protective powers: adjusts the ledger's published limits, admits or evicts endpoints, closes channels and, where the service has a kill switch (the EVM contracts do), turns it on or off. Cannot create channels, register connectors or take part in the economics. | Abuse of power, mitigated by governance; inaction, since a compromised channel left open keeps being exploited; and no in-protocol incentive, a known gap between responsibility and motivation. |
Source: design §4.1 · design §4.2 · design §4.3 · design §4.4 · design §4.5 · design §4.6 (clpr-spec@945082d)
Wider than a phone; scroll it sideways if you need to.
Operational rules worth pinning above your desk: removeConnector MUST NOT succeed while in-flight messages exist; endpoint signing accounts must be pre-funded because reimbursement is post-consensus; a compromised verifier is handled by advisory, admin close, patched verifier, new channel, migration, and “the window of vulnerability is between discovery and close”; a proof format upgrade follows the same new-channel path while the source keeps the old format alive long enough to migrate.
Wider than a phone; scroll it sideways if you need to.
The four cross-role scenarios, as step-throughs; each step lights the roles that act:
Four stories: the roles working together
Wider than a phone; scroll it sideways if you need to.
Before anything happens
Press Next to walk through the 4 steps. The roles that act in each step light up above.
A verifier developer publishes and audits a verifier for the target proof system.
A connector operator evaluates it, deploys it, registers the channel on both ledgers, and registers as a connector on both sides.
An application developer evaluates the channel and the connector and integrates them.
End users use the application; the trust chain is active from top to bottom.
Before anything happens
Press Next to walk through the 6 steps. The roles that act in each step light up above.
The source ledger announces a proof format upgrade.
A verifier developer publishes a verifier for the new format.
A connector operator creates a new channel with it and registers as a connector.
Application developers migrate to the new channel.
The source ledger switches formats; the old channel's verifier stops working.
The service admin closes the old channel. The source ledger keeps the old format alive long enough for the migration.
Before anything happens
Press Next to walk through the 5 steps. The roles that act in each step light up above.
A vulnerability is discovered and a security advisory issued.
The service admin closes the affected channels; their queues drain.
The verifier developer publishes a patched implementation.
Connector operators create new channels with the patched verifier.
Application developers migrate. The window of exposure is between discovery and close.
Before anything happens
Press Next to walk through the 4 steps. The roles that act in each step light up above.
The connector operator updates its authorization contract to reject all new messages.
Application developers switch to another connector or notify their users.
Messages already queued complete normally.
Once every in-flight message has settled, the operator removes the connector and recovers its funds.
Source: design §4.1 · design §4.2 · design §4.3 · design §4.4 · design §4.5 · design §4.6 · design §4.7 · design §4.7 · design §4.7 · design §4.7 (clpr-spec@945082d)
Section 19of 23
Adding a ledger: the verifier developer
Source: design §3.1.5 · verifier specifications · organization profile · design §4.4 (clpr-spec@945082d, org-profile@528e9c0)
Four steps: deploy a CLPR Service on the new ledger; build a verifier for its proofs, including defining the trust anchor and its rotation; publish it for peers to evaluate; and let anyone deploy it and register a channel. No protocol change is needed on existing ledgers. Expanded into the eight operational steps below.
Adding a new ledger type
Wider than a phone; scroll it sideways if you need to.
The pattern that makes CLPR extensible
The CLPR Service implementation on existing ledgers does not change when a new one joins; they only gain a verifier and a Channel. The alignment between the new ledger's endpoint (which builds bundles in that ledger's format) and the verifier it ships for other ledgers (which reads that format) is the whole trick.
Implement a CLPR Service and CLPR Endpoints for the new ledger type
The Service can be native to the node or a smart contract; either way it must conform to the behavior in the specification. The Endpoints read the new ledger's CLPR state, build state-proven bundles in the ledger's own data format, and speak the common sync protocol.
Implement a Hiero verifier that runs on the new ledger
So the new ledger can check proofs coming from Hiero.
Implement a verifier for the new ledger that runs on Hiero
So Hiero can check proofs coming from the new ledger. This verifier also translates the new ledger's bundle format into what the receiving Service expects.
Deploy each verifier to the opposite network
Verifiers are ordinary contracts on the receiving side. Deploying one is permissionless.
Set up a Channel between the two CLPR Services
Commit on both ledgers, then reveal on both, naming the verifier each side will use. The verifier's address is fixed for the channel's lifetime, though the contract behind it may be upgradeable.
Share each network's endpoints with the other
The endpoint list is on-chain state on each side and is proved into the channel at creation; later changes travel inside bundle proofs.
Set up a common Connector bonded to the Channel
The same connector identity is registered on both ledgers: it authorizes on the sending side and pays on the receiving side.
Deploy applications that use the Connector and Channel
Applications implement two callbacks and never touch proofs, relays or verification.
Source: organization profile README, "Example of Extension Process" (org-profile@528e9c0)
Each kind of ledger needs three short documents (state_proof.md, verifier_contract.md, trust_anchor.md) saying how its proofs are built, checked and kept current, plus a verifier that implements IClprVerifier. The verifiers that exist today, documented or as code only:
| Kind of ledger | What its proof is | Its CLPR Service | Its endpoints | Status |
|---|---|---|---|---|
| Hiero networks: Hedera and private HashSpheres | A joint threshold signature by the network's validators over a summary of its state, plus the paths down to each record it proves | Built into the consensus node | Every consensus node is one | documented and built |
| EVM ledgers running QBFT | A block header carrying the validators' seals, plus a proof of the storage slots that hold the data | Solidity contracts | The endpoint program | documented and built |
| Ethereum mainnet | Sync-committee signatures checked by a light client | Solidity contracts | None: the endpoint program does not support it | verifier code only |
| Sei, a CometBFT network | CometBFT validator signatures (Ed25519) | Solidity contracts | The endpoint program | verifier code only |
| Avalanche | Not implemented; a mock verifier stands in | Solidity contracts | None: the endpoint program does not support it | stand-in only |
Source: Hiero state proof · Hiero verifier · HieroVerifier.sol · QBFT state proof · QBFT verifier · QBFTVerifier.sol · EthMainnetVerifier.sol · verifier README · endpoint proof types · SeiCometBftVerifier.sol · verifier README · MockAvalancheVerifier.sol (clpr-spec@945082d, clpr-smart-contracts@276c8d5, clpr-endpoint@2ccd48d)
The verifier developer is an off-chain role with no protocol identity: ledger implementers, connector operators, application developers with high-value flows and auditors all have reasons to write one. A proof format change needs a new verifier and a new channel; an upgrade key on a proxy verifier is a single point of compromise; and the protocol pays nothing for maintenance.
Section 20of 23
The reference implementations
Source: contracts docs/architecture.md · contracts README · endpoint README · design §3.1.5 · PeerSelector.java · org profile (clpr-smart-contracts@276c8d5, clpr-endpoint@2ccd48d, clpr-spec@945082d, org-profile@528e9c0)
Everything CLPR is made of. Tap a piece.
Wider than a phone; scroll it sideways if you need to.
The specification
Where: LFDT-CLPR/clpr-spec
The rulebook every other piece must follow. Two documents: a design document that explains the why (trust model, layers, roles, recovery) and a protocol specification that fixes the what (wire formats, state, algorithms, a pseudo-API). Alongside them, one folder per supported proof system saying exactly how that ledger's proofs are built and checked, and the decision records that explain the main changes made to the text.
clpr-service.md (design), clpr-service-spec.md (normative: protobufs, on-ledger state model, verifier interface, §4 algorithms, §6 pseudo-API), verifiers/{hiero,qbft}/ (state_proof.md, verifier_contract.md, trust_anchor.md), ADR/ (five decision records, the newest in the final form the ADR guide defines) and clpr-options-to-consider.md (thirteen undecided items). The repository's LICENSE file is Apache-2.0; under the Labs charter, specifications are to be developed pursuant to the Community Specification License 1.0.
Pinned: design document · protocol specification · verifiers/ · ADR/ · LICENSE · Labs charter (PDF)
CLPR Service, native (Hiero)
Where: hiero-ledger/hiero-consensus-node
On Hiero networks the CLPR Service is built into the node software itself. Its queues and channels live in the node's own state tree, which is what makes them provable with the network's own signature. Contracts reach it through a fixed system address.
Reachable from the EVM at system address 0x…016e. State in the Merkle tree → provable via Hiero state proofs (Channel leaf tag 482, ClprMessageValue leaf tag 498). The implementation lives in hiero-ledger/hiero-consensus-node.
Pinned: org profile: initial OSS · design §3.1.0
CLPR Service, contract (EVM)
Where: LFDT-CLPR/clpr-smart-contracts
On EVM ledgers the CLPR Service is a set of Solidity contracts. One immutable front door holds all the state and forwards each call to a module that knows how to handle it: channels, messaging, bundles, connectors, administration. Built and tested with Foundry.
ClprService.sol is a DELEGATECALL router over ClprServiceStorage.sol; logic modules ChannelLogic, MessagingLogic, BundleLogic, ConnectorLogic, AdminLogic; state-mutating libraries BundleLib, ConnectorLib, ManifestLib; pure codecs ClprProtobuf*, ClprTypes. Read functions are not `view` (call via eth_call). storage-layout.json is a CI-checked snapshot because endpoints derive proof slots from it. Deploy through the viem CLI; clprEnabled stays false until explicitly enabled. Known divergences from the spec are listed in docs/bundle-flow.md and docs/quirks.md.
Pinned: ClprService.sol · docs/architecture.md · IClprService.sol · README
Verifier contracts
Where: LFDT-CLPR/clpr-smart-contracts/src/verifiers
The specialists that check another ledger's proofs. Each channel names one when it is created and keeps that address for life, though the contract behind it may be upgradeable. Two have verifier specifications in the specification repository: one for Hiero networks (checking the network's joint threshold signature) and one for EVM ledgers running QBFT (checking validator seals on block headers plus storage proofs). The repository also carries verifier code for other consensus families.
hiero/HieroVerifier.sol (decodes the StateProof protobuf, derives the SHA-384 block root, calls TSSVerifier.sol for the hinTS BLS12-381 aggregate signature using EIP-2537 precompiles, walks leaf paths) with wraps/ helpers (WRAPS verification key, Poseidon/BN254); evm/qbft/QBFTVerifier.sol (committed seals, EIP-1186 account and storage proofs, epoch headers for rotation); evm/ethereum/EthMainnetVerifier.sol (sync-committee BLS light client); evm/sei/SeiCometBftVerifier.sol (CometBFT, Ed25519); evm/avalanche/MockAvalancheVerifier.sol. The spec's verifier documents describe a native verification path at 0x16e on Hiero.
Pinned: HieroVerifier.sol · QBFTVerifier.sol · IClprVerifier.sol · spec verifiers/ · design §3.1.5
Endpoint (relay) for EVM ledgers
Where: LFDT-CLPR/clpr-endpoint
The endpoint program for EVM ledgers. It watches its own ledger's CLPR Service, talks to endpoints on the other side over gRPC, builds proofs of what its ledger has queued, dry-runs every bundle it receives, and submits the good ones as ordinary transactions. Its keys pay its own transaction fees and identify it to other endpoints over mutual TLS when the operator enables it; none of them can make a ledger accept a message.
Java 25, Gradle, seven modules: proto (protobuf/PBJ), core (interfaces: TransactionSubmitter, ContractStateReader, BundlePayloadCodec, BundleConstructor, PeerSyncClient, PeerEndpointCache, CommitmentLevel), evm (JSON-RPC, ABI codec, ECDSA signer, EIP-1559 submission), grpc-server (Helidon + PBJ), grpc-client, sync (per-channel virtual-thread loops, peer selection: random, weighted by past successes; failing peers back off), app (YAML config, wiring, Prometheus metrics). Two listeners: info (9546, plaintext) and sync (9545, optional mandatory mTLS). Local proof types QBFT and CometBFT; Hiero only as a peer. Dockerfile and a Helm chart are included. Polls the chain rather than subscribing to events.
Pinned: README · docs/architecture.md · docs/sync-protocol.md · relay-config-example.yaml
Endpoints on Hiero
Where: part of the native implementation
On a Hiero network there is no separate endpoint program: every consensus node is automatically an endpoint, and the list of them is kept up to date by the network itself. The node builds and signs the proofs as part of producing blocks; the endpoint only fetches the finished proof and carries it.
The endpoint manifest is derived from the consensus roster (joins, leaves and identity rotations bump its version; stake-only changes do not). Proofs are built by the node's state-proof machinery and retrieved by the endpoint; the relay's HieroProofCodec parses them and submits them untrimmed. Misbehaving nodes are removed by Hiero governance, which removes them from the manifest.
Pinned: design §3.1.2 · verifiers/hiero/state_proof.md
The sync protocol
Where: spec §1.5; clpr-endpoint proto + grpc modules
How two endpoints talk: specified to run over mutual TLS, each sync is one stream for one channel, on which each side says how far it has got and receives the next messages after that point, up to the bundle limits, together with the proof. Requests are cheap and unproven; bundles are proven. Nothing is delivered that the receiver did not ask for.
gRPC ClprEndpointService.sync: a bidirectional stream of ClprSyncPayload (channel_id, optional BundleRequest, optional BundleResponse); one stream is one cycle; both fields absent closes it. mTLS with an on-chain ECDSA P-384 CA per endpoint (≤ 512 bytes) and a per-process Ed25519 leaf. Message sizes bounded by max_sync_bytes. The reference endpoint still implements the earlier unary exchange: one ClprSyncPayload (channel_id, bundle_payload) each way, defined in clpr-relay-proto and served by clpr-relay-grpc-server.
Pinned: spec §1.5 · ADR 2026-07-14 · clpr-relay-proto · ClprSyncService.java
Connector contracts
Where: IClprConnector (EVM); native registration on Hiero
The account that pays, present on both ledgers under one identity. On the sending side its contract decides whether to approve a message; on the receiving side its balance pays for running the message and its locked stake is what gets fined if it breaks its promise. Anyone can register one, on any channel, with a balance and a stake.
IClprConnector: authorizeOutboundMessage(channelId, targetApplication, sender, messageData) → bool; payForExecution(amount) must transfer exactly amount to the caller; onInboundMessage(...) is a best-effort notification under a gas stipend. Registered by commit-reveal with connector_id = keccak256(channel_id ‖ public_key ‖ salt). EVM specifics: per-connector queue quota and geometric slashing (docs/evm-platform-spec.md).
Pinned: IClprConnector.sol · design §3.3.1 · spec §6.3
Applications
Where: IClprApplication; anything you deploy
Your program. It calls one function to send and implements two callbacks to receive: one for an incoming message (whatever it returns becomes the reply) and one for the reply to a message it sent. It chooses, and should vet, the channel and the connector for each message; proofs, endpoints, verification and payment are handled below it.
IClprApplication.onClprMessage(channelId, sender, messageData) → responseData; onClprResponse(channelId, messageId, status, responseData), invoked best-effort and only for senders with code. Send with IClprService.sendMessage(channelId, connectorId, targetApplication, messageData) → messageId. The repository's E2EApplication.sol shows a contract acting as both sender and receiver.
Pinned: IClprApplication.sol · E2EApplication.sol · design §3.4 · design §2.4 · IClprService.sol
The lab: LF Decentralized Trust
Where: LFDT-CLPR/governance and .github
Where the specification, the smart contracts and the EVM endpoint are governed: an incubation space under the Linux Foundation's LF Decentralized Trust, offered by the Hedera Governing Council and open to anyone under the Apache License 2.0 with a Developer Certificate of Origin sign-off on every contribution.
governance/: the signed Labs charter (TAC oversight; Apache-2.0 code; DCO; CSL-1.0 specifications; CC BY 4.0 docs), teams.yml (clpr-maintainers with maintain rights everywhere; lfdt-staff with admin on .github and governance) and CODEOWNERS. .github/profile/README.md: scope of the lab, terminology, the four layers, the extension process.
Pinned: organization profile · teams.yml · Labs charter (PDF)
EVM: clpr-smart-contracts
A Foundry project. ClprService.sol is an immutable router holding all state and dispatching by DELEGATECALL to five logic modules (ChannelLogic, MessagingLogic, BundleLogic, ConnectorLogic, AdminLogic) that share one storage layout (ClprServiceStorage.sol); state-mutating helpers (BundleLib, ConnectorLib, ManifestLib) take storage references, ClprProtobuf is a pure protobuf codec, and ClprTypes holds the enums, structs, custom errors and shared events. Two consequences to know before you integrate: read functions are not annotated view (call them via eth_call), and storage-layout.json is a committed snapshot that CI regenerates and diffs, because every logic module writes into that one layout. Deployment goes through a viem CLI; clprEnabled is false after deploy until an explicit enable, and when the owner is a multisig the script stops and prints the admin calls. Verifiers live under src/verifiers/: hiero/HieroVerifier.sol, which decodes the StateProof, derives the SHA-384 block root, has TSSVerifier.sol check the hinTS aggregate signature with the EIP-2537 precompiles and walks the leaf paths, with wraps/ helpers (WRAPS verification key, Poseidon, BN254); evm/qbft/QBFTVerifier.sol; and beacon-chain BLS (EthMainnetVerifier), Sei/CometBFT and a mock Avalanche verifier. Interfaces the protocol calls outward are IClprApplication, IClprConnector and IClprVerifier; the public ABI is IClprService.
Wider than a phone; scroll it sideways if you need to.
The repository records its non-obvious design choices and pipeline mapping (docs/bundle-flow.md, docs/quirks.md, docs/evm-platform-spec.md). Divergences that hold in the pinned code: the hash folded as sha256(prev ‖ sha256(payload)); a REDACTED message type still present although ADR 2026-08-01 removed redaction from the spec; bundles that must run to the sender’s newest message rather than a prefix; and the platform bindings described under connectors above. Some of the docs are themselves behind the code: several items on their divergence list are fixed in it (payload size is checked against the peer’s throttles, Channel.peerThrottles is populated, cross-bundle response ordering is validated, a malformed data message gets an error reply instead of unwinding the bundle), and bundle-flow.md still describes submitBundle as gated to registered endpoints, while the pinned code is permissionless. When in doubt, the Solidity is the reference.
Hiero: the native service
On Hiero the CLPR Service is a native service inside the consensus node, reachable from the EVM at the system address 0x…016e. Its state is part of the node’s Merkle tree, which is why its queue can be proven directly. A native verification path for Hiero proofs (VerifyConfigCall/VerifyBundleCall) is documented alongside the verifiers, and verifiers for other ledgers are contracts deployed to Hiero. The native implementation lives in the hiero-ledger/hiero-consensus-node repository.
The endpoint: clpr-endpoint
A standalone Java 25 relay for EVM ledgers, built with Gradle from seven modules: protobuf/PBJ types, core domain types, a Helidon gRPC server, an outbound gRPC client, EVM interaction over JSON-RPC, sync orchestration and peer management, and the application entry point. It serves two listeners: an always-plaintext info port (default 9546) for chain-verifiable public data, and the sync data plane (default 9545), plaintext by default and mandatory mTLS when enabled, minting the Ed25519 leaf from the operator’s P-384 CA key exactly as specified. The reference endpoint still serves and calls the earlier unary sync (one ClprSyncPayload each way, no BundleRequest); the streaming cycle described in the sync section is the specified behaviour. Local networks are declared with a proof type (QBFT, or CometBFT for Sei); Hiero appears only as a channel’s peer proof type. The maxMessageSize default is 1 MiB, and the config comments warn that the PBJ default of 10 KiB “is too small for production payloads”. Its QbftBundleConstructor builds the RLP 5-tuple (7 items with a manifest proof), AccountTransactionSubmitter previews each submitBundle with eth_call before paying, and HieroProofCodec parses proofs the Hiero node produced. The repository’s docs/ folder is the map: architecture.md (the seven modules, the app → sync/grpc/evm → core → proto dependency direction, the core abstractions TransactionSubmitter, ContractStateReader, BundlePayloadCodec, BundleConstructor, PeerSyncClient, PeerEndpointCache, CommitmentLevel), sync-protocol.md, evm-integration.md (the contract surface it calls, EIP-1559 submission, nonce handling, and why it polls getChannel instead of subscribing to events), multi-chain-design-changes.md (the three-tier local-network → service → channel hierarchy and the checklist for adding a chain type) and configuration.md. Some of those documents describe work in flight, for instance a stub bundle constructor, so read them against the pinned code. Outbound: a per-channel state-change poller notices nextMessageId advancing, the channel’s sync task reads the new messages and builds a payload, a peer is picked at random, weighted by past successes, with failing peers backed off, and the peer’s sync RPC is invoked. Inbound: the peer’s bundle is decoded; for EVM-format bundles, messages the local ledger already accepted are trimmed so a duplicate cannot sink the bundle as a replay (a Hiero proof is submitted whole); and the submitter signs and sends submitBundle client-side, polling for the receipt.
Wider than a phone; scroll it sideways if you need to.
Act VI · Reference
Evolution, governance, glossary
How the text changes, who governs the lab, and the terms in one place.
Section 21of 23
How the specification evolves
Source: ADR guide · options to consider (clpr-spec@945082d)
The text comes in two parts. The design document (clpr-service.md) explains the reasoning: the problem, the layers, the participants, the trust model, and what happens in each failure. The protocol specification (clpr-service-spec.md) is the normative part every implementation is measured against: the exact bytes on the wire, the on-ledger state, the step-by-step algorithms, and a language-independent description of every operation a service offers. Next to them sit one folder per proof system under verifiers/, the decision records under ADR/, and the list of options under consideration.
Wider than a phone; scroll it sideways if you need to.
Three rules govern change. Any change to the bytes that travel between ledgers raises the protocol version, and two ledgers must agree on it or stop talking. Data an implementation does not recognize is rejected, never skipped. And the EVM contracts keep a written list of where they still differ from the text.
Decisions that change the specification are recorded as Architecture Decision Records, named YYYY-MM-DD-short-title.md, with a fixed final form: an abstract ending in an enumerated “Changes in this ADR”, a problem description per discrete problem, the options considered and non-goals, the complete solution and the tests that check it, plus optional appendices for line-by-line spec patches and implementation notes.
Wider than a phone; scroll it sideways if you need to.
Only final-form ADRs are meant to be committed to main. Five are there; the newest, 2026-08-01, follows the final form, and the four earlier records use an older layout:
| ADR | Decision |
|---|---|
| 2026-07-02 | Add an immutable ChannelContext (channel id, peer service address) to the verifier interface, separate from the rotating trust anchor, so EVM/QBFT verifiers can derive storage slots and provenance cannot be misattributed. |
| 2026-07-03 | Move endpoint lists out of ConfigUpdate into a proven, versioned on-ledger manifest refreshed through bundle payloads, fixing the complete-turnover deadlock; two-step admission with bond on permissionless ledgers. |
| 2026-07-14 | Replace the unary sync RPC with a bidirectional-streaming request/response cycle that eliminates systematic duplicate sends; adopt a P-384 root CA / Ed25519 leaf mTLS model. |
| 2026-07-17 | Rename ClprQueueMetadata to a minimal, separately provable ChannelSyncData; allow bundles to carry a proper prefix of the backlog. |
| 2026-08-01 | Remove message redaction: redacted and unredacted encodings were hash-equivalent, so any permissionless submitter could flip them undetected. Simplifies the running hash. |
Wider than a phone; scroll it sideways if you need to.
A separate document lists thirteen options to consider, not yet decided: application-level clawback, migrating messages between channels (three approaches compared), graceful connector wind-down, reconciliation tooling for UNRESOLVED messages, multiple proof formats per channel, the admin incentive gap, verifier developer sustainability, channel-creation anti-griefing, connector deregistration on PAUSED channels, indefinitely PAUSED channels, post-quantum endpoint signing keys, SHA-384 for the running hash, and cross-ledger propagation of endpoint signing keys. If you are looking for somewhere to contribute, that list is the map.
Section 22of 23
Governance and the lab
Source: governance/teams.yml · Labs charter · organization profile · governance/GOVERNANCE.md (governance@1447ae0, org-profile@528e9c0)
CLPR is incubated as a lab of LF Decentralized Trust, a Linux Foundation project. The signed Labs charter, kept in the lab’s governance repository, fixes the rules every lab operates under:
- Oversight by the foundation’s Technical Advisory Council (TAC), which approves project proposals, including incubation and deprecation, organizes and removes sub-projects, and approves the processes for contributing.
- Licensing. Inbound and outbound code under Apache License 2.0, every new inbound code contribution carrying a Developer Certificate of Origin sign-off; specifications under the Community Specification License 1.0; documentation under CC BY 4.0. Exceptions need a two-thirds vote of the entire TAC. Files should carry SPDX identifiers.
- Copyright in new contributions stays with its holders, and no contributor is required to assign it. Project trademarks are transferred to LF Projects, LLC, which holds them for the project; the Hedera Governing Council undertook to donate the CLPR brand and trademark to LFDT once the lab was accepted.
- Openness. Participation cannot be refused on any basis that is not reasonable and non-discriminatory, and “the output of all Project discussions, proposals, timelines, decisions, and status should be made open and easily visible to all”.
The mission of the Labs project is “to support development of very early open projects”.
Inside the lab, every team and its rights on each repository are defined in teams.yml, and a change to a role or team is a pull request against that file referencing a governance issue. Anyone who contributes is a Contributor and needs no team. Committers are per repository: the spec, smart-contracts and endpoint repositories each have a <repository-name>-committers team with write access. One clpr-maintainers team, with maintain rights on every repository, takes the decisions that affect the whole organization, including creating repositories and adding or removing Committers and Maintainers. General teams cover release engineering (clpr-release-engineers), security (security-maintainers) and foundation staff (lfdt-staff, admin on .github and governance). Decisions aim for consensus; otherwise the Maintainers vote, in the weekly call when more than half of them are present or asynchronously over 14 days, and a vote passes when more Maintainers vote for it than against it.
The lab’s scope: collaboration on the specification, the reference implementations of service, verifiers and endpoints, application development, and native integrations into other ledger types.
Section 23of 23
Glossary
Source: design §2.1 (clpr-spec@945082d)
Application
On EVM: a contract implementing IClprApplication (onClprMessage and onClprResponse) that sends by calling IClprService.sendMessage. Each platform defines its own callback interface.
in plain words
CLPR Service
The core on-ledger component: single source of truth for all CLPR state, holder of channels, connectors, endpoint manifests and locked funds. Native to the node on Hiero; a smart contract on EVM ledgers.
in plain words
Channel
An on-ledger entity representing a communication path to one specific peer CLPR Service, bound to one verifier contract for its lifetime. Many channels may exist between the same two ledgers.
in plain words
Verifier contract
A contract implementing verifyConfig and verifyBundle, at an address fixed for the channel's lifetime; it may sit behind an upgradeable proxy. It authenticates state proofs from the peer against the channel's trust anchor and decodes bundle content into the local format.
in plain words
Trust anchor
Opaque, verifier-defined bytes encoding the peer ledger's current signing authority. For Hiero: the ledger_id. For QBFT, in the specification: the validator set hash, the code hash and the epoch data (the EVM contracts encode a single validator address instead). Rotated only through verifyBundle.
in plain words
Connector
An economic entity registered on both ledgers under the same derived id. Authorizes outbound messages, funds inbound execution, and is subject to slashing for connector-attributable failures.
in plain words
Endpoint
A node running the sync protocol: exchanging bundles with peer endpoints over gRPC, obtaining state proofs, pre-verifying, and submitting bundles. On Hiero every consensus node is one; elsewhere endpoints register and post a bond.
in plain words
Message
ClprMessagePayload: a oneof of ClprMessage (Data), ClprMessageReply (Response) or ClprControlMessage (Control). Every Data Message produces exactly one Response Message, in order.
in plain words
Bundle
An ordered batch of message payloads plus a state proof over the anchor message. The running hash chain extends the proof to every message in the batch.
in plain words
State proof
A cryptographic proof that specific data exists in a ledger's committed state, verified against the trust anchor. State proofs are CLPR's sole mechanism of cross-ledger trust.
in plain words
Running hash
SHA-256(previous_running_hash ‖ serialized_payload) in the specification (the EVM contracts hash the payload first: sha256(previous ‖ sha256(payload))), initialised to 32 zero bytes, kept separately for sent and received messages.
in plain words
CLPR Service admin
Exclusively protective authority: configure and close. Cannot create channels, register connectors or take part in economic activity. The network operator or governing body on Hiero; whoever controls the CLPR Service contract on EVM.
in plain words
Plus a few terms that only appear on this page: anchor message, the last message in a bundle, whose running hash the state proof covers; acknowledgement, the peer’s received_message_id as reported in ChannelSyncData, distinct from a response; close-notification, the final bundle an endpoint owes the remote once its side reaches CLOSED; transition bundle, a bundle whose proof advances the trust anchor, possibly carrying no messages; NoProgress, the rejection of a bundle satisfying none of the five progress criteria; HashSphere, a private or permissioned network running Hiero; CAIP-2, the namespace:reference chain identifier standard used for chain_id.
Repositories and commits
The LFDT-CLPR repositories and commits this explainer describes, as of 2026-11-02.
| Repository | Commit | Committed | Used for |
|---|---|---|---|
| LFDT-CLPR/clpr-spec | 945082d | 2026-08-31 | Protocol specification (clpr-service.md, clpr-service-spec.md), ADRs and verifier documents. The primary source for every protocol claim on both pages. |
| LFDT-CLPR/clpr-smart-contracts | 276c8d5 | 2026-09-01 | Solidity CLPR Service, logic modules and Verifier contracts for EVM ledgers. Source of the application interfaces and of every pinned code link on the developer page. |
| LFDT-CLPR/clpr-endpoint | 2ccd48d | 2026-09-01 | Java CLPR Endpoint (relay) for EVM ledgers: gRPC sync, proof construction, bundle submission. Source for the endpoint and sync-protocol sections. |
| LFDT-CLPR/governance | 1447ae0 | 2026-10-28 | Lab governance: LF Decentralized Trust Labs charter, teams and repository permissions. Source for the governance section. |
| LFDT-CLPR/.github | 528e9c0 | 2026-10-28 | Organization profile README: lab scope, terminology, the four layers and the extension process. Source for the 'what CLPR is' framing and the LFDT section. |