Repository navigation
fix(utils): use UTF-8 byte length for the EIP-191 prefix in hashEthereumMessage - #7328
Conversation
|
Claude finished @ganchoradkov's task in 1m 48s —— View job Code Review
The fix is correct and well-tested. One finding from the breaking-changes analysis: Found 1 issue(s)Issue 1: Changeset version level may be too low for an observable public-API output changeID: brk-signatures-hashEthereumMessage-bytelength-change-c3f2 Context:
Recommendation: Consider bumping the changeset to "@walletconnect/utils": minorImplementation ( Tests: Good use of independent oracles ( |
There was a problem hiding this comment.
Pull request overview
Fixes @walletconnect/utils EIP-191 hashing for non-ASCII messages by switching the prefix length from JavaScript UTF-16 code units to UTF-8 byte length, ensuring signature verification matches standards-compliant signers and restoring correct wc_sessionAuthenticate behavior for SIWE/CACAO messages containing Unicode.
Changes:
- Update
hashEthereumMessageto prefix the UTF-8 byte length and hashprefixBytes || messageBytes. - Add regression tests (with independent hashing/signing oracles via
ox, plus a hardcoded ethers v6 vector) covering non-ASCII hashing and both EIP-191/EIP-1271 verification paths. - Add a changeset documenting the behavioral fix and compatibility notes.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| packages/utils/src/signatures.ts | Fix EIP-191 prefix length calculation by using UTF-8 byte length and hashing concatenated byte arrays. |
| packages/utils/test/signatures.spec.ts | Add focused tests validating hashEthereumMessage against independent oracles and ensuring non-ASCII signatures verify correctly. |
| packages/utils/test/cacao.spec.ts | Add end-to-end CACAO validation regression test for a non-ASCII statement under eip191. |
| .changeset/eip191-utf8-byte-length.md | Document the patch-level behavior change and why it’s a correctness fix with ASCII unchanged. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| @@ -0,0 +1,5 @@ | |||
| --- | |||
| "@walletconnect/utils": patch | |||
There was a problem hiding this comment.
🤖 Auto Review Issue: Changeset version level may be too low for an observable public-API output change
Severity: MEDIUM
Category: breaking_change
Tool: Claude Auto Review
Context:
- Pattern:
hashEthereumMessageis a public export of@walletconnect/utils. Its return value changes for any non-ASCII input. The changeset marks this aspatch. - Risk: Any consumer that calls
hashEthereumMessagedirectly and stores, logs, or compares the resulting hash (e.g. a cached "already verified" state) will observe a different value after upgrading. Cross-version comparisons diverge. - Impact: Narrow in practice — the old hash was non-EIP-191-compliant and no wallet would ever produce a signature over it — but the output of a named public function changes, which is conventionally a
minorbump. - Trigger: Consumer stores an EIP-191 hash computed pre-upgrade for a non-ASCII message and then compares it against a freshly-computed one post-upgrade.
Recommendation: Consider bumping the changeset to minor and adding an explicit migration note that persisted hashes for non-ASCII messages must be recomputed. The new output matches ethers.hashMessage / ox PersonalMessage.getSignPayload — citing this gives consumers a clear cross-reference.
"@walletconnect/utils": minor…eumMessage `hashEthereumMessage` prefixed `message.length` — UTF-16 code units — but EIP-191 (version 0x45) requires the length of the UTF-8 encoded message. The two differ for any non-ASCII character, so a SIWE/CACAO message with a localized or emoji statement, or a Unicode domain, hashed to a value that no compliant signer (ethers, viem/ox, MetaMask, geth, Safe…) produces, and both `eip191` and `eip1271` verification deterministically failed for every wallet type. Hashes of ASCII messages are unchanged. This restores the behaviour the SDK had before 45189aa (2.18.0), when it delegated to ethers' `hashMessage`. Tests use `ox` and a hardcoded ethers v6 vector as independent oracles and cover eip191 recovery, eip1271 calldata, and an end-to-end `validateSignedCacao` with a non-ASCII statement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
b02aa41 to
c247899
Compare
Description
hashEthereumMessagebuilt the EIP-191 prefix withmessage.length, which counts UTF-16 code units. EIP-191 (version0x45) requires the length of the UTF-8 encoded message. The two are equal for ASCII but differ for any non-ASCII character, so a SIWE/CACAO message with a localized or emoji statement (Sign in to Café ☕,登录到应用,Let's go 🚀) or a Unicode domain hashed to a value that no compliant signer produces (ethers, viem/ox, MetaMasketh-sig-util, web3.js, geth, Safe all hash over bytes). The result was a deterministicfalsefrom bothisValidEip191Signatureand the non-pre-hashed path ofisValidEip1271Signature— i.e.wc_sessionAuthenticatefailed for every wallet type against such dApps, on both the wallet and dApp side.sequenceDiagram participant W as Wallet (compliant signer) participant S as @walletconnect/utils Note over W: bytes = utf8(message) W->>W: sign keccak("\x19Ethereum Signed Message:\n" + bytes.length + bytes) W->>S: cacao { s: signature } rect rgb(255, 235, 235) Note over S: before: prefix uses message.length (UTF-16 units)<br/>→ different digest for any non-ASCII char → recovered address ≠ signer → false end rect rgb(235, 255, 235) Note over S: after: prefix uses utf8(message).length<br/>→ identical digest → verification succeeds endCompatibility
hashMessage, and matches the other WalletConnect SDKs.hashEthereumMessageis a public export; consumers calling it directly now get the standard EIP-191 hash for non-ASCII input (noted in the changeset).Independent of #7327 (different region of the same file); verified with
git merge-treethat the two branches merge cleanly in either order.Type of change
How has this been tested?
New tests use independent oracles (
ox, already a@walletconnect/utilsdependency, and a hardcoded ethers v6.17 vector) rather than sharing the implementation:signatures.spec.ts›hashEthereumMessageoxPersonalMessage.getSignPayloadfor ASCII, Latin-1, CJK, emoji (surrogate pair) and an IDN-domain SIWE header;hashMessagevector for"Sign in to Café ☕ — 登录 🚀"(25 code units / 36 bytes);isValidEip191Signatureaccepts a signature over a non-ASCII message produced by a compliant signer (and rejects a tampered message);isValidEip1271Signature(fetch-stubbed) embeds the byte-length digest in theeth_callcalldata.cacao.spec.ts: end-to-endvalidateSignedCacaowith a non-ASCIIstatementand aneip191signature overformatMessage()output.packages/utilssignatures.spec.ts+cacao.spec.tswith a realTEST_PROJECT_ID: 45/45 pass, including the pre-existing ASCII vectors (unchanged hashes).prettier --check,tsc --noEmit -p packages/utils, eslint on touched files: clean, no new findings.Examples/Screenshots (Optional)
Signature from a compliant signer (
ox) over each message, verified withisValidEip191Signature:Sign in to Café ☕登录到应用Let's go 🚀münchen.de wants you to sign in…Checklist
@walletconnect/utils)Additional Information (Optional)
Found while triaging the
0x-domain EIP-1271 report fixed in #7327. Changeset included:@walletconnect/utilspatch.🤖 Generated with Claude Code