丁丁打折网 - 网友优惠券分享网站,有688999个用户

京东优惠券 小米有品优惠券

当前位置 : 首页>web3>以太坊元交易:免手续费合约开发实战指南

以太坊元交易:免手续费合约开发实战指南

类别:web3 发布时间:2025-10-22 14:55

好的,这是根据您的要求重写后的文章:

深入理解以太坊元交易:从 OpenZeppelin 源码到实战应用

上文提到,以太坊普通交易需要消耗手续费,有没有办法让用户在交易时免除手续费呢?答案是肯定的,这就是元交易。元交易通过在交易中嵌套交易,巧妙地实现了免手续费的目的。

本文将深入剖析开源库 OpenZeppelin/openzeppelin-contracts 中元交易合约的实现,帮助你快速掌握元交易的实现细节,并为后续更深入的技术探索打下基础。

预备知识

元交易涉及 ECDSA 和 EIP712 等技术,如果你已经熟悉这些概念,可以直接跳到实现分析部分。

Hash(哈希)

哈希,也称为散列或数字摘要,是一种将任意长度的信息转换为固定长度结果的技术。哈希函数就像一个神奇的转换器,将各种大小的信息压缩成一个独特的“指纹”。对于相同的输入,哈希函数总是生成相同的“指纹”,即使原始数据发生微小的改变,生成的哈希值也会截然不同。以太坊采用的是 Keccak-256 算法。

ECDSA(椭圆曲线数字签名算法)

ECDSA 是一种基于椭圆曲线密码学的数字签名算法,用于验证数据的真实性。你可以把它想象成一个真实的签名,你可以识别某些人的签名,但无法在未经允许的情况下伪造它。

ECDSA 不会加密数据或阻止他人访问,但可以确保数据没有被篡改。

在以太坊中,ECDSA 用于对原始数据的哈希值进行签名和恢复。

用户 A 使用私钥对原始数据的哈希值进行签名,生成签名(Signature)。任何人都可以使用该签名和哈希值恢复出签名人的地址。

EIP712(以太坊类型化结构化数据哈希与签名)

EIP712 旨在提高链上使用的链下消息签名的可用性。链下消息签名可以节省手续费,减少区块链上的交易数量。EIP712 概述了一种编码数据及其结构的方案,允许在签名时将数据显示给用户进行验证。

下图展示了用户在签署 EIP712 消息时显示的示例。

元交易合约的实现分析

以下分析基于 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 实现的一个 Solidity 库,它实现了从哈希值中恢复地址的方法。ForwardRequest 结构体定义了一个交易中用于签名的基本组成成分。与以太坊交易不同的是,没有 gasPrice,因为智能合约的执行只关心 gas 的消耗。ForwardRequest 中的 nonce 概念与以太坊类似,都是为了避免双花攻击,但这里的 nonce 仅由智能合约维护,跟普通的以太坊交易中的 nonce 无关。

构造函数中直接使用 EIP712 的构造函数进行初始化,EIP712 的构造函数签名为:constructor(string memory name, string memory version),其中 name 是合约名称,version 是合约版本,这将作为 EIP712 签名验证的一部分。

mapping(address => uint256) private _nonces;

function getNonce(address from) public view returns (uint256) {return _nonces[from];}

为了避免双花攻击,在智能合约中维护 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 函数用于验证签名是否有效。要恢复地址,需要经过 ECDSA 的签名以及用于签名的原始数据。此处,ECDSA 签名的原始数据是经过 ABI 编码的 keccak256(abi.encode(_TYPEHASH, req.from, req.to, req.value, req.gas, req.nonce, keccak256(req.data))) ForwardRequest 结构体数据的哈希值。通过调用 ECDSA 库中的 recover 函数,传入签名,就可以恢复得到签名者的地址。

_nonces[req.from] == req.nonce 用于确保交易的调用是顺序的,且不会遭受双花攻击。signer == req.from 避免签名者与实际元交易发送者不匹配。

接下来看如何执行元交易。

function execute(ForwardRequest calldata req, bytes calldata signature)publicpayablereturns (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 方法的时候,根据元交易参数,指定了 call 的 gas 与 value 值。这里并不直接将元交易的 data 字段当作 call 操作的 data,而是将 data 与 from 进行 ABI 编码后一起作为 call 操作的参数,这在目标合约(也就是 req.to)中会被解析,从而得到交易的发送者,在下面会详细讲解。

assert(gasleft() > req.gas / 63) 简单理解为避免中继器(代为执行元交易的人)恶意地或无意地使用足够低的 gas 使得交易执行成功,而元交易执行失败。

ERC2771

要支持元交易,仅实现元交易智能合约是不够的,因为目标合约无法知道实际的元交易 from 是谁。如果没有额外的措施,它将只能够从 msg.sender 中获取,由于在元交易合约实现中,是通过 Address.call 调用的,因此将得到的发送者是元交易合约的地址。ERC2771 则解决了该问题。

abstract contract ERC2771Context is Context

ERC2771Context 继承了 Context,而 Context 中简单封装了从 msg.sendermsg.data,以便规范这两个功能的使用,且能够让其在子合约中修改其行为。要求使用 Context 合约获取 msg 相关的数据,而不是直接使用 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) 验证元交易的调用方是期望的元交易合约地址。assembly 代码将上文的元交易合约中 req.to.call{...}(abi.encodePacked(req.data, req.from)) 编码进的 data 部分内容的 req.from 获取到,然后再返回该值。

元交易使用概览

让我们来尝试简单使用元交易合约,要支持元交易,你所编写的合约必须继承 ERC2771Context。在这里简单实现一个 NFT 合约,在部署它之前,你必须先部署元交易合约,将元交易合约地址作为参数传递给 NFT 合约构造函数。

// SPDX-License-Identifier: GPL3.0pragma 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 没有足够的资金支付手续费来铸造一个 NFT,她可以签署一个元交易,元交易的 data 是由 abi.encodeWithSignature(functionSelector, parmas...) 得到的,将该元交易递交给具有足够资金的 Bob,Bob 调用元交易合约 MinimalForwarder.execute(req, signature),从而让 Alice 的元交易成功执行。

希望这篇文章能够帮助你理解以太坊元交易的原理和实现,并为你进一步探索区块链技术提供一些启发。

丁丁打折网©版权所有,未经许可严禁复制或镜像 ICP证: 湘ICP备20009233号-2

Powered by 丁丁打折网本站为非营利性网站,本站内容均来自网络转载或网友提供,如有侵权或夸大不实请及时联系我们删除!本站不承担任何争议和法律责任!
技术支持:丁丁网 dddazhe@hotmail.com & 2010-2020 All rights reserved