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 chainId
EIP-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_signTypedData
JSON 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 结构体并兼容的结构化数据规…...
Day 3:Leetcode 比特位计数+只出现一次的数字 II
比特位计数 本质是一个递推,时间复杂度O(n),空间复杂度O(n)。 class Solution { public:vector<int> countBits(int n) {vector<int> ans(n 1);ans[0] 0;//ans[1] 1;for(int i 1; i < n; i){if(i & 1){ans[i] ans[i/2] 1;}else…...
CentOS7安装conda
root用户登录虚拟机后更新yum yum update 下载Miniconda,路径就在/root下 wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh conda分为Miniconda和Anaconda, Miniconda是Conda的轻量级版本,只包含conda和Python…...
[Linux]从零开始的vs code交叉调试arm Linux程序教程
一、前言 最近的项目中需要集成rknn的视觉识别,在这之前我并且没有将rknn集成到自己项目的经验。这里我需要在rknn原本demo的基础上我还需要集成自己的业务代码。但是又有一个问题,原本rknn我们都是使用交叉编译编译到开发板上的,并且我们还要…...
【顶刊级科研绘图】AI支持下Nature级数据可视化(如何画图、如何标注、如何改图、如何美化、如何组合、如何排序)
技术点目录 第一章、绘图原则与概念:规范清晰简洁自明第二章、DeepSeek、ChatGPT、R绘图系统:八仙过海各显神通第三章、美学设计与细节:完美图表华丽呈现第四章、数据类型与图表:宝典在手各个击破第五章、统计分析与可视化&#x…...
CSRF跨站请求伪造——入门篇【DVWA靶场low级别writeup】
CSRF跨站请求伪造——入门篇 0. 前言1. 什么是CSRF2. 一次完整的CSRF攻击 0. 前言 本文将带你实现一次完整的CSRF攻击,内容较为基础。需要你掌握的基础知识有: 了解cookie;已经安装了DVWA的靶场环境(本地的或云的)&am…...
Spring Boot应用中实现Jar包热更新的实践指南
Spring Boot应用中实现Jar包热更新的实践指南 一、引言 在现代软件开发中,快速迭代和持续交付是至关重要的。对于基于Spring Boot的应用程序,一旦部署到生产环境,传统的更新方式通常是重新打包并重启应用,这不仅耗时,…...
JVM深入原理(七)(一):运行时数据区
目录 7. JVM运行时数据区 7.1. 运行时数据区-总览 7.2. 运行时数据区-查看内存对象 7.3. 运行时数据区-程序计数器 7.3.1. 程序计数器-作用 7.3.2. 字节码指令执行流程 7.4. 运行时数据区-Java虚拟机栈 7.4.1. 栈-概述 7.4.2. 栈帧-组成 7.4.2.1. 栈帧-帧数据 7.4.2…...
约瑟夫环的四种(数组,链表,递归,迭代)解决方案,与空间、时间复杂度分析
以下方法均没有考虑结果集的空间与时间复杂度 1.数组解法 实现代码(未优化) class Main {public static void main(String[] args){Scanner read new Scanner(System.in);int n read.nextInt();int m read.nextInt();int[] people new int[n]; //…...
skynet.start 的作用详细解析
目录 skynet.start 的作用详细解析1. 功能概述2. 基本用法3. 关键作用(1) 注册消息处理函数(2) 启动事件循环(3) 服务生命周期管理 4. 与其他函数的协作5. 未调用 skynet.start 的后果6. 高级场景:何时不需要 skynet.start7. 总结 skynet.start 的作用详细解析 在 …...
Apache Doris 2025 Roadmap:构建 GenAI 时代实时高效统一的数据底座
在全球 290 位开发者的协作下,Apache Doris 在 2024 年完成了 7000 次代码提交,并发布了 22 个版本,实现在实时分析、湖仓一体和半结构化数据分析等核心场景的技术突破及创新。 2025 年,Apache Doris 社区将秉承“以场景驱动创新…...
springboot+easyexcel实现下载excels模板下拉选择
定义下拉注解 Target(ElementType.FIELD) Retention(RetentionPolicy.RUNTIME) public interface ExcelDropDown {/*** 固定下拉选项*/String[] source() default {};/*** 动态数据源key(从上下文中获取)*/String sourceMethod() default "";…...
vue3+ts+element-plus 开发一个页面模块的详细过程
目录、文件名均使用kebab-case(短横线分隔式)命名规范 子组件目录:./progress-ctrl/comps 1、新建页面文件 progress-ctrl.vue <script setup lang"ts" name"progress-ctrl"></script><template>&l…...
软考《信息系统运行管理员》- 7.1 物联网运维
物联网的概念及特征 物联网是在计算机互联网的基础上,通过射频识别 (RFID) 、 无线传感器、红外感应器、 全球定位系统、激光扫描器等信息传感设备,按约定的协议,把物与物之间通过网络连接起来, 进行信息交换和通信,以…...
【GPT入门】第33 课 一文吃透 LangChain:chain 结合 with_fallbacks ([]) 的实战指南
[TOC](【GPT入门】第33课 一文吃透 LangChain:chain 结合 with_fallbacks ([]) 的实战指南) 1. fallback概述 模型回退,可以设置在llm上,也可以设置在chain上,都带有with_fallbacks([])函数 2. llm的回退 2.1 代码 核心代码&…...
裴蜀定理:整数解的奥秘
裴蜀定理:整数解的奥秘 在数学的世界里,裴蜀定理(Bzout’s Theorem)是数论中一个非常重要的定理,它揭示了二次方程和整数解之间的关系。它不仅仅是纯粹的理论知识,还在计算机科学、密码学、算法优化等多个…...
Table as Thought论文精读
标题:Table as Thought: Exploring Structured Thoughts in LLM Reasoning 作者:Zhenjie Sun, Naihao Deng, Haofei Yu, Jiaxuan You 单位:University of Illinois Urbana-Champaign, University of Michigan 摘要: llm的推理…...
PyQt6实例_A股日数据维护工具_使用
目录 前置: 下载预备更新的数据 使用工具更新 用工具下载未复权、前复权、权息数据 在PostgreSQL添加两个数据表 工具&视频 前置: 1 本系列将以 “PyQt6实例_A股日数据维护工具” 开头放置在“PyQt6实例”专栏 2 日数据可在“数据库”专栏&…...
MySQL客户端工具-图形化工具-DataGrip 安装与使用
一. 常见的图形化工具 二. DataGrip 安装 官网:DataGrip:由 JetBrains 开发的数据库和 SQL 跨平台 IDE 二. DataGrip 使用...
企业管理系统的功能架构设计与实现
一、企业管理系统的核心功能模块 企业管理系统作为现代企业的中枢神经系统,涵盖了多个核心功能模块,以确保企业运营的顺畅与高效。这些功能模块通常包括: 人力资源管理模块:负责员工信息的录入、维护、查询及统计分析,…...
1.Qt信号与槽
本篇主要介绍信号和槽,如何关联信号和槽以及用QPixmap在窗口中自适应显示图片 本文部分ppt、视频截图原链接:[萌马工作室的个人空间-萌马工作室个人主页-哔哩哔哩视频] 1. 信号 一般不需要主动发送信号,只有自定义的一些控件才需要做信号的…...
再生认证体系有哪些?不同标准对应的要求及可以做的审核机构
再生认证体系 标准 GRS再生回收认证要求 再生原材料的上游企业:需要具备GRS认证证书,以确保原材料的可追溯性和再生成分。 认证条件: 最终商品的再生成分比例必须至少为20%。 只有由至少50%的回收材料制成的产品才能贴上GRS标签。 认证机构…...
[CISSP] [6] 密码学和对称密钥算法
密码学的目标 1. 机密性(Confidentiality) 目标:保护信息不被未授权访问。 通过 加密(Encryption)技术确保数据只能被授权方解密和读取。主要方法: 对称加密(AES、3DES)ÿ…...
thinkphp每条一级栏目中可自定义添加多条二级栏目,每条二级栏目包含多个字段信息
小程序客户端需要展示团购详情这种结构的内容,后台会新增多条套餐,每条套餐可以新增多条菜品信息,每条菜品信息包含菜品名称,价格,份数等字段信息,类似于购物网的商品多规格属性,数据表中以json类型存储,手写了一个后台添加和编辑的demo 添加页面 编辑页面(json数据…...
混杂模式(Promiscuous Mode)与 Trunk 端口的区别详解
一、混杂模式(Promiscuous Mode) 1. 定义与工作原理 定义:混杂模式是网络接口的一种工作模式,允许接口接收通过其物理链路的所有数据包,而不仅是目标地址为本机的数据包。工作层级:OSI 数据链路层&#x…...
Spring Boot项目信创国产化适配指南
将 Spring Boot 项目适配信创国产化环境,需要从底层基础设施到上层应用组件进行全面替换和调整。以下是主要替换点和适配步骤的总结: 一、基础软件替换 1. JDK 替换 国外JDK:Oracle JDK、OpenJDK国产JDK: 阿里龙井(D…...
MySQL:数据类型
数值类型 数值类型用于存储整数、小数、浮点数等,主要分为整数类型和浮点类型。 整数类型 数据类型存储大小取值范围(有符号)取值范围(无符号)说明TINYINT1字节-128 ~ 1270 ~ 255小整数,如布尔值&#x…...
maven引入项目内本地包方法
最近在写java实现excel转pdf功能; 网上有个包很好用,免费:spire.xls.free-5.3.0.jar。 但是maven打包项目时报错,找不到这个包。 jar包位置如下: 在项目/src/jar/spire.xls.free-5.3.0.jar。 解决方法:…...
ARP协议
ARP协议 ARP协议的作用 当网络设备有数据要发送给另一台网络设备时,必须要知道对方的网络层地址(即IP地址)。IP地址由网络层来提供,但是仅有IP地址是不够的,IP数据报文必须封装成帧才能通过数据链路进行发送。数据帧…...
科技赋能安居梦:中建海龙以模块化革新重塑城市更新范式
在北京市西城区桦皮厂胡同,一栋始建于上世纪70年代的住宅楼正经历着一场脱胎换骨的蜕变。这座曾被鉴定为D级危房的建筑,在中建海龙科技有限公司(以下简称“中建海龙”)的匠心打造下,仅用三个月便完成"原拆原建&qu…...
2025 AI智能数字农业研讨会在苏州启幕,科技助农与数据兴业成焦点
4月2日,以"科技助农数据兴业”为主题的2025AI智能数字农业研讨会在苏州国际博览中心盛大启幕。本次盛会吸引了来自全国各地相关部门领导、知名专家学者、行业协会组织,以及县级市农业企业代表、县级市农产品销售商等万名嘉宾齐聚姑苏城,…...
2000-2021年 全国各地区城镇登记失业率数据
全国各地区城镇登记失业率数据2000-2021年.ziphttps://download.csdn.net/download/2401_84585615/90259723 https://download.csdn.net/download/2401_84585615/90259723 城镇登记失业率是衡量地区就业状况的重要指标,反映了在一定时期内,符合就业条件的…...
Cursor的主要好处
以下是Cursor的主要好处: 代码生成与优化 • 快速生成代码:根据简短描述或部分代码片段,Cursor能快速生成完整代码模块,还能智能预测下一步操作,将光标放在合适位置,让开发者一路Tab键顺滑编写代码。 • …...
超便捷语音转文字工具CapsWriter-Offline本地部署与远程使用全流程
文章目录 前言1. 软件与模型下载2. 本地使用测试3. 异地远程使用3.1 内网穿透工具下载安装3.2 配置公网地址3.3 修改config文件3.4 异地远程访问服务端 4. 配置固定公网地址4.1 修改config文件 5. 固定tcp公网地址远程访问服务端 前言 今天给大家安利一个绝对能让你工作效率飙…...
什么是数据仓库
什么是数据仓库 Data warehouse 是面向主题的 主要根据各种数据来源,来进行历史分析 形成一个趋势分析 为数据挖掘、预测建模、机器学习提供基础数据 与传统数据库比如gaussdb的区别。数据仓库注重历史数据分析,guassdb注重实时事务处理 数据仓库时企业的…...
【动态规划】二分优化最长上升子序列
最长上升子序列 II 题解 题目传送门:AcWing 896. 最长上升子序列 II 一、题目描述 给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。 输入格式: 第一行包含整数 N第二行包含 N 个整数,表示完整序列 输…...
MySQL的安装与初始化流程
MySQL概述 MySQL是一个关系型数据库管理系统,由瑞典MySQL AB公司开发,MySQL AB公司被Sun公司收购,Sun公司又被Oracle公司收购,目前属于Oracle公司。 MySQL是目前最流行的关系型数据库管理系统,在WEB应用方面MySQL是最…...
flink standalone集群模式部署
一. 环境准备 1、下载并安装jdk11 2、下载flink 并解压 3、确保服务器之间的免密登录 二、集群搭建 搭建集群至少有三台机器,每台机器的分配角色如下 master: jobManager salve01:taskManager salve02:taskManager 1、在JobManager(…...
Linux线程概念与控制:【线程概念(页表)】【Linux线程控制】【线程ID及进程地址空间布局】【线程封装】
目录 一. 线程概念 1.1什么是线程 1.2分页式存储管理 1.2.1虚拟地址和页表的由来 1.2.2物理内存管理 1.2.3页表 1.2.4页目录结构 1.2.5二级页表地址转换 1.3线程的优点 二.进程VS线程 三.Linux线程控制 3.1POSIX线程库 3.2创建线程 编辑 pthread库是个什么东西 …...
7-6 混合类型数据格式化输入
本题要求编写程序,顺序读入浮点数1、整数、字符、浮点数2,再按照字符、整数、浮点数1、浮点数2的顺序输出。 输入格式: 输入在一行中顺序给出浮点数1、整数、字符、浮点数2,其间以1个空格分隔。 输出格式: 在一行中…...
最新全开源码支付系统,赠送3套模板
最新全开源码支付系统,赠送3套模板 码支付是专为个人站长打造的聚合免签系统,拥有卓越的性能和丰富的功能。它采用全新轻量化的界面UI 让您能更方便快捷地解决知识付费和运营赞助的难题,同时提供实时监控和管理功能,让您随时随地…...
Eclipse Leshan 常见问题解答 (FAQ) 笔记
本笔记基于 Eclipse Leshan Wiki - F.A.Q. 页面内容,旨在解答关于 Eclipse Leshan(一个开源的 LwM2M 服务器和客户端 Java 实现)的常见问题,帮助您更好地理解和使用该工具。 一、Leshan 是什么,我该如何使用它&#x…...
【6】数据结构的栈篇章
目录标题 栈的定义顺序栈的实现顺序栈的初始化入栈出栈获取栈顶元素顺序栈总代码与调试 双端栈的实现双端栈的初始化入栈出栈双端栈总代码与调试 链栈的实现链栈的初始化入栈出栈获取栈顶元素链栈总代码与调试 栈的定义 定义:栈(Stack)是一种…...
开源虚拟化管理平台Proxmox VE部署超融合
Proxmox VE 是一个功能强大、开源的虚拟化平台,结合了 KVM 和 LXC,同时支持高可用集群、存储管理(ZFS、Ceph)和备份恢复。相比 VMware ESXi 和 Hyper-V,PVE 具有开源、低成本、高灵活性的特点,适用于中小企…...
C语言基础要素(019):输出ASCII码表
计算机以二进制处理信息,但二进制对人类并不友好。比如说我们规定用二进制值 01000001 表示字母’A’,显然通过键盘输入或屏幕阅读此数据而理解它为字母A,是比较困难的。为了有效的使用信息,先驱者们创建了一种称为ASCII码的交换代…...
函数柯里化(Currying)介绍(一种将接受多个参数的函数转换为一系列接受单一参数的函数的技术)
文章目录 柯里化的特点示例普通函数柯里化实现使用Lodash进行柯里化 应用场景总结 函数柯里化(Currying)是一种将接受多个参数的函数转换为一系列接受单一参数的函数的技术。换句话说,柯里化将一个多参数函数转化为一系列嵌套的单参数函数。 …...
基于大模型的主动脉瓣病变预测及治疗方案研究报告
目录 一、引言 1.1 研究背景 1.2 研究目的 1.3 研究意义 二、大模型预测主动脉瓣病变原理 2.1 大模型介绍 2.2 数据收集与处理 2.3 模型训练与优化 三、术前预测与评估 3.1 主动脉瓣病变类型及程度预测 3.2 患者整体状况评估 3.3 手术风险预测 四、术中应用与监测…...
VSCode开发者工具快捷键
自动生成浏览器文件.html的快捷方式 在文本里输入: ! enter VSCode常用快捷键列表 代码格式化:Shift Alt F向上或向下移动一行:Alt Up 或者 Alt Down快速复制一行代码:Shift Alt Up 或者 Shift Alt Down快速保…...
AI助力PPT制作,让演示变得轻松高效
AI助力PPT制作,让演示变得轻松高效!随着科技的进步,AI技术早已渗透到各行各业,特别是在办公领域,AI制作PPT已不再是未来的梦想,而是现实的工具。以前你可能需要花费数小时来制作一个完美的PPT,如…...
行业专家视角下的技术选型与任务适配深度解析
行业专家视角下的技术选型与任务适配深度解析 一、任务属性与技术栈的映射逻辑 (1)学术类项目需优先考虑技术严谨性、可复现性和理论深度: 机器学习模型开发:PyTorchJupyterMLflow形成完整实验闭环,TensorFlow Exte…...