Skip to content
ecdsa.com
Node.js · WebCrypto

viem

wevm's viem, the TypeScript interface to Ethereum used by wagmi: 65-byte r‖s‖v signatures with v = 27/28, EIP-191 hashing and RFC 6979 — but, unlike ethers, it recovers high-S signatures and does not read EIP-2098 compact ones.
License
MIT
Checked version
2.57.7 ·
Source
github.com/wevm/viem
Documentation
viem.sh/

Compatibility at a glance

ECDSAEthereum and Bitcoin

ECDSA curves
secp256k1
EdDSA
none
Public key formats
sec1-uncompressed

viem is a typed, tree-shakeable library for Ethereum clients, accounts, contracts and ABI coding, and the foundation of wagmi. Local accounts from privateKeyToAccount() sign with @noble/curves (viem 2.57.7 depends on 1.9.1 of it), so their signatures are deterministic RFC 6979 and low-S — identical, for the same key and message, to what ethers produces.

account.signMessage() applies the EIP-191 prefix and keccak-256 and returns 65 bytes ending in v = 27 or 28; sign() with a hash parameter signs a 32-byte digest unchanged, which is how EIP-712 digests and transaction hashes are signed. The check confirmed both against independent implementations.

Verification is more permissive than in ethers. recoverMessageAddress() and the verifyMessage utility accepted the mirrored high-S signature — mathematically valid, but a form ethers rejects with non-canonical s. If your system deduplicates or stores signatures, normalize s yourself, or a second, different-looking signature for the same message will be accepted. In the other direction, the 64-byte EIP-2098 compact form failed with invalid signature length in both functions; expand it with parseCompactSignature and compactSignatureToSignature first.

The standalone verifyMessage utility only checks externally owned accounts. Smart-contract wallets (Safe, account-abstraction wallets, passkey wallets) sign through ERC-1271 or ERC-6492, and only the verifyMessage action on a public client asks the contract. Inspect typed data with the EIP-712 inspector, sign-in messages with the SIWE verifier, and raw signatures with the Ethereum signature verifier.

The sample signs a sign-in message and checks it two ways: by verification against the address and by recovering the signer.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/viem/run.sh ran viem 2.57.7 on Node.js 24.22.0; viem depends on @noble/curves 1.9.1 in the container image node:24.22.0-bookworm-slim on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • ECDSA signature encoding

    rsv-65

    signMessage() returns 65 bytes as hex: r, s and v.

    Recorded by the check
    fact ecdsa-signature-encoding = rsv-65
  • Recovery id v

    27-28

    v is 27 or 28; parseSignature() also reports it as yParity 0 or 1.

    Recorded by the check
    fact recovery-v = 27-28
  • Message prefix

    eip-191

    hashMessage() equals keccak-256 of the EIP-191 prefix, the decimal length and the message, computed independently.

    Recorded by the check
    fact message-prefix = eip-191
  • Default hash

    keccak256

    Message signatures use keccak-256.

    Recorded by the check
    fact ecdsa-default-hash = keccak256
  • What the signing call hashes

    hashes-message-or-digest

    signMessage() hashes with the EIP-191 prefix; sign() with a hash parameter signs a 32-byte digest unchanged and produced the identical signature for that digest.

    Recorded by the check
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: sign() over the EIP-191 digest equals signMessage(): true
  • ECDSA nonce

    rfc6979

    RFC 6979: the signature matched @noble/curves' deterministic signature over the same digest.

    Recorded by the check
    fact ecdsa-nonce = rfc6979
  • Low-S when signing

    low-s

    No high-S value among 64 message signatures.

    Recorded by the check
    fact ecdsa-low-s-sign = low-s
  • High-S when verifying

    accepts-high-s

    The mirrored signature (r, n − s) with v flipped recovered the signer in recoverMessageAddress() and verified in the verifyMessage utility.

    Recorded by the check
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: recoverMessageAddress with (r, n - s, flipped v): recovers the signer
  • Signature shapes accepted

    v-27-28v-0-1

    65-byte signatures with v = 27/28 or v = 0/1 recovered the signer; the 64-byte EIP-2098 form failed with invalid signature length.

    Recorded by the check
    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 curves

    secp256k1

    secp256k1 only.

    Recorded by the check
    fact ecdsa-curves = secp256k1
  • EdDSA

    none

    No EdDSA.

    Recorded by the check
    fact eddsa = none
  • Post-quantum algorithms

    none

    No post-quantum algorithms.

    Recorded by the check
    fact pqc = none
  • Public key formats

    sec1-uncompressed

    account.publicKey is the 65-byte uncompressed SEC1 point.

    Recorded by the check
    fact public-key-formats = sec1-uncompressed
    evidence public-key-formats: account.publicKey is 65 bytes starting with 0x04: true

Usage

Sign a sign-in message with a local account, then verify it against the address and recover the signer.
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);

The snippet harness runs every sample verbatim in the container image node:24.22.0-bookworm-slim, and the run fails unless the output matches.

Setup
npm init -y >/dev/null && npm install --no-audit --no-fund viem@2.57.7
Command
node usage.mts
Output must match (regular expression)
signature bytes: 65\nvalid: true signer matches: true

Libraries