哈希时间锁定合约(HTLC)的原理与实现

简介

HTLC 读者应该自己去了解它是什么。简要而言,它通过使用哈希锁和时间锁确保交易双方在无须相互信任的情况下完成交易。

合约概述

HTLC 允许发送方在特定条件下将资金锁定在合约中,接收方揭露原像后可以提取资金。如果在规定时间内条件未被满足,发送方可以取回被锁定的资金。合约的核心功能包括:

  1. newContract(receiver, hashlock, timelock):创建新的HTLC。

  2. withdraw(contractId, preimage):在过期前,接收方使用哈希锁的原像提取资金。

  3. refund(contractId):在时间锁过期后,发送方可申请退款。

在 ETH 中实现

我们以参考 1 中的 HashedTimelock 合约为讲解对象。

伪代码

源代码太长,我们抽离出其逻辑:

 1class HTLC:
 2    def __init__(self):
 3        self.contracts = {}
 4
 5    def create_contract(self, sender, receiver, amount, hashlock, timelock):
 6        contract_id = sha256(f"{sender}{receiver}{amount}{hashlock}{timelock}".encode()).hexdigest()
 7        if contract_id in self.contracts:
 8            raise Exception("Contract already exists")
 9
10        self.contracts[contract_id] = {
11            "sender": sender,
12            "receiver": receiver,
13            "amount": amount,
14            "hashlock": hashlock,
15            "timelock": timelock,
16            "withdrawn": False,
17            "refunded": False,
18            "preimage": None
19        }
20        return contract_id
21
22    def withdraw(self, contract_id, preimage):
23        if contract_id not in self.contracts:
24            raise Exception("Contract does not exist")
25
26        contract = self.contracts[contract_id]
27        if contract["receiver"] != get_current_user():
28            raise Exception("Only the receiver can withdraw")
29        if contract["withdrawn"]:
30            raise Exception("Funds already withdrawn")
31        if contract["timelock"] <= time.time():
32            raise Exception("Timelock has expired")
33        if contract["hashlock"] != sha256(preimage.encode()).hexdigest():
34            raise Exception("Invalid preimage")
35
36        contract["withdrawn"] = True
37        contract["preimage"] = preimage
38        transfer_funds(contract["amount"], contract["receiver"])
39
40    def refund(self, contract_id):
41        if contract_id not in self.contracts:
42            raise Exception("Contract does not exist")
43
44        contract = self.contracts[contract_id]
45        if contract["sender"] != get_current_user():
46            raise Exception("Only the sender can refund")
47        if contract["refunded"]:
48            raise Exception("Funds already refunded")
49        if contract["withdrawn"]:
50            raise Exception("Funds already withdrawn")
51        if contract["timelock"] > time.time():
52            raise Exception("Timelock has not expired")
53
54        contract["refunded"] = True
55        transfer_funds(contract["amount"], contract["sender"])

从伪代码可以体会到实现 HTLC 的逻辑,非常简单。

  1. create_contract(sender, receiver, amount, hashlock, timelock) 的逻辑

    • 创建一个新的HTLC合约。包括必要信息:谁转给谁,哈希锁(即需要提供的原像),时间锁(即过期时间)。

    • 哈希消息,生成唯一的合约ID。哈希的输入是根据所有必要信息生成的,防止了篡改。

    • 检查是否存在重复的合约。

    • 存储合约的详细信息。

  2. withdraw(contract_id, preimage) 的逻辑

    • 验证合约存在性和接收方身份。

    • 检查资金是否已被提取。

    • 检查时间锁是否未过期。

    • 验证原像是否匹配哈希锁。

    • 标记资金已提取并记录原像。

  3. refund(contract_id) 的逻辑

    • 验证合约存在性和发送方身份。

    • 检查资金是否已被退款或提取。

    • 检查时间锁是否已过期。

    • 标记资金已退款。

在 EVM 中,我们每个合约可以享有存储空间,并且有充分的编码发挥空间,因此实现 HTLC 很容易。那么在 BTC 中呢?

在 BTC 中实现

