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.

Ledger A Ledger B application application verifier trust anchor verifier trust anchor endpoint in the node endpoint a relay

Step through all 28 steps →

About 95 minutes, with things to play with Repositories and commits are listed at the end of the page Non-technical version →

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:

End User exposure: application-level vets Application Developer exposure: fees, users' assets vets Connector Operator exposure: balance, stake (slashable) vets Channel (verifier) verifier code, peer legitimacy built by Verifier Developer exposure: none in-protocol vets Distributed Ledger proof system, cryptography outside the chain Endpoint Operator serves every channel; fronts the fees CLPR Service Admin configure, close; no economic incentive The party with economic exposure does the vetting. The verifier developer has none in-protocol; the admin has power but no economic incentive.
Who checks whom. Each role vets the one below it before committing value. The connector operator carries the most exposure; the verifier developer, whose code every channel depends on, has none inside the protocol; the admin and the endpoint operator sit outside the chain. Source: design §2.4 (from the spec)

Wider than a phone; scroll it sideways if you need to.

  1. Its own ledger’s consensus: Ledger A orders and finalizes correctly.
  2. The local CLPR Service: the implementation on A executes the protocol faithfully.
  3. The channel’s verifier: the contract that validates B’s proofs. “A buggy or compromised verifier could accept fabricated proofs.”
  4. The remote ledger’s consensus and CLPR Service.
  5. 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.

Safety no fabricated message accepted Ledger A consensus and CLPR Service A Ledger B consensus and CLPR Service B Verifier chosen per channel; vet before use Even with dishonest endpoints, no fabricated messages can be accepted; a dishonest connector cannot forge or tamper either. Liveness things keep moving Endpoints at least one honest per side must relay bundles A dishonest endpoint can withhold bundles; a dishonest connector can refuse to pay. Neither forges. Buggy or compromised verifier "could accept fabricated proofs": the safety piece a channel adds and an application chooses. There is no verifier allow-list; evaluate the verifier contract behind a channel before using it. Safety lives on the ledgers and the verifier the channel chose. Liveness depends on one honest endpoint per side.
Two different failures. Safety (no fabricated message accepted) rests on the two ledgers, their CLPR Services and the channel's verifier, the piece a channel adds and an application chooses. Liveness (things keep moving) rests on at least one honest endpoint per side. An endpoint can only stall; it cannot forge. Source: design §2.3 · spec §8.2 (from the spec)

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)

LayerResponsibilityKey abstractionsCapability gained
NetworkTransport between ledger endpoints; channel establishment (permissionless, commit-reveal); trust anchor updates; throttle negotiationChannel, endpoint, verifier contract, gRPC stream, protobuf encodingTwo ledgers can connect.
MessagingOrdered, reliable, state-proven queuing and deliveryMessage queues, bundles, running hashes, state proofsTwo ledgers can pass messages.
Payment & RoutingConnector authorization and payment; dispatch to applications; response generation; slashingConnector contracts, application interfaces, slashingMessages are paid for, submitters reimbursed where a connector can pay, misbehaving connectors punishable.
ApplicationCross-ledger applicationsRemote contract calls, asset movement, atomic swapsApplications address a destination ledger, application and connector.
4 Application remote contract calls · asset movement atomic swaps Applications address a destination ledger, application and connector. 3 Payment & Routing connector contracts · application interfaces slashing Messages are paid for, submitters reimbursed where a connector can pay, misbehaving connectors punishable. 2 Messaging message queues · bundles running hashes · state proofs Two ledgers can pass messages. 1 Network channel · endpoint · verifier contract gRPC sync · protobuf Two ledgers can connect. Read upward: connect, then pass messages, then pay for them, then build on them.
Four layers, four capabilities. The table's rows as a stack: Network lets two ledgers connect, Messaging lets them pass ordered and proven messages, Payment & Routing pays for and polices delivery, and Applications sit on top. The sections below follow the same order. Source: design §3 · organization profile (from the spec)

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.

Your contract on A calls sendMessage; receives onClprResponse Target contract on B onClprMessage runs; returned bytes = the reply CLPR Service A assigns message_id; enqueues the message; delivers the response Endpoints sync, prove, submitBundle both directions CLPR Service B verifies the bundle; charges the connector; queues the reply 1 sendMessage(…) → message_id 8 onClprResponse(…) same message_id 2 bundle + proof 3 submitBundle 4 onClprMessage(…) channelId, sender, data 5 returned bytes = the reply 6 Response Message 7 submitBundle The message_id from step 1 is the key you match in step 8.
One call out, two callbacks in. sendMessage returns a message_id; the destination's onClprMessage return value becomes the reply; that reply reaches your onClprResponse under the same message_id. Steps 2, 3, 6 and 7 are the transport the earlier sections describe. Source: spec §6.4 · spec §6.5 Application Delivery · design §3.3.3 · IClprApplication.sol (from the spec)

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.

Simulation

One message, there and back: the 28 steps

step 0 / 28
Two ledgers, their CLPR Services, queues, connectors, verifiers and endpoints, with a message travelling from the left ledger to the right and its reply coming back. Hiero ledger CLPR Service is native to the node Application sends, gets the reply CLPR Service Outbound queue running hash Verifier for QBFT proofs Connector authorizes & pays sync data · trust anchor Endpoint every consensus node consensus · threshold-signed blocks EVM ledger CLPR Service is a smart contract Application receives, answers CLPR Service Verifier for Hiero proofs Outbound queue running hash Connector pays on arrival sync data · trust anchor consensus · QBFT sealed headers Endpoint separate process gRPC JSON-RPC

Wider than a phone; scroll it sideways if you need to.

