29

异地情侣如何安全地传递情书 — 哈希时间锁定机制剖析

 4 years ago
source link: https://insights.thoughtworks.cn/hashed-timelock-agreements/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在探索学习区块链扩容方面的技术时,了解到跨链是区块链二层扩容的重要部分,而实现跨链的技术主要有:公证人技术、中继/侧链技术、哈希时间锁定技术。接下来,我们将在这篇文章中详细介绍哈希时间锁定技术的原理及实现等。

故事

从前有一对分隔异地的情侣,他们用写信互诉衷肠,不过他们担心邮递员会偷窥信中的情话。男主想到了一个好点子,他让邮递员将一个盒子投递给女主,盒子里放着一把打开的锁,女主心领神会,写好书信,放入盒中,用那把锁锁住盒子。男主收到盒子后,用唯一的钥匙打开了盒子,读懂了女主的心情。他们完成了安全的书信联系。这就是下文探讨的哈希时间锁定的基本原理。

概述

哈希时间锁定(Hash Time Lock Contract),也被称为哈希锁定,其本质是一种智能合约。这一概念最早出现在 2013 年 BitcoinTalk 论坛里的一次讨论中,最早在比特币的闪电网络中得到应用和实现,且该机制来源于 Atomic Swap

在闪电网络的支付通道中,它是通过哈希时间锁定智能合约来实现的。也就是限时转账,通过该智能合约,双方约定转账方先冻结一笔钱,并 通过哈希锁将发起方的交易代币锁定 ,如果 在规定时间内 有人能够提供之前生成支付的加密证明,并且与之前约定的哈希值一致,交易即可完成。

可见在哈希时间锁定机制中包含有 哈希锁时间锁 两把锁,通过这两把锁的巧妙配合来保证区块链上交易的原子性,即只有满足一定的时间条件和哈希条件才能达成该交易,否则就什么也不会发生。

那么拆开来看,何为哈希锁以及时间锁呢?

哈希锁

所谓哈希锁,即通过哈希值对一次交易加锁,该锁只能由这个哈希值的原值进行解锁。例如:字符串 “key” 经过哈希函数求值之后得到的值为 “1se@& #^”,那么通过 “1se@& #^” 加锁后的交易,在不考虑哈希碰撞的情况下,就只能由原值 “key” 进行解锁。

时间锁

所谓时间锁,即在规定的时间内才可以开锁。例如:通过时间锁规定开锁的有效时间为 1 个小时,开锁的条件为输入正确的哈希值的原值。那么要想解锁这个时间锁的唯一条件就是在 1 个小时内输入正确的哈希值原值,若在 1 个小时后进行解锁,尽管哈希值输入正确了,该时间锁仍然不会被解锁。

接收方只能在规定时间内凭借哈希值的原值来解锁这次交易,在这段时间内,尽管发送方知道哈希值的原值,但他仍然无权解锁,这样就限制了发送方在给接收方共享了秘钥后自己提前退款这样的作恶行为。

同样的,若在有效时间内,接收方没有用哈希值原值进行解锁,那么在有效时间过了之后,尽管接收方得到了哈希值原值,他也不可能解锁成功,因为超时后只能由发送方解锁这边交易了,即这笔资产会退回到发送方账户中。

通过哈希锁和时间锁的巧妙配合,就可以对资产的发送方和接收方形成相互制约,同时保证资产的交换要么发生,要么不发生,最终保证了该笔交易的原子性。

解决了什么问题

通过传统的中心化交易所进行资产的交易时,通常我们需要先将资产交给交易所,再由交易所进行撮合,最终促成交易的达成。但由于这样的交易所通常是中心化的交易所,因此必然会存在对交易所的信任问题,这就带来了一定的交易风险,还会产生较高的手续费。

而通过哈希时间锁定机制进行资产交易时,可以通过哈希锁和时间锁的双重保障,对资产的发送方和接收方都形成制约,从而促进交易的发生。若双方按照哈希时间锁的规则进行交易,则交易就可达成;若交易失败,实际上在区块链上并未发生任何的资产交换,也就无需支付额外的交易费用了。因此, 通过哈希时间锁定机制可以有效保证跨链交易的原子性,而不需第三方公证人进行信任担保。

单向哈希时间锁定

