Skip to content

Signing Messages

A wallet can sign an arbitrary message without sending any transaction at all, no gas, no blockchain interaction, just a cryptographic proof that the holder of a specific private key endorsed a specific piece of data. This chapter covers why that's useful, and the specific format (EIP-191) that makes a plain-message signature distinguishable from a transaction signature.

Signing and verifying a plain message

import { verifyMessage } from "viem";

// Signing (from a connected wallet):
const signature = await walletClient.signMessage({
  account: userAddress,
  message: "Sign in to Example App",
});

// Verifying (can be done by anyone, without any wallet connection at all):
const isValid = await verifyMessage({
  address: userAddress,
  message: "Sign in to Example App",
  signature,
});

console.log("signature valid:", isValid);

Why sign a message at all, if it does nothing on-chain

The most common use case is authentication without a password: an application asks a user to sign a specific, often randomly generated message ("Sign in to Example App, nonce: 8f3a2b..."), and treats a valid signature as proof the user controls the claimed address, the same digital signature authentication property covered generally in Cryptography, applied here to prove identity rather than to authorize a fund transfer. This is genuinely useful precisely because it costs nothing and requires no blockchain interaction: no gas, no waiting for confirmation, just an instant cryptographic proof.

EIP-191: why signed messages start with a specific prefix

A raw ECDSA signature (see ECDSA) can't tell, from the signature alone, what kind of data was actually signed. This creates a real, documented danger: if plain message signing used the identical signing process as transaction signing, a malicious site could trick a user into "signing a message" that a wallet or a different tool might actually be able to reinterpret and rebroadcast as a valid, authorized transaction. EIP-191 closes this gap by requiring every plain message signature to be computed over the message prefixed with "\x19Ethereum Signed Message:\n" + message.length, before hashing and signing, a prefix specifically chosen because it can never collide with the start of a validly RLP-encoded transaction, guaranteeing a message signature can never be misinterpreted as authorizing a real transaction.

Common misconceptions

Signing a message is not risk-free just because it costs no gas (a malicious dapp can still present a disguised message for signing whose content, once decoded, actually authorizes something harmful (particularly relevant for the structured, more expressive signatures covered next in Typed Data and EIP-712)) the "no gas" property means no transaction fee is charged, not that the action being authorized is automatically safe.

A valid signature over a message does not, by itself, prove anything about the truthfulness of the message's content. It only proves the holder of a specific private key endorsed that exact text; if an application asks a user to sign "I am 18 years or older," a valid signature proves the user's wallet endorsed that statement, not that it's factually true.

Further reading


← Previous: Sending Transactions · Back to Building Web3 Applications · Next: Typed Data and EIP-712 →