每個人都在讨論無gas以太坊交易,因為沒有人喜歡支付gas費用。但是以太坊網絡能夠精準地運轉恰恰是因為交易需要手續費。那麼如何實作無gas交易呢?讓我們一起學習無gas以太坊交易的魔法!
在這篇文章中,我們将學習如何實作無gas交易模式。你會發現雖然在以太坊上沒有免費的午餐,但可以用有趣的方式來轉移gas成本。利用本文中學到的知識,你的DApp使用者就可以省掉gas,獲得更好的使用者體驗,或者在你的智能合約中建構新穎的代理模式。
不過等一下!還不止這些!為了友善你的使用,我已經将相關工具放到這個
Github倉庫了。是以現在你要實作無gas以太坊交易的門檻已經大大降低了。
現在讓我們開始吧!
用自己熟悉的語言學習 以太坊DApp開發 : Java | Php Python .Net / C# Golang Node.JS Flutter / Dart
1、一些背景知識
我不得不承認,雖然我了解如何在智能合約中實作無gas交易,但是并不太了解背後的密碼學知識。不過對我而言這算不上大的障礙,是以如果你也不太熟悉密碼學,相信也不會影響你實作無gas以太坊交易。
據我所知,我的私鑰被用來簽名發送到以太坊網絡的交易,在這個過程中運用了一些密碼學技術來識别我的身份并存入變量msg.sender,這是以太坊中通路控制的基石。
無gas交易背後的魔法在于,我們可以用自己的私鑰為希望執行的合約交易 制作一個簽名。
簽名是鍊下生成的,無需消耗任何gas。一旦簽名完成,就可以将交易發送給其他人替我們執行,同時也替我們支付gas費用。
使用簽名的合約函數通常就是一個普通的函數,不過支援傳入額外的簽名參數。例如在dai.sol中,我們可以看到如下的approve函數:
function approve(address usr, uint wad) external returns (bool)
同時也可以看到permit函數,它和approve做的事情一樣,隻是支援額外的簽名參數:
function permit(address holder, address spender, uint256 nonce, uint256 expiry, bool allowed, uint8 v, bytes32 r, bytes32 s) external
不用擔心看不懂這些額外的參數,下面會講解。我們需要注意的是,上面這兩個函數是如何處理allowance映射的:
function approve(address usr, uint wad) external returns (bool)
{
allowance[msg.sender][usr] = wad;
…
}
function permit(
address holder, address spender,
uint256 nonce, uint256 expiry, bool allowed,
uint8 v, bytes32 r, bytes32 s
) external {
…
allowance[holder][spender] = wad;
…
}
- 如果你調用approve方法,那麼就意味着允許spender賬号操作不超過wad個你持有的代币。- 如果你把一個有效簽名給了其他人,那麼那個人就可以通過調用permit方法 來允許spender賬号操作不超過wad個你持有的代币。
是不是一樣?
是以基本上來說,無gas交易背後的模式就是制作一個簽名,别人用這個簽名就可以用你的身份安全地執行一個特殊的交易,就像你授權别人執行一個方法。
這其實就是一種代理模式。
2、無gas交易規範
如果你和我一樣,那你可能馬上就會深入研究代碼。我立刻注意到了一個注釋:
// — — EIP712 niceties — -
看起來是一個以太坊規範,是以我就
研究了一下,不過當時并沒有了解。現在我已經了解,并且可以用淺顯的話語來解釋了。
EIP712描述了為合約方法生成簽名的通用方式。其他的EIP則描述如何在特定的用例中運用EIP712。例如
EIP2612描述如何将EIP712簽名用于permit方法,該方法和ERC20代币中的approve方法實作相同的功能,就像我們在前面看到的。
如果你隻是想實作一個已經定義過的簽名方法,比如為你的MetaCoin合約添加支援簽名的approve方法,那麼閱讀EIP2612就夠了。更簡單的辦法就是直接繼承一個已經實作了EIP2612的合約。
在這篇文章中,我們将研究dai.sol中的一種無gas交易實作,這會幫助我們更清晰地了解其内部機制。dai.sol的無gas實作是在EIP2612之前完成的,是以有一些差別。不過這不是大問題。
3、簽名構成
在dai.sol中可以看到EIP712的一個早期實作,它允許dai持有者在鍊下計算簽名并交由spender代為執行approve方法,而不是由dai持有者直接調用approve方法。
整個實作包含4個部分:
- DOMAIN_SEPARATOR
- PERMIT_TYPEHASH
- nonces變量
- permit函數
下面是DOMAIN_SEPARATOR以及相關的變量:
string public constant name = "Dai Stablecoin";
string public constant version = "1";
bytes32 public DOMAIN_SEPARATOR;constructor(uint256 chainId_) public {
...
DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256(
"EIP712Domain(string name,string version," +
"uint256 chainId,address verifyingContract)"
),
keccak256(bytes(name)),
keccak256(bytes(version)),
chainId_,
address(this)
));
}
DOMAIN_SEPARATOR就是一個用來唯一辨別智能合約的哈希,它是利用一個标記EIP712域(合約名稱、版本、鍊ID、部署位址)的字元串構造的。
所有這些資訊在構造函數中進行哈希并存入DOMAIN_SEPARATOR變量,dai持有者在生成簽名時需要使用這個變量值,并且在執行permit方法時需要比對。DOMAIN_SEPARATOR可以確定一個簽名僅對單一合約有效。
下圖是PERMIT_TYPEHASH:

