本文档介绍了以太坊签名标准 EIP-191 和 EIP-712,这些标准旨在标准化签名数据的格式,提高交易数据的可读性,并防止重放攻击。EIP-191 定义了签名数据的一般结构,而 EIP-712 进一步标准化了版本特定数据和签名数据的格式,通过引入 typed structured data hashing,使得签名在钱包中更易读,并增强了安全性。
了解你需要知道的关于(以太坊改进提案)EIP-191、EIP-712 和以太坊签名标准的所有内容。
为了理解签名创建、验证以及如何防止重放攻击,首先需要理解以太坊改进提案 EIP-191 和 EIP-712。
在签名交易时,需要一种更简单的方式来读取交易数据。 例如,在创建这些标准之前,在 MetaMask 中签署交易时会显示以下消息:

图片:在 MetaMask 中签署交易时,EIP-712 之前的非结构化消息
有了这些标准,交易可以用可读的方式显示:

图片:在 MetaMask 中签署交易时,使用 EIP-712 的结构化消息
此外,EIP-712 是防止重放攻击的关键 - 用于防止重放攻击的数据被编码在结构化数据中。
本文概述了这些标准、它们的动机以及如何实现它们。
—— 本文的完整源代码由 Patrick Collins 编写,可以在 GitHub 上查看。
建议阅读这篇 ECDSA 签名文章 以了解前两个概念的基础知识。
—— 请注意,本文中的代码仅用于演示目的,尚未经过彻底的安全审查,请勿将其用作生产代码。
对于简单签名,在智能合约中实现验证函数包括创建一个函数 getSimpleSigner(),该函数接受要签名的消息(可以是任何数据)和签名的 (r, s, v) 分量。该函数对消息进行哈希处理,并使用预编译 ecrecover 检索签名者,并返回结果:
function getSignerSimple(uint256 message, uint8 _v, bytes32 _r, bytes32 _s) public pure returns (address) {
// 对要签名的消息进行哈希处理
bytes32 hashedMessage = bytes32(message); // 如果是字符串,使用 keccak256(abi.encodePacked(string))
// 检索签名者
address signer = ecrecover(hashedMessage, _v, _r, _s);
return signer;
}
ecrecover 是一个 预编译,是一个内置于以太坊协议中的函数,它使用 签名的 (r, s, v) 分量 从任何消息中检索签名者。
然后,函数 verifySignerSimple() 将检索到的签名者与预期的签名者进行比较,如果结果不是预期的签名者,则恢复:
function verifySignerSimple(
uint256 message,
uint8 _v,
bytes32 _r,
bytes32 _s,
address signer
)
public
pure
returns (bool)
{
address actualSigner = getSignerSimple(message, _v, _r, _s);
require(signer == actualSigner);
return true;
}
这就是签名在根本级别上的工作方式:获取一些哈希消息加上消息的签名,检索签名者,并检查它是否是预期的地址。
但这存在一个问题,需要一种使用预制签名发送交易的方式:赞助交易。 这在智能合约之外已经可以实现,但是需要一种将其构建到智能合约中的函数中的方法。 例如,Bob 签署一条消息(一笔交易),并将签名提供给 Alice。 Alice 使用此签名发送交易,这意味着 Bob 可以支付她的 gas 费。 因此,引入了 EIP-191。
EIP-191 签名数据标准 提出了以下签名数据格式:0x19 <1 字节版本> <版本特定数据> <要签名的数据>
0x19:前缀。0x19 是因为它未在任何其他上下文中使用。<1 字节版本>:使用的“签名数据”版本。0x00:带有预期验证器的数据。0x01:结构化数据 - 最常用于生产应用程序,并且与 EIP-712 相关联,将在下一节中讨论。0x02:personal_sign 消息。<要签名的数据>:要签名的消息。以下 getSigner191() 函数演示了如何设置 EIP-191 签名:
function getSigner191(uint256 message, uint8 _v, bytes32 _r, bytes32 _s) public view returns (address) {
// 计算哈希以进行验证时的参数
// 1:byte(0x19) - 初始 0x19 字节
// 2:byte(0) - 版本字节
// 3:版本特定数据,对于版本 0,它是预期的验证器地址
// 4-6:应用程序特定数据
bytes1 prefix = bytes1(0x19);
bytes1 eip191Version = bytes1(0);
address indendedValidatorAddress = address(this);
bytes32 applicationSpecificData = bytes32(message);
// 0x19 < 1 字节版本> < 版本特定数据> < 要签名的数据>
bytes32 hashedMessage =
keccak256(abi.encodePacked(prefix, eip191Version, indendedValidatorAddress, applicationSpecificData));
address signer = ecrecover(hashedMessage, _v, _r, _s);
return signer;
}
正如所观察到的,使用此标准检索签名者更为冗长。
然后可以将签名者与预期的签名者进行比较,如下所示:
function verifySigner191(
uint256 message,
uint8 _v,
bytes32 _r,
bytes32 _s,
address signer
)
public
view
returns (bool)
{
address actualSigner = getSigner191(message, _v, _r, _s);
require(signer == actualSigner);
return true;
}
但是,如果 <要签名的数据> 更复杂怎么办? 需要一种格式化数据的方式,以便更容易理解。 因此,需要标准化数据格式,并引入了 EIP-712。
EIP-191 不够具体,需要标准化应用程序(版本)特定数据。 这意味着可以更容易地读取签名并从钱包中显示签名,例如 MetaMask,并防止 重放攻击。
EIP-712 引入了标准化数据:类型化的结构化数据哈希和签名。
签名现在具有以下结构:
0x19 0x01 <domainSeparator> <hashStruct(message)>
0x19:前缀(与之前相同)0x01:版本<domainSeparator>:这是与版本关联的数据。eip712Domain:
struct EIP712Domain {
string name;
string version;
uint256 chainId;
address verifyingContract;
// bytes32 salt; not required
}
这意味着合约可以知道签名是否专门为自己创建的。 知道了这一点,可以将 EIP-712 数据重写为:
0x19 0x01 <hashStruct(eip712Domain)> <hashStruct(message)>
但是,什么是 hash struct?
hash struct 的符号定义是
hashStruct(s : 𝕊) = keccak256(typeHash ‖ encodeData(s))
其中 typeHash = keccak256(encodeType(typeOf(s)))
hash struct 是一个 struct 的哈希,包括:
struct 的外观的哈希值 - typehash。 typehash 是 struct(类型)的哈希值。 对于 <domainSeparator>,typehash 为:
// EIP721 域 struct 的哈希值
bytes32 constant EIP712DOMAIN_TYPEHASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
数据的哈希值。 对于域分隔符,该数据是 eip721Domain struct 数据:
// 定义“域”struct 的外观。
EIP712Domain eip_712_domain_separator_struct = EIP712Domain({
name: "SignatureVerifier", // 这可以是任何东西
version: "1", // 这可以是任何东西
chainId: 1, // 理想情况下是 chainId
verifyingContract: address(this) // 理想情况下,将其设置为“this”,但可以是任何验证签名的合约
});
将这些放在一起,<domainSeparator> 变为:
// 现在知道了签名的格式,定义谁将验证签名。
bytes32 public immutable i_domain_separator = keccak256(
abi.encode(
EIP712DOMAIN_TYPEHASH,
keccak256(bytes(eip_712_domain_separator_struct.name)),
keccak256(bytes(eip_712_domain_separator_struct.version)),
eip_712_domain_separator_struct.chainId,
eip_712_domain_separator_struct.verifyingContract
)
);
<hashStruct(message)>:要签名的消息的 hash struct。使用 hash struct 的先前定义,定义 typehash:
// 定义消息 hash struct 的外观。
struct Message {
uint256 number;
}
bytes32 public constant MESSAGE_TYPEHASH = keccak256("Message(uint256 number)");
然后,<hashStruct(message)> 变为:
bytes32 hashedMessage = keccak256(abi.encode(MESSAGE_TYPEHASH, Message({ number: message })));
可以将 EIP-712 数据视为:
0x19 0x01 <验证此签名的人的哈希值,以及验证者的外观> < 签名结构化消息的哈希值,以及签名的外观>
将这些放在一起,get signer 函数变为:
function getSignerEIP712(uint256 message, uint8 _v, bytes32 _r, bytes32 _s) public view returns (address) {
// 计算哈希以进行验证时的参数
// 1:byte(0x19) - 初始 0x19 字节
// 2:byte(1) - 版本字节
// 3:域分隔符的 hashstruct(包括域 struct 的 typehash)
// 4:消息的 hashstruct(包括消息 struct 的 typehash)
bytes1 prefix = bytes1(0x19);
bytes1 eip712Version = bytes1(0x01); // EIP-712 是 EIP-191 的版本 1
bytes32 hashStructOfDomainSeparator = i_domain_separator;
// 对消息 struct 进行哈希处理
bytes32 hashedMessage = keccak256(abi.encode(MESSAGE_TYPEHASH, Message({ number: message })));
// 最后,将它们全部组合起来
bytes32 digest = keccak256(abi.encodePacked(prefix, eip712Version, hashStructOfDomainSeparator, hashedMessage));
return ecrecover(digest, _v, _r, _s);
}
然后可以通过将其与预期的签名者进行比较来验证签名者,如下所示:
function verifySigner712(
uint256 message,
uint8 _v,
bytes32 _r,
bytes32 _s,
address signer
)
public
view
returns (bool)
{
address actualSigner = getSignerEIP712(message, _v, _r, _s);
require(signer == actualSigner);
return true;
}
如前所述,EIP-712 是防止 重放攻击 的关键。
理解 EIP-191 和 EIP-712 对于理解如何创建抗重放数据以签名到签名中非常重要。 EIP-712 结构中的额外数据确保了抗重放性。
为了防止重放攻击,智能合约必须:
s 值限制为单个半值—— 有关签名重放攻击以及如何防止它们的更多信息,请参阅 本综合指南 。
在本指南中,你了解了以太坊签名标准是如何工作的。 总结如下:
为了充分理解签名创建、验证和签名重放,必须理解这两个标准。 这种理解是编写安全智能合约的关键。
- 原文链接: cyfrin.io/blog/understan...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!