-
Notifications
You must be signed in to change notification settings - Fork 513
extension-bolt: simple taproot channels (feature 80/81) #995
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
d7b1fe6
to
ec8c7b4
Compare
Some things that came up in meatspace discussions:
|
I think the commit_sig should contain the sender's "remote nonce" and the revoke_and_ack contain the sender's "local nonce". Also since funding_locked will be sent repeatedly with scid-alias when that is merged and deployed, then there should probably be language to define that the nonces are only sent the first time? |
let's try to pick naming conventions for nonces that doesn't make me cry over the asymmetry |
Some points: This interacts with the 2-of-3 goal of @moneyball . If one participant uses a 2-of-3 and owns ALL 3 keys, then it is fine and we can just have MuSig2 with both channel endpoints. But the 2-of-3 goal is that one channel endpoint is really a nodelet-like setup: there is one sub-participant with 2 keys and another "server" participant with 1 key, a la GreenWallet. This requires composable MuSig2. Now I think composable MuSig2, if it can be proven safe, just requires two -- This interacts with VLS as well @ksedgwic . The nonce A similar technique may also be useful for the server in the 2-of-3 of @moneyball; rather than maintain a state for each channel of each client, the client could store the per-channel |
So I talked to @jonasnick, and as I understand it, we can work with just two |
Re recursive musig2: I'm gonna give the implementation a shot (outside the LN context, just the musig-within-musig) just to double check my assumptions re not needing to modify the (revised) nonce exchange flow. |
i made a pull request on this pull request with script fixes |
Why not the revocation key? When i publish an old state, the remote party can claim my output and htlcs with the key path, but not his own output, and also has to wait a block. If we set the internal key to the revocation key it will give the remote party more privacy, nobody on chain can see which outputs were to local and to remote (and htlcs if they are swept along). It will also give more consistency with other output as they also have the revocation key as internal key. it will also be cheaper (or get a higher fee rate with the same amount of sats), this only requires a signature from a taptweaked revocation key (65) instead of a signature (65), the script (36) and the controlblock (34) (incl length prefix) |
#995 (comment) makes it invisible for outside observers to identify the to_remote output in case of a revoked commitment. if there are some htlcs on it that are long expired and the second stage is broadcasted (like in the fee siphoning attack), the funds go to the local delayed pubkey + relative timelock. outside observers can now see which output was the to_local one, just search the output of an htlc 2nd stage tx in the commitment transaction. example ctx: 15c262aeaa0c5a44e9e5f25dd6ad51b4162ec4e23668d568dc2c6ad98ae31023 (testnet) the transaction with the expired htlc reveals the to_local output. (it is already revealed by the script, but this wouldnt be the case with a revoked taproot ctx) this can be fixed by tweaking the local delayed pubkey with the hash of EDIT: no secret is needed, instead a taptweak like tweak can be done. everywhere where a local delayed pubkey is used, it is tweaked with for clarity: htlc outputs that send funds to the local delayed pubkey use a tweaked local delayed pubkey where the output index of the htlc output on the commitment transaction is used, not the htlc success or timeout tx |
this would preserve privacy, but you'd also need to do this for the |
hmmm true, so it is either privacy, with no key reuse or no utxo set bloat. btw another idea about anchors and less utxo set bloat: |
The |
if this is a problem B can tweak the key before using it without A even knowing (also A has to do this because the lexicographically smaller key is tweaked), but i dont think it is, lnd uses a separate bip32 tree for this (separate from the wallet) (btw without taproot funding pubkeys were revealed every time a channel was closed) |
afaict the algorithm in this bip is generalized for 32 byte pubkeys and more than 2 signers, the 'simple' musig2 with the pubkey's with the parity bit known looks like this equation i used, btw i got it here https://github.com/t-bast/lightning-docs/blob/master/schnorr.md#musig2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some old comments I forgot to submit
Most recent comment is noting that partial sigs are 32 bytes, so this needs explicit defining somewhere, since signature types seem to assume 64(may have missed it).
nvm this wouldn't work because keys are only revealed when swept without signature to make this problem somewhat easier i suggest to remove the now that the this special case can of course be seen from both sides:
even more rare: revocation no anchor keys are revealed here because with the revocation key the taproot key path is used. i don't know to make anchor sweepable in this case long story short:
Questions/feedback welcome! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should switch to miniscript-compatible scripts as much as possible (it's only small changes).
The revoke_script
remains the same because to spend them after the 16-blocks delay, people would need information that is not part of the published commit tx.
<local_delayedpubkey> OP_CHECKSIG | ||
<to_self_delay> OP_CHECKSEQUENCEVERIFY OP_DROP | ||
* `revoke_script` is the delay script: | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one cannot be made miniscript-compatible without loosing the NOOP push of local_delayedpubkey
which is needed to spend the anchor output once the 16 blocks delay has passed if a revoked commit is published (see explanation above).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean? Looking at the other changes for miniscript compat, we just need to make the OP_CHECKSIG
into an OP_CHECKSIGVERIFY
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I mean is that it does not seem possible to generate :
<local_delayedpubkey> OP_DROP
<revocation_pubkey> OP_CHECKSIG
with miniscript (more specifically you cannot generate the <local_delayedpubkey> OP_DROP
part).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah that makes sense. Will leave this one as is for now...
In an alternate dimension, we would've used the annex here, as we just want to make sure it's published so others can use this value to sweep the anchors.
* `anchor_output_key = anchor_internal_key + tagged_hash("TapTweak", anchor_internal_key || anchor_script_root)` | ||
* `anchor_script_root = tapscript_root([anchor_script])` | ||
* `anchor_script`: | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already miniscript-compatible: older(16)
Pushed a commit to make the scripts mini script compatible as suggested by @sstone. There was one script that couldn't be "fixed", as it uses a data push of a checksig followed by a drop (we just want to publish this data in the witness). In terms of miniscript compat, can't one just use the |
A `taproot_output_key` commits to an internal key, and optional script root via | ||
the following mapping: | ||
``` | ||
taproot_output_key = taproot_internal_key + tagged_hash("TapTweak", taproot_internal_key || script_root)*G |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor: no (re)-definition of G
the elliptic curve generator point, contrary to the other elements e.g script_root
.
bip340 excerpt:
"The constant G refers to the base point, for which x(G) = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 and y(G) = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8."
https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#design
The `script_root` is the root of a Tapscript tree as defined in BIP 341. A tree | ||
is composed of `tap_leaves` and `tap_branches`. | ||
|
||
A `tap_leaf` is a two tuple of (`leaf_version`, `leaf_script`). A `tap_leaf` is serialized as: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i wonder if the correctness of the leaf_version
(i.e == 0xc0
or == 0xc1
) is verified once the funding output has hit the chain (no option_zero_conf
mode) to be sure the script_root
built from counterparty contributions is the correct one committed on-chain.
from looking on channel_ready
recipient, there is no say that the script_root
must be verified. it might be elsewhere in this document. e.g for offered HTLCs there is a split of the timeout and success paths in individual script leaves.
In this extension BOLT, we specify the initial flavor of taproot channels to be deployed. This channel type uses musig2 aggregated keys and signatures for the funding output, making it a normal single signature key path spend. All outputs are then updated to use P2T2 (segwit v1) outputs. The coop close process has been simplified to always terminate, and the co-op close transaction now also flags RBF to make way for future schemes that enable the process to be restarted which enables co-op close fee bumping. A top-level key spend output is used to the revocation of HTLC outputs. The revocation for the local output uses a script path to ensure that information needed to sweep the anchors by 3rd parties is always revealed on chain.
1ad02ee
to
4c1314a
Compare
Aight, I just pushed two major updates to the BOLT
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've tested simple taproot channels against https://github.com/Roasbeef/lnd/tree/prod-taproot-chans and we can
- open channels
- create and pay HTLCs
For these tests to work we had to:
- modify lnd's commitment weight for taproot channels (which is not specified yet) and set it to 960 instead of 968 (because we expect the tx output count to be encoded on 1 byte instead of 3).
- implement the single nonce TLV fallback when the map is missing for
revoke_and_ack
Closing does not work yet though, as lnd does not seem to understand closing_complete
(or I could not configure it properly ?), but we implemented the changes described in #995 (comment) and I believe that there's everything we need for option_simple_close
but I proposed changes to some of the descriptions.
- MUST extract the partial signature (first 32 bytes) and the sender's next | ||
closee nonce (remaining 66 bytes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- MUST extract the partial signature (first 32 bytes) and the sender's next | |
closee nonce (remaining 66 bytes) | |
- MUST extract the partial signature (first 32 bytes) and the sender's JIT | |
nonce (remaining 66 bytes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here the distinct terminology was used on purpose. I used closee/closer here, as the roles are always non ambiguous. If you're sending closing_complete
, then you're the closer.
I think we should agree on what terminology we'll use here. I adopted the closee/closer based on the initial comment @t-bast left outlining a protocol (adapted it a bit as you see here).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's the use of "next" that is wrong, but I guess that just removing it could be enough.
(1, 2, 3) to distinguish taproot signatures. Each `partial_sig_with_nonce` | ||
contains: | ||
- 32 bytes: MuSig2 partial signature | ||
- 66 bytes: The sender's next closee nonce for potential RBF iterations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 66 bytes: The sender's next closee nonce for potential RBF iterations | |
- 66 bytes: The sender's JIT nonce used to generate the partial signature. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, what about The sender's closee nonce used to generate the partial signature
?
are delivered just-in-time with signatures using an asymmetric pattern: | ||
|
||
- `closing_complete` uses `PartialSigWithNonce` (98 bytes) to bundle the | ||
signature with the next closee nonce |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
signature with the next closee nonce | |
signature with the JIT nonce it was generated with |
- MUST store (in memory) the extracted nonce for potential future RBF | ||
iterations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove ? The receiver does not need to store these, only the sender must remember them for when they receive closing_sig
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, will remove.
the `shutdown` message or the previous `PartialSigWithNonce` in | ||
`closing_complete` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the `shutdown` message or the previous `PartialSigWithNonce` in | |
`closing_complete` | |
the `shutdown` message or the `next_closee_nonce` in | |
the previous `closing_sig` |
- MUST use the appropriate nonces for verification: | ||
- The sender's closee nonce (from their `shutdown` message or previous | ||
RBF round) | ||
- The receiver's own closer nonce (locally generated) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- The receiver's own closer nonce (locally generated) | |
- The receiver's own closer nonce used to generate their `closing_complete` |
With this JIT nonce approach for coop close, an implementation only needs to | ||
store (in memory) the current closee nonce for the remote and local party. When | ||
either side is ready to sign, it'll generate a new closer nonce, and then that | ||
along with the sig. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this JIT nonce approach for coop close, an implementation only needs to | |
store (in memory) the current closee nonce for the remote and local party. When | |
either side is ready to sign, it'll generate a new closer nonce, and then that | |
along with the sig. | |
With this JIT nonce approach for coop close, an implementation only needs to | |
store (in memory) the current closee nonce for the remote and local party, and | |
the closer nonces they used in their `closing_complete` message. When | |
either side is ready to sign, it'll generate new closer nonces, and send them | |
along with the partial signatures. |
1. `tlv_stream`: `revoke_and_ack_tlvs` | ||
2. types: | ||
1. type: 4 (`next_local_nonce`) | ||
2. data: | ||
* [`66*byte`: `public_nonce`] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should reuse the same nonce map
TLV that is attached to channel_reestablish
, it is easier to use with splices.
Typically we use the most conservative values for the weight estimates. One example is assuming the largest possible push for op codes like |
The RBF support is behind an additional build flag: |
You mean reading the existing filed if the newer nonce map isn't used? |
Yes. |
This PR puts forth two concepts:
The extensions described in this document have purposefully excluded any gossip related changes, as the there doesn't yet appear to be a predominant direction we'd all like to head in (nu nu gossip vs kick the can and add schnorr).
Most of the changes here described are pretty routine: use musig2 when relevant, and create simple tapscript trees to fold in areas where the script has multiple conditional paths. The main consideration with
musig2
is ofc: how to handle nonces. This document takes a very conservative stance, and simply proposes that all nonces be 100% ephemeral, and forgotten, even after a connection has been dropped. This has some non-obvious implications w.r.t the retransmission flow. Beyond that, it's mostly: piggy back the nonce set of nonces (4 public nonces total, since there're "two" messages) on a message to avoid having to add additional round trips.The other "new" thing this adds is the generation/existence of a NUMs point, which is used to ensure that certain paths can only be spent via the script spend path (like the to remote output for the remote party, as this inherits anchor outputs semantics).
This is still marked as draft, as it's just barely to the point of being readable, and still has a lot of clean ups to be done w.r.t notation, clarify, wording, and full specification.