HTLC

Hash Time Locked Contract (HTLC) is a conditional payment that can be spent in two ways. Either sender (the person who locked the funds in) can unlock them after some number of blocks have been mined or the receiver (the person whom the funds are intended for) when he gets reveald the secret code that only sender knows.

Note that each condition also runs a OP_EQUALVERIFY check on a public key to enforce that:

  • only the sender can spend after X amount of blocks are mined
  • only the recipient can spend if he knows the secret (sender or anyone else can not spend these coins if they know the secret)

HTLC are used in Lightning Network, atomic swaps, same-chain coin swaps and other advanced protocols.

There are different ways of how HTLC's can be constructed and bellow example is just one of the way and not necesseraly the best way. For example OP_CHECKLOCKTIMEVERIFY would better ber replaced with OP_CSV, and the script itself could be optimized to be smaller in size.

Generate address

 1import hashlib
 2
 3from bitcoin import SelectParams
 4from bitcoin.core import b2x, x, lx, COIN, COutPoint, CMutableTxOut, CMutableTxIn, CMutableTransaction, Hash160, CScriptWitness, CTxInWitness, CTxWitness
 5from bitcoin.core.script import CScript, OP_0, OP_IF, OP_ELSE, OP_SHA256, OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKLOCKTIMEVERIFY, OP_CHECKSIG, SignatureHash, SIGHASH_ALL, OP_DROP, OP_ENDIF, SIGVERSION_WITNESS_V0
 6from bitcoin.wallet import CBitcoinAddress, CBitcoinSecret, P2WSHBitcoinAddress
 7
 8SelectParams('regtest')
 9
10# addresses generate via bitcoin-cli public key and private key were fetched using bitcoin-cli commands: `getaddressinfo` and `dumpprivkey`
11# 1. (sender): bcrt1qz52gzlzcesun0cy4v8u5k6uwrjmqayhvf0w806 (03d28046cd12e83832cca3fc4428d254e60092e06fa3f3b8de32062c1b07f58976)
12# 2. (recipient): bcrt1q3qk7a5d2963feda6d6lrrvrvsnp05zlzasme8f (02743d0627a342afdcd7b1577d4d175f0c5206bc3da193bfecd5594b2eba1d256c)
13
14# private key for recipient bcrt1q3qk7a5d2963feda6d6lrrvrvsnp05zlzasme8f (generated via bitcoin-cli)
15private_key_recipient = "cR6XDAkF7urxoX3PioYfWwCB5MJTVuCNTdeUc5t2nceKPL2nBmfJ"
16seckey_recipient = CBitcoinSecret(private_key_recipient)
17
18# private key for sender bcrt1qz52gzlzcesun0cy4v8u5k6uwrjmqayhvf0w806 (generated via bitcoin-cli)
19private_key_sender = "cNQREEPKSd7ugdCMAQNEedNpYhGrgXiqfyWSt4H7jfm722FWFz2V"
20seckey_sender = CBitcoinSecret(private_key_sender)
21
22# secret and preimage
23secret = b"super secret code"
24preimage = hashlib.sha256(secret).digest()
25
26lockduration = 5  # in 5 blocks
27current_blocknum = 274  # current number of blocks
28redeem_blocknum = current_blocknum + lockduration
29recipientpubkey = x("02743d0627a342afdcd7b1577d4d175f0c5206bc3da193bfecd5594b2eba1d256c")
30senderpubkey = x("03d28046cd12e83832cca3fc4428d254e60092e06fa3f3b8de32062c1b07f58976")
31
32# Create a htlc script where in order to spend either of the two conditions need to be met:
33# 1. recipient can claim funds by providing a secret
34# 2. sender can claim funds after X blocks have been mined
35witness_script = CScript([
36    OP_IF,
37        OP_SHA256, preimage, OP_EQUALVERIFY, OP_DUP, OP_HASH160, Hash160(recipientpubkey),
38    OP_ELSE,
39        redeem_blocknum, OP_CHECKLOCKTIMEVERIFY, OP_DROP, OP_DUP, OP_HASH160, Hash160(senderpubkey),
40    OP_ENDIF,
41    OP_EQUALVERIFY,
42    OP_CHECKSIG,
43])
44
45script_hash = hashlib.sha256(witness_script).digest()
46script_pubkey = CScript([OP_0, script_hash])
47address = P2WSHBitcoinAddress.from_scriptPubKey(script_pubkey)
48print('Address:', str(address))
49# Address: bcrt1q5xpn8y8hlkf5nqc2sqkph64qmj0exf6cwtlqknf34y9s4ejga2zs3f2m9s

Spend from address (via secret)

In this example we will spend the funds by using the secret.

 1# we are continuing the code from above
 2
 3txid = lx("2c3759145da44e8a75d24e3535f643de4e28fb85e9f779fa1d0255c7f8b17729")
 4vout = 0
 5
 6# Specify the amount send to your P2WSH address.
 7amount = int(1 * COIN)
 8
 9# Calculate an amount for the upcoming new UTXO. Set a high fee (5%) to bypass bitcoind minfee
