Skip to content

Latest commit

 

History

History
833 lines (675 loc) · 34.9 KB

zzz-dynamic-commit.md

File metadata and controls

833 lines (675 loc) · 34.9 KB

Extension Bolt ZZZ: Dynamic Commitments

Authors:

Created: 2022-06-24

Table of Contents

Introduction

TODO

Specification

Proposal Messages

Three new messages are introduced that are common to all dynamic commitment flows. They let each side propose what they want to change about the channel.

dyn_begin_propose

This message is sent when a node wants to begin the dynamic commitment negotiation process. This is a signaling message, similar to shutdown in the cooperative close flow.

  1. type: 111 (dyn_begin_propose)
  2. data:
    • [32*byte:channel_id]
    • [byte: begin_propose_flags]

Only the least-significant-bit of begin_propose_flags is defined, the reject bit.

Requirements

The sending node:

  • MUST set channel_id to a valid channel they have with the
    recipient.
  • MUST set undefined bits in begin_propose_flags to 0.
  • MUST set the reject bit in begin_propose_flags if they are rejecting the dynamic commitment negotiation request.
  • MUST NOT send update_add_htlc messages after sending this unless one of the following is true:
    • dynamic commitment negotiation has finished
    • a dyn_begin_propose with the reject bit has been received.
    • a reconnection has occurred.
  • MUST only send one dyn_begin_propose during a single negotiation.
  • MUST fail to forward additional incoming HTLCs from the peer.

The receiving node:

  • if channel_id does not match an existing channel it has with the peer:
    • MUST close the connection.
  • if the reject bit is set, but it hasn't sent a dyn_begin_propose:
    • MUST send an error and fail the channel.
  • if an update_add_htlc is received after this point and negotiation hasn't finished or terminated:
    • MUST send an error and fail the channel.
  • if it has not already sent dyn_begin_propose in this round of negotiation:
    • MUST reply with dyn_begin_propose either rejecting or accepting the negotiation request.

Rationale

TODO: flow diagram

This has similar semantics to the shutdown message where the channel comes to a state where updates may only be removed. The reject bit is necessary to avoid having to reconnect in order to have a useable channel state again.

dyn_propose

This message is sent when neither side owes the other either a revoke_and_ack or commitment_signed message and each side's commitment has no HTLCs. For now, only the recipients_new_self_delay parameter is defined in negotiation. After negotiation completes, commitment signatures will use these parameters. The overall message flow looks like this:

    +-------+                               +-------+
    |       |--(1)----- dyn_propose ------->|       |
    |       |                               |       |
    |       |<-(2)----- dyn_propose --------|       |
    |   A   |--(3)--- dyn_propose_reply --->|   B   |
    |       |                               |       |
    |       |<-(4)--- dyn_propose_reply ----|       |
    +-------+                               +-------+
  1. type: 113 (dyn_propose)

  2. data:

    • [32*byte:channel_id]
    • [dyn_propose_tlvs:tlvs]
  3. tlv_stream: dyn_propose_tlvs

  4. types:

    1. type: 0 (to_self_delay)
    2. data:
      • [u16:recipients_new_self_delay]

Requirements

The sending node:

  • MUST set channel_id to an existing one it has with the recipient.
  • MUST NOT send a dyn_propose if a prior one is waiting for dyn_propose_reply.
  • MUST remember its last sent dyn_propose parameters.
  • MUST send this message as soon as both side's commitment transaction is free of any HTLCs and both sides have sent dyn_begin_propose.

The receiving node:

  • if channel_id does not match an existing channel it has with the sender:
    • MUST send an error and close the connection.
  • if it does not agree with a parameter:
    • MUST send a dyn_propose_reply with the reject bit set.
  • else:
    • MUST send a dyn_propose_reply without the reject bit set.

Rationale

The requirement to not allow trimming outputs is just to make the dynamic commitment flow as uninvasive as possible to the commitment transaction. A similar requirement should be added for any new parameter such as the channel_reserve.

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.

dyn_propose_reply

This message is sent in response to a dyn_propose. It may either accept or reject the dyn_propose. If it rejects a dyn_propose, it allows the counterparty to send another dyn_propose to try again. If for some reason, negotiation is taking too long, it is possible to exit this phase by reconnecting as long as the exiting node hasn't sent dyn_propose_reply without the reject bit.

  1. type: 115 (dyn_propose_reply)
  2. data:
    • [32*byte:channel_id]
    • [byte: propose_reply_flags]

