跳到主要内容
ecdsa.com
Node.js · WebCrypto

viem

wevm 开发的 viem,wagmi 所用的以太坊 TypeScript 接口:65 字节的 r‖s‖v 签名(v = 27/28)、EIP-191 哈希和 RFC 6979——但与 ethers 不同,它能从 high-S 签名中恢复签名者,却不能读取 EIP-2098 紧凑签名。
许可证
MIT
检查的版本
2.57.7 ·
源代码
github.com/wevm/viem
文档
viem.sh/

兼容性概览

ECDSA以太坊与比特币

ECDSA 曲线
secp256k1
EdDSA
none
公钥格式
sec1-uncompressed

viem 是带类型、支持 tree-shaking 的库,用于以太坊客户端、账户、合约和 ABI 编解码,也是 wagmi 的基础。privateKeyToAccount() 创建的本地账户用 @noble/curves 签名(viem 2.57.7 依赖它的 1.9.1 版本),因此签名是确定性的 RFC 6979 签名,并且是 low-S——对于相同的密钥和消息,与 ethers 生成的签名完全相同。

account.signMessage() 加上 EIP-191 前缀并计算 keccak-256,返回以 v = 27 或 28 结尾的 65 字节;带 hash 参数的 sign() 对 32 字节摘要原样签名,EIP-712 摘要和交易哈希就是这样签名的。本次检查用独立实现核对了这两者。

验证比 ethers 宽松。recoverMessageAddress() 和 verifyMessage 工具函数接受了镜像得到的 high-S 签名——它在数学上有效,但 ethers 会拒绝这种形式,报错 non-canonical s。如果你的系统会对签名去重或存储签名,就要自行规范化 s,否则同一条消息的第二个、看起来不同的签名也会被接受。反过来,64 字节的 EIP-2098 紧凑形式在这两个函数中都报错 invalid signature length;应先用 parseCompactSignature 和 compactSignatureToSignature 将其展开。

独立的 verifyMessage 工具函数只检查外部拥有账户(EOA)。智能合约钱包(Safe、账户抽象钱包、通行密钥钱包)通过 ERC-1271 或 ERC-6492 签名,只有公共客户端(public client)上的 verifyMessage 操作才会向合约查询。可以用 EIP-712 查看器查看类型化数据,用 SIWE 验证工具检查登录消息,用以太坊签名验证工具检查原始签名。

示例对一条登录消息签名,并用两种方式检查:针对地址验证,以及恢复出签名者。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/viem/run.sh 于2026年10月20日在容器镜像 node:24.22.0-bookworm-slim 中运行了 viem 2.57.7(运行时为 Node.js 24.22.0; viem depends on @noble/curves 1.9.1);本页内容必须与它的输出一致,每条事实都列出了对应的输出行。

  • ECDSA 签名编码

    rsv-65

    signMessage() 以十六进制返回 65 字节:r、s 和 v。

    检查记录
    fact ecdsa-signature-encoding = rsv-65
  • 恢复 ID v

    27-28

    v 为 27 或 28;parseSignature() 还会将其表示为 yParity 0 或 1。

    检查记录
    fact recovery-v = 27-28
  • 消息前缀

    eip-191

    hashMessage() 的结果与独立计算的 keccak-256 一致,计算对象是 EIP-191 前缀、十进制长度和消息本身。

    检查记录
    fact message-prefix = eip-191
  • 默认哈希

    keccak256

    消息签名使用 keccak-256。

    检查记录
    fact ecdsa-default-hash = keccak256
  • 签名调用对哪些内容做哈希

    hashes-message-or-digest

    signMessage() 对加上 EIP-191 前缀的消息计算哈希;带 hash 参数的 sign() 对 32 字节摘要原样签名,对该摘要生成了完全相同的签名。

    检查记录
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: sign() over the EIP-191 digest equals signMessage(): true
  • ECDSA 的 nonce

    rfc6979

    RFC 6979:签名与 @noble/curves 对同一摘要生成的确定性签名一致。

    检查记录
    fact ecdsa-nonce = rfc6979
  • 签名时的 low-S

    low-s

    64 个消息签名中没有 high-S 值。

    检查记录
    fact ecdsa-low-s-sign = low-s
  • 验证时的 high-S

    accepts-high-s

    翻转 v 后的镜像签名 (r, n − s) 在 recoverMessageAddress() 中恢复出了签名者,在 verifyMessage 工具函数中也验证通过。

    检查记录
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: recoverMessageAddress with (r, n - s, flipped v): recovers the signer
  • 接受的签名形式

    v-27-28v-0-1

    v = 27/28 或 v = 0/1 的 65 字节签名都能恢复出签名者;64 字节的 EIP-2098 形式报错 invalid signature length。

    检查记录
    fact signature-input-shapes = v-27-28, v-0-1
    evidence signature-input-shapes: v-27-28: recovers the signer
    evidence signature-input-shapes: v-0-1: recovers the signer
    evidence signature-input-shapes: eip-2098: invalid signature length
    evidence signature-input-shapes: verifyMessage utility, high-S twin: true
    evidence signature-input-shapes: verifyMessage utility, eip-2098: invalid signature length
    evidence signature-input-shapes: eip-2098 expanded with parseCompactSignature and compactSignatureToSignature: recovers the signer
    evidence signature-input-shapes: parseSignature returns v 28 and yParity 1
  • ECDSA 曲线

    secp256k1

    仅支持 secp256k1。

    检查记录
    fact ecdsa-curves = secp256k1
  • EdDSA

    none

    不支持 EdDSA。

    检查记录
    fact eddsa = none
  • 后量子算法

    none

    不支持后量子算法。

    检查记录
    fact pqc = none
  • 公钥格式

    sec1-uncompressed

    account.publicKey 是 65 字节的 SEC1 未压缩点。

    检查记录
    fact public-key-formats = sec1-uncompressed
    evidence public-key-formats: account.publicKey is 65 bytes starting with 0x04: true

用法

用本地账户对登录消息签名,然后针对地址验证该签名,并恢复出签名者。
import { recoverMessageAddress, verifyMessage } from "viem";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(generatePrivateKey());
const message = "Sign in to example.com\nNonce: 8f2c1a";

// EIP-191 personal_sign, RFC 6979, low-S, v = 27 or 28.
const signature = await account.signMessage({ message });

// For EOAs this is ecrecover plus an address comparison; for smart-contract
// wallets use the publicClient.verifyMessage action, which also checks ERC-1271.
const valid = await verifyMessage({ address: account.address, message, signature });
const signer = await recoverMessageAddress({ message, signature });

console.log("signature bytes:", (signature.length - 2) / 2);
console.log("valid:", valid, "signer matches:", signer === account.address);

代码片段测试工具会在容器镜像 node:24.22.0-bookworm-slim 中原样运行每个示例,只要输出不匹配,运行就会失败。

环境准备
npm init -y >/dev/null && npm install --no-audit --no-fund viem@2.57.7
命令
node usage.mts
输出必须匹配(正则表达式)
signature bytes: 65\nvalid: true signer matches: true

库