Skip to content

Sort JCS object keys by UTF-16 code unit, not code point - #3

Merged
laxsharma merged 1 commit into
mainfrom
fix/jcs-utf16-key-order
Aug 21, 2026
Merged

Sort JCS object keys by UTF-16 code unit, not code point#3
laxsharma merged 1 commit into
mainfrom
fix/jcs-utf16-key-order

Conversation

@laxsharma

Copy link
Copy Markdown
Member

What

jcs() in tools/validate.py was built on json.dumps(sort_keys=True), which orders
object keys by Unicode code point. RFC 8785 section 3.2.3 orders them by UTF-16
code unit
.

The two agree throughout the Basic Multilingual Plane and diverge above it, because a
supplementary-plane key encodes as a surrogate pair beginning U+D800 and therefore
sorts below keys in U+E000..U+FFFF:

code point (before):  {"":1,"\U00010000":2}
UTF-16     (after) :  {"\U00010000":2,"":1}

No published hash changes

Every key in every shipped example is ASCII, so the two orderings already agreed on all
of them. Recomputed with the fix:

Commitment Before After
spec_hash sha256:bb0e87ce…c47adf35 identical
criteria_hash sha256:d9205d4f…6d66c69f7 identical
vtc_hash sha256:3184cbd5…dc8e1b705 identical

This closes a latent defect, not a live one. Nothing already published is wrong.

Why it was worth fixing rather than documenting

The module caveat disclosed only the number-serialization limitation and said nothing
about key order, which was the more misleading of the two omissions. A canonicalizer
carrying this shortcut passes an ASCII or BMP vector by accident, so the defect is
invisible to exactly the tests an implementer would think to write, and would surface
on the first non-BMP key the implementation ever saw.

Changes

  • tools/validate.py: _utf16_key_order() reorders keys recursively before
    serialization; jcs() no longer passes sort_keys.
  • Two vectors under a new == canonicalization == section, one at the root and one
    nested, so the property is pinned at every depth rather than only at the top.
  • Module docstring and README now name what is fixed (key order) and what remains
    unimplemented (ECMAScript number serialization over the full float range).

Verified that the previous implementation fails both new vectors and the new one passes,
so these are not vacuous.

Provenance

Prompted by a canonicalization vector raised by @wowlegend in
x402-foundation/x402#3065, which pins the same divergence class from the JavaScript
side. The vector that catches this shape is theirs; the bug is ours.

RFC 8785 section 3.2.3 orders object keys by UTF-16 code unit.
json.dumps(sort_keys=True) orders them by Unicode code point. The two
agree throughout the Basic Multilingual Plane and diverge above it,
where a supplementary-plane key encodes as a surrogate pair beginning
U+D800 and therefore sorts below keys in U+E000..U+FFFF.

No published hash changes. Every key in every shipped example is ASCII,
so the two orderings already agreed on all of them; spec_hash,
criteria_hash and vtc_hash are byte-identical before and after. This
closes a latent defect rather than correcting a live one.

The module caveat previously disclosed only the number-serialization
limitation and said nothing about key order, which was the more
misleading omission of the two: a canonicalizer built on the shortcut
passes an ASCII vector by accident and would fail on the first non-BMP
key it ever saw. Two vectors now pin the behaviour, one at the root and
one nested, and the caveat names what is fixed and what is still not
implemented.

Prompted by a canonicalization vector raised by @wowlegend in
x402-foundation/x402#3065, which pins the same divergence class.
@laxsharma
laxsharma merged commit 4619a45 into main Aug 21, 2026
4 checks passed
@laxsharma
laxsharma deleted the fix/jcs-utf16-key-order branch August 21, 2026 18:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant