Перейти к содержимому
ecdsa.com
Node.js · WebCrypto

viem

viem от wevm — интерфейс TypeScript к Ethereum, на котором работает wagmi: 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/

Совместимость вкратце

ECDSAEthereum и Bitcoin

EdDSA
none

viem — типизированная библиотека с поддержкой tree shaking для клиентов Ethereum, аккаунтов, контрактов и кодирования ABI, на которой построен wagmi. Локальные аккаунты из privateKeyToAccount() подписывают с помощью @noble/curves (viem 2.57.7 зависит от её версии 1.9.1), поэтому их подписи детерминированы по RFC 6979 и имеют low-S, то есть для того же ключа и сообщения совпадают с тем, что выдаёт ethers.

account.signMessage() добавляет префикс EIP-191, хеширует keccak-256 и возвращает 65 байт, заканчивающиеся значением v = 27 или 28; sign() с параметром hash подписывает 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, и к контракту обращается только действие verifyMessage публичного клиента. Разбирайте типизированные данные в инспекторе EIP-712, сообщения для входа — в средстве проверки SIWE, а необработанные подписи — в проверке подписей Ethereum.

Пример подписывает сообщение для входа и проверяет его двумя способами: проверкой по адресу и восстановлением подписанта.

Факты и проверки, на которых они основаны

Ничего из приведённого ниже не скопировано из документации. 20 октября 2026 г. проверка content/libraries/verify/viem/run.sh запустила viem 2.57.7 в среде Node.js 24.22.0; viem depends on @noble/curves 1.9.1 в образе контейнера node:24.22.0-bookworm-slim; содержимое страницы должно совпадать с выводом проверки, а у каждого факта приведены соответствующие строки вывода.

  • Кодировка подписи ECDSA

    rsv-65

    signMessage() возвращает 65 байт в hex: r, s и v.

    Что записала проверка
    fact ecdsa-signature-encoding = rsv-65
  • Идентификатор восстановления 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; sign() с параметром hash подписывает 32-байтовый дайджест без изменений и для этого дайджеста выдал идентичную подпись.

    Что записала проверка
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: sign() over the EIP-191 digest equals signMessage(): true
  • Nonce в ECDSA

    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

    Зеркальная подпись (r, n − s) с инвертированным v: 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

    По 65-байтовым подписям с v = 27/28 или v = 0/1 подписант восстановлен; 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

Библиотеки