Skip to content
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

BIP Draft: unspendable() Descriptor Key Expression #1746

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

andrewtoth
Copy link
Contributor

@andrewtoth andrewtoth commented Jan 17, 2025

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

bip-xxxx.mediawiki Outdated Show resolved Hide resolved

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.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorted with what serialization? Textual descriptor serialization? Binary BIP32 serialization? Uncompressed? Compressed? X-only?

@pythcoiner
Copy link

pythcoiner commented Jan 18, 2025

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?

@pythcoiner
Copy link

pythcoiner commented Jan 18, 2025

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

@andrewtoth
Copy link
Contributor Author

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.

@pythcoiner
Copy link

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)

@andrewtoth
Copy link
Contributor Author

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

@bitcoin bitcoin deleted a comment from SupavineeSerksiri6315 Jan 18, 2025
@darosior
Copy link
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.

@darosior
Copy link
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.

@andrewtoth
Copy link
Contributor Author

disingenuous

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

@pythcoiner
Copy link

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.

@darosior
Copy link
Member

darosior commented Jan 19, 2025

You can have duplicate xpubs without having duplicate keys in the scripts. An (uninteresting) example is tr(_,{pk(xpubA/0/*),pk(xpubA/1/*)}).

@pythcoiner
Copy link

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"

@pythcoiner
Copy link

pythcoiner commented Jan 19, 2025

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?

@andrewtoth
Copy link
Contributor Author

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.

@pythcoiner
Copy link

pythcoiner commented Jan 19, 2025

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

@andrewtoth
Copy link
Contributor Author

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

@darosior
Copy link
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.

@scgbckbone
Copy link
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

Copy link
Contributor

@bigspider bigspider left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

* <tt>tr(unspendable()/0, {pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/0),pk(xpub661MyMwAqRbcFXsZHGwUFzya6zhjaLUoKt2jKZTsWEoHAPjUERUbW215Fy6DGNLZdNDyMo8WJLgouGNRypxvDFc3MgW8TvRJdpbzsxuyfvr/0/0)})</tt>
The following has two identical public keys which are deduplicated, and then the remaining two public keys are sorted:xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw
* <tt>tr(unspendable()/0, {pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/0),{pk(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw),pk(xpub661MyMwAqRbcFXsZHGwUFzya6zhjaLUoKt2jKZTsWEoHAPjUERUbW215Fy6DGNLZdNDyMo8WJLgouGNRypxvDFc3MgW8TvRJdpbzsxuyfvr/0/0)}})</tt>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

The following produce identical extended public keys and scripts:
* <tt>tr(unspendable()/0, pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/0))</tt>
* <tt>tr(unspendable()/0, pk(xpub661MyMwAqRbcFXsZHGwUFzya6zhjaLUoKt2jKZTsWEoHAPjUERUbW215Fy6DGNLZdNDyMo8WJLgouGNRypxvDFc3MgW8TvRJdpbzsxuyfvr/0/0))</tt>
* <tt>tr(unspendable()/0, pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/<0;1>/*))</tt>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.


===<tt>unspendable()/NUM/.../*</tt>===

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this limitation over musig.
musig(KEY, KEY, ..., KEY)/<M,N>/* is the scheme supported for MuSig2 in wallet policies.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

@sipa sipa Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

@jonatack jonatack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.


==Motivation==

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
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 for which they have the private key to spend the transaction out from the rest of the participants.

==Rationale==

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either "restrictions" should be singular, or "is" should be "are"

s/expressions which/expressions, which/

@darosior
Copy link
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.

@andrewtoth
Copy link
Contributor Author

andrewtoth commented Jan 20, 2025

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants