RETRACTED CHIP-2026-06 CashTokens FT+NFT Ambiguity Fix

This is a maintenance CHIP that removes an annoying wart in the Script introspection API that’s been left from CashTokens, see here for background: https://github.com/cashtokens/cashtokens/issues/29

RETRACTED 2026-06-16. Turns out the CHIP’s approach is not really free: it would force technical debt to wallet developers, who would now have to worry about this wart when trying to optimize user balances into least number of UTXOs.

Summary

Under the current CashTokens Script introspection API, a UTXO holding both a fungible token (FT) amount and an immutable NFT with a zero-byte commitment is indistinguishable from a UTXO holding only the FT.
This proposal adds a consensus rule prohibiting new outputs from carrying that combination.
Existing UTXOs already in the ambiguous state remain spendable but must be split into separate FT and NFT outputs when spent.
Blockchain scan shows that this exotic combination appeared only a few times and closing the gap is unlikely to cause grief to anyone.


3 Likes

NACK

This breaks userspace in surprising ways. Imposes extra cost on apps out there that will now surprisingly break if this activates… previously valid txns become invalid, etc.

Live with the wart – or devise a new op-code that disambiguates – we can create an “extended opcode space” for new refined opcodes, if need be.

Breaking userspace is bad. We should avoid it as much as possible.

1 Like

I guess it’s not good that Calin doesn’t like it.

But at a first glance this seems like a sensible fix to me.

The tiny number of UTXOs affected do not become unspendable, and it closes the gap so more aren’t created in the future.

I think the suggestion to design a new OP_code might be overkill in the other direction. But worth a discussion I suppose.

1 Like

This immediately imposes a cost on every wallet out there to remember this corner case exception.

EC right now would need to be updated else randomly when sending tokens you get unexpected errors that txn is invalid.

All because 1 op-code out there is ill-specified.

Rather than fix the op-code we push the “ugliness” up to userspace / apps.

This is just the epitome of a bad idea.

Why? Because it’s legitimate to want to pack UTXOs with as much stuff as possible (wallets sometimes want to do this) → it’s legitimate to have empty NFTs live alongside >0 FT. This is a common case in EC if you just use the wallet and you happen to create empty NFTs for yourself.

Now EC has to remember not to do this. EC breaks. Right away.

This is just a bad idea. Pushing ugliness that was inherent in a mis-specified op-code – up to where everybody can see it – is not good.

The root of the problem is that the OP_*COMMITMENT op-codes do not properly distinguish between empty commitment and missing commitment. This is the op-code’s specification’s fault.

A new op-code that allows disambiguation is the solution. Not this.

Is that true? If it’s handled at the node/consensus level, then wallets don’t need to know about it (if they don’t have token minting capability)? For instance in Selene afaik it’s not possible to make this kind of transaction yet (we don’t support token minting within the app just yet) so it’s not a problem for us.

And that’s Selene. Binance still on send/receive BCH on legacy addresses or the rest of the laggard BCH ecosystem isn’t going to notice.

For the few users that are impacted by this change, it seems like a recovery tool could be made that helped do the splitting so they weren’t affected. In fact perhaps if such a tool was created and published alongside the CHIP itself @bitcoincashautist then it would improve the case for this solution. Virtually no-one is affected, and people who are affected have the tool to fix their problem (which they only need fixed once, and then it won’t occur again) provided.

Nope.

They need to know about it because they compose txns. Some set of txns that were previously ok, now are insta-invalid. Wallets need to know these things. Cost to remember this rule is imposed on apps. All because some op-code was ill-specified. Existing apps like EC now would make invalid txns through normal UI actions!

Just the epitome of a bad idea – we expose ugliness to the world with extra rules to create “beauty” on some deep level nobody sees but specialized contract authors.

Better solution: some new op-code to disambiguate.

1 Like

BTW the title of this is misleading – it leads one to think that “this is a simple ambiguity fix nobody will see” → in reality this is a “Fix” that will break userspace.

Better title:

“CashTokens FT+NFT wallet breakage cost imposition for specific reasons”

:stuck_out_tongue:

1 Like

I just gave you an example of a popular and BCH focussed wallet that CAN’T currently compose such transactions. And as far as I can tell, that’s the case for the vast majority of BCH supporting wallets that exist (most of which aren’t even up to date with CashTokens).

