EIP-712: Signing and Verifying Typed Ethereum messages
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.
The hex string containing bytes looks something like this in Metamask when an end user wishes to sign (from the legacy
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.
pip install -r requirements.txt
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.
0x583e7a6f0de3ecbe8e878878d5ac5c19bc1c807e respectively, for this example, on the Goerli Ethereum Test network.
Setting up the frontend
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,
Now the frontend can be accessed through the browser at
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,
Fill in the respective deployed contract addresses for the flat and nested struct examples in
sign_nested.js. Find the section at the end where the script sends a XHR to the python tornado server running at
Putting it all together
sign_nested.html. According to our section on setting up the frontend they will be available at
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:
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
That's the address I used to sign the above typed structured data.
Sign a nested structured typed message object
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.
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
The ECDSA signature generated by
eth_signTypedData is 65 bytes long. It is broken down into three components
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
This contains pre-calculated values of
v components of the signature
- signed by the private key of the Ethereum address
- 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
v components yields the expected signer address,
A note about
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_nested.js prepare the message object to be passed to
eth_signTypedData via Metamask.
Let us take a look at the code inside
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.
message object holds the nested
authorizer object with elementary typed fields:
The python code.
It runs a tornado(HTTP) server that listens on port 6635 on three endpoints
sign.jssends the 'flat' message object and the signature generated by
eth_signTypedDataon the same message object to this endpoint
sign_nested.jssends the 'nested' message object and the signature generated by
eth_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/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
EIP712NestedStruct.sol: revisit section Details of the structured data type to be signed
Structure sent via XHR from
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.