Perpetual Protocol 如何透過 Meta Transaction 銜接 Layer 2 的 xDai 網路

Perpetual Protocol 最近決定採用 xDai Network 作為目前 Layer 2 的解決方案,xDai 有許多優點讓我們可以在比較少的開發成本下就可以轉移到 Layer 2 的網路。本篇文章將會從網頁前端的觀點講解一些技術細節如 Meta Transaction, EIP712 結構化簽名, Layer 2 Deposit/Withdraw 流程以及整合途中遇到的技術難題。

如果對為什麼 Perpetual Protocol 選擇 xDai Network 可以閱讀下面這篇文章。

簡介

採用 Layer 2 方案主要的目的是希望系統比較不要受到 Ethereum 浮動交易費影響導致系統運作價格昂貴,而在智能合約的層級採用 Layer 2 後,從網頁前端的角度來看要達成的目標則是使用者不需要改變太多操作習慣就可以使用 Perpetual Protocol。

在 Layer 2 解決方案中我們最不希望看到的就是使用者會需要在 Ethereum 跟 xDai 之間透過變更 RPC Endpoint URL 的方式切換不同的網路,因為切換的時候需要輸入 RPC Endpoint 跟其他相關資料太過於麻煩瑣碎,而且使用者也容易搞混自己在哪個網路。

整個 Perpetual Protocol 簡易的架構圖如下圖所示,使用者在 Layer 1 Ethereum 中可以透過 Bridge 將原本在 Ethereum 的 ERC20 token 轉換成在 Layer 2 xDai 的 ERC677 token (相容於 ERC20 但加上了一些新功能)。

把代幣從 Layer 1 Deposit 到 Layer 2 之後,接下來我們就採用 Meta Transaction 跟 Layer 2 的智能合約互動,在這樣的設計下使用者不需要在 MetaMask 中把自己的網路切換到 xDai 的 Endpoint 就可以使用 Perpetual Protocol 的所有功能。

完成這樣的設置所需要了解的技術有以下幾個:

  • Meta Transaction
  • EIP-712: 結構化資料簽名
  • 銜接 Layer 1/Layer 2 的 Deposit/Withdraw 流程

接下來就一一介紹。

Meta Transaction

Meta Transaction 是一種代替使用者遞送交易的方式,通常如果 dapp 想要幫使用者代付手續費的時候就會採用 Meta Transaction。平常送交易的時候我們都是在 MetaMask 對整個交易進行簽名之後送出。而 Meta Transaction 其實就是把整個交易簽名後但不自己送出,而是把交易的所執行的函式、參數以及簽名送給一個 Relayer,由 Relayer 幫忙真的送出交易。

舉個例子,比如說有一個合約 TokenSwap 可以用 USDC 交換 DAI token,提供了一個函式如下:

function swap(uint amountUsdcIn)

如果透過 MetaMask 操作時會跳出一個對話視窗確認交易內容,按下送出後則會廣播出去。

簽名所需要的資料包含了私鑰、執行函式所需的參數外,還包含了付 gas 所需的資訊,簽名完後就會廣播到網路上去,當節點完成你的交易時也會向你收取你承諾的手續費。

左側最後送出的結果會在右側圖表的 Data from user 整包傳入

Meta Transaction 則是會讓 User 針對要執行的函式與參數進行簽名,再把簽名、函式與參數全部一起傳給 Relayer,通常 Relayer 會是一個 web API service 來接收這些資訊。

這個 web service 收到後,就會再發出一個真正的 Transaction 到區塊鏈上面。這樣的架構就可以讓 Relayer 代付 gas 費用的狀況下送出交易。

同時如上圖所示,如果要讓合約支援 Meta Transaction 就會需要修改傳入參數,額外加入一個簽名欄位才可以在合約內驗證簽名是否無誤,所以原本的 swap 就會變成類似這樣的修改。

function swap(uint amountUsdcIn, signature bytes)
// 實際上會需要新增三個欄位 r, s, v,這邊講解原理方便就不特別寫出了

那 Meta Transaction 這樣的機制要怎麼使用在 Layer 1/Layer 2 的跨鏈 dapp 上呢?主要就是不管是在哪個網路的區塊鏈上,使用者都可以用同一個 Private Key 來簽名不同網路的交易,既然我們所有在 Layer 2 的操作都可以透過 Meta Transaction 的方式來完成,所以我們就可以在保持 MetaMask 連接在 Layer 1 Ethereum 的狀態下,直接對一個訊息簽名,而這個簽名與要執行的操作打包起來的資料會透過 Relayer 送到 Layer 2 的 xDai 網路,這樣就可以在不切換網路的狀況來完成所有操作了。

EIP-712 結構化資料簽名

一般如果用 eth_sign 在 MetaMask 簽名的時候,通常你簽的訊息都會是一串的 hex 數字。

如果改用 personal_sign 時會少掉紅色的警語,不過訊息還是一樣採用 Hex 表示,這樣會讓使用者不知道他到底簽了怎樣的訊息。EIP-712 就是一個規範在簽名的時候,客戶端如 MetaMask 會顯示即將要簽名的訊息如下圖所示。