The least-significant bit of propose_reply_flags is defined as the reject bit.

Requirements

The sending node:

  • MUST set channel_id to a valid channel they have with the recipient.
  • MUST set undefined bits in propose_reply_flags to 0.
  • MUST set the reject bit in propose_reply_flags if they are rejecting the newest dyn_propose.
  • MUST NOT send this message if there is no outstanding dyn_propose from the counterparty.
  • if the reject bit is not set:
    • MUST remember the related dyn_propose parameters and 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 close the connection.
  • if there isn't an outstanding dyn_propose it has sent:
    • MUST send an error and fail the channel.
  • if the reject bit was set:
    • MUST forget its last sent dyn_propose parameters.

A node:

  • once it has both sent and received dyn_propose_reply without the reject bit set:
    • MUST increment their propose_height.

Rationale

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.

Reestablish

channel_reestablish

A new TLV that denotes the node's current propose_height is included.

  1. tlv_stream: channel_reestablish_tlvs
  2. types:
    1. type: 20 (propose_height)
    2. data:
      • [u64:propose_height]

Requirements

The sending node:

  • MUST set propose_height to the number of dynamic proposal negotiations it has completed. The point at which it is incremented is described in the dyn_propose_reply section.

The receiving node:

  • if the received propose_height equals its own propose_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.
  • if the received propose_height is 1 greater than its own propose_height:
    • if it does not have any remote parameters stored for the received propose_height:
      • MUST send an error and fail the channel. The remote node is either lying about the propose_height or the recipient has lost data since its not possible to advance the height without the recipient storing the remote's parameters.
    • resume using the channel with its last-sent dyn_propose and the stored dyn_propose parameters and increment its propose_height.
  • if the received propose_height is 1 less than its own propose_height:
    • resume using the channel with the new parameters.
  • else:
    • MUST send an error and fail the channel. State was lost.

Rationale

If both sides have sent and received dyn_propose_reply without the reject bit before the connection closed, it is simple to continue. If one side has sent and received dyn_propose_reply without the reject bit and the other side has only sent dyn_propose_reply, the flow is recoverable on reconnection as the side that hasn't received dyn_propose_reply knows that the other side accepted their last sent dyn_propose based on the propose_height in the reestablish message.

Musig2 Taproot

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 funder of the channel will also propose a fee to use for an intermediate "kickoff" transaction.

Extensions to dyn_propose:

  1. tlv_stream: dyn_propose_tlvs
  2. types:
    1. type: 2 (channel_type)
    2. data:
      • [...*byte:type]
    3. type: 4 (kickoff_feerate)
    4. data:
      • [u32:kickoff_feerate_per_kw]
    5. type: 6 (taproot_funding_key)
    6. data:
      • [point:funding_key]

Requirements

The sending node:

  • if it is the funder:
    • MUST only send kickoff_feerate if they can pay for the sum of the kickoff transaction fee and the anchor outputs, while adhering to the channel_reserve restriction.
  • MUST set taproot_funding_key to a valid secp256k1 compressed public key.

The receiving node:

  • if it is the fundee:
    • MUST reject the dyn_propose if the funder cannot pay for the sum of the kickoff transaction fee and the anchor outputs at the kickoff_feerate.
    • 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 dyn_propose if taproot_funding_key is not a valid secp256k1 compressed public key.
  • MAY reject the dyn_propose if it does not agree with the channel_type or the kickoff_feerate.

Rationale

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.

Extensions to dyn_propose_reply:

  1. tlv_stream: dyn_propose_reply_tlvs
  2. types:
    1. type: 0 (local_musig2_pubnonce)
    2. data:
      • [66*byte:nonces]

Requirements

The sending node:

  • if it is accepting a channel_type of simple_taproot_channel:
    • MUST set local_musig2_pubnonce to the nonce that it will use to verify local commitments.

The receiving node:

  • MUST send an error and fail the channel if local_musig2_pubnonce cannot be parsed as two compressed secp256k1 points.

Signing Phase

