Using OP_CODESEPARATOR to implement Taproot on BCH

Currently, we have <pubkey> OP_CHECKSIG, usually wrapped with OP_HASH160, when we have outputs that could be spent by providing any valid signature. There were some ideas how to implement Taproot on BCH but I found nothing that involved OP_CODESEPARATOR, so I share it here.

The basic idea is simple: OP_CODESEPARATOR can be used to separate many parts of the script. So, if we want to get 2-of-2 multisig that will be hidden behind some public key, we could do it that way:

output script: <pubkeyAlice> OP_CHECKSIG
input script: <signatureBob> <pubkeyBob> OP_CHECKSIGVERIFY OP_CODESEPARATOR <signatureAlice>

That would mean two things: first, for each TapBCH public key, there is a need to provide some valid signature. That means, all previous owners are protected, because they are not exposed to any new cryptographic risks. Second: there could be more ways to spend any coin that is hidden behind some public key.

And when it comes to unspendable public keys, then in practice they are not needed at all. We have Schnorr signatures, so we can start from any N-of-N multisig that will guarantee, that all participants agreed to everything. And the best thing is that by placing OP_CODESEPARATOR in the right position in our input script, we can decide, which things are signed, and which are not. So, it is possible to make some signature for a shared key, that will be valid only if the input script will contain OP_CODESEPARATOR <some> <opcodes> <goes> <here>, that will be joined with <sharedPubkey> OP_CHECKSIG.

So, as far as I know, transaction standardness is the only limit that stops users from using those features right here and right now. Or maybe I am wrong and those transactions are standard? Can you see any obvious ways, how this way of spending coins can be unsafe, assuming that we use Schnorr signatures, and nobody knows the private key to the shared public key?

So many questions

  • What exactly do we gain from taproot that we don’t gain from other advanced upgrades (like CashTokens, Group Tokens, 64bit Numbers, Introspection)?
  • What exactly did BTC gain from implementing Taproot?
  • Should taproot be implemented as a protocol rule (hard-fork) or as a soft-fork like in BTC (probably a rhetorical question) and why?
  • How does taproot affect the future scalability of Bitcoin(Cash)?
  • How does taproot affect the privacy of Bitcoin(Cash)? I heard some rumours the net effect is negative. Change my mind.

This is not how it works. You can’t have OP_CHECKSIGVERIFY and OP_CODESEPARATOR in the input script, only push operations are allowed. Spenders are free to add whatever data they want. If the output script is <pubkeyAlice> OP_CHECKSIG then <signatureAlice> is enough, and in fact if you add more data to the input it will fail the TX because of clean stack rule.

You can think of “locking script” as a function definition, and “unlocking script” as function variables. Input will unlock if the function evaluates to true. P2SH works the same, but the locking script is a hash of fuction definition, and the function definition is revealed at time of spending alongside the data to unlock it. P2SH merely authenticates the function definition, so you can’t slip in your own code and must use the one that the output creator intended.

Also, I think there’s a misunderstanding of OP_CODESEPARATOR. It doesn’t hide anything, everything is revealed as part of redeem script and it’s just a marker in the locking script for operation of other opcodes, which makes “self-mutating” covenants easier.

OP_CODESEPARATOR:

Makes OP_CHECK(MULTI)SIG(VERIFY) use the subset of the script of everything after the most recently-executed OP_CODESEPARATOR when computing the sighash.

This lets you have a “free” part not covered by the signature at the start so spenders can update the contract with some data.
Introspection opcode also make use of it, to allow contracts that inspect their own code to obtain a subset. Useful when you want to have a covenant that requires a fixed part of the code to be passed on but want to allow to change the header (usually the pubkey template).

OP_ACTIVEBYTECODE:

OP_ACTIVEBYTECODE pushes the serialized bytecode for the instructions currently under evaluation beginning after the most recently executed OP_CODESEPARATOR and continuing through the final instruction. If no OP_CODESEPARATOR has been executed, OP_ACTIVEBYTECODE pushes the full, serialized bytecode for the instructions currently under evaluation. (In the Satoshi implementation, this is simply the CScript contents passed to the current EvalScript .)

What is it that you want to achieve? Allow multiple spending paths and hide unused spending paths? This is possible with “side-cart” contract designs relying on introspection opcodes. The “main” output would store a merkle tree and all the funds, and it would require another input with dust amount to be successfully spent alongside it in the same TX which would introduce the code to be run into the TX and hash to a leaf node of the “main” input.

