Understanding Channel Force Closures
It took me forever to fully understand the intricacies of channel force closures. Sources online were either too high level or too specific about implementation details - this post aims to be an in-between. The content of this post dives into the transactions of a force closure. This post will answer specific questions like:
- What are the outputs in a commitment transaction?
- What transactions will be broadcasted onchain?
Cooperative Channel Close
To start, let’s talk about cooperative channel closures, when life is roses and butterflies.
In a cooperative closure, both parties agree on the final balance and broadcast a closing transaction that contains the wallet outputs for each party
(https://github.com/lnbook/lnbook/blob/develop/07_payment_channels.asciidoc)
Force Channel Close
Force closures happen when one party isn’t cooperating with the other, resulting in the counterparty to initiate a force close. In addition, if one party decides to cheat another, then they need to be punished. For force closure, one party publishes the latest commitment transaction.
(https://github.com/lnbook/lnbook/blob/develop/07_payment_channels.asciidoc)
A commitment transaction is the latest state of the channel. If Alice has a channel with Bob, both Alice and Bob have their own version of a commitment transaction with the latest balances.
(https://github.com/lnbook/lnbook/blob/develop/07_payment_channels.asciidoc)
Each commitment transaction has an output with a locking script that looks like:
OP_IF
# Penalty transaction
<revocationpubkey>
OP_ELSE
<to_self_delay>
OP_CHECKSEQUENCEVERIFY
OP_DROP
<local_delayedpubkey>
OP_ENDIF
OP_CHECKSIG
The to_self_delay
timelock allows the counterparty to publish a revocation secret in the event that one party tries to cheat from publishing an old commitment transaction
Let’s look at the revocation path.
Revocation Secret
When a new commitment transaction is created, it reflects the latest agreed-upon channel state. Each party generates a revocation key and revocation secret for the previous commitment transaction. If a party tries to broadcast an old commitment transaction to the blockchain (attempting to cheat), the counterparty can react by using the revocation secret associated with that old state to claim all the funds in the channel, effectively penalizing the cheating party.
(https://github.com/lnbook/lnbook/blob/develop/07_payment_channels.asciidoc)
The Outputs of a Commitment Tx
The commitment tx has 4 different output types for various situations, 3 of which have HTLC locking scripts.
Suppose Bob is broadcasting his commitment transaction.
(https://github.com/lightning/bolts/issues/553#issuecomment-455641943)
1. Output to Alice
This output is straightforward - there is no locking script. Alice can spend it without constraint.
2. Output to Bob
This output goes to Bob with the same locking script mentioned above
OP_IF
# Penalty transaction
<revocationpubkey>
OP_ELSE
<to_self_delay>
OP_CHECKSEQUENCEVERIFY
OP_DROP
<local_delayedpubkey>
OP_ENDIF
OP_CHECKSIG
Bob needs to wait to_self_delay
to give time for Alice to dispute any bad behavior.
3. HTLC Offered by Bob
This represents an in-flight HTLC that Bob makes to Alice. For a better understanding of HTLCs I highly recommend reading these posts by Elle Mouton. By far the best explanations of LN on the internet.
To summarize from Elle:
This contract is essentially a locking script that has 3 conditions to spend it:
- To HTLC timeout tx address. If the HTLC times out after the
cltv expiry
, then the funds will be sent to a HTLC timeout tx - To Alice’s address. Alice can spend if she provides a preimage
- To Alice’s address. Alice can spend if she provides a revocation secret
The conditions to spend the HTLC timeout output are:
- To Bob’s address. Bob can spend after
to_self_delay
- To Alice’s address. Alice can spend if Alice provides a revocation secret
Why is there a separate HTLC-timeout transaction you might ask?
There are two timeouts that need to accounted for.
to_self_delay
is related to the commitment transaction and allows time for the counterparty to punish bad behaviorcltv_expiry
is related to the HTLC and allows the sender to get their money back in the event that the recipient is unresponsive.
Putting both conditions in the same locking script causes problems. If the first spending condition in the locking script were instead:
- To Bob’s output, after
to_self_delay
+cltv_expiry
This extends the HTLC timeout by to_self_delay
! This gives extra time for Alice to spend the funds even if the HTLC is expired. A separate transaction is needed to separate this logic.
4. HTLC Received by Bob
This represents an in-flight HTLC that Bob receives from Alice.
Again, to summarize from Elle:
This contract is essentially a locking script that has 3 conditions to spend it:
- To HTLC success tx address. If Bob provides the preimage, then the funds will be sent to a HTLC success tx
- To Alice’s address. Alice can spend after
cltv_expiry
- To Alice’s address. Alice can spend if she provides a revocation secret
The conditions to spend the HTLC success output are:
- To Bob’s address. Bob can spend after
to_self_delay
- To Alice’s address. Alice can spend if Alice provides a revocation secret
Why is there a separate HTLC-success transaction you might ask?
Again, putting both conditions in the same locking script causes problems. If the first spending condition in the locking script were instead:
- To Bob’s output, after
to_self_delay
+ preimage
Then there’s a chance that to_self_delay
is longer than the cltv_expiry
, then Alice could claim her funds back even though Bob has the preimage! A separate transaction is needed to separate this logic.
Why are there two revocation paths?
Because either the commitment tx or the HTLC timeout/success tx can be revoked, depending on which one is published onchain.
An Example to Sum up
Consider a newly established channel between Alice and Bob. Say, Alice funded the channel with 1 BTC, and 0.1 BTC goes to Bob initially. Each of them has their own version of the initial commitment transaction, each with two outputs: 0.9 to Alice, 0.1 to Bob.
Imagine Alice wants to send 0.2 BTC to Bob.
A new pair of commitment transactions is created, each of them with three outputs: 0.7 to Alice, 0.1 to Bob, and 0.2 as an in-flight HTLC.
Once the HTLC fails/succeeds, as long as both sides commit to the new state, the new commitment transaction will have 2 outputs: 0.7 to Alice, 0.3 to Bob.
Finally, there could be up to 3 layers of onchain transactions from a channel force close. E.g.
- Commitment tx that spends the funding tx
- HTLC timeout / success tx that spends the commitment tx
- Revocation tx that spends the HTLC tx
Put Your Learnings to the Test
The replacement cycling attack is an attack vector in LN. Super difficult to pull off, but possible today. This uses all the concepts and terminology from this post.
How does a lightning replacement cycling attack work?
— mononaut (tx/acc) (@mononautical) October 21, 2023
There's a lot of discussion about this newly discovered vulnerability on the mailing lists, but the actual mechanism is a bit hard to follow.
So here's an illustrated primer...
🧵 1/n pic.twitter.com/mvvS8bEc5f