所谓单向哈希时间锁定,指的是资产的发送方随机生成一个秘钥 x,并通过一个哈希函数 h() 得到 x 对应的哈希值 h(x),然后构建一个智能合约,并在合约中规定,只有资产接收方用 x 来解锁该合约才可以得到合约中锁定的资产,再设定一个超时时间,并规定只有在该超时时间内接收方通过秘钥 x 来解锁才有效,若超过超时时间,只能由资产的发送方才能解锁并退回资产。

举个例子,就好像我在一栋大楼的保险箱里放了一些资产,我见到你之后把那个保险箱的钥匙给你,并告诉你:你只有在 1 个小时之内去某个位置找到这个保险箱,就可以解锁取走这些资产,否则 1 个小时之后,尽管你有钥匙,也无法取走保险箱里的资产,那时只有我有权限解锁该保险箱取走该资产了。

接下来,我们就以以太坊中的智能合约语言 Solidity 为例,看一下哈希时间锁定的具体实现:

首先我们需要对哈希时间锁定合约进行定义,我们需要设计该合约的 数据结构 ,以及其对应的 3 个最主要的方法,分别为: 合约的构建取出资产退回资产

contract HashedTimelock {
    ...
    struct LockContract {
        address payable sender;
        address payable receiver;
        uint amount;
        bytes32 hashlock; // use sha256 hash function
        uint timelock; // UNIX timestamp seconds - locked UNTIL this time
        bool withdraw;
        bool refunded;
        bytes32 preimage; // it's secret, sha256(_preimage) should equal to hashlock
    }
    ...

    function newContract (address payable _receiver, bytes32 _hashlock, uint _timelock) ... {
            ...
    }

    function withdraw(bytes32 _contractId, bytes32 _preimage) ... {
        ... 
    } 

    function refund(bytes32 _contractId) ... {
        ... 
    }
}

可以看到在 LockContract 的定义中,我们规定了该笔合约的发送方(sender)和接收方(receiver),即该笔合约中锁定的资产只能被发送方或接收方取出;接下来定义了哈希锁(hashlock)和时间锁(timelock),哈希锁对应的秘钥为 preimage,这里我们若采用 sha256 的加密方式,那么 sha256(preimage) 就一定等于 hashlock;除此之外我们还定义了该合约中锁定的资产数量 amount,以及该合约对应的状态,即是否被取出(withdraw)、是否被退回(refunded)。

contract HashedTimelock {
    ...
    modifier fundsSent() {
        require(msg.value > 0, "msg.value must be > 0");
        _;
    }
    modifier futureTimelock(uint _time) {
                // 唯一的要求就是 timelock 时间锁指定的时间要在最后一个区块产生的时间(now)之后
        require(_time > now, "timelock time must be in the future");
        _;
    }

    function newContract(address payable _receiver, bytes32 _hashlock, uint _timelock)
        external payable fundsSent futureTimelock(_timelock) returns (bytes32 contractId)
    {
        contractId = sha256(abi.encodePacked(msg.sender, _receiver, msg.value, _hashlock, _timelock));

                // 若具有相同参数的合约已经存在,这次新建合约的请求就会被拒绝。
                // sender 只有更改其中的一个参数,以创建一个不同的合约。
        if (haveContract(contractId))
            revert();

        contracts[contractId] = LockContract( ... );
    }
    ...
    function haveContract(bytes32 _contractId)
        internal
        view
        returns (bool exists)
    {
        exists = (contracts[_contractId].sender != address(0));
    }
}

新建哈希时间锁定合约方法(newContract) 中,我们先根据该合约的发送方、接收方、资产总量、哈希锁和时间锁这些参数生成一个合约 ID(contractId),且规定不同构建相同的合约。同时,约定构建合约时必须锁定资产(modifier fundsSent),且传入有效的时间锁,即超时时间大于当前时间(modifier futureTimelock)。之后就通过构造方法生成了 LockContract,并返回 contractId。通过该方法就构建了一笔智能合约,且该合约的地址为 contractId。

