How to Verify a Message Signature on Ethereum

This tutorial will teach you how to sign and verify a message signature using Web3.js and Ethers.js

Message signatures can be generated with any arbitrary message and an Ethereum wallet’s private key. Message signatures can be used to create a verification system for any application requiring a user to prove their identity. For example, you might consider using this tutorial to create an application allowing users to e-sign documents or pdfs. Creating and verifying signatures does not require a connection to the Ethereum network because it utilizes a message, wallet address, and private key to generate a signature hash. This means the entire process can occur off-chain and does not cost any gas to execute.

In part one of this tutorial, we will explore how a signature can be generated and verified using Viem, Ethers.js, or Web3.js libraries.

In part two, we will build upon what we learned in part one to build a full-stack signature generation DApp using ReactJS. With Ethers.js, we will use the provided starter files to create a frontend UI that lets you connect to a MetaMask wallet to sign/verify messages.

Part two of this tutorial will not cover ReactJS. We will only focus on the functionality necessary to connect the frontend UI to MetaMask. Therefore, you should have an understanding of React and React hooks such as useState and useEffect.


Prerequisites

Before you continue in this tutorial, please ensure that you have accomplished the following:

  • Install Node.js.
  • Install a MetaMask browser wallet.
  • Install an IDE (such as VS Code).
  • Create an Alchemy account.

Install Node.js

Head to Node.js and download the LTS version.

You can verify your installation was successful by running npm -version in your macOS terminal or Windows command prompt. A successful installation will display a version number, such as:

shell
$6.4.1

Install MetaMask

Install MetaMask, a virtual wallet extension used to manage your Ethereum address and private key.

Install an IDE

A development environment makes editing code in our project much easier to navigate. If you would like to follow along with exactly what I am using for this tutorial go ahead and install Visual Studio Code. However, feel free to use whatever development environment you prefer.

Connect to Alchemy

Although we are not sending any transactions on-chain, we will still use an Alchemy API key so we may monitor on-chain functionality if we so choose to add it in the future.

  1. Create a free Alchemy account.
  2. From the Alchemy Dashboard, hover over Apps then click +Create App.
  3. Name your app Signature-Generator.
  4. Select Ethereum as your chain and Sepolia as your network.
    • Note: Because this tutorial does not perform any on-chain activity, you could use any testnet.
  5. Click Create app.

3816

Your dashboard should look like this


Setup Project Environment

Open VS Code (or your preferred IDE) and enter the following in the terminal:

shell
$mkdir my verify-msg-signature
>cd verify-msg-signature

Once inside our project directory, initialize npm (node package manager) with the following command:

shell
$npm init

Press enter and answer the project prompt as follows:

json
1package name: (signature-generator)
2version: (1.0.0)
3description:
4entry point: (index.js)
5test command:
6git repository:
7keywords:
8author:
9license: (ISC)

Press enter again to complete the prompt. If successful, a package.json file will have been created in your directory.


Install environment tools

The tools you will need to complete this tutorial are:

  • Viem (recommended) or Ethers.js to utilize their cryptographic functions and create unique signatures.
  • dotenv so that you can store your private key and API key safely.

To install the above tools, ensure you are still inside your root folder and type the following commands in your terminal:

Viem (Recommended):

shell
$npm install viem

Ethers.js:

shell
$npm install --save ethers

Dotenv:

shell
$npm install dotenv --save

Create a Dotenv File

Create an .env file in your root folder. The file must be named .env or it will not be recognized.

In the .env file, we will store all of our sensitive information (i.e., our Alchemy API key and MetaMask private key).

Copy the following into your .env file:

.env
API_URL = "https://eth-sepolia.g.alchemy.com/v2/{YOUR_ALCHEMY_API_KEY}"
PRIVATE_KEY = "{YOUR_PRIVATE_KEY}"
  • Replace {YOUR_ALCHEMY_API_KEY} with your Alchemy API key found in your app’s dashboard, under VIEW KEY:

1903

  • Replace {YOUR_PRIVATE_KEY}with your MetaMask private key.

To retrieve your MetaMask private key:

  1. Open the extension, click on the three dots menu, and choose Account Details.

535

2. Click Export Private Key and enter your MetaMask password.

536

3. Replace the Private Key in your .env file with your MetaMask Private Key.


Verify Message Signatures

The following section provides two options for verifying message signatures:

  • Using Viem (recommended).
  • Using Ethers.js v6.

Depending on your preferred library, feel free to use the appropriate tabs.

In your root folder create a file named VerifyMsg.js and add the following lines of code to it:

