详解EIP712链下签名

  • Rayer
  • 更新于 2024-05-30 19:23
  • 阅读 2114

EIP712EIP712是以太坊的一次改进提案,旨在将签名的过程从链上转移至链下,节省Gas费。EIP712的完整细节可以参考EIP-712:Typedstructureddatahashingandsigning为什么要用EIP712链下消息签名,链上验证的形式,可以省去多

EIP712

EIP712是以太坊的一次改进提案,旨在将签名的过程从链上转移至链下,节省Gas费。

EIP712的完整细节可以参考

EIP-712: Typed structured data hashing and signing

为什么要用EIP712

  • 链下消息签名,链上验证的形式,可以省去多余的approve操作,节省Gas费。
  • EIP712链下签名的信息可以让用户感知到比较直观的结构化信息,而不是一串无法观察的编码
    • 未使用EIP712

image.png

  • 使用EIP712

image.png

使用EIP712的步骤

第一步,定义你的签名结构体数据类型。

当你需要实现不同的功能的时候,你会需要用不同的结构体存储数据。在签名时你需要一个唯一可以体现你的身份信息的签名,那么你理应有一个自定义的结构体数据。

假设我们需要创建一个邮件的链下签名,我们应该把发送方的地址,接受者的地址,邮件内容作为一个结构体,在构造自己的自定义的逻辑时,可以适当的根据你的功能需求拓展你的结构体。

struct Mail {
    address from;
    address to;
    string contents;
}

第二步,定义你的hashStruct(结构体hash)

  • hashStruct(s : 𝕊) = keccak256(typeHash ‖ encodeData(s)) 其中 typeHash = keccak256(encodeType(typeOf(s)))

注意: typeHash 是给定结构类型的常量,不需要运行时计算。

假设是我们第一步示例中的Email结构体,其计算方法如下

bytes32 typeHash = keccak256("Mail(address from,address to,string contents");

如果结构类型引用其他结构类型(而这些结构类型又引用更多结构类型),则收集引用的结构类型集,按名称排序并附加到编码中。一个示例编码是 Transaction(Person from,Person to,Asset tx)Asset(address token,uint256 amount)Person(address wallet,string name) 。

第三步,定义你的encodeData

这一步是往你刚才定义的struct中添加数据作为你的签名内容,形成一个独一无二的签名用作链上验证。

        bytes32 structHash = keccak256(
            abi.encode(
                typeHash,
                from, 
                to, 
                keccak256(contents)
        )

高效的实现方法

function hashStruct(Mail memory mail) pure returns (bytes32 hash) {

    // Compute sub-hashes
    bytes32 typeHash = MAIL_TYPEHASH;
    bytes32 contentsHash = keccak256(mail.contents);

    assembly {
        // Back up select memory
        let temp1 := mload(sub(mail, 32))
        let temp2 := mload(add(mail, 128))

        // Write typeHash and sub-hashes
        mstore(sub(mail, 32), typeHash)
        mstore(add(mail, 64), contentsHash)

        // Compute hash
        hash := keccak256(sub(mail, 32), 128)

        // Restore memory
        mstore(sub(mail, 32), temp1)
        mstore(add(mail, 64), temp2)
    }
}

第四步,定义你的Domain Separator

domainSeparator = hashStruct(eip712Domain)

其中 eip712Domain 的类型是名为 EIP712Domain 的结构,具有以下一个或多个字段。协议设计者只需要包含对其签名域有意义的字段。未使用的字段被排除在结构类型之外。

  • string name 签名域的用户可读名称,即 DApp 或协议的名称。
  • string version 签名域的当前主要版本。不同版本的签名不兼容。
  • uint256 chainId EIP-155 链 ID。如果用户代理与当前活动链不匹配,则应拒绝签名。
  • address verifyingContract 将验证签名的合约地址。用户代理可以进行特定于合同的网络钓鱼预防。
  • bytes32 salt 协议的消歧义盐。这可以用作最后手段的域分隔符。

第五步,生成摘要hash

到这一步,你已经把你要调用的合约的信息独一无二地表示出来了,同时你也把你要对合约的函数所提交的内容也用签名的形式独一无二的表现出来了,你可以用上述的信息编码,生成的你签名摘要。

    bytes32 digest = keccak256(
        abi.encodePacked(
            "\\x19\\x01",
            domainSeparator,
            structHash
        )
    );

更省Gas的写法可以参考openzepplin的实现

function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 digest) {
    /// @solidity memory-safe-assembly
    assembly {
        let ptr := mload(0x40)
        mstore(ptr, hex"19_01")
        mstore(add(ptr, 0x02), domainSeparator)
        mstore(add(ptr, 0x22), structHash)
        digest := keccak256(ptr, 0x42)
    }
}

第六步,验证签名者

在链上,合约可以获得上述的数据生成一个与你相同的摘要Hash,接下来只要通过你传入的密钥信息来还原出签名者信息,对比签名者信息与发送者地址是否一致,就能确认这些信息是否由你本人提交。加密算法为ECDSA椭圆曲线加密。

 address signer = ECDSA.recover(hash, v, r, s);
        if (signer != owner) {
            revert ERC2612InvalidSigner(signer, owner);
        }

在链下,你的v,r,s由你的私钥签名摘要信息后生成

    (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);

在web3.js中调用流程

var domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
{ name: "salt", type: "bytes32" }
];
var mail = [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "contents", type: "string" }
];
const domainData = {
name: "app",
version: "1",
chainId: 3,
verifyingContract: cardExchangeContract,
salt: "0xa222082684812afae4e093416fff16bc218b569abe4db590b6a058e1f2c1cd3e"
};
var message = {
from: [0xzch....fsha],
to: [0xzsdas....rwjfs],
contents: [this is content]
};
var data = JSON.stringify({
types: {
EIP712Domain: domain,
Mail: mail,
},
domain: domainData,
primaryType: "Mail",
message: message
});
window.web3.currentProvider.sendAsync({
method: "eth_signTypedData_v4",
params: [address, data],
from: address
}, function(error, result) {
if (error) {
errorCallback();
} else {
const signature = result.result.substring(2);
const r = "0x" + signature.substring(0, 64);
const s = "0x" + signature.substring(64, 128);
const v = parseInt(signature.substring(128, 130), 16);
successCallback(signature, r, s, v);
}
});

总结

如果你需要使用EIP712,你需要准备三个数据,DomainSeparatorTypedDataHash你的密钥v,r,s

  • DomainSeparator用于唯一地标识你的合约
  • TypedDataHash用于唯一地调用你要使用的函数
  • 密钥v,r,s用于验证你的身份

涉及的安全问题

Replay attacks  重放攻击

该标准仅涉及签名消息和验证签名。在许多实际应用中,签名消息用于授权操作,例如令牌交换。实施者确保应用程序在两次看到相同的签名消息时行为正确,这一点非常重要。例如,重复的消息应该被拒绝或者授权的操作应该是幂等的。其实现方式取决于应用程序,超出了本标准的范围。

Frontrunning attacks   抢先攻击

可靠地广播签名的机制是特定于应用程序的,超出了本标准的范围。当签名被广播到区块链以在合约中使用时,应用程序必须能够抵御抢先交易攻击。在这种攻击中,攻击者拦截签名并将其在原始预期用途发生之前提交给合约。当攻击者首先提交签名时,应用程序应正确运行,例如拒绝签名或简单地产生与签名者预期完全相同的效果。

点赞 3
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Rayer
Rayer
0x156c...41d0
希望能共同成长,有工作机会和技术交流沟通可以联系VX:cHenYuBiz