CHIP 2024-12 OP_EVAL: Function Evaluation

And ignoring all the actual relevant points against your chip that have been made.

But, really, you’re barking up the wrong tree. You are the one making a suggestion that goes againts decades of actual software engineering practices.
You are violently against a practically free way of avoiding said problems.
And said barking is without any arguments. The wider bitcoin cash ecosystem doesn’t need you to “review” anything. This specific feature is not hard to do. If you refuse to even address actual critisism of your proposal, then please go away. We don’t need that. We need someone that actually can work with others. More people working together get better results.

At this point NOT accepting that there is a one byte-costing approach which avoids well documented and known problems is just plain malicious.

Again, you have given NO arguments why you don’t want this one feature in the list of possible features for the common idea of subroutines. While not even mentioning, let alone discussing, any other features that would be pretty cool to have.

You’d know, considering you’re doing all the barking with no arguments.

1 Like

I would be very interested in hearing if contract coders see value in features like:

  • having a subroutine that is able to alter the stack in a way that goes against the basic “function” based design of cashscript. Or any programming language using function design.
    Specifically, a subroutine would be able to remove more from the stack than “expected” by the function signature. Which means you can’t expect it to behave the same even if you call it with the exact same arguments.

  • the ability to have the unlocking script have a push which is your unlocking code. Not like p2sh where the hash has to match, but without hash verification. A transaction mined on-chain that you can write code to unlock.

  • the ability to cut / join and otherwise alter a subroutine’s code.

Anyone interested in any of those features? Is the risk of making mistakes worth it to you?

Script is not CashScript. Script is more akin to assembly, which is why EVAL is fine.

If you wrote a function in CashScript that takes in 2 args and returns 1 then the compiler would create a well-behaved Script bytecode for that function - which doesn’t even try to break out by consuming more than 2 stack items, so why would we need low-level guarantees when we’re already controlling the bytecode to be eval’d and can ourselves guarantee that it adheres to calling convention or have the compiler produce compliant bytecode?

A function written in CashScript can’t surprise the compiler because compiler is the one creating the bytecode - compiler decides what’s the max. number of stack items it will pop during execution.

If the contract author wants to make his program modular, to occasionally “load” some of his code to be executed from input’s data or from another input/output, he always needs to authenticate the code against a commitment (dup, hash, equalverify) - even when using OP_EXEC - which is why Jason makes a good argument that OP_EXEC doesn’t really add security, it only adds some flexibility for one class of use-cases that would be akin to 3rd party plugins, but we don’t see much use for those.

2 Likes

Heavy debate on merits and details is very important. Attacks on character, ad hominem, passive aggression, caricaturing others, etc. are not warranted or welcome here. Fair warning.

2 Likes

I pasted the whole HTML of this page to Claude and asked him to find examples of:

Here are the receipts, for posteriority:

Looking through the thread, here are some examples of unconstructive behavior:

From Post #23 (Tom):

  • Accuses Jason of deliberately ignoring points: “Repeating that so often while ignoring all the actually quotable objections I made is really quite enlightening.”
  • Sarcastic/dismissive: “Yeah, because who would have the audacity to ask people with experience! Where is the fun in that?”
  • Personal attack suggesting malicious intent: “he knows he’s in the wrong and his lovely op-eval is rotten under the surface”
  • Questions motives: “Jason may think he came up with this all on his own, but sorry to say that he’s about 60 years late to that party”

From Post #27 (Tom):

  • Hostile/dismissive: “If you refuse to even address actual critisism of your proposal, then please go away”
  • Accuses of malicious intent: “is just plain malicious”

From Post #28 (bitcoincashautist to Tom):

  • Retaliatory snark: “You’d know, considering you’re doing all the barking with no arguments.”

In Post #31, a moderator (emergent_reasons) had to step in and warn: “Heavy debate on merits and details is very important. Attacks on character, ad hominem, passive aggression, caricaturing others, etc. are not warranted or welcome here. Fair warning.”

The exchange appears to have devolved from technical discussion into personal attacks, particularly from Post #23 onward.

3 Likes

Can just paste the whole html. We are living in the future :smiley:

Ok back to OP_EVAL.

2 Likes

I’ve been thinking about this for a while and I certainly don’t think stack protection should be implemented for OP_EVAL.
Messing with the stack is a feature that could open the door for many compile time optimizations.
A contrived example could be a reusable bytecode to clean up the stack:

