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

如果在时间窗口结束后,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.accounts[0]
Bob = w3.eth.accounts[1]
创建一个部署函数
from solcx import compile_source
def deploy(account, source_code, constructor_args=()):
compiled_erc20 = compile_source(source_code, import_remappings=["@openzeppelin=openzeppelin-contracts"], output_values=['abi','bin'])
_, erc20_intf = compiled_erc20.popitem()
erc20_abi = erc20_intf["abi"]
erc20_bytecode = erc20_intf["bin"]
contract = w3.eth.contract(abi=erc20_abi, bytecode=erc20_bytecode)
tx_hash = contract.constructor(*constructor_args).transact({'from': account})
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
token = w3.eth.contract(address=tx_receipt.contractAddress, abi=erc20_abi)
return token
3. 部署测试Token合约
AliceToken = deploy(Alice, test_token_sol_code, ["AliceToken", "AT", 10**20])
BobToken = deploy(Bob, test_token_sol_code, ["BobToken", "BT", 10**20])
print("AliceToken address:", AliceToken.address)
print("BobToken address:", BobToken.address)
AliceToken address: 0xF2E246BB76DF876Cef8b38ae84130F4F55De395b
BobToken address: 0x153b84F377C6C7a7D93Bd9a717E48097Ca6Cfd11
4. 部署HTLC合约
AliceHTLC = deploy(Alice, htlc_sol_code)
BobHTLC = deploy(Bob, htlc_sol_code)
print("AliceHTLC address:", AliceHTLC.address)
print("BobHTLC address:", BobHTLC.address)
AliceHTLC address: 0x2946259E0334f33A064106302415aD3391BeD384
BobHTLC address: 0xa45EeF86CC2eB1477872b07a1298FFa29313610D
5. 生成随机 secret S 和 hash H
import secrets
secret = secrets.token_bytes(32)
hashValue = w3.keccak(secret)
print("Secret (S):", secret.hex())
print("Hash(S):", hashValue.hex())
Secret (S): 28043561724a3983d3e2637b0af3c12fbeb7f7402dd1c5457748684ab9955d7a
Hash(S): 8af06c659d48be18a95e929b1e8ec6cb7bbe01bfb1e2f02bea1436c70b24c735
6. Alice 在链 A 锁定 AliceToken
import time
amountA = 1234
unlockA = int(time.time()) + 1000
AliceToken.functions.approve(AliceHTLC.address, amountA).transact({"from": Alice})
AliceHTLC.functions.lock(hashValue, unlockA, amountA, AliceToken.address, Bob).transact({"from": Alice})
print(f"Alice locked {amountA} AliceToken for Bob on chain A")
Alice locked 1234 AliceToken for Bob on chain A
7. Bob 查询锁是否存在
import time
unlockTime, amount, tokenAddress, senderAddres, receiverAddress = AliceHTLC.functions.locks(hashValue).call()
assert unlockTime > time.time()
assert amount == 1234
assert tokenAddress == AliceToken.address
assert senderAddres == Alice
assert receiverAddress == Bob
print("Bob 验证HTLC锁正确")
Bob 验证HTLC锁正确
8. Bob 在链 B 锁定 BobToken
amountB = 4000
unlockB = int(time.time()) + 500 # Bob 的时间更短
BobToken.functions.approve(BobHTLC.address, amountB).transact({"from": Bob})
BobHTLC.functions.lock(hashValue, unlockB, amountB, BobToken.address, Alice).transact({"from": Bob})
print(f"Bob locked {amountB} BobToken for Alice on chain B")
Bob locked 4000 BobToken for Alice on chain B
9. Alice 使用秘密 S 在链 B 领取 Bob 的 BobToken
BobHTLC.functions.claim(secret).transact({"from": Alice})
print("Alice claimed Bob's BobToken on chain B (secret revealed)")
Alice claimed Bob's BobToken on chain B (secret revealed)
10. Bob 监听 BobHTLC 的事件并获取 secret(S)
event = BobHTLC.events.Claimed.create_filter(
from_block=w3.eth.block_number,
to_block='latest'
).get_all_entries()[0]
preimage = event['args']['preImage'] # 这就是 S
hashValue = event['args']['hashValue']
11. Bob 看到 S,立刻在链 A 领取 Alice 的 AliceToken
AliceHTLC.functions.claim(preimage).transact({"from": Bob})
print("Bob claimed Alice's AliceToken on chain A")
Bob claimed Alice's AliceToken on chain A
12. 打印最终余额
print("=== Final Balances ===")
print("Alice AliceToken:", AliceToken.functions.balanceOf(Alice).call())
print("Alice BobToken:", BobToken.functions.balanceOf(Alice).call())
print("Bob AliceToken:", AliceToken.functions.balanceOf(Bob).call())
print("Bob BobToken:", BobToken.functions.balanceOf(Bob).call())
=== Final Balances ===
Alice AliceToken: 99999999999999998766
Alice BobToken: 4000
Bob AliceToken: 1234
Bob BobToken: 99999999999999996000
模拟交易超时情况
流程:
- Alice 生成哈希 H,设置了很短的 unlock time
- Bob 使用 hash(H) 在链 B 上锁定 BobToken
- Bob 等待一段时间,但 Alice 从未使用 preimage(S) 来 claim
- 时间超过 Bob 为自己设置的 unlock time
- Alice 尝试 claim,但失败
- Bob 调用 retake() 把 BobToken 原路退回到自己账户
- Alice 调用 retake() 把 AliceToken 原路退回到自己账户
import time
import secrets
print("=== Initial Balances ===")
print("Alice AliceToken:", AliceToken.functions.balanceOf(Alice).call())
print("Bob BobToken:", BobToken.functions.balanceOf(Bob).call())
print(f"Alice locked {amountA} AliceToken for Bob on chain A")
secret = secrets.token_bytes(32)
hashValue = w3.keccak(secret)
amountA = 10000
unlockA = int(time.time()) + 1 # 超时设置为1秒后
AliceToken.functions.approve(AliceHTLC.address, amountA).transact({"from": Alice})
AliceHTLC.functions.lock(hashValue, unlockA, amountA, AliceToken.address, Bob).transact({"from": Alice})
print(f"Bob locked {amountB} BobToken for Alice on chain B")
amountB = 10000
unlockB = unlockA - 1 # Bob 的时间更短
BobToken.functions.approve(BobHTLC.address, amountB).transact({"from": Bob})
BobHTLC.functions.lock(hashValue, unlockB, amountB, BobToken.address, Alice).transact({"from": Bob})
print("=== During Transaction Balances ===")
print("Alice AliceToken:", AliceToken.functions.balanceOf(Alice).call())
print("Bob BobToken:", BobToken.functions.balanceOf(Bob).call())
print("Waiting for the lock to expire on chain A")
time.sleep(2)
try:
BobHTLC.functions.claim(secret).transact({"from": Alice})
print("Alice claimed Bob's BobToken on chain B (secret revealed)")
except Exception as e:
print("Alice failed to claim Bob's BobToken on chain B", e)
# Bob 开始退款
print("Bob calls retake() ...")
BobHTLC.functions.retake(hashValue).transact({"from": Bob})
print("Bob retook his BobToken from chain B!")
# Alice 尝试退款
print("Alice calls retake() ...")
AliceHTLC.functions.retake(hashValue).transact({"from": Alice})
print("Alice retook his AliceToken from chain B!")
print("=== Final Balances ===")
print("Alice AliceToken:", AliceToken.functions.balanceOf(Alice).call())
print("Bob BobToken:", BobToken.functions.balanceOf(Bob).call())
=== Initial Balances ===
Alice AliceToken: 99999999999999998766
Bob BobToken: 99999999999999996000
Alice locked 1234 AliceToken for Bob on chain A
Bob locked 4000 BobToken for Alice on chain B
=== During Transaction Balances ===
Alice AliceToken: 99999999999999988766
Bob BobToken: 99999999999999986000
Waiting for the lock to expire on chain A
Alice failed to claim Bob's BobToken on chain B execution reverted: HTLC: can only claim before the unlock time
Bob calls retake() ...
Bob retook his BobToken from chain B!
Alice calls retake() ...
Alice retook his AliceToken from chain B!
=== Final Balances ===
Alice AliceToken: 99999999999999998766
Bob BobToken: 99999999999999996000
为什么钱可以原路退回?
HTLC 设计的数学安全性保证:
- “Claim” 必须在 未过期 且提供正确的 preimage(S)
- “Retake” 必须在 过期后由 sender 执行
- 两者互斥,不可能同时发生
所以:
- 如果对方不响应,永远不会出现资金冻结问题
- HTLC 的超时机制保证你的钱永远不会被困住
下一部分简单的介绍一下HTLC的深层原理
区块链跨链原子交换与合约安全:从 HTLC 到合约互调的本质
在不同区块链之间安全交换资产,是加密世界的一个核心挑战。传统方案需要中心化的托管方,但这会带来信任问题。为了解决这一点,密码学提出了一个优美的方案:原子交换(Atomic Swap)。
下面从 HTLC 的原理讲起,解释原子交换的流程,再深入到很多开发者容易误解的点:
为什么合约可以“任意调用”其他合约?合约调用是否需要私钥?会不会被恶意调用?这些问题的答案都指向 EVM 的设计哲学。
1. 原子交换的核心:HTLC
HTLC(Hash Time-Locked Contract)是一种保证“要么双方都成功、要么都失败”的跨链交易机制。
它包含两个关键结构:
✅ 哈希锁:确保只有掌握秘密 S 的人能领取资金
Alice 随机生成一个秘密 S,并计算哈希 H = hash(S)。
HTLC 中资金的领取条件是:
只要你能提供 preimage S,使得 hash(S) == H,就可以把钱领走。
✅ 时间锁:确保对方不执行时自己的钱能退回
锁中设置一个:
unlockTime
超过时间就原路返还给发送方。
📌 简化后的跨链原子交换流程
- Alice 生成秘密 S,并计算哈希 H。
- Alice 在链 A 部署 HTLC_A,锁定 AliceToken,哈希为 H,受益人是 Bob。
- Bob 验证锁金成功。
- Bob 在链 B 部署 HTLC_B,锁定 BobToken,哈希同样为 H,受益人是 Alice。
- Alice 领取 Bob 的 BobToken,需要提交 S → S 上链被公开。
- Bob 得到 S,用 S 领取 Alice 的 AliceToken。
- 双方成功交换。
关键点:
Bob 在领取之前不需要知道 S,他只需要知道 H(一个公开值)。
2. “Bob 如何看到 S?”
当 Alice 在 Bob 的链上 claim(S) 时,交易 calldata 会直接把 S 放到链上,所有节点(包括 Bob)都能看到。
在 EVM 中,这非常简单:
- 每笔交易的 calldata 是公开的
- 任何人都可以从链上事件或交易输入读取 preimage
因此 Bob 完全可以自动地监听合约事件,当 Alice 领取资金时立刻得到 S。
3. 为什么 HTLC 合约能替 Alice 或 Bob 转账?
在 ERC20 中,资产属于某个地址。智能合约要想转走某人的代币,需要:
- 这个人先
approve(HTLC_contract, amount) - 合约内部才能调用:
erc20.transferFrom(user, address(this), amount)
之后资金处于 HTLC 合约名下。
当满足条件后,HTLC 再调用:
erc20.transfer(receiver, amount)
注意:合约不需要私钥,因为它调用的是 ERC20 合约的 “函数”,不是签名交易。
只有外部账户(EOA)需要签名,合约不需要。
4. 合约为什么可以随意调用其他合约?
这是新手最容易误解的点。
在 EVM 中:
任何合约都可以调用任何合约,不需要权限,也不需要私钥。
比如:
IToken(tokenAddress).transfer(...);
但重点在于:
🟥 合约可以 call,但无法绕过对方的访问控制
如果对方合约这样写:
require(msg.sender == owner);
那黑客合约调用 10 万次也没用。
🟩 这就像给别人发 API 请求
你可以发请求,但服务器完全可以拒绝。
5. 那能否随意写个恶意合约破坏别人?
答案是:
❌ 不能随意破坏
✔ 能利用对方合约本身的漏洞攻击
智能合约攻击永远来自目标合约自己的漏洞,如:
- 重入攻击
- delegatecall 漏洞
- 整数溢出
- 错误的权限检查
- 不安全的 approve 逻辑
攻击者无法直接绕过安全检查,只能利用受害合约内部的错误路径。
6. 小结:区块链设计的安全哲学
HTLC 展示了密码学在跨链信任问题上的优雅解决方案:
用哈希锁 + 时间锁实现原子性。
ERC20 和 EVM 的设计同样体现了简单但稳固的安全原则:
- 合约可以自由调用,但不能越权
- 合约能转账,是因为用户授权过
- 攻击永远来自目标合约的 bug,而不是黑客的“无敌权限”
只要合约本身没有漏洞,EVM 的安全边界是非常牢固的。
852

被折叠的 条评论
为什么被折叠?