The signing phase is after the negotiation phase. The original funding output spends to an intermediate transaction that pays to a v1 witness script with an aggregated musig2 key derived from both parties taproot_funding_key sent in dyn_propose. As in the simple-taproot-channels proposal, the commitment_signed, revoke_and_ack, and channel_reestablish messages include nonces.

Commitment Transaction

  • 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 kickoff transaction's musig2 funding outpoint.
    • txin[0] sequence: upper 8 bits are 0x80, lower 24 bits are upper 24 bits of the obscured commitment number
    • txin[0] script bytes: 0
    • txin[0] witness: <key_path_sig>

The 48-bit commitment number is computed by XOR as described in BOLT#03.

Commitment Transaction Construction

  1. Initialize the commitment transaction version and locktime.
  2. Initialize the commitment transaction input.
  3. Calculate which committed HTLCs need to be trimmed.
  4. Calculate the commitment transaction fee via commitment feerate * commitment_transaction_weight/1000, making sure to round down. Subtract this from the funder's output.
  5. 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.
  6. Subtract the kickoff transaction's fee from the funder's output.
  7. For every offered HTLC, if it is not trimmed, add an offered HTLC output.
  8. For every received HTLC, if it is not trimmed, add a received HTLC output.
  9. If the to_local output is greater or equal to the dust limit, add a to_local output.
  10. If the to_remote output is greater or equal to the dust limit, add a to_remote output.
  11. If to_local exists or there are untrimmed HTLCs, add a to_local_anchor.
  12. If to_remote exists or there are untrimmed HTLCs, add a to_remote_anchor. The to_remote_anchor uses the remote party's taproot_funding_key.
  13. Sort the outputs into BIP 69+CLTV order.

commitment_signed

The commitment_signed message does not change, but adds a nonce in the TLV section per the simple-taproot-channels proposal. It changes what it signs in the following ways:

  1. tlv_stream: commit_sig_tlvs
  2. types:
    1. type: 2 (partial_signature_with_nonce)
    2. data:
      • [98*byte:partial_signature || public_nonce]
Requirements

The sending node:

  • MUST increment the commitment number when signing.
  • MUST sign for any negotiated parameters that modified the commitment transaction (e.g. to_self_delay).

The receiving node:

  • MUST send an error and fail the channel if the signature does not sign the commitment transaction as constructed above.
  • MUST send an error and fail the channel if partial_signature is not a valid Schnorr signature.
  • MUST send an error and fail the channel if public_nonce cannot be parsed as two compressed secp256k1 points.

Kickoff Transaction

  • version: 2
  • locktime: 0
  • txin count: 1
    • txin[0] outpoint: txid and output_index from funding_created message
    • txin[0] sequence: 0xffffffff-2
    • txin[0] script bytes: 0
    • txin[0] witness: 0 <signature_for_pubkey1> <signature_for_pubkey2>
  • txout count: 3
    • txout[0]: anchor_output_1 or anchor_output_2
    • txout[1]: anchor_output_1 or anchor_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 the kickoff transaction's fee. It is encumbered by a version 1 witness script where taproot_funding_key1/taproot_funding_key2 are from dyn_propose_reply:

  • 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))

Kickoff Transaction Construction

  1. Initialize the commitment transaction version and locktime.
  2. Initialize the commitment transaction input.
  3. Calculate the kickoff transaction fee via kickoff_feerate*kickoff_transaction_weight/1000, making sure to round down. Subtract this value from the new funding output.
  4. Subtract two times the fixed anchor size of 330 satoshis from the new funding output.
  5. Add a funding output with the new funding amount.
  6. Add an anchor output for each party.
  7. Sort the outputs into BIP 69+CLTV order.

kickoff_sig

The kickoff_sig is a signature that is combined with the corresponding one from the remote party 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.

  1. type: 777 (kickoff_sig)
  2. data:
    • [32*byte:channel_id]
    • [signature:signature]
Requirements

The sending node:

  • MUST set channel_id to a valid channel they have 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 if channel_id does not match an existing channel it has with the sender.
  • MUST send an error and fail the channel if signature is not valid for the kickoff transaction as constructed above OR non-compliant with the LOW-S-standard rule. LOWS

revoke_and_ack

Per the simple-taproot-channel proposal, the revoke_and_ack message contains a single nonce used by the recipient to construct a commitment transaction for the sender.

  1. tlv_stream: revoke_and_ack_tlvs
  2. types:
    1. type: 2 (local_musig2_pubnonce)
    2. data:
      • [66*byte:nonces]