If you just want to hide which key is used (among many) then you don’t even need a side-cart because you don’t need to execute the revealed data, so you’d just authenticate a key against a leaf node and verify the signature.

Too bad P2SH was not present from the start, because if it was - we’d probably have just 1 output template and P2PK would be hidden “inside” so all outputs would look the same at rest and nobody could tell what they do until they’re spent. Here’s how to embed P2PK inside P2SH:

  • Locking script:
    OP_HASH160 OP_DATA_20 redeem_script OP_EQUAL
  • Redeem script:
    OP_DATA_20 pubkey OP_CHECKSIG
  • Unlocking script:
    <push signature> <push redeem_script>.
1 Like

What exactly do we gain from taproot that we don’t gain from other advanced upgrades (like CashTokens, Group Tokens, 64bit Numbers, Introspection)?

It is all about hiding P2PKH and P2SH behind the same address type. So, if we want to keep the same output type, then it is logical to change the input script.

What exactly did BTC gain from implementing Taproot?

See above. Of course they will “gain” it only by abandoning older address types and fully moving into Taproot, so as long as it is not the case, there are more address types, instead of less.

Should taproot be implemented as a protocol rule (hard-fork) or as a soft-fork like in BTC (probably a rhetorical question) and why?

It is obvious that BCH is open to hard forks. Why? Because that’s what happened in 2017, and BCH will follow that path, by doing hard forks every time when needed. Because it is easier to implement that.

How does taproot affect the future scalability of Bitcoin(Cash)?

By skipping parts of the script that are not executed. So, if you can spend a coin in <this_way> OR <that_way>, you only reveal one of them, which is used, and the other one is hidden.

How does taproot affect the privacy of Bitcoin(Cash)? I heard some rumours the net effect is negative. Change my mind.

If you cannot see all conditions, then there is always a chance that “this output could be spent in one more way, but nobody knows about that”. The net effect is negative, because BTC introduced it as another address type, so if people will refuse to upgrade their addresses, there will be more address types in use.

This is not how it works.

So, if it is invalid in the current consensus, then my idea can be done only in a hard fork way. But of course this can have some other drawbacks, then something else should be used, it is just an idea, the whole question if BCH should implement Taproot at all is still open, and I don’t have a strong opinion on that.

It doesn’t hide anything

You don’t have to know the script before OP_CODESEPARATOR to make a valid signature. So, Alice can prepare a script, and pass transaction hash, output number, and a part of the script, starting from OP_CODESEPARATOR to Bob. Then, Bob can validate it, and sign it, without knowing the full script. If Bob passed a fresh, random key to Alice, it is totally safe, because then no other things are connected with such key.

The whole idea of OP_CODESEPARATOR is about signing only a part of the script. And if some part of the input script is committed to that public key, it is possible to avoid malleability.

What is it that you want to achieve? Allow multiple spending paths and hide unused spending paths? This is possible with “side-cart” contract designs relying on introspection opcodes.

Thank you for that, I need to dig more into those topics. Because it may turn out that all Taproot functionalities are already there, then nothing else is needed, and then it is possible to explain to people coming from BTC that “we don’t need Taproot, because we can do the same things in other ways”.

Too bad P2SH was not present from the start, because if it was - we’d probably have just 1 output template and P2PK would be hidden “inside” so all outputs would look the same at rest and nobody could tell what they do until they’re spent.

I thought about the opposite thing: having only P2PK, and doing any kind of “pay to script” by committing to that public key. But yes, having only P2SH would achieve the same result.

1 Like

True, but you still have to reveal the whole script with the input regardless of those separators, they only affect signature preimage building and the new OP_ACTIVEBYTECODE. You sign only a part of it, but the whole must be revealed, authenticated against the P2SH enclosure hash, and the whole is executed. OP_CODESEPARATOR only influences what’s covered by the signature, not by the enclosure hash - the whole script is still there, revealed, executed, and recorded with the TX into our public ledger.

Made a demo and published it here: Emulating OP_EVAL using Bitcoin Cash Native Introspection Opcodes (OP_UTXOBYTECODE) | by bitcoincashautist | Jul, 2022 | Medium

1 Like