BIP Draft: unspendable() Descriptor Key Expression #1746

pull andrewtoth wants to merge 9 commits into bitcoin:master from andrewtoth:bip-verifiably-unspendable-key-path changing 1 files +111 −0
  1. andrewtoth commented at 2:48 pm on January 17, 2025: contributor

    This is a BIP Draft for an unspendable key expression that can be used as the taproot internal key. The expression creates a provably unspendable key that is deterministically created with only the descriptor. This allows all participants to verify that the keypath is unspendable, while also hiding that fact from outside observers.

    Previous discussion on delving https://delvingbitcoin.org/t/unspendable-keys-in-descriptors/304.

    Mailing list post: https://groups.google.com/g/bitcoindev/c/xWxy8DtW6m8

  2. feat: initial draft of unspendable key path for multisig fbbf808196
  3. fix: code formatting 8201dad92c
  4. Update specification and add test vectors 4fd1272375
  5. Forbid musig expressions 3ef6f88e33
  6. Add backwards compatibility section 7a3704836d
  7. Merge pull request #2 from andrewtoth/bip-verifiably-unspendable-key-path
    Update specification and add test vectors
    cff4d8b7f6
  8. Specify which musig expression is forbidden d375d8d2e9
  9. Added more rationale a33c7035b1
  10. in bip-xxxx.mediawiki:17 in a33c7035b1 outdated
    12+  License: BSD-2-Clause
    13+</pre>
    14+
    15+==Abstract==
    16+
    17+This document specifies a <tt>unspendable()</tt> key expression for output script descriptors. The <tt>unspendable()</tt> expression takes multiple public keys as input and produces an unspendable public key that can be independently verified by anyone with knowledge of all the constituent public keys.
    


    sipa commented at 3:24 pm on January 17, 2025:
    It does not look like it takes any inputs? Rather, it’s a function that implicitly operates on the entire descriptor?

    andrewtoth commented at 3:46 pm on January 17, 2025:
    Right, will reword this.
  11. in bip-xxxx.mediawiki:42 in a33c7035b1 outdated
    37+
    38+The <tt>unspendable</tt> expression can only be used as the first argument of a BIP386 <tt>tr(KEY, TREE)</tt> expression. All other <tt>KEY</tt> expressions in the descriptor must be <tt>xpub</tt> encoded extended public keys with exactly 2 unhardened derivation steps. The derivation steps may include <tt>/*</tt> or a BIP389 multipath expression, but still must have only unhardened steps. BIP390 <tt>musig(KEY, KEY, ..., KEY)</tt> expressions are allowed, but the variant with derivation after the expression <tt>musig(KEY, KEY, ..., KEY)/NUM/.../*</tt> is forbidden.
    39+
    40+The <tt>unspendable</tt> expression resolves to an extended public key, which is then further derived. As there is no aggregate private key for an unspendable key, only unhardened derivation is allowed.
    41+
    42+The extended public key is computed by first collecting the public key from all the extended public keys in all the <tt>KEY</tt> expressions. The collection of public keys then has all duplicates removed and the remaining public keys are sorted lexicographically. 
    


    sipa commented at 3:26 pm on January 17, 2025:
    Sorted with what serialization? Textual descriptor serialization? Binary BIP32 serialization? Uncompressed? Compressed? X-only?
  12. Remove reference to inputs and aggregate keys a2e3865450
  13. pythcoiner commented at 2:46 pm on January 18, 2025: none
    cc @benma i remember your comment on BIP-0352, can’t some signing devices have some issue also here about sorting keys with complexes taptrees?
  14. pythcoiner commented at 5:02 pm on January 18, 2025: none

    Note: @darosior had implemented a similar approach in Liana, the main difference is there is no duplicate removing & sorting. Some signing devices already implemented this approach btw:

    cc @NicolasDorier i think i already shared w/ you about this part of the Liana code

    edit: I personally have no strong opinion, just wanted to ping interested ppl

  15. andrewtoth commented at 5:50 pm on January 18, 2025: contributor

    Note: @darosior had implemented a similar approach in Liana, the main difference is there is no duplicate removing & sorting.

    Unfortunately Liana’s approach introduces malleability when using sortedmulti since the keys aren’t sorted, and is incompatible with BIP388 wallet policies since the public keys aren’t deduplicated. This is mentioned in the rationale.

  16. pythcoiner commented at 6:56 pm on January 18, 2025: none

    and is incompatible with BIP388 wallet policies since the public keys aren’t deduplicated. This is mentioned in the rationale.

    I’m not sure to understand this point as iirc at least 2 of mentioned signing devices are using BIP388 wallet policies in their interface w/ liana (i should overlook something)

  17. andrewtoth commented at 7:15 pm on January 18, 2025: contributor

    @pythcoiner the standard proposed in this BIP should work for any general TREE expression. Using Liana’s scheme will work for any wallet policy that does not have duplicate public keys. However, if choosing a policy that contains duplicate public keys in the template, it will not be compatible.

    For instance, the descriptor tr(unspendable(),{key1,key1}) translates to wallet policy tr(_,{@0,@0}). Liana’s scheme using the descriptor will produce an unspendable key of SHA256(key1 + key1), whereas the wallet policy will have a key vector of just key1 so will produce an unspendable key of SHA256(key1).

  18. bitcoin deleted a comment on Jan 18, 2025
  19. darosior commented at 7:39 pm on January 18, 2025: member

    Thanks for working on standardizing this. Let me state in advance that i think this is already an improvement over the current situation with a non-standardized scheme used by multiple implementations but not entirely usable by others. Therefore feel free to discard my suggestions.

    That said it’s really unfortunate we’d have to break the compatibility with the scheme already deployed everywhere just to accommodate sortedmulti, which is in my opinion a descriptor fragment that runs contrary to the spirit of the descriptor language in the first place.

    My feeling is that this really applies to a layer on top of descriptor, as hinted by all the restrictions using this fragment imposes on the global descriptor. It might be interesting to design the fragment such as it can be used by both a lower level logic of descriptors and a higher level logic of wallet policy. For instance the unspendable fragment could contain the chaincode as its parameter such as a “dumb” descriptor-only parser would be able to generating the script. And a smarter parser could check that this chaincode does correspond to a hash of the wallet policy’s xpubs.

    Unfortunately Liana’s approach […] is incompatible with BIP388 wallet policies since the public keys aren’t deduplicated.

    Obviously not true. I think what you meant, which is a more reasonable claim, is that it does not have the property that you can compute the unspendable xpub’s chaincode only from the BIP388 list of keys. That is, simply what Salvatore points out here.

    I don’t remember if i had a particular rationale for not de-duplicating at the time. One advantage it prevents internal key reuse for a larger set of descriptors. It may be preferable to get the property that you can compute the chaincode only from the BIP388 list of keys and not push complexity on signing devices (which are usually more constrained), but stating the already deployed scheme is completely incompatible with wallet policy is disingenuous.

  20. darosior commented at 7:56 pm on January 18, 2025: member
    @pythcoiner for what it’s worth Ledger only check that the key part of the internal xpub is the BIP341 NUMS, it does not derive the chaincode. But you are right that you can use the already deployed scheme with BIP388 wallet policies. And Andrew and Salvatore are right to point out when using wallet policies it’s desirable to be able to derive the chaincode only from the BIP388 list of xpubs, without the wallet policy template.
  21. andrewtoth commented at 8:10 pm on January 18, 2025: contributor

    disingenuous

    What I meant was incompatible with just the key vector and not the template. Apologies for the way that came across.

  22. pythcoiner commented at 2:53 am on January 19, 2025: none

    Using Liana’s scheme will work for any wallet policy that does not have duplicate public keys. However, if choosing a policy that contains duplicate public keys in the template, it will not be compatible.

    Note: I think using Liana’s scheme will work for any descriptor generated by current miniscript “compilers” implementations, afaik both the c++ & rust implem fails if the policy contains duplicates keys.

  23. darosior commented at 3:01 am on January 19, 2025: member
    You can have duplicate xpubs without having duplicate keys in the scripts. An (uninteresting) example is tr(_,{pk(xpubA/0/*),pk(xpubA/1/*)}).
  24. pythcoiner commented at 3:10 am on January 19, 2025: none

    What i mainly wondering is: signing devices that have already followed Liana’s approach will now have to mantain both.

    The only way i can imagine for having this standard being an “upgrade” of actual implementations is by:

    • ruling out a policy that have duplicates keys (is it any valuable reason to have duplicate keys?)
    • sorting only the keys contained in sortedmulti() fragments

    note: I don’t say i’d push in this direction for the standard, but i’m never comfortable with breaking “user space”

  25. pythcoiner commented at 3:58 am on January 19, 2025: none

    when using wallet policies it’s desirable to be able to derive the chaincode only from the BIP388 list of xpubs, without the wallet policy template.

    what about the case my mom policy is:

    • or(and(MOM,SON),and(DAD,older(tl)))

    my father one is:

    • or(and(DAD,SON),and(MOM,older(tl)))

    iiuc w/ the actual proposal, they will have the same unspendable key?

  26. andrewtoth commented at 4:01 am on January 19, 2025: contributor

    they will have the same unspendable key

    Having the same key in some scenarios is acceptable. What is unacceptable is a descriptor or policy that produces the same merkle root but produces a different unspendable key.

  27. pythcoiner commented at 4:22 am on January 19, 2025: none

    Having the same key in some scenarios is acceptable.

    I think it’s still better to avoid if we can

    What is unacceptable is a descriptor or policy that produces the same merkle root but produces a different unspendable key.

    agree, but if it’s an issue only w/ sortedmulti(), maybe only sorting keys in sortedmulti() is a viable solution? with the advantage of not creating issue w/ any other schemes

  28. andrewtoth commented at 7:22 pm on January 19, 2025: contributor

    Thank you @pythcoiner and @darosior for the valuable feedback.

    but if it’s an issue only w/ sortedmulti()

    Looking back at the discussion, it is also an issue with using only the wallet policy key vector without the template to construct the chaincode, as pointed out by Salvatore here.

    the unspendable fragment could contain the chaincode as its parameter such as a “dumb” descriptor-only parser would be able to generating the script. And a smarter parser could check that this chaincode does correspond to a hash of the wallet policy’s xpubs.

    What would be the benefit of such a fragment for the “dumb” parser, vs just using the computed unspendable xpub with the NUMS public key as currently done?

    // - The given descriptor does not contain a Taptree with at least a key in each leaf.

    I see this constraint in the Liana code as well. So we can’t do tr(unspendable, {pk(key),sha256(h)})?

  29. darosior commented at 8:30 pm on January 19, 2025: member

    I see this constraint in the Liana code as well. So we can’t do tr(unspendable, {pk(key),sha256(h)})?

    This is a Liana-specific constraint which i don’t think should apply to this standard. (We have a bunch of constraints on the descriptors accepted in Liana to make it easier to reason about what action can be performed by the user.)

    Note also the sha256(h) leaf is a Miniscript, which here would be considered malleable as it does not have a key check required on all spending paths. For instance the Bitcoin Core wallet would refuse to import it as malleable Miniscript descriptors are considered unsafe (or not “sane”) to use.

  30. scgbckbone commented at 4:04 am on January 20, 2025: contributor

    Some signing devices already implemented this approach btw

    we haven’t implemented any of this tbh, COLDCARD only allows explicit unspendable keys in descriptors https://github.com/Coldcard/firmware/blob/new_edge/docs/taproot.md#provably-unspendable-internal-key

    For instance the unspendable fragment could contain the chaincode as its parameter such as a “dumb” descriptor-only parser would be able to generating the script. And a smarter parser could check that this chaincode does correspond to a hash of the wallet policy’s xpubs.

    If this proposal intends to operate on descriptor level - it needs to have chain code parameter as mentioned above so that signing devices do not need to care

  31. in bip-xxxx.mediawiki:63 in a2e3865450
    58+
    59+The following has two identical public keys which are deduplicated:
    60+* <tt>tr(unspendable()/0, {pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/0),pk(xpub661MyMwAqRbcFXsZHGwUFzya6zhjaLUoKt2jKZTsWEoHAPjUERUbW215Fy6DGNLZdNDyMo8WJLgouGNRypxvDFc3MgW8TvRJdpbzsxuyfvr/0/0)})</tt>
    61+
    62+The following has two identical public keys which are deduplicated, and then the remaining two public keys are sorted:xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw
    63+* <tt>tr(unspendable()/0, {pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/0),{pk(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw),pk(xpub661MyMwAqRbcFXsZHGwUFzya6zhjaLUoKt2jKZTsWEoHAPjUERUbW215Fy6DGNLZdNDyMo8WJLgouGNRypxvDFc3MgW8TvRJdpbzsxuyfvr/0/0)}})</tt>
    


    bigspider commented at 8:55 am on January 20, 2025:

    It seems a bit unusual to have examples of xpubs with the same pubkey in a descriptor (except, of course, NUMS xpubs), although it should indeed be mentioned as a pathological case if it matters for deduplication.

    I think this wouldn’t really happen in practice, as xpubs would usually come from some BIP32 derivation.

    What would happen is to have the same xpub with different multipath derivations in alternative spending paths:

    tr(unspendable()/<0;1>/*, {multi_a(2,xpub_A/<0;1>/*,xpub_B/<0;1>/*),and_v(v:pk(xpub_A/<2;3>/*),older(12960))})

  32. in bip-xxxx.mediawiki:57 in a2e3865450
    52+Todo: These will be filled in when the BIP number is assigned for the tagged hash.
    53+
    54+The following produce identical extended public keys and scripts:
    55+* <tt>tr(unspendable()/0, pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/0))</tt>
    56+* <tt>tr(unspendable()/0, pk(xpub661MyMwAqRbcFXsZHGwUFzya6zhjaLUoKt2jKZTsWEoHAPjUERUbW215Fy6DGNLZdNDyMo8WJLgouGNRypxvDFc3MgW8TvRJdpbzsxuyfvr/0/0))</tt>
    57+* <tt>tr(unspendable()/0, pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/<0;1>/*))</tt>
    


    bigspider commented at 8:58 am on January 20, 2025:
    Since unspendable() is treated like an xpub, in order to be compatible with BIP-388 you would need to have for example unspendable()/<0;1>/*. In general, mixing public keys with /* and other public keys without /* in the same descriptor always leads to key reuse across UTXOs.
  33. in bip-xxxx.mediawiki:38 in a2e3865450
    33+
    34+A new key expression is defined: <tt>unspendable()/NUM/.../*</tt>.
    35+
    36+===<tt>unspendable()/NUM/.../*</tt>===
    37+
    38+The <tt>unspendable</tt> expression can only be used as the first argument of a BIP386 <tt>tr(KEY, TREE)</tt> expression. All other <tt>KEY</tt> expressions in the descriptor must be <tt>xpub</tt> encoded extended public keys with exactly 2 unhardened derivation steps. The derivation steps may include <tt>/*</tt> or a BIP389 multipath expression, but still must have only unhardened steps. BIP390 <tt>musig(KEY, KEY, ..., KEY)</tt> expressions are allowed, but the variant with derivation after the expression <tt>musig(KEY, KEY, ..., KEY)/NUM/.../*</tt> is forbidden.
    


    bigspider commented at 9:06 am on January 20, 2025:
    I don’t understand this limitation over musig. musig(KEY, KEY, ..., KEY)/<M,N>/* is the scheme supported for MuSig2 in wallet policies.

    andrewtoth commented at 3:15 pm on January 20, 2025:
    If musig(KEY, KEY, ..., KEY) resolves to AGG_KEY, then tr(unspendable()/0,musig(KEY, KEY, ..., KEY)/<M,N>/*)) and tr(unspendable()/0,AGG_KEY/<M,N>/*)) would both resolve to the same merkle root but different internal keys. If using just tr(unspendable()/0,musig(KEY, KEY, ..., KEY))), the corresponding tr(unspendable()/0,AGG_KEY)) is invalid since AGG_KEY is not an xpub with 2 derivation paths.

    bigspider commented at 4:30 pm on January 20, 2025:
    Per the previous discussions, the goal was to come up with something that works for both descriptors and wallet policies, and doesn’t require any complicated parsing. Aggregating musig() before using its key (rather than using the individual keys like you’d do for multi/sortedmulti fragments) would completely negate the advantages of sorting and removing duplicates - namely, being able to compute the deterministic chaincode with a simple function of the involved xpubs.

    andrewtoth commented at 4:50 pm on January 20, 2025:
    One of the goals is also to not have any malleability - two descriptors or policies that produce the same merkle root should always produce the same internal key. We cannot know if an xpub in a descriptor or wallet policy is actually an aggregate xpub, which could also be represented with the musig expression.

    sipa commented at 4:55 pm on January 20, 2025:

    @andrewtoth You’re imposing an (arbitrary) distinction though at the xpub level. If you expand a description in any specific position, and replace all xpub/keypath expression with their hex pubkey at that point, the merkle root will still be the same, but since you’ve lost the xpub data, you can’t generate the same unspendable keys anymore.

    (I don’t have an opinion either way here, just pointing out that what kinds of changes you incorporate in the calculation and which you don’t is inherently arbitrary)


    andrewtoth commented at 5:01 pm on January 20, 2025:

    replace all xpub/keypath expression with their hex pubkey

    The proposal here though prohibits any key expression that is not an xpub with exactly unhardened depth of 2.


    sipa commented at 5:07 pm on January 20, 2025:

    Ok, sure, that’s what you need to do to make sure you don’t accidentally derive an incompatible result.

    But for every piece of information in a descriptor you can either ignore it in the unspendable computation, or use it and require it to be present. What those pieces of information are is arbitrary. For example, you could use origin information for all keys for example, and reject any descriptor that doesn’t have it. It’s a tradeoff between the sensitivity of the unspendable keys, and restrictiveness on the set of descriptors it’s allowed to be used in.

  34. bigspider commented at 9:31 am on January 20, 2025: contributor

    Thanks for writing it down, it’s good to see a formal specification.

    Some general comments:

    • It’s clear that this is a lot harder to specify for descriptors, than it is for wallet policies, for the simple reason that unspendable() is a function of all the keys, which are already in a separate list in wallet policies. Defining it for descriptors forces one to add a lot of constraints over the general structure of the descriptor - meaning it’s invalid if those constraints are not supported. This would add a substantial amount of complexity in descriptor parsers that want to support unspendable(), which IMHO would be an obstacle for adoption.
    • For hardware signers that already support miniscript, this is not a security feature; rather, it’s a slight UX improvement. The security feature is to recognize the NUMS xpubs and explicitly mark them as dummy/unspendable when the wallet account is registered on the device. unspendable() just reduces by 1 the number of xpubs that the user has to inspect and compare with the backup. That is, it keeps the information content of the descriptor a bit smaller, which is good.

    I wonder if it wouldn’t be better to only define the standard way of computing the NUMS xpub that software wallets can use, and leave it to the hardware signers to verify if it matches with the taproot internal key. In this way, one avoids having to update all the existing descriptor tools, while adoption can be opt-in (if you don’t implement the deterministic chaincode check, you can just check if the public key is NUMS and have the user inspect the xpub; if you do, then you can recognize the deterministic NUMS xpub, and skip it from the user’s inspection).

  35. in bip-xxxx.mediawiki:25 in a2e3865450
    20+
    21+This BIP is licensed under the BSD 2-clause license.
    22+
    23+==Motivation==
    24+
    25+When creating a multi-party Taproot transaction spending only from the script path, it is useful to be able to prove to all cosigners that they keypath is unspendable. Otherwise a malicious participant could use an internal key which they have the private key for and spend the transaction out from the rest of the participants.
    


    jonatack commented at 3:23 pm on January 20, 2025:
    0When creating a multi-party Taproot transaction spending only from the script path, it is useful to be able to prove to all cosigners that they keypath is unspendable. Otherwise, a malicious participant could use an internal key for which they have the private key to spend the transaction out from the rest of the participants.
    
  36. in bip-xxxx.mediawiki:93 in a2e3865450
    88+A <tt>musig</tt> expression with derivation paths is used:
    89+* <tt>tr(unspendable()/0, musig(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/0)/0/0)</tt>
    90+
    91+==Rationale==
    92+
    93+The restrictions on <tt>KEY</tt> expressions is necessary to not allow multiple <tt>TREE</tt> expressions which would all produce the same merkle root to produce different internal keys.
    


    jonatack commented at 3:26 pm on January 20, 2025:

    Either “restrictions” should be singular, or “is” should be “are”

    s/expressions which/expressions, which/

  37. jonatack commented at 3:41 pm on January 20, 2025: member
    Question regarding the BIP type: is this intended to be a standard for the interoperability of applications? (Note that a Standards Track BIP would need a design document, and before Final status, a reference implementation.) For what it’s worth, BIPS 380, 386, and 390 are Informational.
  38. jonatack added the label New BIP on Jan 20, 2025
  39. darosior commented at 5:14 pm on January 20, 2025: member

    What would be the benefit of such a fragment for the “dumb” parser, vs just using the computed unspendable xpub with the NUMS public key as currently done?

    Compatibility with all descriptors, not only the small subset that corresponds to the restrictions here.

  40. andrewtoth commented at 5:37 pm on January 20, 2025: contributor

    it needs to have chain code parameter as mentioned above so that signing devices do not need to care

    Compatibility with all descriptors, not only the small subset that corresponds to the restrictions here.

    The point of this proposal is so that signing devices do not need to care about the chaincode at all and infer it from the rest of the descriptor. I don’t see much benefit for an unspendable(r) expression. Parsers in both Ledger and Coldcard already parse the xpub and determine it is unspendable by inspecting the public key, and show the user that it is indeed unspendable. However, the xpub still needs to be backed up because of the chaincode. An unspendable(r) does not change that.

    Let me know if there are benefits I haven’t considered, but it seems the only benefit to this type of expression is so that it can be human readable that it is unspendable. But, as soon as you put an xpub in a parser today it can tell you whether it is unspendable or not, and this new expression will require parsers to upgrade to be able to parse it.

    An unspendable(r) expression will fail to parse for unupgraded parsers, while the xpub is backwards compatible and the parser can upgrade to detect the public key at their convenience. If we are going to have parsers upgrade for a new expression anyways, we might as well remove the need for the chaincode.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bips. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-01-21 07:10 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me