本文介绍了 EIP-712 的原理、作用以及如何使用 EIP-712 实现安全的链下签名,使得钱包能够显示可读的信息,合约可以在链上验证签名。同时,通过一个 Go 语言和 Solidity 语言的例子,展示了如何在 Polygon Amoy 测试网上验证 EIP-712 签名,并介绍了基于 EIP-712 构建的 EIP-2612 Permit 签名流程。
签名原始字节 blobs 可能适用于测试,但实际应用程序需要的不仅仅是这些。用户应该知道他们批准的内容,而不仅仅是信任一个随机的十六进制字符串。
EIP-712 为以太坊签名带来了结构化,让钱包可以显示可读的字段,如
owner、spender和amount,同时生成可验证的链上哈希。在这篇文章中,我们将分解它的工作原理,它是如何支持 EIP-2612 "permit" 的,并演练一个在 Polygon Amoy 测试网上完整的 Go + Solidity 验证示例。
签名一个不透明的字节 blob 很容易,但在实际应用中,我们需要签名具有丰富结构的 message,其中包含诸如 "amount"、"recipient" 或 "order ID" 等字段,而不仅仅是随机的十六进制。如果你自己编写哈希算法,很容易犯错,从而破坏安全性。
EIP‑712 通过定义一种清晰、经过同行评审的方式来解决这个问题,即将你的数据结构转换为钱包可以以人类可读形式显示的摘要。这意味着用户可以准确地看到他们正在签名的内容,合约可以在链上验证它,并且你可以通过在链下进行授权而不是在链上进行授权来节省 gas。
工作原理:
扩展的签名范围
编码模式
"\x19\x01" ‖ domainSeparator ‖ hashStruct(message)前导字节始终不同,因此每种情况都是明确的。
类型化数据
struct Mail { address from; address to; string contents; }
uint8–256,int8–256,bytes1–32,bool,addressbytes,stringType[n] / Type[]),嵌套结构体hashStruct
typeHash = keccak256(encodeType("Mail(address from,address to,string contents)"))encodeData = 将每个字段打包到 32 字节的槽中(动态类型的先进行哈希)hashStruct(msg) = keccak256(typeHash ‖ encodeData(msg))域名分隔符(Domain Separator)
EIP712Domain 结构体(例如 { name, version, chainId, verifyingContract }),作为整体进行一次哈希domainSeparator = hashStruct(eip712Domain);
签名和验证
eth_signTypedData_v4(name, types, domain, message) → 钱包显示清晰的字段并返回签名digest = keccak256("\x19\x01"‖domainSeparator‖hashStruct(message)) 并使用 ecrecover 来验证签名者。注意:有关更详细的规范,你可以在这里阅读
Permit
EIP‑2612 “permit” 构建在 EIP‑712 的 类型化结构化数据签名之上。它定义了一个 Permit(owner, spender, value, nonce, deadline) 结构体,钱包使用 EIP‑712 域名分隔符对其进行哈希,呈现给用户以供批准,并在链下签名。token 的 permit(owner, spender, value, deadline, v, r, s) 函数然后通过 EIP‑712 重新计算相同的摘要,使用 ecrecover 来验证持有者的签名和 nonce,并原子地设置 allowance[owner][spender] = value。通过在底层利用 EIP‑712,permit 实现了一种清晰、安全、gas 高效的 UX,而无需单独的链上 approve(...) 调用。
注意: 想要支持这种授权方式的 Token 必须实现
permit方法。
例子:
在这个例子中,我们将编写一个智能合约,根据前面解释的规范来验证 EIP-712。
智能合约视角:
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract PermitVerifier is EIP712 {
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
bytes32 public constant PERMIT_TYPEHASH =
0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
constructor() EIP712("MyDApp", "1") {}
function verifyPermit(
address owner,
address spender,
uint256 value,
uint256 nonce,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external view returns (bool) {
bytes32 structHash = keccak256(
abi.encode(
PERMIT_TYPEHASH,
owner,
spender,
value,
nonce,
deadline
)
);
bytes32 digest = _hashTypedDataV4(structHash);
return ECDSA.recover(digest, v, r, s) == owner;
}
}
钱包视角:
package main
const (
// Polygon Amoy 测试网的公共 RPC URL
NodeRPCURL = "https://polygon-amoy.drpc.org"
AmoyChainID = 80002 // Polygon Amoy 测试网 Chain ID
)
func main() {
// 1) 连接到 Amoy
client, err := ethclient.Dial("https://polygon-amoy.drpc.org")
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// 2) 准备用户签名的相同的 EIP‑712 TypedData
acc2Addr, acc2Priv := account.GetAccount(2)
verifierAddr := common.HexToAddress("0xf80bb731f8ba49624dce8edb1a8188782287ff1e")
domain := apitypes.TypedDataDomain{
Name: "MyDApp",
Version: "1",
ChainId: math.NewHexOrDecimal256(AmoyChainID),
VerifyingContract: verifierAddr.Hex(),
}
types := apitypes.Types{
"EIP712Domain": {
{Name: "name", Type: "string"},
{Name: "version", Type: "string"},
{Name: "chainId", Type: "uint256"},
{Name: "verifyingContract", Type: "address"},
},
"Permit": {
{Name: "owner", Type: "address"},
{Name: "spender", Type: "address"},
{Name: "value", Type: "uint256"},
{Name: "nonce", Type: "uint256"},
{Name: "deadline", Type: "uint256"},
},
}
deadline := big.NewInt(time.Now().Add(time.Hour).Unix())
nonce := big.NewInt(0)
message := apitypes.TypedDataMessage{
"owner": acc2Addr.Hex(),
"spender": acc2Addr.Hex(),
"value": "1000000000000000000",
"nonce": nonce.String(),
"deadline": deadline.String(),
}
typedData := apitypes.TypedData{
Types: types,
PrimaryType: "Permit",
Domain: domain,
Message: message,
}
// 3) 签名或提供你现有的 (v,r,s)
domainSep, _ := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
msgHash, _ := typedData.HashStruct("Permit", typedData.Message)
digest := crypto.Keccak256(
[]byte("\x19\x01"),
domainSep,
msgHash,
)
sig, _ := crypto.Sign(digest, acc2Priv)
r := common.BytesToHash(sig[:32])
s := common.BytesToHash(sig[32:64])
v := uint8(sig[64]) + 27
// 4) ABI‑encode verifyPermit(owner,spender,value,nonce,deadline,v,r,s)
verifierABI := `[{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"verifyPermit","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}]`
parsed, _ := abi.JSON(strings.NewReader(verifierABI))
calldata, _ := parsed.Pack(
"verifyPermit",
acc2Addr,
acc2Addr, // spender (必须与签名内容匹配)
big.NewInt(1e18),
nonce,
deadline,
v, r, s,
)
// 5) 进行 eth_call
msg := ethereum.CallMsg{
To: &verifierAddr,
Data: calldata,
}
res, err := client.CallContract(ctx, msg, nil)
if err != nil {
log.Fatal(err)
}
// 6) 解码 bool 结果
out, err := parsed.Unpack("verifyPermit", res)
if err != nil {
log.Fatal(err)
}
fmt.Println("Signature valid?", out[0].(bool))
if !out[0].(bool) {
log.Fatal("Signature verification failed")
}
fmt.Printf("Signature verified successfully for owner: %s\n", acc2Addr.Hex())
}
你应该看到的输出:
Signature verified successfully for owner: <你的地址>
在这个例子中,我们演练了一个纯粹的 只读 演示,以及如何在链下检查 EIP‑712 签名的直觉。在真正的 permit 流程中,你不会止步于 verifyPermit,你实际上必须在链上调用 token 的 permit(...) 方法来:
allowance(owner, spender)只有在 permit(...) 调用之后,后续的 transferFrom(...) 才会成功。我们的示例展示了核心的签名和恢复逻辑,但生产环境的集成必须调用 tokenPermit.permit(...)。
EIP-712 为以太坊开发者提供了一种可靠的方式,可以从盲签名过渡到结构化、可验证的消息。它定义了如何哈希和签名类型化数据,以便钱包可以显示可读信息,合约可以信任链下授权,用户可以免受误导性提示的侵害。
通过将 EIP-712 与 EIP-2612 的 permit 流程 相结合,应用程序可以让用户在链下批准 token 授权,并在一次调用中在链上确认它们,从而降低 gas 成本并改善 UX。
该标准现在支撑着大多数现代签名模式,从 DEX 订单到 meta-transactions 和后端验证的 swaps。理解如何构建和验证这些摘要不仅有用,而且对于任何构建安全、用户友好的以太坊集成的开发者来说都是必不可少的。
- 原文链接: medium.com/@andrey_obruc...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!