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

Sign

Web3.js provides several off-chain signing functions. This article introduces the following three:

  1. web3.eth.sign
  2. web3.eth.accounts.sign
  3. web3.personal.sign

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

web3.eth.sign

This is an old function, and the signature does not use a prefix.

1
web3.eth.sign(message: Bytes, address: string): Promise<string>
  • message: The message to sign. It can only be binary content with a length of 32 bytes. It can be a Uint8Array or a string starting with 0x.
  • address: Address of the signer.
  • Return: A Promise object, and the content is a signature.

Metamask has disabled this feature. If you use it, you will see an error message in the console:

1
inpage.js:1 MetaMask - RPC Error: eth_sign has been disabled. You must enable it in the advanced settings {code: -32601, message: 'eth_sign has been disabled. You must enable it in the advanced settings'}

If you want to use it, you need to enable the settings in Metamask:
[Settings] => [Advanced] => [Eth_sign requests]
Eth_sign requests

Turn on and enter I only sign what I understand
I only sign what I understand

Try signing again, for example, run the following program:

1
2
3
4
5
const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53';
const account = '0xDD980c315dFA75682F04381E98ea38BD2A151540';

// 0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a356320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f81b
const signature = await web3.eth.sign(message, account);

We can see that the signing content is a hash.
Sign

Because this method is not recommended, a warning will appear every time you sign.
Warning

We can use the library to split the signature.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { fromRpcSig } from 'ethereumjs-util';

// {
// v: 27,
// r: <Buffer e1 07 7f b9 32 1c 18 7d 8a 43 92 68 96 ab ac 54 55 ce 6a dd 26 9e 09 8f 85 5f f0 59 d6 b8 46 a3>,
// s: <Buffer 56 32 0b e5 f6 d7 9c 4d 0e 55 83 d6 b9 a2 e5 0f ae 78 f1 fb 5f f0 55 35 41 e6 9c 66 da e2 b2 f8>
// }
const result = fromRpcSig(signature);

// {
// v: '0x1b',
// r: '0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a3',
// s: '0x56320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f8'
// }
const vrs = {
v: `0x${result.v.toString(16)}`,
r: `0x${result.r.toString('hex')}`,
s: `0x${result.s.toString('hex')}`
};

Corresponding to the smart contracts, it’s the first example without prefix.

1
2
3
4
5
6
7
bytes32 hash = 0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53;
uint8 v = 0x1b;
bytes32 r = 0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a3;
bytes32 s = 0x56320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f8;

// 0xDD980c315dFA75682F04381E98ea38BD2A151540
return ecrecover(hash , v, r, s);

web3.eth.accounts.sign

Private key using this function to sign, and it will add the EIP-191 prefix.

1
web3.eth.accounts.sign(data: string, privateKey: Bytes): SignResult
  • data: Content to sign. It can be text content or binary data(string starting with 0x).
  • privateKey: Signer’s private key. It can be a Uint8Array or a string starting with 0x.
  • Return: Signature object, please refer to the following example.

With the prefix, the signing content is

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

For example, we sign the same content:

1
2
3
4
5
6
7
8
9
10
11
12
const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53';
const privateKey = '0x....';

// {
// message: '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53',
// messageHash: '0x69a1658d9d5e169959e0e2acec0eb76ec15549ebc0680f45cfc25c4a3ca952e6',
// v: '0x1c',
// r: '0x3105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f3',
// s: '0x29ec14347d37d8115cebcf915dc8f6f74c011698d9b7aea15c184f2277197a',
// signature: '0x03105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f30029ec14347d37d8115cebcf915dc8f6f74c011698d9b7aea15c184f2277197a1c'
// }
const signature = web3.eth.accounts.sign(message, privateKey);

You will find that the signature result is different from the above, and the format is also different. At the same time, we also found that there is a bug here. The returned r and s miss leading 0. The correct answer should be:

1
2
0x03105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f3
0x0029ec14347d37d8115cebcf915dc8f6f74c011698d9b7aea15c184f2277197a

