This might be silly (and I also might be a bit behind the ball in the sense that this has already been discussed and is part of the rationale) but there’s been a bit of contention as to whether defining functions from introspected data (e.g. Token NFTs) should be allowed.
If BCH ends up with read-only inputs, there might actually be a very good use-case for this in the sense that we might be able to do something like the following:
Create an NFT that contains a program as commitment - and make this provably impossible to Unlock.
We can then use this NFT (containing the program) as a read-only input.,
These NFTs end up behaving somewhat like custom OP codes/programs built into the blockchain that can be used by contracts/contract-systems.
Obviously, this is only really beneficial if the size of the program is substantially larger than the input itself (and the OP codes required to verify it). And the NFT commitment size is currently capped to 40 bytes, so this would only be useful if this was increased substantially.
I’m not familiar enough with ZKP’s to wrap my head around this yet, but I think this might be what Jason is proposing here: CHIP-2025-01 TXv5: Transaction Version 5 ?
Input 0 (read-only): The ZKP is provided in this unlocking bytecode. The locking bytecode verifies that the transaction is correctly structured and that the ZKP justifies the state transition between the previous application state ( Input 1 ) and the next application state ( output 0 ).
So, for those very storage-expensive programs (ZKPs), we might eventually end up with the capability to build those storage-expensive programs into the blockchain itself as (provably) unspendable UTXOs that can be used as read-only inputs by any contract that needs them.
Contract Devs could then leverage them as people have described above.
// Get the ZKP program at (read-only) input 0's commitment.
<0> OP_UTXOTOKENCOMMITMENT
// Verify it matches the expected program.
OP_DUP OP_HASH256 <expectedProgramHash> OP_EQUALVERIFY
// Define it as a function at id zero.
<0> OP_DEFINE
// TODO: Use the on-chain program
// ...
Obviously, with 40 bytes Token Commitments, the above probably isn’t sensible. But it might serve as good rationale for leaving that door open in future.
(Apologies if this has already been discussed a lot. The potential usefulness of something like this only just clicked for me earlier today.)
As far as I can tell, this is the CHIP most in contention still for the 2026 upgrade, what kind of timeline (given that it seems to change every year, as standards to be ready ahead of time rise because we’re more and more organised) are we expecting that to need to be resolved to lock in?
All stakeholders should have agreement before the lock in date of half November. That november date has been unchanged for various years now.
Likely it helps to get everyone on board earlier, obviously nobody would disagree with that. Please don’t wait until the end if disappointment is to be avoided.
The only hard rule in Bitcoin Cash upgrades is that there can not be an upgrade unless all the stakeholders agree the upgrade is positive and to be done. Everything else is peopleware.
I think we already have most of what we want from functions. I’d prefer to avoid code injection and mutation, and I see compression as the most valuable benefit of functions. Ideally, if a function can be used across multiple contracts (i.e., inputs), it would save a lot of bytes.
Based on the discussions I’ve followed, allowing arbitrary code execution is dangerous but I think it can be contained and enforced. So if we go ahead with this CHIP, I think there are ways where the contract authors can enforce how and what gets executed.
That said, let me share the approach where I think we do not need functions at all.
Let me call this concept “Contract as a Function” a way to achieve function like behaviour using existing opcodes.
Contract as a Function
Contract as a Function: Write to OP_RETURN or nftCommitment
Let’s say ContractA has a function that adds two numbers. It enforces that whatever two parameters are passed to it in the unlocking bytecode are summed, and then it ensures an OP_RETURN output is created to act as the function’s return data.
Any other contract relying on this contract can then read the returned value from the OP_RETURN or from an NFT commitment, thanks to introspection.
Input 0 (ContractA) -> Output 0: back to the same contract so it can be reused
Input 1 (ContractB) -> Output 1: does whatever it needs to do
x -> Output 2: OP_RETURN
When Input1 is evaluated, it uses introspection to read the value from the OP_RETURN. The value is guaranteed to be correct because ContractA enforces that the calculation result must be written into Output 2 as an OP_RETURN
Contract as a function: Process
This type of function does not return any value but processes something. For example, a loop that goes through all the outputs of transaction and ensures that there is no tokenAmount burn
Input 0 (ContractA) -> Output 0: back to the same contract so it can be reused
Input 1 (ContractB) -> Output 1: does whatever it needs to do
Here, ContractA has the code to validate the transaction outputs and ContractB can simply add a check to expect the 0th input to be from ContractA i.e, it enforces the locking bytecode of input 0 to be ContractA.
Contract as a function: Nested functions and Closures
ContractA can be a function contract that internally relies on other function contracts, either providing some information to the parent function or using it’s value to make some logical decisions.
Example:
FunctionContractA: Generates a random number(VRF) and updates it’s own value in it’s nftcommitment
FunctionContractB: Reads the output nftcommitment of functionA and performs a calculation and updates it’s own nftcommitment (Maybe update the global state of a staking contract)
CallerContractA: Reads output of FunctionContractB and does whatever
Input 0 (FunctionContractA) -> Output 0: back to itself with updated commitment
Input 1 (FunctionContracBA) -> Output 0: back to itself with updated commitment
Input 2 (CallerContractA) -> Output 1: does whatever it needs to do
Contract as a Function: Single Input Multiple Uses
The unlocking bytecode of a function contract can follow a predefined structure of byte sequences. For example: <input_index_x><input_index_x_param><input_index_y><input_index_y_param>. Using loops, these segments can be split and processed, with the results later stored in an NFT commitment or OP_RETURN. Relevant contracts can then read this processed information.
Threads
The UTXOs in the function contracts can be dust-sized, since we’re only using them for the ‘code’ they require to be unlocked. Sending the UTXO back to the same script ensures that the function can be executed again. This is not a single-threaded operation, as multiple dust UTXOs can be sent to the function contracts to enable parallel execution.
Libraries
This approach also allows us to have known public contract libraries(e.g., Math, VRF) that can be used by multiple independent contract systems. These contracts simply expect an input from one of these libraries and perform actions accordingly.
In order to address the concerns about code that writes code — the following two proposals (which are mutually exclusive) amend this proposal with additional restrictions.
I see several misunderstandings from the past few weeks about the Functions CHIP as it relates to “code mutability”, “code injection”, and what is already possible today on BCH.
In fact, the phrases “code mutability” and “code injection” appear to have taken on a variety of meanings to different stakeholders.
The purpose of this post is to exhaustively review each known interpretation. I’ll reference this in a new CHIP section, “Rationale: Non-Impact on Code Mutability or Code Injection”.
Code mutation is explicitly disallowed by the Functions CHIP
If you interpret “mutation” and/or “injection” to refer to some ability to mutate the code of an already-defined function, thereby tricking the program into executing an attacker’s code:
The Functions CHIP has never allowed mutation of function bodies. For good reason too – see Rationale: Immutability of Function Bodies (this has been in the CHIP since it was republished as “the Functions CHIP” in May):
This proposal enforces the immutability of function bodies after definition – attempting to redefine a function identifier during evaluation triggers immediate validation failure. Immutable function bodies simplify contract security analysis and tooling, eliminating some potential contract bugs and malicious code path obfuscation techniques. […]
Given the safety advantages of immutable function bodies, coupled with the impracticality and inconsequentiality of potential optimizations made possible by mutability, this proposal deems immutable function bodies to be most prudent.
Native functions are trivial to use safely
If you interpret “mutation” and/or “injection” as ways in which contract authors may accidentally misuse functions, introducing vulnerabilities into their contracts: the Functions CHIP is trivial to use safely.
While it’s impossible to prevent contract authors from making mistakes, the Functions CHIP is far easier to integrate safely in compilers and tooling vs. OP_EVAL, easier to use in hand-written contracts, and both safer and easier to audit than today’s macro-expansion or naive copy/paste-ing.
We already have delegation, it’s simple and useful
If you interpret “mutation” and/or “injection” as a new delegation-like capability: note that delegation is an intentional feature in use by BCH contracts today.
No, the term for this is delegation, and it is easy, common, and very useful today on mainnet. Here’s all it takes: OP_UTXOTOKENCATEGORY <category> OP_EQUAL. And for example, Quantumroot – Receive Address: Token Spend Scripts is an in-context example with detailed explanation (that particular script doesn’t use any 2026 CHIPs).
And of course, the CashTokens CHIP is full of explanations and further rationale on this topic.
In summary: BCH contract authors have already been using delegation for years, and it is already very byte efficient.
If anything, the Functions CHIP’s native, immutable functions make delegation safer by simplifying some contract system dependency graphs – places where bugs and/or malicious code can be hidden.
We already have “code that writes code”
If you interpret “mutation” and/or “injection” as some aspect of Turing completeness which BCH does not yet possess: you are mistaken, BCH is Turing complete.
This section is both the most theoretical and the least relevant to practical usage or evaluation of the Functions CHIP.
Once again, the Functions CHIP has no impact on “static analysis” – see:
On an even more theoretical level: BCH was arguable Turing complete for computations evolving across multiple transactions in 2018 (a paper). As described above, a core motivation of the CashTokens upgrade was to enable such messages to be passed across contracts (i.e. transaction inputs), allowing covenants to efficiently build on each other and interact over time. As transactions can have multiple inputs, this necessarily implies that the same Turing completeness is also available within atomic BCH transactions, and any computation can already be encoded to execute atomically, within a single BCH transaction, provided it fits within the VM limits.
So, yes, it can run Doom (subject to VM limits). See also: @albaDsl’s TurtleVm Proof of Concept, which demos an implementation of a CashVM interpreter executing on CashVM.
Zooming out a bit: it’s relevant to note that native loops would enable an unlimited set of further constructions with this same “code that writes code” aesthetic – meaning that distaste for “code that writes code” also doesn’t logically square with support for the Loops CHIP.
I’ve written even more about this over in the new “Code that Writes Code” topic. If this sub-topic interests you, please kindly review that post and post any responses over there.
Request for additional concerns and/or interpretations
If you know of any other concerns related to “mutation”, “injection”, or another euphemism for “risky” that someone ascribes to the Functions CHIP, please respond by Friday so I can incorporate it into this additional rationale section (DMs ok too). Please write or link to at least a few sentences describing the concern in sufficient technical detail for review. Note that a sufficient description includes the terms “unlocking bytecode” and “locking bytecode" at least once.
Above I’ve provided very specific, good-faith technical responses to every misunderstanding raised, with some stakeholders for the 3rd or 4th time now (in this forum and out of band) – if you believe you haven’t been heard, please give us the courtesy of a falsifiable description.
From a more practical perspective:
Offering contract authors clear, native, immutable functions (the Functions CHIP) would optimize contracts, simplify audits, and even reduce the surface area for “underhanded code” to obfuscate malicious behavior, because honest contracts could use native functions rather than today’s clearly-harder-to-understand workarounds.
Reminder: unsurprising functions are safer functions
Attempting to be “clever” by adding restrictions on function definition locations, esoteric failure cases, demanding various kinds of pre-commitments, or otherwise tinkering with function definition cannot prevent contract authors from making “code mutability” or “code injection” related mistakes in contracts that work on BCH today.
Instead, unusual limitations on native functions are very likely to increase development costs and cause new, practical contract vulnerabilities by making CashVM diverge from the native capabilities of other languages in commensurately unusual/surprising ways.
It would be a shame if one of these so-produced edge cases were to set back the port of a decentralized application from another blockchain ecosystem, delay implementation of BCH as a target in a zkVM compiler, or create a denial-of-service vector in a ported contract due to faulty implementation of some unnecessary workaround.
Unsurprising functions are safer functions, and the Functions CHIP’s native, immutable functions are as boring and unsurprising as functions can be.
Summary
The Functions CHIP reduces the risk of unexpected “code mutation” or “code injection” in practical contracts that work today on Bitcoin Cash.
The Functions CHIP would give contract authors the option to use simple, native, immutable functions. Resolving this basic shortcoming of CashVM would shorten and simplify contracts, leading to smaller transactions and safer, more auditable applications.
What’s Next?
Thank you to the stakeholders who have already reviewed the CHIPs and sent in statements.
I’ll give this a week or so, then I’ll reference this discussion in the rationale and begin pulling in public statements for lock-in.
You misunderstood the problem people are trying to solve. I honestly don’t remember anyone suggesting a problem with code mutation.
The problem is about mixing data and code. Turning data stack items into code stack items.
Both not just allowed by your chip, but specifically part of your design requirements.
You can, for instance, do these:
copy another output script. Cut it up and paste stuff in there. Then turn it into callable code.
have the user push data in the unlocking script and without any checking if this is “correct” just execute it.
I don’t mind you having your own termology for things, the functionality is the point.
The functionality:
At the time the output is signed and broadcast, the code that is going to run at unlocking is likewise set and unchangable.
You can reach that requirement that in more than one way, the p2sh solution is to hash the code and store the hash on the blockchain. I think that works quite well.
If script authors can use OP_DEFINE together with introspection they can also use this where applicable […] OP_DUP OP_HASH256 <hardcoded hash> OP_EQUALVERIFY […]
I think Jason has a point here! And I like the original version of Functions. If we can somehow address General protocol concerns and move on with this, it would be a wonderful upgrade for our ecosystem.
I believe Jason understands the problem, as clearly stated in the GP article above. We need a productive solution please. Time is limited, and we are near the finishing line to ship this safely and demonstrate our trust in the VM limit upgrade as well.
Given that we can go the other way today (OP_ACTIVEBYTECODE for example) I don’t see the the horror here for a low level language as Script. And as said many times before, it’s fundamentally possible to do it with emulators such as TurtleVM.
No, time is not limited. Maybe the amount of changes in one year is fine without this one, they are separate suggestions for a reason.
And most importantly is that this issue has been on the table in clear language for 8 months. Bad planning on Jason’s side is no reason for haste for the Bitcoin Cash community.
Maybe I’m unable to explain it well, which is why there is a long form that I’ve linked to.
I’ve said this now several years in a row to you specifically, BCA, if there can be a phone conversation to have a much higher bandwidth meeting then maybe this is solvable in an hour. It probably was in the ABLA case where you ended up apologizing for not understanding what I’ve been saying for 9 months and that I actually was right… A phone call could have shaved off those 9 months and avoided a lot of problems. Not to mention a lot of frustration all around.
I’m trying to be patient about it. Honestly, I am.
But it is frustrating that nobody seems to be able to understand the simple issue of code insertion as I explained with an actual example. If anyone does, please repeat in your own words to see where the confusion lies.
To be clear, the “issue” has not changed since day one. My argument is exactly the same as it has always been. Which is why we use the git repo to make clear it is immutable.
Bringing up “library distribution” (a concept that is in its infancy and can absolutely change in future) is weird.
It may indicate that you are thinking about things from ONE specific expected usage point of view. But as this is a low level programming language component, the usage that I explained is also possible and very likely to be how programmers will use it, as it is actually closer to how Bitcoin has operated for a decade.
Please do just talk about this in a level headed way so we can improve the proposal, as that is my intention.
Because it is possible to improve and fix. As long as we are able to talk about it without pointing fingers.
The examples I’ve seen could easily be classified as programmer errors. Someone being able to write a broken contract that is exploitable shouldn’t be an obstacle for new functionality since we wouldn’t be able to deploy any VM features.
Hehe, you’re not wrong. But “programming” is a 60 year old profession and as those that have been here a long time know, the entire space is geared towards improving the status quo for programmers.
The main reason why new languages get released all the time is this. Improving the experience for the programmers. Less ability to shoot your own foot. (that’s a 1980s programmer reference, apologies)
I’m not sure why there is any push back at all here.
It’s not like I’m asking for much. It’s just that Jason doesn’t acknowledge he got ideas from me or something. While I did applaud him taking my suggested approach. I don’t really care about getting credit for my many solutions already being deployed in BCH, but it would be nice to not get push back every, EVERY single time.
And I have given a minimal intrusive (like, zero things taken away) solution, which seems to not get discussion either…
So, I insist to try and improve things. Maybe others will join in and we can all have a great Bitcoin Cash Scripting engine.
Tom, I may have missed some context, so to be fully clear:
Is the solution you are advocating for to add OP_DEFINE_VERIFY in addition to the two opcodes (OP_DEFINE / OP_INVOKE) in CHIP-2025-05 Functions? Or is it to replace OP_DEFINE with OP_DEFINE_VERIFY?
Is the above your preferred solution, or are you still in favor of multi-byte OP_DEFINE as in your withdrawn CHIP?
That’s applicable to high level languages.
BCH Script already contains foot guns (in the name of flexibility) and it’s plain weird to reserve one opcode for adding some “safety” for one specific use-case.
If an UTXO is locked and the script requires you to provide some data and a signature you need to validate the pubkey.
If an UTXO is locked and the script requires you to provide a specific NFT commitment you need to validate the category.
If an UTXO is locked and the script requires you to provide a specific code blob you need to validate the hash.
Why do we need extra belt and suspenders for the last example?
Validating “unknown” code with the hash is just one way of trusting external code, another one might be to validate that a specific NFT is used as input or that the code is signed by a known pubkey. Do we need extra opcodes for those also?
The withdrawn chip has a bunch of ideas all together. Stack protection, sharing scripts between utxos, MAST, etc are probably too advanced and while I think they are cheap enough to add there seems to be resistance because today people don’t seem to need them.
So the chip remains withdrawn. I’m not pushing for those ideas. I won’t object to good ideas being stolen, though.
Indeed the idea to have an OP_DEFINE_VERIFY next to the proposed op-define is specifically made because the two opcodes have a very different use cases.
Today we have p2sh, which basically means you have zero code in your output, you present all your code at unlocking time.
The combination of op_define and op_define_verify allows the basics of repeatedly calling a method. But the verify directly ties to the existing way of usage in the p2sh.
What the verify method allows is that people create an output of type p2s. BUT it runs some code that may be held secret until unlocking.
During unlocking (in the input) the code is pushed and that is done securely because the hash is already on the blockchain.
So you re-created p2sh from normal components using a p2s and a op-define-verify.
With this basic concept you can build out the ideas. For instance you can create a poor man’s MAST by defining things only in an if-block and as such you can omit this script in the unlocking if it is not needed.
Additionally, you can use the verify feature to ‘pick’ data from another input on the same transaction securely. Even doing things like OP_CAT and similar. Because the end result is protected by hash. This means that if I create a transaction with 10 outputs then I can avoid copy pasting the script across all of them if they reuse it. Instead a couple of opcodes are used to fetch it.
While the original (non verify) can do this too, a user could spend this by building a transaction that pushes a new output with code that then hacks the original locking scripts. It would mean losing money. To instead use op_define_verify your script solves that problem.
Naturally, the normal define does not go away. So all the things people have been explaining before remain possible.
It is useless either way, as @Jonasillustrated above (which seems to have gone over Tom’s head).
If we’d replace OP_DEFINE with OP_DEFINE_VERIFY then people could still make scripts that run “untrusted” code by doing […] <any blob> OP_DUP OP_HASH256 <hardcoded hash> OP_DEFINE_VERIFY […]. I thought Tom was suggesting this option, which is why I strongly resisted it, because it would hamper cases where you don’t need that specific kind of authentication, like when you have the blob push in locking script - it’s already committed and immutable, no need for additional hash verification and wasting 32 bytes with each define, same how legacy P2PK addresses don’t need to hash-verify the pubkey, and yet nobody can change the evaluated pubkey when spending the UTXO because it’s defined in the locking script rather than pushed by spender like in P2PKH case.
Btw, we had a already had contract that footgunned itself by forgetting to authenticate what should be authenticated, the first version of tapswap had a bug where pat changed my contract from P2PK to P2PKH but forgot to add hash verification. Thankfully nobody exploited it, and pat “upgraded” all contracts by using the exploit and moving people’s offers to the new version.
If we’d add OP_DEFINE_VERIFY alongside OP_DEFINE then I wouldn’t object as strongly because it’s just an optimization of a particular use of OP_DEFINE, probably not worth the trouble, and it would expand the scope of functions CHIP and extra bike shedding could prevent it from getting over the line for '26 activation. If Tom wants this in addition to OP_DEFINE, he should make a stand-alone CHIP to try add it.