Ethereum was the first platform to enable Turing-complete smart contracts back in 2015. It is still the most popular platform for smart contracts, even as other smart contract platforms have emerged, like EOS or Tezos. And although Ethereum was the first platform with Turing-complete smart contracts, it was already possible to create rudimentary contracts on Bitcoin using a language called Bitcoin Script. And Bitcoin Cash has recently been improving its smart contract capabilities. While not as advanced as Ethereum's, they allow for useful on-chain contracts with unique benefits.
This article focuses on the unique differences between smart contracts on these three platforms. Since the focus is on the smart contract and scripting functionality, we don't go into the platforms' fundamentals or blockchain in general. This article is also specifically about on-chain smart contracts. Several second layer smart contract solutions exist, such as RSK. These solutions are definitely worth discussing, but are better suited for an article of their own.
Ethereum: Stateful & Turing-complete
Ethereum is the biggest smart contract platform in the world, and with good reason. Smart contracts in Ethereum use the Ethereum Virtual Machine (EVM), which is a Turing-complete Virtual Machine. This means that the EVM is able to compute anything, given enough resources. This is conceptually similar to many other general-purpose platforms, such as the JVM, which is used in the execution of Java programs.
The EVM explained
A big difference between these general-purpose platforms and Ethereum's EVM is that Ethereum's smart contract code is executed by all Ethereum nodes to verify transaction validity. To compensate mining nodes for the execution of this code, all EVM opcodes have an associated gas cost, referenced in the image below.
Every transaction uses an amount of gas, depending on the opcodes used. Gas is paid for with Ethereum's native currency, Ether. To limit the amount of computation these nodes have to do per block, there is a limit on the amount of gas that can be used in a single block, called the block gas limit.
During the execution of smart contract functions, contracts can store and access the necessary data. This data can live in different locations depending on its use. First is the stack, which holds the values used in computations. Only the top 16 items on the stack can be easily accessed, so it's not suitable for longer term storage. The use of a stack for computation is shown below.
To complement the stack, the contract memory data location is used to store, retrieve and pass data within the current contract execution. The values can be retrieved from memory to be used on the stack for computation and the results can be stored back in memory. These values only persist during the current execution; at the end of the execution the memory and stack are wiped.
The final data location is contract storage, which is used to persist data across contract executions. Persistent variables, like token balances, are stored in contract storage. To achieve data persistence, the contract storage is stored on every Ethereum node. So memory is analogous to RAM, while storage is analogous to hard drive storage or a persistent database.
More information about the inner workings of the EVM can be found in this excellent article by MyCrypto.
Writing smart contracts
While all smart contracts use the EVM, most of them are not written by hand using EVM bytecode, like most JVM bytecode is not written by hand. Instead there are several high-level languages that can be used to write smart contracts in Ethereum. The most popular language for this is Solidity, but Vyper also sees some usage. An example of a very simple Solidity smart contract is included below.
pragma solidity ^0.5.0;
contract SimpleStorage {
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public view returns (uint) {
return storedData;
}
}
Interacting with smart contracts
Smart contracts in Ethereum exist as bytecode on the Ethereum network. This means that Solidity code gets compiled to bytecode, which is then deployed to the network by sending a deployment transaction. This is a special kind of transaction without any recipient but with the bytecode as the transaction data.
These deployed contracts exist only as blobs of EVM bytecode on the network, which are nearly impossible to use on their own. To interface with them, an Application Binary Interface (ABI) needs to be provided, which includes a list of all public functions and their parameters. To grant more insight into these contracts, there are services like Etherscan that allow you to verify contract source code so users can inspect the code before using the contracts.
All smart contracts can be accessed directly by connecting to an Ethereum node and using its JSON-RPC interface. But many smart contracts are instead accessed through a frontend application that connects to a node and manages the ABI. This can be done with one of many different Ethereum SDKs like web3js or ethers.js, which call the JSON-RPC under the hood. This offers a better experience for contract users, as the most difficult parts are abstracted away.
Smart contracts can be interacted with in two different ways: through calls and through transactions. A call is a local invocation of a contract function, and it does not broadcast anything to the blockchain. Because of this, calls are read-only, can not make any changes to the contract state and do not incur any fees.
On the other hand, transactions are write operations that do get broadcasted to the network, are included on the blockchain and do incur a miner fee. To take ERC20 tokens as an example, token balances are retrieved with a call, while tokens are transferred with a transaction.
Interaction between smart contracts
These properties of Ethereum - a Turing-complete VM and persistent storage - allow you to create any kind of decentralised applications running on-chain. A great example of this is the DeFi ecosystem, with applications such as Maker, Uniswap and Compound, as well as the ERC20 and ERC721 token standards. These applications allow for complex functionality with an unlimited number of dynamic participants.
On top of that, smart contracts themselves can be participants in other smart contracts, allowing for strong integration and composition between these contracts. An example is a smart contract holding a token balance and lending them out on Compound or exchanging them with Uniswap. Another example is using rDai to automatically invest DAI and contribute the accrued interest to charity.
While smart contracts can interact with each other, every on-chain transaction needs to be originated from an external account. So contract-to-contract interactions still have to be triggered through an initial transaction by a user. After this initial trigger, contract-to-contract interactions are similar to direct interactions.
In other words, it's possible for contracts to call read-only functions of other smart contract or trigger write operations. Since only external accounts can trigger transactions, write operations between contracts are referred to as internal transactions or message calls to make the distinction with user-initiated transactions.
Bitcoin: Stateless & simple
All Bitcoin transactions, including regular transfers, are powered by a stack-based programming language called Bitcoin Script. And while the EVM has been designed for Turing-complete and integrated smart contracts, Bitcoin Script is intentionally more limited and works in a fundamentally different way.
Bitcoin Script explained
Like the EVM, Bitcoin Script uses a stack to hold values and perform computations on these values. But unlike the EVM, the stack is the only data location available in Bitcoin Script. This means that it's difficult to store multiple values to use later in the contract execution. But more importantly, this means that it is impossible to store and modify values that persist across contract executions.
This is the biggest difference between Ethereum's smart contracts and Bitcoin's. Ethereum's model is stateful, while Bitcoin is stateless. Ethereum can be considered analogous to the common imperative mutable-data programming paradigm, while Bitcoin is analogous to the functional immutable-data paradigm.
Bitcoin's model allows transactions to be verified independently and much more efficiently, which makes it easier to parallelise and shard transactions. But without any mutable data storage, it is more difficult to create the complex smart contracts that Ethereum allows. ERC20 contracts, for example, have to keep track of token balances and change them.
Besides these differences in state, there are other things limiting complexity in Bitcoin's smart contracts. Notably, Bitcoin Script lacks support for some arithmetic functions and any form of looping or recursion. Contracts also have an effective size limit of 520 bytes and can contain 201 opcodes at most.
Most smart contracts on Bitcoin fall in a few categories of simple contracts. Examples are multisig contracts that can be spent by multiple participants, or Hashed Timelock Contracts (HTLCs) that can be spent by revealing a secret or reclaimed after time has passed. And because these contracts are very simple, most of the value is gained from combining different contracts with additional off-chain application logic. That way simple contracts can be combined to create complex solutions such as the Lightning Network or Atomic Loans.
Bitcoin transactions explained
Bitcoin transactions are created using indivisable chunks of Bitcoin, called transaction outputs. When these outputs are available, they are called Unspent Transaction Outputs (UTXOs). UTXOs are locked using a locking script (or scriptPubKey
) that specifies the conditions to spend the output. When attempting to spend a UTXO, an unlocking script (or scriptSig
) is provided. These scripts are then executed together; the transaction is only valid if the scripts execute without errors and the resulting value is TRUE.
As with the EVM, all Bitcoin nodes execute these scripts to validate the transactions, but Bitcoin lacks the notion of a gas cost, so instead fees are paid to the miner per byte of transaction data. To limit the work that the nodes have to do, there are also limits in place on some of the more intensive script operations.
Interacting with smart contracts
Smart contracts in Bitcoin are written using the Pay-to-Script-Hash (P2SH) pattern. The locking script in the P2SH pattern contains a script hash, and requires an unlocking script that provides the full script (called the redeem script) as well as the unlocking script for this redeem script. The image below shows this pattern.
The Bitcoin node software validates these smart contract transactions in two phases. First, the redeem script is hashed and checked against the hash in the locking script. If they match, the unlocking script is used to unlock the redeem script as if the redeem script was the initial locking script.
Because only a hash of the bytecode is stored on-chain, the full bytecode has to be stored off-chain, and included in the unlocking script of any contract execution. Because of this, "deployments" of smart contracts are free, but later contract executions are more expensive. Conversely, initial deployments in Ethereum are relatively expensive, while later contract executions are cheaper.
These differences in deployment encourage different kinds of smart contracts. A good example is LocalCryptos, which supports non-custodial local trading for both Ethereum and Bitcoin. For Ethereum it uses a big contract that keeps track of all trades, while for Bitcoin it creates individual contracts for every trade.
Writing smart contracts
While Ethereum has multiple high-level languages that compile to EVM bytecode, there is less of a focus on this with Bitcoin. Although the systems that can be built with them are complex, Bitcoin contracts themselves are usually quite simple. So there is less of a need to abstract away the underlying system. And because of Bitcoin contracts' size limits and high costs per added bytes, it is important to keep contracts as small as possible.
While these kinds of high-level languages are less important in Bitcoin, they do exist. The most elaborate high-level language for Bitcoin is Ivy, which was created in 2017 by Dan Robinson. An example smart contract written in Ivy is included below. The contract can be used to send money that can be reclaimed by the sender if the recipient doesn't spend it timely.
contract TransferWithTimeout(
sender: PublicKey,
recipient: PublicKey,
timeout: Time,
val: Value
) {
clause transfer(senderSig: Signature, recipientSig: Signature) {
verify checkSig(sender, senderSig)
verify checkSig(recipient, recipientSig)
unlock val
}
clause timeout(senderSig: Signature) {
verify checkSig(sender, senderSig)
verify after(timeout)
unlock val
}
}
More recently several researchers at Blockstream released Miniscript, which is a language focused on analysis and composability of smart contracts, rather than abstracting away the underlying system. This seems to be the right path, given the fact that Bitcoin contracts tend to lack the complexity that needs more abstraction.
Bitcoin Cash: Combining features
On one hand there is Ethereum, which is able to create many powerful and useful smart contracts that live completely on-chain. At the same time it presents scaling issues due to its stateful nature. On the other hand, Bitcoin's stateless model for smart contracts allows for independent, simple verification of smart contract transactions. But its scripting system limits the usefulness of its contracts.
Bitcoin Cash and Bitcoin share the same history until they forked, so their underlying scripting systems are the same in functionality and Bitcoin Cash benefits from the same upsides. Since then, a part of the Bitcoin Cash community has recognised the demand for more useful smart contracts. Bitcoin Cash has enabled new functionality, making its smart contracts more useful, while keeping the essential properties that allow for Bitcoin's stateless verification.
Upgraded functionality
To understand the possibilities of smart contracts on Bitcoin Cash we need to look at its biannual network upgrades. These network upgrades are performed every year in May and November since the original hard fork in 2017. We specifically discuss the changes to the Bitcoin Script engine, although several other improvements have been made, such as Schnorr signatures.
Early on in Bitcoin, several opcodes were disabled due to issues that made them unsafe to use. Within the first year after the Bitcoin Cash fork, the developers of the Bitcoin-ABC node addressed these issues and reintroduced the opcodes with slightly amended functionality, shown in the image below. Most importantly, this update enables encoding and decoding of structured data within Bitcoin Script.
Half a year after this, another new opcode was released in the network upgrade of November 2018. The update included OP_CHECKDATASIG
, which allows you to verify a signature for any message inside Bitcoin Script. If we combine all scripting updates that were introduced in 2018, these can be used to bring novel and useful smart contract functionality into Bitcoin Cash.
Writing smart contracts
The new functionality of Bitcoin Cash adds significant complexity to its smart contracts over the original Bitcoin Script. This makes it more important to have an ecosystem that provides higher levels of abstraction through high-level languages, SDKs and tooling.
The two big projects working on this right now are Spedn, which was created by the pseudonymous Tendo Pein, and CashScript, which was created by me and is syntactically inspired by Ethereum's Solidity. These tools make it easier to work with smart contracts in Bitcoin Cash, although they are still in development. We use snippets of CashScript code to illustrate functionality in the sections below.
Oracles
When the past updates to Bitcoin Script are combined, they allow you to bring external data into smart contracts on Bitcoin Cash through trusted oracles. Structured data can be encoded into a byte array, and signed by an oracle provider. Then the smart contract can verify the signature and decode the structured data.
An example of this can be inspected in the HODL Vault example contract below. This contract enforces HODLing until a certain BCH/USD price has been reached. The required BCH/USD price feed is published by an oracle provider and passed into the contract by the user. To increase decentralisation, a smart contract can be set up to use several data sources, rather than trusting a single centralised service.
pragma cashscript ^0.2.0;
contract HodlVault(pubkey ownerPk, pubkey oraclePk, int minBlock, int priceTarget) {
function spend(sig ownerSig, datasig oracleSig, bytes oracleMessage) {
// Decode message: { blockheight, price }
int blockHeight = int(oracleMessage.split(4)[0]);
int price = int(oracleMessage.split(4)[1]);
// Check that message's blockHeight is after minBlock and not in the future
require(blockHeight >= minBlock);
require(tx.time >= blockHeight);
// Check that current price is at least priceTarget
require(price >= priceTarget);
// Handle necessary signature checks
require(checkDataSig(oracleSig, oracleMessage, oraclePk));
require(checkSig(ownerSig, ownerPk));
}
}
Covenants
The second big use case is a technique called covenants, which derives its name from a term used in property law to restrict an object's use. In the case of Bitcoin Cash, it restricts the use of money in a smart contract. So while smart contracts in Bitcoin can only authorise the general spending of money, Bitcoin Cash contracts are able to put constraints on the amount of money that can be spent or who the recipients can be, among other constraints.
When transferring Bitcoin, the sender has to provide a signature to authorise the transaction. To generate this signature, the sender signs a hash representation of the transaction. This hash is called the sighash, while the actual transaction data is contained in the sighash preimage.
By using OP_CHECKSIG
and OP_CHECKDATASIG
with the same signature, we can gain access to the sighash data. The data format can be inspected in the specification, and is included in the image below. An important field is scriptCode
, which contains the bytecode of the smart contract itself. Another is hashOutputs
that allows you to enforce the outputs of a transaction.
While it is good to know how covenants work on a technical level, CashScript has abstracted away most of the complexity associated with covenants. When writing smart contracts with CashScript these fields are readily available without going through the steps to verify and decode the sighash preimage manually.
The first smart contract to use covenants was Licho's Last Will, a smart contract that allows you to put a dead man's switch on your holdings. The contract defines three different functions. The first allows an inheritor to claim the funds after 180 days. The second allows the owner's cold key to spend the money in any way. The third allows the owner's hot key to refresh the 180 day duration by enforcing that the full contract balance is sent back to the contract.
A CashScript version of Last Will is included below, but the original version was written with Spedn and can be inspected here.
pragma cashscript ^0.3.0;
contract LastWill(bytes20 inheritor, bytes20 cold, bytes20 hot) {
function inherit(pubkey pk, sig s) {
require(tx.age >= 180 days);
require(hash160(pk) == inheritor);
require(checkSig(s, pk));
}
function cold(pubkey pk, sig s) {
require(hash160(pk) == cold);
require(checkSig(s, pk));
}
function refresh(pubkey pk, sig s) {
require(hash160(pk) == hot);
require(checkSig(s, pk));
// Construct output
int minerFee = 1000;
bytes8 amount = bytes8(int(bytes(tx.value)) - minerFee);
bytes32 output = new OutputP2SH(amount, hash160(tx.bytecode)
// Check that output matches preimage
require(hash256(output) == tx.hashOutputs);
}
}
While this is one of the simpler examples of a covenant contract, it's possible to create much more interesting covenants. People have used covenants to create Mecenas, a recurring payment system, and Be.cash, a system for payer-offline payment cards. More recently we have even seen AnyHedge, a decentralised derivatives platform. So while Bitcoin Cash uses a different paradigm than Ethereum, it can support complex DeFi projects.
Covenants were first proposed in a paper titled Bitcoin Covenants, which required a new opcode, OP_CHECKOUTPUTVERIFY
. Other proposals followed, such as BIP-119 or OP_CHECKSIGFROMSTACK
. The latter is very similar to Bitcoin Cash's implementation of covenants. More practical information on covenants can be found in Tendo Pein's article on read.cash.
Simulated state
For the Last Will contract it can be valuable to change the inheritor when the contract is already in place. Since we can enforce sending to the current contract by looking at its bytecode, we can enforce sending to a slightly different contract by slightly changing this bytecode. Doing so, we create a function that sends the entire contract's balance to a contract with the exact same bytecode, but with a different inheritor.
We can only do this with constructor parameters of a known size (e.g. 20 bytes). The constructor parameters are always the first data of the contract's bytecode, which is how we are able to easily replace the old data with new data. The inheritor
is the first constructor parameter and it has a size of 20 bytes, so we are able to apply this technique, as can be seen below.
function changeInheritor(pubkey pk, sig s, bytes20 newInheritor) {
require(hash160(pk) == hot);
require(checkSig(s, pk));
// Cut out old inheritor (PUSH1 0x14 <inheritor>)
// Insert new inheritor (PUSH1 0x14 <newInheritor>)
bytes newContract = 0x4c14 + newInheritor + tx.bytecode.split(22)[1];
// Construct output
int minerFee = 1000;
bytes8 amount = bytes8(int(bytes(tx.value)) - minerFee);
bytes32 output = new OutputP2SH(amount, hash160(newContract));
// Check that output matches preimage output
require(hash256(output) == tx.hashOutputs);
}
By using this technique, it's possible to change some variables in a contract while keeping the same rules of the contract. We refer to this as "simulating" state, since it offers some of the benefits of contract state, without some of the drawbacks of a stateful system. There is always a trade-off, so this method has different drawbacks.
The main issue is that we're not actually changing any variables in the original contract, since this is impossible due to Bitcoin Cash's statelessness. Instead, a new contract is created (with a new address) and the full balance can be transferred to this new contract. This causes UX problems due to the new address, but these can be mitigated by having a good application layer abstraction over these smart contracts.
Replacing these variables inside the bytecode can feel quite hacky, especially when trying to replace variables that are deeper inside the bytecode. This functionality should be abstracted away in high-level languages, so that the new bytecode is automatically generated by providing the new parameters. That kind of abstraction could look like this:
bytes32 output = new OutputP2SH(amount, hash160(new LastWill(newInheritor, cold, hot)));
Many abstractions come at the cost of larger contracts, which results in higher fees. Because transaction fees are generally quite low on Bitcoin Cash, this is not a deterrent. But Bitcoin Cash has the same size limits as Bitcoin, so these abstractions do make it more difficult to stay within the limits. This results in many developers having to hand-optimise the compiled bytecode.
Conclusions
Every cryptocurrency has trade-offs in its smart contract systems. Ethereum is the most used smart contract platform with the most extensive functionality. Bitcoin offers a more rudimentary version of smart contracts through its stateless scripting system. This stateless system is more efficient to validate, but offers less functionality. Bitcoin Cash builds on the same foundations as Bitcoin, but has added new functionality. This attempts to achieve a compromise between efficient validation and useful smart contracts. In the end, all are building towards similar goals.
Looking into the future
This article is written in Q4 of 2019, and reflects the current state of the platforms. Cryptocurrency is a fast-paced field, so the people involved with these platforms are always working on improvements that may change the outlook of the ecosystem.
Ethereum is working on a major Ethereum 2.0 release, with its phase 0 planned to go live in Q1 2020. Ethereum 2.0 aims to address the performance of Ethereum 1.x by moving to Proof-of-Stake, implementing sharding, and other improvements. The full roadmap for ETH2.0 is not set in stone yet, and it will likely take several years to roll out. But if this major update is able to to achieve its goals, this can present a strong case for Ethereum.
Smart contract research in Bitcoin is focused on further improving efficiency and privacy of smart contracts. This includes solutions like Taproot and scriptless scripts. At the same time Bitcoin Cash has enabled additional smart contract functionality and is focused on making these changes more accessible.
Disclaimer: I am the author of CashScript, one of the projects working on Bitcoin Cash smart contracts. I have also worked on and contributed to open source Ethereum projects, and currently maintain several Ethereum libraries. The goal of this article is not to advocate for any of the discussed platforms or technologies, but rather to provide a comprehensive comparison for anyone interested in working with smart contracts on Ethereum, Bitcoin and Bitcoin Cash.