10# setting on regtest.
11amount_less_fee = amount * 0.99
12
13# Create the txin structure, which includes the outpoint. The scriptSig defaults to being empty as
14# is necessary for spending a P2WSH output.
15txin = CMutableTxIn(COutPoint(txid, vout))
16
17# Specify a destination address and create the txout.
18destination = CBitcoinAddress("bcrt1q3qk7a5d2963feda6d6lrrvrvsnp05zlzasme8f").to_scriptPubKey() 
19txout = CMutableTxOut(amount_less_fee, destination)
20
21# Create the unsigned transaction.
22tx = CMutableTransaction([txin], [txout])
23
24# Calculate the signature hash for that transaction.
25sighash = SignatureHash(
26    script=witness_script,
27    txTo=tx,
28    inIdx=0,
29    hashtype=SIGHASH_ALL,
30    amount=amount,
31    sigversion=SIGVERSION_WITNESS_V0,
32)
33
34# Now sign it. We have to append the type of signature we want to the end, in this case the usual
35# SIGHASH_ALL.
36sig = seckey_recipient.sign(sighash) + bytes([SIGHASH_ALL])
37
38# Construct a witness for this P2WSH transaction and add to tx.
39witness = CScriptWitness([sig, seckey_recipient.pub, secret, b'\x01', witness_script])
40tx.wit = CTxWitness([CTxInWitness(witness)])
41
42# Done! Print the transaction
43print(b2x(tx.serialize()))
44# outputs: 010000000001012977b1f8c755021dfa79f7e985fb284ede43f635354ed2758a4ea45d1459372c0000000000ffffffff01c09ee60500000000160014882deed1aa2ea29cb7ba6ebe31b06c84c2fa0be205473044022023b1482fb9e119eaac4c623ded42aece99b468d39f6a0a2a21bdb0320a9f2a770220237b99c6063061ae72dd4b39b9fb6390a3f070cdb2b7dee936a8cb0f359cdb7f012102743d0627a342afdcd7b1577d4d175f0c5206bc3da193bfecd5594b2eba1d256c1173757065722073656372657420636f646501015b63a8206bd66227651d0fe5c43863d7b29a4097e31dbf51e6603eee75947bff5e96ae438876a914882deed1aa2ea29cb7ba6ebe31b06c84c2fa0be267021701b17576a9141514817c58cc3937e09561f94b6b8e1cb60e92ec6888ac00000000

Now that we have our signed and encoded transaction, we can broadcast it using sendrawtransaction:

bitcoin-cli sendrawtransaction <transaction>

If the transaction is broadcasted successfully a transaction id will be returned. In this case it was fbf084eece2f14a017eabfe7fd7db3ee42f0245560a851f39cf34131caabfcb7.

Spend from address (after timeout)

In this example we will spend the funds after X blocks have been mined.

 1# we are continuing the code from above
 2
 3txid = lx("1f55451b6fe0d839ada38724d00a94118afedaf6564e492887493e3c9dd0dca4")
 4vout = 0
 5
 6# Specify the amount send to your P2WSH address.
 7amount = int(1 * COIN)
 8
 9# Calculate an amount for the upcoming new UTXO. Set a high fee (5%) to bypass bitcoind minfee
10# setting on regtest.
11amount_less_fee = amount * 0.99
12
13# Create the txin structure, which includes the outpoint. The scriptSig defaults to being empty as
14# is necessary for spending a P2WSH output.
15txin = CMutableTxIn(COutPoint(txid, vout))
16
17# Specify a destination address and create the txout.
18destination = CBitcoinAddress("bcrt1q3qk7a5d2963feda6d6lrrvrvsnp05zlzasme8f").to_scriptPubKey() 
19txout = CMutableTxOut(amount_less_fee, destination)
20
21# The default nSequence of FFFFFFFF won't let you redeem when there's a CHECKTIMELOCKVERIFY
22txin.nSequence = 0
23
24# Create the unsigned transaction.
25tx = CMutableTransaction([txin], [txout])
26
27# nLockTime needs to be at least as large as parameter of CHECKLOCKTIMEVERIFY for script to verify
28tx.nLockTime = redeem_blocknum
29
30# Calculate the signature hash for that transaction.
31sighash = SignatureHash(
32    script=witness_script,
33    txTo=tx,
34    inIdx=0,
35    hashtype=SIGHASH_ALL,
36    amount=amount,
37    sigversion=SIGVERSION_WITNESS_V0,
38)
39
40# Now sign it. We have to append the type of signature we want to the end, in this case the usual
41# SIGHASH_ALL.
42sig = seckey_sender.sign(sighash) + bytes([SIGHASH_ALL])
43
44# Construct a witness for this P2WSH transaction and add to tx.
45witness = CScriptWitness([sig, seckey_sender.pub, b'', witness_script])
46tx.wit = CTxWitness([CTxInWitness(witness)])
47
48# Done! Print the transaction
49print(b2x(tx.serialize()))
50# outputs: 010000000001015f8d477e52c7c4338952a9a8a053d682ff42023097e54df102d47f477f3e274900000000000000000001c09ee60500000000160014882deed1aa2ea29cb7ba6ebe31b06c84c2fa0be204483045022100befaddac4d23e7a5edcf5847e6f5cc127021ebc34719b32680bc040aeece5b7402204736e36df03142e7c0573e6d2e9f44e7936e477fd6c76d59a032de62458e6248012103d28046cd12e83832cca3fc4428d254e60092e06fa3f3b8de32062c1b07f58976005b63a8206bd66227651d0fe5c43863d7b29a4097e31dbf51e6603eee75947bff5e96ae438876a914882deed1aa2ea29cb7ba6ebe31b06c84c2fa0be267021701b17576a9141514817c58cc3937e09561f94b6b8e1cb60e92ec6888ac17010000

Now that we have our signed and encoded transaction, we can broadcast it using sendrawtransaction:

bitcoin-cli sendrawtransaction <transaction>

If the transaction is broadcasted successfully a transaction id will be returned. In this case it was 52a580aad3f436096933dd6d3c58dc5131050bdc2d53eea416e934b0d699d89c.


Sources: