HTLCs (Hash Time-Locked Contracts)
The atomicity primitive that makes Lightning routing work. An HTLC
is "claimable with preimage r such that hash(r) = H, OR refundable
after CLTV expires".
Lifecycle
Sender → first hop
Alice → Bob: update_add_htlc(amount, hash, cltv, onion)
The onion contains routing data for Bob to forward.
Forward chain
Bob receives, peels onion, sees instructions to forward to Carol:
Bob → Carol: update_add_htlc(amount - bob_fee, hash, cltv - bob_delta, onion')
Continues until destination Dave (final hop, onion is empty payload).
Settlement (cooperative)
Dave reveals preimage r:
Dave → Carol: update_fulfill_htlc(channel_id, htlc_id, r)
Carol → Bob: update_fulfill_htlc(...) (verifies hash(r) = H, propagates)
Bob → Alice: update_fulfill_htlc(...)
Each hop earns its fee (held in the HTLC amount).
Settlement (failed)
Carol → Bob: update_fail_htlc(channel_id, htlc_id, encrypted_failure)
Bob → Alice: update_fail_htlc(...)
Failure encryption uses ChaCha20 with onion shared secrets, hop-by-hop (only Alice can decrypt the original failure).
Settlement (force-close)
If a peer is unresponsive past timeout → force-close that channel, HTLC resolved on chain.
CLTV deltas
The HTLC has CLTV expiry. As the payment travels:
- Sender's HTLC has the highest CLTV.
- Each forwarder reduces CLTV by their
cltv_expiry_deltaparameter. - Receiver's HTLC has the smallest CLTV.
This ordering ensures:
- Receiver claims at low CLTV.
- Forwarders have time to claim from upstream.
- If anything goes wrong, refunds happen in safe order.
Standard cltv_expiry_delta: 40-100 blocks per hop typically.
HTLC scripts on chain (post force-close)
For an HTLC the channel partner offered to you:
hash160(payment_hash) OP_EQUALVERIFY <local_htlc_pk> OP_CHECKSIG
OR
<cltv> OP_CHECKLOCKTIMEVERIFY OP_DROP <remote_htlc_pk> OP_CHECKSIG
OR
<revocation_pubkey> OP_CHECKSIG
Three paths: success (preimage), timeout (CLTV), revocation (cheating detection).
2nd-stage HTLC txs
After force-close, HTLC outputs aren't directly spendable — they go through pre-signed 2nd-stage txs:
- HTLC-success-tx: spends HTLC output revealing preimage; output
is
to_local-style with CSV delay before claimable by you. - HTLC-timeout-tx: spends HTLC output after CLTV; same CSV-delayed output.
This 2-stage structure is required for legacy commitment because sigs are pre-committed at channel open (no flexibility post-broadcast).
Anchor zero-fee variant
option_anchors_zero_fee_htlc_tx: HTLC 2nd-stage txs are 0-fee at
sign time, fee-bumped via the commitment's anchors when broadcast.
PTLCs (Point Time-Locked Contracts)
Modern Schnorr/Taproot-based replacement for HTLCs:
- Instead of
hash(r) = H, user * G = T(Schnorr point). - Use adaptor signatures: each hop holds a pre-signature conditional on revealing the secret.
- Privacy gain: each hop sees a different
T(derived per-hop), so observers can't correlate hops by hash value.
Status: BOLT update in flight; implementations partial.
In-flight HTLC limits
Channel-level limits (advertised in accept_channel):
max_accepted_htlcs(default ~483).max_htlc_value_in_flight_msat.dust_limit_satoshis.
Excess HTLCs cause update_add_htlc to fail.
"Stuck" HTLCs
Causes:
- Downstream node unresponsive (route stuck mid-forward).
- Mempool fee spike preventing on-chain resolution.
- Channel partner offline at force-close time.
Recovery:
- Watchtower or self-monitoring + force-close to reclaim funds.
- Standard fix: wait for CLTV expiry, force-close the upstream channel, recover via 2nd-stage timeout tx.
Replacement cycling impact
HTLC-timeout txs are pre-signed at channel open. An attacker could historically "pin" these via low-fee descendants → honest party can't broadcast → CLTV passes upstream → loss.
Post TRUC v3 + ephemeral anchors: pinning surface removed.
Common bugs
- CLTV delta too small → upstream HTLC times out before downstream resolves → forced refund and lost fees.
- Race condition: peer reveals preimage just before CLTV → both parties try to claim, possible loss.
- Misencoded onion failure → hop can't decrypt; routing data lost.
- Custom records (BOLT 4 TLV) incorrectly formatted → some impls reject HTLCs.