If we add the ability to mint NFTs in the future, with some kind of publicly acccessible transaction builder, we’d need to ensure this corner case wasn’t possible. But that’s a trivial thing, maybe 1 or 2 lines of code and I’m not concerned about that need.

Yes, so the question becomes, what is the burden on the tiny number of affected wallets (EC, maybe Cashonize, probably CashToken studio, maybe one or two others) vs the cost of creating an entirely separate OP_code to address what is a tiny issue.

Are we going to make a new OP_Code for every tiny little thing like this? As far as I’m aware, the general principle is to be very very prudent with new OP_Code allocation. And in this case the “ecosystem burden” onto existing wallets/nodes seems very very small.

Cost of a new op-code is minimal – because nobody has to worry about new op-codes unless they care about the new op-code.

Cost of extra byzantine rules regarding what is or is not a valid txn are paid forever because every time somebody must implement a wallet they must be a blockchain lawyer.

It’s a cost that is forever paid by anybody writing code to make txns. Bad design.

1 Like

Just to clarify: One can potentially support many many opcodes there is no real huge cost to a robust op-code language, in principle.

Certainly breaking userspace randomly or having extra corner case rules that are unexpected is uglier/worse, IMHO.

I was under the impression that we have a limited set of OP_Code codepoints to allocate for the entire life of BCH (or maybe we could do a likely serious overhaul to create fresh ones?).

From what I understand, these are the ones we have left. Seems to me like the permanent cost would actually be expending one of these slots forever, rather than making a tiny tweak to a couple of existing affected wallets.

But that’s already true. We’re not adding some new category of burden, just one extra small detail. Somebody has to be a BCH expert to make a BCH wallet, yes - that’s already true. There’s already hundreds of things they need to be aware of to make that work, this is just one more that won’t stand out at all to anyone who arrives once it’s already required. To them, it’s just another one of the requirements in consensus.

Currently non-affected wallets aren’t really relevant.

Yes, forgot to add that part to the CHIP. Yeah, that sucks, some wallets would indeed error, and would have to watch for this wart to avoid such errors (or just split by default). So we don’t really remove the wart, we just move it from Script to TX construction. It’s a trade-off.

Note that for a wallet to hit this edge case the user must have a set of UTXOs from which it’s even possible to create such an output. That means:

  • Have an empty, minting, or mutable NFT.
  • Have some FT amount of the same category.

Hybrid categories are rare as it is, and most have non-empty immutable NFTs to track some token state. If the NFTs is minting/mutable it will typically be held by a contract, not by a user wallet.

It’s very rare to even have UTXOs that could create a TX that would fail the new rule!

But then again, it’s very rare for a contract to hit this edge case, it can be mitigated by category creators: if you use hybrid FT+NFT category then simply have your contract prohibit empty NFTs and put some 0x00 in the commitment!

Yup, I created a new category just now to test this, EC always tries to compact as much balance as it can into 1 UTXO so it would automatically create a faulty TX if preconditions are met (particular structure of user’s balance + trying to send some FTs out).

False. Wallets like EC would automatically create faulty TXs if users had some token balance where both FT and empty NFT exist for the same category. Doesn’t have to be a minting NFT.
I tested Selene and it automatically splits, so it would avoid the edge case by default, but EC (and maybe others, didn’t check) would hit the edge case by default because they try to compact user balance as much as possible.

No need for it, any wallet that automatically splits is the “recovery tool”.

Nah. Once we run out, we can just declare the last byte in the space to mean “extended” (similar to how push opcodes work, something follows after the 1st byte) and switch to 2-byte code-points e.g. some OP_TWOBYTEOPCODE == 0xff01.

So, a pair of OP_UTXOHASNFT and OP_OUTPUTHASNFT would close the 1-bit gap, without annoying anyone, maybe I should propose that instead?

Is this wart even annoying enough to do anything about it? I don’t know, we will learn here by disussing it :slight_smile:

So far I’ve hit it when trying to construct signature preimage with loops+introspection. Others like @sahid.miller have noticed it but afaik it didn’t prevent them from building what they wanted to build. For the case of wanting to commit to entire transaction, the OP_SIGHASH would close the gap.

So, the only use-case of either prohibiting the combination or adding the 2 opcodes are more flexible commitments to TX contents which need to guard against this 1-bit leakage.

