ethers
6.14.3
DOCUMENTATION
Getting Started
Ethereum Basics
Application Programming Interface
Cookbook
Cookbook: ENS Recipes
React Native
Signing
Messages
EIP-712 Typed Data
Migrating from v5
Contributions and Hacking
License and Copyright
Single Page
Documentation »Cookbook »Signing
 Signing

Signing content and providing the content and signature to a Contract allows on-chain validation that a signer has access to the private key of a specific address.

The ecrecover algorithm allows the public key to be determined given some message digest and the signature generated by the private key for that digest. From the public key, the address can then be computed.

How a digest is derived depends on the type of data being signed and a variety of encoding formats are employed. Each format is designed to ensure that they do not collide, so for example, a user cannot be tricked into signing a message which is actually a valid transaction.

For this reason, most APIs in Ethereum do not permit signing a raw digest, and instead require a separate API for each format type and require the related data be specified, protecting the user from accidentally authorizing an action they didn't intend.

 Messages

A signed message can be any data, but it is generally recommended to use human-readable text, as this is easier for a user to verify visually.

This technique could be used, for example, to sign into a service by using the text "I am signing into ethers.org on 2023-06-04 12:57pm". The user can then see the message in MetaMask or on a Ledger Hardware Wallet and accept that they wish to sign the message which the site can then authenticate them with. By providing a timestamp the site can ensure that an older signed message cannot be used again in the future.

The format that is signed uses EIP-191 with the personal sign version code (0x45, or "E").

For those interested in the choice of this prefix, signed messages began as a Bitcoin feature, which used "\x18Bitcoin Signed Message:\n", which was a Bitcoin var-int length-prefixed string (as 0x18 is 24, the length of "Bitcoin Signed Message:\n".). When Ethereum adopted the similar feature, the relevant string was "\x19Ethereum Signed Message:\n".

In one of the most brilliant instances of technical retcon-ing, since 0x19 is invalid as the first byte of a transaction (in Recursive-Length Prefix it indicates a single byte of value 25), the initial byte \x19 has now been adopted as a prefix for some sort of signed data, where the second byte determines how to interpret that data. If the second byte is 69 (the letter "E", as in "Ethereum Signed Message:\n"), then the format is a the above prefixed message format.

So, all existing messages, tools and instances using the signed message format were already EIP-191 compliant, long before the standard existed or was even conceived and allowed for an extensible format for future formats (of which there now a few).

Anyways, the necessary JavaScript and Solidity are provided below.

JavaScript
// The contract below is deployed to Sepolia at this address contractAddress = "0xf554DA5e35b2e40C09DDB481545A395da1736513"; contract = new Contract(contractAddress, [ "function recoverStringFromCompact(string message, (bytes32 r, bytes32 yParityAndS) sig) pure returns (address)", "function recoverStringFromExpanded(string message, (uint8 v, bytes32 r, bytes32 s) sig) pure returns (address)", "function recoverStringFromVRS(string message, uint8 v, bytes32 r, bytes32 s) pure returns (address)", "function recoverStringFromRaw(string message, bytes sig) pure returns (address)", "function recoverHashFromCompact(bytes32 hash, (bytes32 r, bytes32 yParityAndS) sig) pure returns (address)" ], new ethers.InfuraProvider("sepolia")); // The Signer; it does not need to be connected to a Provider to sign signer = new Wallet(id("foobar")); signer.address // '0x0A489345F9E9bc5254E18dd14fA7ECfDB2cE5f21' // Our message message = "Hello World"; // The raw signature; 65 bytes rawSig = await signer.signMessage(message); // '0xa617d0558818c7a479d5063987981b59d6e619332ef52249be8243572ef1086807e381afe644d9bb56b213f6e08374c893db308ac1a5ae2bf8b33bcddcb0f76a1b' // Converting it to a Signature object provides more // flexibility, such as using it as a struct sig = Signature.from(rawSig); // Signature { r: "0xa617d0558818c7a479d5063987981b59d6e619332ef52249be8243572ef10868", s: "0x07e381afe644d9bb56b213f6e08374c893db308ac1a5ae2bf8b33bcddcb0f76a", yParity: 0, networkV: null } // If the signature matches the EIP-2098 format, a Signature // can be passed as the struct value directly, since the // parser will pull out the matching struct keys from sig. await contract.recoverStringFromCompact(message, sig); // '0x0A489345F9E9bc5254E18dd14fA7ECfDB2cE5f21' // Likewise, if the struct keys match an expanded signature // struct, it can also be passed as the struct value directly. await contract.recoverStringFromExpanded(message, sig); // '0x0A489345F9E9bc5254E18dd14fA7ECfDB2cE5f21' // If using an older API which requires the v, r and s be passed // separately, those members are present on the Signature. await contract.recoverStringFromVRS(message, sig.v, sig.r, sig.s); // '0x0A489345F9E9bc5254E18dd14fA7ECfDB2cE5f21' // Or if using an API that expects a raw signature. await contract.recoverStringFromRaw(message, rawSig); // '0x0A489345F9E9bc5254E18dd14fA7ECfDB2cE5f21' // Note: The above recovered addresses matches the signer address

