Files
CHIP-subroutines/readme.md
2025-02-05 13:51:39 +01:00

14 KiB

CHIP 2025-1-Subroutines: avoid code duplication in script and add subroutines

Title: Declare and call subroutines with some new opcodes.
Type: protocol upgrade
Layer: Script
Owner: Tom Zander
Author(s): Tom Zander
Status: withdrawn
Initial Publication Date: 2025-01-25
Latest Revision Date: 2025-02-05
Version: 0.0.4
Table of Contents

Summary

In Bitcoin Cash money is locked into a low level script we call "bitcoin script" and bitcoin addresses are merely a result of this lower level design.

With the May 2025 upgrade the power and utility of bitcoin script has greatly increased which creates the need to keep the growth of script usage on-chain to a minimum.

We propose to introduce 2 opcodes that together define reusable code and call reusable code in the form of subroutines. In computer science these are often called functions or methods. Same idea.

The ability to avoid repeating code on chain is useful to keep transaction sizes down while unlocking greater utility.

Motivation

A bitcoin script is executed in order to find out if a store coin can be spent. The code is thus executed exactly one time in the lifetime of such a script. This means that a script will have to be repeated in full for every usage in the world.

The opcodes in bitcoin, as designed by Satoshi, are very specialistic to balance this out. A standard transaction only needs a very small number of opcodes to have a functional pay-to-pubkey-hash. 5 bytes of code to 20 bytes of data. Super specific opcodes like OP-CHECKSIG is one of those that helps tpo keep block space usage low.

What subroutines can do is allow innovation to happen outside of the consensus layer with new subroutines being shared between users, and they being called very similar to specialistic opcodes.

The direct advantages are reduced transaction sizes but also improved auditability.

Design requirements for safety

This is money we are making programmable, which means that there is a high incentive to steal and there are long lists of such problems on other chains. Draining a Decentralized Autonomous Organization is an experience we should try to avoid.

  1. Only "trusted" code can be run.
    The definition of trusted here is simply that the code was known at the time when the money was locked in. Which means that at the time the transaction was build and signed, we know the code that is meant to unlock it. To make this clear, with P2SH the code is 'locked in' using the hash provided ensuring that only a specific unlocking script can run.

  2. Separation of data and code.
    Subroutines are code, code can use data in many ways. Multiply it, cut it and join it. Code can't do any of those things to other code.

A list of subroutines

This proposal adds a single opcode that allows a push of code to the list of subroutines. The push is very similar to a general push-data opcode we have already, but instead of pushing to the stack it pushes the following bytes to a list of subroutines that a separate opcode can call into later.

This brings us a separation of code and data, a core security requirement. And it allows an absolute minimum amount of bytes used to actually call such a subroutine. <op_0><op_runsub>, two bytes.

To actually reach our goal of maximum re-usability, we also propose for the validation engine to keep a list of subroutines in memory for the entire transaction. This means that a transaction can have multiple outputs that work together if they are spent in one transaction later. One of those outputs can be your subroutines that are then used by multiple others in the same transaction.

What subroutines are aiming to be is a generic "opcode". This is generic because you can ship the code yourself. Like a library. And that avoids changing the consensus layer to reach the same goal. A transaction compiler, or IDE, can include those subroutines as a library and users can just call them without needing any more knowledge about their implementation than their inputs and outputs. Much like any documented opcode that is part of bitcoin script today.

An developer-environment creating a list of subroutines to ship with your script is a very simple and understandable process that is easy to proof read.

Future: op-mast

In Bitcoin Cash there is a "push only" rule that applies to the unlocking script. This is meant to separate code and data, and we should not break that existing practice. Yet, there are reasons to want to have code pushed in the unlocking script, as is evident by the usage of P2SH. One often named need is to have privacy of the unlocking script until needed to actually unlock the coins.

As a marriage made in heaven approach we can extend the ideas from this chip with a separate proposal that introduces a new MAST-like opcode. The goal here is that an unlocking script can push subroutines to the list, but they are marked dirty because they have not been verified. Before we can run those we need to verify that those scripts were actually known at the time of the mining of of the locking script.

Adding a op-mast (which stands for Merklized Abstract Syntax Trees) opcode allows a cheap way to verify the code without it ever violating the safety design principles.
MAST is special in that it allows a list of subroutines to all be verified by a single hash, which saves blockchain space while providing useful functionality.

Technical Specification

We introduce these new opcodes:

Word Value Hex Input Output Description
OP_DEFINESUB 191 0xbd The next byte(s) defines the size of the data pushed to subroutines as little endian
OP_RUNSUB 176 0xb0 Index Runs subroutine Index
OP_RUNSUB2 190 0xbe ii si Runs subroutine si that has been pushed previously on input-index ii

The OP_DEFINESUB does not alter or inspect the stack, it only pushes script to the list of subroutines. The first 2 bytes following OP_DEFINESUB contain the length of the subroutine data, in little endian order. Identical to how op_pushdata2 works.

The subroutine data starts with one byte which is the number of arguments this subroutine will consume from the stack. The rest of the data is the actual code to be executed later. The subroutine is pushed to the list of subroutines and given an index. The first pushed is index zero.

