Hodling bitcoins with OP_CHECKLOCKTIMEVERIFY — A step-by-step guide to manually building a bitcoin transaction with OP_CLTV

OP_CHECKLOCKTIMEVERIFY, aka “hodl”, was proposed, and finalized, by BIP65. Its basic functionality is to lock an output’s funds until a specified time (as a timestamp, or block height). While the concept is popular, there doesn’t seem to be an easy option to create these type of transactions.

In this guide, I was able to successfully manually build a transaction that sends coins to a locked output. I used Bitcoin-S, an open-sourced Scala implementation of the Bitcoin protocol, to create the transactions, and Bitcoin-Core’s RPC daemon on bitcoin’s test network (“testnet”). I should also note that we are building bitcoin transactions from scratch, which can be quite exhaustive and difficult. Even the simplest mistake can result in lost coins. Seems like an appropriate place for a disclaimer: this guide was constructed on testnet, and is strictly for educational purposes. It is strongly recommended to not manually build transactions on mainnet as there is too much room for human error. Please know that if you attempt this on mainnet, you are doing so at your own risk.

Bitcoin transactions are constructed of inputs and outputs, right? Well, sort of. There’s a lot more that goes into building these inputs and outputs. There are outpoints, scriptpubkeys, scriptsigs, versions, sequences, etc etc. With the overwhelming amount of data fields we need to fill, it can get messy very quickly. This is why it is strongly recommended to never manually build transactions on mainnet.

If you’d like to follow along, there are two basic requirements for this guide:

  • Bitcoin-Core (not QT/wallet — we don’t need the entire blockchain, just the daemon. This only takes a few minutes to build from source.)
  • Bitcoin-S-Core (including its dependencies)

First, let’s open a terminal, go to the directory where you installed Bitcoin, and start the Bitcoin-Core daemon:

bitcoind -testnet -daemon

If you don’t have any testnet coins, find a faucet.

Open a second terminal (best to split screen the two terminals), go to your Bitcoin-S-Core directory (install guide), and initialize an SBT console:

tom@tom:~/dev/bitcoin-s-core$ sbt console
...console initiation text...
---Some necessary imports needed---
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.crypto._
import org.bitcoins.core.script.crypto._
import org.bitcoins.core.script._
import org.bitcoins.core.number._
import org.bitcoins.core.config._
import org.bitcoins.core.protocol._
import org.bitcoins.core.script.constant._
import org.bitcoins.core.currency._
import org.bitcoins.core.policy._

We need to create a transaction to fund the output containing OP_CHECKLOCKTIMEVERIFY in its script. Go back to your terminal with the bitcoin daemon, grab an output to spend from so we can fund our CLTV output. I will highlight the pieces of data that we need in bold. We need the output’s transaction ID, index of the output in that transaction, the scriptPubKey to create an input from this output, and the private key needed to spend this output:

tom@tom:~/dev/bitcoin$ src/bitcoin-cli -testnet listunspent
"txid": "2591287d954885b8e32a1f3f28ec70ea0c672fe64f3838c3db2b20161a5a1a67",
"vout": 0,
"address": "msbdC7Pgfn8MUjfpfBYxJGZowkVwLe7MFD",
"account": "",
"scriptPubKey": "76a9148483a4aded35a5ed68c58790a2e97de98f12522988ac",
"amount": 0.04200000,
"confirmations": 6808,
"spendable": true,
"solvable": true
tom@tom:~/dev/bitcoin$ src/bitcoin-cli -testnet dumpprivkey msbdC7Pgfn8MUjfpfBYxJGZowkVwLe7MFD

Inside your sbt console, we will take the transaction ID and vout values from above and create the input which will fund the CLTV output. Link to parent transaction of the output we are spending. Let’s also save the private key for “msgbdC7…” (quick note: bitcoin-core returns WIF private keys), as well as the scriptPubKey needed to spend from this address:

