A wallet can sign a transaction, a login request, or an order. All three may use the same private key. The signature alone does not explain which action the user approved. That meaning comes from the bytes being signed and the rules used to verify them.

EIP-191 gives signed messages an envelope. Its first job is to separate a message from a transaction; its versions add context for different uses.

Transaction × message
Send 1 ETH to 0x3535…3535Nonce 9 · chain 1
Transaction
Transaction bytes
0xec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080
Keccak-256
0xdaf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53
Message
EIP-191 prefixTransaction bytes
0x19457468657265756d205369676e6564204d6573736167653a0a3435ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080
Keccak-256
0x45f6c13fc2939e1b5903cba64693ded25aec9553150ba1b1afb1566a2930054a
ECDSA · same keyTransaction verifier

The diagram sends the same transaction bytes through two signing paths. Switch to Raw digest: both paths produce the digest a transaction verifier expects. With personal_sign, the added envelope changes that digest. The message signature no longer verifies as the same sender's authorization for this transaction.

The transaction is the public EIP-155 example: 1 ETH, nonce 9, chain 1. The hashes are computed locally; the diagram does not use a private key or request a signature. Reusing a raw signature also requires encoding the transaction's recovery value correctly.

For the full path from transfer fields to signed bytes, see Transaction ⁠.

Before the signature

For a conventional Ethereum EOA, ECDSA signs a 32-byte digest. An application first chooses an encoding, hashes the resulting bytes with Keccak-256, and asks the key to sign that digest.

Verification follows the same encoding path. Given the digest and signature, a verifier can recover an address and compare it with the expected signer. A successful match binds that key to those bytes. Whether the bytes authorize a login or a transfer is a separate application rule.

The common serialized message signature is 65 bytes: 32 bytes for r, 32 for s, and one recovery byte v. Do not copy transaction-specific v calculations into message verification; legacy transaction chain protection and message recovery are different conventions. Contract accounts can also use verification schemes beyond EOA recovery.

The transaction boundary

Signing an arbitrary digest is dangerous when its preimage is hidden. If that digest is a valid transaction signing hash, the resulting signature may authorize that transaction. This was the problem with unrestricted raw signing exposed by some historical eth_sign implementations. RPC names alone are not a reliable description of every client's hashing behavior.

EIP-191 concatenates four segments: the 0x19 marker, a version byte, version-specific context, and application data. The diagram shows their layout alongside the boundary an RLP decoder recognizes. 0x19 is one byte, not the four text characters 0x19.

Envelope → RLP boundary
Legacy transaction
Listec
44 payload bytesnonce · fees · to · value · data · …
← one complete RLP list →
List · no trailing bytes
EIP-191 message
Marker19
Version · 1 B00 / 01 / 45
Contextversion-specific data
Payloadapplication data
1 byte ↑Trailing bytes
Scalar · not a transaction list

A legacy transaction must decode as one RLP list of transaction fields. In RLP ⁠, list headers start at 0xc0. A byte below 0x80 instead encodes only itself: 0x19 is a valid one-byte RLP value, but it is not a list header.

An EIP-191 envelope always has at least a version byte after 0x19. An RLP decoder consumes the first byte as a complete item; it cannot treat the remaining version, context, and message as children of that item. The whole envelope therefore cannot be the single RLP list required by a legacy transaction. This is the encoding boundary described in EIP-191.

Removing the prefix afterward does not convert the message signature into a transaction signature. The wallet signed the digest of the prefixed bytes. A transaction verifier computes its digest from the transaction encoding, so that signature does not authorize the transaction for the same signer. Both the byte structure and the resulting digest matter.

Modern typed transactions need a separate qualification. EIP-2718 introduces a transaction-type byte before a type-specific payload. An EIP-1559 signing preimage begins with 0x02, whereas this message begins with 0x19. The legacy RLP argument should not be stretched into a proof about every possible future transaction type.

Personal messages

personal_sign uses the EIP-191 envelope with version 0x45. This is the byte for the letter E in Ethereum Signed Message; the remaining context starts with thereum. There is no additional version byte to insert before that E.

Inside personal_sign
Marker0x19
Version0x45= E
Contextthereum Signed
Message:\n2
PayloadHi2 UTF-8 bytes
Preimage0x19457468657265756d205369676e6564204d6573736167653a0a324869
Keccak-256
Digest · 32 B0xeda8711b921b770df62e222b5d66b947fa4518d50f5684bcdbc762df18f74c6b

The complete prefix is \x19Ethereum Signed Message:\n followed by the message length. That length is the ASCII decimal representation of the message's byte count, with no leading zeroes. The prefix and message are then hashed together.

For the UTF-8 text Hi, the ending is \n2Hi. For 你好, the length is 6, although JavaScript's message.length is 2. Count encoded bytes, not JavaScript string units. Editing the message in the diagram changes both the length field and the resulting digest.

The distinction matters for hashes too. A 32-byte digest represented as 0x plus 64 hexadecimal characters is 66 bytes when signed as text. Decode it first if the intended message is the underlying 32 bytes. Both operations are valid; they produce different signatures.

Transaction and message

The Transaction ⁠ article builds a native transfer's complete signing data from its fields. Here the same type-2 bytes, U, pass through both signing paths: directly into Keccak-256 for the transaction, or into a personal_sign envelope as the message.

Transaction × EIP-191
Type021 B
RLP list headerf048 B payload
chainId011 B
nonce091 B
maxPriorityFeePerGas84773594005 B
maxFeePerGas8506fc23ac006 B
gasLimit8252083 B
to94353535353535353535353535353535353535353521 B
value880de0b6b3a76400001 ETH
data800x · empty
accessListc0[]
U · 50 bytes · signing preimage0x02f0010984773594008506fc23ac00825208943535353535353535353535353535353535353535880de0b6b3a764000080c0
Transaction · type 2
U
Keccak-256
0xfae77debb64203fbaea6213fcde74f1b138c6854c3d7b44ba1c2ced52c2d8c4d
EIP-191 · personal_sign
\x19Ethereum Signed Message:\n50U
Keccak-256
0x5445b54d697f9f341870b441a79095845ed5d28ff1a8e6fc17f6865b813812f8
ECDSAtransaction signature ≠ message signature
EIP-191 preimage · prefix + ASCII length + U0x19457468657265756d205369676e6564204d6573736167653a0a353002f0010984773594008506fc23ac00825208943535353535353535353535353535353535353535880de0b6b3a764000080c0

In the default example, U is 50 bytes. The message path prepends \x19Ethereum Signed Message:\n, followed by ASCII 50 (hex 3530), then the exact U bytes. Its length counts decoded bytes, not the characters of the hexadecimal representation. Both paths end in a 32-byte digest, but the different preimages produce different digests. The message signature is therefore not the transaction authorization expected for the same account.

This diagram uses EIP-1559 type 2, whose signing data begins with 0x02. The opening diagram uses legacy EIP-155, whose signing data is an RLP list without that type byte. In both cases, personal_sign adds its envelope around the complete transaction preimage rather than signing the transaction digest directly.

Three versions

The version byte tells a verifier how to interpret the envelope.

VersionContextPayload
0x00Intended validator addressApplication-defined bytes
0x01EIP-712 domain separatorStructured message hash
0x45Personal-message prefix and lengthMessage bytes

Version 0x00 binds signed data to a particular validator, such as a multisig wallet. It prevents a second wallet with overlapping owners from interpreting the same authorization as its own. The application must still include and enforce any needed nonce, chain binding, target, amount, and deadline.

Version 0x01 is the envelope used by EIP-712 ⁠.

Sign and recover

This example uses ethers 6.17.0 in Node.js. It creates a disposable key locally and makes no network request.

JS
import assert from 'node:assert/strict';
import { Wallet, getBytes, hashMessage, verifyMessage } from 'ethers';
 
const wallet = Wallet.createRandom();
const text = '0x4243';
const bytes = getBytes(text);
 
const signature = await wallet.signMessage(bytes);
assert.equal(verifyMessage(bytes, signature), wallet.address);
assert.notEqual(hashMessage(text), hashMessage(bytes));
assert.notEqual(verifyMessage(text, signature), wallet.address);
 
console.log({
  textDigest: hashMessage(text),
  bytesDigest: hashMessage(bytes),
  signer: verifyMessage(bytes, signature),
});

signMessage adds the personal-message envelope internally. Do not add the prefix yourself and pass the result to signMessage; that signs a second envelope around the first.

verifyMessage returns a recovered address. It does not know which account your application expected. That comparison belongs to the verifier.

What remains unsigned

The personal-message prefix contains no website, chain, expiry, or nonce. If an application needs those boundaries, it must place them in the message and check them during verification.

A contract can accept an EIP-191 signature as an order or spending authorization. Signing costs no transaction gas, but someone else can later submit that signature to a contract. A readable message helps inspection; a prefixed hash can still hide the action being authorized.

For typed application data, continue with EIP-712 ⁠. For authentication, EIP-4361 ⁠ defines the message fields and verification rules around a wallet login.

References