forked from ethereum/ERCs
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[WIP] Add eth_signTypedData as a standard for machine-verifiable and …
…human-readable typed data signing with Ethereum keys (ethereum#712) * Add eip-signTypedData * Change namespace from personal to eth * Change a way schema hash is combined together with data as proposed by @MicahZoltu * Add a note about it being implemented in MetaMask as an experimental feature * Add signerAddress as a parameter * Add test vectors * Fix an example * Missing commas, periods * Address the feedback * Add a missing signerAddress parameter in the example * Change the order of parameters to have an address as a second arg * Wrote motivation * WIP * First draft of specification * Fixes * Update to new EIP format * Assign EIP number * Clarify encoding of short static byte arrays * Removed Solidity changes * Fixup * Fix typos * WIP EIP191 * WIP TODO * WIP Replay attacks * Fixes the sorted by name example encoding * Remove Solidity hash * Added note on replay protection * Redesign domain separator * Include images and simple motivation * Fix up EIP metadata formatting * Add domain separator * Remove replay attacks from todo list * Add Jacob Evans to authors * Clarify encodeData * Rename Message example to Mail * Update mock signing screen * Rework EIP712Domain * Update Solidity example * Update Javascript example * Relocate files * Rename DomainSeparator to EIP712Domain (fix) * Move examples to separate files * Remove httpOrigin domain parameter * Update JSON-Schema * Add registery of version bytes * Add eip712 to eip191 registery * Add requires header * Set correct language on all snipets * GitHub highlighting for Solidity files * Update Web3 API specification * Use abi.encode where possible * Update JSON-RPC specification * Asset path repo is ethereums * Correctly spelling of registry
- Loading branch information
1 parent
c8c48a7
commit d686a65
Showing
7 changed files
with
714 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# GitHub highlighting for Solidity files | ||
# See https://github.com/github/linguist/pull/3973#issuecomment-357507741 | ||
*.sol linguist-language=Solidity |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
const ethUtil = require('ethereumjs-util'); | ||
const abi = require('ethereumjs-abi'); | ||
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 alphabetical | ||
let deps = dependencies(primaryType); | ||
deps = deps.filter(t => t != primaryType); | ||
deps = [primaryType].concat(deps.sort()); | ||
|
||
// Format as a string with fields | ||
let result = ''; | ||
for (let type of deps) { | ||
result += `${type}(${types[type].map(({ name, type }) => `${type} ${name}`).join(',')})`; | ||
} | ||
return result; | ||
} | ||
|
||
function typeHash(primaryType) { | ||
return ethUtil.sha3(encodeType(primaryType)); | ||
} | ||
|
||
function encodeData(primaryType, data) { | ||
let encTypes = []; | ||
let encValues = []; | ||
|
||
// Add typehash | ||
encTypes.push('bytes32'); | ||
encValues.push(typeHash(primaryType)); | ||
|
||
// Add field contents | ||
for (let field of types[primaryType]) { | ||
let value = data[field.name]; | ||
if (field.type == 'string' || field.type == 'bytes') { | ||
encTypes.push('bytes32'); | ||
value = ethUtil.sha3(value); | ||
encValues.push(value); | ||
} else if (types[field.type] !== undefined) { | ||
encTypes.push('bytes32'); | ||
value = ethUtil.sha3(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.sha3(encodeData(primaryType, data)); | ||
} | ||
|
||
function signHash() { | ||
return ethUtil.sha3( | ||
Buffer.concat([ | ||
Buffer.from('1901', 'hex'), | ||
structHash('EIP712Domain', typedData.domain), | ||
structHash(typedData.primaryType, typedData.message), | ||
]), | ||
); | ||
} | ||
|
||
const privateKey = ethUtil.sha3('cow'); | ||
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'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
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: this | ||
verifyingContract: 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 message | ||
Mail 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; | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.