EIPs
EIP-191 — Signed Data Standard
EIP-191 defines how arbitrary data should be hashed before signing so that signatures can never be reused as transactions. The common variant is 0x45 (personal_sign): prefix "\x19Ethereum Signed Message:\n" || len(message) || message, then keccak.
import { parse } from "valibot";
import { AddressSchema } from "@ethernauta/core";
import { personal_sign } from "@ethernauta/eip/191";
import type { Provider } from "@ethernauta/eip/1193";
import { create_provider, encode_chain_id } from "@ethernauta/transport";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
declare const provider: Provider; // see /eips/6963 for discovery
const { signer } = create_provider(provider);
const address = parse(AddressSchema, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
// path 1 — through a wallet
const signature = await personal_sign(["Hello, world", address])(
signer({ chain_id: CHAIN_ID }),
);
void signature; Surface
| Export | Type | Purpose |
|---|---|---|
personal_sign | Signable<Bytes65> | RPC binding for personal_sign. |
build_personal_message | (message: string) => Uint8Array | Compose the \x19... byte sequence. |
build_personal_message_hex | (message: string) => Bytes | Hex version of the above. |
Verifying
The signature can be verified off-chain via ECDSA recovery, or on-chain via EIP-1271:
import { parse } from "valibot";
import { AddressSchema, BytesSchema } from "@ethernauta/core";
import { verify_message } from "@ethernauta/crypto";
import { create_reader, encode_chain_id, http } from "@ethernauta/transport";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
const reader = create_reader([
{ chainId: CHAIN_ID, transports: [http("https://ethereum-rpc.publicnode.com")] },
]);
const address = parse(AddressSchema, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
const signature = parse(BytesSchema, "0x");
const ok = await verify_message({
address,
message: "Hello, world",
signature,
})(reader({ chain_id: CHAIN_ID }));
void ok; verify_message tries EOA recovery first; if the address has code, it falls through to EIP-1271. verify_message_universal adds EIP-6492 for counterfactual accounts.
Why prefix the message
The prefix’s job is to make the resulting digest unmistakable for a transaction digest. Without it, a signed personal message could potentially be replayed as part of a transaction the user never intended.
The literal prefix bytes are 0x19 || 0x45 || "thereum Signed Message:\n" || len(message). The leading 0x19 byte is the EIP-191 identifier; 0x45 is the variant for personal_sign. EIP-712 uses the same 0x19 lead with a different variant.
See also
- EIP-712 — typed-data signing (the structured cousin).
- EIP-1271 — smart-contract signature validation.
- EIP-6492 — counterfactual signature wrapping.
- @ethernauta/crypto —
verify_message.