What are the current problems with Bitcoin Script?

There are ~ 163 Bitcoin Script OPCODES in use today (although using some actively makes the transaction invalid), however the combinatorial possibility for a simple machine with relatively few primatives is still very large…​ We can find Bitcoin Core v0.21.0’s per-transaction script limitations on Github (and below):

// Maximum number of bytes pushable to the stack
static const unsigned int MAX_SCRIPT_ELEMENT_SIZE = 520;

// Maximum number of non-push operations per script
static const int MAX_OPS_PER_SCRIPT = 201;

// Maximum number of public keys per multisig
static const int MAX_PUBKEYS_PER_MULTISIG = 20;

// Maximum script length in bytes
static const int MAX_SCRIPT_SIZE = 10000;

// Maximum number of values on script interpreter stack
static const int MAX_STACK_SIZE = 1000;

// Threshold for nLockTime: below this value it is interpreted as block number,
// otherwise as UNIX timestamp.
static const unsigned int LOCKTIME_THRESHOLD = 500000000; // Tue Nov  5 00:53:20 1985 UTC

// Maximum nLockTime. Since a lock time indicates the last invalid timestamp, a
// transaction with this lock time will never be valid unless lock time
// checking is disabled (by setting all input sequence numbers to
// SEQUENCE_FINAL).
static const uint32_t LOCKTIME_MAX = 0xFFFFFFFFU;

// Tag for input annex. If there are at least two witness elements for a transaction input,
// and the first byte of the last element is 0x50, this last element is called annex, and
// has meanings independent of the script
static constexpr unsigned int ANNEX_TAG = 0x50;

// Validation weight per passing signature (Tapscript only, see BIP 342).
static constexpr uint64_t VALIDATION_WEIGHT_PER_SIGOP_PASSED = 50;

// How much weight budget is added to the witness size (Tapscript only, see BIP 342).
static constexpr uint64_t VALIDATION_WEIGHT_OFFSET = 50;

…​and some script policies also on GitHub (and below):

/** Maximum number of signature check operations in an IsStandard() P2SH script */
static const unsigned int MAX_P2SH_SIGOPS = 15;
/** The maximum number of sigops we're willing to relay/mine in a single tx */
static const unsigned int MAX_STANDARD_TX_SIGOPS_COST = MAX_BLOCK_SIGOPS_COST/5;
/** Default for -bytespersigop */
static const unsigned int DEFAULT_BYTES_PER_SIGOP = 20;
/** The maximum number of witness stack items in a standard P2WSH script */
static const unsigned int MAX_STANDARD_P2WSH_STACK_ITEMS = 100;
/** The maximum size of each witness stack item in a standard P2WSH script */
static const unsigned int MAX_STANDARD_P2WSH_STACK_ITEM_SIZE = 80;
/** The maximum size of each witness stack item in a standard BIP 342 script (Taproot, leaf version 0xc0) */
static const unsigned int MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE = 80;
/** The maximum size of a standard witnessScript */
static const unsigned int MAX_STANDARD_P2WSH_SCRIPT_SIZE = 3600;

Because constructing Scripts is cumbersome and potentially error-prone, we’re not anywhere close to utilising Script’s full potential. It’s difficult to consrtuct correct Scripts initially, hard to verify their correctness and even more difficult to find the most cost-effective way to construct contractual operations with Script.

In addition to this, when you’ve designed your perfect Script, you now need to write other software (wallets etc.) which can construct, verify and generally interoperate with the custom script types, and every time you want to roll out a new Script type, all this must be repeated!

What if instead Bitcoin applications could work with any script, not just the few scripts they were designed for? We wouldn’t be restricted to using one-off designs, and could start designing applications that build and use on-the-fly generated scripts, based on user-specified requirements. Wallet developers could also introduce more script-based options while retaining interoperability with other wallets.

— Wuille & Poelstra

Miniscript

Miniscript is a language for representing Bitcoin Scripts in a structured way, which enables efficient analysis, composition, signing and more.

This allows for example this Bitcoin Script:

<A> OP_CHECKSIG OP_IFDUP OP_NOTIF OP_DUP OP_HASH160 <hash160(B)> OP_EQUALVERIFY OP_CHECKSIGVERIFY <144> OP_CSV OP_ENDIF

where A and B are public keys, to be converted into Miniscript notation as

or_d(c:pk(A),and_v(vc:pk_h(B),older(144)))

This notation makes it clear that the semantics of the script are permitting spending when either A signs, or when B signs after 144 blocks. A large portion of meaningful scripts can be written this way.

Making scripts human (or engineer…) readable is just one of Miniscript’s features. Its primary purpose is enabling automated reasoning over Script, as demonstrated by the use cases below.

Optimised Script

There are often many ways of expressing the same operation (e.g. requiring a signautre) in Bitcoin Script, and whilst an experienced developer might be able to "intuitively" know which way could be optimal on the balance of probability, this is not always correct nor scalable to new developers.

Wuille’s online compiler (also available as C++ source and as part of the rust-miniscript library) can instantly find the optimal Miniscript corresponding to a give spending policy. In the example given above this would be obtained by compiling the policy

or(99@pk(A),1@and(pk(B),older(144)))

…​which is a way of writing that the left side of the or (A signs) has a 99% probability of being taken, and the right side (B signs after 144 blocks) has a 1% chance of being taken.

Generic Spending

Once an efficient script has been found, a common barrier to many uses of Script is the lack of interoperability between different spending mechanisms. Users who wish to implement long-term timelocks or complex multisignature requirements may be blocked by the fear that their counterparties do not (or will not) have the software to recognize and spend the coins controlled by the policy.