~~~Bitcoin-Core returns txIds in big-endian format (what you see on block explorers). However, they are transmitted in little-endian format, which Bitcoin-S-Core currently uses, so we must flip the endianness first. To do that, let's use a utility function in Bitcoin-S-Core called 'flipEndianness' on the txId.~~~
scala> val util = org.bitcoins.core.util.BitcoinSUtil
util: org.bitcoins.core.util.BitcoinSUtil.type = org.bitcoins.core.util.BitcoinSUtil$@422f4ae
scala> util.flipEndianness("2591287d954885b8e32a1f3f28ec70ea0c672fe64f3838c3db2b20161a5a1a67")
res9: String = 671a5a1a16202bdbc338384fe62f670cea70ec283f1f2ae3b88548957d289125
~~~Create the outpoint. A transaction outpoint is a previous transaction's unspent output ("UTXO") that will fund the transaction we are creating. We take the txId (in little-endian) and the index of the output in the previous transaction. ~~~
scala> val outpoint = TransactionOutPoint(DoubleSha256Digest("671a5a1a16202bdbc338384fe62f670cea70ec283f1f2ae3b88548957d289125"), UInt32(0))
outpoint: org.bitcoins.core.protocol.transaction.TransactionOutPoint = TransactionOutPointImpl(DoubleSha256DigestImpl(671a5a1a16202bdbc338384fe62f670cea70ec283f1f2ae3b88548957d289125),UInt32Impl(0))
~~~With the outpoint, we can build the unsigned input. We take in the outpoint, an EmptyScriptSignature (a placeholder to be until we have a signedTx), and a sequence value of zero.~~~
scala> val unsignedInput = TransactionInput(outpoint, EmptyScriptSignature, UInt32(0))
unsignedInput: org.bitcoins.core.protocol.transaction.TransactionInput = TransactionInputImpl(TransactionOutPointImpl(DoubleSha256DigestImpl(671a5a1a16202bdbc338384fe62f670cea70ec283f1f2ae3b88548957d289125),UInt32Impl(0)),EmptyScriptSignature,UInt32Impl(0))
~~~The private key for this outpoint obtained from Bitcoin-Core above.~~~
scala> val fundingPrivKey = ECPrivateKey.fromWIFToPrivateKey("cQ783r3vMAM6AgKEqmSUNLxPKwmg5o6ErUTGPp8jLUhRaUAA11N1")
fundingPrivKey: org.bitcoins.core.crypto.ECPrivateKey = ECPrivateKey(4b530938d6dd84c67126d80f0de1ad72234a84267f6faa63dbba9bf2bb857d42,true)
scala> val fundingPublicKey = fundingPrivKey.publicKey
fundingPublicKey: org.bitcoins.core.crypto.ECPublicKey = ECPublicKey(03188faec2bf8e1589a1f96fdab1db7def26ffe193b3bc368dce14a81a390637ed)
~~~ScriptPubKey to satisfy in order to spend from msbdC7Pgfn8MUjfpfBYxJGZowkVwLe7MFD (taken from 'listunspent' in Bitcoin-Core above) ~~~
scala> val fundingScriptPubKey = ScriptPubKey("76a9148483a4aded35a5ed68c58790a2e97de98f12522988ac")
fundingScriptPubKey: org.bitcoins.core.protocol.script.ScriptPubKey = P2PKHScriptPubKeyImpl(76a9148483a4aded35a5ed68c58790a2e97de98f12522988ac)

To begin building the output, we’ll construct the script to lock our coins. We’ll get a fresh private key from Bitcoin-S-Core, and create a P2PKHScriptPubKey from that private key:

scala> val cltvPrivKey = ECPrivateKey()
cltvPrivKey: org.bitcoins.core.crypto.ECPrivateKey = ECPrivateKey(a46fa89ef4aade9e8396dae572a59748d58a57cc0cdd794d85d9a91788007dd2,true)
scala> val cltvPubKey = cltvPrivKey.publicKey
cltvPubKey: org.bitcoins.core.crypto.ECPublicKey = ECPublicKey(02f0141fadbe6757ed04b7a25dad0c78af8b97af09744318cf15c66a4566fa2ec9)
scala> val p2pkh = P2PKHScriptPubKey(cltvPubKey)
p2pkh: org.bitcoins.core.protocol.script.P2PKHScriptPubKey = P2PKHScriptPubKeyImpl(76a914225d81ac31f9c8368033e1d380b8be1860d53c5d88ac)

The scriptPubKey above is a P2PKHScriptPubKey. We cannot just prepend the OP_CLTV locktime condition to it because that would result in a nonStandardScriptPubKey — not what we want. So we will use the created CLTVScriptPubKey (seen below) as a redeemScript for our P2SHScriptPubKey.

Using Bitcoin-S-Core, take the P2PKHScriptPubKey and a timestamp for the coins to remain locked (set for 1 week from this writing) to create a CLTVScriptPubKey. Then simply use that as the redeemScript for our P2SHScriptPubKey:

