EIP-191 vs EIP-712 é a escolha entre os dois formatos de assinatura do Ethereum para dados que não são uma transação. A EIP-191 (2016) define um prefixo 0x19 que nunca pode iniciar a codificação de uma transação válida, seguido de um byte de versão que seleciona o formato; a versão 0x45 é o personal_sign, usado para texto legível por pessoas. A EIP-712 (2017) é a versão 0x01: ela calcula o hash de dados estruturados e tipados junto com um separador de domínio que identifica a aplicação, a cadeia e o contrato verificador. A EIP-712 é, portanto, um formato específico da EIP-191, e não uma concorrente do padrão inteiro.
Visão geral
EIP-191 personal_sign (versão 0x45) | Dados tipados EIP-712 (versão 0x01) | |
|---|---|---|
| Status | Final, criada em 20/01/2016 | Final, criada em 12/09/2017 |
| Método RPC da carteira | personal_sign | eth_signTypedData_v4 |
| Bytes que entram no hash Keccak-256 | 0x19 "Ethereum Signed Message:\n" comprimento mensagem | 0x19 0x01 domainSeparator hashStruct(message) |
| O que a carteira mostra | O texto, ou hexadecimal bruto se a mensagem for binária | Nomes e valores dos campos, além do domínio (nome, cadeia, contrato) |
| Proteção contra repetição | Nenhuma embutida; o texto da mensagem precisa trazer nonce, domínio e cadeia | Separador de domínio com name, version, chainId, verifyingContract, salt |
| Verificação no contrato | Reconstruir a mensagem com prefixo e depois ecrecover | Recalcular os hashes de tipo e os hashes de struct e depois ecrecover |
| Usos típicos | Sign-In with Ethereum (EIP-4361), prova de posse de endereço | Permit (EIP-2612), Permit2, transações Safe, ordens de exchanges, votos de governança |
| Assinatura | ECDSA secp256k1 de 65 bytes: r, s, v | A mesma |
| Principais armadilhas | Hashes em hexadecimal assinados às cegas; texto livre ambíguo | Aprovações de aparência inofensiva; dispositivos que mostram só um hash |
O que entra no hash de cada uma
A versão 0x45 da EIP-191 antepõe "\x19Ethereum Signed Message:\n" e o comprimento da mensagem em ASCII decimal e então calcula o hash com Keccak-256. O prefixo garante que os bytes assinados não podem ser uma transação codificada em RLP, de modo que um site não consegue enganar uma carteira para que assine uma transferência disfarçada de mensagem. Ele não faz mais nada: se a assinatura significa “entrar em example.com” ou “aprovar gastos ilimitados” depende inteiramente de como o receptor interpreta o texto.
A EIP-712 codifica cada struct como o hash da sua string de tipo seguido de palavras de 32 bytes para os seus campos: strings e bytes passam pelo hash, endereços e inteiros recebem padding à esquerda, e structs aninhadas são substituídas pelos seus próprios hashes. O domínio é codificado da mesma forma, como uma struct EIP712Domain. O digest final é keccak256(0x19 0x01 domainSeparator hashStruct(message)).
O script abaixo calcula os dois com a @noble/curves 2.3.0 e a @noble/hashes 2.3.0 no Node.js 26, usando a mensagem “Ether Mail” e a chave de assinatura do Example.js de referência da EIP-712. Essa chave, keccak256("cow"), é publicada na especificação como vetor de teste: qualquer pessoa pode derivá-la, então o endereço dela nunca deve guardar fundos.
import { secp256k1 } from "@noble/curves/secp256k1.js";
import { keccak_256 } from "@noble/hashes/sha3.js";
import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from "@noble/hashes/utils.js";
const hex = (b) => "0x" + bytesToHex(b);
// Test key published in the EIP-712 reference example (assets/eip-712/Example.js): keccak256("cow").
// It is public, so never send funds to its address.
const secretKey = keccak_256(utf8ToBytes("cow"));
const signer = hex(keccak_256(secp256k1.getPublicKey(secretKey, false).subarray(1)).subarray(12));
const sign = (digest) => {
const sig = secp256k1.sign(digest, secretKey, { prehash: false, format: "recovered" });
return { v: sig[0] + 27, r: hex(sig.subarray(1, 33)), s: hex(sig.subarray(33)) };
};
// EIP-191 version 0x45: personal_sign
const text = utf8ToBytes("hello");
const personalDigest = keccak_256(concatBytes(utf8ToBytes(`\x19Ethereum Signed Message:\n${text.length}`), text));
console.log("EIP-191 personal_sign digest:", hex(personalDigest));
// EIP-712: typed structured data (the "Ether Mail" example from the EIP)
const types = {
EIP712Domain: [["name", "string"], ["version", "string"], ["chainId", "uint256"], ["verifyingContract", "address"]],
Person: [["name", "string"], ["wallet", "address"]],
Mail: [["from", "Person"], ["to", "Person"], ["contents", "string"]],
};
const encodeType = (primary) => {
const deps = new Set();
const walk = (t) => types[t].forEach(([, ft]) => { if (types[ft] && !deps.has(ft)) { deps.add(ft); walk(ft); } });
walk(primary);
return [primary, ...[...deps].sort()].map((t) => `${t}(${types[t].map(([n, ft]) => `${ft} ${n}`).join(",")})`).join("");
};
const encodeValue = (ft, v) =>
types[ft] ? hashStruct(ft, v)
: ft === "string" ? keccak_256(utf8ToBytes(v))
: ft === "address" ? concatBytes(new Uint8Array(12), hexToBytes(v.slice(2)))
: hexToBytes(BigInt(v).toString(16).padStart(64, "0")); // uint256
const hashStruct = (t, v) =>
keccak_256(concatBytes(keccak_256(utf8ToBytes(encodeType(t))), ...types[t].map(([n, ft]) => encodeValue(ft, v[n]))));
const domain = { name: "Ether Mail", version: "1", chainId: 1, verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" };
const mail = {
from: { name: "Cow", wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" },
to: { name: "Bob", wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" },
contents: "Hello, Bob!",
};
const typedDigest = (d) => keccak_256(concatBytes(new Uint8Array([0x19, 0x01]), hashStruct("EIP712Domain", d), hashStruct("Mail", mail)));
console.log("encodeType(Mail):", encodeType("Mail"));
console.log("domainSeparator:", hex(hashStruct("EIP712Domain", domain)));
console.log("hashStruct(Mail):", hex(hashStruct("Mail", mail)));
console.log("EIP-712 digest:", hex(typedDigest(domain)));
console.log("signer:", signer, sign(typedDigest(domain)));
console.log("same mail, chainId 10:", hex(typedDigest({ ...domain, chainId: 10 })));
EIP-191 personal_sign digest: 0x50b2c43fd39106bafbba0da34fc430e1f91e3c96ea2acee2bc34119f92b37750
encodeType(Mail): Mail(Person from,Person to,string contents)Person(string name,address wallet)
domainSeparator: 0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f
hashStruct(Mail): 0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e
EIP-712 digest: 0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2
signer: 0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826 {
v: 28,
r: '0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d',
s: '0x07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b91562'
}
same mail, chainId 10: 0x2752db86097e9749484b4645855d9ff3d1b6109c89ed55a9d7f9021dbc77bcf9
A string de tipo, o separador de domínio, o hash da struct, o digest, o endereço do signatário e v, r, s são exatamente os valores que as asserções do Example.js de referência da EIP-712 esperam, e a assinatura é determinística (RFC 6979), então você pode reproduzi-la. Mudar apenas o chainId produz um digest diferente, e é isso que impede a repetição da assinatura em outra cadeia. O inspetor de EIP-712 mostra os mesmos hashes intermediários para qualquer payload de dados tipados.
O que os usuários realmente veem
A principal vantagem da EIP-712 é a exibição: uma carteira pode mostrar “spender”, “value” e “deadline” em vez de uma string opaca. Isso só ajuda se o dispositivo conseguir exibir os campos e se o usuário os ler. Hoje, a maior parte do phishing que esvazia fundos pede assinaturas EIP-712 totalmente válidas, como um permit (EIP-2612) ou uma aprovação Permit2 que concede a um spender tokens ilimitados, sem transação e sem gás, então o usuário não vê nenhum aviso on-chain. Em fevereiro de 2025, três signatários da Bybit aprovaram uma transação Safe maliciosa em carteiras de hardware que só conseguiam mostrar o hash dela; a transação substituiu a implementação da carteira, e cerca de US$ 1,5 bilhão em ETH foi desviado. Recalcular de forma independente o hash de uma transação Safe, como faz a ferramenta de hash de transações Safe, é a maneira de os signatários conferirem o que um dispositivo mostra em hexadecimal.
O personal_sign tem a fraqueza inversa: as carteiras mostram o texto fielmente, mas um site pode pedir aos usuários que assinem uma string hexadecimal de 32 bytes, que não revela nada. A assinatura de hashes brutos sem nenhum prefixo (eth_sign) foi desativada por padrão no MetaMask em 2023 e removida em 2024 por esse motivo.
Verificação das assinaturas
Em contas de propriedade externa (EOAs), os dois formatos são verificados recalculando o digest e recuperando o signatário com ecrecover (ou o equivalente de uma biblioteca) e, em seguida, comparando o endereço; rejeite assinaturas high-s para evitar a maleabilidade. Carteiras de contrato inteligente (Safe, contas ERC-4337) não conseguem produzir assinaturas ECDSA próprias, então os verificadores precisam chamar isValidSignature(hash, signature) da ERC-1271 e aceitar o valor mágico 0x1626ba7e; a ERC-6492 estende isso a carteiras que ainda não foram implantadas. O verificador de assinaturas Ethereum e o verificador de SIWE tratam os dois casos.
Quando escolher cada uma
- Login e prova de posse de endereço: EIP-191 com uma mensagem EIP-4361 (SIWE) que inclua domínio, URI, ID da cadeia, nonce e expiração.
- Aprovações de tokens sem transação: permit EIP-712 ou Permit2, com prazos curtos.
- Ordens, votos, metatransações, atestados: EIP-712 com um domínio que inclua
chainIdeverifyingContract. - Transações multisig: EIP-712, como o Safe usa, mais uma verificação independente do hash em um segundo dispositivo.
- Texto livre que uma pessoa deve ler: EIP-191.
- Nunca:
personal_signsobre um hash, nem domínios EIP-712 sem ID da cadeia.
Notas de migração
- Ao migrar uma autorização off-chain do
personal_signpara a EIP-712: defina a struct, inclua campos de nonce e de prazo, e continue aceitando o formato antigo só até as assinaturas pendentes expirarem. - Versione o domínio: incremente
versionquando a semântica dos campos mudar, para que assinaturas antigas não possam ser interpretadas sob regras novas. - Verifique com a mesma biblioteca com que você assina e teste com os vetores de referência da EIP-712 mostrados acima.
- Ofereça suporte a carteiras de contrato por meio da ERC-1271 e da ERC-6492, em vez de presumir
ecrecover. - Revogue e faça expirar: dê aos usuários uma forma de revogar permits e use prazos curtos; assinaturas sem expiração continuam utilizáveis indefinidamente.