在数字签名一文中,我们学习了如何对消息进行签名和验证。但在实际应用中,我们经常需要对复杂的结构化数据进行签名,比如订单、投票、授权等。
EIP-712 正是为了解决这个问题而生。它定义了一种对结构化数据进行签名的标准方法,使得:
让我们先对比一下 EIP-712 与基础的消息签名有什么不同:
在数字签名中,我们使用的是简单消息签名:
// 签名的内容
bytes32 messageHash = keccak256(abi.encodePacked("Hello, Ethereum!"));
bytes32 ethSignedMessageHash = keccak256(abi.encodePacked(
"\x19Ethereum Signed Message:\n32",
messageHash
));
问题:
EIP-712 改进了这个过程:
// 签名的内容
struct Order {
address from;
address to;
uint256 amount;
}
// 包含域信息(合约地址、链ID等)
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
domainSeparator, // 包含合约地址、链ID等信息
structHash // 结构化数据的哈希
));
优势:

eth_sign(上) 和 eth_signTypedData(下) 签名对比

EIP-712 提供了一个标准的方法来签名结构化数据:
digest = keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message))
这包括几个关键步骤,让我们逐一了解:
\x19\x01:
这是一个固定的字节,用于区分不同类型的签名以防冲突。0x19 表示是以太坊的签名消息,0x01 则是特定于 EIP-712 的标识。
更多签名数据标准参考 EIP-191
创建域分隔符 (EIP712Domain Separator)
域分隔符定义了签名消息的上下文环境,例如合约的名称、版本号、链 ID 和验证合约地址等。这可以帮助确保签名在正确的环境中被验证,避免在多个应用间的签名冲突,并帮助防止重放攻击。
域分隔符可以包含以下字段,具体取决于协议设计者的需求:
string name:名称,通常是 DApp 或协议的名称string version:当前版本号,不同版本的签名可能不兼容uint256 chainId:链 ID,表明合约所在的区块链的链ID,以确保签名不会在不同的链之间被重用address verifyingContract:将用来验证签名的智能合约地址bytes32 salt:提供了额外的安全随机性示例代码:
bytes32 domainSeparator = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256(bytes(version)),
chainId,
verifyingContract
)
);
创建结构化数据的哈希
假设我们有一个名为 Order 的结构,它包括以下字段:
struct Order {
address from;
address to;
uint256 amount;
}
首先定义并创建该结构的类型哈希:
string constant TYPEHASH = keccak256("Order(address from,address to,uint256 amount)");
然后将类型哈希与实际数据值一起编码并哈希:
bytes32 structHash = keccak256(
abi.encode(
TYPEHASH,
order.from,
order.to,
order.amount
)
);
生成最终签名哈希
结合域分隔符和结构化数据哈希,创建最终的签名哈希:
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
domainSeparator,
structHash
)
);
合约中可以使用 ecrecover 函数来验证签名,确保签名者的地址与预期相匹配,从而验证签名的真实性和完整性。
签名验证步骤如下:
ecrecover 函数恢复签名者的地址:Solidity 语言提供的 ecrecover 函数能够通过给定的消息哈希值及签名(包括r, s, v三个参数)来确定签名者的地址。ecrecover 函数输出的地址与事先定义的期望地址,确保恢复出的地址与预期一致,从而验证签名者的身份。这一步是确认交易发起者身份合法性的关键环节。接下来,我们将给出一个示例智能合约,展示如何在合约中集成 EIP-712 签名机制。这个合约将包括创建 EIP-712 域分隔符,结构化数据的哈希,以及验证签名的功能。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract EIP712Example {
struct Order {
address from;
address to;
uint256 amount;
}
bytes32 constant ORDER_TYPEHASH =
keccak256(
"Order(address from,address to,uint256 amount)"
);
bytes32 public DOMAIN_SEPARATOR;
constructor(string memory name, string memory version) {
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
keccak256(bytes(name)),
keccak256(bytes(version)),
block.chainid,
address(this)
)
);
}
function _hashOrder(Order memory order) internal view returns (bytes32) {
return keccak256(
abi.encode(
ORDER_TYPEHASH,
order.from,
order.to,
order.amount
)
);
}
function _hashTypedData(bytes32 structHash) internal view returns (bytes32) {
return keccak256(
abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)
);
}
function verify(
Order memory order,
bytes memory signature
) external view returns (bool) {
uint8 v;
bytes32 r;
bytes32 s;
if (signature.length != 65) {
return false;
}
assembly {
r := mload(add(signature, 32))
s := mload(add(signature, 64))
v := byte(0, mload(add(signature, 96)))
}
bytes32 structHash = _hashOrder(order);
bytes32 digest = _hashTypedData(structHash);
address signer = ecrecover(digest, v, r, s);
return signer == order.from;
}
}
import { BrowserProvider } from 'ethers';
const domain = {
name: 'MyOrderApp',
version: '1',
chainId: 10, // 使用具体的Chain ID
verifyingContract: '0x2d04c136Ebb705bd2cE858e28BeFe258C1Ec51F7', // 合约具体的合约地址
};
// 类型的定义
const types = {
Order: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'amount', type: 'uint256' }
]
};
// 创建一个符合 Order 结构的消息实例
const order = {
from: '0xfrom...',
to: '0xto...',
amount: 1000
};
async function requestSignature() {
// 使用 ethers v6 连接到以太坊钱包
const provider = new BrowserProvider(window.ethereum);
await provider.send('eth_requestAccounts', []); // 请求用户授权
const signer = await provider.getSigner();
// 发起签名
const signature = await signer.signTypedData(domain, types, order);
console.log('Signature:', signature);
}
const domain = {
name: 'MyOrderApp',
version: '1',
chainId: 10, // 使用具体的Chain ID
verifyingContract: '0x2d04c136Ebb705bd2cE858e28BeFe258C1Ec51F7', // 合约具体的合约地址
};
// 类型的定义
const types = {
Order: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'amount', type: 'uint256' }
]
};
// 创建一个符合 Order 结构的消息实例
const order = {
from: '0xfrom...',
to: '0xto...',
amount: 1000
};
async function requestSignature() {
// 使用 ethers 连接到以太坊钱包
const provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send('eth_requestAccounts', []); // 请求用户授权
const signer = provider.getSigner();
// 发起签名
const signature = await signer._signTypedData(domain, types, order);
console.log('Signature:', signature);
}
import { createWalletClient, custom } from 'viem'
import { mainnet } from 'viem/chains'
const domain = {
name: 'MyOrderApp',
version: '1',
chainId: 10,
verifyingContract: '0x2d04c136Ebb705bd2cE858e28BeFe258C1Ec51F7',
}
const types = {
Order: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'amount', type: 'uint256' }
]
}
const order = {
from: '0xfrom...',
to: '0xto...',
amount: 1000n // 注意:viem 使用 bigint
}
async function requestSignature() {
const client = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum)
})
const [account] = await client.requestAddresses()
// 发起签名
const signature = await client.signTypedData({
account,
domain,
types,
primaryType: 'Order',
message: order,
})
console.log('Signature:', signature)
}
EIP-712 是基础消息签名的进阶版本,它为结构化数据签名提供了标准化的解决方案。
核心优势:
什么时候使用 EIP-712:
什么时候使用基础签名:
通过 EIP-712,开发者可以实施更安全、更透明的签名方案,大幅提升用户体验和合约安全性。