When you deploy a contract you usually control token genesis so can just prohibit empty NFTs at the category level and avoid the problem by not having it.

That leaves only category-agnostic contracts like TapSwap exposed, and only if they want to create a buy order for FT+empty_NFT: they can’t actually express this requirement, can’t make such order.

But they could instead create 2 orders separately, one for the FT, other for the empty NFT.


If we want to remove this wart, we have to pay some cost: either annoy wallet devs or just add 2 new opcodes. Or we can just leave it at status quo because it’s not annoying enough (which was the original CashTokens rationale, to just live with this little exotic ambiguity).

2 Likes

Yeah thanks man for the synthesis here. Basically yeah those are the 2 sides of the story.

I want to thank you for raising this CHIP and bringing it to the fore. We could very well do it this way – for sure. I just think maybe we should just eat the “ugliness” in the op-code level where they began, basically – as you stated above is my position here.

For what it’s worth my arms can be twisted to forbid these typos of new UTXOs going forward but I really do think it’s a degradation in features of what a UTXO may contain – all to plug an op-code that should have perhaps originally been better designed… sigh.

So, a pair of OP_UTXOHASNFT and OP_OUTPUTHASNFT would close the 1-bit gap, without annoying anyone, maybe I should propose that instead?

Question: If we do OP_[UTXO|OUTPUT]HASNFT – or some such – would that then paint a “complete” picture of the “bitfield” as it stands now? Or would an OP_[UTXO|OUTPUT]BITFIELD op-code still be needed to get the full picture?

Ideally we stay away from a bitfield op-code since it seems to marry things to the exact serialization we do now in a way that perhaps we may regret later if we change serialization formats…

1 Like

Yes, it would.

No it wouldn’t. Because hasFT + 0-amount combination is prohibited even though TXO format would support it were it allowed. FT container can’t be empty (so OP_*TOKENAMOUNT returning 0 unambiguously means no FT), but NFT container can be empty (so OP_*NFTCOMMITMENT returning 0 can mean either no NFT or empty container).

Yes, that matches my thinking, too.

1 Like

I think status quo is fine. From a compactness perspective I like that a single UTXO can have an empty NFT + nonzero FT. From a raw script perspective, I can see the annoyance. From a userland perspective, it’s easily handled on the app layer. I think if it’s annoying enough for script authors, it’d be nicer to get an opcode to handle it than to change existing VM behavior.

I think it’s a rare pattern mostly because there haven’t been enough people experimenting with BCH script, not necessarily because it’s a bad idea. While I can’t immediately think of a reason I’d want to do it, the status quo for consensus changes also remains “no by default” unless the CHIP can demonstrate compelling evidence for why the change is necessary and desirable.

It’s already documented in the CashTokens CHIP and it was determined to be a trivial enough quirk that it wasn’t worth delaying the upgrade. I currently still see it as a trivial quirk and I think our R&D efforts are better spent elsewhere.

2 Likes

So it seems like:

OP_UTXOHASNFT and OP_OUTPUTHASNFT (D4 & D5)

might be a solution everyone could get on board with?

2 Likes

Yeah. Question is: does anyone even need it?

My motivation was some future-proofing: so one can commit to the entire transaction using loops+introspection without these “invisible” bits leaking. I thought just banning the FT + empty_NFT combination was a free way to get that, but I was wrong, so I will retract the CHIP and discussion will be useful next time someone asks about it. :slight_smile:

OP_SIGHASH will achieve the same goal (airtight commitment). So, you’d only need loops + introspection if you want something more flexible than those baked-in sighash algorithms. But, if you want flexible then you’re not committing to the whole TX, in which case that one “invisible” bit probably won’t affect your flexible construction.


Adding this to top:

RETRACTED 2026-06-16. Turns out the CHIP’s approach is not really free: it would force technical debt to wallet developers, who would now have to worry about this wart when trying to optimize user balances into least number of UTXOs.

It’s a step toward deterministic contract outputs, but I don’t think it’s possible to enforce the outputs of a contract be totally deterministic, because new capabilities may be added in the future.

The possibility of weird null NFTs being emitted from a contract issuing sets of fungible tokens is annoying, but I’m not sure there is a tremendous amount of risk there, if it can be enforced that the potential NFT is immutable.

1 Like