This is the third and final post in our series about the improvements we’ve deployed on Liquid as part of our Taproot launch. Our previous two posts talked about Taproot itself and Partially Signed Elements Transactions. In this post, we’ll describe the new opcodes that we’ve introduced to Elements Script, what they can be used for, and what we’re doing with them.
This set of opcodes is a major set of improvements to Script since the launch of Elements Alpha in 2015. Our research team has also been innovating on Bitcoin-related applied cryptography, with significant work on MuSig, the Elements blockchain, and the Lightning Network, and continued work on scripting and Simplicity. In particular, we contributed heavily to Taproot on Bitcoin, and some of the changes to Elements Script have been inherited from that, including:
- The removal of fixed numeric limits on opcode counts and script sizes;
- The replacement of ECDSA signatures with Schnorr;
- The replacement of CHECKMULTISIG with CHECKSIGADD, allowing batch-verification of multiple signatures
- Numerous OP_SUCCESS opcodes, which expand the set of opcodes that we could soft-fork into the network relative to the old NOP opcodes.
We’ll also talk about covenants, which give Script the ability to constrain payment flows to construct “smart contracts'' that attach conditions to coins or other assets. Covenants have been proposed for Bitcoin in many forms over the years but have been present in Elements from the very beginning. With Tapscript, we’ve taken developer feedback from our original covenant implementation and introduced new opcodes that will make covenants more expressive, efficient, and ergonomic.
Finally, we’ll briefly talk about Elements Miniscript, our extension of Bitcoin’s Miniscript language, which makes use of these opcodes to produce covenant-enabled scripts. This is under rapid development, and we’re excited to see it opening new applications on Liquid. We’ll cover this subject more in a future post.
Covenants and Assets
Covenants were initially proposed for Bitcoin by Greg Maxwell in 2013 in a somewhat-joking manner, asking users to think up amusingly bad applications for them. While these bad ideas are fun to think about, they unfortunately seem to have overshadowed the idea of covenants to this day, even if the concerns are not particularly practical or even specific to covenants. Partly relating to this line of risk reasoning, covenants have remained a hotly-debated topic in the Bitcoin space.
The most recent and popular proposal for covenants in Bitcoin is OP_CTV, which has deliberately reduced functionality in order to avoid potential controversy.
In our view, these fears are largely overblown. They stem from the idea that covenants could be used to implement a “taint” on existing Bitcoins, such as requiring a 3rd-party signature for their use, which would spread throughout the coin supply and be impossible to remove. But such a scheme can actually be implemented today, by use of 2-of-2 CHECKMULTISIG scripts in which one party refuses to sign transactions that don’t preserve the 2-of-2 “covenant” restriction. Nobody has done this, and with good reason: these coins would obviously not be accepted by users in place of ordinary coins and could not be accepted by users with a fiduciary or other legal duty to custody ordinary coins. Furthermore, the resulting coins would require higher network fees to spend due to their extra weight and increase the complexity of wallets that could spend them.
Trying the same thing with covenants would be even more expensive and require even greater wallet complexity, which nobody would be interested in implementing and users would reject.
Further evidence that covenants are harmless is that they have existed on the Liquid Network since its launch in 2018, and there have been no viral covenants spreading throughout the system. (Although, as we’ll see, there are technical reasons for low uptake of covenants on Liquid in general.)
Covenants would introduce new capabilities to Bitcoin, such as vaults or velocity limits, giving users more flexibility in the ways that they custody their coins.
In Elements, where we have issued asset support, the functionality provided by covenants is much greater; we can:
- Construct open-ended limit orders or other algorithmic trades.
- Construct financial derivatives such as options.
- Create “control tokens” which are NFTs that enable functionality in other contracts.
In a multi-asset scenario with a rich set of arithmetic opcodes, we would be able to match the feature set of Ethereum and competitors, and by avoiding Turing completeness, we should be able to implement strong forms of analysis. But as we will see, the story is more complex than this.
Elements and Bitcoin Script
Elements was written in 2015 as a fork of Bitcoin with extra features and was launched as a test network called Elements Alpha that year. This network predated Segwit, let alone Taproot; it predated issued assets; it had a two-stage peg scheme which ultimately proved unnecessarily complex and bug-prone and was redesigned before deployment on Liquid. Its major features were Confidential Transactions, which were later extended to support issued assets, a nascent scheme for segregating witnesses that was simplified and ported to Bitcoin as Segwit, and a set of extra opcodes for enabling covenants. It is the latter that we’ll focus on in this post.
When we introduced the original Elements and Liquid set of opcodes, Bitcoin Script relied on manual, low-level programming and was also difficult to formally analyze. Our extensions increased the expressivity of the language but didn’t address these underlying limitations. Users were able to develop some advanced applications, such as Lending on Liquid, but this development was more a testament to determined human ingenuity than the ease of programming on our scripting platform. Other applications, such as Bitmatrix, could only be deployed after the upgrades to the script system described in this post. Since then, independently of Elements, Blockstream Research developed Miniscript, a new way to model Bitcoin Script that would solve these problems of analysis.
In particular, with Miniscript, it is possible to automatically enumerate all the keys in a script and determine which sets are needed to produce a transaction; find an upper bound on the size of such a transaction prior to gathering signatures; answer semantic questions about which conditions must be met in order to spend coins; and compute the exact encodings for scripts and witnesses. Miniscript complements Partially Signed Bitcoin Transactions, also developed by Blockstream Research’s Andrew Chow, with Pieter Wuille being part of the original concept and development, which provides a protocol for assembling the data needed to produce a transaction. This allows wallets to sign transactions even if they don’t understand (or pre-date) the exact script being satisfied; they just need to check that the transaction makes sense, produce an ECDSA or Schnorr signature, and let the Miniscript tooling do the rest.
However, even with Miniscript under our belt, we found no straightforward path to “easy covenants” on Elements. Our 2016-era covenant constructions were difficult to use in a generic way, and their large size in practice is hard to avoid being limited by the 201-opcode limit we’d inherited from Bitcoin. So even as Bitcoin Script became more powerful through Miniscript, PSBT and Tapscript (designed with Miniscript in mind), Elements’ extensions to Script were unable to come along for the ride.
Breaking Through: New Abstractions
In February of 2021, Blockstream Researchers Andrew Poelstra and Sanket Kanjalkar finally found a way to combine Elements’ covenants with Miniscript. Essentially, we would construct covenant-enabled scripts inside “the machine,” a fixed-size Script template which would use the CHECKSIGFROMSTACK (CSFS) opcode to extract all of the transactions’ data and leave it on the stack in fixed locations where the “real code'' could efficiently access it. This lets us amortize the high cost of CSFS-style covenants across many conditions on the spending transaction.
In fact, we later moved away from this initial approach, but the step propelled us past the concept limitation of how to practically construct generic covenants, and let us see what the real limitations of Elements Script were:
- 31-bit big-endian arithmetic inherited from Bitcoin, which could not be easily used with the 64-bit little-endian amounts used by transactions.
- Compounding this, no multiplication or division opcodes; Elements had added bitshifts and other arithmetic operations but not the basics needed to do, for example, fee calculations.
- For fee calculations, CSFS-style covenants can’t access the transaction weight and therefore can’t compute its feerate.
- An inability to hash more than 520 bytes of data is caused by the combination of Bitcoin’s 520-byte stack element size limit and its inflexible SHA256 opcode. Combined with CSFS’s need to hash significant amounts of transaction data at once, this created limits on total transaction sizes, making wallet authors’ lives much more difficult.
- An inability to execute scripts with more than 201 opcodes, with dozens required by the CSFS machine and dozens more needed to convert 64-bit integers into 32-bit ones and back.
- An inability to do elliptic curve arithmetic except for signature validation, which can be coerced into doing many things but not into verifying that Taproot outputs are correctly formed. Had we retained this limitation in Tapscript, it would have reduced functionality while increasing the complexity of our scripts.
Once we recognized these problems, they became straightforward to address:
- We added new opcodes for manipulating 64-bit values, including multiplication, as well as opcodes for converting them into the old-style 32-bit values for use with the ordinary Bitcoin opcodes.
- We added, “direct introspection” opcodes to access transaction data, eliminating the need for “the machine” and reducing our opcode counts. We also added a TXWEIGHT opcode to access the weight of the transaction for fee rate calculations.
- We added “streaming hash” opcodes to allow feeding data into a hash engine without needing to put it all into a single stack element. This cleanly avoids the 520-byte stack element limit without requiring any more resource usage from the script interpreter.
- We added an ECMULSCALARVERIFY opcode to handle Taproot outputs, pay-to-contract commitments, and other elliptic curve cryptography operations.
- We followed Bitcoin’s lead in eliminating the 201-opcode limit, although with the above improvements, it may have been manageable.
Once complete, we had a small set of new opcodes that easily fit into the Bitcoin Script model (i.e., no introduced disk accesses, unbounded resource use, or complex control flow such as looping) that solved big problems allowing for production-grade covenants on Elements.
Here is a complete list of the new opcodes.
Having surmounted this barrier, we are ready to tackle some deeper issues with covenants, which are not unique to Elements.
As a concrete example of new things we can do with these opcodes, consider the vaulting scheme where we restrict that only MAX_WITHDRAW can be withdrawn at a time within 60 blocks time.
To create this covenant, we need to respect the following conditions (described in Miniscript notation, which we will cover in detail in a future post).
- Partial Withdrawal(partial_withdraw): If there are more than MAX_WITHDRAW sats in the covenant, then the covenant should have at least total_value - MAX_WITHDRAW remaining.
- "num64_gt(curr_inp_v,MAX_WITHDRAW)": Total input sats more than MAX_SATS
- "asset_eq(curr_inp_asset,out_asset(0))": Input assets and output asset 0 are same
- "num64_geq(out_v(0),sub64(curr_inp_v,MAX_WITHDRAW))": The value of output should be atleast total_value - MAX_WITHDRAW
- "spk_eq(curr_inp_spk,out_spk(0))": The remaining coins are sent to the covenant
- Full Withdrawal(full_withdraw): If the covenant has less than MAX_WITHDRAW sats, then outputs have no constraint
- Key Control(keys): Wait 60 blocks before spending again and require a key K to spend it.
- pk(K): Requires signature from key K
- older(60): 60 blocks waittime before the next withdrawal
- curr_idx_eq(0): The current spending index must be 0. This prevents the so-called half spend problem where we can spend two covenant utxos together, and one of them can escape the covenant
- Finally, we combine all the above conditions: and(keys,or(full_withdraw,partial_withdraw))
It’s possible to extend these with emergency withdrawal keys, multi-step withdrawals, whitelisting, and multi-sig custodies.
Open Problems and Future Work
By extending Miniscript with covenant support and providing opcodes to do this efficiently, we created a framework in which users can easily construct covenant scripts, understand these scripts’ behavior, and create complete transactions that execute these covenant contracts. But in doing so, we undermine the recursive structure of Miniscript and therefore break some forms of analysis.
For example, it’s possible to “compose” two Miniscripts by creating an and_b node whose two children are the two original scripts. (In terms of Script, we are just concatenating the original scripts and adding a BOOLAND opcode at the end.) If a user is convinced that she can satisfy each of the original scripts independently, she can be assured that she can satisfy the combined script.
Conversely, if she wants to determine the satisfiability of a complex script, she can recursively step through the script: whenever she sees an and node she must satisfy both sub-scripts while whenever she sees an or node she must satisfy one of them.
With covenants, this form of composition is no longer valid. While you can still combine two scripts using and_b to get a syntactically valid script, the result might be unsatisfiable even if both sub-scripts can be satisfied. For example, consider combining a script that requires the first output of the transaction to burn asset A with a script that requires the first output of the transaction to burn asset B. Clearly, both cannot be satisfied simultaneously.
In general, because covenants introduce global conditions on the transaction under construction, individual script fragments can have non-local effects on other script fragments. Alternatively, we can observe that in Bitcoin Miniscript we can think of our scripts as monotone functions of spending conditions. A script represents a rule for deciding which sets of spending conditions (signatures, hash preimages, etc.) are valid to spend a coin, and which sets are invalid. These rules being monotone means that if some set of conditions is valid, then any superset is.
With covenants in the picture, this model in terms of monotone functions simply does not apply, and so we lose the general and powerful analysis tools that Miniscript provides. Instead, covenant-enabled Elements Miniscript programs need to be analyzed in an ad-hoc fashion, in which specific questions are asked and answered.
Simplicity, a wholesale replacement for Script that we are working on in parallel, also has this limitation. To make this sort of ad-hoc analysis tenable, Simplicity’s interpreter has a reference implementation in the Coq theorem proving assistant, meaning that while humans need to come up with the right questions to ask, the answers at least will be rock solid and machine-checkable. With Elements Miniscript, programs are entirely dependent on human code review. We hope that we have designed it so that powerful programs can nonetheless be simple and obviously correct.
Going forward, we plan to keep expanding the functionality of Elements Miniscript, write supporting tooling and wallet libraries that can use it to produce and interact with covenants, and continue pushing forward on Simplicity, which will be even more expressive than the newly-expanded Elements Script.