paint-brush
使用 EIP-712 进行以太坊无 Gas 元交易经过@thebojda
1,829 讀數
1,829 讀數

使用 EIP-712 进行以太坊无 Gas 元交易

经过 Laszlo Fazekas12m2023/10/17
Read on Terminal Reader

太長; 讀書

在元交易的情况下,用户创建一个描述交易的结构,然后用他们的私钥对其进行数字签名。这类似于为某人写支票。然后,经过数字签名的交易被发送到中继节点,中继节点将其提交给智能合约。合约验证签名,如果有效,则执行交易。中继节点为合约的执行付费。
featured image - 使用 EIP-712 进行以太坊无 Gas 元交易
Laszlo Fazekas HackerNoon profile picture
0-item


我最近写了一篇关于Karma Money的文章,这是一种基于独特的 ERC-20 代币的替代货币系统。我将 Karma Money 设想为一个封闭的系统,用户还可以用 karma 支付交易费用。构建我们自己的区块链将是实现这一目标的一种方法,但这是一项具有挑战性的任务。为了保证区块链的安全性和可信性,我们需要建立必要的基础设施和足够大的社区。使用现有的区块链会简单得多。有诸如GnosisPolygon之类的链,它们与以太坊完全兼容,并且交易费用非常低。这些链上的 ERC20 交易费用通常低于 1 美分。问题是这笔费用必须用链本身的加密货币支付,这可能会使用户使用链变得复杂。幸运的是,有一个桥接解决方案,即EIP-712金属交易。


在元交易的情况下,用户创建一个描述交易的结构,然后用他们的私钥对其进行数字签名。这类似于为某人写支票。然后,经过数字签名的交易被发送到中继节点,中继节点将其提交给智能合约。合约验证签名,如果有效,则执行交易。中继节点为合约的执行付费。


例如,在 karma 交易中,用户提供交易金额(例如 10 karma 美元)、他们想要发送该金额的以太坊地址以及他们愿意提供的交易费用(以 karma 美元为单位)用于交易。该结构经过数字签名并发送到中继节点。如果节点认为交易费用可以接受,则会将数字签名的结构提交给 karma 合约,该合约验证签名并执行交易。由于交易费用是由中继节点以区块链的本机货币支付的,因此在用户看来,他们好像是用业力美元来支付交易,而不需要自己的区块链。


理论结束后,我们来看看实践。


EIP-712 标准定义了如何以标准化方式对结构化数据包进行签名。 MetaMask 以用户可读的格式显示这些结构化数据。符合 EIP-712 的结构,如 MetaMask 上所示(可以在此 URL 上进行测试)如下所示:


符合 EIP-712 的结构


