Blockstream / miniscript-templates

Templates for Miniscript-based spending policies. For use by Bitcoin wallet developers and users.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Not compatible with hardware wallets - templates should define Miniscripts, not policies

benma opened this issue · comments

(this issue is converted from this PR comment: #11 (comment))

Hi

Nice effort, being able to abstract policies/Miniscripts from users and still give them access to its power is very much needed.

A policy can be compiled to many different Miniscripts that encode the same semantics, and the output of the policy compilation can change between compilers and even between compiler versions.

Hardware wallets generally work at the Miniscript/descriptor level and don't compile policies.

It would help if you could describe how Miniscript templates should work with hardware wallets in more detail. Ideally the hardware wallet user sees a high level description of the semantics based on these templates, but then the templates can't be policies (only), as the compiled Miniscript/descriptor could unexpectedly change.

Another point where guidance would help is with backups: the user needs to have a valid backup of the full descriptor to be able to restore, and at the same time not trust their computer to display the correct information.

Did you already brainstorm solutions to these?

@Rob1Ham

Given the variation of policies that can result in effectively the same spending policies, but different script construction and therefore resulting in different addresses, it probably is best to default to an Output Descriptor post compilation.

I agree that would be good, but one problem might be that Miniscripts/Output descriptors cannot be parametrized like policies can. Changing for example thresholds or number of participants etc. in a policy can result in a totally different Miniscript. So templates might have to be defined for fixed policies without parameters that structurally impact the Miniscript.

As far as I can tell, the current mintts in #11 follow this, but it's something to be mindful of in the future.

Especially for the interoperation with Hardware Wallets, we want to have an explicit predetermined way that a given MINT would align with hardware wallets, and not have to have hardware wallet firmware/software wallet versions whenever the compiler comes out with new versions.

It's not only about the feasibility of bundling a compiler in the HWW, it does not make sense to do it in general: changing a Miniscript (which can happen with different compilers) changes the derived pkScripts/adresses, which would break the wallet (previous UTXOs not accessible anymore, etc). Fyi, here is an example of compilers not agreeing on the Miniscript output (both are valid, but different): sipa/miniscript#139

My ultimate goal with this project is to have a repository of known and validated application of miniscript templates which enable a synergy between users, hardware wallet signers, and software wallet developers. We remove uncertainty and opportunities for user error by deciding on a specific output descriptor for everyone to align on.

👍

It is possible for a HWW to work only with descriptors/miniscripts and still present the user a policy, by "lifting" the miniscript to policy. This is a much simpler operation than compilation and has a unique output. (You can do it by hand by dropping the _xyz part and the xyz: part of each combinator.)

It may be that we want to make mints independent of the specific miniscript, in which case if the HWW uses any miniscript which lifts to a give mint, it would display the mint rather than the script to the user.

I concur with @apoelstra - to reduce maintenance overhead long term - this repo should limit its scope. Each HWW can provide specifics of their implementation in their own documentation.

In this case though we should write a spec defining the policy language and the process of "lifting". Which is a bunch of work...but IMHO ultimately less work than attempting to specify actual miniscripts (which will be messy, risks us codifying something inefficient and having vendors ignore us to save fees, risks us codifying a tradeoff between privacy/efficiency that wallets don't agree with, makes it impossible for us to adapt to branches which are more or less likely to be taken, etc).

FWIW the policy language consists of:

  • leaves pk, sha256, hash256, ripemd160, hash160
  • internal nodes thresh, and, or

And the lifting process is very straightforward. (It's not quite "just drop the _s and :s because it also collapses multi/multi_a/thresh into thresh and maybe a couple other things).

This may even be worth an actual BIP (or BIN or whatever).

There are a couple technical questions we'd need to answer:

  • Should the policy language support OP_TRUE and OP_FALSE (in miniscript these are fragments 1 and 0 IIRC) or should we have a rule for collapsing these away.
  • Should the policy language support a top-level ANYONECANSPEND (which in Miniscript can be described as just 1) or NOBODYCANSPEND (described as 0). Presumably not, but then the lifting process needs to be able to fail.
  • We should specify what it mean for two policies to be "equivalent". Obviously if they are literally equal. But what if they are equal up to ordering of nodes? What if they are equal up to some simple logical transformations? (General functionality equality appears to be NP-hard, even for policies which are monotone functions which ought to be simpler than arbitary circuits .... though I don't know this for sure and I'd love for a motivated grad student to answer this.)
  • Should the policy language directly support placeholder values for timelocks, hashes and keys?

It MINTTS specified the Miniscript/descriptor, it could lead to improved UX and security with backups, in that users could backup the MINTT number and the keys in use instead of needing to understand output descriptors. Similar to how multisig users often backup the keys and "2-of-3 PW2SH multisig", which is easy to understand and verify for users. Output descriptors containing Miniscripts are very hard for users to verify.

.but IMHO ultimately less work than attempting to specify actual miniscripts (which will be messy, risks us codifying something inefficient and having vendors ignore us to save fees, risks us codifying a tradeoff between privacy/efficiency that wallets don't agree with, makes it impossible for us to adapt to branches which are more or less likely to be taken, etc).

It does sound like a ton of work for everyone with lots of difficulties, and code bloat in hardware signers. Issues with specific MINTTs like your examples could be fixed by simply specifying new ones. This sounds much easier to me while keeping the benefits of knowing the exact output descriptor (e.g. better UX/security for wallet backups, see above).

Hi all.

I applaud the efforts to improve the UX for users of miniscript policies (with the ultimate goal being: not having to show the actual policy to the user in most cases); however, I agree with @benma's concerns.

More generally, I'm skeptical that any approach based on human-compiled lists of templates can make substantial progress on the matter.

It is possible for a HWW to work only with descriptors/miniscripts and still present the user a policy, by "lifting" the miniscript to policy. This is a much simpler operation than compilation and has a unique output. (You can do it by hand by dropping the _xyz part and the xyz: part of each combinator.)

It may be that we want to make mints independent of the specific miniscript, in which case if the HWW uses any miniscript which lifts to a give mint, it would display the mint rather than the script to the user.

It's unclear that showing the lifted policy is generally safe, as it's been pointed out. It opens the door to malware swapping your actual miniscript while keeping the lifted policy unchanged, something you would only discover when you try to recover from your backup. That enables ransom attacks ("I know where your funds are, and I'll help you recover for a price").

The only way to make this reasonably safe, in my view, would be to:

  • make sure that the number of possible descriptors that lift to the same policy is bounded by a reasonably small number;
  • have ready tools to enumerate all such descriptors, so that brute-forcing all possible addresses is possible in practice.

That should be enough to deter such attacks.

One concern is that in taproot trees where each leaf is miniscript, you would lift each leaf independently. If each leaf has $N$ valid miniscripts and you have $k$ leaves, that makes it $N^k$ possible miniscripts for the entire tree. Perhaps $N^k$ never ends up being too large in practice, but I wouldn't be confident on making this assumption at this time.

Finally, the reason I'm not excited about this direction is that even if all the concerns above are addressed (which already seems to require a substantial amount of work!), I don't see showing to the user something like

thresh(3,pk(XPUB1),pk(XPUB2),pk(XPUB3),pk(XPUB4),pk(XPUB5),older(100),older(200))

as a substantial improvement over

wsh(thresh(3,pk(XPUB1),s:pk(XPUB2),s:pk(XPUB3),s:pk(XPUB4),s:pk(XPUB5),snu:older(100),snu:older(200)))

The first is surely a tad more developer-friendly, but still very unfriendly for any non-technical person.

It's unclear that showing the lifted policy is generally safe, as it's been pointed out. It opens the door to malware swapping your actual miniscript while keeping the lifted policy unchanged, something you would only discover when you try to recover from your backup.

This is true, and I take your point that we can't realistically try to limit the number of combinations, because as soon as the user composes things there'll be a combinatorial blowup.

I don't see showing to the user something like

The user wouldn't see either of these -- though what they do see would look closer to the first than the second, regardless of whether MINTTs specify descriptors or policies. The problem in the descriptor+miniscript case is that the MINTT itself needs to be much more complicated: it needs to specify the exact miniscript, describe under what parameter sets the miniscript changes (because we always want to be recommending the fee-minimizing form of the policy).

To chime in here, I am greatly appreciative of the input of @benma, @bigspider, and @scgbckbone around the hardware wallet perspective. Having used all three of your products regularly, my intent with the minscript template/mint project was to provide a framework where some of the complexities associated with an end user could be abstracted away by standardizing common types of miniscript output descriptors

Reading through the comments here, my takeaways on how to improve this with hardware wallet functionality in mind:

  1. Templates should not be generally parameterized. If a given template has any deviation, it should have its own template number, to avoid any deviation from what a compiler would formally output versus what a template permits. Does this also extend to timelocks? Either by duration, or going between blockheight and epoch timestamp?
  2. With the deviations between policy and output descriptors, templates maybe should include a policy as a reference, but explicitly a template should be codified as an output descriptor after it has gone through a compiler, to remove any possible ambiguity for the expected on chain behavior.

There are a couple technical questions we'd need to answer:

  • Should the policy language support OP_TRUE and OP_FALSE (in miniscript these are fragments 1 and 0 IIRC) or should we have a rule for collapsing these away.
  • Should the policy language support a top-level ANYONECANSPEND (which in Miniscript can be described as just 1) or NOBODYCANSPEND (described as 0). Presumably not, but then the lifting process needs to be able to fail.
  • We should specify what it mean for two policies to be "equivalent". Obviously if they are literally equal. But what if they are equal up to ordering of nodes? What if they are equal up to some simple logical transformations? (General functionality equality appears to be NP-hard, even for policies which are monotone functions which ought to be simpler than arbitary circuits .... though I don't know this for sure and I'd love for a motivated grad student to answer this.)
  • Should the policy language directly support placeholder values for timelocks, hashes and keys?
  • I haven't seen a use case of miniscript yet that references OP_TRUE and OP_FALSE, so I am unsure of the tradeoffs here.
  • What would be the use case for a top level ANYONECANSPEND/NOBODYCANSPEND option, that could not be greater reduced to just an OP_FALSE/OP_TRUE output? Would result in the same outcome in a much more compact script
  • From my perspective, I'd consider it equivalent if it was possible to have the same set of possible spending conditions be executed, even if nodes are not in the same order. General rules that could be deemed as equivalent are if Nodes within an and() operation are the same (and(pk(A),pk(B)) = and(pk(B),pk(A))), and nodes across the same level with an or() operation (or(pk(A),and(pk(B),older(1))) = or(and(pk(B),older(1)),pk(A))).
  • I think timelocks/hashes/keys having placeholders would be a very compelling use case for further extending the customization
  • Templates should not be generally parameterized. If a given template has any deviation, it should have its own template number, to avoid any deviation from what a compiler would formally output versus what a template permits. Does this also extend to timelocks? Either by duration, or going between blockheight and epoch timestamp?

Ok, this greatly simplifies things. (Yeah, fine if timelocks are "parameters" since they don't meaningfully change the structure of the descriptor.) In this case I agree with others that we should be encoding descriptors, not policies (though maybe we want a policy as a sort of reference, as you suggest). And in that case all my further comments are moot.

But FWIW, Miniscript itself supports ANYONECANSPEND and NOBODYCANSPEND. Not because there is any use case, but because it comes out of the desire that Miniscript as a language doesn't really distinguish between top-level descriptors (in which case these fragments have no value) and leaf nodes (in which case they're sometimes useful to help with composeability, e.g. in the case that you do or_i(X, 0) where X is something useful and 0 is unspendable).

  • From my perspective, I'd consider it equivalent if it was possible to have the same set of possible spending conditions be executed, even if nodes are not in the same order. General rules that could be deemed as equivalent are if Nodes within an and() operation are the same (and(pk(A),pk(B)) = and(pk(B),pk(A))), and nodes across the same level with an or() operation (or(pk(A),and(pk(B),older(1))) = or(and(pk(B),older(1)),pk(A))).

I think that if we're specifying descriptors then equivalence should mean equality. No reordering or functional equivalence or anything. If we were to consider reordering etc. then that's a step toward a potentially-ambiguous "policy language". Which as others have said, could be a DoS/ransom risk.

  • I think timelocks/hashes/keys having placeholders would be a very compelling use case for further extending the customization

Yeah, agreed. So we could maybe specify something like "Miniscript with placeholder keys". Though I would argue that this is an obvious enough extension that we don't need to specify it.