scala> val redeemScript = CLTVScriptPubKey(ScriptNumber(1475093805), p2pkh)
redeemScript: org.bitcoins.core.protocol.script.CLTVScriptPubKey = CLTVScriptPubKeyImpl(042d25ec57b17576a914225d81ac31f9c8368033e1d380b8be1860d53c5d88ac)
scala> val p2sh = P2SHScriptPubKey(redeemScript)
p2sh: org.bitcoins.core.protocol.script.P2SHScriptPubKey = P2SHScriptPubKeyImpl(a914fe38c0827175580a627033a16f7009076254718d87)

**Bit of a side note here. After creating the P2SHScriptPubKey, we could end this guide and simply send coins to “2NGRRcCvFVHPiVaeAUuzuVDb75HD5ZiioM3” — the P2SHAddress derived from this P2SHScriptPubKey. But that’s no fun! We’re going to build and broadcast the entire transaction ourselves.

Continue building the output providing the amount, in satoshis, and our P2SHScriptPubKey. The output we are spending contains .042 tBTC, so we are accounting for a fee of 20,000 satoshis (.042-.0002 = .0418). To go from BTC to satoshis, just multiply the BTC value by 100,000,000:

scala> val cltvOutput = TransactionOutput(Satoshis(Int64(4180000)), p2sh)
cltvOutput: org.bitcoins.core.protocol.transaction.TransactionOutput = TransactionOutputImpl(SatoshisImpl(Int64Impl(4180000)),P2SHScriptPubKeyImpl(a914fe38c0827175580a627033a16f7009076254718d87))

With our unsignedInput and cltvOutput created, we can construct an unsignedTransaction using a txVersion of 1, and a locktime of zero so the transaction will be immediately available to be mined:

scala> val unsignedTx = Transaction(UInt32(1), Seq(unsignedInput), Seq(cltvOutput), UInt32(0))
unsignedTx: org.bitcoins.core.protocol.transaction.Transaction = TransactionImpl(UInt32Impl(1),List(TransactionInputImpl(TransactionOutPointImpl(DoubleSha256DigestImpl(671a5a1a16202bdbc338384fe62f670cea70ec283f1f2ae3b88548957d289125),UInt32Impl(0)),EmptyScriptSignature,UInt32Impl(0))),List(TransactionOutputImpl(SatoshisImpl(Int64Impl(4180000)),P2SHScriptPubKeyImpl(a914fe38c0827175580a627033a16f7009076254718d87))),UInt32Impl(0))

We’ll create a TransactionSignatureComponent in Bitcoin-S-Core. This takes all the relevant information needed to sign the transaction. Specifically, it takes the unsigned transaction, the index of the input we are signing for, the scriptPubKey we are satisfying, and the standard script flags.

