Hash Time-Locked Contracts (HTLCs)
How HTLCs enable trustless multi-hop Lightning payments. Hash locks, time locks, and atomic payment resolution.
Hash Time-Locked Contracts (HTLCs)
HTLCs are the cryptographic mechanism that enables trustless multi-hop payments in Lightning. They ensure that either all hops succeed or all hops fail—atomic execution across untrusted intermediaries.
Why HTLCs Matter for Agents
| Benefit | Description |
|---|---|
| Trustless | No need to trust routing nodes |
| Atomic | All-or-nothing payment resolution |
| Programmable | Conditional payment logic |
| Composable | Chain across multiple hops |
The Two Locks
An HTLC combines two conditions:
1. Hash Lock
The payment is locked by a hash. To claim funds, the recipient must reveal the preimage—the secret value that hashes to the known hash.
hash = SHA256(preimage)
If you know: preimage
And: SHA256(preimage) == hash
Then: You can claim the funds
2. Time Lock
If the preimage isn’t revealed in time, funds return to the sender. This prevents funds from being locked forever.
If: current_time > timeout
Then: Sender can reclaim funds
HTLC Structure
┌─────────────────────────────────────────────┐
│ HTLC │
├─────────────────────────────────────────────┤
│ Amount: 10,000 sats │
│ Hash: a1b2c3d4... │
│ Timeout: block 800,000 │
│ │
│ Claim path (recipient): │
│ IF <hash> == SHA256(<preimage>) │
│ AND <recipient_sig> │
│ │
│ Refund path (sender): │
│ IF <current_block> > 800,000 │
│ AND <sender_sig> │
└─────────────────────────────────────────────┘
Multi-Hop Payment Flow
Setup Phase (Forward)
-
Recipient creates preimage and hash
preimage = random_32_bytes() payment_hash = SHA256(preimage) -
Recipient shares hash in invoice
Invoice contains: payment_hash, amount, expiry -
Sender finds route and creates HTLCs
Alice → Carol → Bob Alice creates HTLC to Carol (hash, timeout=100) Carol creates HTLC to Bob (hash, timeout=90)
Resolution Phase (Backward)
-
Recipient reveals preimage
Bob knows preimage (he created it) Bob gives preimage to Carol, claims HTLC -
Intermediaries resolve backwards
Carol now knows preimage Carol gives preimage to Alice, claims HTLC -
Payment complete
Alice: -10,000 sats Carol: ±0 (minus routing fee) Bob: +10,000 sats (minus fees)
Timeout Decrements
Each hop has a shorter timeout than the previous hop. This ensures intermediaries have time to claim their incoming HTLC after revealing the preimage.
Alice ────────→ Carol ────────→ Bob
timeout=100 timeout=90 timeout=80
│ │ │
└───── 10 blocks ───────────────┘
(cltv_expiry_delta)
If Bob doesn’t reveal the preimage by block 80:
- Bob’s HTLC from Carol times out
- Carol’s HTLC from Alice times out
- Funds return to Alice
HTLC States
┌──────────┐
│ Offered │ ──── fulfill (preimage) ────→ ┌───────────┐
└──────────┘ │ Fulfilled │
│ └───────────┘
│
└──── timeout (blocks pass) ────────→ ┌───────────┐
│ Timed Out │
└───────────┘
HTLC Operations
| Operation | Direction | Meaning |
|---|---|---|
update_add_htlc | Forward | Add new HTLC |
update_fulfill_htlc | Backward | Reveal preimage |
update_fail_htlc | Backward | Fail with reason |
update_fail_malformed_htlc | Backward | Malformed onion |
HTLC Script (On-Chain)
When force-closing with pending HTLCs, the script enforces the conditions:
# Offered HTLC (from Alice's perspective)
OP_DUP OP_HASH160 <revocation_hash> OP_EQUAL
OP_IF
OP_CHECKSIG # Revocation path
OP_ELSE
<remote_pubkey> OP_SWAP OP_SIZE 32 OP_EQUAL
OP_IF
OP_HASH160 <payment_hash> OP_EQUALVERIFY
2 OP_SWAP <local_pubkey> 2 OP_CHECKMULTISIG # Payment path
OP_ELSE
OP_DROP <cltv_expiry> OP_CHECKLOCKTIMEVERIFY OP_DROP
OP_CHECKSIG # Timeout path
OP_ENDIF
OP_ENDIF
HTLC Limits
| Limit | Value | Reason |
|---|---|---|
| Max HTLCs per channel | 483 | Script size limits |
| Min HTLC amount | ~1 sat | Dust limit |
| Max HTLC amount | Channel capacity | Liquidity |
| Typical timeout delta | 40-144 blocks | Security margin |
Failure Reasons
When an HTLC fails, it includes a reason code:
| Code | Reason | Meaning |
|---|---|---|
incorrect_or_unknown_payment_details | Unknown hash | Invoice doesn’t exist |
final_expiry_too_soon | Timeout too close | Not enough time |
incorrect_cltv_expiry | Wrong timeout | Doesn’t match route |
amount_below_minimum | Too small | Below node minimum |
fee_insufficient | Low fee | Need higher routing fee |
channel_disabled | Channel offline | Try different route |
temporary_channel_failure | Liquidity issue | Not enough capacity |
Agent Implementation
Creating a Payment Hash
import hashlib
import secrets
# Generate random preimage
preimage = secrets.token_bytes(32)
# Calculate payment hash
payment_hash = hashlib.sha256(preimage).digest()
print(f"Preimage: {preimage.hex()}")
print(f"Payment hash: {payment_hash.hex()}")
Verifying a Preimage
def verify_preimage(preimage_hex: str, payment_hash_hex: str) -> bool:
preimage = bytes.fromhex(preimage_hex)
expected_hash = bytes.fromhex(payment_hash_hex)
actual_hash = hashlib.sha256(preimage).digest()
return actual_hash == expected_hash
Monitoring HTLC Status
# Via LND
pending = lnd.pending_channels()
for channel in pending.pending_force_closing_channels:
for htlc in channel.pending_htlcs:
print(f"Amount: {htlc.amount}")
print(f"Outgoing: {htlc.outgoing}")
print(f"Blocks until timeout: {htlc.blocks_til_maturity}")
Security Considerations
Preimage Privacy
- Never reuse preimages—reveals payment correlation
- Generate securely—use cryptographic randomness
- Store safely—losing preimage = can’t prove payment
Timeout Selection
- Too short: Risk of force close before resolution
- Too long: Funds locked if payment fails
- Typical: 40-144 blocks per hop
HTLC Probing
Attackers can probe channel balances by sending HTLCs they control:
- Send HTLC with known preimage
- If it routes, reveal preimage
- Learn channel capacity at each hop
Mitigation: Use route hints, private channels
Related Topics
- Payment Channels - Where HTLCs live
- Routing - Finding HTLC paths
- Invoices - Encoding payment hashes
- BOLT-03 - Transaction formats
Machine-Readable Summary
{
"topic": "htlcs",
"key_concepts": [
"hash-lock",
"time-lock",
"preimage",
"atomic-execution"
],
"operations": [
"update_add_htlc",
"update_fulfill_htlc",
"update_fail_htlc"
],
"security_considerations": [
"preimage-privacy",
"timeout-selection",
"htlc-probing"
]
}