Skip to content

Instantly share code, notes, and snippets.

@quake
Last active July 20, 2026 10:08
Show Gist options
  • Select an option

  • Save quake/294c085a0f71e5d09f84fef34131f151 to your computer and use it in GitHub Desktop.

Select an option

Save quake/294c085a0f71e5d09f84fef34131f151 to your computer and use it in GitHub Desktop.
Fiber Integration Approaches for Bitcoin Light — comparing three architectural approaches for integrating Fiber Network (CKB Lightning) into a React Native mobile wallet

Fiber Integration Approaches for Bitcoin Light

Context

Bitcoin Light currently integrates Breez SDK Spark for Bitcoin Lightning Network. Breez operates on a hosted LSP model: a cloud service runs the LN node (via Greenlight), the mobile device holds only keys and signs locally via defaultExternalSigner. Channel management, liquidity, routing, and watchtower are all handled by Breez.

Fiber is a self-hosted Lightning Network implementation built on Nervos CKB. It can run as a native binary (fnn), as a native library compiled into mobile apps (via FFI), in WASM via @nervosnetwork/fiber-js, or as a standalone RPC service. Unlike Breez, Fiber has no hosted LSP infrastructure — no cloud node management, no delegated signing, no liquidity-as-a-service.

We need to decide how to integrate Fiber into the Bitcoin Light mobile wallet. Three approaches are presented below.


Background: Why Breez "Offline Receive" Works (and Why LN Doesn't)

Breez SDK Spark Is NOT Traditional Lightning Network

Despite the API naming (LightningInvoice, LightningPayment), Breez SDK Spark uses the Spark protocol (Statechains), not Lightning Network's HTLC/commitment-transaction model.

At its core, Spark is a shared signing protocol on Bitcoin with a distributed ledger model. There are no payment channels, no commitment transactions, and no HTLCs in the traditional LN sense.

How Spark Enables True Offline Receive

Spark uses FROST threshold signatures with a Spark Entity (SE) — a set of operators holding threshold key shares:

Aggregated public key:  PubKey_Agg = PubKey_User + PubKey_SE

Transferring ownership of a "leaf" (Spark's equivalent of a UTXO):
  PubKey_SE' = PubKey_SE + (sk_Sender - sk_Receiver) * G
  = PubKey_SE + PubKey_Sender - PubKey_Receiver

The key insight: the SE can update state using only public key arithmetic. No private keys are needed from the sender or receiver. The user's private key is required only for:

  1. Sending payments (authorizing a transfer out)
  2. Unilateral exit (broadcasting a pre-signed exit transaction to L1)

For receiving, the user does not need to sign anything. The SE adjusts its key share, and the new state is valid. When the receiver's device comes online, the SDK syncs and emits a PaymentSucceeded event.

This is fundamentally impossible in HTLC-based Lightning Network.

The LN Constraint: Commitment Signing

In traditional Lightning Network (including Fiber's FNP protocol), every channel state update requires both parties to sign a new commitment transaction:

Sender ──→ add_htlc ──→ Peer ──→ Recipient
                                     │
                          commit_signed ← MUST sign here
                                     │
                          revoke_and_ack ← MUST sign here

The commitment_signed message carries the recipient's signature over the new commitment transaction. Without this signature, the HTLC cannot be added to the channel. This is a protocol-level constraint, not an implementation detail.

BOLT12 Doesn't Help Here

BOLT12 (offers + onion messages + blinded paths) solves a different problem:

Problem BOLT11 BOLT12
Invoice is single-use and expires Per-payment bolt11 string Offer: static, long-lived payment identifier
Must know recipient's node_id Routing hints in invoice Onion messages: async communication through the network
Privacy Invoice exposes recipient identity Blinded paths: recipient hidden behind blinded node IDs

BOLT12 makes invoice distribution asynchronous — a sender can request an invoice via onion message, and the recipient replies when online. But this does not change the commitment signing requirement. The commitment_signed round must still happen with the recipient online.

BOLT12 + an always-online LSP node = a complete solution: BOLT12 handles the "who to pay" problem, the LSP handles the "stay online to sign" problem. These two are complementary, not competing.

Three Models of "Offline Receive"

Model Protocol Mechanism User Offline OK?
Statechains (Breez Spark) Spark / FROST SE tweaks key share via public key arithmetic Yes
LSP runs your node (Greenlight, Voltage, Zeus) Traditional LN LSP server is always online, signs on your behalf Yes (node is online; user is offline)
Self-hosted (Umbrel, Phoenix, Approach 1) Traditional LN Your node must be online to sign commitments No

Approach 1: Mobile Full Node + Delegated Watchtower

Compile fiber-lib as a native Rust library for the mobile app (via FFI), using SQLite for local storage. Only the watchtower is outsourced to a trusted third party.

How It Works

┌────────────────────────────────────┐       ┌─────────────────────────────┐
│ Mobile Device                      │       │ Third-Party Watchtower      │
│                                    │       │                             │
│ ┌────────────────────────────────┐ │       │  Monitors CKB chain for     │
│ │ fiber-lib (Rust native, cross- │ │ RPC   │  channel cheating           │
│ │   compiled for ARM64/x86_64)   │ │──────→│  Signs settlement tx when   │
│ │                                │ │       │  needed (holds per-channel  │
│ │ ┌────────────────────────────┐ │ │       │  settlement private key)    │
│ │ │ Tokio runtime              │ │ │       │                             │
│ │ │ - P2P networking (TCP/WS)  │ │ │       └─────────────────────────────┘
│ │ │ - Channel management       │ │ │
│ │ │ - Gossip + routing         │ │ │
│ │ │ - Payment engine           │ │ │
│ │ │ - Invoice management       │ │ │
│ │ │ - Signing (keys on-device) │ │ │
│ │ └────────────────────────────┘ │ │
│ │            ↕ SQLite            │ │
│ │ ┌────────────────────────────┐ │ │
│ │ │ App sandbox storage        │ │ │
│ │ │ (data.sqlite)              │ │ │
│ │ └────────────────────────────┘ │ │
│ └──────┬─────────────────────────┘ │
│        │ FFI (JSI / C++ bridge)    │
│ ┌──────▼─────────────────────────┐ │
│ │ React Native / TypeScript      │ │
│ │ @shared/blockchain (WCI)       │ │
│ └────────────────────────────────┘ │
└────────────────────────────────────┘

Implementation

  • Build: Compile fiber-lib with --features sqlite for Android (aarch64, armv7, x86_64) and iOS (aarch64)
  • FFI: Expose a C-compatible API from fiber-lib, bridge into React Native via JSI or a Turbo Native Module
  • Storage: SQLite via rusqlite (bundled), stored in the app's sandboxed document directory
  • WCI Integration: Wire the native fiber node as a NervosProviders entry with ProviderType.LIGHTNING
  • Config: standalone_watchtower_rpc_url → trusted third-party watchtower; disable_built_in_watchtower: true
  • All signing happens locally — the wallet mnemonic derives keys on-device

Why Rust Native Instead of fiber-js (WASM)

fiber-js (WASM) fiber-lib (Native + SQLite)
Runtime Web Worker + SharedArrayBuffer Native Tokio runtime on OS threads
Storage IndexedDB (browser API overhead) SQLite via rusqlite (direct file I/O)
Networking Only WebSocket outbound (browser limits) Full TCP/WS, could use native socket APIs
Performance WASM interpreter overhead Native code (ARM64/x86_64)
Concurrency Single-threaded WASM Multi-threaded Tokio (real async)
Build complexity Simple (npm package) Cross-compilation toolchain needed
OS integration Limited (Web Worker lifecycle) Can integrate with OS background tasks

What Exists Today

  • SQLite backend (fiber-store with rusqlite + bundled feature) — Store::open_db(path) creates data.sqlite
  • standalone_watchtower_rpc_url — first-class config; node forwards channel events to remote watchtower automatically
  • ReestablishChannel protocol — channel state recovery after disconnection (sub-second in the happy path)
  • Gossip pruning — 4-week hard limit, daily cleanup

What Needs Building

  • FFI boundary layer (C API exports from fiber-lib for init, RPC, events)
  • React Native native module wrapping the FFI
  • Build pipeline for cross-compiling fiber-lib to Android (NDK) and iOS (Xcode)
  • Lifecycle management — start/stop the Tokio runtime with app foreground/background

Pros

  • No Fiber core protocol changes needed — runs the same node code
  • Channel signing keys stay on device (non-custodial)
  • Native performance — real threads, direct file I/O, no WASM overhead
  • SQLite is battle-tested on mobile (used by Signal, WhatsApp, etc.)
  • Zero operational cost beyond the watchtower service
  • Decentralized — the user runs their own node

Cons

  • Watchtower holds local_settlement_key (a per-channel private key). This is a trust requirement — the watchtower can sign settlement transactions and could collude with a channel counterparty.
  • Cross-compilation complexity — needs Rust NDK toolchains for Android, cargo-lipo / cargo-xcode for iOS
  • FFI maintenance — the C API surface must be kept stable across fiber version updates
  • Full node on mobile — P2P networking, gossip subscription, and routing graph run on device
  • Not 24/7 — when the app is backgrounded or offline:
    • Cannot send or receive payments
    • HTLCs may expire; the watchtower must act (funds at risk if watchtower fails)
  • Resource usage — TCP/WS connections, SQLite WAL growth, Tokio thread pool memory
  • No push notifications — the node must be running to receive payments

Key Risks

Risk Mitigation
Watchtower misbehavior (holds settlement key) Use a reputable third-party; settlement key is per-channel, not the wallet master key
HTLC expiry while offline Watchtower must monitor and act reliably
App killed by OS Channel partners may force-close; watchtower must handle
SQLite WAL growth Built-in gossip pruning (4 weeks); periodic VACUUM
Cross-compilation breakage Pin Rust toolchain; CI builds for all targets
FFI API drift across fiber versions Versioned FFI contract; integration tests

Approach 2: Hosted LSP (Breez-like Model)

A server runs the Fiber node. The mobile device only holds keys and signs operations remotely.

How It Works

┌──────────────────────────────────┐       ┌──────────────────────────────┐
│ Mobile Device                    │       │ LSP Server                   │
│                                  │       │                              │
│ ┌──────────────────────────────┐ │       │ ┌──────────────────────────┐ │
│ │ Light Client SDK             │ │       │ │ Fiber Node (fnn)         │ │
│ │ - Key derivation (mnemonic)  │ │ Sign  │ │ - Channel management     │ │
│ │ - Remote signing callbacks   │ │──────→│ │ - P2P networking         │ │
│ │ - Balance & payment queries  │ │       │ │ - Payment routing        │ │
│ │ - No P2P, no gossip          │ │       │ │ - Gossip                 │ │
│ └──────────────────────────────┘ │       │ │ - Watchtower             │ │
│                                  │ ←─────│ │ - CKB RPC                │ │
│  Only signs; never runs a node  │ State │ └──────────────────────────┘ │
│                                  │       │                              │
└──────────────────────────────────┘       │ ┌──────────────────────────┐ │
                                           │ │ Remote Signer Module     │ │
                                           │ │ (calls back to client    │ │
                                           │ │  for every signature)    │ │
                                           │ └──────────────────────────┘ │
                                           └──────────────────────────────┘

Implementation

The critical missing piece is a signer delegation mechanism. Today, Fiber signs everything internally via InMemorySigner, a concrete struct embedded directly in ChannelActorState:

// fiber-types/src/channel.rs — current state
pub struct InMemorySigner {
    pub funding_key: Privkey,
    pub tlc_base_key: Privkey,
    pub musig2_base_nonce: Privkey,
    pub commitment_seed: [u8; 32],
}

No trait. No abstraction. No way to delegate signing.

Required Fiber changes:

  1. ChannelSigner trait — extract a trait from InMemorySigner:

    • sign_commitment_tx(...)
    • generate_musig2_nonce(...)
    • derive_commitment_point(number)
    • sign_revocable_output(...)
  2. RemoteChannelSigner — implement the trait with a callback transport (REST, WebSocket, gRPC)

  3. Refactor ChannelActorState — replace signer: InMemorySigner with signer: Box<dyn ChannelSigner>

  4. Remote signing protocol — define wire format for sign requests/responses, timeout/reconnect logic

  5. Mobile light client SDK — wraps the remote signing callback, handles key derivation from mnemonic

Required LSP infrastructure:

Component Status Effort
Fiber node hosting (multi-tenant) New Medium
Client identity & API key management New Medium
Channel lifecycle management New Medium
Liquidity provisioning New High
Watchtower (built-in to fiber node) Exists Low
CKB RPC node Needs operations Medium
Monitoring, alerting, SLAs New High
Billing / usage metering New Medium

Pros

  • Best mobile UX — near-zero resource usage, instant on, no sync delay
  • 24/7 availability — node runs on servers
  • Push notifications possible — server can notify client of incoming payments
  • True non-custodial — keys never touch the server
  • No watchtower trust issue — the LSP is already trusted to operate the node

Cons

  • Requires significant Fiber core changes (Signer trait + remote signing protocol)
  • Requires building and operating LSP infrastructure (servers, CKB nodes, liquidity, monitoring)
  • Channel commitment signing is on the critical path — latency matters; remote signing must be fast
  • Musig2 nonce generation must happen client-side before each commitment round
  • If the LSP goes down, all clients lose access — single point of failure for availability (not custody)
  • Estimated timeline: 12–20 weeks of development

Key Risks

Risk Mitigation
Signer trait scope creep (many signing operations) Start with sign_commitment_tx only; iterate
Remote signing latency Use WebSocket persistent connection; batch where possible
LSP operational cost Charge service fees per channel/payment
Multi-tenancy complexity Start single-tenant; add multi-tenancy incrementally

Approach 3: Hybrid — Remote Full Node + Mobile RPC Client

A server runs a full Fiber node. The mobile app connects to it via JSON-RPC (similar to CCH standalone mode).

How It Works

┌──────────────────────────────────┐       ┌──────────────────────────────┐
│ Mobile Device                    │       │ Remote Fiber Node            │
│                                  │       │                              │
│ ┌──────────────────────────────┐ │       │ ┌──────────────────────────┐ │
│ │ RPC Client (light)           │ │ RPC   │ │ Fiber Node (fnn)         │ │
│ │ - open_channel (external     │ │──────→│ │ - Full P2P stack         │ │
│ │   funding: user's wallet     │ │       │ │ - Channel management     │ │
│ │   signs funding tx)          │ │       │ │ - Payment engine         │ │
│ │ - new_invoice / send_payment │ │       │ │ - Gossip + routing       │ │
│ │ - list_channels / list_      │ │       │ │ - Watchtower             │ │
│ │   payments                   │ │       │ └──────────────────────────┘ │
│ │ - No P2P, no gossip          │ │       │                              │
│ └──────────────────────────────┘ │       │  Holds all keys.             │
│                                  │       │  Fully custodial.             │
│  Holds CKB wallet for funding   │       │                              │
│  transactions only              │       │                              │
└──────────────────────────────────┘       └──────────────────────────────┘

Implementation

This is essentially a custodial wallet over RPC with an important twist: open_channel_with_external_funding allows the user to fund channels from their own CKB wallet without sending funds to the node. This provides some non-custodial properties for channel funding, but channel signing keys are still on the node.

Required Fiber changes:

The CCH already has a "standalone" mode where it connects to a remote Fiber node via fiber_rpc_url without running the Fiber P2P service locally. This pattern could be extended to general wallet operations.

// Existing pattern in fiber-bin/src/main.rs
if fiber_service.is_none() {
    // CCH standalone: connect to remote fiber node via RPC
    let fiber_rpc_client = RpcClient::new(config.cch.fiber_rpc_url);
}

Currently this pattern is CCH-only — it doesn't extend to general channel/payment/invoice operations. To make it general-purpose:

  1. Extract the "standalone" pattern into a reusable RPC client mode
  2. Ensure all wallet-relevant RPC methods are accessible via the remote client
  3. Build a mobile RPC client SDK that wraps the JSON-RPC API

What exists without changes:

  • The open_channel_with_external_funding + submit_signed_funding_tx flow — user signs funding tx with their own CKB wallet
  • All RPC methods for invoices, payments, channels, and graph queries
  • Biscuit token auth for RPC

Pros

  • Medium effort — most RPC infrastructure exists; mainly client SDK work
  • Good mobile UX — no P2P, no gossip, no sync
  • 24/7 availability — node runs on server
  • Users can provide their own channel funding via external funding flow
  • Leverages existing fiber_rpc_url pattern from CCH

Cons

  • Custodial — the node holds channel signing keys. Users trust the node operator.
  • No key sovereignty — if the remote node is compromised, channel funds can be stolen
  • Single point of failure — node operator can censor payments
  • Shared node resource contention — multiple users sharing one fiber node
  • Not truly "integrating" Fiber — more like providing a hosted wallet interface

Key Risks

Risk Mitigation
Custodial trust Use reputable operator; external funding reduces exposure
Censorship risk Users can run their own node as fallback
Multi-user isolation One fiber node per user (higher cost), or expect trust
External funding UX Two-step flow adds complexity; needs careful mobile UX

Comparison Matrix

Dimension Approach 1: Mobile Full Node (Native) Approach 2: Hosted LSP (Breez-like) Approach 3: Hybrid RPC
Fiber changes needed FFI layer only (no protocol changes) Large (Signer trait, remote signing protocol) Small (extend CCH standalone pattern)
Custodial risk Non-custodial (keys on device) Non-custodial (keys on device) Custodial (keys on server)
Mobile resource usage Medium (native perf, SQLite disk I/O) Minimal (only signing) Low (RPC calls only)
24/7 availability No (watchtower covers security only) Yes Yes
Offline payment risk HTLC expiry → watchtower must act Server handles everything Server handles everything
Infrastructure cost Watchtower service only Full LSP operation Fiber node(s) + CKB RPC
Development effort ~6–10 weeks (FFI + build pipeline) ~12–20 weeks + ongoing ops ~4–8 weeks
Scalability (many users) Each user runs own node (decentralized) LSP scaling challenge (centralized) One node per user or shared
Push notifications Not possible without background service Yes Yes
Graph privacy Full — all gossip visible locally Opaque — user trusts LSP Opaque — user trusts node
Fiber version upgrades User-managed (app update) Server-managed (transparent) Server-managed (transparent)
Storage backend SQLite (rusqlite, bundled) RocksDB (server-side) RocksDB (server-side)

Solving Offline Receive for Fiber (Protocol Level)

The three integration approaches above handle who runs the node. But at the protocol level, enabling offline receive in HTLC-based LN requires additional mechanisms. Three options exist, each with different trade-offs.

A: Shared Keys — Server Signs on Behalf of User

The server holds the user's channel signing keys. When the user is offline, the server signs commitment_signed on their behalf.

Protocol changes needed:

  • ChannelSigner trait (extract from InMemorySigner)
  • RemoteChannelSigner / DelegatedChannelSigner implementation
  • ChannelActorState refactored to hold Box<dyn ChannelSigner>

Security: The server has full signing authority for user↔LSP channels. It can both send and receive on the user's behalf. This requires trust in the LSP operator. The per-channel key model limits blast radius.

Maps to: Approach 2 (Hosted LSP). This is the closest equivalent to the Greenlight model.

Estimated effort: 6–8 weeks of Fiber core changes.

B: LSP Payment Buffer — No Key Sharing

The user↔LSP channel operates normally (user signs when online). When the user is offline:

  1. LSP receives payment into its own balance (not the user's channel)
  2. LSP holds the funds until the user comes online
  3. User signs a new commitment_signed to receive funds through the user↔LSP channel

Protocol changes needed: None. Pure application-layer logic.

Fiber's existing hold invoice (settle_invoice) can serve as the buffer mechanism: LSP creates a hold invoice → receives payment → waits for user → user reveals preimage → LSP settles.

Security: Keys never leave the device. However, funds sit in the LSP's balance during the offline window. The UX shows "pending confirmation" until the user comes online.

Maps to: Approach 3 (Hybrid RPC). Can be layered on top without Fiber changes.

Estimated effort: 1–2 weeks of application code.

C: Receive-Only Sub-Key — Protocol-Level Delegation (Ideal)

The user delegates a receive-only sub-key to the LSP. This key can sign commitment_signed only for operations that add incoming HTLCs. It cannot sign outgoing HTLCs, channel closure, or fund withdrawal.

Protocol changes needed (substantial):

  1. Split InMemorySigner into per-operation sub-signers
  2. Define a FNP handshake extension (receive_agent announcement during ChannelReady)
  3. Add verification logic: counterparty validates that incoming HTLC signatures come from the receive-only sub-key
  4. Adapt revocation logic for receive-only key rotation
  5. New mobile SDK API: delegateReceive(agentPubkey, expiry)

Security: Cryptographically enforced — the LSP literally cannot steal funds or send payments. The receive-only key simply cannot produce valid signatures for those operations. This is the highest-security model.

Estimated effort: 20–30 weeks. Requires new FNP specification, protocol negotiation, cross-implementation compatibility testing.

Protocol-Level Comparison

A: Shared Keys B: LSP Buffer C: Receive-Only Sub-Key
Fiber changes Signer trait (6–8 weeks) Zero Protocol extension (20–30 weeks)
True offline receive Yes (server signs) No (settles on reconnect) Yes (agent signs)
Server can send? Yes (full authority) No No (cryptographically prevented)
Server can steal? Potentially (trust-based) No (funds not in channel) No (key limited)
UX on reconnect Already settled "Pending" → confirmed Already settled
Best paired with Approach 2 Approach 3 Approach 2 (replaces shared keys)

Recommendation

Start with Approach 3 + Protocol B (Hybrid RPC + LSP Buffer) as the immediate path, then evolve toward Approach 2 + Protocol A (Hosted LSP + Shared Keys), with Protocol C (Receive-Only Sub-Key) as the long-term ideal.

Phase 1 (Now): Hybrid RPC + LSP Buffer

  • Deploy a managed Fiber node (one per user or shared)
  • Build mobile RPC client SDK wrapping fiber JSON-RPC
  • Support open_channel_with_external_funding for non-custodial channel funding
  • Layer Protocol B on top: LSP buffers incoming payments, user settles on reconnect
  • Accept custodial channel signing as a known trade-off
  • Validate product-market fit for Fiber on mobile
  • Zero Fiber code changes required

Phase 2 (Next): Non-Custodial LSP with Shared Keys

  • Implement ChannelSigner trait in Fiber core (Protocol A)
  • Build remote signing transport between mobile SDK and LSP
  • Mobile device signs commitment transactions; LSP server does everything else
  • Enables true offline receive — server signs when user is offline
  • Transitions from Approach 3 → Approach 2 incrementally

Phase 3 (Eventually): Receive-Only Sub-Key (Protocol C)

  • Extend FNP with receive_agent handshake and receive-only sub-key capabilities
  • Cryptographically constrain the LSP to receiving only
  • Achieves true non-custodial offline receive without trust
  • Also build the native mobile full node (Approach 1) for power users
  • Three tiers: hosted buffer (Phase 1) → non-custodial hosted (Phase 2) → self-sovereign + protocol-secured (Phase 3)

Open Questions for Discussion

  1. Who operates the infrastructure? (watchtower, fiber nodes, CKB RPC) — in-house or third-party?
  2. Target chain/asset scope? CKB only, or UDT/stablecoins from day one?
  3. Cross-chain with Bitcoin LN? Fiber's CCH enables BTC ↔ CKB swaps — should this be in scope?
  4. WCI interface changes? Current ILightningProvider is single-token (BTC). Fiber needs multi-asset.
  5. Regulatory considerations? Hosted/custodial models may have different compliance requirements.
  6. Funding liquidity? Who provides the CKB to open channels in Phase 1?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment