Thanks to Chain’s new Ivy language for Bitcoin, we’re now able to build smart contracts in Bitcoin with ease - even if it is still experimental. This post covers smart contracts in Bitcoin, Ivy, plus full code examples, including a Mutually Assured Destruction (MAD) Escrow transaction.
If you’ve been following our series on Ethereum smart contracts, you’ll know that they’re an exciting and clever use of blockchains in action. Now, just like Ethereum, Bitcoin also allows us to write smart contracts… and that’s where the similarities end. Bitcoin, unlike Ethereum, uses a lower-lower language (similar to Assembly) called Script and is not Turing complete.
When you send BTCs (Bitcoins) to the contract’s address, they are locked in a “safety box” until the contract is satisfied and then the BTCs are spent. The program itself cannot control the flow of value or maintain any kind of persistent state. In fact, a better name for this would be a smart wallet, since it is nothing more than an additional condition that allows us to “open” the wallet for transactions.
Recently Chain released the Ivy compiler and developer environment for writing Bitcoin smart contracts using a higher level language that compiles to Bitcoin Script. This is good news for everyone - there are far fewer folks out there that can write well in low level languages!
Keep in mind that Ivy is prototype software and is intended for educational purposes only for now. However, since it’s much more developer friendly, we predict that it is going to become more popular in the near future.
HelloWorld in Ivy
Ivy ships with a nice playground that can be used to write and test smart contracts without releasing the contract to the real network and spending real Bitcoins. Ivy is also available as a Node.js SDK (`npm install ivy-bitcoin`) but since it does not support creating real transactions on either the testnet or mainnet (yet!) we can stick to using the `playground` for the purposes of the article.
To start writing programs open https://ivy-lang.org/bitcoin in your browser.
To prove our statement about it being like a smart wallet, let’s examine the simplest possible contract:
contract LockWithPublicKey(publicKey: PublicKey, val: Value) {
clause spend(sig: Signature) {
verify checkSig(publicKey, sig)
unlock val
}
}
If you check the code and think about it for a moment, it’s nothing more than a Bitcoin address for the wallet.
Let’s start with simple wallet that freeze your assets until a specific date - think of it as a safety box with a timer.
Here is the code of the contract:
contract LockUntilTimestamp(publicKey: PublicKey, val: Value, timestamp: Time) {
clause spend(sig: Signature) {
verify checkSig(publicKey, sig)
verify after(timestamp)
unlock val
}
}
It basically uses the `checkSig` and `after` functions (here is a list of all available functions for Ivy) to verify the conditions for the contract’s `spend` function. `publicKey`, `val` and `timestamp` conditions for the contract are provided in the constructor. `checkSig` checks the signature generated by the private key corresponding to the input publicKey.
The Ivy playground provides a nice form to specify initial contract arguments:
Once you `Save` the template and `Create` a contract you will be able to simulate execution with different parameters:
Pay attention to the signature, that will change depending on the `Transaction Details` parameters. Also the `Unlock` button will automatically enable or disable depending on the inputs, which allows you to quickly test your contracts.
It also displays a transaction history:
Even though `clause` looks like an arbitrary function, it is not. Keywords such as this are are strictly defined in Bitcoin smart contracts glossary.
#102 example
Let’s move onto something more advanced. Let’s try to implement MAD (Mutually Assured Destruction) escrow where the seller deposits some amount of the item’s value (it might be even 100% of the value) and the buyer, when placing the order, needs to match the seller’s deposit plus the item’s price. This way transaction can be more secure against fraud:
- if seller does not ship the item and buyer does not confirm the transaction, seller might loose the deposit (in the case of 100% of the item’s value in the deposit, he basically loses the item)
- if buyer does not confirm the transaction, he loses the deposit as well (in case of 100% item’s value in deposit he basically payed twice for the item)
To implement MAD escrow we need to follow the following algorithm (Bart is seller, Homer is buyer, where product cost is 0.05 BTC):
1. Bart and Homer create additional addresses
2. Bart deposits 0.05 BTC on his new address
3. Homer deposits 0.05 BTC on his new address
4. Bart creates a multi-sig wallet with Homer’s public key and sends redeem script to Homer
5. Bart starts transaction with his and Homer’s inputs (addresses from step 1) and multi-sig address (from step 4) as output with 0.1 BTC. Bart signs transaction with his private key and send it to Homer
6. Homer verifies and signs partially signed transaction (from step 5) then broadcasts it to the network
Steps 5-6 ensures neither side runs away after transferring the deposit by the other side.
Now Homer and Bart can exchange a product for the money (Homer might send money to Bart’s private address, Bart sends product to Homer) since the transaction is secured by both sides’ deposits.
Since Ivy is not production ready, we can only show how a multi-sig wallet might look like with Ivy:
contract MadEscrow(
seller: PublicKey,
buyer: PublicKey,
value: Value
) {
clause transfer(sellerSig: Signature, buyerSig: Signature) {
verify checkMultiSig(
[seller, buyer],
[sellerSig, buyerSig]
)
unlock val
}
}
We will use Node.js with the `bitcore` lib to show implementation of step 5 and 6 (previous steps may be easily done via any client app):
// bart_deposit.js
var Insight = require("bitcore-explorers").Insight;
delete global._bitcore;
var bitcore = require("bitcore-lib");
var insight = new Insight("testnet");
var bartPrivKey = 'BART_PRIVATE_KEY';
var bartAddress = 'BART_ADDRESS';
var homerAddress = 'HOMER_ADDRESS';
var escrowAddress = 'ESCROW_ADDRESS';
insight.getUnspentUtxos(bartAddress, function(error, utxos1) {
insight.getUnspentUtxos(homerAddress, function(error, utxos2) {
var tx = new bitcore.Transaction();
tx.from([utxos1, utxos2])
tx.to(escrowAddress, 10000000);
tx.change(bartAddress);
tx.sign(bartPrivKey);
var serialized = tx.toObject();
console.log('Fully signed:', tx.isFullySigned());
console.log(JSON.stringify(serialized));
});
});
// homer_deposit.js
var Insight = require("bitcore-explorers").Insight;
delete global._bitcore;
var bitcore = require("bitcore-lib");
var insight = new Insight("testnet");
var transaction = JSON.parse('OUTPUT_FROM_bart_deposit.js');
var tx = new bitcore.Transaction(transaction);
var homerPrivKey = 'HOMER_PRIVATE_KEY';
tx.sign(homerPrivKey);
console.log('Fully signed:', tx.isFullySigned());
insight.broadcast(tx.serialize(), function(error, transactionId) {
if (error) {
console.log(error);
} else {
console.log(transactionId);
}
});
We need to delete `global._bitcore` before importing `bitcore-lib` to avoid version conflicts during import (there is an incompatibility issue with `bitcore-explorers` and `bitcore-lib` at the time of writing the article).
First we run `bart_deposit.js` which gives us a serialized transaction that is placed in `homer_deposit.js`. After verifying the transaction by Homer, he broadcasts it to the network so both transactions can be done within the single blockchain.
At this moment both parties have transferred a deposit to the multi-sig escrow address and can make the transaction. After a successful transaction they can then withdraw the deposit from the escrow account.
This time we start with Homer, as he is the buyer so he is the first one who should confirm a successful transaction.
Here are how scripts might look like. We intentionally say “might look” like because at the moment it looks like there is a bug in bitcore-lib which prevents us from running this code. However you can achieve the same effect by performing these actions manually on Coinb.in web client.
// homer_withdraw.js
var Insight = require("bitcore-explorers").Insight;
delete global._bitcore;
var bitcore = require("bitcore-lib");
var insight = new Insight("testnet");
var homerPrivKey = 'HOMER_PRIVATE_KEY';
var homerAddress = 'HOMER_ADDRESS';
var bartAddress = 'BART_ADDRESS';
var bartPubKey = 'BART_PUBLIC_KEY';
var homerPubKey = 'HOMER_PUBLIC_KEY';
var escrowAddress = 'ESCROW_ADDRESS';
insight.getUnspentUtxos(escrowAddress, function(error, utxos) {
var tx = new bitcore.Transaction();
tx.from(utxos, [bartPubKey, homerPubKey], 2);
tx.to(homerAddress, 9800000);
tx.to(bartAddress, 9800000);
tx.sign(homerPrivKey);
var serialized = tx.toObject();
console.log('Fully signed:', tx.isFullySigned());
console.log(JSON.stringify(serialized));
});
// barth_withdraw.js
var Insight = require("bitcore-explorers").Insight;
delete global._bitcore;
var bitcore = require("bitcore-lib");
var insight = new Insight("testnet");
bartPrivKey = 'BART_PRIVATE_KEY';
var transaction = JSON.parse('OUTPUT_FROM_homer_withdraw.js');
var tx = new bitcore.Transaction(transaction);
tx.sign(bartPrivKey);
console.log('Fully signed:', tx.isFullySigned());
insight.broadcast(tx.serialize(), function(error, transactionId) {
if (error) {
console.log(error);
} else {
console.log(transactionId);
}
});
Keep in mind that in all transactions here we provide a price in Satoshis (100,000,000 Satoshi = 1 BTC). We also have to include small transaction fees (from and to escrow address) - that’s why we transfer 9,800,000 instead of 10,000,000 Satoshis.
We believe that smart contracts are the future of doing business. Which blockchain are you going to use to help? Bitcoin? Ethereum? We can help you to build smart contracts that will help your business get ahead - or teach your team how to build smart contracts themselves. Contact iRonin for more information on smart contracts, back-end development, or any other cutting edge software technology projects.