contract HashedTimelock {
    ...
    modifier contractExists(bytes32 _contractId) {
        require(haveContract(_contractId), "contractId does not exist");
        _;
    }
    modifier hashlockMatches(bytes32 _contractId, bytes32 _x) {
        require(
            contracts[_contractId].hashlock == sha256(abi.encodePacked(_x)),
            "hashlock hash does not match"
        );
        _;
    }
    modifier withdrawable(bytes32 _contractId) {
        require(contracts[_contractId].receiver == msg.sender, "withdrawable: not receiver");
        require(contracts[_contractId].withdrawn == false, "withdrawable: already withdrawn");
        require(contracts[_contractId].timelock > now, "withdrawable: timelock time must be in the future");
        _;
    }    

    function withdraw(bytes32 _contractId, bytes32 _preimage)
        external contractExists(_contractId) hashlockMatches(_contractId, _preimage) withdrawable(_contractId) returns (bool)
    {
        LockContract storage c = contracts[_contractId];
        c.preimage = _preimage;
        c.withdrawn = true;
        c.receiver.transfer(c.amount);
        return true;
    }
    ...
}

取出资产方法(withdraw) 中,资产接收方通过合约地址(contractId)和哈希锁秘钥(preimage)调用 withdraw 方法,若能通过验证,合约就会将资产转移到资产接收方对应的账户中。该合约首先会验证是否传入有效的合约地址(modifier contractExists),并判断接收方所持有的哈希锁秘钥是否为真(modifier hashlockMatches),同时还会验证当前合约的资产是否可被取出(modifier withdrawable),即:调用 withdraw 方法的是否为合约中定义的资产接收方、该合约中的资产是否还未被取出,以及接收方是否在超时时间内调用的该方法。

contract HashedTimelock {
    ...
    modifier refundable(bytes32 _contractId) {
        require(contracts[_contractId].sender == msg.sender, "refundable: not sender");
        require(contracts[_contractId].refunded == false, "refundable: already refunded");
        require(contracts[_contractId].withdrawn == false, "refundable: already withdrawn");
        require(contracts[_contractId].timelock < = now, "refundable: timelock not yet passed");
        _;
    }

    function refund(bytes32 _contractId)
        external contractExists(_contractId) refundable(_contractId) returns (bool)
    {
        LockContract storage c = contracts[_contractId];
        c.refunded = true;
        c.sender.transfer(c.amount);
        return true;
    }
    ...
}

退回资产方法(refund) 中,资产发送方通过合约地址调用该方法,若通过合约验证,资产就会被退回到资产发送方的账户中。该合约首先会验证是否传入了有效的合约地址(modifier contractExists)。接着验证该合约是否允许退回资产,即:调用 refund 方法的账户是否为合约中规定的资产发送方、该合约中锁定的资产是否还未被取出和退回,以及是否确实超出了合约中规定的超时时间。

节点时间同步机制

这里可能会有一些疑惑。因为我们在设置时间锁的时候,采用的是系统本地绝对时间加上时间间隔的毫秒来表示,那么会不会出现不同结点的本地时间不同而导致时间锁的不一致呢?其实是不会的,以以太坊为例,在节点加入网络同步区块数据的时候,系统就对它进行了时间上的校验,如果节点的本地时间和以太坊网络中的时间不一致,就会导致节点区块数据同步失败。我们可以理解为所有以太坊网络中的节点时间都是一致的,因此这个问题也就不存在了。

到这里对于哈希时间锁定合约的定义就基本完成了,那么该如何使用它呢,我们以两个简单的测试来分别说明取出资产(withdraw)和退回资产(refund)方法的使用。

it('withdraw() should send receiver funds when given the correct secret preimage', async () => {
    const hashPair = newSecretHashPair()
    const htlc = await HashedTimelock.deployed()
    const newContractTx = await htlc.newContract( ... )

    const contractId = txContractId(newContractTx)
    const receiverBalBefore = await getBalance(receiver)

    // receiver calls withdraw with the secret to get the funds
    const withdrawTx = await htlc.withdraw(contractId, hashPair.secret, {
      from: receiver,
    })
    const tx = await web3.eth.getTransaction(withdrawTx.tx)

    // Check contract funds are now at the receiver address
    const expectedBal = receiverBalBefore
      .add(oneFinney)
      .sub(txGas(withdrawTx, tx.gasPrice))

    assertEqualBN(
      await getBalance(receiver),
      expectedBal,
      "receiver balance doesn't match"
    )
    const contractArr = await htlc.getContract.call(contractId)
    const contract = htlcArrayToObj(contractArr)
    assert.isTrue(contract.withdrawn) // withdrawn successful
    assert.isFalse(contract.refunded) // still not refund 
    assert.equal(contract.preimage, hashPair.secret)
  })

通读完了 取出资产的测试 之后,可以看到我们是先构建了一个哈希时间锁定合约(newContractTx),接下来由资产接收方(receiver)调用了取出资产方法(withdraw),待交易执行成功后,我们就可以验证资产接收方的余额(需减去该笔交易的手续费)是否正确,以及合约的状态是否正确(已取出、未退回)。

it('refund() should pass after timelock expiry', async () => {
    const hashPair = newSecretHashPair()
    const htlc = await HashedTimelock.new()
    const timelock1Second = nowSeconds() + 1

    const newContractTx = await htlc.newContract( ... )
    const contractId = txContractId(newContractTx)

    // wait one second so we move past the timelock time
    return new Promise((resolve, reject) =>
      setTimeout(async () => {
        try {
          const balBefore = await getBalance(sender)
          const refundTx = await htlc.refund(contractId, {from: sender})
          const tx = await web3.eth.getTransaction(refundTx.tx)
          // Check contract funds are now at the senders address
          const expectedBal = balBefore.add(oneFinney).sub(txGas(refundTx, tx.gasPrice))
          assertEqualBN(
            await getBalance(sender),
            expectedBal,
            "sender balance doesn't match"
          )
          const contract = await htlc.getContract.call(contractId)
          assert.isTrue(contract[6]) // refunded successful
          assert.isFalse(contract[5]) // withdrawn still false
          resolve()
        } catch (err) {
          reject(err)
        }
      }, 1000)
    )
  })

通读完了 退回资产的测试 之后,可以看到我们给合约中时间锁设置的值为 1s,然后我们在执行合约时,设置了 1s 的等待(setTimeout)之后再执行逻辑,这时就一定过了合约中设置的超时时间了。接下来就由资产的发送方调用 refund 方法,待交易执行成功后,再验证发送方的资产余额,以及该合约的状态是否正确(已退回、未取出)。

对于单项哈希时间锁定合约,发送方将该合约的 地址哈希锁秘钥 都是通过 链下的方式 分享给接收方的。

双向哈希时间锁定

以上我们介绍了单向哈希时间锁定机制,即发送方构建合约,通过将合约地址和秘钥分享给接收方,再由接收方解锁取出资产。但是,单向哈希时间锁定机制的使用是十分受限的,因为在单链上一方向另一方转移资产这件事本身就不需要第三方的参与,直接在链上参与共识达成交易即可。而哈希时间锁定更强大的地方在于双向的哈希时间锁定,即不同用户之间的资产交换,用以去除对第三方中心的信任依赖。而不同用户之间的资产交换,又包括单链资产交换,如:在以太坊上账户 A 以 20 个 ERC20 Token 交换账户 B 的 1 个 ERC721 Token,以及跨链资产交换,如:在以太坊上账户 A 以 10 ETH 和比特币网络中的账户 B 交换 1 BTC。

单链资产交换

我们以以太坊上 ERC20 Token 和 ERC721 Token 之间的交换为例。这时需要先分别定义出针对 ERC20 Token 和 ERC721 Token 的哈希时间锁定合约,它们的定义和 HashedTimelock 合约的定义差别不大,也是主要包含 合约构建取出资产退回资产 三个方法,但是在合约的结构体定义中会有些许不同,因为针对 ERC20 Token 我们需要指出它的 Token 合约地址和 token 总额,而针对 ERC721 Token 来说,我们需要指出它的 Token 合约地址和某一个 token 的 ID 即可。关于它们合约的定义代码,简要概述如下:

contract HashedTimelockERC20 {

        struct LockContract {
            address sender;
            address receiver;
            address tokenContract; // token contract refer to erc20 token
            uint256 amount; // the amount of ecr20 token
            bytes32 hashlock;
            uint256 timelock; 
            bool withdrawn;
            bool refunded;
            bytes32 preimage;
        }

        function newContract(
            address _receiver,
            bytes32 _hashlock,
            uint256 _timelock,
            address _tokenContract,
            uint256 _amount
        ) ... {}

        function withdraw(bytes32 _contractId, bytes32 _preimage) ... {}

        function refund(bytes32 _contractId) ... {}
    }

    contract HashedTimelockERC721 {
        struct LockContract {
            address sender;
            address receiver;
            address tokenContract; // token contract refer to erc721 token
            uint256 tokenId; // token id of ecr721 token
            bytes32 hashlock;
            uint256 timelock; 
            bool withdrawn;
            bool refunded;
            bytes32 preimage;
        }

        function newContract(
            address _receiver,
            bytes32 _hashlock,
            uint256 _timelock,
            address _tokenContract,
            uint256 _tokenId
        ) ... {}

        function withdraw(bytes32 _contractId, bytes32 _preimage) ... {}

        function refund(bytes32 _contractId) ... {}
    }

对于 ERC20 Token 和 ERC721 Token 的哈希时间锁定合约的定义就简要介绍到这儿,关于它们的 newContract、withdraw 和 refund 方法和 HashedTimelock 合约中的方法基本一致,这里就不再赘述。接下来,我们需要解释一下如何使用这两个合约,以 Alice 和 Bod 进行 ECR20 和 ECR721 Token 交换的测试代码进行说明。

before(async () => {
    ....
    // Alice has some tokens to trade
    await CommodityTokens.mint(Alice, 1)
    await CommodityTokens.mint(Alice, 2)

      // Bob has some tokens to make payments
    await PaymentTokens.transfer(Bob, 1000);

    hashPair = newSecretHashPair()
})

it('Step 1: Alice sets up a swap with Bob to transfer the Commodity token #1', async () => {
    timeLock2Sec = nowSeconds() + 2
    const newSwapTx = await newSwap(CommodityTokens, 1, htlcCommodityTokens, {hashlock: hashPair.hash, timelock: timeLock2Sec}, Alice, Bob)
    a2bSwapId = txContractId(newSwapTx)

    // check token balances
    await assertTokenBal(CommodityTokens, Alice, 1, 'Alice has deposited and should have 1 token left');
    await assertTokenBal(CommodityTokens, htlcCommodityTokens.address, 1, 'HTLC should be holding Alice\'s 1 token');
  })

it('Step 2: Bob sets up a swap with Alice in the payment contract', async () => {
    timeLock2Sec = nowSeconds() + 2
    const newSwapTx = await newSwap(PaymentTokens, 50, htlcPaymentTokens, {hashlock: hashPair.hash, timelock: timeLock2Sec}, Bob, Alice)
    b2aSwapId = txContractId(newSwapTx)

    // check token balances
    await assertTokenBal(PaymentTokens, Bob, 950, 'Bob has deposited and should have 950 tokens left')
    await assertTokenBal(PaymentTokens, htlcPaymentTokens.address, 50, 'HTLC should be holding Bob\'s 50 tokens')
})

it('Step 3: Alice as the initiator withdraws from the BobERC721 with the secret', async () => {
    // Alice has the original secret, calls withdraw with the secret to claim the EU tokens
    await htlcPaymentTokens.withdraw(b2aSwapId, hashPair.secret, {
      from: Alice,
    })

    // Check tokens now owned by Alice
    await assertTokenBal(PaymentTokens, Alice, 50, `Alice should now own 50 payment tokens`)

    const contractArr = await htlcPaymentTokens.getContract.call(b2aSwapId)
    const contract = htlcERC20ArrayToObj(contractArr)
    assert.isTrue(contract.withdrawn) // withdrawn set
    assert.isFalse(contract.refunded) // refunded still false
    // with this the secret is out in the open and Bob will have knowledge of it
    assert.equal(contract.preimage, hashPair.secret)

    learnedSecret = contract.preimage
  })

it("Step 4: Bob as the counterparty withdraws from the AliceERC721 with the secret learned from Alice's withdrawal", async () => {
    await htlcCommodityTokens.withdraw(a2bSwapId, learnedSecret, {
      from: Bob,
    })

    // Check tokens now owned by Bob
    await assertTokenBal(CommodityTokens, Bob, 1, `Bob should now own 1 Commodity token`)

    const contractArr = await htlcCommodityTokens.getContract.call(a2bSwapId)
    const contract = htlcERC20ArrayToObj(contractArr)
    assert.isTrue(contract.withdrawn) // withdrawn set
    assert.isFalse(contract.refunded) // refunded still false
    assert.equal(contract.preimage, learnedSecret)
})