上述交易是使用以下简单代码生成的:


 async function main() { if (!window.ethereum || !window.ethereum.isMetaMask) { console.log("Please install MetaMask") return } const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); const chainId = await window.ethereum.request({ method: 'eth_chainId' }); const eip712domain_type_definition = { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "version", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ] } const karma_request_domain = { "name": "Karma Request", "version": "1", "chainId": chainId, "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" } document.getElementById('transfer_request')?.addEventListener("click", async function () { const transfer_request = { "types": { ...eip712domain_type_definition, "TransferRequest": [ { "name": "to", "type": "address" }, { "name": "amount", "type": "uint256" } ] }, "primaryType": "TransferRequest", "domain": karma_request_domain, "message": { "to": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", "amount": 1234 } } let signature = await window.ethereum.request({ "method": "eth_signTypedData_v4", "params": [ accounts[0], transfer_request ] }) alert("Signature: " + signature) }) } main()


eip712domain_type_definition是一般结构的描述,其中包含元数据。 name 字段是结构的名称,version 字段是结构的定义版本,chainId 和 verifyingContract 字段确定消息针对哪个合约。执行合约验证此元数据,以确保签名的交易仅在目标合约上执行。


karma_request_domain包含 EIP712Domain 结构定义的元数据的具体值。


我们发送到 MetaMask 进行签名的实际结构包含在Transfer_request变量中。 types 块包含类型定义。这里,第一个元素是强制性的 EIP712Domain 定义,它描述了元数据。接下来是实际的结构定义,在本例中是 TransferRequest。这是将在 MetaMask 中为用户显示的结构。域块包含元数据的特定值,而消息包含我们要与用户签名的特定结构。


当谈到业力钱时,元交易如何组合并发送到智能合约的示例如下所示:


 const types = { "TransferRequest": [ { "name": "from", "type": "address" }, { "name": "to", "type": "address" }, { "name": "amount", "type": "uint256" }, { "name": "fee", "type": "uint256" }, { "name": "nonce", "type": "uint256" } ] } let nonce = await contract.connect(MINER).getNonce(ALICE.address) const message = { "from": ALICE.address, "to": JOHN.address, "amount": 10, "fee": 1, "nonce": nonce } const signature = await ALICE.signTypedData(karma_request_domain, types, message) await contract.connect(MINER).metaTransfer(ALICE.address, JOHN.address, 10, 1, nonce, signature) assert.equal(await contract.balanceOf(ALICE.address), ethers.toBigInt(11))


types变量定义事务的结构。 “发件人”是发件人的地址,“收件人”是收件人的地址。该金额代表要转移的代币数量。费用是我们向中继节点提供的代币“数量”,以换取执行我们的交易并支付链上本机货币的成本。 “随机数”作为计数器来确保交易的唯一性。如果没有这个字段,一笔交易可能会被执行多次。然而,由于存在随机数,签名的交易只能执行一次。


ethers.js提供的signTypedData函数可以轻松签署EIP-712 结构。它与前面介绍的代码执行相同的操作,但用法更简单。


metaTransfer是 karma 合约执行元交易的方法。让我们看看它是如何工作的:


 function metaTransfer( address from, address to, uint256 amount, uint256 fee, uint256 nonce, bytes calldata signature ) public virtual returns (bool) { uint256 currentNonce = _useNonce(from, nonce); (address recoveredAddress, ECDSA.RecoverError err) = ECDSA.tryRecover( _hashTypedDataV4( keccak256( abi.encode( TRANSFER_REQUEST_TYPEHASH, from, to, amount, fee, currentNonce ) ) ), signature ); require( err == ECDSA.RecoverError.NoError && recoveredAddress == from, "Signature error" ); _transfer(recoveredAddress, to, amount); _transfer(recoveredAddress, msg.sender, fee); return true; }


为了验证签名,我们必须首先生成结构的哈希值。 EIP-712 标准详细描述了执行此操作的具体步骤,其中包括示例智能合约示例 javascript 代码


综上所述,本质就是我们使用abi.encode将TYPEHASH(即结构体描述的哈希值)与结构体的字段结合起来。然后生成 keccak256 哈希值。哈希值被传递给 _hashTypedDataV4 方法,该方法继承自 Karma 合约中的 EIP712 OpenZeppelin 合约。该函数将元数据添加到我们的结构中并生成最终的哈希值,使结构验证变得非常简单和透明。最外面的函数是 ECDSA.tryRecover,它尝试从哈希和签名中恢复签名者的地址。如果与“from”参数的地址匹配,则签名有效。在代码的最后,执行实际的交易,并且执行交易的中继节点接收费用。


EIP-712 是签名结构的通用标准,使其成为实现元交易的众多用途之一。由于签名不仅可以通过智能合约进行验证,它在非区块链应用程序中也非常有用。例如,它可用于服务器端身份验证,其中用户使用其私钥来标识自己。这样的系统可以提供通常与加密货币相关的高级别安全性,从而允许仅通过硬件密钥使用网络应用程序。此外,单个 API 调用也可以借助 MetaMask 进行签名。


我希望 EIP-712 标准的简要概述对许多人有所启发,并且您将能够在基于区块链和非区块链的项目中使用它。


每个代码都可以在karma Money 的 GitHub 存储库上找到。