Subject: Re: [bitcoin-dev] Wallet policies for descriptor wallets

From: Salvatore Ingala <[email-masked]>
Date: Tue, 17 May 2022 10:44:53 +0200

Hi all,

TL;DR: It is easy to convert from wallet policy to descriptors and back;
imho aliases are better left out of descriptors in real world usage; some
more examples given.

I received some very useful feedback on the wallet policy proposal (in this
list and outside); that also led me to realize that my initial post lacked
some clarity and more practical examples.

This post wants to:
- clarify that extracting descriptors from the wallet policy is trivial;
- argue that figuring out the wallet policy (template and list of keys
information) from the descriptor is reasonably easy − automatable for sane
descriptors currently in use, and much more general ones as well;
- give an idea of what the information shown on a hardware wallet screen
would look like (emphasizing compactness);
- explain my point of view on "descriptors with aliases".

This gist demoes conversions from wallet policies to descriptors, and back:
https://gist.github.com/bigspider/10df51401be3aa6120217c03c2836ffa

Note that I would expect/hope software wallets to prefer working directly
with wallet policies − but it might help to have automated tools for the
conversion, for interoperability with tools that do not adopt wallet
policies.

(All the following examples use the `/**` notation as a shortcut for
`/<0,1>/*`; this notation might be dropped without consequences on the rest
of the proposal.)

All the keys in the example I'm proposing are followed by /**. It is
unclear to me if hardware wallets should allow *registration* of wallet
policies with static keys (that is, without any range operator), as that
would incentivize key reuse. The specs still support it as there might be
other use cases.

The policy for miniscript examples not using taproot was generated with the
online compiler: https://bitcoin.sipa.be/miniscript. Many examples are also
borrowed from there.
(To the best of my knowledge, there is no publicly released compiler for
miniscript on taproot, yet)

Note on aliases: it has been pointed out that many miniscript
implementations internally use aliases to refer to the keys. In my opinion,
aliases:
- should be external to the descriptor language, as they bear no
significance for the actual script(s) that the descriptor can produce
- fail to distinguish which part of the KEY expression is part of the
"wallet description", and which part is not

By clearly separating the key information in the vector (typically, an xpub
with key origin information) from the key placeholder expression (which
typically will have the `/**` or `/<0,1>/*` derivation step), wallet
policies semantically represent keys in a way that should be convenient to
both software wallets and hardware signers.

Associating recognizable names to the xpubs (and registering them on the
device) is a good idea for future developments and can greatly improve the
UX, both during wallet setup, or in recognizing outputs for repeated
payments; it should be easy to build this feature on top of wallet policies.

== Examples ==

All the examples show:
- Miniscript policy: semantic spending rules, and optimization hints (can
be compiled to miniscript automatically)
- Miniscript: the actual miniscript descriptor, compiles 1-to-1 to Bitcoin
Script
- Wallet template: the "wallet descriptor template"
- Vector of keys: the list of key information (with key origin information)

Together, the wallet template and the vector of keys are the complet
"wallet policy".

=== Example 1: Either of two keys (equally likely) ===

Miniscript policy: or(pk(key_0),pk(key_1))
Miniscript:
 wsh(or_b([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/<0;1>/*),s:pk([12345678/44'/0'/0']xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<0;1>/*)))

Descriptor template:   wsh(or_b(pk(@0/**),s:pk(@1/**)))
Vector of keys: [

"[d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL",

"[12345678/44'/0'/0']xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"
]

In all the following examples, I will replace the xpubs with aliases in the
miniscript for brevity, and omit the corresponding vector of keys in the
wallet policy.

Of course, in comparing the "information density" (especially for UX
purposes), it is important to take the full descriptor into account.
It is always to be assumed that the keys are xpubs, complete with key
origin information if internal (that is, controlled by the software or
hardware signer that the wallet policy is being with).

=== Example 2: Either of two keys, but one is more likely ===

Miniscript policy: or(99@pk(key_likely),pk(key_unlikely))
Miniscript:           wsh(or_d(pk(key_likely),pkh(key_unlikely)))

Descriptor template: wsh(or_d(pk(@0/**),pkh(@1/**)))
Vector of keys:         <omitted>

=== Example 3: A 3-of-3 that turns into a 2-of-3 after 90 days ===

Miniscript policy: thresh(3,pk(key_0),pk(key_1),pk(key_2),older(12960))
Miniscript:
 wsh(thresh(3,pk(key_0),s:pk(key_1),s:pk(key_2),sln:older(12960)))

Descriptor template:
wsh(thresh(3,pk(@0/**),s:pk(@1/**),s:pk(@2/**),sln:older(12960)))))
Vector of keys:         <omitted>

=== Example 4: The BOLT #3 received HTLC policy ===

Miniscript policy:
andor(pk(key_remote),or_i(and_v(v:pkh(key_local),hash160(395e368b267d64945f30e4b71de1054f364c9473)),older(1008)),pk(key_revocation))
Miniscript:
 wsh(andor(pk(key_remote),or_i(and_v(v:pkh(key_local),hash160(395e368b267d64945f30e4b71de1054f364c9473)),older(1008)),pk(key_revocation)))

Descriptor template:
wsh(andor(pk(@0/**),or_i(and_v(v:pkh(@1/**),hash160(395e368b267d64945f30e4b71de1054f364c9473)),older(1008)),pk(@2/**)))
Vector of keys:          <omitted>

=== Example 5: Taproot complex script (2-of-2 with cold backup and
timelocked inheritance) ===

The likely path is a 2-of-2 of a hot_key and a cosigner_key (2FA-like
service). At any time, a cold_key can be used for signing, and after about
a year, a separate timelocked_key becomes active (for example, to a notary
for inheritance purposes).
The timelock is reset every time UTXOs are spent.

Miniscript policy: or(99@thresh(2,pk(hot_key),pk(cosigner_key)),1@or(99@pk
(cold_key),1@and(pk(timelocked_key),older(52596))))
Miniscript:
 tr(cold_key,{and_v(v:pk(timelocked_key),older(52596)),multi_a(2,hot_key,cosigner_key)})

Descriptor template:
tr(@0/**,{and_v(v:pk(@1/**),older(52596)),multi_a(2,@2/**,@3/**)})
Vector of keys:          <omitted>

=== Example 6: Taproot complex script with MuSig2 ===

The same policy as above, but we assume that the hot wallet and the
cosigner are able to engage in the MuSig2 protocol.
This greatly exemplifies the practical advantage of MuSig2 with taproot in
terms of both transaction cost and privacy.

Miniscript policy: or(99@musig2(hot_key,cosigner_key),1@or(99@pk
(cold_key),1@and(pk(timelocked_key),older(52596))))
Miniscript:
 tr(musig2(hot_key,cosigner_key),{and_v(v:pk(timelocked_key),older(52596)),pk(cold_key)})

Descriptor template:
tr(musig2(@0,@1)/**,{and_v(v:pk(@2/**),older(52596)),pk(@3/**)})
Vector of keys:         <omitted>. Note: the order of keys differs from the
previous example.

Salvatore Ingala