const newSwap = async (token, tokenId, htlc, config, initiator, counterparty) => {
    await token.approve(htlc.address, tokenId, {from: initiator})
    return htlc.newContract(counterparty, config.hashlock, config.timelock, token.address, tokenId, { from: initiator })
}

我们先在执行哈希时间锁定之前对网络进行一下初始的设置,包括:部署 HashedTimelockERC721 合约以及 HashedTimelockERC20 合约,同时还需要部署 ECR721TokenContract 以及 ECR20TokenContract 合约,然后再对 Alice 和 Bob 进行账户的初始化,即 Alice 有 2 个 ERC721 Token,Bob 有 1000 个 ERC20 Token。

接下来,在测试中资产的交换过程一共有 4 个步骤组成。

  1. Alice 构建 ERC721 哈希时间锁定合约,并锁定 1 个 ERC721 Token。
  2. Bob 构建 ERC20 哈希时间锁定合约,并锁定 50 个 ERC20 Token。
  3. Alice 用 ERC20 哈希时间锁定合约的秘钥解开 Bob 构建的哈希时间锁定合约,并取出锁定在合约中的 50 个 ERC20 Token。
  4. Bob 用 ERC721 哈希时间锁定的秘钥解开由 Alice 构建的哈希时间锁定合约,并取出锁定在其中的 1 个 ERC721 Token。

在以上单链的资产交换的例子中,我们可能有一些疑问为什么 Alice 和 Bob 会采用相同的秘钥 hash 后作为哈希锁?这样真的安全么。如果 Alice 和 Bob 约定好我们都用 “key” 作为秘钥哈希后作为哈希锁且设置超时时间为 2h,但又如何能保证他们两个在同一时刻构建好这一合约呢?万一 Alice 先构建好了 ERC721 哈希时间锁定合约,这一消息被 Bob 得知后,他就用自己手里的秘钥 “key” 解锁取出了 Alice 锁定的 ERC721 资产,而自己就不再锁定资产了,这样 Alice 就会遭受亏损,因此这样就破坏了该笔交易的原子性了。

那么,如何才能更好的改进这一流程呢,我们以跨链资产交换为例进行说明。

跨链资产交换

如图 1 所示为通过哈希时间锁定机制在以太坊和比特币网络之间实现资产交换的例子。通过观察这个图中的流程,我们能发现和单链资产交换的流程中有两点不同的地方:

  1. 在跨链资产交换中是由 Alice 一方生成秘钥,她通过哈希之后得到了 hashlock,在她将自己的资产锁定之后,将该 hashlock 告知 Bob,此时 Bob 用同样的 hashlock 将自己的资产锁定。这时,由于 Bob 是不知道 hashlock 的秘钥的,也就无法提前取出 Alice 锁定的资产了,这就解决了之前提到的那个问题。
  2. 我们可以发现 Alice 锁定的 TimeLock 的时长要比 Bob 锁定的 TimeLock 时间长。这是由于当 Alice 解锁了 Bob 锁定的资产后,要留足够的时间给 Bob 解锁 Alice 的资产。若他们两个设置了相同的 timelock 时长,很有可能会出现,Alice 解锁了 Bob 的资产后,Bob 拿到秘钥再去解锁 Alice 的资产,却发现 Alice 的 TimeLock 已经超时无效了,这时又只能退回 Alice 了。这也破坏了该笔交易的原子性。

nqeIRnV.png!web

图 1

这时我们可能还会发现另一个问题。在之前的代码示例中,我们知道所谓的解哈希锁,其实就是通过哈希函数求得用户提供的秘钥所对应的哈希值,若这个刚求得的哈希值和合约中保存的 Hashlock 的值是相同的,那么就证明了用户拥有了对应的秘钥,也即解锁成功。在单链中,我们只要调用同一个哈希函数就行,因为同一个秘钥经过同一个哈希函数一定会得到相同的哈希值。而在跨链哈希时间锁定的场景中,我们也要有这样的要求,即 两条链都支持相同的哈希函数 ,比如:sha256 等。因为如果无法找到两条链中相同的哈希函数,那么尽管我们拥有同一个秘钥,经过不同的哈希函数一定会得到不同的值,这样永远也无法解开这把哈希锁了。而在该例子中,我们可以采用以太坊和比特币共同支持的 sha256 哈希函数。

我们之前提到哈希时间锁定的本质其实是智能合约,但在该例子中,我们以比特币和以太坊之间资产交换来举例,而我们都知道比特币是不像以太坊这样支持编写只能合约的,那么在比特币网络中,Alice 如何实现哈希时间锁定呢。

bip-0199 (Bitcoin Improvement Proposals)中提出,比特币网络中,可以通过一系列的脚本操作来实现哈希时间锁定机制。对于资产的发送方,他可以将该笔资产发送到一个 P2SH 账户中,并设置一段比特币的脚本来验证将来接收方提供的秘钥是否正确,并在这笔交易中设置一个 timelock。而接收方同样需要在 timelock 设置的时间内,用自己得到的秘钥来这个 P2SH 账户中转移资产,否则 timelock 超时后,资产同样会退回到发送方的账户里。虽然,比特币没有智能合约系统,但它也可以通过自己的机制实现哈希时间锁定,下面简要介绍一下哈希时间锁定机制中用到的几个比特币的机制:

  1. 比特币脚本系统:在比特币系统中可以运行简单的程序,而要运行这些简单程序时所编写的语言就是比特币脚本。这些脚本是比特币交易能够进行加锁和解锁的基础。比特币脚本是一个功能比较少的编程语言,它能满足比特币系统的正常运行需求,同时最大化保证了安全性。比如:哈希时间锁中就会用到如下形式的比特币脚本:

    OP_IF
        [HASHOP] <digest> OP_EQUALVERIFY OP_DUP OP_HASH160 <buyer pubkey hash>            
    OP_ELSE
        <num> [TIMEOUTOP] OP_DROP OP_DUP OP_HASH160 <refunder pubkey hash>
    OP_ENDIF
    OP_EQUALVERIFY
    OP_CHECKSIG

    这里的 [HASHOP] 是 OP_SHA256OP_HASH160 ,指的是采用什么样的哈希函数对秘钥进行哈希处理。

    [TIMEOUTOP] 是 OP_CHECKSEQUENCEVERIFYOP_CHECKLOCKTIMEVERIFY ,其中 OP_CHECKLOCKTIMEVERIFY 操作码可以让一个输出的币锁定到未来的某个时间之后才可以被花掉,且这里包含 OP_CHECKLOCKTIMEVERIFY 脚本的输出是会被打包并广播的,但若要花这个输出,就需要等待锁定期才行,这样就保证了锁定的资产不可被双花,且锁定期也不可随意更改,这里验证的时间是绝对时间,通常可以有两种表达方式,一种是时间戳,另一种是块高度。

    OP_CHECKSEQUENCEVERIFY 操作码也是用来锁定资产之后,用来检查是否可以解锁时间限制的,只是 OP_CHECKSEQUENCEVERIFY 操作码采用的是相对时间,比如:一年之后资产可被转移。

  2. P2SH 机制:P2SH 是比特币中的一种地址类型。在比特币中常见的地址有两种类型,分别为:Pay-to-PubKeyHash (P2PKH) 和 Pay-to-ScriptHash (P2SH)。这两种不同的地址类型最主要的差别就是 资金的转出条件不同 。P2PKH 地址中的资金如果要转出,是由发送方决定,只需要发送方提供公钥和私钥签名即可,形式比较固定。而 P2SH 中的资金要想转出,是由接收方决定的,转出条件可以很自由的进行设置。具体来讲,转出条件就是要写到一个赎回脚本 (redeem script) 中,P2SH 中的 S 就代表了赎回脚本。

  3. locktime:在比特币转账的每笔交易中,都有一个 locktime 字段。只有当前时间大于或等于 locktime 时间时,这笔转账才可以会被广播和打包,否则节点将会丢弃这样的转账交易。由于当交易的 locktime 不满足条件时不会被广播,也不会被打包,那么如果我们将 locktime 设置在未来的某个时间,那么这笔交易中的资产是可以被提前花费的,之后到了 locktime 指定的时间时,就会因为找不到资产而使该笔交易变得无效。且这里 locktime 的时间是绝对时间,通常可以有两种表达方式,一种是时间戳,另一种是区块高度。

可以看到我们在比特币中,也可以通过它特有的机制实现哈希时间锁定。那么这样,我们就完成了在比特币和以太坊网络中的资产交换,且由于哈希锁和时间锁的巧妙配合,若 Alice 取出 Bob 锁定的 ETH,则这次资产交换中的所有交易就都会发生,若 Alice 没有取出 Bob 锁定的 ETH,则在这次资产交换中所有的交易就都不会发生,这样就很好的保证了资产交换的原子性,同时我们也会发现在这个过程中,只是通过哈希锁和时间锁进行彼此的制约,是没有任何第三方公证人出现的。

