哈希时间锁合约(HTLC)
哈希时间锁合约(Hashed Timelock Contract,HTLC)是一种智能合约,它允许将价值(如代币)锁定一段固定时间(或一定数量的区块)。在价值被锁定的期间,只有提供正确的密钥(哈希原像)时,才能将其转移给指定的接收方。这确保了价值的转移仅在特定条件下发生,即只有代币的所有者选择揭示密钥时才会发生。本文将展示在 Solidity 中实现 HTLC 的一种可能方式。
跨链原子交换
HTLC 的一个典型应用场景是(原子性地)在一条区块链上交付一种资产,以换取在另一条区块链上支付另一种资产。假设Alice希望在链 A 上向Bob发送某种代币(代币 1),而Bob希望使用链 B 上的某种代币(代币 2)向Alice支付。双方已在线下就交易条款达成一致。“顺利路径”(即双方在时间窗口结束前均未退出交易)的流程如下:

如果在时间窗口结束后,HTLC中的代币仍未被认领,则发送方有权获得退款,而接收方无法再认领该代币。
Solidity 设计
为了编写 Solidity 实现,我们需要选择可以在 HTLC 中锁定的价值类型。为了保持通用性且不过度复杂化合约,本示例将支持任何符合 ERC-20 标准的代币。最简单的选项是仅使用以太币本身,但这会大大限制其潜在应用。由于接口简单,支持任何 ERC-20 代币都是直接的,并且可以推广到各种可能的代币,包括(包装的)以太币。其他选项可能包括 ERC-721 或 ERC-1155 代币。可以轻松调整此示例以支持这些其他代币标准。
在每个 ERC-20 合约中包含一个 HTLC 实现将是浪费且不可行的。相反,我们应该创建一个单独的、与 ERC-20 合约分离的 HTLC,该 HTLC 可以与任意数量的 ERC-20 代币交互。这利用了 Solidity 合约的可组合性。
以下是 HTLC 及其与 ERC-20 代币交互的状态机图。转换所需的先决条件在括号中指示,t 表示区块链提供的当前时间。初始状态由一个没有来源的箭头指向。每个状态转换都是通过调用 HTLC 上的(外部)方法执行的。该图仅涉及与Alice-Bob交易相关的状态。

