Restoring the full power of Bitcoin Script

This article is machine translated
Show original

By Rusty Russell

Source: https://rusty.ozlabs.org/2024/01/19/the-great-opcode-restoration.html

Editor's Note: This article is an overview of author Rusty Russell's "Great Script Restoration" proposal, published publicly in January 2024. The idea behind "Script Restoration" is to restore Bitcoin Script opcodes that were disabled in 2010 due to a DoS issue. To avoid the same DoS issue from re-enabling these opcodes, the author proposes a new method for constraining transaction verification resource usage and proposes new opcodes.

As of September 2025, the author has written four BIPs related to the ideas covered in this article.

In my last few posts, I’ve been musing about what upgrades we might want to add to Bitcoin Script (the Bitcoin scripting language) once we have introspection. Script was stalled at version 0.3.1 due to denial-of-service attacks: this has always been a regret, but features like OP_TXHASH have made Script’s limitations clear.

Obsolete Bitcoin Script

Many people know that Satoshi Nakamoto disabled OP_CAT and several other opcodes in version 0.3.1, but Anthony Towns pointed out that before version 0.3, the bitcoin software also allowed the use of OpenSSL's BIGNUM type to implement values of arbitrary length .

This was the early days of the Bitcoin project, and I completely understand the desire to avoid DoS issues immediately and clearly, and to restore functionality only after the issue had been carefully considered. Unfortunately, it took years later (which is where we are now) for people to understand the difficulty of adding functionality to Script.

Variable-length opcode budget: fully restore script functionality without introducing DoS

BIP-342 replaces the global signature limit with a weight-based signature operation budget designed to support any reasonable signature verification (such as scripts that can be made with miniscript) while being large enough to avoid DoS.

We can use this approach for other operations, as long as the cost of such operations is related to the size of their operands, and similarly remove arbitrary restrictions in the existing scripting system. I call this idea "variable-length opcode (varops)" budgeting because it applies to operations on variable-length objects.

My draft proposal sets the variable-length opcode budget to be simple:

  • The transaction weight is multiplied by 520.

This ensures that even if the budget is enforced on existing scripts, no conceivable script will be unexecutable (e.g., every OP_SHA256 can always operate on a stack element of maximum length, and its own opcode weight is sufficient to cover its budget).

Note: this budget is applied to the entire transaction, not per input: this is in anticipation of memory opcodes, which means a very short script may be checking an otherwise very large input.

The budget consumed by each opcode is as follows (opcodes not listed do not consume budget):

Opcode Variable-length opcode budget consumption
OP_CAT 0
OP_SUBSTR 0
OP_LEFT 0
OP_RIGHT 0
OP_INVERT 1 + len(a) / 8
OP_AND 1 + MAX(len(a), len(b)) / 8
OP_OR 1 + MAX(len(a), len(b)) / 8
OP_XOR 1 + MAX(len(a), len(b)) / 8
OP_2MUL 1 + len(a) / 8
OP_2DIV 1 + len(a) / 8
OP_ADD 1 + MAX(len(a), len(b)) / 8
OP_SUB 1 + MAX(len(a), len(b)) / 8
OP_MUL (1 + len(a) / 8) * (1 + len(b) / 8
OP_DIV (1 + len(a) / 8) * (1 + len(b) / 8
OP_MOD (1 + len(a) / 8) * (1 + len(b) / 8
OP_LSHIFT 1 + len(a) / 8
OP_RSHIFT 1 + len(a) / 8
OP_EQUAL 1 + MAX(len(a), len(b)) / 8
OP_NOTEQUAL 1 + MAX(len(a), len(b)) / 8
OP_SHA256 1 + len(a)
OP_RIPEMD160 0 (fails if len(a) > 520 bytes)
OP_SHA1 0 (fails if len(a) > 520 bytes)
OP_HASH160 1 + len(a)
OP_HASH256 1 + len(a)

Remove other restrictions

Ethan Hilman's proposal to reinstate OP_CAT retains the 520-byte limit. With variable-length opcode budgets, this limit could be removed and replaced with the total stack size limits already in place for taproot v1 (1,000 elements and 520,000 bytes).

Furthermore, if we wish to introduce a new version of SegWit (such as Anthony Towns' " generalized taproot ") or wish to allow "keyless entry ", we can adapt these limits to a reasonable upper limit on block size (perhaps 10,000 elements, or 4M bytes maximum).

Slightly changing the semantics

The values will remain little-endian, but will be unsigned. This simplifies implementation and makes the interaction between bit operations and arithmetic operations much simpler. It allows existing positive numbers to use these opcodes without modification or conversion.

If we intend to use a new SegWit version, existing opcodes can be replaced; otherwise, new opcodes will need to be added (e.g. OP_ADDV ).

Implementation details

The v0.3.0 Bitcoin software uses a simple class wrapper around OpenSSL's BIGNUM type, but for maximum simplicity I reimplemented every opcode without external dependencies.

Except for OP_EQUAL / OP_EQUALVERIFY , every opcode converts to (or from) a little-endian vector of uint64_t . This can be optimized by converting on demand.

OP_DIV , OP_MOD , and OP_MUL are crudely implemented (comparison with libgmp's large number operations shows that more sophisticated methods are much faster).

Benchmarking: Are the above limits low enough to prevent DoS?

Are the above limits high enough to be ignored?

We can remove the 520-byte limit.

But we still need a limit on the total size of the stack: using a new version of SegWit, this limit can be raised to 400,000 bytes; or it can remain the same as the current limit: 520,000 bytes.

In my previous article " Determining the Script Public Key in Script " ( Chinese translation ), I pointed out that sometimes we want to require a specific type of script condition, but not an exact script: an example is a constraint clause of the safe contract type, which requires a delay but does not care whether there is anything else in the script.

The problem is that in a Taproot script, any unknown opcode ( OP_SUCCESSx ) will cause the entire script to fail (not be executed at all), so we need to adjust this a bit. My previous proposal for a separator was awkward, so I came up with a simpler new solution.

Add OP_SEGMENT

Currently, the validator scans the entire tapscript for OP_SUCCESS opcode, and if it finds one, the script passes. This will be changed to:

  1. Scan for OP_SEGMENT and OP_SUCCESSx in the tapscript.
  2. If OP_SEGMENT is found, the script preceding this opcode is executed; if the script does not fail, scanning continues from this opcode.
  3. If OP_SUCCESSx is found, the script passes directly.

Basically, this splits a script into several segments , each of which is executed sequentially. It's not as simple as "split the script into segments using OP_SEGMENT and execute only one segment at a time", because tapscript allows you to include undecodeable content after OP_SUCCESSx , and we want to preserve that ability.

It does nothing when executing OP_SEGMENT : it exists only to limit the coverage of OP_SUCCESS opcode.

accomplish

ExecuteWitnessScript will need to be refactored (possibly as a dedicated ExecuteTapScript , as 21 of its 38 lines of code are in "if Tapscript" conditionals), and it also suggests that the stack limit for the current tapscript will be enforced when encountering OP_SEGMENT , even if followed by OP_SUCCESS .

Interestingly, the core EvalScript functionality will not change, other than ignoring OP_SEGMENT , as it is already very flexible.

A word of warning, I haven't finished the implementation yet, so there might be surprises, but I plan to make a prototype once the idea gets some reviews.

Hope you like it!

(over)

Source
Disclaimer: The content above is only the author's opinion which does not represent any position of Followin, and is not intended as, and shall not be understood or construed as, investment advice from Followin.
Like
Add to Favorites
Comments