đ Bitcoin: Smart Contracts
After my last kerfuffle with Bitcoin, Iâve been hesitant to get involved again. Along with watching the technical progress of Bitcoin over the last 7 years, its been very disappointing. If anything itâs been going backwards.
This post follows my path of explorations into smart contracts as I learn more about them - loosely following the direction I took, with links to the posts that I gained insights from.
I must say that I didnât really ever know what Bitcoin (per the whitepaper) could actually do. I also think that pretty much everyone else promoting alt coins currently have no idea what is currently possible either.
The main disappointment was the disabling of a bunch of the op_codes that are used in the Bitcoin scripting language, and the focus on small blocks. It made no sense.
Now that Bitcoin script has been unfucked (Bitcoin SV), we can now create smart contracts using the scripting language as originally intended by Satoshi.
- What the hell are bitcoin scripts?
- Scripts
- Scrypts
What the hell are bitcoin scripts?
Itâs taken me far to long to wrap my head around how bitcoin âworksâ. For the longest time I had a rough idea, reading the wiki, and a few blog posts, but Iâd never taken the time to actually dig inâŚ
A very high level overview of Bitcoin: Bitcoin is essentially a very large database that stores Transactions and Blocks. Transactions are grouped into Blocks. Blocks are linked together similar to a linked list. (Hence the potentially familiar term Block ChainâŚ)
A Transaction is made up of Inputs and Outputs, essentially forming a graph where each transaction is a node. The relationships between a Transactionsâ Outputs and Inputs are validated by a predicate; a basic script that evaluates to TRUE or FALSE.
Itâs this evaluation of each Input script that determines whether the transaction is valid or not (not quite⌠but good enough for now), and itâs how the system knows that you have the permission to use/move/spend the satoshis.
Scripts are written in a Bitcoin scripting language. Itâs a stack based language similar to Forth. Iâve not used Forth before, but I was able to understand how simple scripts evaluate. Iâd recommend reading a little about the scripting language here.
There are two sides to each script. We have the Locking Script and the Unlocking Script.
Locking Scripts
The Locking Script can be thought of as the âdestinationâ of an Output. You send Bitcoin by âlocking them upâ in a Locking Script. This is what we call a Unspent Transaction Output (UTXO).
Unlocking Scripts
In order to send Bitcoin somewhere you first must provide access to the UTXO, and provide the Unlocking Script that satisfies the locking script that results TRUE after executing.
A good thing to note would be that in the past, the terminology for Locking Script and Unlocking script was different. They were called ScriptPubKey
and ScriptSig
respectively. When you see ScriptPubKey
think Locking Script, and when you see ScriptSig
think Unlocking Script.
Letâs look at an example at a well known Locking & Unlocking script.
Pay-to-Public-Key-Hash (P2PKH)
If youâve ever made a bitcoin transaction to send bitcoin to an address itâs very likely that you created a Pay-to-Public-Key-Hash (P2PKH) script. This is one of the âstandardâ transactions that youâll find in Bitcoin.
The locking script for P2PKH looks like this
OP_DUP OP_HASH160 <Your Public Key Hash> OP_EQUAL OP_CHECKSIG
And the unlocking script
<Your Signature> <Your Public Key>
To validate, the locking script and the unlocking script are combined and then evaluated.
Unlocking Script + Locking Script == true
The script must complete with true
in order to be validated.
In the case of P2PKH, the entire script will look like this
<Your Signature> <Your Public Key> OP_DUP OP_HASH160 <Your Public Key Hash> OP_EQUAL OP_CHECKSIG
and if youâve used the correct keys, then the UTXO will be validated in the transaction!
Now letâs look at an example where weâve wrote our own Locking & Unlocking script.
Scripts
There are many different types of scripts one can use. As long as the combination of Locking Script and Unlocking script that results in a true
value, it doesnât matter what the script is.
This fact opens up a lot of possibilities for what you can do with Bitcoin scripts and is where the idea of âsmart contractsâ come in. In Bitcoin, a smart contract is simply a Locking Script and its corresponding Unlocking Script.
Take this Locking Script:
OP_5 OP_EQUAL
This Locking Script (above) isnât secure at all. Itâs trivial for anyone to spend this UTXO by writing the correct Unlocking Script which would simply be
OP_5
We can see why this is valid after combining the Locking Script and Unlocking Scripts together.
OP_5 OP_5 OP_EQUAL
This pushes two numbers (5) to the stack and then verifies that they are equal. This evaluated to true causing the script to be valid. If we used a different number in the Unlocking Script, the combined script would not evaluate, causing the transaction to be invalid. (If this is lost on you, again I recommend reading a little more about how Bitcoin Script works on the wiki.)
Iâve created two transactions on the Bitcoin Test Network that demonstrate this Locking Script and Unlocking Script.
Here is the transaction that locks 10,000 satoshis into the Locking Script
Here is the transaction that unlocks the 10,000 satoshis and sends it back to our address
The code that was used to create these transactions is available here for reference.
sCrypt
Writing scripts using the OP Codes as we have done above can only get you so far. Xiaohui Liu has created the sCrypt language. From the docs:
sCrypt (pronounced âess cryptâ) is a high-level smart contract language for Bitcoin SV. Bitcoin supports smart contract with its Forth-like stack based Script language. However writing smart contract in native Script is cumbersome and error-prone. It quickly becomes intractable when the contract size and complexity grow.
If youâve used Solidity on Ethereum, then you have a slight advantage as the syntax is similar to writing Solidity scripts.
We can write smart contracts in sCrypt that are then compiled down to the bitcoin OP Codes in order to make more advanced scripts in a higher level language. Letâs take a look.
Re-writing our EqualsFive script in sCrypt
To write the above contract in sCrypt, it would look something like this:
contract EqualsFive {
public function isFive(int x) {
require(x == 5);
}
}
This is a very simple contract that can be unlocked if the value we provide is equal to 5
. While this isnât a very useful contract, itâs enlightening to see what it would look like in sCrypt.
// Compile the contract
const EqualsFive = buildContractClass(loadDesc('equalsFive_desc.json'));
equalsFive = new EqualsFive();
// Generate the locking script
const lockingScript = equalsFive.lockingScript
// Generate the Unlocking Script
const unlockingScript = equalsFive.isFive(5).toScript()
Which results in the compiled Locking Script:
OP_0 OP_PICK OP_5 OP_NUMEQUAL OP_NIP
This locking script is slightly different to the one we wrote by hand due to the way sCrypt has compiled the EqualsFive contract. It looks different, but works the exact same way.
And the compiled Unlocking Script:
OP_5
Which when executed:
OP_5 OP_0 OP_PICK OP_5 OP_NUMEQUAL OP_NIP
Just like our hand written script, when this is executed, it returns true
, thus being validated.
From here on out, the contracts start to get a little more involved.
OP_PUSH_TX
I know what youâre thinking, thatâs all good and well, but there is no way that you can use any âstateâ in these scripts.
That would be a wrong stance to take. You certainly can use state in Bitcoin Scripts. Thereâs a little known method that allows us to do just that.
We can make use of a pseudo operation dubbed OP_PUSH_TX
which allows us to access the âcurrentâ transaction. The current transaction being the transaction in which is being validated. This is a mind bending concept - we put the current transaction inside the unlocking script of the current transaction. How does that work!?
I severely recommend reading the post that introduces this idea a couple of times. It took me a while to understand what was happening, and how we can even do so!
With that in mind, and the myth busted, we can now accept that we have the ability to read and write state in Bitcoin Transactions.
Counter Example
To demonstrate these concepts, sCrypt comes with a Counter contract that stores the current number of times the contract has been executed. Again, Iâm going to defer you to the short sCrypt post that describes this contract here. Take a minute to read that, and then come back. Iâll wait!
This contract places the following requirements on the outputs:
- the locking script code must be equal to what it was before
- the counter value stored in an integer after the locking code must increment by one
contract Counter {
public function increment(SigHashPreimage txPreimage, int amount) {
require(Tx.checkPreimage(txPreimage));
bytes scriptCode = Util.scriptCode(txPreimage);
int scriptLen = len(scriptCode);
// state (i.e., counter value) is at the end
int counter = unpack(scriptCode[scriptLen - Util.DataLen :]);
// increment counter
bytes scriptCode_ = scriptCode[: scriptLen - Util.DataLen] + num2bin(counter + 1, Util.DataLen);
bytes output = Util.buildOutput(scriptCode_, amount);
// ensure output is expected: amount is same with specified
// also output script is the same with scriptCode except counter incremented
require(hash256(output) == Util.hashOutputs(txPreimage));
}
}
Please read the post that explains this far better than I can here.
Advanced Counter Example
Taking the Counter example further..
In the example Counter Contract above, the transaction fee is paid by the Contract itself, limiting its life to the lengths the initial value can cover the transaction costs. How can we make sure that this Contract lives forever?
The Advanced Counter example shows how you can make changes to the Contract so that you can add additional inputs and outputs into the transaction so that the caller has to pay for the transaction fee, and still validate the transaction in script.
It also has a requirement that the balance inside the Contract stays the same, causing the satoshi value locked inside this contract to stay there forever.
Once deployed, Contracts like this one are final because it contains a requirement that the Locking Script is the same as before. To enable you to retrieve the funds, you have to add a function in the Contract before deploying it. This is why we have the testnet!
Again, please read this post that explains the Advanced Counter Example.
A demo scrypt
Iâm using Scrypt. Scrypt is a high-level smart contract language (similar to ethereumâs Solidity) that compiles down to the Bitcoin assembly code.
There is a demo contract in the sCrypt project.
contract Demo {
int x;
int y;
constructor(int x, int y) {
this.x = x;
this.y = y;
}
function sum(int a, int b): int {
return a + b;
}
public function add(int z) {
require(z == this.sum(this.x, this.y));
}
public function sub(int z) {
require(z == this.x - this.y);
}
}
This demo scrypt will compile down to a Locking Script and Unlocking Scripts. The Locking Script is derrived from the constructor, and the Unlocking Scripts are derrived from the public functions.
This demo script is essentially a logic puzzle. Youâd be able to reverse engineer the compiled Locking Script (looking at the op codes) and figure out how to create a valid unlocking script that would make a valid transaction.
To create a transaction that uses this demo scrypt, weâd use the scryptlib
javascript library to compiled the script. We then create an output with the compiled Locking Script and broadcast the transaction.
To unlock the UTXO we just made, we create another transaction using the UTXO and the Demo Contracts Unlocking script.
const amount = 2000
const newAmount = 546
// Compile the contract
const Demo = buildContractClass(loadDesc('demo_desc.json'));
demo = new Demo(4, 7);
// Lock funds into the script.
const lockingTx = await createLockingTx(privateKey.toAddress(), amount)
lockingTx.outputs[0].setScript(demo.lockingScript)
lockingTx.sign(privateKey)
const lockingTxid = await sendTx(lockingTx) // Broadcast the transaction
// Unlock the funds we just locked in using the Contracts Unlocking Script
const unlockingScript = demo.add(11).toScript()
const unlockingTx = await createUnlockingTx(lockingTxid, amount, demo.lockingScript.toASM(), newAmount)
unlockingTx.inputs[0].setScript(unlockingScript)
const unlockingTxid = await sendTx(unlockingTx)
Learning from existing example scripts
- demo.scrypt
- Introduces the concept of scripts. Locking script & unlocking scripts. Itâs taken me longer than I want to admit to wrap my head around how these fit together, and where theyâre located in a transaction.
- counter.scrypt
- Introduces the concept of stateful scripts using the
preimage
of a staged transaction. This allows the unlocking script to access the transaction that is spending the input (locking script).
- Introduces the concept of stateful scripts using the
- advancedCounter.scrypt
- Introduces the concept of paying the transaction fee from another input. Expecting one change output, and verifies that the amount in the counter is the same going forward, that the output script is the same, but with an incremented counter value.
Sentences that helped wrap my head around how contracts work on bitcoin. I stared at these for a while.
- You lock funds into a scripthash (locking script)
- You unlock funds by providing the unlocking script (contract function) to the outpoint in the spending transactions. (The scriptsig)
- The contract is simply the unlocking script of a previous transactions outpoint.
There are two parts to the script that is evaluated when validating a Transaction. When you âsendâ bitcoin, youâre unlocking the satoshis using an Unlocking Script (source), and locking them up in a Locking Script (destination). The locking script is associated with an output in a transaction. Its job is to set the conditions in which the output can be used in a future transaction. The unlocking script should satisfy the outputsâ locking script. When spending an UTXO, you must provide the unlocking script for the transaction to be valid.
Each output has a script (locking script) associated with it. This script prevents just anyone being able to use the unspent output (UTXO) in a later transaction. This locking script is then unlocked by the unlocking script provided when spending the UTXO in a later transaction.
Go forth and conquer!
If youâre a smart contract developer that has wrote solidity contracts for Ethereum, give Bitcoin another go now that itâs been reverted to the original implementation (Bitcoin SV). You never know; you might be surprised with what you can do.
Scrypt: High level smart contract language (like solidity)
Bonus: Also, get ready for a token explosion powered by Bitcoin SV. Matterpool have been working on a Non-Fungible-Token smart contract using native bitcoin script. Check that out here https://www.npmjs.com/package/@matterpool/superasset-js.