Miniscript, by virtue of directly representing spending conditions, allows arbitrary policies to be expressed such that anyone can:

  1. compute the associated address(es) to the script;

  2. determine which signers are necessary or sufficient to signing at a given time;

  3. produce a valid transaction, given a sufficient set of signatures.

Users do not need to worry that all participants are using compatible software, or that such software will continue to exist by the time such timelocks are needed.

They also do not need to worry that their needs may change in a way incompatible with their signing software, assuring them that their use of Bitcoin Script will not constrain them.

Proof-of-Reserves

Related to the problem of signing is the problem of proving reserves, a process by which a company proves that it can spend a set of bitcoins without actually spending them. While tools exist for this, such as Blockstream’s Proof of Reserves Tool, there is not yet a common standard used within the industry. Absent Miniscript, there was probably no possibility of achieving a standard that could encompass the diversity of custody solutions in use today.

Composition of Spending Policies

Let’s take a look at a specific example of a Bitcoin Script developer constrained by the need for interoperability. Consider the case of a company which is custodying a large number of bitcoins, and wishes for these coins to be spendable only with the consent of a majority of its directors. However, some individual directors want to use their own existing wallet software and hardware, with complex signing setups involving multiple keys across disparate locations, and don’t want to participate in a scheme that requires they sign using a single key provided by a dedicated application.

Without Miniscript, producing a script that encompassed all signers’ requirements, while assuring all signers that the complete script was sound and complete, and that their wallet software was compatible with the result, would present an insurmountable problem.

Using Miniscript, these directors can simply compose their policies in their own wallet software, constructing a single policy which describes a threshold requirement, then compile this to Miniscript. They can straightforwardly verify that the output from the compiler matches the original policy, and that the original policy meets everyone’s requirements. They can then use any Miniscript-compatible software to compute addresses on which to receive coins, collect signatures at the time of spending, and assemble these signatures into a valid transaction.

Miniscript builds upon and integrates with many other projects in the Bitcoin ecosystem. In particular, as described above, Miniscript extends Bitcoin Core’s wallet’s output descriptors and complements PSBT to enable fully general updaters and finalizers.

Another blockchain language related to Miniscript is Blockstream’s Simplicity. Like Miniscript, Simplicity is a low-level language designed to be directly embedded in blockchain transactions. Also like Miniscript, Simplicity supports many forms of static analysis that are critical when deploying blockchain contracts, but which nonetheless are very hard or impossible by design in alternatives such as Ethereum’s EVM.

Unlike Simplicity, which is powerful enough to express any computable function, Miniscript is very limited in scope: it can express signature requirements, timelocks, hash preimages, and arbitrary combinations of these. This reduction in scope makes Miniscript easier to reason about for the use cases that it covers, and importantly, allows it to work on the existing Bitcoin blockchain. Simplicity, in contrast, is a radical departure from Bitcoin Script and is still in the early stages of its development.

Conclusions

To make this technology accessible we’ll need integration in commonly used pieces of software. With a Miniscript-compatible PSBT implementation (updater and finalizer), many PSBT signers (including hardware wallet-based ones) could become usable for complex scripts, even without explicit support.

Things we learnt along the way:

  1. Script resource limits complicate policy optimization: The multitude of consensus- and standardness-imposed resource limits (max opcodes, max script size, max stack size, …) make it much harder to find the optimal script for a given policy once it comes close to hitting these limits. This is an interesting insight, as it may guide suggested improvements to Script in the future.

    Relatedly, we were also surprised to learn that the Bitcoin consensus rules actually count the number of keys participating in an executed OP_CHECKMULTISIG(VERIFY) towards the 201 non-push opcodes per script limit.

  2. Variable witness sizes complicate fee estimation: Simple payments and multisig constructions have a witness size that is independent from the exact set of keys that is present. Once we move to more complicated scripts, the witness size becomes variable and may complicate fee estimation. Prospective signers for a particular transaction output may need to guess optimistically that a cheap path will be taken, and construct a corresponding transaction. If not all keys or hashes for that path end up being available, the transaction itself may need to be changed to account for the increased fees.

  3. Segwit’s stack encoding complicates optimization: Since Segwit the input stack is no longer encoded as a script, but as a list of stack elements directly. This had the unfortunate side effect of changing the encoding of “true” from 1 byte to 2 bytes. Due to this, Miniscript needs to care about what subexpressions go into what side of an IF/ELSE/ENDIF structure.

  4. Malleability: Since Segwit, transaction malleability is no longer a deal breaker for the correctness of protocols, but it may still have undesirable effects (such as uncertainty about the fee rate while propagating a transaction, slowing down compact block propagation, and interfering with the usage of hash locks are global publication mechanism). Because of these reasons, we designed Miniscript with non-malleability in mind, and learned how to reason about non-malleability of witnesses in general. To achieve that, Miniscript relies on certain Segwit-specific rules as non-malleability is very cumbersome to achieve in pre-Segwit transaction rules.

  5. Common subexpression elimination is hard: Despite various attempts, Miniscript does not support optimizing out repeated subexpressions. While this is possible in certain specific cases, it seems this is very hard to do in Script in general. The addition of certain stack manipulation opcodes could change this.

Minsc is a high-level scripting language for expressing Bitcoin Script spending conditions. It is based on the Miniscript Policy language, with additional features and syntactic sugar sprinkled on top, including variables, functions, infix notation, and more.

Minsc has a tonne of examples on their website too to look at!