EIP712又名结构化签名。让签名内容是结构化的、有字段名的,用户在钱包里可以清楚看到:我签的是啥:https://learnblockchain.cn/shawn_shaw
EIP712
又名结构化签名。让签名内容是结构化的、有字段名的,用户在钱包里可以清楚看到:我签的是啥!
相信你在使用 metamask
的时候,看见过以下界面,这就是一个典型的 EIP712
签名。它详细展示出了你这次签名所签署的信息是什么。而不是傻乎乎的给你一段看不懂的字节。
想要说清楚 EIP712
是什么,我们必须先了解普通的签名方式。在上一篇文章中,我们聊到了普通数字签名。跳转链接 。
对于普通数字签名而言,只对原数据包的 bytes
数据进行签名,对于用户而言,根本无法辨别你所签名的内容是什么。所以,这个签名操作是危险的!!!
想象一下,假如有一个恶意网站,利用虚假信息骗你去领取空投,调用了一个 metamask
的签名,你根本看不到你要签名什么信息。你傻乎乎的就签名了,结果你链上的 ETH
和相关代币全被盗走了。损失大发。
而 EIP712
的出现,正是为了解决这一难题的。EIP712
结构化签名是一种更加高级、安全的签名方式。并且完全和普通签名兼容(使用相同的签名、验签方式,只是签名的信息格式固定了)
EIP712
可以提供给我们在进行 Dapp
交互的时候,展示出我们所需要签名的结构化信息,方便用户去辨认我们所签署的内容的影响范围。进而去避免盲签的风险。
下面,我们从 EIP-712
开始,逐步去分析 EIP712
结构化签名的实现步骤和原理。
在结构方面,EIP712
签名分为三个部分,分别是 Domain
(域)、Types
(类型)、Message
(信息)。我们以一个例子来进行讲解。
Domain
:
Domain 里面,主要负责定义签名的上下文环境。是固定的。比如,交互的合约地址是什么、链 id
是什么等固定的元信息。经过打包后 hash
存放起来(链上和链下均需存放)
Types
:
在 Types
里面,主要存放这次交互所要签名数据的结构,也是固定的。经过打包 hash
后存放起来(链上和链下均需存放)
假设我们要签名的结构体信息是
struct Permit {
address owner;
address spender;
uint256 value;
}
需要经过 keccak()
计算后存放
keccak256("Permit(address owner,address spender,uint256 value)")
Messages
:消息,这里对应的是这次我们要签署的信息的具体值。这些值每次签名都是不固定的,但必须要和 Types
中的给定的类型要对得上(顺序、个数、类型都必须一致)。这个信息只需要链下存储,和签名的类型 hash
一起得出一个 structHash
。链上只负责验证,不用存储但需要计算这个动态数据。
但实际上,对于链下和链上的交互而言,这个 Message
实际上是和 Types
合并在一起的。可以认为表现出来只有两个部分。Domain
数据 + Types
数据。可以理解为:Domain
和 Types
都是一个结构模板,而实际发送的数据是这个结构模板装载数据的内容。
例如,对于上面的数据,这里存放的是:
owner: 0x123...
spender: 0x456...
value: 100
链下签名
构建 structHash
构造结构哈希,构造方式固定为keccak256(abi.encode(PERMIT_TYPE_HASH, ...));
其中 PERMIT_TYPE_HASH
为 上面提到的 Types
, ...
为我们实际传入的参数。
构建完整 messageHash
构建完整的 16
进制消息哈希需要用第一步的 structHash
,构造的方式固定为keccak256(abi.encodePacked("\x19\x01",DOMAIN_SEPARATOR, structHash));
其中 DOMAIN_SEPARATOR
为链上存储的 Domain
+ 数据
(当合约部署好就固定了)
ECDSA
签名
这里签名的过程和普通 ecdsa
签名兼容。
发送原始数据
+ 签名
发送调用只需吧原始数据和签名发送到合约上即可。
链上验签
使用原始数据恢复出来 structHash
和链下签名的流程类似,都是固定为keccak256(abi.encode(PERMIT_TYPE_HASH, ...));
其中 PERMIT_TYPE_HASH
为 上面提到的 Types
, ...
为我们实际传入的参数。
使用 structHash
恢复出来 messageHash
也和链下签名时的类似,构造的方式固定为keccak256(abi.encodePacked("\x19\x01",DOMAIN_SEPARATOR, structHash));
其中 DOMAIN_SEPARATOR
为链上存储的 Domain
+ 数据
(当合约部署好就固定了)
调用 ECDSA
验签
直接使用 ecdsa
的库,将恢复出来的消息 hash
和签名恢复出来地址。将这个地址和被授权的地址进行比对,若一致则说明验签成功。
这里手搓模拟实现 EIP712
结构化签名合约。真实场景请用 Openzeppelin
代码。
contract ERC712Verify {
// Domain 结构
bytes32 private constant DOMAIN_TYPE_HASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
// Types 结构
bytes32 private constant PERMIT_TYPE_HASH =
keccak256("Permit(address owner,address spender,uint256 value)");
// Domain 结构+值
bytes32 public DOMAIN_SEPARATOR;
constructor(string memory name, string memory version) {
DOMAIN_SEPARATOR = _buildDomainSeparator(name, version);
}
// 实际调用业务
// 根据传入数据,恢复出来消息 Hash ,验签
function permitAndDoSomething(
address owner,
address spender,
uint256 value,
bytes memory signature
) external {
// 恢复出来消息
bytes32 structHash = keccak256(
abi.encode(
PERMIT_TYPE_HASH,
owner,
spender,
value
)
);
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR, structHash));
address recovered = ECDSA.recover(digest, signature);
require(recovered == owner, "verify signature fail");
//todo do some business logic
}
// 固定好 Domain 结构
function _buildDomainSeparator(string memory name, string memory version) private view returns (bytes32) {
return keccak256(abi.encode(DOMAIN_TYPE_HASH, keccak256(bytes(name)), keccak256(bytes(version)), block.chainid, address(this)));
}
}
contract ERC712VarifyTest is Test {
ERC712Verify public verifyContract;
uint256 private privateKey;
address private signer;
bytes32 public DOMAIN_SEPARATOR;
bytes32 private constant PERMIT_TYPE_HASH =
keccak256("Permit(address owner,address spender,uint256 value)");
// 初始化合约,测试账户和私钥
function setUp() public {
verifyContract = new ERC712Verify("MyApp", "1");
privateKey = 0xA11CE; // 自定义私钥(测试用)
DOMAIN_SEPARATOR = verifyContract.DOMAIN_SEPARATOR(); // 发起调用,获取固定的 Domain 结构 + 值
signer = vm.addr(privateKey); // 用私钥获取 signer 地址
}
// 测试签名验证函数
function testPermitAndDoSomething() public {
address spender = address(0xBEEF);
uint256 value = 100;
// 构造结构体哈希
bytes32 structHash = keccak256(
abi.encode(
PERMIT_TYPE_HASH,
signer,
spender,
value
)
);
// 构造完整的 digest
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01", // EIP-712 指定的固定前缀
DOMAIN_SEPARATOR,
structHash
)
);
// 使用 signer 对 digest 进行签名
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);
bytes memory signature = abi.encodePacked(r, s, v);
// 调用合约,验证签名并执行业务逻辑
vm.expectCall(address(verifyContract), abi.encodeWithSelector(verifyContract.permitAndDoSomething.selector, signer, spender, value, signature));
verifyContract.permitAndDoSomething(signer, spender, value, signature);
}
}
foundry
测试 forge test --match-path test/ERC712/ERC712Test.sol -vvvv
测试通过,签名、验签成功。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!