Requirements

The sending node:

  • MUST NOT send this message until the sender has both sent and received kickoff_sig.

The receiving node:

  • MUST send an error and fail the channel if local_musig2_pubnonce cannot be parsed as two compressed secp256k1 points.

Message flow to upgrade a channel to simple-taproot:

    +-------+                               +-------+
    |       |--(1)---- commit_signed------->|       |
    |       |                               |       |
    |       |<-(2)---- commit_signed -------|       |
    |   A   |--(3)----- kickoff_sig ------->|   B   |
    |       |                               |       |
    |       |<-(4)----- kickoff_sig --------|       |
    |       |<-(5)----- rev_and_ack --------|       |
    |       |--(6)----- rev_and_ack ------->|       |
    +-------+                               +-------+

The above message ordering is important. If kickoff_sig is sent before commit_sig, a griefing attack is possible:

    +-------+                               +-------+
    |   A   |--(1)----- kickoff_sig ------->|   B   |
    +-------+                               +-------+

Here, B stops sending messages and instead immediately broadcasts the kickoff transaction. Since neither side has exchanged commitment_signed, the new funding output is unclaimable and is effectively burned. If only A has funds at risk, then only Alice loses money here.

revoke_and_ack must be sent only after sending and receiving kickoff_sig. Take for example the following flow where A sends revoke_and_ack before receiving kickoff_sig:

    +-------+                               +-------+
    |       |--(1)---- commit_signed ------>|       |
    |       |                               |       |
    |       |<-(2)---- commit_signed -------|       |
    |   A   |--(3)----- kickoff_sig ------->|   B   |
    |       |                               |       |
    |       |--(4)----- rev_and_ack ------->|       |
    +-------+                               +-------+

A has revoked their legacy commitment transaction. However, A cannot force close their channel because they do not have a kickoff_sig from B to broadcast the kickoff transaction on their own.

Additionally, including revoke_and_ack in the signing phase means that it is possible to determine when the flow is done upon a subsequent reconnect by examining the next_revocation_numbers sent and received in channel_reestablish.

Continue or fail

Once the proposal phase is complete in the upgrade case, it is not possible to exit without completing the signing stage. Take the below state transition:

    +-------+                               +-------+
    |       |--(1)----- dyn_propose ------->|       |
    |       |                               |       |
    |       |<-(2)----- dyn_propose --------|       |
    |   M   |--(3)--- dyn_propose_reply --->|   B   |
    |       |                               |       |
    |       |?-(4)--- dyn_propose_reply ----|       |
    |       |?-(5)---- commit_signed -------|       |
    +-------+                               +-------+

On reconnect, B is not able to tell if M received either dyn_propose_reply or commitment_signed. If we allowed restarting the negotiation flow in the upgrade case, M might be able to propose N different to_self_delay parameters and receive signatures for N different commitment transactions. M could then complete the signing phase up until receiving kickoff_sig and broadcast any of the N commitment transactions. To avoid this complexity, we require that if one side has sent and received dyn_propose_reply, the negotiation MUST finish or the channel is force closed. Before this point, neither node can determine whether negotiation is done.

The flow where M has 3 different broadcastable commitment transactions:

    +-------+                               +-------+
    |       |--(1)----- dyn_propose ------->|       |
    |       |                               |       |
    |       |<-(2)----- dyn_propose --------|       |
    |   M   |--(3)--- dyn_propose_reply --->|   B   |
    |       |                               |       |
    |       |?-(4)--- dyn_propose_reply ----|       |
    |       |?-(5)---- commit_signed -------|       |
    |       |                               |       |
    |       |           *reconnect*         |       |
    |       |                               |       |
    |       |--(1)----- dyn_propose ------->|       |
    |       |                               |       |
    |       |<-(2)----- dyn_propose --------|       |
    |       |                               |       |
    |       |--(3)--- dyn_propose_reply --->|       |
    |       |                               |       |
    |       |?-(4)--- dyn_propose_reply ----|       |
    |       |?-(5)---- commit_signed -------|       |
    |       |                               |       |
    |       |           *reconnect*         |       |
    |       |                               |       |
    |       |--(1)----- dyn_propose ------->|       |
    |       |                               |       |
    |       |<-(2)----- dyn_propose --------|       |
    |       |                               |       |
    |       |--(3)--- dyn_propose_reply --->|       |
    |       |                               |       |
    |       |<-(4)--- dyn_propose_reply ----|       |
    |       |                               |       |
    |       |<-(5)---- commit_signed -------|       |
    |       |--(6)---- commit_signed ------>|       |
    |       |<-(7)----- kickoff_sig --------|       |
    |       |                               |       |
    +-------+                               +-------+