最初,Alice是(未锁定的)代币的所有者,代币存储在 ERC-20 合约中。锁定是通过将代币的所有权从Alice转移到 HTLC 本身来实现的,从而防止除 HTLC 指定条件外的任何转移。只要Bob提供一个原像 p,使得 h§ 等于Alice提供的哈希值,他就可以认领该代币,其中 h 是哈希函数(本例中为 keccak256)。
以下是 HTLC 的存储示例:
pragma solidity >=0.8.0 <0.9.0;
contract HTLC {
struct Lock {
uint unlockTime;
uint amount;
address tokenAddress;
address senderAddress;
address receiverAddress;
}
mapping(bytes32 => Lock) public locks;
…
}
Lock 结构体存储了锁定特定代币所需的所有信息,除了哈希值。我们可以在 locks 映射中存储任意一组锁,每个锁都通过哈希值进行索引。为了简单起见,我选择了通过哈希值进行索引,但为了使其更通用,可以使用不同的索引,以便使用相同的哈希值锁定多个代币。
可能的状态转换通过以下方法公开:
contract HTLC {
…
function claim(bytes calldata preImage) external { … }
function lock(
bytes32 hashValue,
uint unlockTime,
uint amount,
address tokenAddress,
address receiverAddress
) external { … }
function retake(bytes32 hashValue) external { … }
}
新的锁条目通过 lock 方法插入到 locks 中。如果哈希值已被使用,则此方法将回滚交易。请注意,lock 方法将代币从 msg.sender 转移到 HTLC,因此 msg.sender 必须预先批准 HTLC 使用 ERC-20 的 approve 方法代表她执行此转移。
claim 方法允许 msg.sender 通过哈希处理提供的 preImage 并检查其是否存在于 locks 中来接收锁定的代币。如果存在且解锁时间未到,则检查 msg.sender 是否等于 receiverAddress,之后可以转移代币。请注意,在转移代币之前必须删除 locks 中的条目,以避免重入攻击。
retake 方法的操作与 claim 类似,但不需要原像。
Web3.py 跨链原子交换(HTLC)模拟
完整流程如下:
🌕 步骤 1:Alice 生成 S 和 H
Alice 创建:
S(秘密)
H = SHA256(S)
🌕 步骤 2:Alice 在 A 链上创建 HTLC_A → Bob
这个 HTLC 内容是:
- 如果 Bob 提供秘密 S + Bob 签名 → Bob 拿 Token_A
- 过期后 Alice 退款
Alice 把:
- HTLC 的 txid
- H(哈希)
发送给 Bob。
👉 注意:Alice 并没有泄露 S,只泄露了 H。 H 不能推导出 S。
🌕 步骤 3:Bob 检查 Alice 的 HTLC,确保资金已经锁定
🌕 步骤 4:Bob 在 B 链上创建自己的 HTLC_B → Alice
Bob 会锁定 Token_B,条件同样是:
- 如果 Alice 提供 S + Alice 签名 → Alice 拿 Token_B
- 过期后 Bob 退款
现在两边变成:
Alice 已锁 Token_A
Bob 已锁 Token_B
此时交换才正式成立。
🌕 步骤 5:Alice 在 B 链上用 S 提取 Token_B(暴露秘密)
当 Alice 在 B 链上执行提款交易时:
- S 会被自动记录在区块链数据中
- Bob 会看到这个 S
🌕 步骤 6:Bob 在 A 链上用 S + Bob 私钥签名取走 Token_A
交换完成。
🔐 核心安全论证总结
- ✔ Bob 想取 Token_A → 需要 S
- ✔ 想得到 S → 必须等 Alice 在 B 链上用 S 取 Token_B
- ✔ Alice 想取 Token_B → 需要 Bob 的 HTLC 已经存在
- ✔ 所以 Bob 必须先锁 Token_B 才能获得 S
- ✔ Alice 永远不会提前泄露 S
1. 编写测试的ERC20代币合约和HTLC合约
test_token_sol_code = \”\”\”
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
import \”@openzeppelin/contracts/token/ERC20/ERC20.sol\”;
// ERC20 token used for unit testing
contract TestToken is ERC20 {
constructor(
string memory name,
string memory symbol,
uint256 initialSupply
) ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
}
}
\”\”\”
htlc_sol_code = \”\”\”
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
import \”@openzeppelin/contracts/token/ERC20/ERC20.sol\”;
contract HTLC {
struct Lock {
uint unlockTime;
uint amount;
address tokenAddress;
address senderAddress;
address receiverAddress;
}
mapping(bytes32 => Lock) public locks;
event Claimed(
bytes preImage,
bytes32 hashValue,
uint when,
uint amount,
address tokenAddress,
address senderAddress,
address receiverAddress
);
event Locked(
bytes32 hashValue,
uint when,
uint amount,
address tokenAddress,
address senderAddress,
address receiverAddress
);
event Retaken(
bytes32 hashValue,
uint when,
uint amount,
address tokenAddress,
address senderAddress,
address receiverAddress
);
function claim(bytes calldata preImage) external {
bytes32 hashValue = keccak256(preImage);
Lock storage l = locks[hashValue];
uint amount = l.amount;
require(amount > 0, \”HTLC: not a valid pre-image for any hash\”);
require(block.timestamp < l.unlockTime, \”HTLC: can only claim before the unlock time\”);
address receiverAddress = l.receiverAddress;
require(msg.sender == receiverAddress, \”HTLC: only the receiver can claim\”);
IERC20 erc20 = IERC20(l.tokenAddress);
delete locks[hashValue];
require(erc20.transfer(receiverAddress, amount), \”HTLC: erc20 transfer must be successful\”);
emit Claimed({
preImage: preImage,
hashValue: hashValue,
amount: l.amount,
when: block.timestamp,
tokenAddress: l.tokenAddress,
senderAddress: l.senderAddress,
receiverAddress: l.receiverAddress
});
}
function lock(
bytes32 hashValue,
uint unlockTime,
uint amount,
address tokenAddress,
address receiverAddress
) external {
require(locks[hashValue].amount == 0, \”HTLC: lock cannot already exist for the same hash value\”);
require(amount > 0, \”HTLC: cannot lock zero tokens\”);
locks[hashValue] = Lock({
unlockTime: unlockTime,
amount: amount,
tokenAddress: tokenAddress,
senderAddress: msg.sender,
receiverAddress: receiverAddress
});
IERC20 erc20 = IERC20(tokenAddress);
require(
erc20.transferFrom(msg.sender, address(this), amount),
\”HTLC: erc20 transfer for locking must be successful\”
);
}
function retake(bytes32 hashValue) external {
Lock storage l = locks[hashValue];
uint amount = l.amount;
require(amount > 0, \”HTLC: no lock exists for the given hash\”);
require(block.timestamp >= l.unlockTime, \”HTLC: can only retake on or after the unlock time\”);
address senderAddress = l.senderAddress;
require(msg.sender == senderAddress, \”HTLC: only the sender can retake\”);
IERC20 erc20 = IERC20(l.tokenAddress);
delete locks[hashValue];
require(erc20.transfer(senderAddress, amount), \”HTLC: erc20 transfer must be successful\”);
emit Retaken({
hashValue: hashValue,
amount: l.amount,
when: block.timestamp,
tokenAddress: l.tokenAddress,
senderAddress: l.senderAddress,
receiverAddress: l.receiverAddress
});
}
}
\”\”\”
2. 创建两个测试账户
from web3 import Web3
w3 = Web3(Web3.EthereumTesterProvider())
assert w3.is_connected(), \”无法连接到Ethereum节点\”
Alice = w3.eth



