Malfix is a proposed consensus change in Bitcoin Cash designed to address transaction malleability. It’s a fairly controversial proposal, in part because it would be the most invasive hardfork BCH has done to date, but also because people just don’t believe it’s needed. In this article I will present a new use case for Malfix that has not been considered before … giving BCH full Ethereum-style smart contracting capability.
This might sound crazy at first. How could a fix to transaction malleability transform BCH into an Ethereum rival? Well, let’s dig into it.
At its core Ethereum allows users to create “contracts” on the blockchain. Within these contracts users are allowed to save data. The contract defines a number of functions which, when accessed via a new transaction, allow users read the data, manipulate it in some way, and save it back into the contract.
It’s this core functionality of saving data (which we’ll call state from now on) in the blockchain and having future contracts read and manipulate that Bitcoin is said to be lacking. In Bitcoin we have a scripting language, but it only allows us to put conditions on the release of coins from an address. Most people will tell you that these scripts can’t save state into the blockchain and they can’t read the state from any other transaction the way that Ethereum contracts can.
Or can they?
In November of 2018 Bitcoin Cash added a new opcode called OP_CHECKDATASIG. The BSV crowd fought bitterly to keep this opcode out of the protocol but were ultimately unsuccessful.
The opcode appears to be extremely basic. All it does is accept an arbitrary signature, public key, and message and validates the signature.
This type of functionality is so fundamental to cryptocurrencies that it’s rather amazing it wasn’t included in Bitcoin from the start.
But while it seems rather pedestrian, it’s actually extremely powerful. And every day that goes by we’re learning more and more of its power.
The first major feature enabled by OPCHECKDATASIG is something called covenants. Prior to OPCHECKDATASIG the Bitcoin script could only put conditions on the release of funds from an address. That’s it. For example a script could say “Alice can unlock and spend the funds from this address if she can provide a valid input to the script. But once the funds are unlocked she can send them to any address she wishes.”
A covenant is a restriction on which address Alice is allowed to send the funds to. How does OP_CHECKDATASIG allow us to place this restriction? Consider the following script:
This is a typical spend script for a Bitcoin transaction. The OP_CHECKSIG opcode hashes the transaction data and checks that signature is valid for the given public key and transaction hash.
Now imagine we take the exact same signature and public key that just passed the OPCHECKSIG check and run them through OPCHECKDATASIG:
This script will evaluate to true if the message is equal to the raw transaction data for this transaction. Since OPCHECKDATASIG will hash the message and validate the signature against the public key and the hash, if the same signature and public key pass both the OPCHECKSIG and OP_CHECKDATASIG checks then we know the message that is left on the stack must be the raw data for this transaction.
This is very clever. We’ve now gained access to the raw transaction data and can parse it with the script and make sure the outputs in the transaction are sending the coins to where we want them sent. In other words, we can now use the scripting language to place restrictions (covenants) on which address(s) a transaction is allowed to spend to.
What can we do with that? We’ll a number of interesting things. For example, there’s the Last Will contract which implements a dead man switch with hot and cold keys. There’s the Vault scripts designed by Emin Gün Sirer and coauthors which makes it extremely difficult to steal coins.
We can also create a form of a transaction loop. No, Bitcoin scripts cannot loop. This is by design. But we could put a covenant on a script that requires the coins be sent back into the same address from which they are being spent. Basically once the coins go into the address they are in there permanently and cannot come out. I call this script a “loop” because every time someone spends from this address, the same script gets executed again. This will keep happening as long as someone is willing to keep making transactions (which obviously costs fees).
But this seems rather useless. Why would we want to do that?
Building Ethereum-Style Smart Contracts
There’s even more we can do once we have the raw transaction data on the stack. We can push the previous transaction to the stack and validate that it is indeed the previous transaction by hashing the previous transaction and comparing the hash to the outpoint hash in the current transaction.
But wait there’s even more!
Normally when a script executes any data that is computed as part of the script is for all intents and purposes lost. The results of the computation (if there are any) are not saved anywhere in the blockchain and are certainly not accessible by other transactions. However, using covenants we can require that the result of the computation be saved in the transaction’s OPRETURN output. This requires that the spender of the funds execute most of the script locally on his computer to determine the result of the computation. Once the result is calculated the covenant requires it be placed in OPRETURN.
Now notice what we have. The script executes and performs a bunch of computation. The result of that computation is saved in the transaction. And this transaction data is accessible to the next transaction spending from this one. In other words, Bitcoin scripts are now capable of saving state in the blockchain in such a way that future transactions can read that state, perform some computations and mutate it in some way, and save it back to the chain. We’ve basically just created Ethereum on BCH!
Just an example of what you can do with this. Ethereum has something called ENS ― the Ethereum Naming System. It’s basically a smart contract that allows people to register names (or domains) in the blockchain and map data (such as an IP address or an Ethereum payment address) to their name.
Using the above scheme I created a BNS (Bitcoin Naming System) smart contract design which basically does the same thing. The contract is an anyone-can-spend looping contract like described above. When someone wants to register a name they push the current transaction, previous transaction, name to register, and a merklix exclusion proof to the stack. The contract loads the root hash of the contract’s state tree from the previous transaction, verifies the exclusion proof against the previous merklix root proving that nobody has previously registered this name, then updates the root hash and saves the new state in the transaction. Just like the ENS smart contract on Ethereum, BNS manages a state database of names and prevents double registration. Names can then turned into quantity one SLP tokens and traded.
We’ve made it 99% of the way here before running into a big problem. When you push data to the stack the BCH consensus rules require that the data push be under 520 bytes. When pushing the previous transaction to the stack, if it’s not under 520 bytes the transaction will fail.
It’s conceivable that the transaction could be under 520 bytes, but it grows with each new transaction. Watch this.
Transaction B pushes previous transaction A to the stack.
Transaction C pushes previous transaction B to the stack which contains transaction A.
Transaction D pushes previous transaction C to the stack which contains transactions A and B.
After one or two transactions we’ve exceeded the 520 byte limit. So we’ve got so close to Ethereum-style smart contracts, but we fell just short.
Malfix introduces a new transaction version (v3). Transactions with this version have two transaction IDs. The normal TXID, calculated the normal way and a new UTXID which is calculated as a hash of the transaction with the input scripts removed. In most places you would still use the TXID like normal. The sole difference would be when referencing a version 3 transaction in a transaction input’s previous outpoint field. There you would reference the UTXID instead.
What this does for our above scheme is it means that we don’t have to push input scripts onto the stack when pushing the previous transaction. This will keep our data pushes under 520 bytes permanently (they won’t grow with each transaction like before).
This one relatively small change is all that is needed to make it work.
Issues With Malfix
Malfix is relatively simple to implement from a full node perspective. The main problem with it is unlike every previous Bitcoin Cash hardfork, this one is fairly invasive. All wallets would need to be upgraded to know how to spend from a version 3 transaction.
If a wallet was not upgraded and received a version 3 transaction it couldn’t spend those coins.
We could possibly mitigate this by bumping the version byte (or the reserved bit) in the Bitcoin Cash address format.
Upgraded wallets could be programmed to only build version 1 or 2 transactions when sending coins to addresses with the old version byte and (optionally) use version 3 transactions when sending to addresses with the new version byte. This should mostly prevent non-upgraded wallets from receiving version 3 transaction and allow for them to upgrade at their own pace.
There are likely other types of services that could break due to malfix and we’d have to think long and hard about what those might be and how to ease the upgrade.
Comparison to Ethereum
So does this mean BCH has complete Ethereum functionality? Not quite. Remember Ethereum was designed from the ground up to work this way. Bitcoin clearly wasn’t designed with this type of functionality in mind and we can only make it work by employing clever, hacky solutions to work around the inherent limitations of Bitcoin. So I’d expect Ethereum contracts to always be easier to work with though higher level languages like spedn would certainly help the developer experience.
BCH also saves state differently than the Ethereum. In the later you can basically save as much of any type of data you want in the contract as long as you’re paying gas for it.
In BCH we can only save a maximum of 220 bytes in OP_RETURN, which is basically only enough for a few hashes. This means that our state is going to need to be the root of a tree of data rather than the data itself as was the case of the BNS contract. The people working with the contract will have the responsibility of storing the actual data themselves (though the state can always be rebuilt locally by scanning all transactions of the contract).
And finally, on Ethereum contracts can make function calls to other contracts. I’m not sure if that would be possible on BCH though I haven’t yet given it much thought.
So all in all creating Ethereum-style smart contracts on BCH is indeed feasible if we have malfix though in the end we probably won’t be able to do Ethereum as well as Ethereum. But it’s still a massive upgrade to the scripting language.