EIP-712:类型化结构化数据的哈希与签名
1. 引言
以太坊 EIP-712: 类型化结构化数据的哈希与签名,是一种用于对类型化结构化数据(而不仅仅是字节串)进行哈希和签名 的标准。
其包括:
- 编码函数正确性的理论框架,
- 类似于 Solidity 结构体并兼容的结构化数据规范,
- 对这些结构实例的安全哈希算法,
- 将这些实例安全地包含在可签名消息集合中的方法,
- 一种可扩展的域分离机制,
- 新的 RPC 调用
eth_signTypedData, - EVM 中该哈希算法的优化实现。
该标准不包含重放保护机制。
2. 动机
如果只关注字节串,数据签名已经是一个被解决的问题。不幸的是,在现实世界中,人们关心的是复杂且有意义的消息。对结构化数据进行哈希并非易事,错误可能会导致系统安全性的丧失。
因此,“don’t roll your own crypto 不要自己实现加密算法”这一原则适用。相反,需要使用经过同行评审和充分测试的标准方法。本 EIP 旨在成为这样的标准。
本 EIP 旨在改善链下消息签名在链上的可用性。链下消息签名的采用正在增加,因为它节省了 Gas 并减少了区块链上的交易数量。目前,已签名的消息是一个不透明的十六进制字符串,用户无法理解消息的组成内容。

在此,概述了一种方案,以编码数据及其结构,使其在签名时可供用户验证。下面是根据本提案,用户在签署消息时可能会看到的示例界面。

