Skip to content

demosdk: Hashing.sha256 digests tx content as binary not UTF-8 → 'Transaction hash mismatch' on any non-ASCII tx (root cause + 1-line fix) #968

Description

@cX3po

Hashing.sha256 digests tx content as binary, not UTF-8 → Transaction hash mismatch on any non-ASCII tx

Impact: any transaction whose content carries a non-ASCII character (em dash, curly quote, accented letter, emoji, any UTF-8 > 0x7F) fails client-side confirm with [Tx Validation] [SIGNATURE ERROR] Transaction hash mismatch. Pure-ASCII transactions are unaffected, so this hides easily until real-world content (names, feed text, descriptions) flows through — storage-program payloads, memos, listing/agreement bodies, etc.

Root cause: build/encryption/Hashing.js

static sha256(message) {
    const md = forge.sha256.create();
    md.update(message);          // ← no encoding arg → node-forge treats the string as raw/binary
    return md.digest().toHex();
}

Demos.sign() (build/websdk/demosclass.js:548) sets raw_tx.hash = Hashing.sha256(serialized). node-forge's md.update(str) without a second arg interprets str as a binary string (one byte per code unit, high bytes mangled), so for any code point > 0x7F the client hash is computed over corrupted bytes. The validating node re-hashes the received JSON as UTF-8, so the two digests disagree.

Corroboration that UTF-8 is the intended wire encoding: the SDK's own (deprecated) signer build/websdk/utils/sha256.js hashes via TextEncoder (UTF-8), and a live devnet node accepts UTF-8-hashed txs and rejects binary-hashed ones. So Hashing.sha256 is the outlier.

Fix — one line:

md.update(message, "utf8");

Verified live: with this applied, previously-failing non-ASCII payloads confirm and broadcast normally; all-ASCII behaviour is unchanged. The other Hashing.sha256 call sites we found are ASCII-only auth strings, so the change is inert for them.

Minimal repro: sign+confirm any tx whose serialized content contains (U+2014). Fails before the patch, passes after. String vs object payload, program-name characters, wallet/instance/connect-order are all irrelevant — the single trigger is a non-ASCII byte in tx.content.

Happy to open a PR against demosdk if that's the easier path — flagging here since this is the repo we file websdk issues on. This one's worth a point release: it silently breaks settlement for any real-world string content.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions