EIP-712: Signing and Verifying Typed Ethereum messages
Prerequisites
It is absolutely critical that you would have gone through at least one of our onboarding guides that will teach you the way EthVigil handles user accounts, signing up, logging in, deploying contracts etc.
If you haven't, go check them out.
Evolution of message signing standards on Ethereum
For some context, check out the legacy way of signing and verifying messages on Ethereum.
The above approach only allows for signing of a bytestring. Which obviously, does not translate well to a good user experience.
EIP stands for Ethereum Improvement Proposal. Read the EIP-712 document here
The document states the motivation behind EIP-712 as
to improve the usability of off-chain message signing for use on-chain. We are seeing growing adoption of off-chain message signing as it saves gas and reduces the number of transactions on the blockchain. Currently signed messages are an opaque hex string displayed to the user with little context about the items that make up the message.
Legacy eth_sign
The hex string containing bytes looks something like this in Metamask when an end user wishes to sign (from the legacy eth_sign approach):

Improved eth_signTypedData as described in EIP-712
Instead, EIP-712 presents a clear schema and structure of the message to be signed that looks like this in Metamask

How to run this example
The code for this example can be found in our github repo.
Python
pip install -r requirements.txt
python submit_proof.py
Run ngrok
Open another terminal window/tab.
Tunnel it to port 6635 on localhost
./ngrok http 6635
We will need the link to set up webhook integration on the SignatureExtracted event emitted fom the contracts.

Deploy the smart contracts
Note down the deployed contract addresses. 0x8e12f01dae5fe7f1122dc42f2cb084f2f9e8aa03 and 0x583e7a6f0de3ecbe8e878878d5ac5c19bc1c807e respectively, for this example, on the Goerli Ethereum Test network.
Setting up the frontend
sign_flat.html&sign.jssign_nested.html&sign_nested.js
Arrange these to be served through a server. For this example, we will use the Python 3 module http.server directly from the command line for a quick setup.
On further interaction, sign.js launches Metamask to generate a 65 byte signed data that is sent to the python script, submit_proof.py
Now the frontend can be accessed through the browser at http://localhost:8000

Switch Metamask network to Goerli test net
This is necesssary because Metamask places a strict check on the network ID specified in the data message object sent to eth_signTypedData. And all our signed messages are sent to contracts deployed on this specific test network, Goerli.
Configure webhook integration with deployed contracts
Find out the ngrok forwarding URL from the above section
Add the ngrok URL as a webhook integration with a trailing /webhook path, for eg, https://8237bb46.ngrok.io/flat

Edit javascript files
Fill in the respective deployed contract addresses for the flat and nested struct examples in sign.js and sign_nested.js. Find the section at the end where the script sends a XHR to the python tornado server running at http://localhost:6635
Putting it all together
Visit either sign_flat.html or sign_nested.html. According to our section on setting up the frontend they will be available at
- http://localhost:8000/sign_flat.html
- http://localhost:8000/sign_nested.html
Sign a flat structured typed message object
Click on the button on the page. It should cause Metamask to pop up a new window. Observe the message object contents and typed schema.

Sign and you should see something like this:

Check the python submit_proof.py logs
This is an actual tx on the goerli testnet. You can verify it on Etherscan explorer
Soon the python script will also receive the event data payload corresponding to the SignatureExtracted event

Observe ['event_data']['signer'] is 0x00ead698a5c3c72d5a28429e9e6d6c076c086997
That's the address I used to sign the above typed structured data.

Sign a nested structured typed message object
Visit the sign_nested.html file and follow the similar steps as mentioned above.
Observe how the nested data structure shows up on the signing alert window.

Check the submit_proof.py logs as described above.
Structure of this example
For a detailed flow diagram, visit the last section in this doc: Detailed Flow Diagram
An overview of the entire setup

The smart contracts
There are two contracts included with this example to demonstrate two different scenarios regarding the complexity of the data structure to be signed.
- A flat structure with elementary types – EIP712FlatStruct.sol
- A nested structure – EIP712NestedStruct.sol
Both of them have two methods to play with
1. function submitProof()
The ECDSA signature generated by eth_signTypedData is 65 bytes long. It is broken down into three components r, s, v which are passed to this method.
The in-built Solidity function ecrecover() is run against the hashed value of the contents of the message object (Unit memory _msg)
Learn more about the structure of the message objects in the section: Details of the data type to be signed
2. function testVerify()
This contains pre-calculated values of r, s, v components of the signature
- signed by the private key of the Ethereum address
0x00EAd698A5C3c72D5a28429E9E6D6c076c086997 - corresponding to the message object,
_msgobj(also populated within the method)
This is a call to demonstrate that running ecrecover() against the hash of the contents of the _msgobj and the r, s, v components yields the expected signer address, 0x00EAd698A5C3c72D5a28429E9E6D6c076c086997.
A note about DOMAIN_SEPARATOR
From the solidity code of the smart contracts,
Compare it to the standard laid down in EIP-712 docs
This example assumes the following
- a constant
chainId= 5, because our code is deployed on the Goerli testnet through the Beta EthVigil APIs (contained in the smart contract code, not dynamically initialized through constructors or transactions)- a fixed value for
verifyingContract. This is again left to the app designer and contract author to agree on to prevent phishing attacks.bytes32 saltis omitted without consequence
You can take up implementing the above features as an exercise of your own. Do reach out to us and we would be happy to assist in your development efforts.
Details of the structured typed data to be signed
- Flat structure with elementary types – EIP712FlatStruct.sol
- Nested structure – EIP712NestedStruct.sol
Preparing message object to be signed
Take a look at the links in Further reading section if you are not familiar with the EIP-712 standard. A detailed discussion of concepts like domain, type descriptor strings are out of the scope of this document.
sign.js and sign_nested.js prepare the message object to be passed to eth_signTypedData via Metamask.
Let us take a look at the code inside sign_nested.js
data is the JSON serialized representation of the message format as defined by EIP-712. The message key in it holds the actual contents of the message object to be signed.
The message object holds the nested authorizer object with elementary typed fields: uint256 userId, address wallet
The python code. submit_proof.py
It runs a tornado(HTTP) server that listens on port 6635 on three endpoints
/flat-sign.jssends the 'flat' message object and the signature generated byeth_signTypedDataon the same message object to this endpoint/nested-sign_nested.jssends the 'nested' message object and the signature generated byeth_signTypedDataon the same message object to this endpoint/webhook- Registered as a webhook integration endpoint on the deployed contract via EthVigil APIs
How does EthVigil API accept nested struct types as function arguments?
Once the frontend sends the signed message object as well as the message data structure (revisit Preparing message object to be signed in case of doubts) to http://localhost:6635/flat or http://localhost:6635/nested, the python script submit_proof.py sends the arguments corresponding to the solidity function submitProof() to EthVigil API in a list format which is encoded by the API to represent the message object correctly in the transaction sent to this contract.
Check Detailed Flow section to have a quick graphic representation of the above logic
Structure expected by
submitProof()inEIP712NestedStruct.sol: revisit section Details of the structured data type to be signedStructure sent via XHR from
sign_nested.jstosubmit_proof.py(http://localhost:6635/nested):Check the
messageObjectfield
The following section of the code expands the message object received into a list in the same order as expected by the function encoding, and as described in the type description in the smart contract
bytes32 private constant UNIT_TYPEHASH = keccak256("Unit(string actionType,uint256 timestamp,Identity authorizer)Identity(uint256 userId,address wallet)");
This expanded ordered list is sent as a JSON serialized string to the EthVigil API call.
Detailed flow diagram