<push bytecode to pop everything recursively from the stack>
<some more pushes>
[...]
OP_DEPTH
OP_IF
  // Do some stuff, leave "true" on stack to exit
  OP_IF
    <index to stack clean bytecode> OP_PICK OP_EVAL
  OP_ENDIF
OP_ENDIF
OP_DEPTH
OP_IF
  // Do some more stuff, leave "true" on stack to exit
  OP_IF
    <index to stack clean bytecode> OP_PICK OP_EVAL
  OP_ENDIF
OP_ENDIF
[...]
2 Likes

Another example:

Given some arbitrary data blob you want to push all bytes directly after 0x00 as a new item on the stack, i.e. 0x00010002045600780x01, 0x02, 0x78.
It’s possible to do a recursive bytesequence to do those pushes and execute with OP_EVAL.

For OP_EXEC you’d have to first do OP_EXEC on one bytecode that does the counting and returns that as a push so you know how many items the real function will produce that you could give as an argument to another bytecode.

2 Likes

Great question!

Look at Jonas’s post to get the answer. Because people will try to work around it. :slight_smile:

And more to the point, because safety of your money should not be optional.

The important part here is that this stack protection which Jason is focusing on fighting is actually practically free.
So the question really that should be asked is why there is pushback at all. Why are some people very actively advocating against a free stack protection.

What? I wrote examples in low level Script, not CashScript.

Safety from what? A misbehaving compiler (like, for instance cashc)?

I literally gave from-the-top-of-my-head examples of things that would be more convoluted (i.e. not free) with stack protection.
In the first example each IF-block would need to end with an explicit number of OP_DROPs.
In the second example there would be two different OP_EXECs, one for counting the number of pushes the second one would produce and pushing that number as an input to the second OP_EXEC. Since the second example is recursive a counter for the OP_EXEC parameter count would also need to be tracked thru the callstack.
Adding stack protection and pushing a number for specifying the number of parameters adds no value. Just because the bytecode can’t do unspecified things with the stack doesn’t make it “secure”.

That being said, I still think OP_EVAL (and friends) is a can of worms. I am not (yet) in favor of adding it.

2 Likes

Very glad to see others experimenting with OP_EVAL and reviewing OP_EXEC/stack isolation, thanks @bitcoincashautist and @Jonas!

Ignoring the missing context for a moment: this just describes a poorly-factored program. Concatenative languages are underappreciated:

  • With OP_EXEC: trusted_code_a <untrusted_code> <0> <0> OP_EXEC trusted_code_bc
  • Saving 2+ bytes with OP_EVAL: trusted_code_ab <untrusted_code> OP_EVAL trusted_code_c

But again – everything that matters is being hidden behind undefined untrusted_code and trusted_code identifiers. It’s not possible to properly review any code snippet without context.

In this case, you’ve restated (omitting context) a less-efficient formulation of my attempted steelman of OP_EXEC (“User-provided pure functions”). The same questions apply:

  1. Why untrusted_code rather than untrusted_result? I.e. what specific feature of this scenario makes it impossible or less efficient to provide code rather than a precomputed result?
  2. Which party first created this contract? Why were they unable to “compile in” the untrusted_code?
    1. If not the originator, whose wallet provided the untrusted_code?
    2. How did that user/wallet audit the resulting contract behavior (including the “untrusted” code)?
    3. If applicable, how did the covenant or other contract parties review the untrusted_code before somehow internalizing it or signing on to the new contract? Given a malicious input at this stage, can the covenant be permanently frozen? (I.e. is there already a critical vulnerability in the evaluation prior to the evaluation in which OP_EXEC is assumed to add some sort of security?)
  3. Can this scenario be made more efficient by removing the OP_EVAL/OP_EXEC and using a pre-commitment structure (“covenants-as-standards”), where the untrusted_code gets “compiled” into resulting child covenants?
    1. If so, does that need to happen on-chain, or are we wasting on-chain bytes to pretend that end-user wallets don’t need to audit the full child covenant’s behavior (including the “untrusted” code).
  4. Specifically, what “untrusted” code can make the contract misbehave?
    1. Who stands to lose money if it misbehaves? (If it’s just the current spender – again, we’re dealing with a wallet bug.)
    2. If a third party or the covenant are at risk: is there an equivalent exploit path that does not rely on stack item counts? I.e. if the “untrusted” code is a market making algorithm, can it be rewritten to offer the exploiter more control over the “price” such that stealing funds does not require any sort of stack manipulation, only an unexpected mathematical result which otherwise obeys the “function signature”?