1import { createWalletClient, http } from 'viem'
2import { privateKeyToAccount } from 'viem/accounts'
3import { mainnet } from 'viem/chains'
4
5const main = async () => {
6 require("dotenv").config();
7 const { API_URL, PRIVATE_KEY } = process.env;
8
9 // Create account from private key
10 const account = privateKeyToAccount(PRIVATE_KEY);
11
12 // Create wallet client
13 const walletClient = createWalletClient({
14 account,
15 chain: mainnet,
16 transport: http(API_URL)
17 });
18
19 console.log('Wallet address:', account.address);
20};
21
22main();

The code above creates an asynchronous function that contains the necessary variables to start using Alchemy’s provider with Ethers. Below, you can see the same code with commented explanations at each step:

1const main = async () => {
2 require("dotenv").config();
3 // Imports the secret .env file where our Private Key and API are stored
4 const { API_URL, PRIVATE_KEY } = process.env;
5 // We can now use these aliases instead of using our actual keys.
6 const { ethers } = require("ethers");
7 // Importing Ethers library
8 const { hashMessage } = require("@ethersproject/hash");
9 // Importing the hashMessage function which takes a string and converts it to a hash
10 // We need this because the Ethers sign function takes a message hash
11 // Note: We do not need this when using the Web3 library because the sign function automatically converts the message into a hash
12 // Creates a new provider instance with Alchemy using Ethers.js
13 const ethersAlchemyProvider = new ethers.JsonRpcProvider(API_URL);
14 };
15
16 main();

In the same function, create a message to sign and a wallet instance, then use the wallet to both:

  1. Sign our message with the library’s signMessage function.
  2. Verify it with the verification utilities.

The following code accomplishes the above and describes each action with commented notes:

1import { createWalletClient, http } from 'viem'
2import { privateKeyToAccount } from 'viem/accounts'
3import { mainnet } from 'viem/chains'
4import { verifyMessage } from 'viem'
5
6const main = async () => {
7 require("dotenv").config();
8 const { API_URL, PRIVATE_KEY } = process.env;
9
10 // Create account from private key
11 const account = privateKeyToAccount(PRIVATE_KEY);
12
13 // Create wallet client
14 const walletClient = createWalletClient({
15 account,
16 chain: mainnet,
17 transport: http(API_URL)
18 });
19
20 const message = "Let's verify the signature of this message!";
21 console.log('Wallet address:', account.address);
22
23 // Sign the message
24 const signature = await walletClient.signMessage({
25 account,
26 message
27 });
28
29 // Verify the signature
30 const isValid = await verifyMessage({
31 address: account.address,
32 message,
33 signature
34 });
35
36 console.log('Signature:', signature);
37 console.log('Is valid signature:', isValid);
38};
39
40main();

When using web3.js you can alternatively use the following to verify a message signature:

Web3.js Sign Alternative
1const messageSigner = web3.eth.accounts.recover(message, signMessage.v, signMessage.r, signMessage.s);

Great! Now, we should add tests to check whether our message was signed and verified correctly.

The following code is the entire script with the checks:

1import { createWalletClient, http } from 'viem'
2import { privateKeyToAccount } from 'viem/accounts'
3import { mainnet } from 'viem/chains'
4import { verifyMessage } from 'viem'
5
6const main = async () => {
7 require("dotenv").config();
8 const { API_URL, PRIVATE_KEY } = process.env;
9
10 try {
11 // Create account from private key
12 const account = privateKeyToAccount(PRIVATE_KEY);
13
14 // Create wallet client
15 const walletClient = createWalletClient({
16 account,
17 chain: mainnet,
18 transport: http(API_URL)
19 });
20
21 const message = "Let's verify the signature of this message!";
22
23 // Sign the message
24 const signature = await walletClient.signMessage({
25 account,
26 message
27 });
28
29 // Verify the signature
30 const isValid = await verifyMessage({
31 address: account.address,
32 message,
33 signature
34 });
35
36 console.log("Success! The message: " + message + " was signed with the signature: " + signature);
37 console.log("The signer was: " + account.address);
38 console.log("Signature verification result: " + (isValid ? "Valid" : "Invalid"));
39
40 } catch (err) {
41 console.log("Something went wrong while verifying your message signature: " + err);
42 }
43};
44
45main();

To use your script, type the following command in your terminal:

shell
$node VerifyMsg.js

If successful, the message signature hash and signer address should return something like the following:

shell
$Success! The message: Let's verify the signature of this message! was signed with the signature: 0x16a08da8a50dc4ec2abf080528440821fc749323c69b6d38d88b8dedc03961772a7da6a2c74fcbde325085e552fcb197673e2a4741189bd6f9d9e1d07236c37c1b
>The signer was: 0x5DAAC14781a5C4AF2B0673467364Cba46Da935dB
>Signature verification result: Valid

Awesome! You successfully signed a message and verified its signature!

You now know how to verify message signatures using Viem and Ethers.js. Check out part two to learn how to create a signature generator DApp and verify signatures using MetaMask!