There is this bug in v4.1.1, it is normal in older versions 1.x. We can use the signature property first. Using prefix in smart contracts to check:

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

I have a PR to fix this bug. It should be no problem in 4.1.2 and above.

Or sign plain text content:

1
2
3
4
5
6
7
8
9
10
11
const message = 'OK!';

// {
// message: 'OK!',
// messageHash: '0x9632976ae3bb56254c6cc1a35ad3d11e7a7040fb6dde433ad2a1249afaf062f3',
// v: '0x1c',
// r: '0x7dff4feac0d0bf31d5b11ef3793bf9d00e7b9028eaaa9737f5079685ff435c9c',
// s: '0x7a22e79b7dab0b0d37524a0f9621d98b76fbc77b6402f87d142746eee61edcb2',
// signature: '0x7dff4feac0d0bf31d5b11ef3793bf9d00e7b9028eaaa9737f5079685ff435c9c7a22e79b7dab0b0d37524a0f9621d98b76fbc77b6402f87d142746eee61edcb21c'
// }
const signature = await web3.eth.accounts.sign(message, privateKey);

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

web3.eth.personal.sign

Browser wallets using this function to sign, and it will be prefixed with EIP-191.

1
web3.eth.personal.sign(data: string, address: string, passphrase: string): Promise<string>
  • data: Content to sign. It can be text content or binary data(string starting with 0x).
  • address: Address of the signer.
  • passphrase: Wallet password, not required.
  • Return: Signature object, please refer to the following example.

The prefix rules are the same as web3.eth.accounts.sign and will not repeat.

For example, execute the following signature:

1
2
3
4
5
const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53';
const account = '0xDD980c315dFA75682F04381E98ea38BD2A151540';

// 0x03105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f30029ec14347d37d8115cebcf915dc8f6f74c011698d9b7aea15c184f2277197a1c
const signature = await web3.eth.personal.sign(message, account, '');

A message will pop up as follows:
Sign

The result is the same as web3.eth.accounts.sign, but the format returned is different. Also try text:

1
2
3
4
const message = 'OK!';

// 0x7dff4feac0d0bf31d5b11ef3793bf9d00e7b9028eaaa9737f5079685ff435c9c7a22e79b7dab0b0d37524a0f9621d98b76fbc77b6402f87d142746eee61edcb21c
const signature = await web3.eth.personal.sign(message, account, '');

A message will pop up as follows:
Sign Text

But if it is data starting with 0x, but the length is not 32 Bytes

1
2
3
// Truncate the last 1 Byte
const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e';
const signature = await web3.eth.personal.sign(message, account, '');

The result will be a string of garbled characters
Garbled Characters

The smart contracts is also the same as web3.eth.accounts.sign 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 library 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 Web3.js. It can be used as back-end program for login. We can use the following function:

  1. web3.eth.accounts.recover
  2. web3.eth.personal.ecRecover

web3.eth.accounts.recover

This function supports multiple verification methods. You can enter a splitted or not splitted signature, with or without a prefix. There are multiple input methods:

1
web3.eth.accounts.recover(data: string, v: string, r: string, s: string, prefixed:? boolean): string
  • data: Content to sign. It can be text content or binary data(string starting with 0x).
  • v: v value of signature. A string starting with 0x.
  • r: r value of signature. A string starting with 0x.
  • s: s value of signature. A string starting with 0x.
  • prefixed: true means no prefix, false means use prefix. Default is false.
  • Return: Signer’s address.

or an not splitted signature

1
web3.eth.accounts.recover(data: A string, signature: string, prefixed:? boolean): string
  • data: Content to sign. It can be text content or binary data(string starting with 0x).
  • signature: Not splitted signature. A string starting with 0x.
  • prefixed: true means no prefix, false means use prefix. Default false
  • Return: Signer’s address.

or object

1
2
3
4
5
6
7
8
web3.eth.accounts.recover(data: SignatureObject, _: string, prefixed:? boolean): string