Again:

1 Like

In Bitcoin Cash we have today efforts underway that try to define a system of “templates”, whic are close to libbitauth templates.

The core features aimed at is that normal users can use this library of pre-defined ASM to build their transactions on top of.

Adding any sort of eval/exec to this mix allows those templates to reference library code. Which I’m sure will happen, as NPM shows, people are doing that kind of optimizations because we are mostly lazy beings. Nothing wrong with that. :slight_smile:

The “untrusted” concept is thus absolutly relevant in the wider scope of things.

Using either proposal you can get that code from another output using introspection. But that code may not be vetted as well as it should have been. Which makes it untrusted.

The basic concept of either op-eval/op-exec both allow the unlocking script to have a push with that code, absolutely qualifies as untrusted if you don’t go through the extra steps of validating it.

So, in short, the idea that we need not add (free!!!) protection rules because there is no way to mistakingly end up running “bad” code in your transaction is naive. Nobody downloads plain executables from the Internet either, right? In reality they do, virus’ protection companies exist. That’s the reality we need to operate in.

Wallet bugs are certainly possible. Please don’t dismiss that as a problem that doesn’t exist or can be ignored.

So what? You can replace a .dll on your system with a hacker’s. Why would you do it, though? Just verify the hash so you know what you’re loading.

It’s agnostic of the source. You can just have all the pushes of eval scripts you need be part of the locking bytecode which makes them trusted and you can just call them with <N> OP_PICK OP_EVAL whenever you need them.

As the contract author - choice is yours. You want trusted? Either provide it as part of main script’s pushes (part of locking bytecode), or hash-verify if you load externally (either by unlocking data push or introspection). You want untrusted? Then refactor your contract so those come after the last VERIFY of the main’s logic you want to protect.

Script is low-level, we don’t need it to hold people’s hands.

:100:

I’ll add that OP_EVAL is a contract implementation detail, it’s purpose is compression. We can already do MAST-like constructions, we can already run “functions” (words) multiple times by duplicating the code, VM limits are already solved with or without OP_EVAL, etc.

OP_EVAL just allows contracts to be expressed in fewer bytes (often added by a compiler stage rather than by deliberate design of the contract author).

Thanks for all the great comments @bitcoincashautist – just want to note (because I :green_heart:ed the post) that the analogy still isn’t accurate: I see a great variety of uses for “3rd party plugins” across multiple interpretations of that phrase.

Instead, I’m saying that OP_EXEC does not add any “flexibility” or “security” in any possible case.

Stack “isolation” fundamentally misunderstands contract development. It’s an “obvious” solution at the top of a Dunning–Kruger peak.


I really want to encourage people to get past reasoning by analogy about OP_EVAL and/or OP_EXEC:

  • There’s no security-for-efficiency “tradeoff”
  • Bytecode isn’t “untrusted” or “trusted”
  • This is not a question of “architecture” or “philosophy”
  • There is no “caller” nor “callee”
  • It’s not a “RISC vs. CISC” thing
  • “higher-level” and “lower-level” are nonsensical in this context
  • OP_EXEC is not “isolation” nor a “sandbox”
  • OP_EXEC doesn’t add “flexibility”, etc.

If you care to understand the topic, drop these mental crutches. Otherwise, here’s a correct analogy:

2 Likes

When you get a chance, could you try to describe these concerns? I’d love to address them in the CHIP’s rationale.

2 Likes

The claim here is interesting in that what we are doing on bitcoincashresearch is designing a plaform for random people we have never met to start and do “contract development”.
That is to say, it makes zero sense to claim some future people will or will not do something. If given the chance, people will do something. I have the entire history of (computer) security to back that up.

Jason then goes and writes:

  • Bytecode isn’t “untrusted” or “trusted”

which doesn’t even attempt to disprove or otherwise debate the actual specific technical detailed post I wrote here. It just states the opposite like it is true.

I do agree with various other points he makes there, though.

  • There is no “caller” nor “callee”
  • It’s not a “RISC vs. CISC” thing
  • “higher-level” and “lower-level” are nonsensical in this context