在比特币(BTC)中实现哈希时间锁定合约(HTLC)并不像在以太坊上那样直接,因为比特币缺乏图灵完备的智能合约能力。然而,比特币的脚本语言尽管相对简单,但仍然能够实现HTLC的基本功能。原因在于,HTLC 本质上就是做下面的判断:

  1. 哈希是多少。这可以在创建交易时计算,不需要在链上完成。

  2. 是否哈希匹配。这可以通过 OP_CHECKSIG 指令实现。

  3. 是否过期。这可以通过交易自带的超时机制实现。

故可行。

比特币中的HTLC

我们需要只用比特币的脚本语言,通过锁定一定数量的比特币,要求接收方在规定时间内提供正确的哈希锁原像,否则发送方可以取回资金。基本的流程如下:

  1. 创建哈希时间锁定交易(HTLT)

    • 发送方生成一个随机数(nonce) $x$ 作为原像,并计算其哈希 $h = \operatorname{SHA256}(x)$。

    • 发送方向比特币网络广播一笔特殊交易,将资金锁定在一个多重条件(HTLC)脚本中。

  2. HTLC脚本条件

    • 接收方提供正确的哈希原像 $x$。

    • 或者,在时间锁 $timelock$ 过期后,发送方可以取回资金。

BIP199 HTLC

以下是摘自 BIP199 的代码:

A Hashed Time-Locked Contract (HTLC) is a script that permits a designated party (the “seller”) to spend funds by disclosing the preimage of a hash. It also permits a second party (the “buyer”) to spend the funds after a timeout is reached, in a refund situation.

The script takes the following form:

    OP_IF
        [HASHOP] <digest> OP_EQUALVERIFY OP_DUP OP_HASH160 <seller pubkey hash>            
    OP_ELSE
        <num> [TIMEOUTOP] OP_DROP OP_DUP OP_HASH160 <buyer pubkey hash>
    OP_ENDIF
    OP_EQUALVERIFY
    OP_CHECKSIG
  • [HASHOP] is either OP_SHA256 or OP_HASH160.

  • [TIMEOUTOP] is either OP_CHECKSEQUENCEVERIFY or OP_CHECKLOCKTIMEVERIFY.

其中:

  1. 哈希锁分支

    • IF [HASHOP] <digest> OP_EQUALVERIFY ...: 如果提供的原像<digest> 哈希后匹配,卖方可以提供他的公钥和签名以取款。
  2. 时间锁分支

    • ELSE <num> [TIMEOUTOP] OP_DROP: 如果卖方未能在规定时间内提供正确的哈希原像,买方在时间锁过期后可以提供他的公钥和签名以取回资金。

使用 BIP-199 HTLC

上面的代码会被嵌入到比特币交易的 scriptPubKey 中,用于锁定比特币。

创建和锁定比特币

  1. 创建 P2SH 地址:首先,你需要将这个脚本嵌入到一个 P2SH(Pay-to-Script-Hash)地址中。P2SH 地址是比特币的一种支付方式,它允许复杂的脚本被简化为一个哈希值。

    1# 创建赎回脚本
    2redeemScript="OP_IF ..."
    3
    4# 将赎回脚本转换为 P2SH 地址(其实就是计算出赎回脚本的哈希值)
    5p2shAddress=$(bitcoin-cli addmultisigaddress 1 '["'$redeemScript'"]' | jq -r '.address')
    

    ``

  2. 发送比特币到 P2SH 地址:使用生成的 P2SH 地址接收比特币。

    1# 创建一个发送到 P2SH 地址的原始交易
    2rawTx=$(bitcoin-cli createrawtransaction '[]' '{"'$p2shAddress'":0.01}')
    3
    4# 签名并发送交易
    5signedTx=$(bitcoin-cli signrawtransactionwithwallet $rawTx | jq -r '.hex')
    6txid=$(bitcoin-cli sendrawtransaction $signedTx)
    

    ``

花费比特币

要花费这些比特币,必须提供一个满足该脚本条件的 scriptSig。这里我们有两个路径:

  • 提供 secret:scriptSig:[sig] [pubkey] [secret]

  • 提供 locktime:scriptSig:[sig] [pubkey] [locktime]

