EIP-712: Ethereum Typed Structured Data Hashing and Signing
EIP-712(Ethereum Improvement Proposal 712)是以太坊生态系统中的一个标准,旨在为以太坊智能合约中的数据提供类型化的结构化哈希和签名。它使得用户可以对易于理解的结构化数据进行签名,提高了签名数据的安全性和可读性。
背景
在EIP-712之前,签名消息的格式和处理方式相对简单。用户需要对一个简单的字符串或十六进制数据进行签名,然后将签名与原始数据一起发送到智能合约。这种方法存在两个主要问题:
- 可读性差:签名的数据通常很难理解,用户可能无法知道他们签名的确切含义。
- 安全性低:由于数据的表示不唯一,攻击者可以通过改变数据的结构来欺骗用户签署错误的信息。
EIP-712通过引入类型化的结构化数据来解决这些问题,使签名过程更加安全和易于理解。
EIP-712规范
EIP-712规范主要定义了以下几个部分:
1. 数据结构
EIP-712中的数据结构基于Solidity的结构定义。数据结构中的每个字段都有一个类型(例如uint256,address,string等)和一个名称。例如:
struct Person {
string name;
uint256 age;
}此外,EIP-712还支持嵌套结构和数组,这使得数据结构更加灵活。
2. 域分隔符(Domain Separator)
域分隔符是一个包含有关签名上下文的结构化数据对象,主要用于区分不同的应用程序和合约实例。这样可以防止重放攻击,因为签名者签名的数据与特定的应用程序和合约实例相关联。
域分隔符包含以下字段:
name:应用程序或智能合约的名称。version:应用程序或智能合约的版本。chainId:以太坊链的ID。verifyingContract:正在验证签名的智能合约地址。salt:一个随机值,用于确保每个合约实例具有唯一的域分隔符。
3. 数据的哈希和编码
EIP-712定义了如何对类型化的结构化数据进行哈希和编码。首先,需要对数据结构的类型和字段进行编码,然后对实际的数据值进行编码。这样可以确保数据的表示具有唯一性,提高签名的安全性。
编码过程包括以下几个步骤:
-
对数据结构的类型和字段进行编码。这是通过将结构定义转换为类似于Solidity函数签名的格式来实现的。例如,上述
Person结构的编码将是Person(string name,uint256 age)。 -
对实际的数据值进行编码。这是通过将数据结构中的每个字段值编码为ABI编码的顺序排列来实现的。例如,如果
Person结构的实例是{name: "Alice", age: 30},则编码后的数据将是["Alice", 30]。 -
将类型编码和值编码组合在一起,然后计算结果的keccak256哈希。这将产生结构化数据的唯一表示。
4. 签名过程
EIP-712签名过程包括以下几个步骤:
-
计算域分隔符的哈希。这是通过对域分隔符数据结构进行编码和哈希来实现的。
-
计算结构化数据的哈希。
-
将域分隔符哈希和数据哈希组合在一起,然后计算结果的keccak256哈希。
-
使用以太坊私钥对最终哈希进行签名。
签名完成后,可以将签名与原始数据一起发送到智能合约。智能合约可以使用签名者的公钥恢复签名以验证其有效性。
EIP-712使用案例
EIP-712可用于各种以太坊智能合约和DApp场景,例如:
-
身份验证:用户可以对包含其个人信息的结构化数据进行签名,以证明自己的身份。这对于去中心化身份管理系统非常有用。
-
授权:用户可以对特定操作(例如投票、贷款、交易等)的结构化数据进行签名,以授权智能合约执行该操作。
-
订单签署:在去中心化交易所(DEX)中,用户可以对交易订单的结构化数据进行签名,以创建一个有效的交易请求。
身份验证
为了演示EIP-712如何应用于身份验证,我们将创建一个简单的智能合约,该合约允许用户通过对包含其个人信息的结构化数据进行签名来证明其身份。我们将遵循以下步骤:
1. 定义域分隔符和数据结构
首先,我们需要定义一个身份验证请求的数据结构以及与智能合约相关的域分隔符。在Solidity合约中,我们可以这样做:
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol";
contract IdentityVerification is EIP712 {
struct Identity {
string name;
uint256 age;
string country;
uint256 timestamp;
}
bytes32 private constant IDENTITY_TYPEHASH = keccak256("Identity(string name,uint256 age,string country,uint256 timestamp)");
constructor() EIP712("IdentityVerification", "1") {}
// ...
}在这个例子中,我们定义了一个名为Identity的结构,其中包含用户的姓名、年龄和国籍,以及请求的时间戳。我们还计算了这个结构的类型哈希。
2. 实现签名验证功能
接下来,我们需要在合约中实现一个方法来验证用户对Identity结构实例的签名。这将包括以下步骤:
- 计算数据的哈希。
- 从签名中恢复签名者的地址。
- 验证签名者地址是否与期望的地址匹配。
我们可以在Solidity合约中实现这个功能,如下所示:
// ...
function verifyIdentity(
address signer,
string memory name,
uint256 age,
string memory country,
uint256 timestamp,
uint8 v,
bytes32 r,
bytes32 s
) public view returns (bool) {
Identity memory identity = Identity(name, age, country, timestamp);
bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(IDENTITY_TYPEHASH, keccak256(bytes(name)), age, keccak256(bytes(country)), timestamp)));
address recoveredAddress = ecrecover(digest, v, r, s);
return recoveredAddress == signer;
}
// ...在这个例子中,我们首先使用EIP-712规范计算数据的哈希。然后,我们使用ecrecover函数从签名中恢复签名者的地址,并将其与提供的signer地址进行比较。如果这两个地址相同,那么我们认为签名是有效的。
3. 使用EIP-712对数据进行签名
为了与智能合约交互,用户需要使用其私钥对一个Identity结构实例进行签名。这可以在前端应用程序中使用Web3.js或Ethers.js库完成。以下是使用Ethers.js进行签名的示例代码:
import { ethers } from "ethers";
async function signIdentity(identity, privateKey) {
const domain = {
name: "IdentityVerification",
version: "1",
chainId: 1, // Use the correct chainId for the deployed contract
verifyingContract: "0x...", // Address of the deployed IdentityVerification contract
};
const types = {
Identity: [
{ name: "name", type: "string" },
{ name: "age", type: "uint256" },
{ name: "country", type: "string" },
{ name: "timestamp", type: "uint256" },
],
};
const signer = new ethers.Wallet(privateKey);
const data = {
types,
domain,
primaryType: "Identity",
message: identity,
};
const signature = await signer._signTypedData(domain, types, identity);
return signature;
}
这个signIdentity函数接收一个Identity结构实例和一个私钥,然后使用Ethers.js库的_signTypedData方法对其进行签名。请注意,您需要使用与合约部署的网络相对应的chainId。
4. 在前端应用程序中验证签名
有了签名后,我们可以在前端应用程序中调用智能合约的verifyIdentity方法来验证签名是否有效。以下是一个示例:
async function verifySignature(identity, signature, contract) {
const [v, r, s] = ethers.utils.splitSignature(signature);
const signerAddress = "0x..."; // Address corresponding to the private key used for signing
const result = await contract.verifyIdentity(
signerAddress,
identity.name,
identity.age,
identity.country,
identity.timestamp,
v,
r,
s
);
if (result) {
console.log("Signature is valid!");
} else {
console.log("Signature is invalid!");
}
}这个verifySignature函数接收一个Identity结构实例、一个签名和一个IdentityVerification合约的实例。我们首先使用Ethers.js的splitSignature方法将签名拆分为v、r和s组件。然后,我们调用合约的verifyIdentity方法并传入相应的参数。如果返回结果为true,则签名有效;否则,签名无效。
授权
1. 创建智能合约
首先,我们需要创建一个用于授权的智能合约。在这个合约中,我们定义一个Authorization结构,以及一个用于验证EIP-712签名的方法。
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol";
contract Authorization is EIP712 {
struct Authorization {
address grantor;
address grantee;
uint256 expiry;
}
bytes32 private constant AUTHORIZATION_TYPEHASH =
keccak256("Authorization(address grantor,address grantee,uint256 expiry)");
constructor() EIP712("Authorization", "1") {}
function verifyAuthorization(
address grantor,
address grantee,
uint256 expiry,
uint8 v,
bytes32 r,
bytes32 s
) external view returns (bool) {
bytes32 structHash =
keccak256(abi.encode(AUTHORIZATION_TYPEHASH, grantor, grantee, expiry));
bytes32 digest = _hashTypedDataV4(structHash);
address signer = ECDSA.recover(digest, v, r, s);
return signer == grantor;
}
}在这个合约中,我们使用了OpenZeppelin的EIP712合约。我们定义了一个Authorization结构,它包含了授权者(grantor)、被授权者(grantee)以及授权过期时间。我们还定义了一个verifyAuthorization方法,该方法使用EIP-712签名验证授权。
2. 签名授权
接下来,我们需要在前端应用程序中创建一个授权对象,并使用EIP-712对其进行签名。我们可以使用Ethers.js库来实现这一点。
const ethers = require("ethers");
async function signAuthorization(authorization, privateKey) {
const domain = {
name: "Authorization",
version: "1",
chainId: 1, // Use the correct chainId for the deployed contract
verifyingContract: "0x...", // Address of the deployed Authorization contract
};
const types = {
Authorization: [
{ name: "grantor", type: "address" },
{ name: "grantee", type: "address" },
{ name: "expiry", type: "uint256" },
],
};
const signer = new ethers.Wallet(privateKey);
const data = {
types,
domain,
primaryType: "Authorization",
message: authorization,
};
const signature = await signer._signTypedData(domain, types, authorization);
return signature;
}这个signAuthorization函数接收一个Authorization结构实例和一个私钥,然后使用Ethers.js库的_signTypedData方法对其进行签名。注意要使用与合约部署的网络相对应的chainId。
3. 验证签名
有了签名后,我们可以在前端应用程序中调用智能合约的“verifyAuthorization方法来验证签名是否有效。首先,我们需要将签名分解为v, r, 和s`组件,然后调用智能合约的方法。
const ethers = require("ethers");
async function verifySignature(contract, authorization, signature) {
const sig = ethers.utils.splitSignature(signature);
const isValid = await contract.verifyAuthorization(
authorization.grantor,
authorization.grantee,
authorization.expiry,
sig.v,
sig.r,
sig.s
);
return isValid;
}这个verifySignature函数接收一个已部署的Authorization智能合约实例、一个Authorization结构实例和一个签名。它将签名拆分为v, r, 和s组件,然后调用verifyAuthorization方法来验证签名。
示例:
现在我们可以在一个完整的例子中展示如何使用EIP-712签名来处理授权。
async function main() {
// Replace with your privateKey and contract address
const privateKey = "0x...";
const contractAddress = "0x...";
const provider = new ethers.providers.JsonRpcProvider("http://localhost:8545");
const wallet = new ethers.Wallet(privateKey, provider);
const Authorization = await ethers.getContractFactory("Authorization");
const contract = Authorization.attach(contractAddress).connect(wallet);
const authorization = {
grantor: wallet.address,
grantee: "0x...",
expiry: Date.now() + 60 * 60 * 1000, // Expires in 1 hour
};
const signature = await signAuthorization(authorization, privateKey);
console.log("Signature:", signature);
const isValid = await verifySignature(contract, authorization, signature);
console.log("Is signature valid?", isValid);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});这个示例将演示如何创建一个授权对象、使用EIP-712对其进行签名,然后调用智能合约的verifyAuthorization方法来验证签名是否有效。
总结
EIP-712为以太坊生态系统中的结构化数据签名提供了一种类型化的方法,提高了签名数据的安全性和可读性。通过使用EIP-712,开发人员可以更轻松地为其智能合约和DApp实现安全、可靠且易于理解的签名功能。