Using Web3.js to Sign and Recover
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:
- web3.eth.sign
- web3.eth.accounts.sign
- 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]
Turn on and enter I only sign what I understand
Try signing again, for example, run the following program:
1 | const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53'; |
We can see that the signing content is a hash.
Because this method is not recommended, a warning will appear every time you sign.
We can use the library to split the signature.
1 | import { fromRpcSig } from 'ethereumjs-util'; |
Corresponding to the smart contracts, it’s the first example without prefix.
1 | bytes32 hash = 0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53; |
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 | const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53'; |
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 | 0x03105c6f9bcc43236f5ef5e78038506c2551fc11d65701097295094c06c9d4f3 |
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 | bytes32 messageHash = 0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53; |
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 | const message = 'OK!'; |
Solidity
1 | string memory message = "OK!"; |
Note that if the text is UTF-8, the length of the signature on the chain must be bytes. For example:
1 | // 6 |
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 | const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53'; |
A message will pop up as follows:
The result is the same as web3.eth.accounts.sign
, but the format returned is different. Also try text:
1 | const message = 'OK!'; |
A message will pop up as follows:
But if it is data starting with 0x, but the length is not 32 Bytes
1 | // Truncate the last 1 Byte |
The result will be a string of 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 | function splitSignature(signature) { |
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:
- web3.eth.accounts.recover
- 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 | web3.eth.accounts.recover(data: SignatureObject, _: string, prefixed:? boolean): 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 | const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53'; |
Corresponding to web3.eth.accounts.sign and web3.eth.personal.sign
1 | const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53'; |
Plain text content
1 | const message = 'OK!'; |
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 | const message = '0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53'; |
Plain text content
1 | const message = 'OK!'; |
Signature Malleability
Web3.js also has the problem of Signature Malleability, and we can also check the value of s:
1 | if (s > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0n) { |
Conclusion
web3.eth.sign
is depracated and should not be used.- Private key wallet uses
web3.eth.accounts.sign
to sign. - Browser wallet uses
web3.personal.sign
to sign. - 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