CHIP 2024-12 OP_EVAL: Function Evaluation

Function Evaluation (OP_EVAL) would let Bitcoin Cash contracts be efficiently factored into reusable functions. Optimizes contract sizes for finite field arithmetic, pairing-based crypto, ZKP systems, homomorphic encryption, post-quantum crypto, and many other applications.

CHIP:

Some discussion:

https://x.com/bitjson/status/1867470838099791966

4 Likes

Please add OP_EXEC to evaluation of alternatives. The main difference is in being able to wall-off parts of parent execution context stack. Rationale on why we shouldn’t have some of this functionality in OP_EVAL would be good.

More details: Brainstorming OP_EVAL

Re. this:

In signature operations, the covered bytecode includes only the active bytecode beginning from the last executed code separator (A.K.A. pbegincodehash ).

This is actually causing problems for things like DSPs, because from signature preimage there’s no way to tell whether it is covering a full redeem (or eval) script or just a slice of it.

Maybe we could use the opportunity to fix it.

From Telegram:

bitcoincashautist, [10/27/24 10:45 AM]
btw, this little OP_CODESEPARATOR bastard makes it so that you need to add whole prevout TXs to DSPs if you want to support P2SH & bare multisig

src/script/interpreter.cpp · master · Bitcoin Cash Node / Bitcoin Cash Node · GitLab

bitcoincashautist, [10/27/24 10:47 AM]
because signature will cover only the part of redeem script after the codeseparator, and the signer can’t tell if some stuff (like opdrop and replacing key&sig with some dummy ones) was executed before

bitcoincashautist, [10/27/24 10:47 AM]
in the signature preimage there’s no indication whether it’s a full redeem script, or sliced due to codeseparator

bitcoincashautist, [10/27/24 10:48 AM]
we could fix this by adding 1 byte in case codeseparator was used

2 Likes

Cool to see this new CHIP! :smiley:

I know the idea of OP_EVAL is really old, dating back from 2011, when it was mainly thought about as something for MAST-like constructions or emulating P2SH. In the motivation you describe it as a way to allow for re-usable function, so the rational for an OP_EVAL opcode changed significantly over time.

That’s why I would also really like to see a comparison/ evaluation of alternatives to understand how it affects the specific opcode implementation.

I think it would be great to also see some simple/minimal “Usage Examples” where just the concept of re-usable functions are demonstrated.

I also posted this thought elsewhere, about how executing user-provided script could lead to a situation where a user might be able to use eval to circumvent the rest of the contract execution:

i know eval in javascript is a big security concern, I wonder how this relates to the blockchain context.
A user should never be able to push data which is then evaluated as script if this can affect the rest of the execution of the program

2 Likes

If eval-script can modify the parent stack then allowing a non-authenticated script to be provided by spender would make the UTXO anyonecanspend.

Consider this trivial example:

OP_IF
    OP_DUP OP_SHA256 <hash_of_function> OP_EQUALVERIFY OP_EVAL
OP_ELSE
    OP_EVAL
OP_ENDIF

The 1st branch can only run the specific eval-script that matches the hash. The 2nd branch can run anything - and if the anything is read from the input script then spender can just provide OP_1 or OP_DROP OP_1 or whatever will make the contract pass.

But what if we wanted to make some covenant where users can specify by themselves how to secure their part of the contract? We’d need a way to wall off the outer context so eval-script has strict limitation on before/after stack state, which is what the alternative, OP_EXEC, would enable.

1 Like

Thanks! Yes, I need to add more supporting material, just wanted to get the technical proposals out as soon as possible so people can start experimenting with it. The draft implementation is also live in Bitauth IDE:

Fibonacci numbers via both OP_BEGIN/OP_UNTIL and OP_EVAL recursion, for anyone interested in experimenting:

“Playing with Fibonacci numbers” - Bitauth IDE

1 Like

Collecting some other discussion:

https://x.com/bitjson/status/1867614179622084958:

[…] it’s useful to clarify here that there are two reasonable ways to implement [OP_EVAL].

Most software will choose to just recursively call the script evaluation function again, but more constrained systems (esp. hardware-implemented Forths) might do what Libauth (intentionally) does: copy the current execution info to a new control stack frame, then swap the currently evaluating instructions in place (resetting ip/pc to 0). When you get to the end of instructions, check the control stack for more work to resume until it’s empty (or contract rejected). (Btw, if you squint, this is like tail-call elimination.)

And a comparison to BIP 12’s OP_EVAL:

https://x.com/bitjson/status/1867611387046113636

Looking at https://en.bitcoin.it/wiki/BIP_0012:

  • Doesn’t save stack frames to the control stack (their proper location in a Forth implementation if you want to maximize implementability e.g. hardware) + limited depth to 2: seriously hampers the overall usefulness by preventing deep re-use of functions. In general, we want functions to be able to call as many sub-functions as they need until the work is done. For now our existing 100-depth control stack is probably plenty, but nothing prevents that from increasing if there’s demand and/or tail-call optimization (though note that loops are almost always more byte efficient in our bytecode, so it’s hard to justify effort or complexity spent on tail-call optimization today).

  • We’ve preserved the usefulness of OP_CODESEPARATORs across past upgrades, this did not (and of course, this didn’t know about OP_ACTIVEBYTECODE); OP_EVAL CHIP also avoids some traps here where signature-covered bytecode includes more than the top evaluation (it’s useful that the function behaves the same regardless of where it’s called; sig checks in a particular function should pass given the same signature across any control stack configuration)

  • IIRC, this cleared the alternate stack? Anti-feature, would waste bytes across functions which need the extra communication bandwidth (you’d just be removing one of two stacks by which code can “call” functions, forcing all code to stack juggling rather than letting subroutines accept e.g. two separate arrays in whatever cases where it’s optimal).

