Du kannst diesen Artikel auch auf Deutsch lesen.
Part 1: How does Bitcoin Script work, and why is it so difficult?
Part 2: What's Miniscript, and how does it make Bitcoin scripting easier?
Part 3: Writing a Miniscript parser and analyzer in Go
In the first part of this article series, we looked at examples of how to lock bitcoins using elaborate conditions like
(person1 can spend anytime) OR (person2 can spend in one year) using Bitcoin Script, Bitcoin's native scripting language. These examples demonstrated how difficult it was for developers to craft such conditions and use them in practice due to the shortcomings and pitfalls of Bitcoin Script. It was difficult to develop the script that encodes the desired spending conditions, as well as creating valid witnesses that can spend the bitcoin.
Miniscript solves these problems by introducing fragments that encapsulate pieces of Bitcoin Script, making it easier to write more complex Bitcoin Scripts by composing the fragments.
This article dives into the details of how Miniscript works and how it maps to Bitcoin Script.
Please take the time to read the first part before diving in, as we will rely on the Bitcoin Script concepts introduced there, e.g. how Bitcoin Scripts are executed to determine if a coin can be spent.
What is Miniscript?
Miniscript comes in two independent pieces:
The policy language is a high level expression language that is composable and easy for humans to read and write. The policy language can automatically be compiled into a suitable Miniscript by a policy compiler. An example policy is
or(pk(key_1),pk(key_2)), which means "either key_1 or key_2 can spend".
A Miniscript expression can look for example like
or_b(pk(key_1),s:pk(key_2)), which was compiled from the above policy using sipa's compiler. It is a composition of fragments such as
Each fragment maps to a particular piece of Bitcoin Script. For example,
pk(key) maps to
<key> OP_CHECKSIG, and
or_b(X,Y) maps to
[X] [Y] BOOLOR.
Miniscript itself is an expression language that is easy to read for computers as well as for humans.
For computers, this is important so that your wallet application can decode a Miniscript expression and turn it into receive addresses and let you spend your coins.
For humans, it is important so that you can make a backup of your wallet descriptor by writing it down on paper, and use the same descriptor to interact with different apps and tools. For example, you may want to import your wallet descriptor into a portfolio monitoring or tax reporting tool, or import it into a mobile watch-only wallet. A wallet descriptor can include a miniscript and can be used to derive addresses.
The Miniscript expression language looks very similar to the policy language, so it is easy to confuse the two. It is important to understand that they are two completely independent languages:
The policy language is a tool to help developers easily craft spending conditions, as it is very easy to write. It has only a handful of primitives such as
older(time), etc., and they all compose with each other perfectly. The policy language is simply a tool for developers and is not standard. One policy expression is not guaranteed to always produce the same Miniscript expression.
Miniscript on the other hand is well specified. Only Miniscript is used to derive Bitcoin Script, valid witnesses, and perform analysis about correctness. One Miniscript expression will always map to the same Bitcoin Script.
So why can't developers write Miniscript expressions directly? Why do we rely on a policy compiler? The reason is that Miniscript expressions are not trivial to write by hand, as they inherit some of the complexities of Bitcoin Script by nature of encapsulating it:
- There are many more fragments in Miniscript than in the policy language. For example, there are at least four different ways to express
X or Yin Miniscript:
or_i(X,Y), each mapping to different ways in Bitcoin Script to express
X or Y, each with different characteristics in terms of script size, witness size, and how they compose with other fragments.
- There can be many different miniscripts that encode the same conditions. The policy compiler can help to optimize e.g. for script size to reduce transaction fees.
- Not all Miniscript expressions produce valid scripts - the fragments must be composed in a way that they obey correctness properties. The policy compiler makes sure to only produce valid miniscripts.
Hands-on: mapping to Bitcoin Script
To understand how a Miniscript expressions encodes a Bitcoin Script, let's see it in action by breaking down the following expression:
Each fragment maps to a particular piece of Bitcoin Script.
pk(key) maps to
<key> OP_CHECKSIG, and
or_b(X,Y) maps to
[X] [Y] BOOLOR.
Every fragment can also be wrapped with wrappers, denoted by letters before the colon ":". The
s:X wrapper maps to
OP_SWAP [X]. Wrappers are basically the same as every other fragment, but more concise. In a way, they are just syntactic sugar. You can think of e.g.
s:X to be the same as
dv:older(144) to be the same as
So the whole Miniscript expression above translates to this Bitcoin Script:
<key_1> OP_CHECKSIG OP_SWAP <key_2> OP_CHECKSIG OP_BOOLOR \_________________/ \ \________________/ / \ X \ X / / \ \__________________/ / \ Y=s:X / \_______________________________________________/ or_b(X,Y)
In this particular example, a witness to spend the coins would be of the form
<signature2> <signature1>, where at least one of the signatures has to be valid.
OP_SWAP is needed so the
<key_2> OP_CHECKSIG part is applied not to the top stack element, which contains the result of
<signature1> <key_1> OP_CHECKSIG, but on the element one below the top of the stack, which contains the signature for the second key.
There are a total of 22 defined fragments and 11 wrappers, each of which has a concrete mapping to Bitcoin Script. See the list of all of them and their mappings in the specification here.
Let's break down a more complicated expression. Let's recall the spending condition that we used in the first part of this series as a motivating example:
pubkey1 OR (pubkey2 in one year)
The script we used to encode this condition was fairly complicated and difficult to develop by hand:
<pubkey1> OP_CHECKSIG OP_IFDUP OP_NOTIF <pubkey2> OP_CHECKSIGVERIFY <52560 (one year)> OP_CHECKSEQUENCEVERIFY OP_ENDIF
With Miniscript however, this spending condition can be expressed with the following policy:
which compiles to Miniscript expression:
which in turn maps to the above complicated Script by mapping the fragments to their Script counterparts like this:
[X] [Y], so in this case
<pubkey2> OP_CHECKSIGVERIFY <52560> OP_CHECKSEQUENCEVERIFY
[X] OP_IFDUP OP_NOTIF [Y] OP_ENDIF, resulting in the final script above.
10@ gives a hint to the policy compiler that we expect this spending path to be much more likely than the other. The compiler can use this information to optimize the total script size for this case.
Once the Bitcoin Script is known, a wallet app can convert it into a receive address like
bc1... and let you receive coins on it. These coins will be locked using the spending conditions expressed in the original policy/miniscript.
The corresponding witnesses to spend coins can also be automatically generated from the Miniscript expression, as each fragment defines how to construct valid satisfactions and dissatisfactions for it. A Miniscript-powered wallet application would use this method to allow you to make transactions.
The list of all satisfactions and dissatisfactions for each fragment can be found under "Basic satisfactions" here.
For example, the
pk(key) fragment, which maps to
<key> OP_CHECKSIG, can be satisfied by
<signature>. The final script execution would be
<signature> <key> OP_CHECKSIG and leave a
1 on the stack if the signature was valid and a
0 otherwise. The same fragment can be dissatisfied by the empty invalid signature
<>. A dissatisfaction means that the script is not aborted and continues by leaving a
0 on the stack.
Refer to the first part of this article series about how witnesses and scripts are combined and executed to allow coins to be spent.
Let's generate the witnesses for the same expression as in the example above:
pk(key)fragment can be satisfied by
<signature>, and dissatisified by the empty signature
[X] [Y] OP_BOOLOR) can be satisifed by three different satisfactions: either X and Y are satisfied, or either one of them:
[satisfaction for Y][satisfaction for X], or
[dissatisfaction for Y][satisfaction for X], or
[satisfaction for Y][dissatisfaction for X].
- Combining the two:
or_b(pk(key_1),s:pk(key_2))has these three valid witnesses:
In this article we looked at how Miniscript simplifies the development of complex spending and how it enables wallet applications to receive and spend coins locked with such conditions.
In the next instalment, we will write an actual full-fledged Miniscript parser and correctness analyzer in the Go programming language. It will be capable of generating Bitcoin receive addresses from arbitrary Miniscript expressions. Stay tuned!
Don’t own a BitBox yet?
Keeping your crypto secure doesn't have to be hard. The BitBox02 hardware wallet stores the private keys for your cryptocurrencies offline. So you can manage your coins safely.
The BitBox02 also comes in Bitcoin-only version, featuring a radically focused firmware: less code means less attack surface, which further improves your security when only storing Bitcoin.
Shift Crypto is a privately-held company based in Zurich, Switzerland. Our team of Bitcoin contributors, crypto experts, and security engineers builds products that enable customers to enjoy a stress-free journey from novice to mastery level of cryptocurrency management. The BitBox02, our second generation hardware wallet, lets users store, protect, and transact Bitcoin and other cryptocurrencies with ease - along with its software companion, the BitBoxApp.