Authors:
- Keagan McClelland [email protected]
- Olaoluwa Osuntokun [email protected]
- Eugene Siegel [email protected]
Created: TODO
TODO
This document describes a protocol for changing channel parameters that were negotiated at the conception of the channel. Implementation of the protocol described in this document will enable channel peers to re-negotiate and update channel terms as if the channel was being opened for the first time while avoiding UTXO churn whenever possible and therefore preserving the continuity of identity for the channel whose terms are being changed.
It is well understood that closing channels is a costly thing to do. Not only is it costly from a chain fees perspective (where we pay for moving the funds from the channel UTXO back to the main wallet collection), it is also costly from a service availability and reputation perspective.
After channels are closed, they are no longer usable for forwarding HTLC traffic and even if we were to immediately replace the channel with another equally capable one, the closure event is visible to the entire network. Since routes are computed by the source, the network-wide visibility of channel closures directly impacts whether or not the sender will be able to use a channel.
Beyond that, one of the pathfinding heuristics that is frequently used to assess channel reliability is the channel age. The longevity of a channel is therefore a key asset that any running Lightning node should want to preserve, if possible.
It follows from the above that we should try to minimize channel closure events
when we can manage to do so. This is the main motivation of this proposal. Prior
to this extension BOLT, there hasn't been a way to change some of the channel
parameters established in the {open|accept}_channel
messages without resorting
to a full channel closure, even if the channel counterparty consents. This
limitation can be remediated by introducing a protocol to renegotiate these
parameters.
Notable in particular, is that one of the channel parameters we wish to
renegotiate is the the channel_type
itself. With the advent of Simple Taproot
Channels (STCs), we have the opportunity to take advantage of the cost savings
and privacy capabilities afforded by the 2021 Taproot Soft Fork, with further
aspirations to be able to deploy Point Time-Lock Contracts (PTLCs) to the
Lightning Network. The sooner that network participants can upgrade to STCs the
more we will have the necessary network infrastructure to be able to make
effective use of PTLCs when the protocols for them are ready for deployment.
Due to the design of STCs, and the fact that they take full advantage of the
capabilities afforded by Schnorr Signatures, there is no way to construct a
valid channel_announcement
message that references the output corresponding to
the nodes' joint public key. As such, even if we were to directly spend an
existing funding output to a new STC funding output, and even with the provision
in BOLT 7 to delay graph pruning by 12 blocks after the channel point is spent,
we have no way of advertising the STC to the network at the time of writing of
this proposal.
That said, there is a development effort, concurrent with this, for a new gossip system that is capable of understanding the announcements of new STCs. However, even with a new gossip system capable of understanding the STC construction and announcement, it will take quite some time for such a system to be broadly deployed across the Lightning Network. In the interim, to combat the disincentive of upgrading to STCs, this proposal to enable the change of these channel parameters (including channel types) without requiring channel turnover is submitted.
This proposal includes a detailed section on the preliminaries to document some of the rationale for the design that is presented later. If you are a Bitcoin and Lightning Network protocol expert or you are uninterested in the thought process behind what is presented here, you may wish to skip to the Design Overview section to save time.
As described in BOLT 2, during the channel opening procedure there are a number
of parameters specified in the open_channel
and accept_channel
messages that
remain static over the lifetime of the channel. A subset of these are updatable
using other messages defined in the protocol. However, after accounting for the
channel parameters that can be changed using existing mechanisms, there remains
a list of parameters which are unchangeable. We list these below:
- dust_limit_satoshis
- max_htlc_value_in_flight_msat
- channel_reserve_satoshis
- to_self_delay
- max_accepted_htlcs
- funding_pubkey
- revocation_basepoint
- payment_basepoint
- delayed_payment_basepoint
- htlc_basepoint
- first_per_commitment_point
- channel_flags
- upfront_shutdown
- channel_type
After some analysis during the development of this proposal, we determined that the basepoint values don't make sense to rotate. There is no obvious value in doing so and it carries additional administrative costs to rotate them. Finally, changing the upfront_shutdown script over the lifetime of the channel is self-defeating and so we exclude it as well. The list of channel parameters remaining after we filter out these values is thus:
- dust_limit_satoshis
- max_htlc_value_in_flight_msat
- channel_reserve_satoshis
- to_self_delay
- max_accepted_htlcs
- funding_pubkey
- channel_type
The design presented here is intended to allow for arbitrary changes to these values that currently have no facilities for change in any other way.
It is at this point that we need to take a brief detour and review how the broader Lightning Network comes to discover and verify the existence of public channels. When the funding transaction for a channel has confirmed, the participating parties will jointly produce a message that attests to their ownership of the UTXO and its viability as a routing edge for payment senders.
BOLT 7 details all of the specifics of this message and how it is computed but one of the notable aspects of this process is that the receivers of these gossip messages verify that the UTXO underwriting the channel must be a P2WSH output with a pre-defined script using the participants' public keys, specified in BOLT 3. This will present issues for us which will become clearer in the next section.
While alternative gossip systems that can describe STCs are being designed, they have not been deployed in any known implementation of the Lightning Network Protocol and even when such a design is implemented, there will be a prolonged period of time wherein a substantial number of nodes on the network will remain unable to process messages of this variety, rendering useless any channels that can only be announced in this manner.
This brings us to talking about what channel constructions are actually inexpressible by the existing gossip system. As we alluded to earlier, Taproot channels cannot be discovered using the existing gossip message structure and interpretation.
In November of 2021 the "Taproot" upgrade was activated on Bitcoin's mainnet, creating a new output type that is subsequently useful to higher layer protocols such as the Lightning Network. Since then, the Lightning Network protocol designers have offered a proposal for a channel construction that makes use of the Taproot output type. It is beyond the scope of this document to make a thorough case for why such a channel construction is useful but we assume that it is for our purposes here.
While Taproot channels are useful, they present some novel challenges with respect to network-wide interoperability. Notably, a useful Taproot channel construction must by definition make use of the new Taproot output type, which does not and cannot use the output script format detailed in BOLT 3 for the funding output. Pairing this fact with what we described in the previous section, it is necessarily the case that the funding output of a Taproot channel cannot be properly announced by the current gossip system.
The main goal of this proposal is to be able to change all of the historically "static" channel parameters, including the channel type, which includes channels built off of output types that our gossip system currently doesn't understand, in a world where we are trying to preserve the channel identity of the original channel. This is a tall order.
Most of these parameters can be changed by simply expressing the desire to change them, and should the recipient agree, we apply these changes, and exchange new commitment transactions making any necessary adjustments prescribed by the channel parameter changes.
The exception to this is certain changes to the channel type. As detailed in the preliminaries, the funding output of a Taproot transaction is fundamentally different from the funding output of the other channel types that are currently defined. This means that we must spend the funding output of the original channel into a new Taproot output before we have a functioning Taproot channel.
The key insight in this design is that we extend the concept of a commitment transaction to include the possibility of a pair of transactions wherein we have a "kickoff transaction" that is comprised of a single input (the original funding output) and a single output (the new funding output) and then building the new commitment transaction off of the new funding output in whatever manner is detailed in the specification for the target channel type. This may not always be necessary, but it is certainly necessary for using this proposal to convert existing channels into Taproot channels.
There are two phases to this channel upgrade process: proposal, and execution. During the proposal phase the only goal is to agree on a set of updates to the current channel state machine. Assuming an agreement can be reached, we will proceed to the execution phase. During the execution phase, we apply the updates to the channel state machine, exchanging the necessary information to be able to apply those updates.
As a prerequisite to the proposal phase of a Dynamic Commitment negotiation, the channel must be in a quiesced state.
In every dynamic commitment negotiation, there are two roles: the initiator
and the responder
. It is necessary for both nodes to agree on which node is
the initiator
and which node is the responder
. This is important because if
the dynamic commitment negotiation results in a re-anchoring step (described
later), it is the initiator
that is responsible for paying the fees for the
kickoff transaction.
The following TLVs are used throughout the negotiation phase of the protocol and are common to all messages in the negotiation phase.
- type: 0
data:
- [
u64
:dust_limit_satoshis
]
- [
- type: 1
data:
- [
u64
:senders_max_htlc_value_in_flight_msat
]
- [
- type: 2
data:
- [
u64
:recipients_channel_reserve_satoshis
]
- [
- type: 3
data:
- [
u16
:recipients_to_self_delay
]
- [
- type: 4
data:
- [
u16
:senders_max_accepted_htlcs
]
- [
- type: 5
data:
- [
point
:senders_funding_pubkey
]
- [
- type: 6
data:
- [
...*byte
:channel_type
]
- [
- type: 7
data:
- [
u32
:kickoff_feerate_per_kw
]
- [
Three new messages are introduced that are common to all dynamic commitment flows. They let each channel party propose which channel parameters they wish to change as well as accept or reject the proposal made by their counterparty.
This message is sent to negotiate the parameters of a dynamic commitment
upgrade. The overall protocol flow looks similar to what is depicted below.
This message is always sent by the initiator
and the responder
.
+-------+ +-------+
| |--(1)---- dyn_propose -------->| |
| | | |
| |<-(2)---- dyn_propose ---------| |
| A | | B |
| |--(3)------ dyn_ack ---------->| |
| | | |
| |<-(4)------ dyn_ack -----------| |
+-------+ +-------+
-
type: 111 (
dyn_propose
) -
data:
- [
32*byte
:channel_id
] - [
u8
:initiator
] - [
dyn_propose_tlvs
:tlvs
]
- [
-
tlv_stream
:dyn_propose_tlvs
-
types:
- type: 0 (
dust_limit_satoshis
) - data:
- [
u64
:dust_limit_satoshis
]
- [
- type: 1 (
max_htlc_value_in_flight_msat
) - data:
- [
u64
:senders_max_htlc_value_in_flight_msat
]
- [
- type: 2 (
channel_reserve_satoshis
) - data:
- [
u64
:recipients_channel_reserve_satoshis
]
- [
- type: 3 (
to_self_delay
) - data:
- [
u16
:recipients_to_self_delay
]
- [
- type: 4 (
max_accepted_htlcs
) - data:
- [
u16
:senders_max_accepted_htlcs
]
- [
- type: 5 (
funding_pubkey
) - data:
- [
point
:senders_funding_pubkey
]
- [
- type: 6 (
channel_type
) - data:
- [
...*byte
:channel_type
]
- [
- type: 7 (
kickoff_feerate
) - data:
- [
u32
:kickoff_feerate_per_kw
]
- [
- type: 0 (
TODO: handle edge case where both nodes send dyn_propose
as initiator
The sending node:
- MUST set
channel_id
to an existing one it has with the recipient. - MUST NOT send a set of TLV parameters that would violate the requirements of the identically named parameters in BOLT 2 or associated extensions.
- MUST remember its last sent
dyn_propose
parameters. - if it is currently waiting for a response (
dyn_ack
ordyn_reject
):- MUST NOT send another
dyn_propose
- SHOULD close the connection if it exceeds an acceptable time frame.
- MUST NOT send another
- if it is the
initiator
:- MUST set
initiator
to 1 - if it sets
channel_type
and thechannel_type
conversion requires re-anchoring (see appendix for conversions that require re-anchoring)- MUST set
kickoff_feerate
- MUST set
- MUST set
- if it is the
responder
:- MUST set
initiator
to 0 - MUST set the
channel_type
TLV to the same value as the one sent by theinitiator
- MUST NOT set the
kickoff_feerate
TLV - MUST NOT send a set of TLV parameters that would violate the requirements
of the identically named parameters in BOLT 2 assuming the acceptance
of the parameters it received in the
initiator
'sdyn_propose
message.
- MUST set
The receiving node:
- if
channel_id
does not match an existing channel it has with the sender:- SHOULD send an
error
and close the connection.
- SHOULD send an
- if it wishes to update additional parameters as part of the same dynamic
commitment negotiation AND has not yet sent a
dyn_ack
message:- MUST send a
dyn_propose
with its desired parameters - MUST NOT send a
dyn_propose
after adyn_ack
for the same negotiation - MUST send a
dyn_ack
to accept the parameters it was sent - MUST NOT send a
dyn_reject
- MUST send a
NOTE FOR REVIEWERS: These messages all interact with each other, so feedback is welcome for how to restructure this section so that the invariants it prescribes are found in the most intuitive place.
The set of parameters used in this message to renegotiate channel parameters can't violate the invariants set out in BOLT 2. This is because we are simply trying change channel parameters without a close event. BOLT 2 specifies constraints on these parameters to make sure they are internally consistent and secure in all contexts.
Since the initiator
is the one that is responsible for paying the fees for the
kickoff transaction if it is required (like for certain channel_type
changes),
it follows that the responder
cannot change the channel_type
. Since the
kickoff_feerate
paid by the initiator
, it should be set only if the sender
is the initiator
.
The requirement for a node to remember what it last sent and for it to remember what it accepted is necessary to recover on reestablish. See the reestablish section for more details.
This message is sent in response to a dyn_propose
indicating that it has
accepted the proposal.
- type: 113 (
dyn_ack
) - data:
- [
32*byte
:channel_id
]
- [
The sending node:
- MUST set
channel_id
to a valid channel it has with the recipient. - MUST NOT send this message if it has not received a
dyn_propose
- MUST NOT send this message if it has already sent a
dyn_ack
for the current negotiation. - MUST NOT send this message if it has already sent a
dyn_reject
for the current negotiation. - MUST remember the parameters of
dyn_propose
message to which thedyn_ack
is responding for the nextpropose_height
. - MUST remember the local and remote commitment heights for the next
propose_height
.
The receiving node:
- if
channel_id
does not match an existing channel it has with the peer:- MUST send an
error
and close the connection.
- MUST send an
- if there isn't an outstanding
dyn_propose
it has sent:- MUST send an
error
and fail the channel.
- MUST send an
A node:
- once it has both sent and received
dyn_ack
- MUST increment its
propose_height
.
- MUST increment its
The propose_height
starts at 0 for a channel and is incremented by 1 every
time the dynamic commitment proposal phase completes for a channel. See the
reestablish section for why this is needed.
This message is sent in response to a dyn_propose
indicating that it rejects
the proposal.
- type: 115 (
dyn_reject
) - data:
- [
32*byte
:channel_id
] - [
...*byte
:update_rejections
]
- [
The sending node:
- MUST set
channel_id
to a valid channel it has with the recipient. - MUST NOT send this message if it has not received a
dyn_propose
- MUST NOT send this message if it has already sent a
dyn_ack
for the current negotiation. - MUST NOT send this message if it has already sent a
dyn_reject
for the current negotiation. - if it will not accept any dynamic commitment negotiation:
- SHOULD send a
dyn_reject
with zero value forupdate_rejections
- SHOULD send a
- if it does not agree with one or more parameters:
- MUST send a
dyn_reject
with the bit index (using the same layout as feature negotiation) set corresponding to the TLV type number.- Example: an objection to the
dust_limit
would be encoded as 0b00000001, an objection tomax_value_in_flight
would be encoded as 0b00000010, and an objection to both would be encoded as 0b00000011.
- Example: an objection to the
- MUST send a
- if it has sent a
dyn_propose
in the current negotiation- MUST forget its last sent
dyn_propose
parameters
- MUST forget its last sent
- MUST forget the parameters of the
dyn_propose
message to which thedyn_reject
is responding.
The receiving node:
- if
channel_id
does not match an existing channel it has with the peer- MUST close the connection
- if there isn't an outstanding
dyn_propose
it has sent- MUST send an
error
and fail the channel
- MUST send an
- MUST forget its last sent
dyn_propose
parameters. - if the
update_rejections
is a zero value- SHOULD NOT re-attempt another dynamic commitment negotation for the remaining lifecycle of the connection
- if the
update_rejections
is a non-zero value:- MAY re-attempt another dynamic commitment negotiation
- if a dynamic commitment negotiation is re-attempted:
- SHOULD relax the parameters whose TLV types match the bits that were set
in the
update_rejections
value. - if no sensible interpretation of "relax" exists:
- SHOULD NOT re-attempt a dynamic commitment negotiation with this parameter set.
- SHOULD relax the parameters whose TLV types match the bits that were set
in the
By sending back the TLVs that a node explicitly rejects makes it easier to come
to an agreement on a proposal that will work. By not sending back any TLVs in
the dyn_reject
, a node signals it is not interested in moving the negotiation
forward at all and further negotiation should not be attempted.
A new TLV that denotes the node's current propose_height
is included.
tlv_stream
:channel_reestablish_tlvs
- types:
- type: 20 (
dyn_height
) - data:
- [
u64
:dyn_height
]
- [
- type: 20 (
The sending node:
- MUST set
dyn_height
to the number of dynamic commitment negotiations it has completed. The point at which it is incremented is described in thedyn_ack
section.
The receiving node:
- if the received
dyn_height
equals its owndyn_height
:- MUST forget any stored proposal state for
propose_height
+1 in case negotiation didn't complete. Can continue using the channel. - SHOULD forget any state that is unnecessary for heights <=
propose_height
.
- MUST forget any stored proposal state for
- if the received
dyn_height
is 1 greater than its owndyn_height
:- if it does not have any remote parameters stored for the received
dyn_height
:- MUST send an
error
and fail the channel. The remote node is either lying about thedyn_height
or the recipient has lost data since its not possible to advance the height without the recipient storing the remote's parameters.
- MUST send an
- resume using the channel with its last-sent
dyn_propose
and the storeddyn_propose
parameters and increment itspropose_height
.
- if it does not have any remote parameters stored for the received
- if the received
dyn_height
is 1 less than its owndyn_height
:- resume using the channel with the new parameters.
- else:
- MUST send an
error
and fail the channel. State was lost.
- MUST send an
If both sides have sent and received dyn_ack
before the connection closed, it
is simple to continue. If one side has sent and received dyn_ack
the other
side has only sent dyn_ack
, the flow is recoverable on reconnection as the
side that hasn't received dyn_ack
knows that the other side accepted their
last sent dyn_propose
based on the dyn_height
in the reestablish
message.
There are three fundamental types of execution paths:
- Rules Change - No additional state change is required, the next state will be expected to follow the new rules that have been negotiated. Channel may resume normal operation.
- Commitment Update - Additional state change is required, a new commitment
transaction is expected to be exchanged at this point, following the expected
parameters. Nodes will exchange
commitment_signed
andrevoke_and_ack
s for transactions agreeing to the new rules, then Channel may resume normal operation. - Funding Output Update - Signatures for a transaction that spends the original funding output into a new funding output will be exchanged.
- NOTE FOR REVIEWERS: This transaction is currently symmetric which burdens us with the constraint that a reanchoring step can only be done once over the lifetime of the channel. If we want to be able to securely do this multiple times, we must make kickoff transactions revocable, and therefore asymmetric, and therefore must start issuing commitment signatures in pairs. See Appendix for details.
For execution we try and have the smallest execution overhead. The option that is selected from the list above will be the one with the highest number that is triggered by the below rules: Funding Output Update > Commitment Update > Rules Change.
- If either channel party changes
dust_limit_satoshis
: Commitment Update - If either channel party changes
max_htlc_value_in_flight_msat
: Rules Change - If either channel party changes
channel_reserve_satoshis
: Rules Change - If either channel party changes
to_self_delay
: Commitment Update - If either channel party changes
max_accepted_htlcs
: Rules Change - If either channel party changes
funding_pubkey
: Funding Output Update - If new
channel_type
requires different funding output script than the oldchannel_type
: Funding Output Update
If all that is required to execute the terms of the dynamic commitment negotiation is a rules change, then channel operation may resume as normal under the new rules. It is possible that the current channel state would violate the constraints specified by the new rules. When we execute a rules change, only new channel states will be evaluated against the new rules. As long as the new channel state moves towards the constraint boundary, it will be accepted.
If an update to the commitment transaction is required to execute the terms of
the dynamic commitment negotiation, then once both channel parties have
irrevocably committed to a state with no HTLC outputs, new commitment signatures
MUST be exchanged. This requires both parties to send commitment_signed
messages that adhere to the new channel parameters. Once a node has received
a commitment_signed
in accordance with the new channel parameters, it MUST
issue a revoke_and_ack
as it normally would. Once both nodes have done this,
normal channel operation is resumed.
If a Funding Output Change is required, then once both channel parties have irrevocably committed to a state with no HTLC outputs, new commitment signatures AND kickoff signatures MUST be exchanged. To accomplish this the following steps are taken:
- Build kickoff transaction
- Build commitment transaction that spends kickoff output
- Issue a
commitment_signed
message according to new channel parameters - Upon receipt of the remote party's
commitment_signed
message, issue akickoff_sig
message. - Upon receipt of the remote party's
kickoff_sig
message, issue arevoke_and_ack
for the final commitment built off of the original funding output.
+-------+ +-------+
| |--(1)---- commit_signed------->| |
| | | |
| |<-(2)---- commit_signed -------| |
| | | |
| | | |
| |<-(3)----- kickoff_sig --------| |
| A | | B |
| |--(4)----- kickoff_sig ------->| |
| | | |
| | | |
| |--(5)---- revoke_and_ack ----->| |
| | | |
| |<-(6)---- revoke_and_ack ------| |
+-------+ +-------+
The commitment signed message has to be issued first to ensure that the money
locked to the new funding output (created by the kickoff transaction) can be
unilaterally recovered. If the kickoff_sig
were sent first, the receiver could
stop responding and broadcast the kickoff transaction, burning the funds for
both parties. If the channel balance is overwhelmingly imbalanced towards the
side issuing the kickoff_sig
, this could be costly to the victim while being
comparatively cheap for the attacker.
Similarly, if we revoke_and_ack
prior to receiving a kickoff_sig
then we
may have a situation where we remove our ability to broadcast the old commitment
transaction before the path to the new commitment transaction has been fully
signed.
- version: 2
- locktime: 0
- txin count: 1
txin[0]
outpoint:txid
andoutput_index
fromfunding_created
messagetxin[0]
sequence: 0xfffffffdtxin[0]
script bytes: 0txin[0]
witness:0 <signature_for_pubkey1> <signature_for_pubkey2>
- txout count: 3
txout[0]
:anchor_output_1
oranchor_output_2
txout[1]
:anchor_output_1
oranchor_output_2
txout[2]
:p2tr_funding_output
The anchor outputs have a value of 330 satoshis. They are encumbered by a version 1 witness script:
OP_1 anchor_output_key
- where:
anchor_internal_key = original_local_funding_pubkey/original_remote_funding_pubkey
anchor_output_key = anchor_internal_key + tagged_hash("TapTweak", anchor_internal_key || anchor_script_root)
anchor_script_root = tapscript_root([anchor_script])
anchor_script
:OP_16 OP_CHECKSEQUENCEVERIFY
The new funding output has a value of the original funding output minus the sum
of 660 satoshis and this kickoff transaction's fee. It is encumbered by a
version 1 witness script where taproot_funding_key1/taproot_funding_key2
are
from dyn_ack
:
OP_1 funding_key
- where:
funding_key = combined_funding_key + tagged_hash("TapTweak", combined_funding_key)*G
combined_funding_key = musig2.KeyAgg(musig2.KeySort(taproot_funding_key1, taproot_funding_key2))
- Initialize the commitment transaction version and locktime.
- Initialize the commitment transaction input.
- Calculate this kickoff transaction's fee via
kickoff_feerate_per_kw
*kickoff_transaction_weight
/1000, making sure to round down. Subtract this value from the new funding output. - Subtract two times the fixed anchor size of 330 satoshis from the new funding output.
- Add a funding output with the new funding amount.
- Add an anchor output for each party.
- Sort the outputs into BIP 69+CLTV order.
- version: 2
- locktime: upper 8 bits are 0x20, lower 24 bits are the lower 24 bits of the obscured commitment number
- txin count: 1
txin[0]
outpoint: the matching kickoff transaction's funding outpoint.txin[0]
sequence: upper 8 bits are 0x80, lower 24 bits are upper 24 bits of the obscured commitment numbertxin[0]
script bytes: 0txin[0]
witness:<key_path_sig>
The 48-bit commitment number is computed by XOR
as described in BOLT#03.
- Initialize the commitment transaction version and locktime.
- Initialize the commitment transaction input.
- Calculate which committed HTLCs need to be trimmed.
- Calculate the commitment transaction fee via
commitment feerate *
commitment_transaction_weight
/1000, making sure to round down. Subtract this from the funder's output. - Subtract four times the fixed anchor size of 330 satoshis from the funder's output. Two of the anchors are from the commitment transaction and two are from the kickoff transaction.
- Subtract the matching kickoff transaction's fee from the funder's output.
- For every offered HTLC, if it is not trimmed, add an offered HTLC output.
- For every received HTLC, if it is not trimmed, add a received HTLC output.
- If the
to_local
output is greater or equal to the dust limit, add ato_local
output. - If the
to_remote
output is greater or equal to the dust limit, add ato_remote
output. - If
to_local
exists or there are untrimmed HTLCs, add ato_local_anchor
. - If
to_remote
exists or there are untrimmed HTLCs, add ato_remote_anchor
. Theto_remote_anchor
uses the remote party'staproot_funding_key
. - Sort the outputs into BIP 69+CLTV order.
Commitment signed messages are exchanged as normal with the exception of a
different construction procedure detailed in the prior step. NOTE: "as normal"
means that this message MUST include all TLVs that would be required for
the updated channel_type
e.g. Musig2 Taproot.
The kickoff_sig is a message containing a signature that the fundee sends to the funder who then combines it with their own signature to spend from the original funding outpoint into the new musig2 output. To keep things simple, no additional inputs are added to the intermediate transaction. An anchor output is attached to either side for fee-bumping.
- type: 777 (
kickoff_sig
) - data:
- [
32*byte
:channel_id
] - [
signature
:signature
]
- [
The sending node:
- MUST set
channel_id
to a valid channel it has with the recipient. - MUST NOT send this message before receiving the peer's
commitment_signed
.
The receiving node:
- MUST send an
error
and fail the channel ifchannel_id
does not match an existing channel it has with the sender. - MUST send an
error
and fail the channel ifsignature
is not valid for the kickoff transaction as constructed above OR non-compliant with the LOW-S-standard rule. LOWS - MUST NOT send a
revoke_and_ack
for the final pre-dynamic commitment transaction until it has received a validkickoff_sig
The kickoff_sig
cannot be issued until the commitment_signed
message has
been received to prevent griefing by broadcasting a kickoff for which there is
no exit. The revoke_and_ack
for the last pre-dynamic commitment has to wait
for the kickoff_sig
because if the last commitment built off of the original
funding output is revoked before the kickoff_sig
has been received, then if
a peer becomes non-cooperative from that point forward, funds are effectively
burned.
This section describes how dynamic commitments can upgrade regular channels to
simple taproot channels. The regular dynamic proposal phase is executed followed
by a signing phase. A channel_type
of option_taproot
will be included in
dyn_propose
and both sides must agree on it. The initiator
of the upgrade
will also propose a feerate to use for an intermediate "kickoff" transaction.
tlv_stream
:dyn_propose_tlvs
- types:
- type: 5 (
funding_pubkey
) - data:
- [
point
:senders_funding_pubkey
]
- [
- type: 6 (
channel_type
) - data:
- [
...*byte
:type
]
- [
- type: 7 (
kickoff_feerate
) - data:
- [
u32
:kickoff_feerate_per_kw
]
- [
- type: 5 (
The sending node:
- if it is the
initiator
:- MUST only send
kickoff_feerate
if they can pay for the kickoff transaction fee and the anchor outputs, while adhering to thechannel_reserve
restriction.
- MUST only send
- MUST set
taproot_funding_key
to a valid secp256k1 compressed public key. - SHOULD use a sufficiently high
kickoff_feerate
to be prepared for worst-case fee environment scenarios.- NOTE FOR REVIEWERS: We can also add a message to update the kickoff fee
rate if we have revocable kickoffs, similar to
update_fee
for commitment transactions to make sure the kickoff has a sufficient fee to enter the mempool. Anchors can be used to fee bump the kickoff beyond the min mempool fee. Revocable kickoffs are possible but significantly increase the design complexity.
- NOTE FOR REVIEWERS: We can also add a message to update the kickoff fee
rate if we have revocable kickoffs, similar to
The receiving node:
- if it is the
responder
:- MUST reject the
dyn_propose
if theinitiator
cannot pay for the kickoff transaction fee and the anchor outputs. - MUST reject the
dyn_propose
if, after calculating the amount of the new funding output, the new commmitment transaction would not be able to pay for any outputs at the current commitment feerate.
- MUST reject the
- MUST reject the
dyn_propose
iftaproot_funding_key
is not a valid secp256k1 compressed public key. - MAY reject the
dyn_propose
if it does not agree with thechannel_type
The dyn_propose
renegotiates the funding keys as otherwise signatures for the
funding keys would be exchanged in both the ECDSA and Schnorr contexts. This can
lead to an attack outlined in BIP340.
Renegotiating funding keys avoids this issue. Note that the various basepoints
exchanged in open_channel
and accept_channel
are not renegotiated. Because
the private keys change with each commitment transaction they sign due to the
per_commitment_point
construction, the basepoints can be used in both ECDSA
and Schnorr contexts.
tlv_stream
:dyn_ack_tlvs
- types:
- type: 0 (
local_musig2_pubnonce
) - data:
- [
66*byte
:nonces
]
- [
- type: 0 (
The sending node:
- if it is accepting a
channel_type
ofsimple_taproot_channel
:- MUST set
local_musig2_pubnonce
to the nonce that it will use to verify local commitments.
- MUST set
The receiving node:
- MUST send an
error
and fail the channel iflocal_musig2_pubnonce
cannot be parsed as two compressed secp256k1 points.
Originally, Bitcoin Core's default mempool settings allowed an unconfirmed transaction to have up to 25 decendants in the mempool. Past this limit, any descendants would be rejected. This was used as a DoS mitigation in Bitcoin Core and affected the security of LN channels. Before the anchors commitment type was introduced, pinning in the LN was where a counterparty broadcasted the commitment transaction and created a chain of 25 descendants spending from one of the commitment's outputs. The time-sensitive commitment transaction could be "pinned" to the bottom of the mempool. This was addressed with a change to Bitcoin Core called CPFP Carve-out.
CPFP Carve-out was introduced to Bitcoin Core in bitcoin/bitcoin#15681. If a Bitcoin node receives a transaction that is rejected due to any of the mempool size or ancestor/descendant restrictions being hit, it will try to accept the transaction again. This second try will succeed only if:
- the transaction is 40kWU or less
- it has only one ancestor in the mempool
This change, in conjunction with the anchor commitment type, decreases the efficacy of the pinning attack since the honest party can still attach an anchor despite the descendant size limit being hit.
The safety guarantees of CPFP Carve-out break due to the structure of the kickoff transaction. The kickoff transaction contains 3 spendable outputs: the local party's anchor, the remote party's anchor, and the new funding output. All three of these outputs can be spent immediately. A malicious counterparty can pin the kickoff transaction by:
- spending from their anchor output to create a descendant chain of 25 transactions AND
- spending from the new funding output using the new commitment transaction, "using up" the CPFP Carve-out slot designated for the honest party. NOTE FOR REVIEWERS: The semantics of CPFP carve-out are not entirely clear as to whether or not there is only one CPFP-Carve-Out "slot" or if the only two requirements are the 40kWU limit and a single unconfirmed ancestor. If we have more than one "slot" available, this is no longer a concern.
Depending on fee conditions, it may not be possible for the honest party to get these transactions confirmed until the mempool clears up.
If we were to get rid of the kickoff transaction's anchor outputs, the problem still arises. A malicious counterparty could still pin the kickoff transaction by:
- broadcasting the commitment transaction
- spending from their commitment anchor output and creating a descendant chain of 25 transactions
The honest party is unable to use their anchor on the commitment transaction as:
- the descendant limit of 25 transactions has been hit
- the anchor spend would have 2 ancestors (the commitment and kickoff transactions)
The above pinning scenarios highlight the complexity of second-layer protocols and mempool restrictions. In this proposal, pinning is still possible, but risk can be controlled if nodes reduce their max_htlc_value_in_flight_msat values while the kickoff transaction is unconfirmed
If we allow adding HTLCs before the kickoff transaction confirmed on-chain, the pinning attack has a tangible benefit: the ability to steal the value of an HTLC.
The above proposal specifies a process wherein we can reanchor the funding output exactly once. This is because even if we revoke all commitment transactions built off of the first kickoff transaction, we still are vulnerable to griefing if we do not revoke the kickoff transaction itself. In this case one party may choose to burn all funds in a channel by broadcasting the kickoff transaction when no unrevoked commitment transactions remain. To deal with this we can either only reanchor once, as proposed above, allowing us to guarantee we will never encounter a situation where there are no valid commitment transactions, or we can make the kickoff transactions revocable.
To make them revocable we can reuse the same scheme that we use for commitment transactions. In this case Alice's kickoff transaction would allow Bob to claim all funds if Bob knows Alice's revocation secret. Similarly, Alice could claim all channel funds if Bob broadcasts his kickoff transaction and Alice knows Bob's revocation secret.
An unfortunate consequence of this scheme is that since we now have two possible "new" funding outputs (one for each of the potential kickoff transactions), we now have to send all of our commitment signatures in pairs. At any given time there would be four valid commitment transactions:
- Alice's commitment built off of Alice's kickoff
- Alice's commitment built off of Bob's kickoff
- Bob's commitment built off of Alice's kickoff
- Bob's commitment built off of Bob's kickoff
NOTE FOR REVIEWERS: There may be an opportunity to make the kickoff transactions symmetric while still allowing them to be revocable using adaptor signature tricks, but this will require more research from those with a deeper understanding of the cryptographic primitives.
Since DER-encoded signatures vary in size, we assume a worst-case signature size of 73 bytes to keep things simple. The kickoff transaction has an expected weight of 944WU and the commitment transaction has an expected weight of 960WU.
General weights:
-
p2tr: 34 bytes
- OP_1: 1 byte
- OP_DATA: 1 byte (witness_script_SHA256 length)
- witness_script_SHA256: 32 bytes
-
witness_header: 2 bytes
- flag: 1 byte
- marker: 1 byte
-
funding_output_script: 71 bytes
- OP_2: 1 byte
- OP_DATA: 1 byte (pub_key_alice length)
- pub_key_alice: 33 bytes
- OP_DATA: 1 byte (pub_key_bob length)
- pub_key_bob: 33 bytes
- OP_2: 1 byte
- OP_CHECKMULTISIG: 1 byte
-
funding_input_witness: 222 bytes
- number_of_witness_elements: 1 byte
- nil_length: 1 byte
- sig_alice_length: 1 byte
- sig_alice: 73 bytes
- sig_bob_length: 1 byte
- sig_bob: 73 bytes
- witness_script_length: 1 byte
- witness_script: 71 bytes (funding_output_script)
-
kickoff_txin_0: 41 bytes (excl. witness)
- previous_out_point: 36 bytes
- hash: 32 bytes
- index: 4 bytes
- var_int: 1 byte (script_sig length)
- script_sig: 0 bytes
- witness: <---- part of the witness data
- sequence: 4 bytes
- previous_out_point: 36 bytes
-
musig2_funding_output: 43 bytes
- value: 8 bytes
- var_int: 1 byte (pk_script length)
- pk_script (p2tr): 34 bytes
-
anchor_output: 43 bytes
- value: 8 bytes
- var_int: 1 byte (pk_script length)
- pk_script (p2tr): 34 bytes
-
kickoff_transaction: 180 bytes (excl. witness)
- version: 4 bytes
- witness_header: <---- part of the witness data
- count_tx_in: 1 byte
- tx_in: 41 bytes
- kickoff_txin_0: 41 bytes
- count_tx_out: 1 byte
- tx_out: 129 bytes
- musig2_funding_output: 43 bytes
- anchor_output_local: 43 bytes
- anchor_output_remote: 43 bytes
- lock_time: 4 bytes
- Multiplying non-witness data by 4 gives a weight of:
- kickoff_transaction_weight = 180vbytes * 4 = 720WU
- Adding the witness data:
- kickoff_transaction_weight += (funding_input_witness + witness_header)
- kickoff_transaction_weight = 944WU
Here we assume that both parties have an output on the commitment transaction. This is to keep the weight consistent across potentially different commitment transactions.
-
musig2_funding_input_witness: 66 bytes
- number_of_witness_elements: 1 byte
- musig2_signature_length: 1 byte
- musig2_signature: 64 bytes
-
commitment_txin_0: 41 bytes (excl. witness)
- previous_out_point: 36 bytes
- hash: 32 bytes
- index: 4 bytes
- var_int: 1 byte (script_sig length)
- script_sig: 0 bytes
- witness: <---- part of the witness data
- sequence: 4 bytes
- previous_out_point: 36 bytes
-
to_local: 43 bytes
- value: 8 bytes
- var_int: 1 byte (pk_script length)
- pk_script (p2tr): 34 bytes
-
to_remote: 43 bytes
- value: 8 bytes
- var_int: 1 byte (pk_script length)
- pk_script (p2tr): 34 bytes
-
to_local_anchor: 43 bytes
- value: 8 bytes
- var_int: 1 byte (pk_script length)
- pk_script (p2tr): 34 bytes
-
to_remote_anchor: 43 bytes
- value: 8 bytes
- var_int: 1 byte (pk_script length)
- pk_script (p2tr): 34 bytes
-
commitment_transaction: 225 bytes (excl. witness)
- version: 4 bytes
- witness_header: <---- part of the witness data
- count_tx_in: 1 byte
- tx_in: 41 bytes
- commitment_txin_0: 41 bytes
- count_tx_out: 3 byte
- tx_out: 172 bytes
- to_local: 43 bytes
- to_remote: 43 bytes
- to_local_anchor: 43 bytes
- to_remote_anchor: 43 bytes
- lock_time: 4 bytes
- Multiplying non-witness data by 4 gives a weight of:
- commitment_transaction_weight = 223vbytes * 4 = 892WU
- Adding the witness data:
- commitment_transaction_weight += (musig2_funding_input_witness + witness_header)
- commitment_transaction_weight = 960WU