OP_RUNSUB reads a single integer from the stack and will then run the code in-place with the index being the index in the list of previously defined subroutines.

  • The code that is run will enjoy a little stack protection that means the stack zero point is reset at the start of the subroutine. Reading the stack will fail if values are read or popped that are below the local view of the stack.
  • The definition of a subroutine specified the number of arguments expected, these are required to be on stack and having less will fail.
    The new zero-point of the stack will be such that only the expected arguments are visible.
  • The subroutine gets an empty alt-stack which is lost when the subroutine finishes.
  • Calling a subroutine from another subroutine is allowed and works as expected. At this time we set the maximum depth to 255.
  • OP_CODESEPARATOR is changed to fail the script immediately if it is encountered in a subroutine.

OP_RUNSUB2 is identical to OP_RUNSUB except that it fetches the subroutine list from earlier processed inputs on this same transaction. The user needs to provide both an index of the input as well as an index in that input's list of subroutines.

To limit the number of times the entire transaction scripts are run to at most one, only scripts defined in lower numbered inputs can be run. This means that script writers need to take ordering into account on the transactions. A DEFINESUB encountered while processing the unlocking of input 1 will be usable to RUNSUB while processing the unlocking of input 2, but not during the earlier unlocking of input 0.

Limits

The maximum number of subroutines that can be defined in a single script is 253. Adding more will instantly fail the script.

There is no explicit limit on the length of the subroutines. The implicit limits of other parts of the vm will be good enough.

The maximum number of subroutines calling subroutines recursively is set to 255. Exceeding that depth will make the script fail instantly.

Example

A future version of cash script (the high level compiler) could have a feature like this: (source)

library Math {
  function muldiv(int x, int y, int z) returns (int) {
    return x * y / z;
  }
}

In an actual contract they could use this like:

import { muldiv } from "Math.cash";

contract Example() {
    function test(int x, int y, int z) {
        int result = muldiv(x, y, z);
        require(result == 5);
    }
}

This would compile down to cash-assembly in two steps. First, the muldiv method would be compiled to byte-code and turned into a subroutine. This example is pretty short, just 2 opcodes. <OP_MUL><OP_DIV>, which in bytecode is 0x9596.

If we just encode the 'example' contract the output would be:

<OP_DEFINESUB>00 03 03 95 96
<OP_0><OP_RUNSUB>
<OP_5>
<OP_NUMEQUALVERIFY>

The define-sub opcode you should read very similar to a op-pushdata type, the following bytes are what is actually used by the opcode. The first by 2 bytes indicate the push length, which is just 3 bytes. Then we have the (second) 03 which indicates the number of arguments that this subroutine has. Followed by the 2-byte actual code.

Comparing alternatives

The chip shares most of the advantages this chip has. It has no extra advantages that are not present in this proposal.

The main safety design requirements are, on the other hand, completely missing from the 2024 op-eval. The author claimed that future writers of scripts won't make mistakes or copy other people's code that could be wrong. An assumption that goes against the experiences of programmers worldwide.

Next to the main considerations from the safety design requirement being violated, the 2024 op-eval also has explicitly stated it will allow a subroutine to alter the stack or alt-stack. This is a choice that allows code to be made near impossible to audit. For instance even with the same arguments the called code may behave differently: mistakes will be made. Authors using code libraries will be hurt.

The proposal really makes it extremely easy to make fatal mistakes, like for instance a OP_CHECK(MULTI)SIG(VERIFY) inside of a subroutine is suggested to not verify the actual transaction-output, but instead it would verify a signature over the subroutine code. This violates the least-surprise rule for programming and will make transaction builder code need to increase their complexity to unlock those outputs.

The code being passed by stack has a downside since the (planned) May 2025 upgrade of vm-limits. Specifically, there is a need to copy or push the code to execute. And that has a vm-limit cost to it. The vm-limits idea uses that as a replacement for charging actual code running. But in the case of op-eval that is not true. The effect is that a charge will be made twice on the limits. Any "fix" would be inelegant.

This 2024 op-eval chip is also poor for reusing scripts across multiple outputs on the same transaction. While introspection might pull an entire output and op_split may be used, that severely limits it's safety compared to this proposal where many subroutines can be defined.

BIP-0012 (2011) op-eval.

This was a contender for what we now know as p2sh. Its bip states the main issue:

The main objection to OP_EVAL is that it adds complexity, and complexity is the enemy of security. Also, evaluating data as code has a long record of being a source of security vulnerabilities.

The community went with p2sh instead for various reasons, but the above point certainly played a part. The safety requirements detailed in the motivation will solve this.

The bip defined the recursion depth maximum to 2, which shows a different mindset.

Nexa's op-exec. (docs)

The main problem with this approach is that it combines a function definition in the actual call. It specified not just the code on-stack, it also states the number of parameters from stack and the number of stack items it returns. The value of defining the number of returned items is not clear.

The effect is that each call is very expensive in bytes used. At least 3 pushes, or 2 pushes and a pick, more than all other proposals reviewed.

The main issue is again the mixing of code and data. But what is good is that it also introduces what it calls a "barrier" for stack usage as a basic and cheap way to avoid money being lost.

Feedback & Reviews

Discussion

People on different proposals, some even on different chains, have used pretty nice sounding opcode names and to avoid confusion this proposal opted to pick a not before used name. As the author I'm absolutely not married to the names proposed here, the structure and concepts are important and the actual name of the opcode is front-end work that should be decided separately. Names like op-exec, op-eval, op_call, op_jump all come to mind as useful alternatives that can be used instead of the ones proposed in this chip.

Changelog

Public discussion has shown a distinct lack of interest in "saving bytes" on the blockchain. If it were free, sure, but tradeoffs needed show this not to be something that Bitcoin Cash should do.

Copyright (C) 2025 Tom Zander

Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.