22 KiB
Lightning Is Getting Taprooty Scriptless-Scripty
This article contains a summary of the many changes that Taproot will bring to lightning. But first of all, a disclaimer:
While Taproot brings many privacy improvements to the table, there are a lot of other, unrelated updates to lightning that are higher priority and orthogonal to it, and implementers are already struggling to find time to review and implement everything. Please be patient, Taproot is coming, but it will take time!
Schnorr, Musig2 and adaptor signatures have been covered in this article: they are the foundations upon which we'll build everything, so I suggest reading it if you want to understand the low-level details of the following proposals.
Now that this is out of the way, let's dive into Taproot stuff.
Table of Contents
- Musig2 Channel Funding
- Taproot Lightning Transactions
- Point Time-Locked Contracts
- Further Changes
- Resources
Musig2 Channel Funding
Thanks to Schnorr and Musig2, we can make lightning channels indistinguishable from any other key path spend in the cooperative case.
The funding transaction output will be a taproot output without a script path, where the
internal_pubkey
is the Musig2 aggregated public key of the two channel participants.
We need to add Musig2 nonces to existing messages (in the extension
tlv stream) but don't need to
define any new message. For example, when updating a commitment, revoke_and_ack
will contain the
next set of nonces and commit_sig
will contain the current nonces and the (partial) signature.
Changing the funding output is a good opportunity to also introduce xpubs. Instead of directly
exchanging a funding_pubkey
when opening the channel, participants exchange a funding_xpub
and then derive the actual funding_pubkey
with non-hardened derivation. Future updates of the
funding output (e.g. splicing funds in or out) can simply increment a derivation counter, which
removes the need to explicitly share the next funding pubkeys and simplifies backup. This idea was
proposed in ajtowns' mailing list post.
This change is quite simple and can be added relatively quickly (once Musig2 has been finalized).
Taproot Lightning Transactions
The existing transaction structure (detailed in this article) can be updated to use Taproot scripts.
This doesn't change the protocol: nodes still exchange the same messages when sending payments
(update_add_htlc
, commit_sig
, revoke_and_ack
, update_fulfill_htlc
, update_fail_htlc
).
We simply take the existing scripts and split them into several branches of a taproot tree, leveraging the key path spend whenever it makes sense.
The commitment transaction will then become:
{
"version": 2,
"locktime": 543210000,
"vin": [
{
"txid": "...",
"vout": ...,
"scriptSig": "<signature for musig2(pubkey1, pubkey2)>",
"sequence": 2500123456
}
],
"vout": [
{
"value": 0.5,
"output_type": "to_local",
"scriptPubKey": {
"internal_pubkey": "
# funds go to the remote node with the revocation key
musig2(<revocationpubkey>,<remote_pubkey>)
",
"tapleaf": "
# or back to us after a relative delay (<to_self_delay>)
<local_delayedpubkey>
OP_CHECKSIGVERIFY
<to_self_delay>
OP_CHECKSEQUENCEVERIFY
"
}
},
{
"value": 0.3,
"output_type": "to_remote",
"scriptPubKey": {
"internal_pubkey": "<unused_nums_point>",
"tapleaf": "
# funds go back to the other channel participant after 1 block
<remote_pubkey>
OP_CHECKSIGVERIFY
1 OP_CHECKSEQUENCEVERIFY
"
}
},
{
"value": 0.00000330,
"output_type": "local_anchor",
"scriptPubKey": {
"internal_pubkey": "<local_delayedpubkey>",
"tapleaf": "
# after a relative timelock of 16 blocks, anyone can claim this tiny amount
# once the to_local output has been spent, revealing the local_delayedpubkey
OP_16 OP_CHECKSEQUENCEVERIFY
"
}
},
{
"value": 0.00000330,
"output_type": "remote_anchor",
"scriptPubKey": {
"internal_pubkey": "<remote_pubkey>",
"tapleaf": "
# after a relative timelock of 16 blocks, anyone can claim this tiny amount
# once the to_remote output has been spent, revealing the remote_pubkey
OP_16 OP_CHECKSEQUENCEVERIFY
"
}
},
{
"value": 0.05,
"output_type": "offered_htlc",
"scriptPubKey": {
"internal_pubkey": "
# funds go to the remote node with the revocation key
musig2(<revocationpubkey>,<remote_pubkey>)
",
"tapleaf_1": "
# funds go back to us via a second-stage HTLC-timeout transaction (which contains an absolute delay)
# NB: we also need the remote signature, which prevents us from unilaterally changing the HTLC-timeout transaction
<local_htlcpubkey> OP_CHECKSIGVERIFY <remote_htlcpubkey> OP_CHECKSIGVERIFY
1 OP_CHECKSEQUENCEVERIFY
",
"tapleaf_2": "
# funds go to the remote node if it has the payment preimage.
OP_SIZE 32 OP_EQUALVERIFY OP_HASH160 <RIPEMD160(payment_hash)> OP_EQUALVERIFY
<remote_htlcpubkey>
OP_CHECKSIGVERIFY
1 OP_CHECKSEQUENCEVERIFY
"
}
},
{
"value": 0.08,
"output_type": "received_htlc",
"scriptPubKey": {
"internal_pubkey": "
# funds go to the remote node with the revocation key
musig2(<revocationpubkey>,<remote_pubkey>)
",
"tapleaf_1": "
# funds go to us via a second-stage HTLC-success transaction once we have the payment preimage
# NB: we also need the remote signature, which prevents us from unilaterally changing the HTLC-success transaction
OP_SIZE 32 OP_EQUALVERIFY OP_HASH160 <RIPEMD160(payment_hash)> OP_EQUALVERIFY
<local_htlcpubkey> OP_CHECKSIGVERIFY <remote_htlcpubkey> OP_CHECKSIGVERIFY
1 OP_CHECKSEQUENCEVERIFY
",
"tapleaf_2": "
# funds go to the remote node after an absolute delay (timeout)
<remote_htlcpubkey>
OP_CHECKSIGVERIFY
1 OP_CHECKSEQUENCEVERIFY
<cltv_expiry>
OP_CHECKLOCKTIMEVERIFY
OP_DROP
"
}
},
]
}
You should notice that the public keys used in the anchor outputs have changed.
We were previously using the local_funding_pubkey
and remote_funding_pubkey
because since they
were revealed in the witness of the funding input, it let anyone watching the blockchain claim
these outputs after 16 blocks to avoid bloating the utxo set.
But with Musig2 the individual funding public keys are never revealed, so we need to use other
public keys that may be revealed when outputs of the commitment transaction are spent.
A taproot HTLC-success transaction looks like:
{
"version": 2,
"locktime": 0,
"vin": [
{
"txid": "...",
"vout": 42,
"scriptSig": "<remotehtlcsig> <localhtlcsig> <payment_preimage>",
"sequence": 1
}
],
"vout": [
{
"value": 0.04,
"scriptPubKey": {
"internal_pubkey": "
# funds go to the remote node with the revocation key
musig2(<revocationpubkey>,<remote_pubkey>)
",
"tapleaf": "
# or back to us after a relative delay (<to_self_delay>)
<local_delayedpubkey>
OP_CHECKSIGVERIFY
<to_self_delay>
OP_CHECKSEQUENCEVERIFY
"
}
}
]
}
A taproot HTLC-timeout transaction looks like:
{
"version": 2,
"locktime": <cltv_expiry>,
"vin": [
{
"txid": "...",
"vout": 42,
"scriptSig": "<remotehtlcsig> <localhtlcsig>",
"sequence": 1
}
],
"vout": [
{
"value": 0.04,
"scriptPubKey": {
"internal_pubkey": "
# funds go to the remote node with the revocation key
musig2(<revocationpubkey>,<remote_pubkey>)
",
"tapleaf": "
# or back to us after a relative delay (<to_self_delay>)
<local_delayedpubkey>
OP_CHECKSIGVERIFY
<to_self_delay>
OP_CHECKSEQUENCEVERIFY
"
}
}
]
}
Do note that the changes described in this section only make sense to do if we're able to integrate PTLCs without radically changing the transaction format. We will explore that in the next section.
Point Time-Locked Contracts
Once lightning transactions use taproot, we'd like to add support for PTLCs (Point Time Locked Contracts) in addition to HTLCs (Hash Time Locked Contracts). PTLCs can only be used when the whole route supports them, which means we'll have to keep supporting HTLCs until the majority of the network has been updated.
The main benefit of PTLCs is payment decorrelation: instead of using the same secret for each hop
in the route (payment_hash
for HTLCs) we can use different secrets for each hop, which provides
much better privacy.
We can use scriptless scripts multi-hop locks to allow routing PTLCs across multiple hops.
Conceptually, what we would like to do when offering a PTLC is to add an output with the following structure (or something similar) to our commitment transaction:
{
"internal_pubkey": "
# funds go to the remote node with the revocation key
musig2(<revocationpubkey>,<remote_pubkey>)
",
"tapleaf_1": "
# funds go back to us via a second-stage PTLC-timeout transaction (which contains an absolute delay)
# NB: we need the remote signature, which prevents us from unilaterally changing the PTLC-timeout transaction
<remote_ptlcpubkey> OP_CHECKSIGVERIFY <local_ptlcpubkey> OP_CHECKSIGVERIFY
1 OP_CHECKSEQUENCEVERIFY
",
"tapleaf_2": "
# funds go to the remote node via a second-stage Claim-PTLC-success transaction by completing an adaptor sig, revealing the payment secret
# NB: we don't use musig2 here because it would force local and remote signatures to use the same sighash flags
<local_ptlcpubkey> OP_CHECKSIGVERIFY <remote_ptlcpubkey> OP_CHECKSIGVERIFY
1 OP_CHECKSEQUENCEVERIFY
"
}
But this introduces a fundamental change compared to HTLCs: the receiver cannot directly claim the PTLC from our commitment transaction once they have the secret. Instead, we need to introduce a second-stage transaction that must be pre-signed.
First of all, let's explain why this fundamental change is necessary.
With HTLCs, the secret (payment preimage) could be revealed by the receiver in the spending script of the HTLC output. Once the HTLC was spent, the sender could simply look at the signature script to learn the secret and propagate it upstream.
With PTLCs, the secret is a private key. The way it is revealed is a two steps process:
- the sender provides an adaptor signature based on the payment point
- the receiver completes the adaptor signature (using the payment secret) to spend the output
With both the adaptor signature and the complete signature, the sender can extract the secret. We can see a few problems emerge from that two steps process:
- more signatures need to be exchanged, because each peer needs to send signatures for 2nd-stage transactions that let the remote peer claim PTLCs from the local commitment
- claiming successful PTLCs from the remote peer's commitment now requires using RBF and sighash
flags similar to anchor outputs HTLC transactions (
sighash_single | sighash_anyonecanpay
trick) - before signing a commitment update, peers must obtain from their counterparty adaptor signatures for their pending received PTLCs in the future remote commitment
Let's now detail a strawman, high-level proposal that enables PTLCs.
First of all, have a look at the existing transaction structure. We simply add two new types of outputs to the commit tx (PTLC offered / PTLC received):
+------------+
| funding tx |
+------------+
|
| +------------------------+
+------->| commit tx B |
+------------------------+
| | | | | | | |
| | | | | | | | A's main output
| | | | | | | +-----------------> to A after a 1-block relative delay
| | | | | | |
| | | | | | | +---> to B after relative delay
| | | | | | | B's main output |
| | | | | | +-----------------+
| | | | | | |
| | | | | | +---> to A with revocation key
| | | | | |
| | | | | | A's anchor output
| | | | | +--------------------> to A immediately (or anyone after 16-block relative delay)
| | | | |
| | | | | B's anchor output
| | | | +-----------------------> to B immediately (or anyone after 16-block relative delay)
| | | |
| | | | (B's RBF inputs) ---+
| | | | | +---> to B after relative delay
| | | | +---->+-----------------+ |
| | | | +---------->| HTLC-timeout tx |------------+
| | | | HTLC offered by B | +-----------------+ |
| | | +-------------------+ (after timeout + 1-block delay) +---> to A with revocation key
| | | |
| | | +---> to A with payment preimage after a 1-block relative delay
| | | |
| | | +---> to A with revocation key
| | |
| | | (B's RBF inputs) ---+
| | | | +---> to B after relative delay
| | | +---->+-----------------+ |
| | | +------------>| HTLC-success tx |----------------+
| | | HTLC received by B | +-----------------+ |
| | +--------------------+ (with payment preimage + 1-block delay) +---> to A with revocation key
| | |
| | +---> to A after timeout (absolute delay + 1-block relative delay)
| | |
| | +---> to A with revocation key
| |
| | (B's RBF inputs) ---+
| | | +---> to B after relative delay
| | +---->+-----------------+ |
| | +------------>| PTLC-timeout tx |-------------+
| | PTLC offered by B | +-----------------+ |
| +-------------------+ (after timeout + 1-block delay) +---> to A with revocation key
| |
| | (A's RBF inputs) ---+
| | |
| | +---->+-----------------------+
| +-------------------------->| Claim-PTLC-success tx |--------------> to A
| | +-----------------------+
| | (with payment secret + 1-block delay)
| |
| +---> to A with revocation key
|
| (B's RBF inputs) ---+
| | +---> to B after relative delay
| +---->+-----------------+ |
| +---------->| PTLC-success tx |----------------+
| PTLC received by B | +-----------------+ |
+--------------------+ (with payment secret + 1-block delay) +---> to A with revocation key
|
+---> to A after timeout (absolute delay + 1-block relative delay)
|
+---> to A with revocation key
You should notice that the two PTLC outputs are very similar to the HTLC ones.
The only difference is the introduction of the claim-ptlc-success
transaction.
This claim-ptlc-success
transaction directly pays to the remote peer (no delay, no revocation).
The current protocol for updating commitments is:
Alice Bob
| commitment_signed |
|------------------------>|
| revoke_and_ack |
|<------------------------|
| commitment_signed |
|<------------------------|
| revoke_and_ack |
|------------------------>|
Alice -> Bob: commitment_signed
channel id
signature for Bob to spend funding tx
sigs for Bob to spend HTLCs from his next commitment
Bob -> Alice: revoke_and_ack
channel id
reveal previous commitment secret
next commitment point
Bob -> Alice: commitment_signed
channel id
signature for Alice to spend funding tx
sigs for Alice to spend HTLCs from her next commitment
Alice -> Bob: revoke_and_ack
channel id
reveal previous commitment secret
next commitment point
The main difficulty introduced by the claim-ptlc-success
transaction is that Alice needs to
obtain adaptor signatures from Bob before she can send her commitment_signed
. Let's detail why.
Let's assume that there is currently a pending PTLC paying Alice in both commitments. What happens
if Alice sends commitment_signed
to Bob?
Bob now has a new version of his commitment transaction, that he can broadcast. But Alice is unable
to spend her PTLC output from this transaction, because she doesn't have Bob's signature for the
new corresponding claim-ptlc-success
(even if she obtains the payment secret).
This can be fixed by changing the protocol:
Alice Bob
| commitment_proposed |
|------------------------>|
| commitment_proposed |
|<------------------------|
| commitment_signed |
|<------------------------|
| revoke_and_ack |
|------------------------>|
| commitment_signed |
|------------------------>|
| revoke_and_ack |
|<------------------------|
Alice -> Bob: commitment_proposed
channel id
adaptor sigs for PTLCs to Bob in Alice's next commitment (claim-ptlc-success)
musig nonces for Alice's signature on Alice's next commitment
Bob -> Alice: commitment_proposed
channel id
adaptor sigs for PTLCs to Alice in Bob's next commitment (claim-ptlc-success)
musig nonces for Bob's signature on Bob's next commitment
Bob -> Alice: commitment_signed
channel id
musig nonces for Bob's signature on Alice's next commitment
Bob's signature on Alice's next commitment
sigs for Alice to spend HTLCs and PTLCs from her next commitment
Alice -> Bob: revoke_and_ack
channel id
reveal previous commitment secret
next commitment point
Alice -> Bob: commitment_signed
channel id
musig nonces for Alice's signature on Bob's next commitment
Alice's signature on Bob's next commitment
sigs for Bob to spend HTLCs and PTLCs from his next commitment
Bob -> Alice: revoke_and_ack
channel id
reveal previous commitment secret
next commitment point
The commitment_signed
and revoke_and_ack
are mostly unchanged, the only difference is that the
ptlc signatures in commitment_signed
are now adaptor signatures for PTLC-success transactions.
This change adds half a round-trip compared to the previous protocol, and changes which peer signs a new commitment first (with the previous protocol, Alice was signing first, but now it's Bob who signs first to save half another round-trip).
Further changes
We could wait for Eltoo before doing any kind of change that fundamentally updates the transaction structure and update protocol. However Eltoo requires a bitcoin soft-fork, so there is no guarantee that it will ever be possible.
Alternatively, ajtowns
proposed a new transaction format design on the mailing list in his
lightning over taproot with PTLCs post.
This proposal deserves its own article, which will be written once we have a first set of taproot updates deployed on the network.