scala> val txSigComponent = TransactionSignatureComponent(unsignedTx, UInt32(0), fundingScriptPubKey, Policy.standardScriptVerifyFlags)
txSigComponent: org.bitcoins.core.crypto.TransactionSignatureComponent = TransactionSignatureComponentImpl(TransactionImpl(UInt32Impl(1),List(TransactionInputImpl(TransactionOutPointImpl(DoubleSha256DigestImpl(671a5a1a16202bdbc338384fe62f670cea70ec283f1f2ae3b88548957d289125),UInt32Impl(0)),EmptyScriptSignature,UInt32Impl(0))),List(TransactionOutputImpl(SatoshisImpl(Int64Impl(4180000)),P2SHScriptPubKeyImpl(a914fe38c0827175580a627033a16f7009076254718d87))),UInt32Impl(0)),UInt32Impl(0),P2PKHScriptPubKeyImpl(76a9148483a4aded35a5ed68c58790a2e97de98f12522988ac),List(ScriptVerifyP2SH, ScriptVerifyDerSig, ScriptVerifyStrictEnc, ScriptVerifyMinimalData, ScriptVerifyNullDummy, ScriptVerifyDiscourageUpgradableNOPs, ScriptVerifyCleanStack, ScriptVerifyCheckLocktimeVerify, ScriptVerifyCheckSequenceV...

Almost there. Next, we’ll create an ECDigitalSignature with TransactionSignatureCreator. This takes in the txSignatureComponent we just created, the private key needed to spend from the fundingScriptPubKey, and the signature hashtype. The default option is SIGHASH_ALL, which simply signs all the outputs. For more info on these hashtypes, refer to the bitcoin wiki.

scala> val sig = TransactionSignatureCreator.createSig(txSigComponent, fundingPrivKey, SIGHASH_ALL.defaultValue)
sig: org.bitcoins.core.crypto.ECDigitalSignature = ECDigitalSignature(304502210093da588ea52f721f5fb87973d45fb34561768c63bacf5c6b1abffc81e98127e1022042f1afde35b14ff13eef81af7282f36116e2d4a14f0c13ed701785c0a52a65e401)

Now we need to wrap our signature and the fundingPublicKey (saved above) into a P2PKHScriptSignature object.

scala> val scriptSig = P2PKHScriptSignature(sig, fundingPublicKey)
scriptSig: org.bitcoins.core.protocol.script.P2PKHScriptSignature = P2PKHScriptSignatureImpl(48304502210093da588ea52f721f5fb87973d45fb34561768c63bacf5c6b1abffc81e98127e1022042f1afde35b14ff13eef81af7282f36116e2d4a14f0c13ed701785c0a52a65e4012102f0141fadbe6757ed04b7a25dad0c78af8b97af09744318cf15c66a4566fa2ec9)

OK, now remember how our input was unsigned? Well, now we can replace the EmptyScriptSignature with the signed scriptSig we just created:

scala> val signedInput = TransactionInput(outpoint, scriptSig, UInt32(0))
signedInput: org.bitcoins.core.protocol.transaction.TransactionInput = TransactionInputImpl(TransactionOutPointImpl(DoubleSha256DigestImpl(671a5a1a16202bdbc338384fe62f670cea70ec283f1f2ae3b88548957d289125),UInt32Impl(0)),P2PKHScriptSignatureImpl(47304402200ee1a98c1a6ef3d0bb7293ca86fbc68c913cb69a74391ae6f6eada002c73ecfe022078381f1417b68fad15b77310a2b69ab2fabdfcfe0cec4a4bd43ca5600b04f7aa012103188faec2bf8e1589a1f96fdab1db7def26ffe193b3bc368dce14a81a390637ed),UInt32Impl(0))

Just as we constructed the unsignedTx, but replacing the unsignedInput with the signedInput:

scala> val signedTx = Transaction(UInt32(1), Seq(signedInput), Seq(cltvOutput), UInt32(0))
signedTx: org.bitcoins.core.protocol.transaction.Transaction = TransactionImpl(UInt32Impl(1),List(TransactionInputImpl(TransactionOutPointImpl(DoubleSha256DigestImpl(671a5a1a16202bdbc338384fe62f670cea70ec283f1f2ae3b88548957d289125),UInt32Impl(0)),P2PKHScriptSignatureImpl(47304402200ee1a98c1a6ef3d0bb7293ca86fbc68c913cb69a74391ae6f6eada002c73ecfe022078381f1417b68fad15b77310a2b69ab2fabdfcfe0cec4a4bd43ca5600b04f7aa012103188faec2bf8e1589a1f96fdab1db7def26ffe193b3bc368dce14a81a390637ed),UInt32Impl(0))),List(TransactionOutputImpl(SatoshisImpl(Int64Impl(4180000)),P2SHScriptPubKeyImpl(a914fe38c0827175580a627033a16f7009076254718d87))),UInt32Impl(0))

Awesome — we created a signed transaction that sends coins to a CLTV output, which will lock our coins until the timestamp we specified earlier!

The last thing is to manually broadcast the raw signed transaction to the network. Bitcoin-S-Core doesn’t currently have this functionality yet (soon!), but this can be done in a couple ways:

To get the raw hex of the signed transaction:

scala> signedTx.hex
res12: String = 0100000001671a5a1a16202bdbc338384fe62f670cea70ec283f1f2ae3b88548957d289125000000006a47304402200ee1a98c1a6ef3d0bb7293ca86fbc68c913cb69a74391ae6f6eada002c73ecfe022078381f1417b68fad15b77310a2b69ab2fabdfcfe0cec4a4bd43ca5600b04f7aa012103188faec2bf8e1589a1f96fdab1db7def26ffe193b3bc368dce14a81a390637ed000000000120c83f000000000017a914fe38c0827175580a627033a16f7009076254718d8700000000

And that’s all there is to it! /s

You can see the transaction here. OP_CLTV allows for a lot of innovation with Bitcoin, including payment channels, which we plan to implement into Bitcoin-S-Core in the coming months after adding a full SPV-node and wallet.

I’ll come back in a week after the time-lock has past, or after Wed, 28 Sep 2016 20:16:45 GMT to be exact, and try to spend the coins. However, since all the necessary spending information is above and we used testnet, maybe someone will beat me to it!

If you enjoyed reading, please follow us on GitHub/Twitter:

GitHub: https://github.com/bitcoin-s/bitcoin-s-core

Twitter: https://twitter.com/TomMcCabeBTC