Before you press play

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.

  1. Getting in line
  2. 1sendMessage()
  3. 2channel checks
  4. 3quota
  5. 4authorize()
  6. 5config sync
  7. 6enqueue
  8. Proving it happened
  9. 7TSS signature
  10. 8gRPC open
  11. 9fetch proof
  12. 10gRPC send
  13. 11pre-verify
  14. Crossing
  15. 12submitBundle()
  16. 13verifyBundle()
  17. 14hash chain
  18. 15gas budget
  19. 16onClprMessage()
  20. 17settle gas
  21. 18reply queued
  22. 19BundleProcessed
  23. Coming home
  24. 20QBFT proof
  25. 21gRPC back
  26. 22HAPI submit
  27. 23verify seals
  28. 24verify state
  29. 25ordering
  30. Closing the loop
  31. 26onClprResponse()
  32. 27delivered
  33. 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.

H44 = SHA-256(H43 ‖ payload44) · H0 = 32 zero bytes H40 H41 H42 H43 H44 sent_running_hash msg 40 DATA acknowledged, deleted msg 41 DATA ClprMessage msg 42 RESPONSE answers inbound 37 msg 43 CONTROL ConfigUpdate msg 44 DATA ClprMessage slot 45 empty next write acked_message_id = 41 the peer has proven it received up to here; entries ≤ 41 may be deleted, except Data Messages, which wait for their reply 42 … 44 unacknowledged, still in the queue next_message_id = 45 the next sendMessage stores here (step 8) then increments the counter (step 9) storage key (channel_id, message_id) · read by id range when building a bundle · deleted once acked Merkle state map on Hiero; contract storage on EVM ledgers, where gas makes deletion matter Three message classes, one id sequence, one hash chain: nothing is sorted by type.
One queue per direction, nothing sorted by kind. Data messages, responses and control messages share the same ids and the same running hash chain. acked_message_id marks what the peer has proven it received and what can be deleted, except Data Messages, which are kept until their response arrives; next_message_id is where sendMessage writes next. Message ids are illustrative. The formula is the specification's; the pinned reference contracts fold sha256(prev ‖ sha256(payload)). Source: spec §1.4 · spec §4.1 · design §3.2.2 (from the spec)

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 × 32

serialized_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.

Simulation

Five messages, one chain of running hashes

chain intact

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.

#messagerunning hash after itproven?check
1 ping … -
2 pay Bo 5 … -
3 config update … -
4 ping … -
5 pay Cy 2 … 🔏 anchor

🔏 anchor: …

Each running hash is computed from the previous running hash and the message. Try changing something and watch where the chain breaks.

Enqueue (sendMessage)

  1. Look up the channel; reject unless ACTIVE. 1a. If last_config_timestamp is behind the current configuration’s consensus timestamp, enqueue a ConfigUpdate control message first and update the timestamp.
  2. Look up the connector by (channel_id, connector_id); reject if absent.
  3. Call the connector’s authorization contract; reject if not authorized.
  4. Check len(message_data) ≤ peer_config.throttles.max_message_payload_bytes. A stricter local limit is allowed; exceeding the peer’s is not.
  5. Check next_message_id − acked_message_id < max_queue_depth.
  6. Build the ClprMessage, stamping sender from the transaction caller.
  7. Compute the running hash. 8. Store under (channel_id, next_message_id). 9. Set sent_running_hash; increment next_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.

Ledger B receiver received_message_id = 42 · stored H42 CLPR Service recomputes H43 … H46 from its stored H42; must equal the H46 the verifier returned BundleRequest(current_received_message_id: 42) unproven: shapes the bundle, never changes state Ledger A · sender outbound queue unacked ids 43 … 52 · next_message_id = 53 (illustrative) 42 received Bundle 43 … 46 · at the cap (4 here) 43 44 45 46 anchor H42 H43 H44 H45 H46 47 … 52 later cycle next bundle: 47 slice starts at received + 1 = 43 State proof from A's validators: H46 + ChannelSyncData submitted as one tx One proof over the anchor; the running hash extends it back over every earlier message in the slice.
One proof, many messages. The endpoint takes messages from received + 1 up to the cap (max_messages_per_bundle or max_sync_bytes), proves only the last one (the anchor), and the receiver recomputes the running hash from its own stored value. Since ADR 2026-07-17 the slice may be a proper prefix, so a large backlog drains in bundles. Ids and the cap are illustrative. Source: design §3.2.3 · spec §4.1 · ADR 2026-07-17 (from the spec)

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:

#CriterionHolds when…
1New messagesanchor message id is greater than this side’s stored received_message_id
2Trust anchor advancementverifier returns new_trust_anchor_id ≠ Channel.trust_anchor_id
3Endpoint manifest advancementnew_endpoint_manifest.version > Channel.endpoint_manifest.version
4Acknowledgement progresssync_data.received_message_id > Channel.acked_message_id
5Channel 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.

Endpoint submitBundle(channel_id, bundle_payload) opaque proof bytes Verifier contract verifyBundle(payload, trust_anchor, ctx) cryptography only Channel state received_message_id acked_message_id running hash · manifest trust_anchor · ctx passed in returns, or reverts Verified contents sync_data · messages[] anchor_message_id · anchor_running_hash new_trust_anchor · new_endpoint_manifest checked against stored state CLPR Service logic 1a progress · 1b manifest · 1c anchor 2 size · 3 replay · 4 running hash 5 ack / status · 5c lazy config 6 dispatch, one message at a time updated only if every check passes; out-of-order replies only set PAUSED Applications on B per message: resolve connector, pay, call, enqueue the reply step 6 The service's checks catch verifier bugs. They do not catch a compromised verifier, which would return fabricated data with matching hashes.
Two jobs, one transaction. The verifier answers one question: is this payload authentically the peer's state under the current trust anchor? Progress, size, contiguity, the running hash, acknowledgements, status transitions and dispatch are the service's own logic and run the same way for every proof system. Source: spec §4.2 · design §3.2.5 (from the spec)

Wider than a phone; scroll it sideways if you need to.

Simulation

One inbound bundle, checked step by step (spec §4.2)

no faultstep 0 of 16ACTIVE

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

