Thank you for your interest in this post! We’re undergoing a rebranding process, so please excuse us if some names are out of date. Also have in mind that this post might not reference the latest version of our products. For up-to-date guides, please check our documentation site.
The signing and validation of Ethereum signatures is a key technology required for Meta-transactions, and this tutorial will focus on a practical way to get started with understanding those signatures. Recently, Zeppelin announced that it had joined the Gas Station Network Alliance, a new group focused on the standardization of Meta-transactions for the Ethereum network. Meta-transactions are designed to solve the onboarding problem that many DApps face when trying to get new users: who should pay the gas? It’s hard to onboard new users if users don’t already have ETH, so Meta-transactions allow for someone else (usually the DApp developer) to pay the gas for them and allow for a smooth onboarding experience.
The technology that makes Ethereum signatures possible (and nearly all of blockchain!) is ECDSA, or Eliptic Curve Digital Signature Algorithm. They are used in both Bitcoin and Ethereum, and form a foundational technology for cryptography in general. Besides Meta-transactions, Ethereum signatures are used for decentralized exchanges, state channels, and a host of other very interesting applications on Ethereum (and blockchain in general). The OpenZeppelin library provides a handy signature recovery library that is already audited and secure, if you’re interested in building production code.
So what are we going to do?
We’re going to:
- Assume you have a basic node project setup where you can create and run a simple JavaScript program.
- Assume you know how to use https://remix.ethereum.org, which we’ll use to create and deploy a simple Solidity contract.
- Write a JavaScript that will sign a message with a private key.
- Write a Solidity contract that will recover the signer’s public key.
If you’ve been looking for a tutorial on creating or validating signatures, you’ll notice that most examples rely upon using the web3 library and connecting to an Ethereum node. I wanted something simpler that was straight to the point, so I opted for the eth-crypto library. For a more in-depth understanding of ECDSA signatures, Hackernoon has a great deep dive.
eth-crypto
The library we’re going to use is eth-crypto. I found it via this little comment in a Stack Exchange post that told me everything I needed to know.
eth-crypto actually reference this comment in their GitHub.
I dug into it a bit, and sure enough, eth-crypto is pretty rad, and it seemed to give me exactly what I wanted. No need to create a new Web3()
object. Easy!
Signing with eth-crypto
Signing something using the eth-crypto library is a straightforward process.
const EthCrypto = require("eth-crypto"); const signerIdentity = EthCrypto.createIdentity(); const message = EthCrypto.hash.Keccak256(<<your message to be singed>>); const signature = EthCrypto.sign(signerIdentity.privateKey, message)
That’s it. Most web3 signature tutorials assume that you’re either signing something in the browser (and thus want to use MetaMask) or using a node to handle your private keys. I’m not concerned about private key security, and I’m not running anything in a browser. Eth-crypto will generate a private key for you, which is stored on the returned signerIdentity
object and is ready to use to sign whatever you want. If you open up your signerIdentityobject
, it will look something like this: (FYI: this is a fake private key.)
{ address: "0x90f8bf6a349f320ead074411a4b0e7944ea8c9c1", privateKey: "0x4f3edf983ac236a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d", publicKey: "971a0b8bd54f7698b017b7785e77a5d7da2154ab5fab68644f8af9276edbec4fd001c57af2611fc58760992b7c4a575d6d1f6a875b68963dc868d06729efb2e5" };
Easy enough.
Sign something
Alright, time to get started. In this example, let’s sign the value “Hello World!”.
Create a JavaScript file in your node project. I use nano, but an IDE is fine too. Let’s call it signSomething.js
const EthCrypto = require("eth-crypto"); const signerIdentity = EthCrypto.createIdentity(); const message = EthCrypto.hash.keccak256([ {type: "string",value: "Hello World!"} ]); const signature = EthCrypto.sign(signerIdentity.privateKey, message); console.log(`message: ${message}`); console.log(`signature: ${signature}`); console.log(`signer public key: ${signerIdentity.address}`);
Since we’re eventually going to try to validate this signature on-chain using Solidity, you’ll notice my message isn’t just the result of hashing “Hello World!”; rather, it’s the result of hashing the object [{type: "string", value: "Hello World!"}]
. Basically, this is because we need to match the format that the Solidity tool abi.encodePacked
uses, which is the tool we’ll be using later to rebuild the same hash in a smart contract.
You’ll need to follow this format for all the values you want to sign in a message. So, for example, if you wanted to sign a message that included a number (5, for example) and a string (Banana, for example), you would need to sign an object that looks like this:
[ { type: "uint256", value: "5" }, { type: "string", value: "Banana" } ]
Feel free to read about all the possible data types in Solidity.
Save your existing code and run it with:
node signSomething.js
You should get an output like this:
message: 0x3ea2f1d0abf3fc66cf29eebb70cbd4e7fe762ef8a09bcc06c8edf641230afec0 signature: 0x1556a70d76cc452ae54e83bb167a9041f0d062d000fa0dcb42593f77c544f6471643d14dbd6a6edc658f4b16699a585181a08dba4f6d16a9273e0e2cbed622da1b signer public key: 0x80C67eEC6f8518B5Bb707ECc718B53782AC71543]
Great!
Now we have our message (a hash of our “Hello World” object), a signed version of the message as our signature, and, for convenience, the public key that corresponds to the private key stored in the signerIdentity
object that was used to sign the message. Time to switch over to Solidity.
Verifying in Solidity
Now that we have a way to sign a message, we need to be able to check the signature in the blockchain. Open up remix.ethereum.org and start a new file. Here we’ll create the Solidity code necessary to verify our signature (i.e., to verify that the corresponding private key to the Signer Public key was indeed used to sign the message).
Thanks to the built-in Solidity function ecrecover, recovering the public key is pretty easy to do. But first, we’re going to need to break down our signature into its respective r, s, and v values. (Confused? Read this). This can be done directly in the eth-crypto library, but I’m going to do it in Solidity for now. The code is adapted from the eth-crypto tutorials (a great reference, btw).
pragma solidity ^0.5.0; contract VerifySig { function splitSignature(bytes memory sig) public pure returns (uint8, bytes32, bytes32) { require(sig.length == 65); bytes32 r; bytes32 s; uint8 v; assembly { // first 32 bytes, after the length prefix r := mload(add(sig, 32)) // second 32 bytes s := mload(add(sig, 64)) // final byte (first byte of the next 32 bytes) v := byte(0, mload(add(sig, 96))) } return (v, r, s); } }
Using Remix, deploy the above code and then call the function splitSignature
with the value of signature that our JavaScript code above returned. The function call should return something similar to this, which represents our v, r, and s values respectively.
- 0: uint8: 27
- 1: bytes32: 0x1556a70d76cc452ae54e83bb167a9041f0d062d000fa0dcb42593f77c544f647
- 2: bytes32: 0x1643d14dbd6a6edc658f4b16699a585181a08dba4f6d16a9273e0e2cbed622da
Goody!
We can now use our original message hash and the Solidity function ecrecover(messageHash, v,r,s)
to get back the address of the signer. Interestingly, ecrecover(messageHash, v,r,s)
will nearly always return an address. It’s up to us to be sure to check that it’s the correct address.
So let’s build a contract that takes our original hashed message and our signed message, and returns to us the address used to sign the message.
First, we’ll split our signed message into its parts (v, r, s), and then we’ll run ecrecover along with our original hashed message.
Again, adapted from the eth-crypto tutorials.
pragma solidity ^0.5.0; contract Verify { function recoverSigner(bytes32 message, bytes memory sig) public pure returns (address) { uint8 v; bytes32 r; bytes32 s; (v, r, s) = splitSignature(sig); return ecrecover(message, v, r, s); } function splitSignature(bytes memory sig) public pure returns (uint8, bytes32, bytes32) { require(sig.length == 65); bytes32 r; bytes32 s; uint8 v; assembly { // first 32 bytes, after the length prefix r := mload(add(sig, 32)) // second 32 bytes s := mload(add(sig, 64)) // final byte (first byte of the next 32 bytes) v := byte(0, mload(add(sig, 96))) } return (v, r, s); } }
Now if you call the function recoverSigner
(which calls into splitSignature
) using the message and signature from our JavaScript code, it should return the public key that corresponds to the private key that was used to sign the message.
Tada!
How is this useful?
Good question! Because we can independently recover the public key without knowing the private key, we can prove that if someone provides us a signed message that we can recover a public key from, we know that the person who owns the corresponding private key signed the message.
Totally legit signature.
For our smart contracts, this means that if we provide them all the ingredients of a message, (so the smart contract can hash it) and the signature of a message we have already hashed ourselves, the smart contract will be able to recover the address of the signer. If this address is a correct address (i.e., we keep a record of what addresses the smart contract recognizes as valid), then the smart contract knows that the ingredients of a message are valid. Make sense?
Lets get specific:
First, add an address called importantAddress as a variable in a contract and set it with a constructor.
code omitted for brevity... contract Verify { address importantAddress; constructor (address _importantAddress) public{ importantAddress = _importantAddress; } ...code omitted for brevity
When we create the contract, we set the address the smart contract will consider to be the address with the authority to sign messages.
Now create a function called isValidData
, which is going to take in three parameters: an integer, a string, and a signed message. The point of isValidData
is only to prove that the owner of importantAddress
signed a message that included our integer and our string.
Our function first needs to rebuild the message in the same way our JavaScript program has built it. Remember, we can’t just pass our integer and string into the hashing function; we need to build that special object like we did before.
Return to the JavaScript code and make a change from:
const message = EthCrypto.hash.keccak256([ { type: "string", value: "Hello World!" } ]);
to:
const message = EthCrypto.hash.keccak256([ { type: "uint256", value: "5" }, { type: "string", value: "Banana" } ]);
Run the JavaScript program again. It should again give us a message, signature, and Signer Public key, just like before (but with different values, of course).
In Solidity, we’re going to need to create a function that will build the same message from the same values (5, Banana)
so that it can compare it with the signature we have already generated in our JavaScript program, to recover our signing address and to check if the signing address is the same as our importantAddress
.
function isValidData(uint256 _number, string memory _word, bytes memory sig) public view returns(bool){
bytes32 message = keccak256(abi.encodePacked(_number, _word));
return (recoverSigner(message, sig) == importantAddress);
}
So our new JavaScript code should be as follows:
const EthCrypto = require("eth-crypto"); const signerIdentity = EthCrypto.createIdentity(); const message = EthCrypto.hash.keccak256([ { type: "uint256", value: "5" }, { type: "string", value: "Banana" } ]); const signature = EthCrypto.sign(signerIdentity.privateKey, message); console.log(`Message: ${message}`); console.log(`Signature: ${signature}`); console.log(`Signer Public key: ${signerIdentity.address}`);
The new Solidity code should be:
pragma solidity ^0.5.0; contract Verify { address importantAddress; constructor (address _importantAddress) public{ importantAddress = _importantAddress; } function isValidData(uint256 _number, string memory _word, bytes memory sig) public view returns(bool){ bytes32 message = keccak256(abi.encodePacked(_number, _word)); return (recoverSigner(message, sig) == importantAddress); } function recoverSigner(bytes32 message, bytes memory sig) public pure returns (address) { uint8 v; bytes32 r; bytes32 s; (v, r, s) = splitSignature(sig); return ecrecover(message, v, r, s); } function splitSignature(bytes memory sig) public pure returns (uint8, bytes32, bytes32) { require(sig.length == 65); bytes32 r; bytes32 s; uint8 v; assembly { // first 32 bytes, after the length prefix r := mload(add(sig, 32)) // second 32 bytes s := mload(add(sig, 64)) // final byte (first byte of the next 32 bytes) v := byte(0, mload(add(sig, 96))) } return (v, r, s); } }
First, run your JavaScript code. It should give you an output similar to the following:
Message: 0x97c943890b15f4dea02c3ae1653252489599957b280a95bf2e533fdbc8facb58 Signature: 0xd361e8ea11167286b3e9874de12a2e82a46a12d5adada287fc356f7a1583ce352aa8da5efafc3996294bddbafbec34f46932c081c9853e1233df46b2a2d216021c Signer Public key: 0x98b4d7B30aa38BadB24D95517796e19127975dD5
Copy your Signer Public key, this will be our importantAddress
. In Remix, deploy a new instance of the Verify contract using your Signer Public key as the constructor argument.
Interact directly with your contract in Remix by calling the isValidData
function with the arguments 5,"Banana",<<Signature>>
, where <<Signature>>
is the signature value from your JavaScript code output. If you did everything correctly, it should return true. Try the code again with the integer 6 instead of 5, and you will see that it returns false.
And this is cool because…?
This isn’t cool, it’s AWESOME! You just verified in a smart contract that the owner of this public key signed the message 5
and Banana
without the smart contract knowing the private key. This means you could use the JavaScript code to sign any sort of information and then share these signatures with whomever you want. The smart contract can always verify which signatures are valid. Here we are using a trivial example, but rather than 5
and Banana
, we could easily have used "withdrawal amount"
and "withdrawal address"
(with some serious security caveats!).
Now that you have the basics, dig into some more advanced tutorial.
Or, if you want to start learning about how this applies to Meta-transactions