It’s just interesting how Jason is pushing for an idea that has been rejected by the bitcoin community a decade ago. Not even touching the ideas that could fix it, because that would mean debating those ideas. Instead we see him undercutting that possible debate by repeatedly making elusive statements about how basic security would not be needed in bitcoin cash because… something fuzzy.

Anyway, this debate seems not to go anywhere where it may improve the proposal.

What are you even trying to debate that he’s supposedly “undercutting”? Some alternative you randomly came up with that solves non-existent problems?

You’ve been doing the “undercutting” by writing word salads and trying to make parallels that don’t make sense in this context and then whining and making ad-hominems at Jason who’s been very patient here and tolerant of your passive aggression.

We all learned the pattern from previous CHIP cyles: you being wrong and everyone having to spend insane amount of energy and time showing exactly how you’re wrong, all the while tolerating your annoying condescenting tone coming from the peak of Dunning-Kruger hill. I don’t blame Jason for trying to avoid such timesink here.

I don’t know if you’re malicious or incompetent, I’m leaning towards incompetence, but outcome is the same: it’s a drag.

Your posts here show how clueless you are about how smart contract stuff works and yet you posture as some authority because of some “history” and expect people to give you the attention and authority you think you deserve. Ironic above you tried to call out people’s egos all the while your own ego desperately trying to be relevant.

2 Likes

Sure, need to contemplate it some more since it’s an unknown unknowns kind of thing. OP_EVAL is really powerful, especially with recursion, and I start to wonder if the loops CHIP is needed or that would be syntactical sugar.

1 Like

Makes sense, please let me know if you come across items we need to address in the CHIP :+1:

You can technically achieve some compression of iteration logic with OP_EVAL, but loops are generally much more efficient, e.g. merkle tree validation or aggregations like “sum all input values”.

For a good direct comparison, see the VMB tests for each CHIP (loops, eval).

OP_BEGIN/OP_UNTIL: Fibonacci to 13

  • Unlock: <6>
  • Lock (16 bytes): <0> <1> OP_ROT OP_BEGIN OP_1SUB OP_TOALTSTACK OP_SWAP OP_OVER OP_ADD OP_FROMALTSTACK OP_IFDUP OP_NOT OP_UNTIL OP_NIP <13> OP_EQUAL

Screenshot 2025-01-21 at 7.29.40 PM

OP_EVAL: Fibonacci to 13

  • Unlock: <6>
  • Lock (21 bytes): <0> <1> OP_ROT <OP_1SUB OP_TOALTSTACK OP_SWAP OP_OVER OP_ADD OP_FROMALTSTACK OP_IFDUP OP_IF OP_ACTIVEBYTECODE OP_ELSE <0> OP_ENDIF OP_EVAL> OP_EVAL OP_NIP <13> OP_EQUAL

Screenshot 2025-01-21 at 7.29.53 PM

And here’s link to open these scripts in Bitauth IDE →

Comparison between OP_EVAL and Loops

You can see that OP_ACTIVEBYTECODE + internal conditionals allow us to emulate loops with recursive OP_EVAL, but it requires some overhead (larger transaction sizes), is often harder to review, and cannot iterate beyond 100 without some sort of “trampoline pattern” to limit control stack depth. (So OP_EVAL is even worse for >100 iterations, e.g. summing all inputs in large transactions.) Depending on the length of the iterated code, the overall cost of pushing evaluated code can also accumulate very quickly and make otherwise-reasonable things impractical (esp. aggregations from all inputs/outputs).

On the other hand, only OP_EVAL can most efficiently “compress” any shape of contract, esp. common functions called in disparate locations. While you can technically wrap your whole contract in a giant loop + (ab)use conditional logic to activate and deactivate various code snippets at the right moment(s), that setup is going to produce quite a bit more overhead than simply calling functions in the right locations.

Summary

OP_EVAL and loops each have their strengths, and they’re often better together.

Loops are ideal for iteration, and OP_EVAL is ideal for compression of all other code sequences longer than 3 bytes.


I’ll also add that logistically, it’s quite a bit easier to implement both Loops and OP_EVAL at the same time: each requires modification to the control stack + carefully testing and benchmarking the VM’s control flow behavior(s) for performance regressions. (Though thanks to the VM Limits upgrade, we already have a very strong benchmarking suite for this purpose.)

2 Likes