Before submitting:
    Computing the running hashes…

    Source: spec §4.2 · §2.1.2 · §4.1 · §4.4 · §4.5 · §4.6 · design §3.2.5 · §3.2.8. Running hashes are real SHA-256 over the UTF-8 text of each message; the protocol hashes the canonical protobuf serialization of ClprMessagePayload.

    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”.

    Application on A sendMessage → Ma1 Application on B Ma1 processed → reply Rb1 CLPR Service A outbound queue Ma1 kept until Rb1 matches, then deleted acked_message_id = 1 CLPR Service B processes inbound one at a time received_message_id = 1 Rb1 in outbound queue Endpoints bundles both ways 1 Ma1 6 Rb1 Ma1 deleted 3 Ma1 reply Rb1 2 bundle: Ma1 Data Message 4 acknowledgement in every sync 5 bundle: Rb1 Response Message The ack frees Response and Control Messages; only the matched response frees a Data Message.
    Delivered is not answered. The acknowledgement rides in every sync payload and says the peer has the message; the response is a message of its own, generated after the destination application ran. The source keeps a Data Message past acknowledgement and deletes it only when its response arrives and matches. Source: design §3.2.8 · spec §4.4 (from the spec)

    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.

    Simulation

    Acks, replies, and what may be deleted when

    Ma1Ma2Ra3Ma4 ACTIVE

    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

    DATAMa1#1 · data message from A in flight, not yet acked
    DATAMa2#2 · data message from A in flight, not yet acked
    REPLYRa3#3 · A's reply to an earlier message from B in flight, not yet acked
    DATAMa4#4 · data message from A in flight, not yet acked

    Latest bundle from Ledger B none yet

    nothing has arrived

    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.

    Ledger A local ledger: holds the channel Channel chain_id = eip155:1 peer service_address = 0x…1 names its peer by both eip155:1 one CAIP-2 id, one ledger CLPR Service 1 service_address 0x…1 publishes chain_id eip155:1 CLPR Service 2 service_address 0x…2 also publishes eip155:1 peer = (eip155:1, 0x…1) ✕ chain_id alone cannot tell them apart Peer identity (ChainID, ClprServiceAddress) a compound key; neither half is enough Private network hashsphere:acme-prod self-assigned, unregistered namespace A peer is (ChainID, ClprServiceAddress); the chain id names a ledger, not a service.
    Two services, one chain id. Nothing stops two CLPR Service contracts on the same ledger from both claiming eip155:1, so a channel identifies its peer by chain id and service address together. Private networks may self-assign a namespace. Source: design §3.1.1 · spec §1.1 (from the spec)

    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:

    FieldMeaningEnforced by
    max_messages_per_bundleHard cap on messages in one bundle, sized to this ledger’s per-transaction execution budgetSending endpoints must respect it; destination rejects the whole bundle at verification step 2
    max_message_payload_bytesLargest single payload this ledger will acceptSource at enqueue (against the peer’s advertised value); destination at bundle processing (authoritative)
    max_gas_per_messageCompute budget for one message; bounds worst-case execution timeDestination, as the gas cap on the application callback
    max_queue_depthMaximum unacknowledged messages in the outbound queue per channel: natural backpressureSource at enqueue
    max_sync_bytesLargest serialized ClprSyncPayload accepted at the gRPC levelEndpoints; must exceed one payload plus proof overhead or the channel deadlocks
    max_local_endpointsCap on this service’s live endpoint manifestService, at admission
    max_peer_endpointsHow many of a peer’s endpoints to cache per channel; the first N in manifest order are keptService, 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.

    Channel on Ledger A A's own record of the channel the record, kept on A channel_id chain_id verifier_contract · immutable trust_anchor endpoint_manifest acked_message_id sent_running_hash received_running_hash … and the rest of the record ChannelSyncData (A's own) status · received_message_id next_message_id Channel on Ledger B same channel_id, its own record the record, kept on B the same fields, its own values from A's sync data, B sets: acked_message_id = A's received_message_id and reads A's status (how a close propagates) ChannelSyncData (B's own) status · received_message_id next_message_id where a rule says Channel.status read this side's ChannelSyncData.status A's ChannelSyncData, state-proven bundle bundle B's, the same in reverse Each side's ChannelSyncData is proven in every bundle; that is how status crosses the gap.
    What a proof actually covers. Each ledger keeps its own Channel record; each side's ChannelSyncData, holding status, received_message_id and next_message_id, is state-proven and carried in every bundle, alongside whatever else the verifier proves. That is how one side's close reaches the other. Source: spec §2.1 (from the spec)

    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.

    You hold a channel name and a key 1 claim 2 reveal Ledger A: claim keccak256(id ‖ key) nothing readable Ledger B: claim the same hash nothing readable post the hash post the hash Watcher of Ledger A sees only the hash cannot claim a name it never saw Ledger A: reveal name + key + signature names the verifier proofs about B Ledger B: reveal name + key + signature names the verifier proofs about A reveal the name and key, prove you hold the key Channel ACTIVE same id on both · verifier fixed for life Both claims go in before either is opened, so nobody can race you on the second ledger.
    Claim on both, then open on both. The commit is a hash that reveals neither the name nor the key. Only after both ledgers hold a commit do you reveal, prove you hold the key, and name the verifier. Source: spec §5.1.1 · spec §5.1.2 · spec §5.1.3 · design §3.1.3 (from the spec)

    Wider than a phone; scroll it sideways if you need to.

    Step-through

    Claiming a channel on two ledgers without being front-run

    step 0 / 7
    Registrantany account, no permission registerChannel(commitment) completeChannel(reveal) registerChannel(commitment) completeChannel(reveal) Ledger A none CLPR Service no channel record Verifier for B's proofsverifyConfig ✓ proofs fetched from Bconfig · endpoint list Ledger B none CLPR Service no channel record Verifier for A's proofsverifyConfig ✓ proofs fetched from Aconfig · endpoint list Attacker watching Ledger Asees only the hash reads the tx: sees the hash only ✕cannot register the id, cannot reveal

    Wider than a phone; scroll it sideways if you need to.

    The registrant holds a channel id and a keypair. Neither ledger knows anything yet.
    Step 0 · off-chain

    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

    channel_id… 32 random bytes
    public_key… 64 random bytes standing in for an uncompressed secp256k1 key
    ownership_commitment… = keccak256(channel_id ‖ public_key)
    keccak256(channel_id)… what the reveal signature is over
    signature(placeholder) no real ECDSA runs on this page
    Full hex, to check with your own keccak256

    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”.

    completeChannel checks first: commitment preimage signature over keccak256(channel_id) proofs verifyConfig config_proof_bytes endpoint_manifest_proof_bytes runs once, on the named verifier reverts if revert a proof fails, service_address mismatch, or manifest version 0 stored Channel record chain_id peer_config_timestamp throttles channel_context (immutable) trust_anchor (+ id) endpoint_manifest version ≥ 1, truncated to max_peer_endpoints verifier_contract fixed for the channel's life becomes ACTIVE ChannelSyncData status: PENDING → ACTIVE received_message_id = 0 next_message_id = 1 The Channel record stores no registration key; the verifier it named stays for the channel's life.
    What completeChannel stores. The verifier checks the two proofs and returns the peer's identity, configuration, throttles, the immutable channel context, the initial trust anchor and the initial endpoint manifest; the service stores all of it and the channel becomes ACTIVE. Source: spec §5.1.3 · spec §3.1 (from the spec)

    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

    Simulation

    The life of a channel

    no channel yet
    deleted closeChannel registerChannel completeChannel closeChannel or peer CLOSING reply mismatch ordered bundle closeChannel or peer CLOSING queue acked peer DRAINED/CLOSED + queue acked or closeChannel (no channel) nothing PENDING claimed, silent ACTIVE normal operation CLOSING no new sends PAUSED out-of-order replies DRAINED all our data acked CLOSED terminal

    Wider than a phone; scroll it sideways if you need to.

    you / admin
    arrives in a bundle
    state: NONEOnly some events make sense in each state; the others are greyed out. Start by registering a channel.

    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.

    Admin (Ledger A) closeChannel Ledger A ACTIVE normal operation (or PAUSED) CLOSING sendMessage rejected; inbound still served DRAINED all own Data Messages acked CLOSED peer DRAINED or CLOSED, and all own messages acked Ledger B ACTIVE normal operation CLOSING followed the peer's status DRAINED all own Data Messages acked CLOSED peer DRAINED or CLOSED, and all own messages acked bundle: status = CLOSING bundles keep flowing (responses, acks); then each reports status = DRAINED bundle: status = CLOSED Closing is a two-sided drain: a side closes only when it has drained and the peer says it has too.
    How a close crosses the gap. The admin closes on one ledger; the new status rides in ChannelSyncData on the next bundle, the peer follows into CLOSING, each side drains its own queue, and each reaches CLOSED only once the peer reports DRAINED or CLOSED and every message it sent is acknowledged. Source: spec §2.1 · design §3.1.3 (from the spec)

    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 enter pending, with a bond whose minimum the platform specification defines; the admin calls addEndpoint to admit it to the live manifest, or to add an endpoint directly with no bond (updateEndpointManifest does either for a batch, one version bump for the whole batch). removeEndpoint by 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).

    Ledger A · local manifest ClprEndpointManifest version n · service_address ClprEndpoint entries service_endpoint (opt.) tls_certificate the source of truth Ledger B · Channel endpoint_manifest peer manifest a proven copy version m ≤ n ≤ max_peer_endpoints replaced whole, by proof changes only by proof completeChannel verifyConfig(proof) 1 proof stores verifyBundle new_endpoint_manifest .version > m · step 1b 2 bundle replaces anyone, directly getEndpointManifest() in a bundle → submitBundle 3 reads submits the old way message queue ConfigUpdate, before ADR 2026-07-03; could deadlock ✕ never reaches B's copy The copy is replaced whole, only by a proof with a higher version, and never through the queue.
    Two manifests, one direction of truth. Ledger A's local manifest is the source; each channel on Ledger B holds a proven copy. The copy changes only by proof: populated at completeChannel, then replaced whole whenever a higher version arrives inside a bundle, including a manifest-only bundle anyone can submit directly, which is what makes recovery scenario R1 possible. Source: design §3.1.2 · spec §2.4 · ADR 2026-07-03 (from the spec)

    Wider than a phone; scroll it sideways if you need to.

    1 at channel creation 2 the verifier checks it, then it is stored Ledger A's endpoint list on-chain, kept by its CLPR Service Hiero: its consensus nodes, automatic EVM: apply with a bond, admin admits Proof of the list sealed by A's validators Ledger B's copy, per channel checked by the verifier, stored with the channel; never typed in by hand Later changes to the list arrive inside ordinary bundles 3 via the endpoints, inside a bundle Every endpoint on A replaced at once? anyone fetches the new list, wraps it in a proof and submits it directly 4 as an ordinary transaction, no endpoint needed Nobody configures the other side's endpoints by hand; a proof does, and anyone can submit one.
    Where the addresses come from. Each ledger publishes its own endpoint list on-chain; the other ledger stores a proven copy per channel and refreshes it from bundles. If every endpoint on one side vanishes at once, anyone can prove the new list and submit it directly. Source: design §3.1.2 · spec §2.4.1 · spec §2.4.2 · design §5 (from the spec)

    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.

    Endpoint on A holds A's outbound queue: #42 #43 #44 Endpoint on B "I have received up to #41" 1 request: "give me everything after #41" cheap, unproven, changes nothing 2 #42 #43 #44 + proof sealed by Ledger A what was missing, up to the bundle limits 3 submit, after a local dry run Ledger B spec rejects stale; EVM skips too low: stale, rejected or skipped here; too high: messages delayed, not lost Nothing is pushed. The request cannot change any ledger; a wrong number only wastes effort.
    Ask, then receive. Each endpoint tells its peer how far it has got and gets back the messages after that point, up to the bundle limits, with a proof. The request carries no proof and needs none, because it cannot change anything. Source: spec §1.5 · design §3.1.4 (from the spec)

    Wider than a phone; scroll it sideways if you need to.

    mutual TLS; each sync is one stream for one channel; each side says how far it got Home ledger outbound queue, and the peer's endpoint manifest Endpoint 1 watch the outbound queue 2 fetch or build the proof 3 sync with a peer 4 dry-run what arrives 5 submit the good ones Peer endpoint the same five things, from the other side Peer ledger read the missing messages, plus the proof read · submit what arrived dry run the ledger's own check, run locally, before any fee submit as an ordinary transaction would be rejected: dropped, nothing paid With a correct verifier an endpoint can delay, not forge; the dry run spares the honest one the fee.
    The endpoint's loop. Watch the outbound queue, fetch or build the proof, sync with a peer, dry-run what arrives, submit only what would pass. The dry run is why a forged bundle costs the honest endpoint nothing. Source: design §3.1.4 · spec §1.5 (from the spec)

    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 verifier

    A 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.

    Step-through

    One sync cycle as specified: four messages on one stream

    step 0 / 4
    Ledger ACLPR Service Endpoint Ainitiator Endpoint Bresponder Ledger BCLPR Service one stream = one sync cycle · one channel_id · mutual TLS ① ClprSyncPayload(bundle_request: A's state) builds a bundle shaped by A's request ② ClprSyncPayload(bundle_request: B's state,bundle_response: bundle for A) verifyBundle locally ✓, then submit submitBundle builds a bundle shaped by B's request ③ ClprSyncPayload(bundle_response: bundle for B) verifyBundle locally ✓, then submit submitBundle ④ ClprSyncPayload(both fields absent) closes the stream; the connection and its TLS session MAY persist, and the next cycle opens a new stream on it press Nextto send message ①

    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.

    Step 0 · before the first message

    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

    Ledger A (Endpoint A's home)
    Ledger B (Endpoint B's home)

    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.

    Private ledger its manifest entry: ClprEndpoint tls_certificate service_endpoint omitted Private endpoint initiator only no inbound port opens every stream itself the only side that can initiate on this channel Public ledger its manifest entry: ClprEndpoint service_endpoint: ip:port tls_certificate Public endpoint service_endpoint published can be dialled responder in this cycle still sends its own BundleRequest, and is answered opens the stream, sends its BundleRequest 1 ✕ no address to dial 2 a bundle, plus its own BundleRequest 3 answers that request with a bundle one bidirectional stream, one channel; the connection may persist across cycles No inbound port, same protocol: on one stream both sides still ask and both sides still deliver.
    Dial out, never in. Leaving service_endpoint out of a manifest entry means nobody can call that endpoint; it opens every sync itself. In the specified stream, which is bidirectional, the public side still gets its own request answered in the same cycle. Source: spec §1.5 · design §3.1.4 (from the spec)

    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.

    Endpoint bundle_payload opaque proof bytes Channel storage trust_anchor trust_anchor_id opaque to the service ChannelContext channel_id, service_address payload passed in Verifier verifyBundle(payload, trust_anchor, ctx) read-only; no per-channel state returns or revert Returned contents sync_data · messages[] anchor_message_id anchor_running_hash new_trust_anchor (+id) new_endpoint_manifest checked CLPR Service the service's checks: hash chain · ordering replay · progress stores new_trust_anchor before any message (step 1c) The verifier validates that the bundle belongs to (trust_anchor, service_address, channel_id). Hash chain, ordering, replay and progress are the service's job.
    Stateless on purpose. Mutable trust material comes in and goes out through the trust_anchor parameters; constant identity comes in through ChannelContext, a field designed never to rotate. The service stores what the verifier returns and never reads the proof bytes itself. Source: spec §3.1 · design §3.1.5 · ADR 2026-07-02 (from the spec)

    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.

    Simulation

    Trust anchors: keeping up with the source's validators

    anchor current
    Encoding:
    one authority at a time (a QBFT validator set)
    Top: the source chain's validator sets by epoch. Bottom: the endpoint, the verifier call, and the destination Channel's stored trust anchor with the authorities it currently accepts. Source ledger: who seals its blocks outbound queue: 2 pending messages epoch 1 V1 signing now epoch 2 V2 epoch 3 V3 epoch 4 V4 epoch 5 V5 epoch 6 V6 epoch 7 V7 trusted by the destination first block of an epoch: sealed by the previous validator set, lists the next set in its extraData Endpoint online, sees epoch 1 local record of anchor ids: 1 remote anchor id 1: current Verifier verifyBundle(payload, anchor, ctx) bundle: none yet verdict Destination ledger: Channel trust_anchor: V1 trust_anchor_id: 1 accepts proofs sealed by V1 V2 the Service never inspects the bytes channel_context: immutable channel_id · service_address every sync payload carries trust_anchor_id = 1 (self-reported and unproven: a cue, not evidence)

    Wider than a phone; scroll it sideways if you need to.

    transactions sent 0 reverted (fee wasted) 0 messages delivered 0 waiting on the source 2 the timeline ends at epoch 7.
    Source
    Endpoint
    Outage

    Concrete encodings

    QBFT (EVM ledger) verifier. The anchor is abi.encode(…) of four fields, 128 bytes in all:

    • bytes32 validatorSetHash keccak256 of the sorted validator addresses
    • bytes32 codeHash keccak256 of the ClprService bytecode
    • uint64 epochLength blocks per epoch
    • uint64 epochNumber epoch of this validator set
    • trust_anchor_id = epochNumber as 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 verifyConfig at completeChannel and afterwards changes only when verifyBundle returns a non-empty new_trust_anchor. There is no admin key, governance call or ConfigUpdate path; 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_anchor and new_trust_anchor_id atomically 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 anchorledger_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 bytesA StateProof protobuf: BlockStateProof (ledger_id, BLS12-381 aggregate TSS signature over a 48-byte SHA-384 block root) plus one MerkleLeafProof per proven leafRLP 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 provenThe 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
    VerificationRecompute 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 proofsThe Hiero node itself; the relay fetches the finished proof and only parses itThe relay, from eth_getBlockByNumber and eth_getProof
    RotationNot 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.

    Hiero (TSS / hinTS) QBFT (EVM ledger) TSS signature BLS12-381 aggregate over the block root, checked against the trusted ledger_id signs block root 48-byte SHA-384 Merkle root domain prefixes 0x00 · 0x01 · 0x02 Merkle paths Channel leaf tag 482 queue metadata: ids, running hashes, status ClprMessageValue tag 498 one leaf per message: payload + running hash QBFT block header committed seals in extraData; ECDSA signers recovered; one must be the anchor's validator keccak the header stateRoot EIP-1186 account proof for the CLPR Service walked to the account's storageRoot storage proofs five channel slots, six with messages status + nextMessageId ackedMessageId + receivedMessageId sentRunningHash · receivedRunningHash + manifest version; the newest message's hash what verifyBundle returns to the service sync_data · messages anchor_message_id · anchor_running_hash Different trees, different signatures, the same handful of values at the end.
    Two state trees, one question. A Hiero proof walks from a threshold-signed block root down to the channel and message leaves; a QBFT proof walks from a sealed header through the account proof to five channel storage slots, six when the bundle carries messages. Both deliver the same sync data and anchor hash for the service's own checks. Source: hiero/state_proof.md · qbft/state_proof.md (from the spec)

    Wider than a phone; scroll it sideways if you need to.

    Ledger A's chain of blocks Two channels into Ledger B block block block finalized cannot be undone block block recent still tentative newest block "send 5 to Bo" a different block, no such line reversed replaced after the fact Channel "fast" verifier accepts the latest block delivered it at once the block is reversed Phantom message on B "send 5 to Bo" ran on Ledger B; the line no longer exists on A Channel "finalized" verifier accepts finalized blocks only still waiting; it never delivers it The verifier sets how final a block must be; anything high-value belongs on a finalized-only channel.
    How sure is sure enough. On ledgers where recent blocks can be undone, a verifier that accepts the latest block is fast but can deliver a phantom message. One that accepts finalized blocks only is slower and never delivers one. Two channels between the same ledgers can use different verifiers. Source: design §3.1.5 · spec §8.3 (from the spec)

    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.

    Application on A sendMessage(channel_id, connector_id, …) Application on B called only after the connector pays the service asks authorizeMessage(sender, target_application, message_size, message_data) Connector on A connector_id = keccak256(channel_id ‖ public_key ‖ salt) true B can pay Channel Data Message · connector_id key: (channel_id, connector_id) look up Connector on B same connector_id balance pays execution, locked_stake is slashable charged, then called no counterpart on B Not registered on B CONNECTOR_NOT_FOUND error reply goes home; source connector slashed Registered on one side only: authorizes on the source, CONNECTOR_NOT_FOUND on the destination.
    Bilateral by construction. The same derived identity is registered on both ledgers of a channel: on the source it authorizes the message, on the destination it pays for executing it. Storage is keyed by (channel_id, connector_id), so a squatter on a third ledger affects only channels there. Source: spec §2.2 · spec §3.3 · design §3.3 (from the spec)

    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.

    Simulation

    A message arrives on Ledger B. Who pays whom?

    SUCCESS

    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

    execution cost4 units (the submitter already paid this to Ledger B as the transaction fee)
    connector charged4.4 units = 4 × (1 + 10/100)
    submitter receives4.4 units, all of it
    submitter's net+0.40 units; the margin
    balance after15.60 units
    reply to Ledger ASUCCESS (or APPLICATION_ERROR if the application reverted; no penalty either way)
    back on Ledger Athe reply is delivered to the sending application; the source connector is charged as for any message, and there is no fine
    charge = 4 × (1 + 10/100) = 4.4 units The submitter fronted the transaction fee and is repaid with a margin on top. There is no protocol treasury: each charge is paid to whoever submitted the bundle, and when the reply arrives the source connector is charged and the Ledger A submitter paid in the same way. The margin is the profit for whoever runs the endpoint; drag the balance down to see what happens when the promise to pay is broken.

    Slashing

    Outcome on the destinationDestination-side actionSource-side action when the reply arrives
    SUCCESSCharge connector; pay submitterDeliver response; no penalty
    APPLICATION_ERRORCharge connector; pay submitterDeliver error; no penalty (the connector paid; the app reverted)
    CONNECTOR_NOT_FOUNDNo connector to slash; submitter absorbs the costSlash source connector’s locked_stake; reimburse the source submitter
    CONNECTOR_UNDERFUNDEDSlash destination connector’s stake; reimburse submitterSlash 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)

    Simulation

    Pick a failure. What does the protocol do?

    ACTIVE
    application and connector
    endpoints and peer ledger
    ordering and limits
    trust

    The application on the other side crashes

    What happensThe bundle still succeeds. The application's revert is caught and the reply carries APPLICATION_ERROR; the EVM contracts also put the revert data in it. Delivery worked; the application simply did not like the message.
    Who paysThe connector pays for the gas used, as normal. No fine.
    Channel stateACTIVE
    Recovers?Nothing to recover. The sending application decides what to do with the error.
    case: app · state: ACTIVE · source: design §3.3.3Thirteen cases; pick one above and the card and the state pill follow it.
    #ScenarioRecovery pathStatus
    R1Endpoints rotated during a partition; none of the new ones knownAnyone proves the remote manifest and submits it as a bundle on-chain; no gRPC neededWorks
    R2Source upgrades its proof formatNew channel with a new verifier; applications migrate; admin closes the old channelWorks (new channel)
    R3Endpoints rotated, proof format unchangedManifest-update bundle as R1Works
    R4Endpoints rotated and proof format changedR2 then R1 on the new channelWorks (new channel)
    R5Verifier compromised or brokenAdmin closes; new channel with a correct verifierWorks; in-flight messages may be unresolved
    R6Peer queue state corrupted; cannot order responsesChannel PAUSED; auto-resumes if the peer fixes it; admin may closeWorks (requires peer fix)
    R7Network partition, endpoints unchangedSyncs resume; ids and hashes verify integrityWorks
    R8Peer ledger downQueue fills to max_queue_depth, then backpressure; resumes where it stoppedWorks
    R9Both sides’ endpoints change at onceEither side submits a manifest-update bundle directlyWorks

    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 finalized for 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.
    CLPR Service submitBundle processing message i submitBundle and sendMessage have a reentrancy guard guard held 1 checks, then all effects for i Channel state (spec: advanced first) received_message_id = i received_running_hash Connector charge the required order 2 dispatch onClprMessage Target application arbitrary code; it may call back into the service onClprMessage(channelId, sender, data) 3 a re-entrant call meets state that has moved on, and the guard Spec: message i is counted and hashed before your callback runs.
    Checks, effects, then the application. Reentrancy guards and the checks-effects-interactions order are required: all Channel state for a message updated before control passes to the target application. The pinned reference contracts rely on the reentrancy guard; they update the received counters after dispatch and charge the connector after the call, from the gas used. Source: spec §8.5 · design §3.3.3 (from the spec)

    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)

    RoleWhat they doWhat they risk
    End userUses 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 developerSends 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 operatorOften 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 developerAn 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 operatorOn 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 adminBroad 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)

    Each arrow: checks the next role before putting anything at risk User at stake: what the app holds for them Application developer at stake: users' assets and their fees to connectors Connector operator at stake: the most; balance plus stake, fined on failure Verifier developer at stake: nothing inside the protocol; channels rest on it Other ledger's proof system what the verifier must get right none Money at risk: most in the middle, none at the end Outside the chain Endpoint operator fronts fees; repaid with a margin when the connector pays; trusts its ledger's CLPR Service Service admin sets limits, admits endpoints, closes channels in emergencies; stake: none Follow the arrows: each role checks the next. Follow the money: heaviest in the middle, absent at the end.
    A chain of vetting. Each role checks the next role in the chain before it risks anything. The connector operator carries the most money and is expected to look hardest; the verifier developer, whose code every channel depends on, has nothing at stake inside the protocol; the admin has power without stake. Both are known gaps: nothing in the protocol pays for verifier maintenance, and the admin has no economic incentive to act. Source: design §2.4 · design §4 (from the spec)

    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.

    Trigger compromise: security advisory upgrade: proof format change 1 Verifier Developer publishes a patched or new-format verifier 2 Connector Operator new channel with it; connector on both ledgers 3 registers CLPR Service Admin closeChannel(old) compromise: at once; upgrade: after migration closes Old channel in-flight may end UNRESOLVED CLOSING → DRAINED → CLOSED verifier address fixed New channel correct verifier; ACTIVE new traffic goes here migrates Application Developer channel_id · connector_id switch and migrate users 4 A channel's verifier address is fixed for its life, so a different verifier means a new channel. For a compromise, the window of vulnerability is between discovery and close.
    A different verifier means a new channel. The verifier developer publishes, the connector operator registers, the application developer migrates and the admin closes the old channel. For a compromise the close comes first and in-flight messages may end UNRESOLVED; for a planned format upgrade the close comes last, after the source has kept the old format alive long enough to migrate. Source: design §4.7 · design §5 (from the spec)

    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:

    Step-through

    Four stories: the roles working together

    step 0
    Story:
    End user Application developer Connector operator Verifier developer Endpoint operator CLPR Service admin

    Wider than a phone; scroll it sideways if you need to.

    Setting up a new cross-ledger route

    Before anything happens

    Press Next to walk through the 4 steps. The roles that act in each step light up above.

    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)

    story 1 · step 0Four stories, six roles. Each step names who acts; nobody acts alone for long.

    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.

    Step-through

    Adding a new ledger type

    step 0 / 8
    New ledger type Hiero network CLPR Service Endpoints Hiero verifier(runs here) Connector Application CLPR Service Endpoints(every node) New-ledger verifier(runs here) Connector Application deployed deployed Channel (verifier addressfixed on each side) endpoint lists,proved on-chain

    Wider than a phone; scroll it sideways if you need to.

    Before you start

    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.

    Source: organization profile README, "Example of Extension Process" (org-profile@528e9c0)

    step 0 / 8 · existing CLPR Services stay as they are

    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 ledgerWhat its proof isIts CLPR ServiceIts endpointsStatus
    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)

    Map

    Everything CLPR is made of. Tap a piece.

    nothing selected
    The specification design document · protocol specification · verifier specs (Hiero, QBFT) · decision records Hiero network Applications Connectors CLPR Service · native service channels · queues · connectors · locked funds inside the consensus node, state in the Merkle tree system address 0x…016e Verifiers for peers' proofs (system contract / native) Endpoints · every consensus node fetch state proofs, build bundles EVM ledger (e.g. QBFT) Applications Connectors CLPR Service · Solidity contracts ClprService.sol router → logic modules channels · messaging · bundles · connectors · admin clpr-smart-contracts Verifier contracts: HieroVerifier, QBFTVerifier, … Endpoint · separate Java relay clpr-endpoint · JSON-RPC to the node gRPC sync gRPC sync · one exchange/cycle unproven requests, proven bundles a Channel on each side: same id, own verifier, own queue, own running hash LF Decentralized Trust lab · github.com/LFDT-CLPR clpr-spec · clpr-smart-contracts · clpr-endpoint · governance · .github · Apache-2.0, DCO, Labs charter

    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)

    selected: spec · LFDT-CLPR/clpr-spec

    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.

    Caller application · endpoint · admin call · reads are not view: use eth_call ClprService.sol immutable router · holds all state dispatches each call by DELEGATECALL ClprProtobuf* ClprTypes pure helpers: types, events, errors ChannelLogic MessagingLogic BundleLogic ConnectorLogic AdminLogic ClprServiceStorage.sol the one shared storage layout: every module reads and writes the router's own storage BundleLib · ConnectorLib · ManifestLib state-mutating helpers; they take storage references snapshot storage-layout.json committed snapshot; CI regenerates and diffs it Endpoint proves the same slots One contract owns the state; modules borrow its storage; CI guards the layout that proofs depend on.
    One router, five modules, one storage layout. ClprService.sol is not upgradeable and holds every byte of state; each call is DELEGATECALLed to a logic module that writes into the router's own storage. Because every module writes into that one layout, storage-layout.json is committed and CI regenerates and diffs it on every change; the endpoint's proofs depend on the same slots. Source: contracts docs/architecture.md · ClprService.sol (reference implementation)

    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.

    Local EVM node JSON-RPC getChannel is polled; no event subscription Peer endpoint sync RPC port 9545 mTLS when on: P-384 CA, Ed25519 leaf Outbound · the local queue grew State-change poller one per channel; wakes the sync task when nextMessageId advances Sync task reads the new messages, builds payload and proof QbftBundleConstructor Peer picker random, weighted by past successes; failing peers back off Inbound · a peer's bundle arrived Decode and trim parses the peer's bundle; drops messages the ledger already accepted (EVM) Dry-run eth_call preview of submitBundle; skipped if it would revert Submitter signs submitBundle client-side · EIP-1559 polls for the receipt info port 9546 always plaintext · chain-verifiable public data Out: poll, build, pick a peer, send. In: decode, trim, dry-run, then sign and pay.
    Two lanes through one relay. On the way out the relay notices the queue grow, builds a payload with a proof and sends it to a peer in a sync call (unary in the reference endpoint; the spec defines a stream), the peer picked at random, weighted by past successes. On the way in it decodes the peer's bundle, drops messages its ledger already accepted (for EVM-format bundles) so a duplicate cannot sink the bundle as a replay, previews submitBundle with eth_call, and only then signs and pays. Source: endpoint docs/architecture.md · docs/sync-protocol.md (reference implementation)

    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.

    Design document clpr-service.md the why: problem, layers, roles, trust, failures Protocol specification clpr-service-spec.md the what: byte formats, state, algorithms, operations one folder per proof system verifiers/hiero state_proof verifier_contract trust_anchor verifiers/qbft state_proof verifier_contract trust_anchor ADR/ five dated decision records changes changes Options to consider thirteen ideas not yet decided Two documents carry the rules; the records say why they changed; the options list says what is still open.
    The document set. The design document explains the reasoning and the protocol specification fixes the rules. One folder per supported proof system says how that ledger's proofs are built and checked. Decision records carry changes with their reasoning; the options list holds what is undecided. Source: clpr-spec · ADR/README.md · verifiers/README.md · options to consider

    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.

    YYYY-MM-DD-short-title.md one dated file Problem what is broken, in one statement e.g. a redacted and a full message hash alike Options considered weighed; rejected ones stay on record e.g. prove every message; too costly Accepted solution the one chosen, written in full e.g. remove redaction Tests checks a correct implementation e.g. both sides agree on the hash Specification files design document protocol spec verifier documents The change ships with its argument: anyone can read why the text says what it says.
    One file carries the change and its reasoning. An Architecture Decision Record states the problem, the options weighed, the accepted solution and the tests that check it, with optional appendices for line-by-line spec patches and implementation notes. The example lines follow the 2026-08-01 record that removed message redaction. Source: ADR guide · ADR 2026-08-01

    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:

    ADRDecision
    2026-07-02Add 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-03Move 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-14Replace 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-17Rename ClprQueueMetadata to a minimal, separately provable ChannelSyncData; allow bundles to carry a proper prefix of the backlog.
    2026-08-01Remove message redaction: redacted and unredacted encodings were hash-equivalent, so any permissionless submitter could flip them undetected. Simplifies the running hash.
    Message m, unredacted full payload bytes; contributes payload_hash Message m′, redacted a marker carrying the same payload_hash, directly Running hash, before SHA-256(prev ‖ payload_hash) the chain only ever saw payload_hash, never which encoding supplied it same digest hash(m) = hash(m′) Any submitter submitBundle is not gated; swaps m′ in for m swapped bundle Verifier + service only the anchor is state-proven; the chain is recomputed from the bytes passes either way nothing in the chain tells the two encodings apart Since ADR 2026-08-01 no redaction: one encoding per message, one chain contribution running_hash = SHA-256(previous_running_hash ‖ serialized_payload) · protocol version bump pinned EVM contracts still implement redaction Two encodings, one hash: the chain cannot tell them apart, so the feature was removed.
    Why redaction was removed. A redacted and an unredacted encoding of the same message folded to the same running hash, so anyone submitting a bundle could flip one for the other and every check would still pass. ADR 2026-08-01 deleted the feature; the pinned EVM contracts predate the ADR and still implement redaction (redactMessage, owner-only) and the old double hash. Source: ADR 2026-08-01 · spec §4.1 (from the spec)

    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
    The program that wants to talk to another ledger. It implements two callbacks: one for when a message arrives, one for when a reply comes back.

    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
    The protocol's own part of each ledger. It keeps the outbound queues, checks the proofs, routes messages to applications, and handles the money.

    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
    A standing agreement between two ledgers to exchange messages, with its own numbered queue in each direction and a fixed way of checking the other side's proofs.

    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
    The specialist that knows how to check one particular ledger's signatures and proofs. Each channel picks one when it is created and keeps that address for life, though the code behind it may be upgradeable.

    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
    Who is allowed to sign for the other ledger right now. Stored per channel and only ever updated by a proof that the previous signers approved the change.

    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
    The account that pays. It approves a message on the sending side and pays for running it on the receiving side. If it breaks that promise, it is fined.

    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
    The program that carries messages between ledgers. It watches its own ledger, collects proofs, talks to endpoints on the other side, and submits what it receives as an ordinary transaction. It cannot forge anything.

    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
    A parcel of bytes plus an address. Three kinds share one queue: data (what applications send), responses (the answer to a data message), and control (protocol housekeeping).

    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
    A batch of messages that travels together with one proof covering the last of them.

    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
    A sealed receipt from a whole network, showing that a specific piece of data really is in its records.

    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
    A hash of everything ever sent on a channel, updated with each message so that nothing can be inserted, removed or reordered unnoticed.

    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
    The emergency brake. Can change the ledger's published limits, admit and evict endpoints, and close channels.

    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.

    RepositoryCommitCommittedUsed 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.