web3的跨链(HTLC原子交换 )python实现

哈希时间锁合约(HTLC)

哈希时间锁合约(Hashed Timelock Contract,HTLC)是一种智能合约,它允许将价值(如代币)锁定一段固定时间(或一定数量的区块)。在价值被锁定的期间,只有提供正确的密钥(哈希原像)时,才能将其转移给指定的接收方。这确保了价值的转移仅在特定条件下发生,即只有代币的所有者选择揭示密钥时才会发生。本文将展示在 Solidity 中实现 HTLC 的一种可能方式。

跨链原子交换

HTLC 的一个典型应用场景是(原子性地)在一条区块链上交付一种资产,以换取在另一条区块链上支付另一种资产。假设Alice希望在链 A 上向Bob发送某种代币(代币 1),而Bob希望使用链 B 上的某种代币(代币 2)向Alice支付。双方已在线下就交易条款达成一致。“顺利路径”(即双方在时间窗口结束前均未退出交易)的流程如下:

  1. Alice生成一个随机的(密钥)原像,对其进行哈希处理,并使用该哈希值在链 A 上锁定她的代币。只有Bob在锁定时间结束前使用正确的哈希原像才能认领该代币。
  2. 哈希值现在通过链 A 上的 HTLC 对Bob可见,因此他使用该哈希值在链 B 上的 HTLC 中锁定他的代币。
  3. Alice随后可以使用她的原像认领链 B 上锁定的代币。
  4. 由于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

模拟交易超时情况

流程:

  1. Alice 生成哈希 H,设置了很短的 unlock time
  2. Bob 使用 hash(H) 在链 B 上锁定 BobToken
  3. Bob 等待一段时间,但 Alice 从未使用 preimage(S) 来 claim
  4. 时间超过 Bob 为自己设置的 unlock time
  5. Alice 尝试 claim,但失败
  6. Bob 调用 retake() 把 BobToken 原路退回到自己账户
  7. 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

超过时间就原路返还给发送方。

📌 简化后的跨链原子交换流程

  1. Alice 生成秘密 S,并计算哈希 H。
  2. Alice 在链 A 部署 HTLC_A,锁定 AliceToken,哈希为 H,受益人是 Bob。
  3. Bob 验证锁金成功。
  4. Bob 在链 B 部署 HTLC_B,锁定 BobToken,哈希同样为 H,受益人是 Alice。
  5. Alice 领取 Bob 的 BobToken,需要提交 S → S 上链被公开。
  6. Bob 得到 S,用 S 领取 Alice 的 AliceToken。
  7. 双方成功交换。

关键点:

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 的安全边界是非常牢固的。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值