ERC191 Link to heading

EIP-191 解决的核心问题:防止签名被 “冒充交易” Link to heading

以太坊中,交易本身是通过 RLP 编码后签名(如转账、调用合约的交易)。如果用户直接对普通消息签名,签名数据可能和交易的 RLP 编码 “撞格式”,导致攻击者将 “消息签名” 伪装成 “交易签名”,盗走资产。

EIP-191 的方案:给所有 “非交易签名”(如登录、权限验证)强制添加统一前缀,让其格式和交易的 RLP 编码完全不同,从根源上避免混淆。

EIP-191 的通用结构 Link to heading

EIP191: 区分交易签名和其他信息签名( [0x19] + [1字节版本] + [版本特定数据] + [待签名数据] )

  • 0x19 初始字节: 这个初始字节确保 signed_data 不是有效的 RLP 编码,从而防止签名数据被误解为以太坊交易。
    • 以太坊交易的 RLP 编码 永远不会以 0x19 开头(RLP 首字节是长度或类型标记,范围多为 0x00~0xbf0xc0~0xff,但 0x19 不在交易 RLP 的合法首字节范围内)。
    • 只要签名数据以 0x19 开头,就能确定 这不是交易,避免被误解为转账请求。
  • <1 byte version >:可以自行定义签名数据版本,占用一字节,可以是0;
    • 用 1 个字节(uint8)定义签名的 “类型”,方便后续解析。
    • 0x00:用于 “带验证者的签名”(如指定只有某个地址能验证该签名);
    • 0x45(即字符 ‘E’ 的十六进制):用于 personal_sign(MetaMask 等钱包的默认签名方式,如 SIWE 登录)
  • < version sepific data >: 不定长的消息头数据; 本特定数据(场景化扩展
    • 根据 版本 字段,填充不同的元数据,用于描述签名的上下文:
    • 若版本是 0x00(带验证者):这里可以是 验证者地址(如 0x123…),表示 “只有该地址能验证此签名”;
    • 若版本是 0x45(personal_sign):这里固定为 “Ethereum Signed Message:\n” + 消息长度(如消息是 “hello”,长度是 5,则这部分是 “Ethereum Signed Message:\n5”)。
  • < data to sign >:原始签名数据; (用户实际要签名的内容)
    • 比如 SIWE 的登录消息、NFT 白名单的验证消息(如 用户地址 + tokenID)。

example EIP191 - PERSON_SIGN 签名 Link to heading

import { hashMessage } from "viem"
import { Hex, keccak256, stringToHex } from 'viem'
import { privateKeyToAccount } from "viem/accounts"

// 创建一个异步主函数来执行我们的代码
async function main() {
    try {
        // 1. 消息哈希方式
        const message = "hello word!";
        
        // 使用默认编码方式(EIP-191)哈希消息
        const messageHash = hashMessage(message);
        console.log("默认哈希结果:", messageHash);

        // 使用EIP191编码方式哈希消息
        const customHash = eip191EncodeAndHash(message);
        console.log("自定义EIP191哈希结果:", customHash);

        // 2. 签名
        // 注意:这里使用了示例私钥,实际使用时请替换为您自己的私钥
        // 警告:永远不要在代码中硬编码真实的私钥
        const privateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; // 这是一个示例私钥
        const account = privateKeyToAccount(privateKey);
        
        // 签名消息
        const signature = await account.signMessage({message});
        console.log("签名结果:", signature);
        
    } catch (error) {
        console.error("发生错误:", error);
    }
}

// EIP191编码和哈希函数
function eip191EncodeAndHash(message: string): Hex {
    const prefix = `\x19Ethereum Signed Message:\n${message.length}`;
    const prefixMessage = prefix + message;
    return keccak256(stringToHex(prefixMessage));
}

// 执行主函数
main().catch(console.error);

默认哈希结果: 0xe16f87fa942cb72e98d5639102334b4f1ea608c9f9d18de65167a239269315c3
自定义EIP191哈希结果: 0xe16f87fa942cb72e98d5639102334b4f1ea608c9f9d18de65167a239269315c3
签名结果: 0x744f8cff3e53be8374b9c3e37234b2db6eaaf63aa501106e72b9007490ba1c4531914eb52a37285376b44381b9d0ba807a6b89b6b98e526b1215686ccd122d0b1b