A Step-by-Step Guide to Developing Bitcoin Smart Contracts


The first step in building any smart contract is a high-level design. Here we choose to implement a common transaction type Pay To Public Key Hash (P2PKH) in the Bitcoin network. There are two main reasons to use this type of transaction as an example:

  • P2PKH is the most popular type of transactions in the bitcoin network used to sending bitcoins from one to another, which is necessary for beginners to understand
  • By implementing this classic transaction type, one can more intuitively understand the capabilities and usage of Script/sCrypt

What’s P2PKH?

It’s locking script is:

<Signature> <Public Key>

Receiving of P2PKH

If Alice wants to transfer bitcoins to Bob, Bob first needs to tell Alice his public key hash value ( usually known as the bitcoin address, which is similar to a bank account number). Alice uses this value to construct a P2PKH locking script (here denoted as LS¹) and sends the transaction to the miner. After the miner verifies that it is correct, the transaction is recorded on chain.

Spending of P2PKH

When Bob wants to spend the bitcoins received, he needs to provide two pieces of information to construct a unlocking script:

  • The original public key that the above public key hash value is calculated from;
  • A Signature calculated by using the private key corresponding to the original public key.

Verification of P2PKH

When miners receive the new transaction, they need to validate it, which involves two main steps:

  • Concatenate the unlocking script with the locking script to form the following verification Script:
<Signature> <Public Key> OP_DUP OP_HASH160 <Public Key Hash> OP_EQUALVERIFY OP_CHECKSIG
  • Use the Bitcoin Virtual Machine to execute this verification Script to check whether the execution result is valid. There are two critical checks in the verification process:
  1. Verify that the hash, which can be calculate by the public key information provided in the unlocking script, is equal to the hash provided in the locking script. If it passes, the public key is indeed the recipient address of the previous transaction. (It is equivalent to verifying that the receiving address of the previous transfer is Bob’s bank account number)
  2. Verify that the signature provided in the unlocking script matches the public key information. If it passes, it means that Alice does have control over the private key corresponding to this public key.


With this design goal in mind, we can get started. Firstly, we install the sCrypt extension in VS Code.

git clone git@github.com:sCrypt-Inc/boilerplate.git
Contract DemoP2PKH
  • A contract variable pubKeyHash of type Ripemd160, corresponding to the previous P2PKH locking script <Public Key Hash> ;
  • Constructorconstructor, used to initialize the contract variable;
  • A custom public function named unlock which has two parameter with type Sig and PubKey, corresponding to the previous P2PKH unlocking script<Signature> <Public Key>.The implementation logic also corresponds to the P2PKH validation described earlier.

Unit testing

With the code, the next step is to verify that the implementation is correct, and the normal way to do this is to add some unit tests. The test file for the above contract is tests/js/p2pkh.scrypttest.js, the code is as follows:

  • First Import sCrypt’s Javascript/Typescript library scrypttest:
const { buildContractClass, bsv } = require('scrypttest');
  • Use the function buildContractClass to get the class object reflected into Javascript of the contract DemoP2PKH:
const DemoP2PKH = buildContractClass(path.join(__dirname, '../../contracts/p2pkh.scrypt'), tx, inputIndex, inputSatoshis)
  • Use arguments (that is, the hex format of the public key hash) to instantiate the contract:
demo = new DemoP2PKH(toHex(pkh))
  • Test the public method of the contract instance and expect it to succeed:
sig = signTx(tx, privateKey, demo.getLockingScript())
expect(demo.unlock(toHex(sig), toHex(publicKey))).to.equal(true);
  • Expect it to fail if the signature cannot be verified because the wrong private key is used:
sig = signTx(tx, privateKey2, demo.getLockingScript())
expect(demo.unlock(toHex(sig), toHex(publicKey))).to.equal(false);


Having only the above unit test is not enough, because when the test fails, we can hardly figure out why and there is no more information to help us pinpoint the problem. Enter the sCrypt Debugger.

  • program: the contract file for this debug configuration.
  • constructorParams: the contract’s constructor argument list, separated by comma.
  • entryMethod: specifies the public function name to be debugged.
  • entryMethodParams: specify the actual argument list of the public function to be debugged, also separated by comma.
  • txContext: specifies context information about the current transaction at debugging time, where:
  1. txContext.hex: the hex format of the transaction, can be signed or unsigned.
  2. txContext.inputIndex: the input index corresponding to the contract-locked UTXO to be spent.
  3. txContext.inputSatoshis: The amount of bitcoins corresponding to the contract-locked UTXO to be spent, in unit of satoshi.

Deploy and Call the Contract

Before using a contract in a production environment, a developer should test on testnet to ensure that the contract code meets expectations. For the example in this article, you can run the command node tests/js/p2pkh.js in the project root directory.


When we first run the file, we see output like this:

New privKey generated for testnet: cMtFUvwk43MwBoWs15fU15jWmQEk27yJJjEkWotmPjHHRuXU9qGq
With address: moJnB7AND5TW8suRmdHPbY6knpfE1uJ15n
You could fund the address on testnet & use the privKey to complete the test
  1. A private key on the test network is required;
  2. The private key has sufficient balance in the corresponding address for testing.
const privKey = ''

Expected Result

With the aforementioned setup in place, you are ready to run the command again. Normally you would see the following output:

Contract Deployed Successfully! TxId: bc929f1dddc6652896c7c162314e2651fbcd26495bd1ccf9568219e22fea2fb8
Contract Method Called Successfully! TxId: ce2dba497065d33c1e07bf710ad94e9600c6413e053b4abec2bd8562aea3dc20

Contract Deployment

  1. Create a new locking transaction:
const amountInContract = 10000
const deployTx = await createLockingTx(privateKey.toAddress(), amountInContract)
const deployTxId = await sendTx(deployTx)

Contract Call

  1. Create a new unlocking transaction:
const spendAmount = amountInContract / 10
const methodCallTx = createUnlockingTx(deployTxId, amountInContract, p2pkh.lockingScript.toASM(), spendAmount)
const sig = signTx(methodCallTx, privateKey, p2pkh.lockingScript.toASM(), amountInContract)
const unlockingScript = p2pkh.unlock(new Sig(toHex(sig)), new PubKey(toHex(publicKey))).toScript()
const methodCallTxId = await sendTx(methodCallTx)


We walk through an example of developing a smart contract on Bitcoin, which serves a starting point for developing more.



Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store


sCrypt (https://scrypt.io) is a company with a mission to provide integrated on-chain smart contracting solutions on Bitcoin SV