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.

7 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:

4 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.)

2 Likes

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

6 Likes