BOLT-08: Transport Encryption
Lightning encrypted transport protocol. Noise Protocol handshake and encrypted messaging.
| Type | Basis of Lightning Technology |
| Number | bolt-08 |
| Status | Final |
| Authors | Lightning Network Developers |
| Original | https://github.com/lightning/bolts/blob/master/08-transport.md |
BOLT-08: Encrypted Transport
BOLT-08 specifies the encrypted communication layer for Lightning nodes using the Noise Protocol Framework. All Lightning messages are encrypted and authenticated.
Specification Summary
| Aspect | Value |
|---|---|
| Status | Final |
| Layer | Transport |
| Purpose | Encryption and authentication |
| Protocol | Noise_XK |
Noise Protocol
Lightning uses Noise_XK with:
- X: Initiator’s static key transmitted encrypted
- K: Responder’s static key known to initiator
Cryptographic Primitives
| Primitive | Algorithm |
|---|---|
| Cipher | ChaCha20-Poly1305 |
| Hash | SHA-256 |
| DH | secp256k1 ECDH |
Handshake
Three-message handshake establishes encrypted channel:
Initiator Responder
│ │
│──── Act One ──────────────────────→│
│ (ephemeral key) │
│ │
│←─── Act Two ───────────────────────│
│ (ephemeral key + encrypted) │
│ │
│──── Act Three ────────────────────→│
│ (static key encrypted) │
│ │
│ Encrypted channel established │
│ │
Act One
Initiator sends ephemeral public key:
┌─────────────────────────────────────────┐
│ version (1 byte): 0x00 │
│ ephemeral_key (33 bytes) │
│ tag (16 bytes): ChaCha20-Poly1305 tag │
└─────────────────────────────────────────┘
Total: 50 bytes
def act_one(initiator_ephemeral, responder_static):
# Initialize handshake state
h = sha256(b"Noise_XK_secp256k1_ChaChaPoly_SHA256")
h = sha256(h + b"lightning")
# Mix responder's static key
h = sha256(h + responder_static.serialize())
# Generate ephemeral key
e = initiator_ephemeral
h = sha256(h + e.public_key.serialize())
# ECDH with responder's static
es = ecdh(e, responder_static)
ck, temp_k1 = hkdf(ck, es)
# Encrypt empty payload
c = chacha20_poly1305_encrypt(temp_k1, 0, h, b"")
h = sha256(h + c)
return bytes([0x00]) + e.public_key.serialize() + c
Act Two
Responder sends their ephemeral key and encrypted acknowledgment:
┌─────────────────────────────────────────┐
│ version (1 byte): 0x00 │
│ ephemeral_key (33 bytes) │
│ tag (16 bytes) │
└─────────────────────────────────────────┘
Total: 50 bytes
Act Three
Initiator sends encrypted static key:
┌─────────────────────────────────────────┐
│ version (1 byte): 0x00 │
│ encrypted_pubkey (49 bytes) │
│ tag (16 bytes) │
└─────────────────────────────────────────┘
Total: 66 bytes
After Act Three, both parties derive symmetric keys for encryption.
Message Encryption
After handshake, all messages are encrypted:
Encrypted Message Format
┌─────────────────────────────────────────┐
│ length (2 bytes): encrypted │
│ length_tag (16 bytes) │
├─────────────────────────────────────────┤
│ ciphertext (length bytes) │
│ ciphertext_tag (16 bytes) │
└─────────────────────────────────────────┘
Encryption Process
class NoiseTransport:
def __init__(self, send_key, recv_key):
self.send_key = send_key
self.recv_key = recv_key
self.send_nonce = 0
self.recv_nonce = 0
def encrypt(self, plaintext: bytes) -> bytes:
# Encrypt length
length = len(plaintext)
length_bytes = length.to_bytes(2, 'big')
enc_length = chacha20_poly1305_encrypt(
self.send_key,
self.send_nonce,
b"",
length_bytes
)
self.send_nonce += 1
# Encrypt payload
enc_payload = chacha20_poly1305_encrypt(
self.send_key,
self.send_nonce,
b"",
plaintext
)
self.send_nonce += 1
# Key rotation every 1000 messages
if self.send_nonce >= 1000:
self.rotate_send_key()
return enc_length + enc_payload
def decrypt(self, ciphertext: bytes) -> bytes:
# Decrypt length (first 18 bytes)
length_bytes = chacha20_poly1305_decrypt(
self.recv_key,
self.recv_nonce,
b"",
ciphertext[:18]
)
self.recv_nonce += 1
length = int.from_bytes(length_bytes, 'big')
# Decrypt payload
payload = chacha20_poly1305_decrypt(
self.recv_key,
self.recv_nonce,
b"",
ciphertext[18:18+length+16]
)
self.recv_nonce += 1
if self.recv_nonce >= 1000:
self.rotate_recv_key()
return payload
Key Rotation
After 1000 messages, keys are rotated:
def rotate_key(key, chaining_key):
"""Rotate encryption key using HKDF."""
new_chaining_key, new_key = hkdf(chaining_key, key)
return new_key, new_chaining_key
This provides forward secrecy—compromised keys don’t expose past messages.
Connection Establishment
TCP Connection
Default port: 9735
def connect_to_node(node_pubkey, address, port=9735):
# TCP connect
socket = tcp_connect(address, port)
# Perform Noise handshake
transport = noise_handshake(socket, node_pubkey)
# Send init message
transport.send(create_init_message())
# Receive init
peer_init = transport.receive()
return LightningConnection(transport, peer_init.features)
Tor Support
Nodes can use Tor for privacy:
# .onion address (v3)
pubkey@abcdefghijklmnopqrstuvwxyz234567abcdefghijklmnopqrstuvwxyz.onion:9735
Security Properties
| Property | Guarantee |
|---|---|
| Confidentiality | Messages encrypted |
| Authentication | Node identity verified |
| Forward Secrecy | Key rotation protects past |
| Integrity | Poly1305 authentication |
Implementation Notes
Nonce Management
- 64-bit nonces (only 32 bits used before rotation)
- Must never reuse nonce with same key
- Rotation at 1000 prevents overflow issues
Error Handling
- Invalid handshake: Close connection
- Decryption failure: Close connection
- Don’t reveal which step failed (timing attacks)
Related BOLTs
Machine-Readable Summary
{
"bolt": "08",
"title": "Encrypted Transport",
"status": "final",
"protocol": "Noise_XK_secp256k1_ChaChaPoly_SHA256",
"handshake_messages": 3,
"handshake_bytes": 166,
"key_rotation_interval": 1000,
"default_port": 9735
}