Cross-Chain HTLC
Atomic cross-chain payments between CKB (Fiber) and Bitcoin (Lightning) networks — architecture, mechanics, and developer guide
This document describes the Cross-Chain Hub (CCH) feature in Fiber, which enables atomic swaps between CKB (via Fiber) and Bitcoin (via Lightning Network). If you want to integrate cross-chain payments or run a CCH node, you are in the right place.
What Is CCH?
CCH (Cross-Chain Hub) is a service built into Fiber that bridges the CKB ecosystem and the Bitcoin Lightning Network using Hash Time-Locked Contracts (HTLCs). It lets users atomically swap wrapped BTC on CKB for native BTC on Lightning, and vice versa — without trusting a third party with custody.
The key guarantee is atomicity: either both legs of the swap settle, or neither does. The CCH operator (commonly called Ingrid in the literature) cannot steal funds because the same payment hash / preimage locks both sides.
Why It Matters
For developers building on Fiber, CCH unlocks several possibilities:
- Payment interoperability: A Fiber user can pay any Lightning invoice, and a Lightning user can pay any Fiber invoice.
- Liquidity bootstrapping: CCH operators can earn fees by providing swap liquidity between the two networks.
- No custodial risk: The swap is enforced by HTLCs on both chains, not by legal contracts.
Core Concepts
Shared Payment Hash
Both sides of the swap use the same SHA-256 payment hash. The incoming invoice is locked with this hash; the outgoing payment is also locked with the same hash. When the outgoing payment succeeds, the CCH learns the preimage and uses it to settle the incoming invoice.
Time-Lock Safety
The CCH must have enough time to claim the outgoing preimage and then settle the incoming invoice before the incoming time lock expires. This is enforced by two rules:
- Static check at order creation: The outgoing invoice's minimum final expiry must be less than half of the incoming invoice's final expiry.
- Dynamic check before sending outgoing payment: After the incoming payment is accepted, the CCH computes the remaining time on the incoming HTLC and caps the outgoing route expiry to at most half of that remaining time.
If there is insufficient time, the order is failed automatically.
Wrapped BTC
CCH swaps use a wrapped BTC UDT on CKB. The invoice generated by the CCH for CKB payments includes the udt_type_script of this wrapped BTC, ensuring the payer knows exactly which token to transfer. The CCH operator configures the wrapped BTC type script via:
wrapped_btc_type_script_args— the script args (used when Fiber runs in-process)wrapped_btc_type_script— the full JSON script (required in standalone mode)
Architecture
CCH is implemented as a set of cooperating actors inside the Fiber node. Here is the high-level architecture:
┌─────────────────┐ ┌─────────────────┐
│ CchActor │────▶│ CchFiberAgent │──▶ Fiber Network (in-process or RPC)
│ (main hub) │ │ (add/send/settle)
└────────┬────────┘ └─────────────────┘
│
│ manages
▼
┌─────────────────┐ ┌─────────────────┐
│ Scheduler │ │ ActionDispatcher│
│ (expiry/prune) │ │ (execute swaps) │
└─────────────────┘ └─────────────────┘
│ │
│ ▼
│ ┌─────────────────┐
│ │ LndTrackerActor │──▶ LND gRPC
│ │ (track invoices │ (Lightning)
│ │ & payments) │
│ └─────────────────┘
│
▼
┌─────────────────┐
│ OrderStore │──▶ Persistent DB
│ (CchOrderStore) │
└─────────────────┘Key Components
| Component | Role |
|---|---|
CchActor | Main actor. Handles RPC requests, manages order lifecycle, subscribes to store changes and tracking events. |
CchFiberAgentRef | Unified interface to Fiber. Can operate in-process (direct actor calls) or over HTTP RPC (standalone mode). |
LndTrackerActor | Tracks Lightning invoices and payments via LND gRPC. Emits CchTrackingEvents back to CchActor. |
Scheduler | Schedules order expiry and final-order pruning using a priority queue. |
ActionDispatcher | Dispatches concrete actions (TrackIncomingInvoice, SendOutgoingPayment, TrackOutgoingPayment, SettleIncomingInvoice) based on order state. |
CchOrderStateMachine | Pure state machine that transitions orders between statuses based on events. |
Order State Machine
An order progresses through these statuses:
Pending ──▶ IncomingAccepted ──▶ OutgoingInFlight ──▶ OutgoingSuccess ──▶ Success
│ │
└────────────────────┴─────────────────────────────────────▶ Failed| Status | Meaning |
|---|---|
Pending | Order created. Waiting for the incoming invoice to be paid / accepted. |
IncomingAccepted | Incoming HTLC has been accepted (e.g., LND hold invoice is Accepted, or Fiber invoice is Received). Ready to send outgoing payment. |
OutgoingInFlight | Outgoing payment is in flight. |
OutgoingSuccess | Outgoing payment settled. Preimage obtained. Ready to settle incoming invoice. |
Success | Both sides settled. Order complete. |
Failed | Something went wrong (expired, payment failed, invalid invoice, etc.). |
State transitions are validated by CchOrderStateMachine::allow_transition. Invalid transitions are rejected and logged.
Action Lifecycle
Actions are triggered by state changes:
- On order creation (
on_starting):TrackIncomingInvoiceis scheduled. - On
IncomingAccepted(on_entering):SendOutgoingPayment+TrackOutgoingPaymentare scheduled. - On
OutgoingInFlight(on_entering):TrackOutgoingPaymentis scheduled (ensures we keep watching). - On
OutgoingSuccess(on_entering):SettleIncomingInvoiceis scheduled. - On final states (
Success/Failed): No further actions. Scheduler queues a prune job (21 days later).
Actions are executed with exponential backoff retry (1 second base, capped at 10 minutes). Permanent errors fail the order immediately; transient errors are retried.
Two Swap Directions
Send BTC (CKB → Lightning)
Alice wants Bob (on Lightning) to receive BTC.
- Bob creates a Lightning invoice for, say, 10,000 sats.
- Alice calls
send_btcwith Bob'sbtc_pay_req. - CCH validates:
- The BTC invoice network matches the CKB network (mainnet ↔ mainnet, testnet ↔ testnet, regtest ↔ regtest).
- The BTC invoice has an amount.
- The BTC invoice's
min_final_cltv_expiry_deltais safe relative to CKB's final TLC expiry. - The invoice has not expired and has enough remaining expiry.
- CCH computes the fee:
fee = base_fee + amount * fee_rate / 1_000_000. - CCH creates a Fiber invoice for
amount + feesats, locked with the samepayment_hash, using the wrapped BTC type script. - Alice pays the Fiber invoice through the Fiber network.
- CCH detects the incoming payment (via store changes or LND tracking).
- CCH sends the outgoing Lightning payment to Bob's invoice.
- Upon success, CCH obtains the preimage and settles the Fiber invoice internally.
- Order reaches
Success.
Receive BTC (Lightning → CKB)
Alice (on Fiber) wants to receive BTC from Bob (on Lightning).
- Alice creates a Fiber invoice for, say, 10,000 wrapped-BTC-sats, using the wrapped BTC type script and
hash_algorithm = sha256. - Alice calls
receive_btcwith herfiber_pay_req. - CCH validates:
- The Fiber invoice currency matches the configured network.
- The Fiber invoice has an amount.
- The Fiber invoice's
final_tlc_minimum_expiry_deltais safe relative to BTC's final CLTV expiry. - The invoice's UDT type script matches the configured
wrapped_btc_type_script. - The hash algorithm is
sha256(required for LND compatibility).
- CCH computes the fee:
fee = base_fee + amount * fee_rate / 1_000_000. - CCH creates a Lightning hold invoice via LND for
(amount + fee) * 1000millisats, locked with the samepayment_hash. - Bob pays the Lightning invoice through the Lightning network.
- LND detects the incoming payment and emits an
Acceptedevent. - CCH sends the outgoing Fiber payment to Alice's invoice.
- Upon success, CCH obtains the preimage from the Fiber payment.
- CCH settles the Lightning hold invoice with the preimage.
- Order reaches
Success.
Running a CCH Node
Configuration
CCH is enabled by adding a [cch] section to your Fiber config file (or via CLI flags / env vars):
[cch]
lnd_rpc_url = "https://127.0.0.1:10009"
lnd_cert_path = "/path/to/tls.cert"
lnd_macaroon_path = "/path/to/admin.macaroon"
wrapped_btc_type_script_args = "0x..."
base_fee_sats = 0
fee_rate_per_million_sats = 1| Config Key | Default | Description |
|---|---|---|
lnd_rpc_url | https://127.0.0.1:10009 | LND gRPC endpoint. |
lnd_cert_path | — | Path to LND TLS certificate. Omit for well-known CAs. |
lnd_macaroon_path | — | Path to LND macaroon for authentication. |
wrapped_btc_type_script_args | — | Args for the wrapped BTC UDT type script. Must have 8 decimal places. |
wrapped_btc_type_script | — | Full JSON type script. Required in standalone mode. |
order_expiry_delta_seconds | 129600 (36h) | How long an order remains active before auto-failure. |
btc_final_tlc_expiry_delta_blocks | 360 (~60h) | Final hop CLTV expiry for outgoing Lightning invoices. |
ckb_final_tlc_expiry_delta_seconds | 216000 (60h) | Final hop TLC expiry for outgoing Fiber invoices. |
min_outgoing_invoice_expiry_delta_seconds | 21600 (6h) | Minimum acceptable expiry for outgoing invoices. |
base_fee_sats | 0 | Flat fee per swap. |
fee_rate_per_million_sats | 1 | Proportional fee rate (parts per million). |
fiber_rpc_url | — | When set, CCH runs as a standalone service talking to Fiber over HTTP/WebSocket. |
Standalone Mode
CCH can run as a separate process from the Fiber node. In this mode:
fiber_rpc_urlmust point to a running Fiber node's HTTP RPC endpoint.wrapped_btc_type_scriptmust be provided as full JSON (the contracts context is not initialized).- CCH subscribes to Fiber store changes via WebSocket (
subscribe_store_changes). - Reconnects automatically on WebSocket failure.
This is useful for scaling or isolating the swap service from the payment routing node.
In-Process Mode
When CCH and Fiber run in the same process (the default if both are configured):
- CCH talks to the
NetworkActordirectly via actor messages. - Store changes are propagated through an
OutputPort, so CCH reacts instantly to invoice and payment updates. - No HTTP/WebSocket overhead.
RPC API
CCH exposes three JSON-RPC methods. For detailed schema, see the CCH API Reference.
send_btc
Create a CCH order to pay a Lightning invoice from CKB.
{
"jsonrpc": "2.0",
"method": "send_btc",
"params": {
"btc_pay_req": "lnbc100u1p...",
"currency": "Fibb"
},
"id": 1
}receive_btc
Create a CCH order to receive BTC via a Fiber invoice.
{
"jsonrpc": "2.0",
"method": "receive_btc",
"params": {
"fiber_pay_req": "fibb1000..."
},
"id": 1
}get_cch_order
Poll an order by payment hash to monitor its status.
{
"jsonrpc": "2.0",
"method": "get_cch_order",
"params": {
"payment_hash": "0xabcd1234..."
},
"id": 1
}Security & Safety Mechanisms
Preimage Verification
When the outgoing payment succeeds, the state machine verifies that the returned preimage hashes to the original payment_hash using SHA-256. If there is a mismatch, the order is failed. This prevents a malicious network from tricking the CCH with an incorrect preimage.
Expiry Cascade
The CCH guarantees that it always has a time buffer to settle the incoming side after the outgoing side succeeds:
- Static validation at order creation ensures the outgoing invoice's final expiry is less than half of the incoming invoice's final expiry.
- Dynamic validation before sending the outgoing payment computes the actual remaining time on the incoming HTLC and caps the outgoing route expiry to half of that. If the remaining time is too short, the order is failed.
Permanent vs. Transient Errors
Actions distinguish between permanent and transient failures:
- Permanent (e.g.,
invalid payment request,invoice expired,no path found,payment hash mismatch): The order is failed immediately. - Transient (e.g., network timeout, LND temporary error): The action is retried with exponential backoff.
Order Pruning
Final orders (Success or Failed) are kept in the database for 21 days (configurable via PRUNE_DELAY_SECONDS in the scheduler) and then automatically deleted. This prevents unbounded database growth.
Error Reference
Common errors you may encounter when creating or processing orders:
| Error | When It Happens |
|---|---|
BTCInvoiceNetworkMismatch | The Lightning invoice is for a different BTC network than the CKB network maps to. |
CKBInvoiceNetworkMismatch | The Fiber invoice currency does not match the CCH node's configured network. |
BTCInvoiceFinalTlcExpiryDeltaTooLarge | The Lightning invoice's CLTV expiry leaves insufficient margin for the CKB side. |
CKBInvoiceFinalTlcExpiryDeltaTooLarge | The Fiber invoice's TLC expiry leaves insufficient margin for the BTC side. |
OutgoingInvoiceExpiryTooShort | The invoice expires sooner than min_outgoing_invoice_expiry_delta_seconds. |
WrappedBTCTypescriptMismatch | The Fiber invoice's UDT type script does not match the configured wrapped BTC. |
CKBInvoiceIncompatibleHashAlgorithm | The Fiber invoice uses a hash algorithm other than SHA-256. |
SendBTCOrderAmountTooLarge / ReceiveBTCOrderAmountTooLarge | Amount or fee calculation overflowed. |
SettledPaymentMissingPreimage | Outgoing payment reported success but no preimage was returned. |
PreimageHashMismatch | The returned preimage does not hash to the expected payment hash. |
Building on Top of CCH
If you are a developer integrating cross-chain swaps into your wallet or dApp, here are some practical patterns:
Polling Loop for Order Status
After creating an order, poll get_cch_order until the status is terminal:
loop {
let order = client.get_cch_order(payment_hash).await?;
match order.status {
CchOrderStatus::Success => break Ok(()),
CchOrderStatus::Failed => break Err(order.failure_reason.unwrap_or_default()),
_ => tokio::time::sleep(Duration::from_secs(5)).await,
}
}Fee Estimation
Before creating an order, you can estimate the fee locally:
let fee = base_fee + amount * fee_rate / 1_000_000;Both base_fee_sats and fee_rate_per_million_sats are exposed via the CCH operator's config. A well-designed UI should display the estimated fee to the user before they confirm the swap.
Wrapped BTC Discovery
The CCH response includes wrapped_btc_type_script. If you are building a wallet that pays CCH-generated Fiber invoices, verify that the invoice's udt_type_script matches a known wrapped BTC token. Do not blindly pay invoices with unknown type scripts.
Source Code Pointers
If you want to dive deeper into the implementation, here are the key files in the Fiber repository:
| File | What It Contains |
|---|---|
crates/fiber-lib/src/cch/mod.rs | Module exports and public types. |
crates/fiber-lib/src/cch/actor.rs | Main CchActor: message handling, send_btc, receive_btc, store-change mapping. |
crates/fiber-lib/src/cch/order/state_machine.rs | CchOrderStateMachine: validates state transitions. |
crates/fiber-lib/src/cch/actions/ | Action dispatchers and executors for each step of the swap. |
crates/fiber-lib/src/cch/trackers/lnd_trackers.rs | LndTrackerActor: LND invoice/payment tracking with concurrency limits. |
crates/fiber-lib/src/cch/scheduler.rs | Order expiry and pruning scheduler. |
crates/fiber-lib/src/cch/cch_fiber_agent.rs | CchFiberAgentRef: abstracts in-process vs. HTTP RPC backends. |
crates/fiber-lib/src/cch/config.rs | CchConfig and default constants. |
crates/fiber-lib/src/rpc/cch.rs | JSON-RPC server implementation. |
crates/fiber-types/src/cch.rs | Core data types: CchOrder, CchOrderStatus, CchInvoice. |
Future: PTLC
Fiber plans to migrate from HTLC to Point Time-Locked Contracts (PTLC), which offer:
- Improved privacy: No shared payment hash across hops (the hash is hidden via adaptor signatures).
- Reduced on-chain footprint: Smaller witness data when channels are force-closed.
- Enhanced security: Resistance to wormhole attacks in multi-hop routing.
PTLC will replace the SHA-256 hash lock with Schnorr adaptor signatures. Until then, CCH requires hash_algorithm = Sha256 for LND compatibility.
Related Topics
- Invoice Guide — How Fiber invoices work
- Hold Invoice — Deferred settlement for conditional payments
- CCH API Reference — Complete RPC schema