元交易合約如何實(shí)現(xiàn)?智能合約開發(fā)實(shí)戰(zhàn):元交易(Metatransaction)系列二
引言
上文中提到,普通的 ETH 交易并不能夠做到讓用戶無需 gas 費(fèi),需要交易中嵌套一個交易,即元交易,來實(shí)現(xiàn)免 gas 費(fèi)。
本文將分析開源庫 OpenZeppelin/openzeppelin-contracts 中的元交易合約的實(shí)現(xiàn),讓你能夠快速入門元交易實(shí)現(xiàn)細(xì)節(jié),從而能夠自己對后續(xù)更多的相關(guān)技術(shù)深入探索。
前置知識概述
元交易會涉及到 ECDSA 與 EIP712 等知識,如果你是熟手,可以跳過此節(jié)內(nèi)容,直接瀏覽具體實(shí)現(xiàn)分析部分。
Hash
也稱哈希、散列、數(shù)字摘要。通過哈希函數(shù),可以將長短不一的信息轉(zhuǎn)化為一段長度任意但可預(yù)測的(確定性的)結(jié)果。這是一類神奇的函數(shù),可以將一大堆信息轉(zhuǎn)變成一串短的,可作為摘要的數(shù)據(jù) “指紋”。對于一個給定的輸入而言,生成的 “指紋” 始終一致。如果你的原始數(shù)據(jù)中有任何細(xì)微的改動,生成的哈希值將大不相同。以太坊中采用的是 Keccak-256 算法。
ECDSA
在密碼學(xué)中,ECDSA(Elliptic Curve Digital Signature Algorithm,橢圓曲線數(shù)字簽名算法)是使用橢圓曲線密碼學(xué)的數(shù)字簽名算法(DSA)的一個變種。
主要用于對數(shù)據(jù)(比如一個文件)創(chuàng)建數(shù)字簽名,以便于你在不破壞它的安全性的前提下對它的真實(shí)性進(jìn)行驗證??梢詫⑺胂蟪梢粋€實(shí)際的簽名,你可以識別部分人的簽名,但是你無法在別人不知道的情況下偽造它。
你不應(yīng)該將ECDSA與用來對數(shù)據(jù)進(jìn)行加密的AES(高級加密標(biāo)準(zhǔn))相混淆。ECDSA不會對數(shù)據(jù)進(jìn)行加密、或阻止別人看到或訪問你的數(shù)據(jù),它可以防止的是確保數(shù)據(jù)沒有被篡改。
如圖所示,在以太坊中,ECDSA 用于對原始數(shù)據(jù)的 hash 值進(jìn)行簽名及恢復(fù)。
將原始數(shù)據(jù)通過 hash 函數(shù)得到它的 hash 值后,用戶 A 用自己的私鑰對該 hash 值進(jìn)行簽名,得到 Signature(簽名)。有了該簽名與 hash 值,任何人都能夠從中恢復(fù)出簽名人的錢包地址,在這里用戶 B 則恢復(fù)得到了用戶 A 的錢包地址。
EIP712
Ethereum Improvement Proposals (EIPs),你可以在這里查看所有的 EIPs。EIP712 (Ethereum typed structured data hashing and signing)以太坊類型的結(jié)構(gòu)化數(shù)據(jù)哈希與簽名。
如果我們只關(guān)心字節(jié)字符串的話,簽名數(shù)據(jù)是一個已經(jīng)解決了的問題。但不幸的是,在現(xiàn)實(shí)世界中,我們關(guān)心的是復(fù)雜而有意義的信息,對結(jié)構(gòu)化數(shù)據(jù)進(jìn)行哈希是非常重要的,錯誤會導(dǎo)致系統(tǒng)安全屬性的丟失。
此 EIP 旨在提高鏈上使用的鏈下消息簽名的可用性。我們看到越來越多的人采用鏈下消息簽名,因為它節(jié)省了 gas 費(fèi),減少了區(qū)塊鏈上的交易數(shù)量。當(dāng)前簽名消息是一個不透明的十六進(jìn)制字符串,顯示給用戶,關(guān)于組成消息的項目的上下文很少。
EIP712 概述了一個編碼數(shù)據(jù)及其結(jié)構(gòu)的方案,該方案允許在簽名時將數(shù)據(jù)顯示給用戶進(jìn)行驗證。下面是一個用戶在簽署 EIP712 消息時顯示的示例。
元交易合約的實(shí)現(xiàn)
此分析針對 openzeppelin-contracts v4.3.2 版本。
contract MinimalForwarder is EIP712 { using ECDSA for bytes32; struct ForwardRequest { address from; address to; uint256 value; uint256 gas; uint256 nonce; bytes data; } constructor() EIP712("MinimalForwarder", "0.0.1") {} }
ECDSA 是 openzeppelin 實(shí)現(xiàn)的一個 solidity 庫,它實(shí)現(xiàn)了從 hash 值中恢復(fù)錢包地址的方法,將它應(yīng)用在 bytes32 上,就可以直接在 bytes32 上調(diào)用 recover 方法。recover 函數(shù)簽名:function recover(bytes32 hash, bytes memory signature) internal pure returns (address) 。
ForwardRequest 結(jié)構(gòu)體定義了一個交易中用于簽名的基本組成成分。與以太坊交易不同的是沒有 gasPrice,因為智能合約的執(zhí)行只關(guān)心 gas 的消耗。ForwardRequest 中 的 nonce 概念與以太坊類似,都是為了避免雙花攻擊,但這里的 nonce 僅由智能合約維護(hù),跟普通的以太坊交易中的 nonce 無關(guān)。
構(gòu)造函數(shù)中直接使用 EIP712 的構(gòu)造函數(shù)進(jìn)行初始化,EIP712 的構(gòu)造函數(shù)簽名為:constructor(string memory name, string memory version) ,其中 name 是合約名稱,version 是合約版本,這將作為 EIP712 簽名驗證的一部分,它在部署時,將自動獲取合約的地址、chainId 等信息。意味著,即便有相同的 ForwardRequest 結(jié)構(gòu)體數(shù)據(jù),但合約地址或區(qū)塊鏈網(wǎng)絡(luò)不同,也會導(dǎo)致簽名無效。
mapping(address => uint256) private _nonces; function getNonce(address from) public view returns (uint256) { return _nonces[from]; }
為了避免雙花攻擊,在智能合約中維護(hù) nonce 是必要的。
bytes32 private constant _TYPEHASH = keccak256("ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data)"); function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) { address signer = _hashTypedDataV4( keccak256(abi.encode(_TYPEHASH, req.from, req.to, req.value, req.gas, req.nonce, keccak256(req.data))) ).recover(signature); return _nonces[req.from] == req.nonce && signer == req.from; }
看到 verify 函數(shù),我們知道,要將錢包地址恢復(fù),至少需要經(jīng)過 ECDSA 的簽名以及用于簽名的原始數(shù)據(jù),而此處,ECDSA 簽名的原始數(shù)據(jù)就是經(jīng)過 abi 編碼的 keccak256(abi.encode(_TYPEHASH, req.from, req.to, req.value, req.gas, req.nonce, keccak256(req.data))) ForwardRequest 結(jié)構(gòu)體數(shù)據(jù)的哈希值。再通過調(diào)用 ECDSA 庫中的 recover 函數(shù),傳入簽名,就能夠恢復(fù)得到簽名者的錢包地址。
通過 _nonces[req.from] == req.nonce 來確保交易的調(diào)用是順序的,且不會遭受雙花攻擊。signer == req.from 避免簽名者與實(shí)際元交易發(fā)送者不匹配。
接下來看,如何執(zhí)行元交易。
function execute(ForwardRequest calldata req, bytes calldata signature) public payable returns (bool, bytes memory) { require(verify(req, signature), "MinimalForwarder: signature does not match request"); _nonces[req.from] = req.nonce + 1; (bool success, bytes memory returndata) = req.to.call{gas: req.gas, value: req.value}( abi.encodePacked(req.data, req.from) ); // Validate that the relayer has sent enough gas for the call. // See https://ronan.eth.link/blog/ethereum-gas-dangers/ assert(gasleft() > req.gas / 63); return (success, returndata); }
在使用 Address.call 方法的時候,根據(jù)元交易參數(shù),指定了 call 的 gas 與 value 值。需要注意的是,這里并不直接將元交易的 data 字段當(dāng)作 call 操作的 data,而是將 data 與 from 進(jìn)行 abi 編碼后一起作為 call 操作的參數(shù),這在目標(biāo)合約(也就是 req.to)中會被解析,從而得到交易的發(fā)送者,在下面會詳細(xì)講解。
assert(gasleft() > req.gas / 63) 簡單理解為避免中繼器(代為執(zhí)行元交易的人)惡意地或無意地使用足夠低的 gas 使得交易執(zhí)行成功,而元交易執(zhí)行失敗。詳情可以在 ethereum gas dangers 中學(xué)習(xí)。
ERC2771
要支持元交易,僅實(shí)現(xiàn)元交易智能合約是不夠的,因為目標(biāo)合約無法知道實(shí)際的元交易 from 是誰。如果沒有額外的措施,它將只能夠從 msg.sender 中獲取,由于在元交易合約實(shí)現(xiàn)中,是通過 Address.call 調(diào)用的,因此將得到的發(fā)送者是元交易合約的地址。ERC2771 則解決了該問題。
abstract contract ERC2771Context is Context
ERC2771Context 繼承了 Context,而 Context 中簡單封裝了從 msg.sender 與 msg.data ,以便規(guī)范這兩個功能的使用,且能夠讓其在子合約中修改其行為。要求使用 Context 合約獲取 msg 相關(guān)的數(shù)據(jù),而不是直接使用 msg.sender 等。
abstract contract Context { function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } }
ERC2771Context 就修改了 Context 合約的方法。
function _msgSender() internal view virtual override returns (address sender) { if (isTrustedForwarder(msg.sender)) { // The assembly code is more direct than the Solidity version using `abi.decode`. assembly { sender := shr(96, calldataload(sub(calldatasize(), 20))) } } else { return super._msgSender(); } }
先通過 isTrustedForwarder(msg.sender) 驗證元交易的調(diào)用方是期望的元交易合約地址。assembly 代碼將上文的元交易合約中 req.to.call{...}(abi.encodePacked(req.data, req.from)) 編碼進(jìn)的 data 部分內(nèi)容的 req.from 獲取到,然后再返回該值。
元交易使用概覽
讓我們來嘗試簡單使用元交易合約,要支持元交易,你所編寫的合約必須繼承 ERC2771Context。在這里簡單實(shí)現(xiàn)一個 NFT 合約,在部署它之前,你必須先部署元交易合約,將元交易合約地址作為參數(shù)傳遞給 NFT 合約構(gòu)造函數(shù)。
// SPDX-License-Identifier: GPL3.0 pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/metatx/ERC2771Context.sol"; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract NFT is ERC2771Context, ERC721 { using SafeMath for uint256; uint256 private _currentTokenId = 0; constructor( string memory name, string memory symbol, address trustedForwarder ) ERC721(name, symbol) ERC2771Context(trustedForwarder) {} function safeMint() public virtual { safeMint(""); } function safeMint(bytes memory _data) internal virtual { uint256 tokenId = _getNextTokenId(); _incrementTokenId(); _safeMint(_msgSender(), tokenId, _data); } function getCurrTokenId() public virtual view returns (uint256) { return _currentTokenId; } /** * @dev calculates the next token ID based on value of _currentTokenId * @return uint256 for the next token ID */ function _getNextTokenId() internal virtual view returns (uint256) { return _currentTokenId.add(1); } /** * @dev increments the value of _currentTokenId */ function _incrementTokenId() internal virtual { _currentTokenId++; } function _msgSender() internal view virtual override(Context, ERC2771Context) returns (address) { return ERC2771Context._msgSender(); } function _msgData() internal view virtual override(Context, ERC2771Context) returns (bytes calldata) { return ERC2771Context._msgData(); } }
在這個示例中,如果 Alice 沒有足夠的 ETH 支付 gas 費(fèi),來鑄造一個 NFT,她可以簽署一個元交易,元交易的 data 是由 abi.encodeWithSignature(functionSelector, parmas...) 得到的,將該元交易遞交給具有足夠 ETH 的 Bob,Bob 調(diào)用元交易合約 MinimalForwarder.execute(req, signature),從而讓 Alice 的元交易成功執(zhí)行。
以上就是元交易合約如何實(shí)現(xiàn)?智能合約開發(fā)實(shí)戰(zhàn):元交易(Metatransaction)系列二的詳細(xì)內(nèi)容,更多關(guān)于元交易合約實(shí)現(xiàn)的資料請關(guān)注腳本之家其它相關(guān)文章!
你可能感興趣的文章
-
什么是元交易?智能合約開發(fā)實(shí)戰(zhàn):元交易(Metatransaction)系列一
這篇文章主要介紹了什么是元交易?智能合約開發(fā)實(shí)戰(zhàn):元交易(Metatransaction)系列一的相關(guān)資料,希望小編的這篇關(guān)于什么是元交易的文章,能夠幫助各位投資者對元交易有一個…
2021-12-15 -
比特幣Taproot升級11/16啟動!引入智能合約、提高隱私
這篇文章主要介紹了比特幣Taproot升級11/16啟動!引入智能合約、提高隱私的相關(guān)資料,需要的朋友可以參考下,本次升級旨在提高多重簽名(multisig)交易的隱私性以及將輕量級…
2021-12-08 -
以太坊智能合約及大部分Token都在用的ERC標(biāo)準(zhǔn)是什么?
這篇文章主要介紹了以太坊智能合約,以及大部分Token都在用的ERC標(biāo)準(zhǔn)是什么?的相關(guān)資料,希望這篇關(guān)于以太坊智能合約和ERC標(biāo)準(zhǔn)是什么的文章,能夠幫助各位朋友對ERC有個更加…
2021-12-02 -
區(qū)塊鏈智能合約中的并發(fā)性和并行性
這篇文章主要介紹了區(qū)塊鏈智能合約中的并發(fā)性和并行性的相關(guān)資料,希望這篇關(guān)于智能合約中的并發(fā)性和并行性的文章,讓大家都能深入了解智能合約,下面一起來看看詳細(xì)內(nèi)容吧…
2021-11-09 -
一文弄懂區(qū)塊鏈和以太坊智能合約的由來和運(yùn)行原理
這篇文章主要介紹了一文弄懂區(qū)塊鏈和以太坊智能合約的由來和運(yùn)行原理的相關(guān)資料,希望這篇關(guān)于區(qū)塊鏈和智能合約的文章,讓大家能夠深入的了解區(qū)塊鏈和以太坊智能合約的由來…
2021-11-08 -
以太坊智能合約是什么?以太坊的智能合約有什么用
這篇文章主要介紹了以太坊智能合約是什么?以太坊的智能合約有什么用,下面小編W為大家整理一篇比較詳細(xì)的教程,希望能幫助到投資君!…
2021-11-03 -
智能合約地址什么意思?智能合約安全嗎?
這篇文章主要介紹了智能合約地址什么意思?智能合約安全嗎?的相關(guān)資料,智能合約是表示為一段代碼的合約,旨在執(zhí)行一組指令。很多人不知道是智能合約地址什么意思,下面小編…
2021-08-23 -
科普:以太坊智能合約,以及大部分Token都在用的ERC標(biāo)準(zhǔn)是什么?
這篇文章主要介紹了以太坊智能合約,以及大部分Token都在用的ERC標(biāo)準(zhǔn)是什么?ERC表示以太坊版的意見征求稿,ERC中包含技術(shù)和組織等注意事項及標(biāo)準(zhǔn)。在現(xiàn)實(shí)生活當(dāng)中,我們經(jīng)…
2021-05-06 -
區(qū)塊鏈科普:加密貨幣的來歷,演變歷史
這篇文章主要介紹了區(qū)塊鏈科普:加密貨幣的來歷,演變歷史,學(xué)習(xí)加密貨幣時要知道的最重要的一件事是它們的最初目的。第二件最重要的事情是什么不是加密貨幣。如果投資者朋友…
2021-05-05