CHIP-2025-05 Functions: Function Definition and Invocation Operations

Hi everyone,

In December I proposed CHIP 2024-12 OP_EVAL: Function Evaluation to enable reusable functions in Bitcoin Cash contracts. In the due diligence for that proposal, I concluded that the two-opcode approach is a better technical choice (see comparison).

I’ve withdrawn my advocacy for the single-opcode OP_EVAL approach, and modified the CHIP’s technical specification to use the two-opcode approach: OP_DEFINE and OP_INVOKE.

Most of the CHIP remains unchanged, but to minimize confusion, I’ve bumped the version to v2.0.0 and updated the title: CHIP-2025-05 Functions: Function Definition and Invocation.

Questions, feedback, and continued review are appreciated!


Summary

This proposal introduces the OP_DEFINE and OP_INVOKE opcodes, enabling Bitcoin Cash contract bytecode to be factored into reusable functions.

Motivation & Benefits

  • Reduced transaction sizes – By eliminating duplicated bytecode, contract lengths can be optimized for a wider variety of use cases: finite field arithmetic, pairing-based cryptography, zero-knowledge proof systems, homomorphic encryption, post-quantum cryptography, and other important applications for the future security and competitiveness of Bitcoin Cash.

  • Stronger privacy and operational security – By enabling contract designs which leak less information about security measures, current assets, and transaction history, this proposal strengthens privacy and operational security for a variety of use cases.

  • Improved auditability – Without reusable functions, contracts are forced to unnecessarily duplicate significant logic. This proposal enables contracts to be written using more succinct, auditable patterns.

8 Likes

The draft implementation is updated in Libauth v3.1.0-next.5 and live in Bitauth IDE:

https://ide.bitauth.com/

Thanks also to @cculianu for implementing the modification in the existing BCHN patch :rocket:

5 Likes

Should we have a 3rd stack instead of an array?

The Support for Skipping Function Identifiers section in the rationale claims it would make it more brittle, but the array also makes some uses more brittle, as illustrated by @albaDsl 's comment here.

Suppose you have a multi-input contract and they all have some common code, which you can then extract to some OP_RETURN from which all inputs can copy it. With the array approach, contract designers would need to worry about index collisions.
With read-only inputs it would further complicate it, you can’t just use a good routine from another input, the caller would have to parse through it and re-index, or he’d have to make a new UTXO where indexes are better aligned.

What if we had:

  • OP_DEFINE - pop 1 item from the stack and push it to executable stack
  • <n> OP_INVOKE - read the item at depth n in executable stack and execute it
  • <m> OP_UNDEFINE - pop m items from the executable stack, can only pop items defined in the same local context (i.e. can’t pop caller’s definitions)
4 Likes

That would increase contract sizes and add some contract risks – you’re back to OP_EVAL, but with some extra bytes and complexity (essentially a second altstack). See the later comment in that thread:

But even that simultaneously assumes both 1) very advanced compiler recursion techniques (which generally waste bytes vs. loops, so it’s hard to see why you wouldn’t just add some bytes to your functions-defining-functions machinery to make it do what you want) and 2) the compiler doesn’t apply some trivial optimizations via static analysis.

The idea is also taken further in Rationale: Immutability of Function Bodies (which would also “bring back” this OP_EVAL behavior without modifying the functions-defining-functions machinery). Beyond that – adding registers or Forth-like local variables – seems well beyond the scope for this functions CHIP. (If you’re interested though, consider contributing in this topic? Script Machine Registers)

Edit: at a high level, the request is “can we still have lambas like OP_EVAL”. Yes – it’s general computation, just modify your functions-defining-functions machinery to make it work however you like. If lambda-only programming becomes the predominant way of designing contracts, a future proposal could even add an “OP_DEFINE-LAMBDA” that accepts only a function body, assigns the next available function table slot, and returns the new function identifier. (The “native functions” approach is nice because it extends in the same ways as other languages, too.) But it’s important to note that we’re discussing an optimization of fixed overhead – the extra bytes in the functions-defining-functions machinery – for a highly-specific, contract-length-indifferent contract compilation approach, rather than a more fundamental capability.

1 Like

Jason has done a lot of research into “OP_DEFINE / OP_INVOKE” vs OP_EVAL and I trust his conclusion. “OP_DEFINE / OP_INVOKE” does provide functions and recursion which are the key features we need. There are some trade-offs that have been mentioned in the discussion.

With this comment I just want to point out two things: 1) OP_EVAL allows for closure like constructs which “OP_DEFINE / OP_INVOKE” does not allow (assuming an assign-once function table) 2) if we later want to have this feature then implementing OP_EVAL via the assign-once function table does not really work. We would need to add pure OP_EVAL or move to a mutable function table (a more likely outcome if we start out with “OP_DEFINE / OP_INVOKE”).

Here is an example of a “closure” that can be tested in bitauth IDE (2026). It toggles its internal state every time it is invoked:

<0x18c1bc527f7c517f818b529751807c7e7cbc7c7e0063010068> // everyOther "closure"
OP_EVAL
OP_EVAL
OP_EVAL
OP_EVAL
<0xbc527f75517f7781> // getValue from closure
OP_EVAL

This may not seem very useful but it can be a component in functional style programming, which perhaps is not as byte efficient, but promotes composing programs out of small reusable building blocks. If this kind of construct was used with iteration/recursion and assign-once function slots, we would quickly run out of them.

4 Likes

Thanks! I’m excited that you’re exploring FP-style contract composition :rocket:

I agree – a future upgrade could certainly make the case for also adding OP_EVAL, the passable-lambda above (“OP_DEFINE-LAMBDA”), or any number of other overhead-reducing improvements for various contract development styles and use cases.

Just a nit RE “does not allow”: the Bitcoin Cash VM is already computationally universal within a single transaction – computations can both inspect themselves and be continued across multiple inputs (loops and/or functions just make the contracts far smaller and safer/easier to audit). The only kinds of computations that the VM doesn’t “allow” are those made excessively impractical by VM limits. Some of those limits are still very conservative of course, but they’re already plenty high enough for financial calculations, programming-paradigm machinery, business logic, etc.; generally only “heavy cryptographic math” use cases come close.

In this case, there are likely many ways to setup the functions-defining-functions machinery to work however you like, especially if contract bytecode-length optimization isn’t the top priority. (Though with trampolining, it might be quite byte-efficient, too? Libauth’s VM is implemented in an FP-style to make opcode changes and debugging features easier to hack in, but it requires trampolining to avoid call stack issues in all JS environments.)

3 Likes

I’m happy with the direction this is going. Thank you for this revision.

7 Likes

Thank you for continuing the research on this topic!

I’m enthusiastic about the new technical approach to enable the same re-usable functions feature for the Bitcoin Cash VM!

For smart contract tooling and high level language development this will make it significantly easier because 1) functions don’t have to be juggled around as stack items and 2) it removes the concept to do the compiler optimizations to then use OP_EVAL all over the place to save a few bytes, this would very significantly increase the cost of implementation and greatly decrease the auditability of the raw compiled script

These reasons are also well-outlined in the CHIP itself :+1:

So both as a contract developer and as a developer working on CashScript I think this CHIP very much gets to the goals of the prior proposal in a way that’s much nicer for the broader ecosystem

6 Likes

I have not seen any update yet, and some months have passed.
So I’ll repeat the problem I have with this approach, hopefully there is a solution in the work that just hasn’t been published yet :slight_smile:

The basic design of Bitcoin and Bitcoin Cash is that the actual opcodes that run are locked in at the time the transaction that locks the coin is signed.
This specifically means that it is impossible to replace the code that unlocks it with another, making the utxo safe. As that is public and vulnerable for brute-forcing attacks. We recently upgraded to p2sh32 to strengthen that concept.

Or, the short short version, is it IMPOSSIBLE today to create the code unlocking the money that is stored in a signed UTXO after that money was locked up. This is for safety. Being able to write code later means you can brute force the money on-chain.

P2SH and P2SH32 use a hash of the unlocking code for this purpose, MAST is a proposal from the BTC side that does this too a bit more featureful.

The latest chip doesn’t have any concepts of validating the code pushes to be the ones known at time of signing. I am aware that some suggest we can emulate this in other opcodes, but this is an unacceptable proposal as security is simply not something that is optional.

Introducing a way to unlock a UTXO for spending with code that may be written afterwards is a massive massive change in behavior of Bitcoin Cash and one I don’t think is healthy for the coin.

I suggested a solution in a previous number of posts, I won’t repeat it as the owner of this CHIP is aware already. I just wanted to ask for an update towards solving this in the proposal. It already got a lot of progress in the right direction, so I have hope it will get solved.

This is a good observation we can take to heart.
Yes, adding protection may mean “good engineers” feel like they are being treated like children. But they have to admit that there will be a lot of other people that will write code too and that will actually need said protection to not lose their money. Which will reflect on all of Bitcoin Cash.

https://x.com/ChShersh/status/1939720050971169191

BCH cannot optimise for the lowest common denominator - because then it will simply lose. We do not have the status quo advantage or network effect to be the conservative option & still gain ground.

Also, in a system like BCH, upside is far far far more important than downside. People doing stupid stuff go broke (which both stops them doing stupid stuff, and is a strong incentive to be cautious in the first place).

Same way that a bunch of scams may have been funded by Flipstarter, but the fact that it hit a couple of home runs with BCHN & others paid off the unfortunate cost of the failed scams and more - many times over.

Optimise for winning, not to avoid losing.

4 Likes

Absolutely true.

You correctly identified the lowest end, the quote of bad engineers doing bad things. That is the lowest boundary of the gray zone we want to stay in.

The top level of the gray zone is what the CHIP does today, it gives 100% of the responsibility of avoiding code-injection to the script author. Which is fine for the good engineers that don’t see the problem with it. They know how do to it properly.

But the big gray zone in the middle is relevant. We want neither the top nor the low extreme.

The ONLY arguments against my suggestion (again, see earlier description) are absolutely about this specific point. Good engineers feeling it is not needed to provide protection for any other programmers.

All suggested usecases are still going to be possible just fine with a verification built in. The upsides are all going to be there regardless of validation being built in or not.
There are even extra upsides with it built in, in that MAST becomes much easier to do in a consistent way.

So I think we agree on all the high level things,

as I wrote a couple of months ago, this CHIP has already moved a lot into a nice direction. Lets hope we can keep moving that forward.

We had a nice discussion on telegram between BCA, Jonas and me about options.

Here is one option to solve the problem of code injection that may be simple and useful for all.

As per the chip we have OP_DEFINE.
Suggested to add is OP_DEFINE_VERIFY which is effectively an shorthand for
OP_DUP OP_HASH256 <hash> OP_EQUALVERIFY <index> OP_DEFINE

Which makes transition from p2sh very simple:

unlocking script;
push [code]

locking script:
push code-hash <index> OP_DEFINE_VERIFY

In my post: “code injection” means that the locking script that is visible for all can be brute forced with any unlocking script, just find one that unlocks it. Which means you lose your money. That is code injection.
Simple example; someone provides a transaction with code on output one, that is used in output two via introspection.
The user mistakingly assumes that the two outputs to be spent in the same transaction. Which is not an assumption that makes sense, yet you need to understand UTXO quite deeply to realize this.
The user can safely do this by simply using the VERIFY version of DEFINE and providing the hash of the code.