Fiber LogoFiber Docs

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:

  1. Static check at order creation: The outgoing invoice's minimum final expiry must be less than half of the incoming invoice's final expiry.
  2. 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

ComponentRole
CchActorMain actor. Handles RPC requests, manages order lifecycle, subscribes to store changes and tracking events.
CchFiberAgentRefUnified interface to Fiber. Can operate in-process (direct actor calls) or over HTTP RPC (standalone mode).
LndTrackerActorTracks Lightning invoices and payments via LND gRPC. Emits CchTrackingEvents back to CchActor.
SchedulerSchedules order expiry and final-order pruning using a priority queue.
ActionDispatcherDispatches concrete actions (TrackIncomingInvoice, SendOutgoingPayment, TrackOutgoingPayment, SettleIncomingInvoice) based on order state.
CchOrderStateMachinePure 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
StatusMeaning
PendingOrder created. Waiting for the incoming invoice to be paid / accepted.
IncomingAcceptedIncoming HTLC has been accepted (e.g., LND hold invoice is Accepted, or Fiber invoice is Received). Ready to send outgoing payment.
OutgoingInFlightOutgoing payment is in flight.
OutgoingSuccessOutgoing payment settled. Preimage obtained. Ready to settle incoming invoice.
SuccessBoth sides settled. Order complete.
FailedSomething 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:

  1. On order creation (on_starting): TrackIncomingInvoice is scheduled.
  2. On IncomingAccepted (on_entering): SendOutgoingPayment + TrackOutgoingPayment are scheduled.
  3. On OutgoingInFlight (on_entering): TrackOutgoingPayment is scheduled (ensures we keep watching).
  4. On OutgoingSuccess (on_entering): SettleIncomingInvoice is scheduled.
  5. 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.

  1. Bob creates a Lightning invoice for, say, 10,000 sats.
  2. Alice calls send_btc with Bob's btc_pay_req.
  3. 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_delta is safe relative to CKB's final TLC expiry.
    • The invoice has not expired and has enough remaining expiry.
  4. CCH computes the fee: fee = base_fee + amount * fee_rate / 1_000_000.
  5. CCH creates a Fiber invoice for amount + fee sats, locked with the same payment_hash, using the wrapped BTC type script.
  6. Alice pays the Fiber invoice through the Fiber network.
  7. CCH detects the incoming payment (via store changes or LND tracking).
  8. CCH sends the outgoing Lightning payment to Bob's invoice.
  9. Upon success, CCH obtains the preimage and settles the Fiber invoice internally.
  10. Order reaches Success.

Receive BTC (Lightning → CKB)

Alice (on Fiber) wants to receive BTC from Bob (on Lightning).

  1. Alice creates a Fiber invoice for, say, 10,000 wrapped-BTC-sats, using the wrapped BTC type script and hash_algorithm = sha256.
  2. Alice calls receive_btc with her fiber_pay_req.
  3. CCH validates:
    • The Fiber invoice currency matches the configured network.
    • The Fiber invoice has an amount.
    • The Fiber invoice's final_tlc_minimum_expiry_delta is 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).
  4. CCH computes the fee: fee = base_fee + amount * fee_rate / 1_000_000.
  5. CCH creates a Lightning hold invoice via LND for (amount + fee) * 1000 millisats, locked with the same payment_hash.
  6. Bob pays the Lightning invoice through the Lightning network.
  7. LND detects the incoming payment and emits an Accepted event.
  8. CCH sends the outgoing Fiber payment to Alice's invoice.
  9. Upon success, CCH obtains the preimage from the Fiber payment.
  10. CCH settles the Lightning hold invoice with the preimage.
  11. 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 KeyDefaultDescription
lnd_rpc_urlhttps://127.0.0.1:10009LND gRPC endpoint.
lnd_cert_pathPath to LND TLS certificate. Omit for well-known CAs.
lnd_macaroon_pathPath to LND macaroon for authentication.
wrapped_btc_type_script_argsArgs for the wrapped BTC UDT type script. Must have 8 decimal places.
wrapped_btc_type_scriptFull JSON type script. Required in standalone mode.
order_expiry_delta_seconds129600 (36h)How long an order remains active before auto-failure.
btc_final_tlc_expiry_delta_blocks360 (~60h)Final hop CLTV expiry for outgoing Lightning invoices.
ckb_final_tlc_expiry_delta_seconds216000 (60h)Final hop TLC expiry for outgoing Fiber invoices.
min_outgoing_invoice_expiry_delta_seconds21600 (6h)Minimum acceptable expiry for outgoing invoices.
base_fee_sats0Flat fee per swap.
fee_rate_per_million_sats1Proportional fee rate (parts per million).
fiber_rpc_urlWhen 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_url must point to a running Fiber node's HTTP RPC endpoint.
  • wrapped_btc_type_script must 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 NetworkActor directly 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:

ErrorWhen It Happens
BTCInvoiceNetworkMismatchThe Lightning invoice is for a different BTC network than the CKB network maps to.
CKBInvoiceNetworkMismatchThe Fiber invoice currency does not match the CCH node's configured network.
BTCInvoiceFinalTlcExpiryDeltaTooLargeThe Lightning invoice's CLTV expiry leaves insufficient margin for the CKB side.
CKBInvoiceFinalTlcExpiryDeltaTooLargeThe Fiber invoice's TLC expiry leaves insufficient margin for the BTC side.
OutgoingInvoiceExpiryTooShortThe invoice expires sooner than min_outgoing_invoice_expiry_delta_seconds.
WrappedBTCTypescriptMismatchThe Fiber invoice's UDT type script does not match the configured wrapped BTC.
CKBInvoiceIncompatibleHashAlgorithmThe Fiber invoice uses a hash algorithm other than SHA-256.
SendBTCOrderAmountTooLarge / ReceiveBTCOrderAmountTooLargeAmount or fee calculation overflowed.
SettledPaymentMissingPreimageOutgoing payment reported success but no preimage was returned.
PreimageHashMismatchThe 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:

FileWhat It Contains
crates/fiber-lib/src/cch/mod.rsModule exports and public types.
crates/fiber-lib/src/cch/actor.rsMain CchActor: message handling, send_btc, receive_btc, store-change mapping.
crates/fiber-lib/src/cch/order/state_machine.rsCchOrderStateMachine: 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.rsLndTrackerActor: LND invoice/payment tracking with concurrency limits.
crates/fiber-lib/src/cch/scheduler.rsOrder expiry and pruning scheduler.
crates/fiber-lib/src/cch/cch_fiber_agent.rsCchFiberAgentRef: abstracts in-process vs. HTTP RPC backends.
crates/fiber-lib/src/cch/config.rsCchConfig and default constants.
crates/fiber-lib/src/rpc/cch.rsJSON-RPC server implementation.
crates/fiber-types/src/cch.rsCore 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.

Fiber AI Assistant

Ask me anything

I can answer questions about Fiber Network using our documentation.

AI answers are based on Fiber documentation