The downside with requiring the signing phase to complete is that if kickoff_feerate is below the minimum relay feerate, another round cannot be attempted. This means the kickoff transaction will only get into the mempool after it has cleared somewhat. The anchor outputs on the kickoff transaction should be sufficient for confirmation in most cases.

Reestablish during simple-taproot upgrade

channel_reestablish

The channel_reestablish message does not change, but adds a nonce in the TLV section per the simple-taproot-channels proposal.

  1. tlv_stream: channel_reestablish_tlvs
  2. types:
    1. type: 4 (next_local_nonce)
    2. data:
      • [66*byte:public_nonce]

The sending node:

  • MUST set next_local_nonce if the sender sees it has persisted a channel_type of option_simple_taproot from the dyn_propose / dyn_propose_reply negotiation steps.

The receiving node:

  • MUST send an error and fail the channel if next_local_nonce cannot be parsed as two compressed secp256k1 points.

The signing phase is complete when both of the following are true:

  • a node receives a channel_reestablish with a next_revocation_number greater than their local commitment height at the start of the dynamic proposal flow
  • the node's sent next_revocation_number is greater than the remote commitment height at the start of the dynamic proposal flow

When the signing phase is complete, the channel can continue as normal. Note that this requires each side to remember the starting commitment heights when acknowledging and persisting received dyn_propose parameters.

If the connection was closed during the signing phase, the signing phase starts over at the beginning on reconnect:

  • commitment_signed MUST sign the same commitment transaction as before reconnect. Only the nonces will change.
  • kickoff_sig MUST also sign the same kickoff transaction as before reconnect.
  • MUST tolerate receiving duplicate commitment_signed, kickoff_sig, and revoke_and_ack across connection restarts as long as the signing phase is in progress.

This last requirement to tolerate duplicates means that flows like the following are allowed:

    +-------+                               +-------+
    |       |--(1)---- commit_signed  ----->|       |
    |       |                               |       |
    |       |<-(2)---- commit_signed -------|       |
    |   M   |--(3)----- kickoff_sig  ------>|   B   |
    |       |                               |       |
    |       |<-(4)----- kickoff_sig --------|       |
    |       |<-(5)---- revoke_and_ack ------|       |
    |       |                               |       |
    |       |           *reconnect*         |       |
    |       |                               |       |
    |       |--(1)---- commit_signed  ----->|       |
    |       |<-(2)---- commit_signed -------|       |
    |       |                               |       |
    |       |--(3)----- kickoff_sig  ------>|       |
    |       |<-(4)----- kickoff_sig --------|       |
    |       |                               |       |
    |       |<-(5)---- revoke_and_ack ------|       |
    |       |--(6)---- revoke_and_ack ----->|       |
    +-------+                               +-------+

Appendix

Pinning

TODO: tx diagram

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

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.

Dynamic Commitments & CPFP Carve-out

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
  • then spending from the new funding output, "using up" the CPFP Carve-out slot designated for the honest party.

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 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)

Reducing Risk

The above pinning scenarios highlight the complexity of second-layer protocols and mempool restrictions. In this proposal, pinning is still possible, but risk is mitigated because:

  • the kickoff transaction MUST confirm before HTLCs can be added to the commitment transaction
  • no HTLCs exist on the commitment transaction while the kickoff transaction is unconfirmed

If we allowed adding HTLCs before the kickoff transaction confirmed on-chain, the pinning attack would now have a tangible benefit: the ability to steal the value of an HTLC. The second requirement above is very similar to the first requirement: by disallowing HTLCs when dyn_propose is sent, we ensure that the counterparty has no incentive to pin the kickoff transaction.

Weights

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

Kickoff Transaction Weights

  • 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
  • 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

Commitment Transaction Weights

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
  • 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