From c64413f154246279e5ffa36bdc94e80e089261d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 17:01:08 +0000 Subject: [PATCH] Receipt comment: canonical JSON is the JS profile, not Python json.dumps Our docs said the signed bytes are "the same as Python's json.dumps(sort_keys=True, separators=(",", ":"), ensure_ascii=True)". That only holds for ASCII keys and integers: Python writes 1.0 and 1e-06 where JSON.stringify writes 1 and 0.000001, and sorts U+E000 before U+10000 (code points) where the signer sorts by UTF-16 code units. Paid answers carry decimals (prices, liquidity), so a Python verifier following the old text could reject a genuine receipt. TAT Risk surfaced this while pinning the profile for their signed evidence. The normative profile is named js-json-stringify-sorted-utf16-ascii-v1: keys by UTF-16 code units, compact, every code unit from U+007F up as lowercase \uXXXX, numbers as JSON.stringify writes them, UTF-8 bytes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TsD5haPKxeWjYmErHzvU48 --- src/receipt.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/receipt.ts b/src/receipt.ts index 4b3d27f..7c8b3f0 100644 --- a/src/receipt.ts +++ b/src/receipt.ts @@ -5,9 +5,11 @@ // a verdict: a changed, unsigned or foreign answer, or one for another // request, is reported as untrusted instead of used. // -// Canonical JSON: keys sorted at every level, no whitespace, non-ASCII as -// \uXXXX (the same bytes as Python's json.dumps(sort_keys=True, -// separators=(",", ":"), ensure_ascii=True)). +// Canonical JSON (profile js-json-stringify-sorted-utf16-ascii-v1): keys +// sorted by UTF-16 code units at every level, no whitespace, every code unit +// from U+007F up as lowercase \uXXXX, numbers as JSON.stringify writes them, +// UTF-8 bytes. Python's json.dumps matches only for ASCII keys and integers; a +// Python equivalent: github.com/Fizzl13/presign-guard/blob/main/examples/canonical.py import { createHash } from "node:crypto"; import { recoverMessageAddress, type Hex } from "viem";