Onion Messaging In Depth

Rusty Russell
3 min readSep 7, 2021


Feedback from Matt Corallo recently made me realize how complex onion messages can seem, mainly because they’re divided between multiple documents: https://github.com/lightningnetwork/lightning-rfc/blob/route-blinding/proposals/route-blinding.md which describes how to send blinded HTLCs, https://github.com/lightningnetwork/lightning-rfc/pull/759 which proposes onion messages (itself split between the messages themselves, and the format inside the onion), and finally https://github.com/lightningnetwork/lightning-rfc/pull/798 which actually uses them for BOLT 12.

Here’s a description of how it works in the latest draft:

  1. There’s a new message (387, aka onion_message), which contains a “blinding point” and an onion message.
  2. The onion message is just like the one used inside update_add_htlc, except it’s a variable size instead of always having 1300 bytes of payload data.
  3. You can’t just unwrap the onion though: it’s been created for your blinded address, not your normal node id. But you can figure out what that was, using that “blinding point”: you use ECDH to get a shared secret ss and then you can determine the blinding tweakHMAC256("blinded_node_id", ss). You can multiply your private key by that blinding tweak and unwrap the onion, but you can also simply multiply the ephemeral key inside the onion by that blinding tweak (and then use your normal key to unwrap) for the same effect.
  4. Now you get the variable length contents of the hop, called an onionmsg_payload. It’s actually a type-length-value stream, and if you’re the final node it contains the reply path, offer request or whatever. But if you’re an intermediate node it only contains one field, an encrypted blob called “enctlv”.
  5. You use that ss from the blinding point to derive the decryption key: HMAC256("rho", ss)gives they key (this identical formulation is used inside the Sphinx protocol to unwrap the onion itself). ChaCha20-Poly1305’s AEAD is used with a 96-bit all-zero nonce.
  6. This second decryption gives you another TLV stream. This one contains the next node id to send the message to, and optionally a new blinding point to hand it.
  7. If that tlv stream didn’t specify a new blinding point, derive it from the current one by hashing in the shared secret: SHA256(blinding point || ss)
  8. Pass the unwrapped onion and new blinding point to the next peer using a fresh onion message. If it doesn’t support onion messages, no loss, since it’s an odd message that it will happily ignore.

This formulation is kinda complicated: The two layers of encryption in particular. But it’s necessary for blinded paths: you can hand me an entry node_id, a blinding, and a series of blinded node_ids and enctlvs, and I can create an onion which routes through them without knowing what the contents is. That’s immediately useful for sending replies for onion messages, but also useful for adding blinded paths to offers and invoices so the issuer doesn’t reveal their own node_id (particularly useful when requesting a tip or a refund!).

Now, the initial formulation allowed fields outside the enctlv blob, but that made onion messages more distinguishable, so now we always use an enctlv even if it means you have to generate one yourself. This entailed a number change from the original (385) and the new one, so during transition c-lightning will send (and respond) to both.

Fields outside enctlv are still required for sending actual HTLCs along blinded paths: the creator of the onion has to be able to set amounts, for example.



Rusty Russell

Rusty is a Linux kernel dev who wandered into Blockstream, and is currently trying to produce a prototype and spec for bitcoin lightning. Hodls bitcoin (only).