The Solidity Contract has been deployed and verified on the Sepolia testnet at the address 0xf554DA5e35b2e40C09DDB481545A395da1736513.

It provides a variety of examples using various Signature encodings and formats, to recover the address for an EIP-191 signed message.

Solidity
// SPDX-License-Identifier: MIT // For more info, see: https://docs.ethers.org pragma solidity ^0.8.21; // Returns the decimal string representation of value function itoa(uint value) pure returns (string memory) { // Count the length of the decimal string representation uint length = 1; uint v = value; while ((v /= 10) != 0) { length++; } // Allocated enough bytes bytes memory result = new bytes(length); // Place each ASCII string character in the string, // right to left while (true) { length--; // The ASCII value of the modulo 10 value result[length] = bytes1(uint8(0x30 + (value % 10))); value /= 10; if (length == 0) { break; } } return string(result); } contract RecoverMessage { // This is the EIP-2098 compact representation, which reduces gas costs struct SignatureCompact { bytes32 r; bytes32 yParityAndS; } // This is an expanded Signature representation struct SignatureExpanded { uint8 v; bytes32 r; bytes32 s; } // Helper function function _ecrecover(string memory message, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { // Compute the EIP-191 prefixed message bytes memory prefixedMessage = abi.encodePacked( "\x19Ethereum Signed Message:\n", itoa(bytes(message).length), message ); // Compute the message digest bytes32 digest = keccak256(prefixedMessage); // Use the native ecrecover provided by the EVM return ecrecover(digest, v, r, s); } // Recover the address from an EIP-2098 compact Signature, which packs the bit for // v into an unused bit within s, which saves gas overall, costing a little extra // in computation, but saves far more in calldata length. // // This Signature format is 64 bytes in length. function recoverStringFromCompact(string calldata message, SignatureCompact calldata sig) public pure returns (address) { // Decompose the EIP-2098 signature (the struct is 64 bytes in length) uint8 v = 27 + uint8(uint256(sig.yParityAndS) >> 255); bytes32 s = bytes32((uint256(sig.yParityAndS) << 1) >> 1); return _ecrecover(message, v, sig.r, s); } // Recover the address from the expanded Signature struct. // // This Signature format is 96 bytes in length. function recoverStringFromExpanded(string calldata message, SignatureExpanded calldata sig) public pure returns (address) { // The v, r and s are included directly within the struct, which is 96 bytes in length return _ecrecover(message, sig.v, sig.r, sig.s); } // Recover the address from a v, r and s passed directly into the method. // // This Signature format is 96 bytes in length. function recoverStringFromVRS(string calldata message, uint8 v, bytes32 r, bytes32 s) public pure returns (address) { // The v, r and s are included directly within the struct, which is 96 bytes in length return _ecrecover(message, v, r, s); } // Recover the address from a raw signature. The signature is 65 bytes, which when // ABI encoded is 160 bytes long (a pointer, a length and the padded 3 words of data). // // When using raw signatures, some tools return the v as 0 or 1. In this case you must // add 27 to that value as v must be either 27 or 28. // // This Signature format is 65 bytes of data, but when ABI encoded is 160 bytes in length; // a pointer (32 bytes), a length (32 bytes) and the padded 3 words of data (96 bytes). function recoverStringFromRaw(string calldata message, bytes calldata sig) public pure returns (address) { // Sanity check before using assembly require(sig.length == 65, "invalid signature"); // Decompose the raw signature into r, s and v (note the order) uint8 v; bytes32 r; bytes32 s; assembly { r := calldataload(sig.offset) s := calldataload(add(sig.offset, 0x20)) v := calldataload(add(sig.offset, 0x21)) } return _ecrecover(message, v, r, s); } // This is provided as a quick example for those that only need to recover a signature // for a signed hash (highly discouraged; but common), which means we can hardcode the // length in the prefix. This means we can drop the itoa and _ecrecover functions above. function recoverHashFromCompact(bytes32 hash, SignatureCompact calldata sig) public pure returns (address) { bytes memory prefixedMessage = abi.encodePacked( // Notice the length of the message is hard-coded to 32 // here -----------------------v "\x19Ethereum Signed Message:\n32", hash ); bytes32 digest = keccak256(prefixedMessage); // Decompose the EIP-2098 signature uint8 v = 27 + uint8(uint256(sig.yParityAndS) >> 255); bytes32 s = bytes32((uint256(sig.yParityAndS) << 1) >> 1); return ecrecover(digest, v, sig.r, s); } }
 EIP-712 Typed Data

Coming soon...

← React Native
Migrating from v5→
The content of this site is licensed under the Creative Commons License. Generated on December 3, 2024, 8:20pm.