Menu
BOLT-bolt-08 Final

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

AspectValue
StatusFinal
LayerTransport
PurposeEncryption and authentication
ProtocolNoise_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

PrimitiveAlgorithm
CipherChaCha20-Poly1305
HashSHA-256
DHsecp256k1 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

PropertyGuarantee
ConfidentialityMessages encrypted
AuthenticationNode identity verified
Forward SecrecyKey rotation protects past
IntegrityPoly1305 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)

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
}