Sun. Nov 24th, 2024

Bitcoin was initially designed with a fully fleshed out scripting language, intended to encompass and support any potential safe use case that users could come up with in the future. As Satoshi himself put it before he disappeared:

“The nature of Bitcoin is such that once version 0.1 was released, the core design was set in stone for the rest of its lifetime. Because of that, I wanted to design it to support every possible transaction type I could think of. The problem was, each thing required special support code and data fields whether it was used or not, and only covered one special case at a time. It would have been an explosion of special cases. The solution was script, which generalizes the problem so transacting parties can describe their transaction as a predicate that the node network evaluates.” – Satoshi, June 17 2010.

The entire intent was to give users a general enough language that they could compose their own types of transactions as they saw fit. I.e. Give users room to design and experiment with how they programmed their own money.

Before he disappeared Satoshi ripped out 15 of these opcodes, disabling them entirely, and adding a hard limit to how big of a piece of data could be manipulated on the scripting engine stack (520 bytes). This was done because he frankly screwed up, and left open a large number of ways that complicated scripts could be used to denial of service attack the entire network, creating huge and costly to validate transactions that would crash nodes.

These opcodes weren’t removed because Satoshi thought the functionality was dangerous, or people shouldn’t be able to build the things they could with them, but solely (at least apparently) because of the risk to the network at large of them being used without resource constraints to limit the worst case validation cost they could impose on the network.

Every upgrade to Bitcoin since then has ultimately been streamlining the functionality left, correcting other less serious flaws Satoshi left us with, and extending the functionality of that subset of script we were left with.

The Great Script Restoration

At Bitcoin++ in Austin at the start of May, Core Lightning developer Rusty Russell made a pretty radical proposal during the first presentation of the conference. He essentially pitched the idea of turning back on most of the opcodes that Satoshi disabled in 2010 before he disappeared.

For the last few years since Taproot activated in 2021, the development space has been frankly kind of aimless. We all know that Bitcoin is not scalable enough to really service any sizeable chunk of the world’s population in a self sovereign way, and likely not even in a trust minimized or custodial way that can scale beyond very large custodians and service providers incapable of really escaping the long arm of the government.

Anyone who understands Bitcoin on a technological level understands this, it’s not a matter of debate. What is a matter of debate, and a very contentious one, is how to go about addressing this shortcoming. Since Taproot, everyone has been putting forward very narrow proposals intended to address only very particular use cases that could be enabled.

ANYPREVOUT (APO), a proposal to allow signatures to be reusable on different transactions as long as the script and amount of the input was the same was tailored specifically to optimize Lightning and multiparty versions of it. CHECKTEMPLATEVERIFY (CTV), a proposal to enforce coins can only be spent by a transaction that exactly matches a predefined transaction, was designed specifically to extend the functionality of chains of pre-signed transactions by making them completely trustless. OP_VAULT was designed specifically to enable a “timeout period” for cold storage schemes, so that a user could “cancel” a withdrawal from cold storage by sending it to an even colder multisig setup if their keys were compromised.

There are a bunch of other proposals, but I think you get the point. Rather than attempting to comprehensively address the expressivity and programmability needed to scale Bitcoin in a fundamental way, each of the proposals over the last few years was designed to either give a small increase in scalability or improve a single narrow functionality deemed desirable. This I think is the source of why none of these conversations is going anywhere. No one is happy with any other proposal because it doesn’t cater to the use case they want to see built.

Nothing is comprehensive enough for anyone to think, outside of the proposal originator, that it is the sensible next move forward.

That is the logic behind the Great Script Restoration. By pushing through and analyzing a comprehensive restoration of script as Satoshi initially designed it, we can actually try to explore the entire space of what functionality we need, rather than bickering and infighting over what small extension of functionality is good enough for now.

The Opcodes

  • OP_CAT: Takes two pieces of data on the stack and adds them together to form one.
  • OP_SUBSTR: Takes a length argument in bytes, and grabs a piece of data off the stack removing that many bytes from it and putting it back.
  • OP_LEFT & OP_RIGHT: Takes a length argument and removes that many bytes from one side or the other of a piece of data on the stack.
  • OP_INVERT & OP_AND & OP_OR & OP_XOR & OP_UPSHIFT & OP_DOWNSHIFT: Takes a data element from the stack and performs the corresponding bit operation on it.
  • OP_2MUL & OP_2DIV & OP_MUL & OP_DIV & OP_MOD: Math operators for multiplication, division, and modulo operations (returning the remainder of division).

The ones above are the opcodes intended to be restored. In addition to these, Rusty proposes three more to simplify composition of different opcodes.

  • OP_CTV (OR TXHASH/equivalent): Something to allow granular enforcement requiring certain parts of a transaction to be exactly as defined ahead of time.
  • CSFS: Allows verifying signatures against arbitrary data, rather than just the entire transaction. This allows you to require parts of a script, or data they use, to be signed in order to be executed.
  • OP_TWEAKVERIFY: Verifies Schnorr based operations involving public keys, such as adding or subtracting individual public keys from aggregate ones. This can be used to ensure that in the event of one party leaving a shared UTXO unilaterally, everyone else’s funds are sent to an aggregate public key that does not require the party who left to sign in order to spend cooperatively.