简单评估

通过上面的分析,我们可以得出 任何的两条链,只要它们都能基于时间限制或一定的条件限制对资产进行锁定,且这两条链都支持相同的哈希函数,那么我们就可以采用哈希时间锁定机制实现这两条链之间的跨链资产交换

但是,这里我们提到的跨链资产交换具体指的是:Alice 和 Bob 要同时在 A 链和 B 链都拥有账户,且它们达成一直意见,即 Alice 用她在 A 链上的某些资产交换 Bob 在 B 链上的某些资产。本质上这是两笔交易,Alice 在 A 链上将某些资产转移给 Bob 在 A 链上的账户中,同时 Bob 将 B 链上的某些资产转移给 Alice 在 B 链上的账户中。我们可以 通过哈希锁和时间锁相互制约,将这两笔交易进行绑定,从而实现整体资产交换的原子性,因此,我们通常也将哈希时间锁定机制成为原子交换机制 。但是, 哈希时间锁定机制并不支持资产在不同链上的移植,同样不支持跨链预言机 。因此,哈希时间锁定机制在其使用场景上会受到一些限制。

我们再来回想一下在跨链资产交换中的流程,我们能够发现 这次交易能够成功的关键在于 Alice 是否用她所拥有的秘钥解锁了 Bob 锁定的 ETH 。只有 Alice 触发了这笔交易,这次跨链的资产交换才能够成功,否则就什么也不会发生,反而浪费了多笔交易的手续费。

此外,在我们用哈希时间锁定机制进行链间的资产交换时,我们会发现在创建交易时,就已经确定了接收方是谁以及交换的汇率是多少。也就是说我们要自行进行需求的配对,比如一个用户想要用 100 ETH 交换 2 BTC,而只有另一个用户刚好想以 2BTC 交换 100ETH 时,他们的需求才能得到匹配,这时才可以用哈希时间锁定的方式进行资产交换。可见,采用该方法在自行进行需求的撮合时是比较复杂的。

综上分析,哈希时间锁定具有如下优劣势:

优势

  • 较好的保证了跨链交易的 原子性。
  • 从跨链的角度来讲,哈希时间锁定在机制上保证了其 具有较高的安全性
  • 在哈希时间锁定的跨链应用中,交易本身是由各个链独立执行和验证的,且由于哈希锁和时间锁的彼此制约,最终 保证了资产跨链交换中的数据一致性和可验证性
  • 实现相对比较简单 ,只要能够满足哈希锁或时间锁延迟交易执行即可。
  • 通过哈希锁和时间锁的巧妙配合,在 进行链间资产交换时消除了对第三方公证人的信任

劣势

  • 使用场景受限, 只能实现资产交换,但不能实现资产的转移,更不能实现跨链合约的执行。
  • 整体的 交易能否顺利达成会受限于拥有秘钥的一方 ,只有他解锁了对方链上的资产,公布了该秘钥才可以。
  • 需要自行匹配交易需求对手方 ,且只有在彼此双方恰好满足资产交换需求时交易才可以达成,而这一过程通常比较繁琐。
  • 并不适用于所有的区块链之间进行资产交换,它 们必须能够支持相同的哈希函数

总结

要想在不同链之间实现资产交换,通常需要资产交换的双方彼此拥有信任,且能够保证交易的原子性即可。而在哈希时间锁定机制中,交易双方需要提前对资产进行锁定,这一过程就为彼此提供了信任基础;而哈希锁和时间锁的巧妙配合能够同时制约资产交换的双方,从而保证了交易的原子性。

任何两条链只要可以通过 时间一定条件 对链上的资产进行加锁从而延迟交易的执行,且这两条链同时支持同一个哈希函数,那么就可以通过哈希时间锁定机制实现链间资产的交换,可见它相对比较好实现,实施成本较低。

总之,虽然哈希时间锁定的使用场景比较受限,但若只是要实现链间的资产交换,哈希时间锁定可以算是一种性价比较高的方案。

更多精彩洞见,请关注微信公众号:ThoughtWorks洞见


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK