ERC-191 Signed data standard
Final · Signatures & auth · created 2016-01-20
Unifies the prefix of the signed data that a contract verifies with 0x19 and a version byte. This prevents signed data from being disguised as an Ethereum transaction or reused on another wallet.
Why it is needed
Several multisignature wallets accept and execute presigned transactions. A presigned transaction consists of a chunk of data to sign and the signature values r, s, v, but how to interpret this data was not specified. This caused two problems. First, an ordinary Ethereum transaction can be submitted as-is as signed data. Unpacking a transaction yields RLP data that bundles nonce, gasPrice, startGas, to, value and data, along with r, s, v, and if there is no format constraint on signed data, this RLP data passes as a formally valid presigned transaction. Second, a presigned transaction was not tied to a specific validator, that is, a specific wallet. In a situation where A, B and C own a 2/3 wallet X and A, B and D own a 2/3 wallet Y, an attacker can submit to Y, unchanged, the signatures that A and B sent to X.
What to implement
function signatureBasedExecution(address target, uint256 nonce, bytes memory payload, uint8 v, bytes32 r, bytes32 s) public payable: This is the example function from the original. It concatenates 0x19, version 0, its own address, msg.value, nonce and payload, hashes them, recovers the signer with ecrecover, and then runs the wallet logic.
In depth
Basic format of signed data
Signed data (signed_data) is built in the order 0x19 <1 byte version> <version specific data> <data to sign>. First comes the 1 byte 0x19, then a 1 byte version, then data defined per version, and finally the data that is actually signed. The shape of the version-specific data depends on the version byte, and the data to sign can be any content. When verifying a signature, a contract concatenates the bytes in this order, hashes them, and compares the result against the signature.
Why 0x19 was chosen
0x19 was chosen so that signed data cannot be valid RLP. In RLP, a single byte with a value from 0x00 to 0x7f is its own RLP encoding. So signed data that starts with 0x19 cannot be a single RLP structure; it looks like a 1-byte RLP payload followed by something else. As a result, signed data that follows this standard can never be an Ethereum transaction. Also, since the go-ethereum change (ethereum/go-ethereum#2940), personal_sign prepends "\x19Ethereum Signed Message:\n" + len(message) before hashing. This prefix also starts with 0x19, so defining version 0x45 (E) lets such signatures be handled in the same framework.
Registry of version bytes
The original registers three version bytes. 0x00 belongs to this standard (EIP-191) and is used for data that contains the intended validator. 0x01 belongs to EIP-712 and is used for structured data. 0x45 belongs to this standard and is used for personal_sign messages. The shape of the version-specific data that follows the version byte is defined separately for each version. The design allows new uses to be added by changing the version byte after 0x19.
Version 0x00: data with an intended validator
The format is 0x19 <0x00> <intended validator address> <data to sign>. The intended validator address goes in the version-specific data slot. For a multisignature wallet that executes based on a signature it receives, the validator address is the address of that multisignature wallet itself. The data to sign can be any content. Because the wallet address is included in the hash, a signature made for wallet X cannot be used on wallet Y, since the hash does not match.
Versions 0x01 and 0x45: EIP-712 and personal_sign
Version 0x01 is for structured data as defined in EIP-712. This document only registers the version byte and leaves the detailed format to EIP-712. The format of version 0x45 (E) is 0x19 <0x45 (E)> <thereum Signed Message:\n" + len(message)> <data to sign>. In the prefix "\x19Ethereum Signed Message:\n" + len(message) that personal_sign adds, the first letter E is 0x45 in hexadecimal, so E serves as the version byte and the rest, thereum Signed Message:\n + len(message), becomes the version-specific data. The data to sign can be any content.
Example: signature-based execution with version 0x00
The example in the original is written in Solidity 0.8.0. The signatureBasedExecution function takes target, nonce, payload and the signature values v, r, s. The hash used for verification is built as keccak256(abi.encodePacked(byte(0x19), byte(0), address(this), msg.value, nonce, payload)). In order, these are 0x19, version byte 0, address(this) as the validator address, and the application-specific data msg.value, nonce and payload. This hash and v, r, s are passed to ecrecover to recover the signer address. If the recovered address is the owner, the wallet logic proceeds, for example by executing payload on target.