Why We Want To Do This

Layer 2s are inherently an extension of the base layer of Bitcoin, they are by their nature constrained in terms of functionality by the functionality of the base layer. Lightning required three separate softforks, CHECKLOCKTIMEVERIFY (CLTV), CHECKSEQUENCEVERIFY (CSV), and Segregated Witness before it was possible to actually implement it.

You just cannot build more flexible Layer 2s without a more flexible base layer. The only shortcut around that is trusted third parties, pure and simple. That is something I hope we all aspire to remove from every aspect of interacting with Bitcoin at scale that we possibly can.

There are things we need to be able to do that we just can’t do right now in order to safely pack more than two people into a single UTXO in a way that can be enforced trustlessly on the base layer, Bitcoin script is just not flexible enough. At the most basic level we need covenants, we need the ability for script to actually enforce more granular details about the transaction executing them to ensure things like a user safely exiting a UTXO on their own does not put other users’ funds at risk.

At a high view this is the kind of functionality we need:

Introspection: We need to be able to actually inspect specific details about a spending transaction itself on the stack, such as “this amount of money goes to this public key in some output.” That allows me to withdraw my money by myself using a specific Taproot branch of my own, while ensuring that I cannot take anyone else’s money. The script executing would enforce by consensus that the correct amount everyone else owns is sent back to an address composed of the other users’ public keys if I left.

Forward Data Carrying: Say we go even further than the idea of a Lightning channel with more than two people in it, we construct a single UTXO with a massive amount of people in it where anyone can come and go as they please. Somehow, almost always with a merkle tree and its root, we need some way to track who has how much money. That means when someone leaves, we have to be able to ensure that the “record” of who is entitled to what is part of the change UTXO of everyone else’s money. This is essentially a specific use for introspection.

Public Key Modification: We need the ability to ensure that modifications to aggregate public keys can be verified on the stack. The goal to shoot for in UTXO sharing schemes is that there is an aggregate key with everyone involved allowing a cooperative and more efficient movement of funds. Whenever someone leaves a shared UTXO unilaterally, we need to remove their individual key from the aggregate one. Without precomputing all of the possible combinations ahead of time, the only option is to be able to verify that subtracting one key from the aggregate creates a valid key composed of the rest of the individual keys.

How To Make This Safe: Varops

As I said above, the reason all of these opcodes were disabled was to remove risks of denial of service attacks that could quite literally crash the nodes comprising the network. There is a way to solve this, constrain the amount of resources any of these opcodes can use.

We already have such a solution when it comes to signature verification, the most expensive part of verifying Bitcoin scripts. It’s called the sigops budget. Each use of a signature check opcode consumes a certain ‘budget’ of allowed signature operations per block. This places a hard limit on the cost that transactions can impose on users to verify an individual block.

Taproot shifted the way this works, instead of using a single global block limit, each transaction has its own sigops limit proportional to the size of the transaction. This works out essentially to the same global limit, but makes it easier to reason about in terms of how many sigops an individual transaction has available.

The shift in how Taproot handles sigops limits relative to each transaction offers a way to generalize this, which is what Rusty proposes with a varops limit. The idea is to assign a cost for each of the reactivated opcodes to take into account the worst case, i.e. most expensive computational cost to validate, that each opcode could create. With this, every one of these opcodes would have its own “sigops” limit of sorts to restrain how many resources it could consume in verification. It would also be based on the size of any transaction using them, so maintain the ease of reasoning about it, while still adding up to an implicit global limit per block.

This would solve the denial of service risks that caused Satoshi to disable all of these opcodes in the first place.

Forward Momentum

I’m sure many of you are thinking “that is way too big of a change.” I can empathize with that, but I think an important aspect of this project as a proposal to understand is we don’t have to do all of it. The value of this proposal isn’t necessarily actually turning all of this back on as a whole, it’s the fact that we would actually be comprehensively looking at a massive suite of primitives and asking ourselves what we really want out of this in terms of functionality.

It would be a complete about face from the past three years of bickering and arguing over tiny narrow changes that only help certain functionalities. It’s a tent that could bring everyone together under one roof to really comprehensively assess where to go from here. Maybe we do wind up turning all of this back on, maybe we wind up just activating a few things because the consensus is that is all we need to enable functionality everyone agrees we need.

Regardless of what the end result actually is, it can be a productive change in the entire conversation around where we go from here. We can actually map out and get a comprehensive lay of the land, rather than bumbling around arguing over what murky and half lit path to go down next.

This by no means has to be the path forward we take, but I think it is our best shot at deciding which one we do. It’s time to start actually working together in a productive way again. 

You can find the first in a series of video interviews recorded at Bitcoin++ with a handful of developers discussing the proposal of Script Restoration. To start, here is Rusty himself: