BOLT-bolt-04 Final
BOLT-04: Onion Routing
Lightning onion routing protocol. Layered encryption, hop data, and payment privacy.
| Type | Basis of Lightning Technology |
| Number | bolt-04 |
| Status | Final |
| Authors | Lightning Network Developers |
| Original | https://github.com/lightning/bolts/blob/master/04-onion-routing.md |
| Requires |
BOLT-04: Onion Routing
BOLT-04 defines the onion routing protocol that provides payment privacy in Lightning. Each hop only learns the minimum information needed to forward the payment.
Specification Summary
| Aspect | Value |
|---|---|
| Status | Final |
| Layer | Routing |
| Purpose | Payment privacy and routing |
| Dependencies | BOLT-01, BOLT-02 |
Privacy Guarantees
| What Each Node Sees | What They Don’t See |
|---|---|
| Previous hop | Original sender |
| Next hop | Final destination |
| Forwarding amount/fee | Total payment amount |
| Their CLTV delta | Full route length |
Onion Packet Structure
Total size: 1366 bytes (fixed, regardless of route length)
┌─────────────────────────────────────────┐
│ version (1 byte): 0x00 │
├─────────────────────────────────────────┤
│ public_key (33 bytes): ephemeral key │
├─────────────────────────────────────────┤
│ hop_payloads (1300 bytes): encrypted │
├─────────────────────────────────────────┤
│ hmac (32 bytes): authentication │
└─────────────────────────────────────────┘
Sphinx Construction
Key Derivation
For each hop, derive shared secret:
def derive_shared_secret(ephemeral_key, node_pubkey):
"""ECDH to derive shared secret."""
shared_point = ephemeral_key.private_key * node_pubkey
return sha256(shared_point.serialize())
def derive_keys(shared_secret):
"""Derive encryption keys from shared secret."""
return {
'rho': hmac_sha256(shared_secret, b'rho'), # Payload encryption
'mu': hmac_sha256(shared_secret, b'mu'), # HMAC key
'um': hmac_sha256(shared_secret, b'um'), # Error encryption
'pad': hmac_sha256(shared_secret, b'pad'), # Padding
}
Building Onion (Reverse Order)
def build_onion(hops, session_key, associated_data):
"""Construct onion packet for payment route."""
# Start with random filler
filler = generate_filler(hops, session_key)
# Build from last hop backwards
onion = bytes(1300) # Start with zeros
for i in range(len(hops) - 1, -1, -1):
hop = hops[i]
# Derive keys for this hop
ephemeral = derive_ephemeral_key(session_key, hops[:i])
shared = derive_shared_secret(ephemeral, hop.pubkey)
keys = derive_keys(shared)
# Encrypt payload
hop_payload = encode_hop_payload(hop)
stream = generate_stream(keys['rho'], 1300)
# Shift and encrypt
onion = shift_and_encrypt(onion, hop_payload, stream)
# Calculate HMAC
hmac = calculate_hmac(keys['mu'], onion, associated_data)
return OnionPacket(
version=0,
public_key=ephemeral.public_key,
hop_payloads=onion,
hmac=hmac
)
Hop Payload Format
Legacy Format (Deprecated)
Fixed 65 bytes per hop:
┌─────────────────────────────────────────┐
│ short_channel_id (8 bytes) │
│ amt_to_forward (8 bytes) │
│ outgoing_cltv_value (4 bytes) │
│ padding (12 bytes) │
│ hmac (32 bytes) │
└─────────────────────────────────────────┘
TLV Format (Current)
Variable length with BigSize encoding:
┌─────────────────────────────────────────┐
│ length (BigSize) │
├─────────────────────────────────────────┤
│ amt_to_forward (type 2) │
│ outgoing_cltv_value (type 4) │
│ short_channel_id (type 6) │
│ payment_data (type 8) │
│ [additional TLVs] │
├─────────────────────────────────────────┤
│ hmac (32 bytes) │
└─────────────────────────────────────────┘
Payment Data (Final Hop)
payment_data:
payment_secret (32 bytes)
total_msat (8 bytes)
Processing at Each Hop
Unwrapping
def process_onion(onion, node_private_key, associated_data):
"""Process onion at forwarding node."""
# Derive shared secret
shared = ecdh(node_private_key, onion.public_key)
keys = derive_keys(shared)
# Verify HMAC
expected_hmac = calculate_hmac(keys['mu'], onion.hop_payloads, associated_data)
if expected_hmac != onion.hmac:
raise OnionError("HMAC verification failed")
# Decrypt one layer
stream = generate_stream(keys['rho'], 1300)
decrypted = xor(onion.hop_payloads, stream)
# Parse hop payload
hop_payload, next_payload = parse_hop_payload(decrypted)
# Check if final hop
if hop_payload.short_channel_id == 0:
return FinalHop(hop_payload)
# Calculate next ephemeral key
blinding = sha256(onion.public_key || shared)
next_pubkey = onion.public_key * blinding
# Build next onion
next_onion = OnionPacket(
version=0,
public_key=next_pubkey,
hop_payloads=next_payload + zeros(len(hop_payload)),
hmac=hop_payload.hmac
)
return ForwardingHop(hop_payload, next_onion)
Error Handling
Error Packet
When payment fails, error propagates backward:
┌─────────────────────────────────────────┐
│ hmac (32 bytes) │
│ failuremsg_len (2 bytes) │
│ failuremsg (failuremsg_len bytes) │
│ pad_len (2 bytes) │
│ pad (pad_len bytes) │
└─────────────────────────────────────────┘
Error Encryption
Each hop encrypts the error with their um key:
def wrap_error(error, shared_secret):
"""Encrypt error for return path."""
keys = derive_keys(shared_secret)
ammag = hmac_sha256(keys['um'], b'ammag')
stream = generate_stream(ammag, len(error))
return xor(error, stream)
Error Decryption (Sender)
Sender decrypts layer by layer:
def unwrap_error(error, hops, session_key):
"""Decrypt error to find failure point."""
for i, hop in enumerate(hops):
shared = derive_shared_secret_for_hop(session_key, hops[:i+1])
error = wrap_error(error, shared) # Same operation
# Check HMAC
if verify_error_hmac(error, shared):
return (i, parse_error(error))
raise Exception("Could not decrypt error")
Failure Messages
Format
┌─────────────────────────────────────────┐
│ failure_code (2 bytes) │
│ [data]: failure-specific │
└─────────────────────────────────────────┘
Common Failure Codes
| Code | Name | Meaning |
|---|---|---|
| 0x4000 | PERM | Permanent failure |
| 0x2000 | NODE | Node failure |
| 0x1000 | UPDATE | Channel update included |
| 0x400F | incorrect_or_unknown_payment_details | Unknown hash |
| 0x400D | final_expiry_too_soon | CLTV too close |
| 0x1007 | temporary_channel_failure | No liquidity |
| 0x100C | fee_insufficient | Need higher fee |
Failure with Update
┌─────────────────────────────────────────┐
│ failure_code (2 bytes): 0x1XXX │
│ len (2 bytes) │
│ channel_update (len bytes) │
└─────────────────────────────────────────┘
Multi-Path Payments (MPP)
Shared Payment Secret
All parts use same payment_secret:
# Sender creates invoice
payment_secret = random_32_bytes()
payment_hash = sha256(preimage)
# Each part includes in final hop
part1_tlv = {
'payment_data': {
'payment_secret': payment_secret,
'total_msat': 100000000 # Total, not part amount
}
}
Part Identification
Recipient waits for all parts:
- Same payment_hash
- Same payment_secret
- Sum of amounts = total_msat
Agent Implementation
Creating Route Payload
def create_route_payload(route, payment_hash, payment_secret, total_msat):
"""Create TLV payloads for each hop."""
payloads = []
for i, hop in enumerate(route):
is_final = (i == len(route) - 1)
payload = {
2: hop.amount_msat, # amt_to_forward
4: hop.cltv_expiry, # outgoing_cltv
}
if is_final:
# Final hop gets payment data
payload[8] = {
'payment_secret': payment_secret,
'total_msat': total_msat
}
else:
# Intermediate hop gets next channel
payload[6] = hop.short_channel_id
payloads.append(encode_tlv(payload))
return payloads
Related BOLTs
Machine-Readable Summary
{
"bolt": "04",
"title": "Onion Routing",
"status": "final",
"packet_size": 1366,
"key_concepts": [
"sphinx-construction",
"layered-encryption",
"error-propagation",
"tlv-payloads"
],
"privacy": {
"sender_hidden": true,
"receiver_hidden": true,
"amount_hidden": true,
"route_hidden": true
}
}