SignatureObject: {
messageHash: string;
r: string;
s: string;
v: string
}
  • data: Contains signing content and vrs.
  • _: Unused
  • prefixed: true means no prefix, false means use prefix. Default false
  • Return: Signer’s address.

Corresponding to web3.eth.sign

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53';
const v = '0x1b';
const r = '0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a3';
const s = '0x56320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f8';
// 0xDD980c315dFA75682F04381E98ea38BD2A151540
const signer = web3.eth.accounts.recover(message, v, r, s, true);

// or
const signature = '0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a356320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f81b';
const signer = web3.eth.accounts.recover(message, signature, true);

// or
const signatureObj = {
v, r, s,
messageHash: message
};
const signer = web3.eth.accounts.recover(signatureObj, '', true);

Corresponding to web3.eth.accounts.sign and web3.eth.personal.sign

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53';
const v = '0x1c';
const r = '0x03105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f3';
const s = '0x0029ec14347d37d8115cebcf915dc8f6f74c011698d9b7aea15c184f2277197a';
const signer = web3.eth.accounts.recover(message, v, r, s);

// or
const signature = '0x03105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f30029ec14347d37d8115cebcf915dc8f6f74c011698d9b7aea15c184f2277197a1c';
const signer = web3.eth.accounts.recover(message, signature);

// or
const signatureObj = {
v, r, s,
messageHash: message
};
const signer = web3.eth.accounts.recover(signatureObj);

Plain text content

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const message = 'OK!';
const v = '0x1c';
const r = '0x7dff4feac0d0bf31d5b11ef3793bf9d00e7b9028eaaa9737f5079685ff435c9c';
const s = '0x7a22e79b7dab0b0d37524a0f9621d98b76fbc77b6402f87d142746eee61edcb2';
const signer = web3.eth.accounts.recover(message, v, r, s);

// or
const signature = '0x7dff4feac0d0bf31d5b11ef3793bf9d00e7b9028eaaa9737f5079685ff435c9c7a22e79b7dab0b0d37524a0f9621d98b76fbc77b6402f87d142746eee61edcb21c';
const signer = web3.eth.accounts.recover(message, signature);

// or
const signatureObj = {
v, r, s,
messageHash: message
};
const signer = web3.eth.accounts.recover(signatureObj);

web3.eth.personal.ecRecover

This function only supports not splitted signatures and always uses the EIP-191 prefix. Only browser wallet can be used.

1
web3.eth.personal.ecRecover(signedData: string, signature: string): Promise<string>
  • signedData: Content to sign. It can be text content or binary data(string starting with 0x).
  • signature: Not splitted signature. A string starting with 0x.
  • Return value: A Promise object, and the content is the signer’s address.

Corresponding to web3.eth.accounts.sign and web3.eth.personal.sign

1
2
3
4
const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53';
const signature = '0x03105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f30029ec14347d37d8115cebcf915dc8f6f74c011698d9b7aea15c184f2277197a1c';
// 0xdd980c315dfa75682f04381e98ea38bd2a151540
const signer = await web3.eth.personal.ecRecover(message, signature);

Plain text content

1
2
3
4
const message = 'OK!';
const signature = '0x7dff4feac0d0bf31d5b11ef3793bf9d00e7b9028eaaa9737f5079685ff435c9c7a22e79b7dab0b0d37524a0f9621d98b76fbc77b6402f87d142746eee61edcb21c';
// 0xdd980c315dfa75682f04381e98ea38bd2a151540
const signer = await web3.eth.personal.ecRecover(message, signature);

Signature Malleability

Web3.js also has the problem of Signature Malleability, and we can also check the value of s:

1
2
3
if (s > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0n) {
throw new Error('invalid s');
}

Conclusion

  1. web3.eth.sign is depracated and should not be used.
  2. Private key wallet uses web3.eth.accounts.sign to sign.
  3. Browser wallet uses web3.personal.sign to sign.
  4. It is more general to use web3.eth.accounts.recover to recover and verify the signature.

Further Reading

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