異地情侶如何安全地傳遞情書 — 哈希時間鎖定機制剖析

在探索學習區塊鏈擴容方面的技術時,瞭解到跨鏈是區塊鏈二層擴容的重要部分,而實現跨鏈的技術主要有:公證人技術、中繼/側鏈技術、哈希時間鎖定技術。接下來,我們將在這篇文章中詳細介紹哈希時間鎖定技術的原理及實現等。

故事

從前有一對分隔異地的情侶,他們用寫信互訴衷腸,不過他們擔心郵遞員會偷窺信中的情話。男主想到了一個好點子,他讓郵遞員將一個盒子投遞給女主,盒子裏放着一把打開的鎖,女主心領神會,寫好書信,放入盒中,用那把鎖鎖住盒子。男主收到盒子後,用唯一的鑰匙打開了盒子,讀懂了女主的心情。他們完成了安全的書信聯繫。這就是下文探討的哈希時間鎖定的基本原理。

概述

哈希時間鎖定(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 了。這也破壞了該筆交易的原子性。

圖 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徐進
更多精彩洞見,請關注微信公衆號:ThoughtWorks洞見

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章