Lightning Inter-node Protocol: A Primer

The original protocol I wrote for two lightning nodes to communicate was simple: to make a change, you proposed it and sent the signature for their commitment transaction, they replied with the revocation secret for the previous one and a signature for you, then you finished it by providing a signature for them. If both sides proposed changes at the same time, one backed off.

Unfortunately, it was too simple: Joseph Poon wanted something far more efficient. Doing that well has been a longer journey than I wanted, but it’s worth writing up where we got to as it’s bound to cause much confusion.

There are two ways we make it more efficient: batching means we can send more than one update at once, and desynchronization means both sides are independent. The rules for doing this are not very complicated, but the system overall can be very hard to follow; so much so that I ended up writing a protocol simulator to be sure it works.

Each node keeps track of two commitment transactions; its local one (which it will drop to the blockchain if anything goes wrong), and the remote one (which it might see on the blockchain if the otherside releases it). For both the local and remote commitments, it keeps track of two types of changes: unacknowledged changes, and acknowledged changes.

Node A pushes a batch of updates to the remote commitment, such as “offer this new HTLC”, “I redeem your existing HTLC” or “your existing HTLC failed to route”. At some point, A locks them in by sending a commit message with its signature for the new remote commitment transaction with those changes. Node B checks the signature, and returns the revocation preimage for its previous commitment transaction, promising never to use it.

So node B has a signed commitment transaction with the changes, but node A hasn’t. We could have node B send exactly the same updates back to A, followed by a commit, but the revocation message serves the same purpose: A knows that B has seen all those updates, so it can now queue those changes to its own commitment transaction. But it still needs B’s signature.

So B now sends its own update message, signing A’s commitment transaction with those changes applied. A checks the signature, and sends its previous revocation preimage to obsolete the old commitment transaction. Both sides are in sync.

Here’s an autogenerated diagram of A adding HTLC #1:

A Full Cycle for a Single HTLC Add

The General Case

To deal with the case where B makes simultaneous proposals, we need to get a little bit more formal. Remember that each node makes proposals for the remote side; these changes only get applied locally once they’re acked.

  1. When a node first sends an update, it adds it to its remote unacked list.
  2. When a node receives an update, it adds it to its local unacked list. This mirrors the above, so both sides have the same lists (my remote = your local, and vice versa).
  3. When a node sends a commitment, it first applies everything in its remote lists: both acked and unacked.
  4. When a node receives a commitment, it applies everything in its local lists: both acked and unacked. It then copies its local unacked list (ie. all the changes you sent me) into its remote acked list, and sends the revocation preimate.
  5. When a node receives the revocation preimage, it copies its remote unacked list (ie. all the changes it send in step 3) into its local acked list.

The two lists ensure that every change gets applied first on the recipient node, then on the proposer node. There are various ways for an implementation to optimize this of course.

Here’s a case, where they both offer an HTLC at the same time, then commits fly back and forth until they’re both in sync again:


Fee negotiation is a little weird: I have no real interest in what fees the other node’s commitment pays (within reason), but I need to keep fees on my commitment competitive, in case I need to drop the transaction to the blockchain. So unlike HTLCs, they’re not symmetrical.

We fit “fee change” messages into the same yours-then-mine model by ignoring their fee changes when applying changes to our own commitment transaction (technically, we only apply fee changes from the acknowledged set). So we send a fee change like any other change, send the commit, and then when the other side commits, that’s when we finally adjust our own commit transaction’s fee. Fortunately, fee changes should not be very common:

Fee changes following the same flow as other changes.

Ouch My Brain!

Wait, is that an acked or unacked change!??!

There have been several wrong turns in this design; it went through several iterations using explicit ack numbers before I was convinced they were unnecessary. My implementation had a bug when commit messages crossed over which was so confusing I had to go back to first principles; it’s also been a source of confusion on the mailing list.

It’d be easy to blame new baby and lack of sleep, but the truth is that protocols have to be simple enough to implement badly: they will be anyway!

Implementing this in my prototype is the final issue for the 0.3 release, which already has a codename thanks to Braydon Fuller