EIP712来了:期待什么以及如何使用它

  • weijiek
  • 发布于 2020-01-22 19:24
  • 阅读 20

本文详细介绍了以太坊的EIP712标准,旨在提高钱包的签名安全性与可用性。文章探讨了实施该标准所需的步骤,包括数据结构设计、域分隔符的构建、签名代码的编写及签名验证等内容,并提供了详细的JavaScript和Solidity代码示例,适合开发者学习与实践。文章指出EIP712使得用户在签署消息时能更清晰地理解所签署的内容,从而减少潜在的安全风险。

Ethereum 钱包如 MetaMask 将很快引入 EIP712 标准,用于类型化消息签名。该标准允许钱包以结构化和可读的格式显示签名提示中的数据。EIP712 是安全性和可用性的一大进步,因为用户不再需要对不可理解的十六进制字符串进行签名,这种做法可能会让人感到困惑并且不安全。

智能合约和 dApp 开发者应当采纳这一新标准,因为它已被合并入 以太坊改进提案库,主要的钱包提供商也将很快支持它。本文旨在帮助开发者做到这一点。它包括了标准的描述、示例 JavaScript 和 Solidity 代码以及一个工作演示。

在 EIP712 之前

图 1:来自不使用 EIP712 的 dApp 的签名请求

在加密货币领域有句谚语:不要相信; 验证。 然而,在 EIP712 之前,用户很难验证他们被要求签署的数据,这使他们很容易对使用签名消息作为重要价值转移基础的 dApp 给予过多信任。

例如,图 1 显示了一个由去中心化交易所触发的 MetaMask 弹出窗口,该交易所要求用户签署订单的哈希,以将其安全地与他们的钱包地址关联。不幸的是,由于这个哈希是一个十六进制字符串,没有显著技术专长的用户无法轻易验证它是否真的是他们意图订单的哈希。对普通用户来说,盲目相信 dApp 并点击“签名”要容易得多,而不是自己经历重建密码学哈希的技术麻烦。这对安全性是有害的。

如果用户不小心登录一个恶意钓鱼 dApp,它可能会让他们签署错误的订单信息。例如,它可能会欺骗他们以不合理的高价格购买 Ether,而这个交易的实际费用要低得多。为了防止此类攻击,用户必须有某种方法确切知道他们正在签署的内容,而无需自己去重构一个密码学哈希。

EIP712 的实际应用

图 2:来自使用 EIP712 的 dApp 的签名请求

EIP712 在可用性和安全性方面提供了显著的改进。与上述示例相反,当一个支持 EIP712 的 dApp 请求签名时,用户的钱包会展示预先哈希的原始数据,用户可以选择签署。这使用户更容易验证。

如何实现 EIP712

这一新标准引入了几个概念,开发者需要对此熟悉,因此本节将重点介绍在 dApp 中实现 EIP712 所需了解的内容。

假设你正在构建一个去中心化拍卖 dApp,其中投标者在链下签署出价,而智能合约在链上验证这些签名的出价。

1. 设计数据结构

首先,确定你希望用户签名的数据的 JSON 结构。为了这个例子,我们使用以下结构:

{
    amount: 100,
    token: “0x….”,
    id: 15,
    bidder: {
        userId: 323,
        wallet: “0x….”
    }
}

然后我们可以从上述片段中衍生出两个数据结构:Bid,它包括以 ERC20 token 计量的出价 amount 和拍卖 id,以及 Identity,它指定了 userIDwallet 地址。

接下来,将 BidIdentity 作为 structs 在 Solidity 代码中使用。请参阅 EIP712 标准 可获取一系列原生数据类型,如 addressbytes32uint256 等等。

Bid: {
    amount: uint256,
    bidder: Identity
}Identity: {
    userId: uint256,
    wallet: address
}

2. 设计你的域分隔符

下一步是创建一个 域分隔符。此强制字段有助于防止针对一个 dApp 的签名在另一个 dApp 中工作。正如 EIP712 解释

有可能两个 DApps 拥有一个相同的结构,例如 Transfer(address from,address to,uint256 amount),但它们不应兼容。通过引入一个域分隔符,dApp 开发者可以确保没有签名碰撞。