PERMIT_TYPEHASH是函數名(首字母大寫)以及全部參數(包括類型和參數名)的哈希,其目的是清晰界定簽名的适用方法。
在permit方法中需要處理簽名,如果适用的PERMIT_TYPEHASH并不是針對這個方法的,交易就會復原。這樣就確定了一個簽名僅可以用于特定的方法。
下面是nonces映射:
mapping (address => uint) public nonces;
nonces應用用來注冊一個特定的dai持有者已經使用的簽名數量。當建立簽名時,需要包含一個nonces值,當執行permit方法時,nonce必須比對該持有者已經使用的簽名數量。這一措施用來確定簽名僅使用一次。
這三者結合在一起,PERMIT_TYPEHASH、DOMAIN_SEPARATOR以及nonce,就可以確定一個簽名僅可以用于特定的合約、特定的方法,并且隻可以使用一次。
現在讓我們看看在智能合約中是如何處理簽名的。
4、permit方法
permit方法是dai.sol中實作的一個函數,它允許使用簽名來實作approve相同的功能。
// --- Approve by signature ---
function permit(
address holder, address spender,
uint256 nonce, uint256 expiry, bool allowed,
uint8 v, bytes32 r, bytes32 s
) external
正如你看到的,permit方法包含很多參數。這些參數是計算簽名需要的資料,以及簽名資料v、r和s。
傳入建立簽名的參數看起來很傻,但是這是必須的。因為從簽名中能夠恢複出來的隻有簽名建立者的位址。我們需要所有這些參數以及恢複出來的建立者位址來確定簽名的有效性。
首先我們利用這些參數計算一個摘要資料。dai持有者需要在鍊下進行同樣的計算,這是生成簽名的必要環節:
bytes32 digest =
keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(abi.encode(
PERMIT_TYPEHASH,
holder,
spender,
nonce,
expiry,
allowed
))
));
使用ecrecover方法以及v、r和s,我們可以從簽名中恢複出位址。如果這就是dai持有者的位址,那麼我們就知道參數對上了,也就是說DOMAIN_SEPARATOR、PERMIT_TYPEHASH、nonce、holder、spender、expiry以及allowed都對。如何對不上,就拒絕這個簽名:
require(holder == ecrecover(digest, v, r, s), "Dai/invalid-permit");
這個地方需要注意。簽名涉及很多參數,其中有些參數比較晦澀,例如鍊ID(DOMAIN_SEPARATOR的一部分)。其中任何參數對不上都會導緻簽名被拒絕,這使得鍊下簽名的調試非常困難。
現在我們指導持有者已經授權了這個方法調用。接下來我們需要确認簽名沒有被濫用。
首先檢查目前時間是否在expiry之前,這樣可以讓授權僅在特定時間點之前有效。
require(expiry == 0 || now <= expiry, "Dai/permit-expired");
我們也可以檢查具有這個nonce的簽名還沒有使用過,這樣就可以確定一個簽名隻能使用一次。
require(nonce == nonces[holder]++, "Dai/invalid-nonce");
現在通過了!dai.sol更新allowance,觸發事件,就這些簡單的工作了。
uint wad = allowed ? uint(-1) : 0;
allowance[holder][spender] = wad;
emit Approval(holder, spender, wad);
dai.sol合約使用二進制方式處理allowance,在我們提供的
代碼中則使用了更傳統的方式來處理allowance。
5、建立鍊下簽名
建立簽名不适合膽小的人,不過隻需要一點練習和耐心,其實也容易掌握。我們用三個步驟來複制智能合約的permit方法中的邏輯:
- 生成DOMAIN_SEPARATOR
- 生成摘要
- 生成交易簽名
下面的函數将建立DOMAIN_SEPARATOR。它和dai.sol構造函數中的代碼功能一樣,不過使用的是javascript,以及ethers.js中的keccak256、defaultAbiCoder和toUtfBytes。這個函數需要代币名稱、部署位址以及鍊ID,并假設代币版本為"1":
下面的函數将為特定的permit調用建立摘要。注意holder、spender、nonce和expiry都作為參數傳入。同時傳入一個approve.allowed參數,雖然你可以始終将其設定為true。注意這裡的PERMIT_TYPEHASH我們是直接從dai.sol拷貝過來的。
一旦我們得到摘要,那麼進行簽名就相對容易多了。我們使用ethereumjs-util中的ecsign對移除0x字首的摘要資料進行簽名。注意這個步驟我們需要使用者私鑰。
上述js函數的調用方法如下:
注意我們在調用permit時是如何使用之前建立摘要的那些參數的。隻有這樣簽名才會有效。
另一點需要注意的是,在這個代碼片段中user2隻調用兩個交易。user1表示dai持有者,他是建立摘要并進行簽名的賬号。然而user1并不需要消耗任何gas。
user1将簽名給user2,user2使用這個簽名來執行permit方法以及transferFrom方法。
在user1看來,這就是一個無gas交易,他不需要消耗任何wei。
6、結論
本文展示了如何使用無gas交易,澄清了無gas實際上意味着将gas成本轉嫁給了其他人。為此我們需要智能合約中的方法能夠處理預簽名交易。
不過使用這一模式有顯著的好處,是以無gas交易已經被廣泛使用。簽名允許交易的gas成本從使用者轉移到服務提供商,進而消除了很多場景中的使用者進入障礙。無gas交易也支援更加進階的代碼模式實作,通常都會帶來顯著的使用者體驗改善。
原文連結:
如何實作無Gas以太坊交易 — 彙智網