Keelchain
Docs / Lightning

Bitcoin Lightning (decided 2026-09-10)

"Lightning is cheap for BTC and fast, need to support it … make any parameters customizable from backoffice." This document describes the design, the parameters, the operator setup, and what is verified.

1. Model: observer-run Lightning pools

Lightning payments need a hot node with channels. The chain cannot run one, so each observer may run an LND node on behalf of the chain and register it (RegisterLightningNode { node_id }). The chain keeps one pool per observer: BTC the chain has lent that node (funded from the BTC vault by a normal outbound owned by SYSTEM, FundLightningPool), the amount pending in outgoing payments, and what it credited today. The sum of all pools is a reserve account lightning_pool (debit-normal, under the BTC vault's liabilities) so reserves-vs-liabilities still balance.

Everything above is in chain/crates/keel-vm/src/modules/lightning.rs (crates/keel-vm/tests/lightning.rs covers each rule) and the vault hooks in modules/vaults.rs. The node serves GET /v1/lightning (params, pools, open assignments with invoice/amount/owner, pending sweeps, pool_total).

2. Parameters (Backoffice → Chain → Lightning)

All are chain params, changed with SetParam by the param admin, i.e. from the backoffice Chain page like every other parameter:

Key Default Meaning
lightning_enabled 1 Off: invoices refused (deposits and payouts); on-chain BTC still works
lightning_pool_cap_sats 50,000,000 (0.5 BTC) Most BTC one observer's node may hold for the chain; funding above it is refused
lightning_max_deposit_sats 10,000,000 Largest single Lightning deposit
lightning_max_withdraw_sats 10,000,000 Largest single Lightning payout
lightning_max_fee_bps 50 (0.5%) Routing fee allowance in bps of the amount
lightning_min_fee_sats 10 Allowance floor for tiny payouts
lightning_payout_timeout_blocks 600 Blocks an observer has to pay before the chain refunds
lightning_daily_cap_sats 0 (none) Total deposits one observer may credit per day

The marketplace's own fee schedule (flat platform fee per withdrawal, Fees page) applies on top exactly as for on-chain withdrawals. Note: that flat fee is USD-pegged and dominates small Lightning withdrawals; if the goal is "cheap", give Lightning its own tier on the Fees page.

3. Operator setup (observer)

  1. Run lnd (0.19.x) with a funded wallet and channels to well-connected peers. Keep admin.macaroon readable by the observer.

  2. Add to the observer TOML:

    [lightning]
    rest_url = "https://127.0.0.1:8080"
    macaroon_path = "/path/to/admin.macaroon"   # or macaroon_hex
    tls_cert_path = "/path/to/tls.cert"         # or tls_insecure = true on regtest
    api_listen = "127.0.0.1:7201"               # invoice API for the marketplace
    invoice_expiry_secs = 3600
    poll_secs = 2

    On start the observer registers its node id on chain once, then loops: settled invoices bound to an owner → ObserveLightningDeposit; payouts assigned to it → pays and reports. GET /v1/lightning/info on the API answers node id, observer address, channel balance, invoice expiry.

  3. Fund the pool from the vault (needs the observer key): keel send --secret <observer secret> lightning fund <sats> <node on-chain address>. The vault pays a normal batched outbound to the node's wallet; when the observer sees that deposit it credits the pool (LightningPoolFunded).

  4. Sweep excess: keel send … lightning sweep <tx_hash> <sats> before broadcasting the on-chain transaction to the vault's index-0 address.

infra/dev/lightning-e2e.sh stands up two regtest LND nodes with a channel, registers observer 0, funds its pool with 0.03 BTC, deposits 25,000 sats by invoice and withdraws 12,000 sats to an external invoice.

4. Marketplace and apps

5. Verified (2026-09-10, local testnet)

lightning-e2e.sh PASS; through the marketplace API as alice: a 30,000 sat invoice paid from the second LND node was credited in about 8 s and shows as a Lightning deposit with its chain tx id; a 15,000 sat withdrawal to an LND invoice was paid by observer 0 and marked settled 4 s after the request. Test counts: Rust 178 (indexer 20), marketplace 2368, web 699, backoffice 158, explorer 16.

6. Limits and next steps