域分隔符在架构和实施层面需要仔细考虑和努力。开发者和设计师必须决定包括或排除以下哪个字段,以基于他们用例的合理性。

name:dApp 或协议的名称,例如“CryptoKitties”

version:当前这个标准所称的“签名域”的版本。可以是你的 dApp 或平台的版本号。这防止一个版本的签名在其他版本中工作。

chainIdEIP-155 链 ID。阻止应用于一个网络(如测试网)的签名在另一个网络(如主网)中工作。

verifyingContract:将验证结果签名的合约的以太坊地址。在 Solidity 中,this 关键字返回合约自己的地址,它可以在验证签名时使用。

salt:一个唯一的 32 字节值,硬编码在合约和 dApp 中,作为最终手段以区分该 dApp 与其他 dApp。

实际上,使用所有上述字段的域分隔符可能如下所示:

{
    name: "My amazing dApp",
    version: "2",
    chainId: "1",
    verifyingContract: "0x1c56346cd2a2bf3202f771f50d3d14a367b48070",
    salt: "0x43efba6b4ccb1b6faa2625fe562bdd9a23260359"
}

关于 chainId 有一点需要注意的是,钱包提供商应防止签名如果与当前连接的网络不匹配。但由于钱包提供商可能不一定强制执行这一点,因此必须在链上验证 chainId。唯一的警告是合约无法以任何方式找出它们在什么链 ID,因此开发人员必须将 chainId 硬编码到他们的合约中,并要特别注意确保它对应于他们部署的网络。

编辑 (2019 年 5 月 31 日):如果 EIP-1344 在未来的以太坊升级中被纳入(可能是 Istanbul),将会有一种方法让合约程序性地发现 chainId

2.1. 安装 MetaMask 版本 4.14.0 或更高版本

在 MetaMask 的版本 4.14.0 发布之前,其 EIP712 支持由于 ETHSanFrancisco 周末的回退而略有不稳定。从此以后,版本 4.14.0 及更高版本应正确支持 EIP712 签名。

3. 为你的 dApp 编写签名代码

你的 JavaScript dApp 现在需要能够请求 MetaMask 签署你的数据。首先,定义你的数据类型:

const domain = [\
    { name: "name", type: "string" },\
    { name: "version", type: "string" },\
    { name: "chainId", type: "uint256" },\
    { name: "verifyingContract", type: "address" },\
    { name: "salt", type: "bytes32" },\
];const bid = [\
    { name: "amount", type: "uint256" },\
    { name: "bidder", type: "Identity" },\
];const identity = [\
    { name: "userId", type: "uint256" },\
    { name: "wallet", type: "address" },\
];

接下来,定义你的域分隔符和消息数据。

const domainData = {
    name: "My amazing dApp",
    version: "2",
    chainId: parseInt(web3.version.network, 10),
    verifyingContract: "0x1C56346CD2A2Bf3202F771f50d3D14a367B48070",
    salt: "0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558"
};var message = {
    amount: 100,
    bidder: {
        userId: 323,
        wallet: "0x3333333333333333333333333333333333333333"
    }
};

像这样布局你的变量:

const data = JSON.stringify({
    types: {
        EIP712Domain: domain,
        Bid: bid,
        Identity: identity,
    },
    domain: domainData,
    primaryType: "Bid",
    message: message
});

接下来,向 web3 发出 eth_signTypedData_v3 签名调用:

web3.currentProvider.sendAsync(
{
    method: "eth_signTypedData_v3",
    params: [signer, data],
    from: signer
},
function(err, result) {
    if (err) {
        return console.error(err);
    }    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);    // 现在签名由 r、s 和 v 组成。
    }
);

请注意,截至撰写时,MetaMask 和 Cipher Browser 在方法字段中使用 eth_signTypedData_v3 以允许向后兼容,同时 dApp 生态系统采纳该标准。这些钱包的未来版本很可能将其重命名为 eth_signTypedData

4. 为验证合约编写认证代码

回想一下,在钱包提供商签署 EIP712 类型的数据之前,它会先格式化并哈希化它。因此,你的合约也需要能够做到这一点,以便使用 ecrecover 确定签署了哪个地址,你必须在 Solidity 合约代码中复制此格式化/哈希函数。这可能是过程中的最棘手一步,因此必须准确而小心。