同時還會檢查一些基礎資訊,在 Client 端如 MetaMask 會檢查 ChainID 是否正確,送到合約時還會在額外檢查傳入的參數是否正確。

xDai 提供的 bridge 可以將 Ethereum 的 ERC20 token 轉換成在 xDai 的 ERC677 token。除此之外,在 xDAI 上面發出來的 ERC677 token 也全部都有支援 Meta Transaction 的 permit(),同時這個函式也只接受符合 EIP712 的結構化簽名訊息。

由於 EIP712 有助於使用者更透明的理解簽名訊息,Perpetual Protocol 在進行 Layer 2 支援時,也新增一個合約 MetaTxGateway 實作相容於 EIP712 的 Meta Transaction 支援。

Deposit/Withdraw 流程

瞭解了 Meta Transaction 跟 EIP712,接下來就可以來看 Layer 1 與 Layer 2 之間的 Deposit 與 Withdraw 流程是怎麼運作的了。

首先是從 Layer 1 到 Layer 2 的 Deposit 流程,總共要經歷四步流程:

USDT.approve()

假設我們要 Deposit 的 Token 是 USDT,此時使用者需要先呼叫 USDT.approve()允許位於 Layer 1 的 RootBridge 可以使用 MetaMask 使用者的 USDT。

RootBridge.erc20Transfer()

呼叫 rootBridge.erc20Transfer() 時 User 的 USDT 將會被鎖定在由 xDai 提供的 Multi-Token Mediator 裡面,執行完畢後可以在這個 Transaction 裡面取得 UserRequestForAffirmation 事件訊息並且取得包含在其中的 messageId

取得 messageId 後,透過監聽 Layer 2 xDai 官方所提供的 Arbitrary Message Bridge (AMB) 的事件,當出現 AffirmationCompleted 事件訊息,並且其中的 messageId 符合前面我們在 Layer 1 所取得的 messageId 時,這個時候我們就可以確認 Layer 1 的 token 已經順利轉移到 Layer 2 了,此時如果用 xUSDT 的 balanceOf 查詢同一個地址的帳戶時會發現餘額會增加為加上這次 deposit 的數量。

xUSDT.permit()

剛才有提過 xDAI 在 bridge 後的 token 都會實作支援 EIP712 的 permit(),因為我們會需要調用使用者的 token 使用在 Perpetual Protocol 中,所以最後會需要使用 permit() 完成這件事情,並且因為已經到了 layer 2 xDai 網路了,所以從這邊開始使用者僅須透過 MetaMask 簽名即可,不需要再自己送出 Transaction,同時也不需要再付手續費了,目前的規劃是由 Perpetual Protocol 吸收 xDai 上面的交易手續費。

Withdraw 則會與 Deposit 相反方向,監聽的事件與合約不同,但基本上就都大同小異。

技術難題

當我們在評估解決方案的時候,我們在雛型驗證的階段還算滿順利的,當確定 xDai 在智能合約可以完成我們的需求時就開始進入到實作階段。

但萬萬沒想到的是沒測試到 MetaMask。前面有提到 xDai 的 permit() 支援了 EIP712,在執行時在合約中會檢查 chainId 是否符合當下的網路,這個數值在 Layer 1 是 1 (homestead),在 Layer 2 是 100 (xdai)。

我們第一次測試的時候採用了 ChainId 100,此時 MetaMask 報錯說要簽署的訊息當中的 ChainId 不符合當前網路也就是 1。當我們改成 ChainId 1 的時候,xDai 提供的合約則報錯說不符合當前網路的 ChainId 100。

此時我們就陷入了一個死結,要不是 MetaMask 報錯不然就是 xDai 的合約報錯。執行我們自己合約的 MetaTxGateway 是我們自己寫的,所以我們可以讓不論是 1 或 100 都可以執行成功,但 xDai 上面的 token 是由 xDai 組織所佈署,要修改讓執行 permit() 時同時接受兩種 ChainId 就不在我們的控制範圍內。

還好最終我們找到了一個還行的解決方案:執行 permit() 時我們可以自行組合出 EIP712 所需的格式,再透過最基礎的 eth_sign 來簽署訊息,因為 MetaMask 透過 eth_sign 簽名時不知道裡面是 EIP712 的內容,所以也就不會執行 ChainId 的檢查了。唯一的缺點就是執行時會是一個 hex 格式的訊息,並且有一個明顯的警示訊息。

不過這個問題其實就在於正個行業正在快速的發展中,EIP712 當時在制定的時候可能沒有考慮跨鏈的情境,而 xDai 在佈署 bridged token 時也沒考慮到 Meta Transaction 會來自不同 ChainId 的網路,而在這個所有事情都在高速進行的區塊鏈產業中,唯一在執行 permit() 時才會有警示訊息已經是可以讓我安心睡一覺的結果。

後記:當我們在實驗 eth_sign 是否可以簽署一個 EIP712 訊息時其實是失敗的,最後我們發現 ethers.js 會將 eth_sign RPC 擅自改成 personal_sign,在這個行業的基礎建設上只能說到處都是陷阱呢。

參考資訊

旅行、咖啡、科技宅