Again, we have tons of new information and clearer constraints today that make these choices easy. It would be unreasonable to expect any design developed before 2024 (VM limits) to have strong answers to these questions.

2 Likes

Some discussion of sandboxing behavior and NEXA’s OP_EXEC:

1 Like

Regarding OP_EXEC vs OP_EVAL… it all depends on how they end up being used.

OP_EVAL - less “safe” but less overhead; seems like it would be most useful as an internal low-level tool for cashscript or other compilers to use to re-use code… i.e. to call internal functions they 100% control/have generated/have guaranteed known pre and post-conditions.

OP_EXEC - more safe, more overhead; seems like a more generic solution to the problem where both trusted code and “untrusted” code can be exec’d and the caller has guarantees about pre- and post-conditions, since they are enforced. I.e. you know the callee cannot mess with your stack, so you can jump into “untrusted” code, e.g. from a cashtoken commitment or the scriptsig stack or other random places.

So in short – if you envision OP_EVAL only being used for trusted code, ie code that you can verify is what it says it is either because you yourself generated it (compiler), or because you check its hash beforehand (p2sh style), then it’s fine. Less overhead. Lean and mean. But if you ever think it may be used to execute arbitrary contract code – then it’s a landmine waiting to go off.

OP_EXEC enforces a call signature and pre- and post-conditions. Called code cannot touch your stack. All it can do is push results onto your stack but cannot otherwise mess with it. Much safer.

My personal preference is for OP_EXEC. But, I get the efficienty-at-the-expense-of-genericity arguments of OP_EVAL.

Anyway in my ideal world we would have both OP_EVAL and OP_EXEC. This way nobody can make fun of us for being primitive apes.

However if you don’t envision anybody ever needed something more well-defined and secure like OP_EXEC… ever… then OP_EVAL only is fine.

2 Likes

Something missing from the spec: It’s important that an eval’d/exec’d/whatever function not ever end on an imbalanced conditional. Script should fail if the conceptual conditional stack local to the function has any items on it, basically…

The final spec needs to mention that since that could be a source of woe if left unspecified.

Also related/not related: do we need to resurrect OP_RETURN for breaking out of a called function early? (Implicitly also then popping the conditional stack local to that function to 0 as a side-effect)…? Or not worth bothering?

1 Like

Can we please avoid the Waterfall model - Wikipedia development method and actually try the different approaches on some testnet and actual usecases before we get any proposals for consensus rules?

This should start with a fork of bchn and a lot of throw-away scripting code.

(for the non-programmers here, the waterfall model is widely regarded as leading to failure).

You can literally open ide.bitauth.com right now (pick BCH 2026 VM in top-right corner drop-down) and try a script like:

OP_DUP OP_DUP
OP_SHA256 <0x4ae81572f06e1b88fd5ced7a1a000945432e83e1551e6f721ee9c00b8cc33260> OP_EQUAL OP_SWAP
OP_SHA256 <0x060459fe25666da7ddf2ba22fae9f9f507f7659b8c12875a234b320519242868> OP_EQUAL
OP_BOOLOR OP_VERIFY OP_EVAL

To spend, you can provide eval script 0x51 (OP_1) or eval script 0x007551 (OP_0 OP_DROP OP_1) as the input’s pushed data.
It can be any contract hidden behind each of the hashes. Could be some P2PKH for the 1st hash, and 2nd one would have some secret-until-used recovery path.

If combined with loops CHIP you can do full MAST with locking (or redeem) script size only 53 bytes.

// UNLOCKING SCRIPT:

<0x6344d1334be4f53b993030810f63f0452e9c3c569c40acfad81202a360f478a7>
<0> // swap or not
<0x8de0b3c47f112c59745f717a626932264c422a7563954872e237b223af4ad643>
<1> // swap or not
<2> // depth
<0x52> // leaf

// LOCKING SCRIPT:

// store a copy of leaf
OP_DUP OP_TOALTSTACK
// hash the leaf
OP_SHA256 OP_SWAP

// process merkle path and obtain root
OP_BEGIN
OP_1SUB OP_TOALTSTACK
// --
OP_SWAP OP_IF OP_SWAP OP_ENDIF OP_CAT OP_SHA256
// --
OP_FROMALTSTACK OP_IFDUP OP_NOT
OP_UNTIL

// verify root
<0x2fb1740d8e932861b16c08c5d3a8771adad3c78df60569acb0628c185233b99f>
OP_EQUALVERIFY

// execute leaf
OP_FROMALTSTACK OP_EVAL

The leaves in this example are 0x00, 0x51, 0x52, 0x53.

The alternative, OP_EXEC, exists on mainnet of BU’s chain.

that’s just one way of solving the problem.
There are a buch of other approaches that makes sense and actual developers writing actual bitcoin-scripts should try them. Compare them, get a feel for them. Stuff that any language designer, or indeed library author, will do. You don’t settle on the first one without testing the various other approaches too.

Bitauth is a great playground, yet, I don’t think it can emulate a single transaction with a lot of outputs and introspection to, for instance, have a single output that has a bunch of methods reused by the others.

“jmp” or “eval” or “exec” (or loops) are in essence a sysmic shift in the way that the vm can be used. Nobody will understanding by just writing a spec, they need to write tons of small apps and be able to be creative in the setup based on this experience.

Python 2 → 3 is a great example of them failing to have done that in v2 before committing to something. And the process of maturing that hurt them for 15 years.

It’s a full TX validation and ScriptVM engine, consensus-compatible with actual nodes, it can validate any transaction (and if using current consensus spec: generate it and have it be accepted by the network).

With a little more code, Libauth would become a full Javascript node.