首先,在 Solidity 中声明你的数据类型,这你应该已经在上面完成:

struct Identity {
    uint256 userId;
    address wallet;
}struct Bid {
    uint256 amount;
    Identity bidder;
}

接下来,定义适合你的数据结构的类型哈希。请注意,逗号和括号后面没有空格,名称和类型应与上述 JavaScript 代码中指定的完全匹配。

string private constant IDENTITY_TYPE = "Identity(uint256 userId,address wallet)";
string private constant BID_TYPE = "Bid(uint256 amount,Identity bidder)Identity(uint256 userId,address wallet)";

还要定义域分隔符类型哈希。请注意,下面的代码在 chainId 为 1 时是为合约在主网上部署而设计的,并且字符串(例如“我惊人的 dApp”)必须被哈希。

uint256 constant chainId = 1;address constant verifyingContract = 0x1C56346CD2A2Bf3202F771f50d3D14a367B48070;bytes32 constant salt = 0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558;string private constant EIP712_DOMAIN = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)";bytes32 private constant DOMAIN_SEPARATOR = keccak256(abi.encode(
    EIP712_DOMAIN_TYPEHASH,
    keccak256("My amazing dApp"),
    keccak256("2"),
    chainId,
    verifyingContract,
    salt
));

接下来,为每种数据类型编写哈希函数:

function hashIdentity(Identity identity) private pure returns (bytes32) {
    return keccak256(abi.encode(
        IDENTITY_TYPEHASH,
        identity.userId,
        identity.wallet
    ));
}function hashBid(Bid memory bid) private pure returns (bytes32){
    return keccak256(abi.encodePacked(
        "\\x19\\x01",
       DOMAIN_SEPARATOR,
       keccak256(abi.encode(
            BID_TYPEHASH,
            bid.amount,
            hashIdentity(bid.bidder)
        ))
    ));

最后但同样重要的是,编写你的签名验证函数:

function verify(address signer, Bid memory bid, sigR, sigS, sigV) public pure returns (bool) {
    return signer == ecrecover(hashBid(bid), sigV, sigR, sigS);
}

工作演示

有关上述代码的工作演示,请使用 此工具。安装支持 EIP712 的版本的 MetaMask 后,单击页面上的按钮以运行 JavaScript 代码以触发签名请求弹出窗口。单击签署,Solidity 代码将出现在文本框中。

这段代码将包含上述所有哈希代码、MetaMask 生成的签名及你的钱包地址。如果你将其复制并粘贴到 Remix IDE 中,选择 JavaScript VM 环境,然后运行 verify 函数,Remix 将在代码中运行 ecrecover 以获取签署者的地址,将结果与你的钱包地址进行比较,并返回 true 如果它们匹配。

请注意,为了简化起见,此演示生成的 verify 函数与上述给出的示例有所不同,因为 MetaMask 生成的签名将动态插入其中。

图 3:运行验证函数时 Remix 显示的内容

在实际操作中,这就是你的智能合约代码应该如何验证签署的数据。欢迎根据你的需要调整代码。希望这可以为你在为自己的数据结构编写哈希函数时节省时间。

关于 MetaMask 中 “遗留” EIP712 支持的说明

还有一点需要注意的是,当 MetaMask 发布对 EIP712 的支持时,它将不再支持一项实验性“遗留”的类型化数据签名功能,如 2017 年 10 月的这篇博客文章 所述。

编辑(9 月 29 日):据我所知,一旦 MetaMask 使 eth_signTypedData 指向全面的 EIP712 支持,它将通过 eth_signTypedData_v1 调用支持遗留类型化数据签名。

最后说明

总之,EIP712 支持即将到来,开发者应当利用这一点。它显著提高了可用性并帮助防止钓鱼。尽管目前实现有些棘手,但我们希望这篇文章和示例代码能帮助开发者在他们自己的 dApp 和合约中采用它。

感谢

本文由 Koh Wei Jie 撰写,他曾是 ConsenSys 新加坡的一名全栈开发者。非常感谢 Paul Bouchon 和 Dan Finlay 的宝贵反馈和评论。

  • 原文链接: medium.com/metamask/eip7...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
weijiek
weijiek
江湖只有他的大名,没有他的介绍。