-
Notifications
You must be signed in to change notification settings - Fork 6k
[WIP] Add eth_signTypedData as a standard for machine-verifiable and human-readable typed data signing with Ethereum keys #712
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
58 commits
Select commit
Hold shift + click to select a range
21abe25
Add eip-signTypedData
LogvinovLeon 69e486b
Change namespace from personal to eth
LogvinovLeon 75904fc
Change a way schema hash is combined together with data as proposed b…
LogvinovLeon cfa56eb
Add a note about it being implemented in MetaMask as an experimental …
LogvinovLeon 1dd2fbe
Add signerAddress as a parameter
LogvinovLeon 597cdd7
Add test vectors
LogvinovLeon e22ba32
Fix an example
LogvinovLeon 12696f8
Missing commas, periods
pirapira b4e5d70
Address the feedback
LogvinovLeon ec20713
Add a missing signerAddress parameter in the example
LogvinovLeon 745963a
Change the order of parameters to have an address as a second arg
LogvinovLeon dab73be
Wrote motivation
recmo 6d7d340
WIP
recmo c098c14
First draft of specification
recmo 71c5765
Fixes
recmo 0c2297b
Update to new EIP format
recmo 0acbfa1
Assign EIP number
recmo 56f6321
Clarify encoding of short static byte arrays
recmo ae5f668
Removed Solidity changes
recmo 636f0e4
Fixup
recmo 15cbcad
Fix typos
recmo b658f07
WIP EIP191
recmo 0a00916
WIP TODO
recmo d3168c4
WIP Replay attacks
recmo d117b9b
Fixes the sorted by name example encoding
dekz 5703264
Remove Solidity hash
recmo 121b07a
Added note on replay protection
recmo 73cfdec
Redesign domain separator
recmo e2cb963
Include images and simple motivation
dekz 780bb68
Merge branch 'master' into master
dekz 71d93f9
Fix up EIP metadata formatting
dekz 7c35e86
Merge pull request #3 from 0xProject/updates/eip-formatting
dekz e9387c8
Merge branch 'master' into master
dekz 08c3741
Merge branch 'master' into master
dekz 5c307ce
Add domain separator
recmo 4f74a93
Remove replay attacks from todo list
recmo 6bf149b
Add Jacob Evans to authors
recmo 43b838b
Clarify encodeData
recmo 6d173d9
Rename Message example to Mail
recmo f5f80fa
Update mock signing screen
recmo 0c57087
Rework EIP712Domain
recmo 154834e
Update Solidity example
recmo 948aa83
Update Javascript example
recmo bcf5c95
Relocate files
recmo 7b85ba3
Rename DomainSeparator to EIP712Domain (fix)
recmo 18f0248
Move examples to separate files
recmo dcc3b33
Remove httpOrigin domain parameter
recmo d179610
Update JSON-Schema
recmo daba29f
Add registery of version bytes
recmo 452670e
Add eip712 to eip191 registery
recmo 8efb50a
Add requires header
recmo ec327ac
Set correct language on all snipets
recmo bfd3187
GitHub highlighting for Solidity files
recmo 0cabeb3
Update Web3 API specification
recmo 39f049d
Use abi.encode where possible
recmo f062976
Update JSON-RPC specification
recmo 81c4295
Asset path repo is ethereums
recmo 997af5c
Correctly spelling of registry
recmo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
This file contains hidden or 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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -46,6 +46,17 @@ Additionally, `0x19` has been chosen because since ethereum/go-ethereum#2940 , t | |||||
|
|
||||||
| Using `0x19` thus makes it possible to extend the scheme by defining a version `0x45` (`E`) to handle these kinds of signatures. | ||||||
|
|
||||||
| ### Registry of version bytes | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| | Version byte | EIP | Description | ||||||
| | ------------ | -------------- | ----------- | ||||||
| | `0x00` | [191][eip-191] | Data with intended validator | ||||||
| | `0x01` | [712][eip-712] | Structured data | ||||||
| | `0x45` | [191][eip-191] | `personal_sign` messages | ||||||
|
|
||||||
| [eip-191]: https://eips.ethereum.org/EIPS/eip-191 | ||||||
| [eip-712]: https://eips.ethereum.org/EIPS/eip-712 | ||||||
|
|
||||||
| ### Example | ||||||
|
|
||||||
| function submitTransactionPreSigned(address destination, uint value, bytes data, uint nonce, uint8 v, bytes32 r, bytes32 s) | ||||||
|
|
||||||
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or 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 hidden or 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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.