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.
eth_sign
Legacy The hex string containing bytes looks something like this in Metamask when an end user wishes to sign (from the legacy eth_sign
approach):
eth_signTypedData
as described in EIP-712
Improved 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
ngrok
Run 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.js
sign_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
function submitProof()
1. 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
function testVerify()
2. 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
.
DOMAIN_SEPARATOR
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 salt
is 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
submit_proof.py
The python code. It runs a tornado(HTTP) server that listens on port 6635 on three endpoints
/flat
-sign.js
sends the 'flat' message object and the signature generated byeth_signTypedData
on the same message object to this endpoint/nested
-sign_nested.js
sends the 'nested' message object and the signature generated byeth_signTypedData
on 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.js
tosubmit_proof.py
(http://localhost:6635/nested
):Check the
messageObject
field
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.