Solidity - ecrecover this article explains how smart contracts verify signatures. This article will explain how to use Ethers.js to sign and recover. This article uses Ethers.js version 6.7.1.

Signer

The Signer object will be used in the following content. Here is a brief explanation of how to obtain it.

Private Key

1
2
3
4
5
import { JsonRpcProvider, Wallet } from "ethers";

const privateKey = '0x...'
const provider = new JsonRpcProvider(url);
const signer = new Wallet(privateKey, provider);

Browser Wallet

1
2
3
4
import { BrowserProvider } from "ethers";

const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();

Sign

Unlike Web3.js has many methods, Ethers.js has the following synchronous and asynchronous versions:

  • signer.signMessage
  • signer.signMessageSync

For EIP-712 related methods, refer to Using Ethers.js to Sign and Recover EIP-712 Typed Structured Data.

signer.signMessage

Always use EIP-191 prefix

1
signer.signMessage(message: string | Uint8Array): Promise<string>
  • message: Content to sing. If it is a string, is treated as plain text, and binary data uses Uint8Array.
  • Return: A Promise object, and the content is a signature.

With the prefix, the signing content is

1
“\x19Ethereum Signed Message:\n” + message.length + message

For example, sign binary content

1
2
3
4
5
6
7
8
9
10
import { getBytes } from "ethers";

const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53';
const messageHashBytes = getBytes(message);

// use arrayify in v5
// const messageHashBytes = ethers.utils.arrayify(message);

// 0x03105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f30029ec14347d37d8115cebcf915dc8f6f74c011698d9b7aea15c184f2277197a1c
const signature = await signer.signMessage(messageHashBytes);

A message will pop up in Metamask as follows:
簽署

We can use the library to split the signature.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Signature } from "ethers";

// {
// _type: 'signature',
// networkV: null,
// r: '0x03105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f3',
// s: '0x0029ec14347d37d8115cebcf915dc8f6f74c011698d9b7aea15c184f2277197a',
// v: 28
// }
Signature.from(signature).toJSON();

// use splitSignature in v5
// ethers.utils.splitSignature(signature);

Using prefix in smart contracts:

1
2
3
bytes32 messageHash = 0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53;
bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash));
return ecrecover(hash , v, r, s);

Or sign plain text content:

1
2
3
const message = 'OK!';
// 0x7dff4feac0d0bf31d5b11ef3793bf9d00e7b9028eaaa9737f5079685ff435c9c7a22e79b7dab0b0d37524a0f9621d98b76fbc77b6402f87d142746eee61edcb21c
const signature = await signer.signMessage(message);

A message will pop up as follows:
簽署純文字

Solidity

1
2
3
string memory message = "OK!";
bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n3", message));
return ecrecover(hash , v, r, s);

Note that if the text is UTF-8, the length of the signature on the chain must be bytes. For example:

1
2
// 6
new Blob(['中文']).size

signer.signMessageSync

Synchronous version of signMessage, only available in Node.js

1
signer.signMessageSync(message: string | Uint8Array): string
  • message: Content to sing. If it is a string, is treated as plain text, and binary data uses Uint8Array.
  • Return: signature.

Get the signature directly

1
const signature = signer.signMessageSync(message);

Others are the same as signMessage and will not repeat.

Signature Format

The above signature has two formats: Splitted and not splitted. Wee can split it by fixed length. The first 32 bytes are r, the middle 32 bytes are s, and the last 1 byte is v. The example above:

signature: 0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a356320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f81b

r: 0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a3
s: 0x56320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f8
v: 0x1b

Therefore, in addition to using the method mentioned above to split the signature, we can also simply do it ourself:

1
2
3
4
5
6
7
function splitSignature(signature) {
return {
r: `0x${signature.substring(2, 66)}`,
s: `0x${signature.substring(66, 130)}`,
v: `0x${signature.substring(130, 132)}`
}
}

Recover Signature

In addition to verify and recover in smart contracts, we can also use Ethers.js. It can be used as back-end program for login. We can use the following function:

  1. verifyMessage
  2. recoverAddress

verifyMessage

The EIP-191 prefix is ​​always used, and plain text or binary data can be entered for the signature content.

1
verifyMessage(message: Uint8Array | string, sig: SignatureLike): string
  • message: Content to sing. If it is a string, is treated as plain text, and binary data uses Uint8Array.
  • sig: Signature. It can be a not splitted string or an object containing vrs.
  • Return: Signer address.

Binary data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { getBytes, verifyMessage } from "ethers";

const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53';
const messageHashBytes = getBytes(message);
const signature = '0x03105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f30029ec14347d37d8115cebcf915dc8f6f74c011698d9b7aea15c184f2277197a1c';

// or vrs
// const signature = {
// r: '0x03105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f3',
// s: '0x0029ec14347d37d8115cebcf915dc8f6f74c011698d9b7aea15c184f2277197a',
// v: 28
// };

// 0xDD980c315dFA75682F04381E98ea38BD2A151540
const address = verifyMessage(messageHashBytes, signature);

Plain text content

1
2
const message = 'OK!';
const address = verifyMessage(message, signature);

recoverAddress

The EIP-191 prefix is ​​always used, and pm;y binary data can be entered for the signature content.

1
recoverAddress(digest: Uint8Array | string, signature: SignatureLike): string
  • digest: Signed content, the string must be a string starting with 0x or use Uint8Array.
  • message: Content to sing. If it is a string, it must be starting with 0x, or uses Uint8Array.
  • sig: Signature. It can be a not splitted string or an object containing vrs.
  • Return: Signer address.

Strings starting with 0x can be passwd in directly or converted using getBytes.

1
2
3
4
import { recoverAddress } from "ethers";

const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53';
const address = recoverAddress(message, signature);

Signature Malleability

Ethers.js has no problem with Signature Malleability. A tampered signature will cause an INVALID_ARGUMENT error.

Conclusion

  1. It is more general to use signMessage for signing.
  2. To verify and recover the plain text of the signature, you can use verifyMessage.
  3. Use recoverAddress to verify and recover the binary content of the signature.

Further Reading

Solidity - ecrecover
Solidity - Signature Malleability
Using Web3.js to Sign and Recover
Using Ethers.js to Sign and Recover EIP-712 Typed Structured Data