I wrote a long evaluation comparing OP_EVAL to an optimized version of OP_EXEC here:
Can anyone identify any use cases for the stack-isolating behavior of Nexa’s OP_EXEC?
Fully-formed product ideas not needed – even contrived examples to demonstrate an advantage would be great.
Even if we steelman OP_EXEC to be less wasteful per-invocation vs. OP_EVAL (e.g. by accepting the “function signature” in some encoding that is pre-concatenated with the evaluated bytecode, wasting those bytes only once per function definition), I’m having trouble devising any scenarios where the function signature adds value:
- The resulting transactions require more bytes than OP_EVAL.
- Defining a per-contract, bytecode-based “OP_EXEC API” is complex, error prone, and incompatible with existing contract systems. E.g. for an existing multisig wallet, each multisig signer has to be taught to understand and sign for the new OP_EXEC-based input type rather than simply signing their known input type in new transactions.
- Stack isolation adds no security value. When you compare an end-to-end example OP_EXEC-based delegation scheme vs. constructions that we already have today (sibling inputs, introspection, CashTokens, etc.), OP_EXEC systems have more potential security pitfalls with no bandwidth savings.
Some ideas I’ve reviewed:
Contract compression via reusable functions
Transaction compilers should be capable of minimizing any contract by identifying and factoring sequences of bytecode into functions, pushing them to the stack at optimal times and OP_PICK OP_EVAL
-ing as needed.
This is the “common case”. Nearly all complex contracts can save at least a few bytes by factoring reused sequences of bytecode into functions. For contracts implementing finite field arithmetic, zero-knowledge proofs, post-quantum cryptography, and other non-trivial computations, these savings are significant enough to enable many currently-impractical applications – cutting KBs or MBs from total transaction sizes.
Reviewing OP_EXEC:
Stack isolation here adds no security and significantly increases compiler and contract complexity. Existing OP_EXEC-like proposals would waste several bytes per function invocation, but the steelman-ed OP_EXEC above could reduce that to only a few bytes per function definition. Depending on contract complexity, this may still result in hundreds or thousands of wasted bytes per transaction.
MAST-like constructions (“Merklized Alternative Script Tree”)
The UTXO commits to the hash of one or more spending paths which get revealed and authenticated at spending time. This has already been possible on Bitcoin Cash since 2023, but OP_EVAL or OP_EXEC would save bytes by avoiding the need for a sibling input.
For less-interactive setups, hashes can be either directly referenced or committed within a merkle tree.
For interactive setups, participants can prepare a data signature over each allowed spending path using an aggregated Schnorr key, eliminating the need for unpacking a particular hash from a merkle tree. Spending then requires only one of the pre-signed scripts and any unlocking material (variable length) + aggregated signature (65 bytes) + aggregated key (33 bytes). Collaborative spending from the address is also “included”, costing no further bytes (like with BTC’s Taproot).
Reviewing OP_EXEC:
Stack isolation offers no additional security for these cases and wastes a few bytes per definition/invocation depending on precise semantics.
Post-funding assigned instructions
In these cases, OP_EVAL or OP_EXEC could be used to avoid committing to all spending paths prior to the UTXO being funded; the contract instead commits to some other method for authenticating later-assigned instructions. For example, a spending path in the contract could accept instructions authorized by some key, a large deposit, a held CashToken, etc.
Note the overlap between these cases and “Mast-like constructions” above. However, rather than aiming to reduce the on-chain footprint of contracts, these cases aim to delegate control over the spending path to whatever mechanism authorizes the new instructions.
Reviewing OP_EXEC:
Again, stack isolation operates at the wrong level to be useful here. If the instruction authorizer is malicious, they can likely lock up funds by committing the contract to unspendable instructions (e.g. OP_RETURN); if the instruction authorizer wants to take the funds, they can always commit the contract to instructions allowing spends from a key they hold. In fact, I’m skeptical that there are any use cases in this category which aren’t either 1) security theater (the authorizer is just a custodian), or 2) more efficient to do with just CashTokens (in cases of delegation to decentralized contract systems).
User-provided pure functions
This is my attempted steelman of an OP_EXEC use case: a decentralized application author wants to allow some user(s) to provide a pure function (in VM bytecode) which accepts some raw input from the contract and computes some result(s) which are used by the contract. To make this plausible, we need a reason to accept a function rather than precomputed results, i.e. the contract needs to 1) save and run the function later using yet-unknown inputs or 2) prove that the same pure function was faithfully performed against multiple sets of inputs.
Contrived example: a decentralized exchange protocol allows users to submit new market making algorithms, and anyone can deposit assets or trade with any market maker.
Ignoring all the data, timing, and incentive questions – can we design a scenario in which OP_EXEC at least saves some bytes vs. OP_EVAL by taking advantage of the “built-in” isolation?
Presumably this is the kind of situation made “safer” by isolating an evaluation: some users are risking funds in contracts which execute arbitrary code provided by different users. If the market maker function has some secret exploit, the function author can exploit it to drain the market maker of other people’s money.
Reviewing OP_EXEC:
Once again, stack isolation is protecting the wrong thing – policing stack usage neither prevents the function from including an underhanded exploit nor can it prevent the function from surprise-bricking the contract (<funds> <100_BCH> OP_GREATERTHAN OP_IF OP_RETURN OP_ENDIF
).
Fundamentally, the stack usage of an evaluation is simply not relevant to contract security. If the function uses too many items or produces too few, the rest of the contract will fail and the attempted transaction will be invalid (and if the contract can be frozen by a failing function, we’re doomed with or without an isolated stack). If the stack contains something we don’t want the pure function to modify, we need only rearrange our contract to push it later (or validate it later, if the data comes from unlocking bytecode).
So: stack isolation remains useless while wasting several bytes per function definition or invocation. If it mattered, contracts could easily prevent segments of bytecode from manipulating the stack in unexpected ways, but again, the validation that really matters in this scenario would have to look at the actual contents of the pure function. In reality, the end user('s wallet) is ultimately responsible for verifying the safety and security of the contract they’re using.
In general, I’m very skeptical that user-provided pure functions are the optimal construction for any use case. If a contract system requires on-chain configurability, it’s almost certainly more efficient to “build up” state by expressing the configuration as one or more parameters for fixed contract instructions.
And at a higher level, it’s almost certainly even more efficient to simply use smaller, purpose-built contracts rather than configure (on-chain) a one-size-fits-all contract. E.g. because Jedex (Joint-Execution Decentralized Exchange) is carefully broken into efficient, single-purpose contracts, almost all Jedex inputs are smaller than standard single-signature inputs (P2PKH).
To summarize:
As of now, I’m skeptical that OP_EXEC has any plausible advantages vs. OP_EVAL, and OP_EXEC has serious disadvantages in protocol complexity, contract complexity, and overall transaction sizes.
Please let me know of any other use cases I should review, and please leave a comment if I can answer any questions.