Skip to content

Commit

Permalink
[WIP] Add eth_signTypedData as a standard for machine-verifiable and …
Browse files Browse the repository at this point in the history
…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
LogvinovLeon authored and Arachnid committed Jun 9, 2018
1 parent c8c48a7 commit d686a65
Show file tree
Hide file tree
Showing 7 changed files with 714 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitattributes
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
11 changes: 11 additions & 0 deletions EIPS/eip-191.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

| 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)
Expand Down
446 changes: 446 additions & 0 deletions EIPS/eip-712.md

Large diffs are not rendered by default.

148 changes: 148 additions & 0 deletions assets/eip-712/Example.js
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');
106 changes: 106 additions & 0 deletions assets/eip-712/Example.sol
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;
}
}
Binary file added assets/eip-712/eth_sign.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/eip-712/eth_signTypedData.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit d686a65

Please sign in to comment.