下面只以其中一个为例。

  1. 构建 scriptSigscriptSig 必须包含满足条件的签名和公钥。

    1# 构建 scriptSig
    2scriptSig="0 <signature> <seller pubkey> <secret>"
    

    ``

  2. 创建花费交易:使用 scriptSig 创建一个新的交易,花费锁定在 P2SH 地址中的比特币。

     1# 获取未花费输出
     2utxo=$(bitcoin-cli listunspent 0 9999999 '["'$p2shAddress'"]' | jq -r '.[0]')
     3
     4# 创建一个花费 P2SH 地址的原始交易
     5rawSpendTx=$(bitcoin-cli createrawtransaction '[{"txid":"'$utxo.txid'","vout":'$utxo.vout',"scriptPubKey":"'$utxo.scriptPubKey'","redeemScript":"'$redeemScript'"}]' '{"<recipient address>":0.0099}')
     6
     7# 签名交易
     8signedSpendTx=$(bitcoin-cli signrawtransactionwithwallet $rawSpendTx | jq -r '.hex')
     9
    10# 发送交易
    11spendTxid=$(bitcoin-cli sendrawtransaction $signedSpendTx)
    

    ``

栈的变化分析

  1. 初始栈

    • 栈:[sig] [pubkey] [secret] 1 或 [sig] [pubkey] [locktime] 0
  2. 执行 OP_IF

买家取款路径(提供 secret)

  • 栈:[sig] [pubkey] [secret] 1

  • 执行 OP_IF

    • 由于 1 是真值,执行 IF 分支。

    • 栈:[sig] [pubkey] [secret]

  1. 执行 IF 分支
  • 执行 OP_SHA256 <digest> OP_EQUALVERIFY

    • 栈:[sig] [pubkey]

    • OP_SHA256 对 secret 进行哈希运算并与 <digest> 比较。

    • OP_EQUALVERIFY 验证通过,栈变为 [sig] [pubkey]

  • 执行 OP_DUP OP_HASH160 <seller pubkey hash>

    • 栈:[sig] [pubkey] [pubkey]

    • OP_DUP 复制栈顶元素,栈变为 [sig] [pubkey] [pubkey]

    • OP_HASH160 对栈顶元素进行哈希运算并与 <seller pubkey hash> 比较。

    • 验证通过,栈变为 [sig] [pubkey]

卖家取款路径(提供 locktime)

  • 栈:[sig] [pubkey] [locktime] 0

  • 执行 OP_IF

    • 由于 0 是假值,执行 ELSE 分支。

    • 栈:[sig] [pubkey] [locktime]

  1. 执行 ELSE 分支
  • 执行 <locktime> OP_CHECKLOCKTIMEVERIFY OP_DROP

    • 栈:[sig] [pubkey]

    • OP_CHECKLOCKTIMEVERIFY 验证交易的 nLockTime 是否大于等于 locktime。

    • OP_DROP 移除栈顶元素 locktime,栈变为 [sig] [pubkey]

  • 执行 OP_DUP OP_HASH160 <buyer pubkey hash>

    • 栈:[sig] [pubkey] [pubkey]

    • OP_DUP 复制栈顶元素,栈变为 [sig] [pubkey] [pubkey]

    • OP_HASH160 对栈顶元素进行哈希运算并与 <buyer pubkey hash> 比较。

    • 验证通过,栈变为 [sig] [pubkey]

公共部分

  • 两个分支合并后的栈均为 [sig] [pubkey]

  • 执行 OP_EQUALVERIFY

    • 栈:[sig] [pubkey]

    • OP_EQUALVERIFY 验证公钥和签名是否匹配,栈变为 [pubkey]

  • 执行 OP_CHECKSIG

    • 验证签名是否有效,验证通过后栈变为空,脚本执行成功。

参考

  1. hashed-timelock-contract-ethereum/contracts/HashedTimelock.sol at master · chatch/hashed-timelock-contract-ethereum (github.com)

  2. Bolt 3 HTLC 的实现和 BIP-199 不完全相同,后续有时间再整理发表。ref