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
The only Bitcoin spending conditions in widespread use today are simple single-sigs and simple multisigs, even though Bitcoin Script, the language used to encode spending conditions in Bitcoin transactions, is much more powerful than that. Why is that the case?
The reason for this is that Bitcoin Script, while appearing to be a simple stack-based language on the surface, is actually very difficult to use in practice. For every new spending condition a developer might want to create, a lot of time has to be spent making sure it is correct and sound under all circumstances, which can be hard to reason about. We will look into a motivating example shortly.
Most importantly, the lack of standardization and tooling for these types of scripts can make it difficult for wallets and other software to interoperate with them. In practice this means that even if you decide to take on the effort of developing a new script, you will end up with a wallet that is non-standard. Other wallets will not be compatible, which is obviously bad for users.
In this article series, we will dive deep into Bitcoin Miniscript. Bitcoin Miniscript is a high-level language for expressing Bitcoin Script, aiming to make it easy for wallet developers to create complex spending conditions and to reason about their correctness and soundness, and for all wallets and tools to easily interact with them.
As developers of the BitBox02 hardware wallet, we are always exploring ways to improve security and usability for self-custody. In the past, we've implemented and deployed the anti-klepto protocol and improved multisig security for all hardware wallet vendors.
Kevin and Antoine of Revault/wizardsardine contacted us to discuss the possibility of adding support for Miniscript to the BitBox02. It was also a hot topic of the BTC Azores '22 unconference, where Antoine gave a great lesson about it and Salvatore showed his progress about integrating Miniscript into Ledger
We see Miniscript, along with covenants and MuSig, as important developments for self-custody and the BitBox02.
Miniscript enables more advanced spending conditions, which Wizardsardine showcases in Liana, a new type of wallet they are developing.
By adding support for Miniscript to the BitBox02, advanced wallets like Liana can be secured by your BitBox02. This would also open the door for the development of more advanced self-custody solutions within the BitBoxApp.
Before any integration, we first needed to build up our knowledge about Miniscript. In my experience, the best way to learn about a software engineering topic is through hands-on implementation. By building it yourself, you are forced to consider every detail. Another effective method is to explain it to others. This blog post series serves both purposes - it includes an implementation of Miniscript and aims to teach you Miniscript in a different format that what is available today.
Very short primer on Bitcoin Script
Bitcoins are generally locked by scripts which encode what kind of conditions need to be met in order to spend the coins. In this series we will focus on P2WSH (Pay-to-Witness-Script-hash). There, the witness script encodes the conditions that need to be met in order to spend a bitcoin. They commonly include public keys. The witness is the data required to satisfy the spending conditions. Witnesses commonly include signatures corresponding to the public keys.
To spend a coin locked with a witness script, the transaction spending it must include a valid witness. The witness and the witness script are evaluated according to the Bitcoin Script rules.
A Bitcoin address like for example
bc1q2fhgukymf0caaqrhfxrdju4wm94wwrch2ukntl5fuc0faz8zm49q0h6ss8 is simply an encoding of the fact that it is a P2WSH output containing the hash of the witness script. Bitcoin nodes know that when they see a coin sent to such an output, the transaction that spends this coin must include the corresponding witness script, plus the witness needed to satisfy the witness script.
For example, to encode a simple single-signature key condition, the witness script would be:
and the witness would be
When verifying the script, the witness and the witness script are executed in order, starting on an empty stack:
- Initial stack: empty.
- Signature is pushed on the stack:
publicKeyis pushed on the stack:
OP_CHECKSIGremoves the two top stack elements, verifies the signature, and pushes a
0upon failure, or a
If exactly one non-zero element is left on the stack and the script did not abort, the witness is valid and the coin can be spent.
In Bitcoin Script, there is a whole array of different OP-codes apart from
OP_CHECKSIG, which can be used to encode more complex spending conditions. For example, you can lock coins in a multi-signature output using
OP_CHECKMULTISIG, or lock the coins for a period of time using
Motivating example for Miniscript
Miniscript helps developers create more secure and efficient Bitcoin scripts by addressing several issues with the Bitcoin Script language. Let's look at some simple spending conditions one might want to have to illustrate the difficulties of working directly with Bitcoin Script. Later, we will see how Miniscript solves these problems and makes it much easier to develop and deploy new spending conditions.
For example, let's say we want one of two persons to be able to spend a coin. The simplest solution is to use
OP_CHECKMULTISIG. The witness script is:
1 <publicKey1> <publicKey2> 2 OP_CHECKMULTISIG
OP_CHECKMULTISIG how many signatures need to be provided, and the
2 tells it how many public keys there are.
The witness is:
(the empty element in the begining of the witness is actually useless and exists because of a bug in the original implementation of
OP_CHECKMULTISIG, which removes one element too many from the stack).
The resulting verification script,
<> <signature> 1 <publicKey1> <publicKey2> 2 OP_CHECKMULTISIG, leaves a
1 on the stack if there is one valid signature matching either public key, or
Now let's slightly change the semantics. Instead of
pubkey1 OR pubkey2, let's try
pubkey1 OR (pubkey2 in one year): the coin can be spent by one person at any time, or by another person after waiting for one year. Since
OP_CHECKMULTISIG can only check signatures and not time locks, the script has to change completely. There can be many scripts that implement one set of spending conditions. Here is one of many possible solutions for this one:
<pubkey1> OP_CHECKSIG OP_IFDUP OP_NOTIF <pubkey2> OP_CHECKSIGVERIFY <52560 (one year)> OP_CHECKSEQUENCEVERIFY OP_ENDIF
52560 is one year in number of Bitcoin blocks, which arrive once every ten minutes on average.
- At any time:
<signature for pubkey1>
- Only if one year has passed:
<signature for pubkey2> <>.
Let's look at the execution of the script using the second witness. As before, the witness and the witness script are executed in order starting on an empty stack. Assuming one year has passed and the signature is valid:
- Initial stack: empty.
- Push signature:
<signature for pubkey2>
- Push empty element:
<signature for pubkey2> <>
- Push pubkey1:
<signature for pubkey2> <> <pubkey1>
- OP_CHECKSIG removes two elements
<pubkey1>from the stack and checks the signature against this pubkey. Since the signature is empty, it is an invalid signature and
<signature for pubkey2> 0.
- OP_IFDUP duplicates the top stack element if it is not zero. Since it is zero, nothing happens:
<signature for pubkey2> 0.
- OP_NOTIF: Removes the top stack element. If it is 0, the statements until OP_ENDIF are executed:
<signature for pubkey2>.
- Push pubkey2:
<signature for pubkey2> <pubkey2>.
- OP_CHECKSIGVERIFY removes two elements (signature and pubkey) and verifies the signature. If valid, nothing happens, otherwise execution is aborted with an error). Stack is now: empty.
- Push 52560 (numeric value representing one year):
- OP_CHECKSEQUENCEVERIFY looks at the last element as a time duration. If the coin to be spent is younger, the script aborts with an error. If it is older, nothing happens:
- OP_ENDIF: ends the OP_IF block.
- The stack has exactly one non-zero element left, so the script succeeds and the coin can be spent.
In step 5, the empty signature element is called a "dissatisfaction". It is required to skip over the part that verifies the first pubkey, which we did not want to use. Note that only the empty element is a valid dissatisfaction for
<key> OP_CHECKSIG according to BIP141 - any other invalid signature results in script abortion:
Signature(s) must be null vector(s) if an OP_CHECKSIG or OP_CHECKMULTISIG is failed (for both pre-segregated witness script and P2WSH. See BIP146)
Note also that the script only succeeds because the time-lock of one year is non-zero. If we used the same script but simply replaced the number from one year to 0, you might assume that our original semantics
pubkey1 OR (pubkey2 in one year) would change to
pubkey1 OR (pubkey2 anytime). Since a 0 would be left on the stack at the end, the script would always fail and the actual semantics has accidentally changed to
pubkey1 only, and the second pubkey can never spend.
As a hands-on exercise, try to execute the script above using the first witness, assuming the first person signs and the second person doesn't, and convince yourself that it works.
As you can see, creating such scripts and valid witnesses in Bitcoin Script is a laborious and error-prone process, even for simple semantics. The sequence of stack operations is complicated and challenging to construct and reason about. If you want to expand the spending conditions, the development process basically starts from scratch. A deep understanding of Bitcoin Script is required. Among other things, you need to be aware of the cleanstack rule (which requires only one non-zero element left at the end of execution) and that only the empty element is a valid dissatisfaction for a signature check.
To summarize, working directly with Script is difficult for the following reasons:
- Script does not compose well, meaning that small changes in desired spending conditions can result in vastly different scripts.
- The Script op-codes have different semantics for failure/success, making it difficult to compose and reuse them. Some of them push a 0 or 1 on the stack upon failure or success, while others abort the whole execution upon failure and nothing in case of success.
- There are many possible solution scripts for one set of desired spending conditions, making it hard to decide which one to use.
- Creating valid witnesses for all circumstances is difficult.
- There are consensus and standardness limits on the size of scripts, the number of opcodes and signatures, and the number of the witness stack elements which a developer must take into consideration to avoid rejection by the network.
- Designing a complex script that leaves exactly one non-empty element on the stack at the end of the execution is challenging, as we saw in the example above.
In the next instalment of this series, we will take a look at what Miniscript is in detail and how it drastically simplifies Bitcoin Script, making it feasible to use elaborate spending conditions in practice. Stay tuned!
Go to part two.
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.