3. 规范
可签名消息集合从交易和字节串 𝕋 ∪ 𝔹⁸ⁿ 扩展到包括结构化数据 𝕊。新的可签名消息集合为 𝕋 ∪ 𝔹⁸ⁿ ∪ 𝕊。这些消息被编码为适合哈希和签名的字节串,如下所示:
encode(transaction : 𝕋) = RLP_encode(transaction)encode(message : 𝔹⁸ⁿ) = "\x19Ethereum Signed Message:\n" ‖ len(message) ‖ message
其中len(message)是message字节数的 非零填充 ASCII 十进制编码。encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19\x01" ‖ domainSeparator ‖ hashStruct(message)
其中domainSeparator和hashStruct(message)定义如下。
此编码是确定性的,因为其组成部分是确定性的。该编码是单射的,因为三种情况的首字节总是不同。(RLP_encode(transaction) 不会以 \x19 开头。)
此编码符合 EIP-191 规范。其中,“版本字节” 固定为 0x01,“版本特定数据” 是 32 字节的域分隔符 domainSeparator,“要签名的数据” 是 32 字节的 hashStruct(message)。
3.1 类型化结构化数据 𝕊 的定义
为了定义所有结构化数据的集合,首先定义可接受的类型。类似于 ABIv2,这些类型与 Solidity 类型密切相关。采用 Solidity 语法有助于解释定义。该标准特定于以太坊虚拟机(EVM),但旨在不依赖于更高级别的语言。如:
struct Mail {address from;address to;string contents;
}
其中:
- 定义:struct 结构体类型
结构体类型的名称是一个有效的标识符,包含零个或多个成员变量。成员变量具有成员类型和名称。 - 定义:member 成员类型
成员类型可以是原子类型、动态类型或引用类型。 - 定义:原子类型
原子类型包括bytes1到bytes32、uint8到uint256、int8到int256、bool和address。这些类型与 Solidity 定义相对应。需要注意的是,没有uint和int的别名。此外,合约地址始终是address。本标准不支持定点数类型,未来版本可能会增加新的原子类型。 - 定义:动态类型
动态类型包括bytes和string。这些类型在类型声明方面类似于原子类型,但它们的编码方式不同。 - 定义:引用类型
引用类型包括数组和结构体。数组可以是固定大小的Type[n]或动态大小的Type[]。结构体是对其他结构体的引用,通过其名称标识。本标准支持递归结构体类型。 - 定义:结构化类型数据
𝕊
𝕊包含所有结构体类型的所有实例。
3.2 hashStruct 的定义
hashStruct 函数定义如下:
hashStruct(s : 𝕊) = keccak256(typeHash ‖ encodeData(s))
其中typeHash = keccak256(encodeType(typeOf(s)))
注意:对于给定的结构体类型,typeHash 是一个常量,无需在运行时计算。
3.3 encodeType 的定义
结构体类型编码为 name ‖ "(" ‖ member₁ ‖ "," ‖ member₂ ‖ "," ‖ … ‖ memberₙ ")",其中每个成员的表示形式为 type ‖ " " ‖ name。
如,上述 Mail 结构体的编码为 Mail(address from,address to,string contents)。
如果结构体类型引用了其他结构体类型(这些结构体类型又进一步引用其他结构体类型),则收集所有引用的结构体类型,按名称排序,并附加到编码中。如:
Transaction(Person from,Person to,Asset tx)Asset(address token,uint256 amount)Person(address wallet,string name)
3.4 domainSeparator 的定义
domainSeparator = hashStruct(eip712Domain)
其中 eip712Domain 的类型是 EIP712Domain 结构体,包含以下字段之一或多个。这些字段用于区分不同的签名域。未使用的字段不会包含在结构体类型中。
string name签名域的用户可读名称,如 DApp 或协议的名称。string version当前签名域的主要版本。不同版本的签名不兼容。uint256 chainIdEIP-155 的链 ID。address verifyingContract用于验证签名的合约地址。bytes32 salt作为协议的区分标识符。
未来的标准扩展可能会增加新的字段,用户代理可据此提供更多安全措施或提示用户。
3.5 eth_signTypedData JSON RPC 规范说明
eth_signTypedData 方法被添加到以太坊 JSON-RPC,类似于 eth_sign 方法。
3.5.1 eth_signTypedData
该签名方法计算以太坊特定的签名:
sign(keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message))),如上所定义。
注意:用于签名的地址必须是解锁状态。
其中参数:
- 1)
Address- 20 字节 - 用于签名消息的账户地址。 - 2)
TypedData- 需要签名的结构化数据。
结构化数据是一个包含类型信息、域分隔符参数和消息对象的 JSON 对象。
以下是 TypedData 参数的 JSON Schema 定义:
{"type": "object","properties": {"types": {"type": "object","properties": {"EIP712Domain": {"type": "array"}},"additionalProperties": {"type": "array","items": {"type": "object","properties": {"name": {"type": "string"},"type": {"type": "string"}},"required": ["name", "type"]}},"required": ["EIP712Domain"]},"primaryType": {"type": "string"},"domain": {"type": "object"},"message": {"type": "object"}},"required": ["types", "primaryType", "domain", "message"]
}
返回值为:
- 返回
DATA类型,即签名结果。
与eth_sign方法相同,签名是一个以0x开头的 65 字节十六进制字符串,编码了以太坊黄皮书附录 F 中的r、s和v参数,采用大端字节序格式:
- 字节 0-31:包含
r参数 - 字节 32-63:包含
s参数 - 最后一个字节:包含
v参数
注意:
v参数包括链 ID,符合 EIP-155 的规范。
示例:
-
请求:
curl -X POST --data '{"jsonrpc":"2.0","method":"eth_signTypedData","params":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", {"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Person":[{"name":"name","type":"string"},{"name":"wallet","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person"},{"name":"contents","type":"string"}]},"primaryType":"Mail","domain":{"name":"Ether Mail","version":"1","chainId":1,"verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},"message":{"from":{"name":"Cow","wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},"to":{"name":"Bob","wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"},"contents":"Hello, Bob!"}}],"id":1}' -
返回结果:
{"id": 1,"jsonrpc": "2.0","result": "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c" }
一个示例展示如何使用 Solidity 的 ecrecover 来验证 eth_signTypedData 计算出的签名,代码可参考 Example.js。
该合约已部署在 Ropsten 和 Rinkeby 测试网络上。
// using ethereumjs-util 7.1.3
const ethUtil = require('ethereumjs-util');// using ethereumjs-abi 0.6.9
const abi = require('ethereumjs-abi');// using chai 4.3.4
const chai = require('chai');const typedData = {types: {EIP712Domain: [{ name: 'name', type: 'string' },{ name: 'version', type: 'string' },{ name: 'chainId', type: 'uint256' },{ name: 'verifyingContract', type: 'address' },],Person: [{ name: 'name', type: 'string' },{ name: 'wallet', type: 'address' }],Mail: [{ name: 'from', type: 'Person' },{ name: 'to', type: 'Person' },{ name: 'contents', type: 'string' }],},primaryType: 'Mail',domain: {name: 'Ether Mail',version: '1',chainId: 1,verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',},message: {from: {name: 'Cow',wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',},to: {name: 'Bob',wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',},contents: 'Hello, Bob!',},
};const types = typedData.types;// Recursively finds all the dependencies of a type
function dependencies(primaryType, found = []) {if (found.includes(primaryType)) {return found;}if (types[primaryType] === undefined) {return found;}found.push(primaryType);for (let field of types[primaryType]) {for (let dep of dependencies(field.type, found)) {if (!found.includes(dep)) {found.push(dep);}}}return found;
}function encodeType(primaryType) {// Get dependencies primary first, then alphabeticallet deps = dependencies(primaryType);deps = deps.filter(t => t != primaryType);deps = [primaryType].concat(deps.sort());// Format as a string with fieldslet result = '';for (let type of deps) {result += `${type}(${types[type].map(({ name, type }) => `${type} ${name}`).join(',')})`;}return result;
}function typeHash(primaryType) {return ethUtil.keccakFromString(encodeType(primaryType), 256);
}function encodeData(primaryType, data) {let encTypes = [];let encValues = [];// Add typehashencTypes.push('bytes32');encValues.push(typeHash(primaryType));// Add field contentsfor (let field of types[primaryType]) {let value = data[field.name];if (field.type == 'string' || field.type == 'bytes') {encTypes.push('bytes32');value = ethUtil.keccakFromString(value, 256);encValues.push(value);} else if (types[field.type] !== undefined) {encTypes.push('bytes32');value = ethUtil.keccak256(encodeData(field.type, value));encValues.push(value);} else if (field.type.lastIndexOf(']') === field.type.length - 1) {throw 'TODO: Arrays currently unimplemented in encodeData';} else {encTypes.push(field.type);encValues.push(value);}}return abi.rawEncode(encTypes, encValues);
}function structHash(primaryType, data) {return ethUtil.keccak256(encodeData(primaryType, data));
}function signHash() {return ethUtil.keccak256(Buffer.concat([Buffer.from('1901', 'hex'),structHash('EIP712Domain', typedData.domain),structHash(typedData.primaryType, typedData.message),]),);
}const privateKey = ethUtil.keccakFromString('cow', 256);
const address = ethUtil.privateToAddress(privateKey);
const sig = ethUtil.ecsign(signHash(), privateKey);const expect = chai.expect;
expect(encodeType('Mail')).to.equal('Mail(Person from,Person to,string contents)Person(string name,address wallet)');
expect(ethUtil.bufferToHex(typeHash('Mail'))).to.equal('0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2',
);
expect(ethUtil.bufferToHex(encodeData(typedData.primaryType, typedData.message))).to.equal('0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8',
);
expect(ethUtil.bufferToHex(structHash(typedData.primaryType, typedData.message))).to.equal('0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e',
);
expect(ethUtil.bufferToHex(structHash('EIP712Domain', typedData.domain))).to.equal('0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f',
);
expect(ethUtil.bufferToHex(signHash())).to.equal('0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2');
expect(ethUtil.bufferToHex(address)).to.equal('0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826');
expect(sig.v).to.equal(28);
expect(ethUtil.bufferToHex(sig.r)).to.equal('0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d');
expect(ethUtil.bufferToHex(sig.s)).to.equal('0x07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b91562');
3.5.2 personal_signTypedData
还应有一个对应的 personal_signTypedData 方法,该方法接受账户的密码作为最后一个参数。
3.6 Web3 API 规范
在 Web3.js 版本 1 中新增了两个方法,与 web3.eth.sign 和 web3.eth.personal.sign 方法对应。
3.6.1 web3.eth.signTypedData
web3.eth.signTypedData(typedData, address [, callback])
该方法使用特定账户签名结构化数据,该账户需要是解锁状态。
其中参数:
- 1)
Object- 包含域分隔符和待签名的结构化数据,结构遵循eth_signTypedDataJSON RPC 调用中指定的 JSON-Schema。 - 2)
String|Number- 用于签名的数据的地址,或者本地钱包web3.eth.accounts.wallet中的地址或索引。 - 3)
Function(可选) - 可选回调函数,第一个参数返回错误对象,第二个参数返回签名结果。
注意:参数
address也可以是web3.eth.accounts.wallet中的地址或索引,此时会使用该账户的私钥本地签名。
返回值为:
Promise返回String类型的签名,与eth_signTypedData方法返回的结果相同。
示例:
可以参考 eth_signTypedData JSON-RPC 示例中的 typedData 值:
web3.eth.signTypedData(typedData, "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826")
.then(console.log);
> "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c"
3.6.2 web3.eth.personal.signTypedData
web3.eth.personal.signTypedData(typedData, address, password [, callback])
此方法与 web3.eth.signTypedData 基本相同,但额外增加了 password 参数,类似于 web3.eth.personal.sign。
4. 设计原理(Rationale)
encode 函数针对新的类型扩展了新的处理方式,编码的首字节用于区分不同的情况。
因此,直接使用 domainSeparator 或 typeHash 作为编码的起始位置是不安全的。尽管构造一个 typeHash 作为有效 RLP 编码交易的前缀很困难,但理论上仍然可能发生。
作用域分隔符(Domain Separator)的作用有:
-
1)防止不同 DApp 之间的签名冲突
假设两个 DApp 恰好设计了相同的结构,如Transfer(address from,address to,uint256 amount),但它们的签名不应该兼容。引入作用域分隔符后,DApp 开发者可以确保不会出现签名冲突。 -
2)允许同一 DApp 内部区分不同签名用途
在同一个 DApp 内,同一结构可能需要多种签名。如,在Transfer交易中,可能既需要from签名,也需要to签名。通过提供不同的作用域分隔符,可以区分这两种签名。
方案 1:使用目标合约地址作为作用域分隔符
- 这种方法可以解决合约间的类型冲突问题,但无法区分相同结构的不同签名用途。因此,该标准建议在适当情况下使用目标合约地址。
4.1 typeHash 设计原理
typeHash 设计为 Solidity 编译时的常量,如:
bytes32 constant MAIL_TYPEHASH = keccak256("Mail(address from,address to,string contents)");
对于 typeHash,曾考虑过以下几种替代方案,但因各种原因被否决:
- 方案 2:使用 ABIv2 函数签名
采用bytes4作为哈希值的长度不足以抵抗哈希碰撞。此外,与函数签名不同,使用较长的哈希值几乎不会增加运行时成本。 - 方案 3:将 ABIv2 函数签名扩展为 256 位
这种方式虽然可以捕获类型信息,但无法表达函数以外的语义。例如,在 EIP-20 和 EIP-721 中,transfer(address,uint256)产生了实际碰撞:前者的uint256代表的是数量,而后者代表的是唯一 ID。总体而言,ABIv2 旨在增强兼容性,而哈希标准应优先考虑不可兼容性,以避免冲突。 - 方案 4:将 256 位 ABIv2 签名扩展为包含参数名和结构体名
如,Mail结构体可以被编码为:Mail(Person(string name,address wallet) from,Person(string name,address wallet) to,string contents)。
但这种方案比现有的解决方案要长得多,并且字符串的长度可能会随着输入的增加呈指数级增长(如:struct A { B a; B b; }; struct B { C a; C b; }; …)。此外,该方案不支持递归结构体类型(如:struct List { uint256 value; List next; })。 - 方案 5:包含 natspec 文档
这种方式在schemaHash中加入了更多的语义信息,进一步降低了哈希碰撞的可能性。然而,这会导致文档的扩展和修改成为破坏性变更(breaking change),违背了通常的假设。此外,这也使schemaHash机制变得过于冗长。
4.2 encodeData 设计原理
encodeData 允许 Solidity 轻松实现 hashStruct 方法:
function hashStruct(Mail memory mail) pure returns (bytes32 hash) {return keccak256(abi.encode(MAIL_TYPEHASH,mail.from,mail.to,keccak256(mail.contents)));
}
同时,它也可以在 EVM 内高效地进行原地计算:
function hashStruct(Mail memory mail) pure returns (bytes32 hash) {// 计算子哈希bytes32 typeHash = MAIL_TYPEHASH;bytes32 contentsHash = keccak256(mail.contents);assembly {// 备份内存let temp1 := mload(sub(mail, 32))let temp2 := mload(add(mail, 128))// 写入 typeHash 和 contentsHashmstore(sub(mail, 32), typeHash)mstore(add(mail, 64), contentsHash)// 计算哈希hash := keccak256(sub(mail, 32), 128)// 恢复内存mstore(sub(mail, 32), temp1)mstore(add(mail, 64), temp2)}
}
原地计算的实现对结构体在内存中的布局做出了较强但合理的假设。具体而言,它假设结构体不会被分配到地址 32 以下的位置,成员按顺序存储,所有值都填充至 32 字节的边界,并且动态类型和引用类型以 32 字节的指针形式存储。
被否决的替代方案有:
- 方案 6:紧凑打包(Tight Packing)
在 Solidity 中,使用keccak256处理多个参数时,默认会采用紧凑打包的方式。这种方式可以最小化需要哈希的字节数,但在 EVM 中需要复杂的打包指令,因此不支持原地计算。 - 方案 7:ABIv2 编码
随着abi.encode的引入,可以使用abi.encode作为encodeData函数。然而,ABIv2 标准本身未能满足确定性安全准则。相同数据可能存在多种有效的 ABIv2 编码。此外,ABIv2 也不支持原地计算。 - 方案 8:在
hashStruct中省略typeHash
可以选择不在hashStruct中包含typeHash,而是将其与域分隔符(domain separator)合并。这种方式更高效,但会导致 Soliditykeccak256哈希函数的语义不具备单射性(injective)。 - 方案 9:支持循环数据结构
当前标准针对树状数据结构进行了优化,但未定义循环数据结构的处理方式。要支持循环数据结构,需要维护一个栈来记录当前路径,并在检测到循环时使用栈偏移量进行替换。然而,这种方式的规范和实现都异常复杂,并且会破坏可组合性(composability),因为成员值的哈希值会依赖于遍历路径。
同样,直接实现该标准对于有向无环图(DAG)来说也不是最优的。递归遍历成员时,可能会多次访问相同的节点。可以使用记忆化(memoization)来优化这一过程。
4.3 domainSeparator 的设计原理
由于不同的域(domain)有不同的需求,因此采用了一种可扩展的方案:DApp 指定一个 EIP712Domain 结构体类型,并创建一个 eip712Domain 实例,将其传递给用户代理(user-agent)。用户代理可以根据其中的字段采取不同的验证措施。
5. 向后兼容性(Backwards Compatibility)
当前的 RPC 调用、web3 方法以及 SomeStruct.typeHash 参数尚未被定义。定义它们不应影响现有 DApp 的行为。
在 Solidity 中,表达式 keccak256(someInstance)(其中 someInstance 是结构体类型 SomeStruct 的一个实例)是有效的语法。当前,它计算的是该实例内存地址的 keccak256 哈希值。这种行为应被视为危险的,因为在某些情况下它可能表现正确,但在其他情况下可能会导致确定性失败或单射性问题。依赖当前行为的 DApp 应被视为存在严重风险。
6. 测试用例
示例合约可以在 Example.sol 中找到,JavaScript 的签名示例可以参考 Example.js。
// 示例合约
pragma solidity ^0.4.24;contract Example {struct EIP712Domain {string name;string version;uint256 chainId;address verifyingContract;}struct Person {string name;address wallet;}struct Mail {Person from;Person to;string contents;}bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");bytes32 constant PERSON_TYPEHASH = keccak256("Person(string name,address wallet)");bytes32 constant MAIL_TYPEHASH = keccak256("Mail(Person from,Person to,string contents)Person(string name,address wallet)");bytes32 DOMAIN_SEPARATOR;constructor () public {DOMAIN_SEPARATOR = hash(EIP712Domain({name: "Ether Mail",version: '1',chainId: 1,// verifyingContract: thisverifyingContract: 0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC}));}function hash(EIP712Domain eip712Domain) internal pure returns (bytes32) {return keccak256(abi.encode(EIP712DOMAIN_TYPEHASH,keccak256(bytes(eip712Domain.name)),keccak256(bytes(eip712Domain.version)),eip712Domain.chainId,eip712Domain.verifyingContract));}function hash(Person person) internal pure returns (bytes32) {return keccak256(abi.encode(PERSON_TYPEHASH,keccak256(bytes(person.name)),person.wallet));}function hash(Mail mail) internal pure returns (bytes32) {return keccak256(abi.encode(MAIL_TYPEHASH,hash(mail.from),hash(mail.to),keccak256(bytes(mail.contents))));}function verify(Mail mail, uint8 v, bytes32 r, bytes32 s) internal view returns (bool) {// Note: we need to use `encodePacked` here instead of `encode`.bytes32 digest = keccak256(abi.encodePacked("\x19\x01",DOMAIN_SEPARATOR,hash(mail)));return ecrecover(digest, v, r, s) == mail.from.wallet;}function test() public view returns (bool) {// Example signed messageMail memory mail = Mail({from: Person({name: "Cow",wallet: 0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826}),to: Person({name: "Bob",wallet: 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB}),contents: "Hello, Bob!"});uint8 v = 28;bytes32 r = 0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d;bytes32 s = 0x07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b91562;assert(DOMAIN_SEPARATOR == 0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f);assert(hash(mail) == 0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e);assert(verify(mail, v, r, s));return true;}
}
相应的JavaScript签名示例代码为:
// using ethereumjs-util 7.1.3
const ethUtil = require('ethereumjs-util');// using ethereumjs-abi 0.6.9
const abi = require('ethereumjs-abi');// using chai 4.3.4
const chai = require('chai');const typedData = {types: {EIP712Domain: [{ name: 'name', type: 'string' },{ name: 'version', type: 'string' },{ name: 'chainId', type: 'uint256' },{ name: 'verifyingContract', type: 'address' },],Person: [{ name: 'name', type: 'string' },{ name: 'wallet', type: 'address' }],Mail: [{ name: 'from', type: 'Person' },{ name: 'to', type: 'Person' },{ name: 'contents', type: 'string' }],},primaryType: 'Mail',domain: {name: 'Ether Mail',version: '1',chainId: 1,verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',},message: {from: {name: 'Cow',wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',},to: {name: 'Bob',wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',},contents: 'Hello, Bob!',},
};const types = typedData.types;// Recursively finds all the dependencies of a type
function dependencies(primaryType, found = []) {if (found.includes(primaryType)) {return found;}if (types[primaryType] === undefined) {return found;}found.push(primaryType);for (let field of types[primaryType]) {for (let dep of dependencies(field.type, found)) {if (!found.includes(dep)) {found.push(dep);}}}return found;
}function encodeType(primaryType) {// Get dependencies primary first, then alphabeticallet deps = dependencies(primaryType);deps = deps.filter(t => t != primaryType);deps = [primaryType].concat(deps.sort());// Format as a string with fieldslet result = '';for (let type of deps) {result += `${type}(${types[type].map(({ name, type }) => `${type} ${name}`).join(',')})`;}return result;
}function typeHash(primaryType) {return ethUtil.keccakFromString(encodeType(primaryType), 256);
}function encodeData(primaryType, data) {let encTypes = [];let encValues = [];// Add typehashencTypes.push('bytes32');encValues.push(typeHash(primaryType));// Add field contentsfor (let field of types[primaryType]) {let value = data[field.name];if (field.type == 'string' || field.type == 'bytes') {encTypes.push('bytes32');value = ethUtil.keccakFromString(value, 256);encValues.push(value);} else if (types[field.type] !== undefined) {encTypes.push('bytes32');value = ethUtil.keccak256(encodeData(field.type, value));encValues.push(value);} else if (field.type.lastIndexOf(']') === field.type.length - 1) {throw 'TODO: Arrays currently unimplemented in encodeData';} else {encTypes.push(field.type);encValues.push(value);}}return abi.rawEncode(encTypes, encValues);
}function structHash(primaryType, data) {return ethUtil.keccak256(encodeData(primaryType, data));
}function signHash() {return ethUtil.keccak256(Buffer.concat([Buffer.from('1901', 'hex'),structHash('EIP712Domain', typedData.domain),structHash(typedData.primaryType, typedData.message),]),);
}const privateKey = ethUtil.keccakFromString('cow', 256);
const address = ethUtil.privateToAddress(privateKey);
const sig = ethUtil.ecsign(signHash(), privateKey);const expect = chai.expect;
expect(encodeType('Mail')).to.equal('Mail(Person from,Person to,string contents)Person(string name,address wallet)');
expect(ethUtil.bufferToHex(typeHash('Mail'))).to.equal('0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2',
);
expect(ethUtil.bufferToHex(encodeData(typedData.primaryType, typedData.message))).to.equal('0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8',
);
expect(ethUtil.bufferToHex(structHash(typedData.primaryType, typedData.message))).to.equal('0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e',
);
expect(ethUtil.bufferToHex(structHash('EIP712Domain', typedData.domain))).to.equal('0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f',
);
expect(ethUtil.bufferToHex(signHash())).to.equal('0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2');
expect(ethUtil.bufferToHex(address)).to.equal('0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826');
expect(sig.v).to.equal(28);
expect(ethUtil.bufferToHex(sig.r)).to.equal('0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d');
expect(ethUtil.bufferToHex(sig.s)).to.equal('0x07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b91562');
7. 安全性考量(Security Considerations)
7.1 重放攻击(Replay Attacks)
本标准仅涉及消息签名和签名验证。在许多实际应用中,签名消息用于授权某项操作,如代币交换。实施者必须确保应用程序在收到相同的签名消息两次时能够正确处理。如,重复的消息应被拒绝,或者授权的操作应具有幂等性。具体实现方式取决于应用场景,超出了本标准的范围。
7.2 交易抢跑攻击(Frontrunning Attacks)
可靠地广播签名的机制取决于具体的应用,超出了本标准的范围。当签名被广播到区块链并用于合约时,应用程序必须能够防范抢跑攻击。在这种攻击中,攻击者拦截签名并在原始预期用途发生之前将其提交到合约。应用程序应确保在攻击者率先提交签名时仍能正确处理,如拒绝该签名,或仅产生与签名者预期完全相同的效果。
参考资料
[1] EIP-712: Typed structured data hashing and signing – A procedure for hashing and signing of typed structured data as opposed to just bytestrings.
相关文章:
EIP-712:类型化结构化数据的哈希与签名
1. 引言 以太坊 EIP-712: 类型化结构化数据的哈希与签名,是一种用于对类型化结构化数据(而不仅仅是字节串)进行哈希和签名 的标准。 其包括: 编码函数正确性的理论框架,类似于 Solidity 结构体并兼容的结构化数据规…...
spring boot 集成redis 中RedisTemplate 、SessionCallback和RedisCallback使用对比详解,最后表格总结
对比详解 1. RedisTemplate 功能:Spring Data Redis的核心模板类,提供对Redis的通用操作(如字符串、哈希、列表、集合等)。使用场景:常规的Redis增删改查操作。特点: 支持序列化配置(如String…...
基于S函数的simulink仿真
基于S函数的simulink仿真 S函数可以用计算机语言来描述动态系统。在控制系统设计中,S函数可以用来描述控制算法、自适应算法和模型动力学方程。 S函数中使用文本方式输入公式和方程,适合复杂动态系统的数学描述,并且在仿真过程中可以对仿真…...
每日一题洛谷P8664 [蓝桥杯 2018 省 A] 付账问题c++
P8664 [蓝桥杯 2018 省 A] 付账问题 - 洛谷 (luogu.com.cn) 思路:要使方差小,那么钱不能一下付的太多,可以让钱少的全付玩,剩下还需要的钱再让钱多的付(把钱少的补上)。 将钱排序,遍历一遍&…...
迅饶科技X2Modbus网关-GetUser信息泄露漏洞
免责声明:本号提供的网络安全信息仅供参考,不构成专业建议。作者不对任何由于使用本文信息而导致的直接或间接损害承担责任。如涉及侵权,请及时与我联系,我将尽快处理并删除相关内容。 漏洞描述 该漏洞的存在是由于GetUser接口在…...
【Pandas】pandas DataFrame values
Pandas2.2 DataFrame Attributes and underlying data 方法描述DataFrame.index用于获取 DataFrame 的行索引DataFrame.columns用于获取 DataFrame 的列标签DataFrame.dtypes用于获取 DataFrame 中每一列的数据类型DataFrame.info([verbose, buf, max_cols, …])用于提供 Dat…...
蓝桥杯Java B组省赛真题高频考点近6年统计分类
基础考点 考点高频难度模拟9基础枚举5基础思维4基础动态规划3基础规律2基础单位换算2基础搜索 1基础双指针1基础数学1基础哈希表1基础暴力1基础Dijkstra1基础 二分1基础 中等考点 考点高频难度动态规划6中等数学5中等枚举4中等模拟3中等思维3中等贪心3中等前缀和3中等二分2中…...
关于inode,dentry结合软链接及硬链接的实验
一、背景 在之前的博客 缺页异常导致的iowait打印出相关文件的绝对路径-CSDN博客 里 2.2.3 一节里,我们讲到了file,fd,inode,dentry,super_block这几个概念,在这篇博客里,我们针对inode和dentr…...
PandasAI:当数据分析遇上自然语言处理
数据科学的新范式 在数据爆炸的时代,传统的数据分析工具正面临着前所未有的挑战。数据科学家们常常需要花费70%的时间在数据清洗和探索上,而真正的价值创造时间却被大幅压缩。PandasAI的出现,正在改变这一现状——它将生成式AI的强大能力注入…...
Unity网络开发基础 (3) Socket入门 TCP同步连接 与 简单封装练习
本文章不作任何商业用途 仅作学习与交流 教程来自Unity唐老狮 关于练习题部分是我观看教程之后自己实现 所以和老师写法可能不太一样 唐老师说掌握其基本思路即可,因为前端程序一般不需要去写后端逻辑 1.认识Socket的重要API Socket是什么 Socket(套接字࿰…...
做题记录:和为K的子数组
来自leetcode 560 前言 自己只会暴力,这里就是记录一下前缀和哈希表的做法,来自灵神的前缀和哈希表:从两次遍历到一次遍历,附变形题 正文 首先,这道题无法使用滑动窗口,因为滑动窗口需要满足单调性&am…...
VMware虚拟机卡顿、CPU利用率低、编译Linux内核慢,问题解决与实验对比
目录 一、总结在前面(节约时间就只看这里)0 环境说明1 遇到的问题:2 问题的原因:3 解决办法:4 实验验证:5 关于虚拟机内核数量设置6 关于强行指定Vm能用的CPU内核 二、管理员启动,实验对比实验1…...
【7】数据结构的队列篇章
目录标题 队列的定义顺序队列的实现初始化入队出队顺序队列总代码与调试 循环队列的实现初始化入队出队获取队首元素循环队列总代码与调试 链式队列的实现链式队列的初始化入队出队获取队首元素链式队列总代码与调试 队列的定义 定义:队列(Queue&#x…...
颜色归一化操作
当我们不太关注图像具体细节,只关注图像大致的内容时,为了避免光照角度、光照强度对图像的影响,可以采用下面进行归一化操作。这种颜色系统具有通道对表面方向、照明方向具有鲁棒性的特性,适用于图像分割等领域,在机器…...
2874. 有序三元组中的最大值 II
给你一个下标从 0 开始的整数数组 。nums 请你从所有满足 的下标三元组 中,找出并返回下标三元组的最大值。 如果所有满足条件的三元组的值都是负数,则返回 。i < j < k(i, j, k)0 下标三元组 的值等于 。(i, j, k)(nums[i] - nums[j]) * nums[k…...
05-Spring Security 认证与授权机制源码解析
Spring Security 认证与授权机制源码解析 结合之前的IOC、AOP、事务管理, 这一篇讲讲Spring 的安全性,以下是小弟对Spring Security的一些理解,以及在真实面试中碰到的一些问题做了些整理,欢迎各位大佬一起观摩指点!&a…...
深度学习处理文本(6)
理解词嵌入 重要的是,进行one-hot编码时,你做了一个与特征工程有关的决策。你向模型中注入了有关特征空间结构的基本假设。这个假设是:你所编码的不同词元之间是相互独立的。事实上,one-hot向量之间都是相互正交的。对于单词而言…...
STL-vector的使用
1.STL-vector 向量是可以改变其大小的线性序列容器。向量使用连续的空间存储元素,表明向量可以像数组通过下标来访问元素,但是向量的大小可以动态变化。向量的容量可能大于其元素需要的实际容量,向量通过消耗更多的内存来换取存储管理效率。…...
MySQL深入
体系结构 连接层:主要处理客户端的连接进行授权认证、校验权限等相关操作 服务层:如sql的接口、解析、优化在这里完成,所有跨存储引擎的操作在这里完成 引擎层:索引是在存储引擎层实现的,所以不同的存储引擎他的索引…...
为什么LoRA在目标检测方向不奏效?
最近在思考,为啥目标检测方向没有出现LORA的相关用法,搜索到了一篇文章,挺有深度的。 Why LoRA Struggles with Object Detection (and Why I Learned This the Hard Way) 链接:https://medium.com/predict/why-lora-struggles-with-object-detection-and-why-i-learned-…...
Vue面试常考内容[从宏观到微观]
以下是Vue面试常考内容的系统性解析,从框架设计思想到源码实现细节,结合最新技术动态(截至2025年4月)整理而成: 一、宏观层面:Vue设计哲学与框架定位 渐进式框架核心 • 分层可扩展架构:从视图层核心逐步集成路由、状态管理等能力,支持"按需取用"的渐进式开发…...
Genspark:重新定义搜索体验的AI智能体引擎
关于我们 飞书-华彬智融知识库 由前百度高管景鲲(Eric Jing)和朱凯华(Kay Zhu)联合创立的AI搜索引擎Genspark,正以革命性的技术架构和用户导向的设计理念,为全球用户带来一场搜索体验的范式革命。本文将基…...
从零实现Json-Rpc框架】- 项目实现 - 服务端主题实现及整体封装
📢博客主页:https://blog.csdn.net/2301_779549673 📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson 📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正! &…...
AI助力PPT制作,让演示变得轻松高效
AI助力PPT制作,让演示变得轻松高效!随着科技的进步,AI技术早已渗透到各行各业,特别是在办公领域,AI制作PPT已不再是未来的梦想,而是现实的工具。以前你可能需要花费数小时来制作一个完美的PPT,如…...
React-01React创建第一个项目(npm install -g create-react-app)
1. React特点 JSX是javaScript语法的扩展,React开发不一定使用JSX。单向响应的数据流,React实现单向数据流,减少重复代码,比传统数据绑定更简单。等等 JSX是js的语法扩展,允许在js中编写类似HTML的代码 const …...
HTML应用指南:利用POST请求获取三大运营商5G基站位置信息(二)
在当前信息技术迅猛发展的背景下,第五代移动通信(5G)技术作为新一代的无线通信标准,正逐步成为推动社会进步和产业升级的关键驱动力。三大电信运营商(中国移动、中国联通、中国电信)在全国范围内的5G基站部署,不仅极大地提升了网络性能,也为智能城市、物联网、自动驾驶…...
C++学习笔记之内存管理
仅用于记录学习理解 选择题答案及解析 globalVar:C(数据段 (静态区)) 解析:全局变量存放在数据段(静态区),生命周期从程序开始到结束,程序运行期间一直存在。 staticGlobalVar&…...
针对 MySQL 数据库中 主键/唯一约束的更新方法 和 ON DUPLICATE KEY UPDATE 语法的详细说明及示例,并以表格总结
以下是针对 MySQL 数据库中 主键/唯一约束的更新方法 和 ON DUPLICATE KEY UPDATE 语法的详细说明及示例,并以表格总结: 一、主键的更新 1. 更新主键的条件 允许更新:MySQL 允许更新主键列,但需满足以下条件: 唯一性…...
day21 学习笔记
文章目录 前言一、删除数据二、索引操作1.loc方法2.iloc方法 三、添加数据1.loc方法添加数据2.concat方法拼接数据 四、重置索引 前言 通过今天的学习,我掌握了对Pandas对象数据元素进行增删操作以及重置索引的操作 一、删除数据 DataFrame.drop(labelsNone, axis…...
【MyBatis】深入解析 MyBatis XML 开发:增删改查操作和方法命名规范、@Param 重命名参数、XML 返回自增主键方法
增删改查操作 接下来,我们来实现一下用户的增加、删除和修改的操作。 增( Insert ) UserInfoMapper接口: 我们写好UserInfoMapper接口后,自动生成 XML 代码; UserInfoMapper.xml实现: 增删改查方法命名规范 如果我们…...
