Skip to content
Tyche Institute

Tyche Labs · Finding 001 · 24 August 2026

The same number, two canonical forms

The first round of our open interlaboratory comparison was meant to measure whether an independently written verifier reproduces our reference verdicts. Before any outside participant arrived, it produced a finding against us: our own two reference implementations disagree, and the one we call the reference is the one that is wrong.

What happened

An action evidence package is signed over canonical JSON. A verifier re-serialises the received object, hashes it, and compares. That only works if every implementation agrees on the byte string. For one vector in the round — an amount of 1e-7 inside the signed payload — they do not. CPython writes 1e-07; JavaScript writes 1e-7. The Python reference allowed the package; the JavaScript port recomputed a different hash and rejected it as mutated content. Same document, same keys, opposite verdicts.

It is not one number

Probing the space systematically, ten of nineteen tested values diverge, through five distinct mechanisms:

mechanism value CPython ECMAScript
exponent zero-padding1e-71e-071e-7
threshold for exponential form0.000011e-050.00001
large integers1e161e+1610000000000000000
integral float2.02.02
precision above 2539007199254740993exact9007199254740992

The last row is not a formatting question at all. An ECMAScript JSON parser puts the integer into a binary64 double and the value silently changes before any canonicalisation runs. No serialisation rule can repair that; only a bound on the input can.

Which side is wrong

RFC 8785, the JSON Canonicalization Scheme, requires numbers to be serialised by the ECMAScript rule of ECMA-262. Where the two runtimes differ on formatting, the conformant output is the JavaScript one — so the deviation belongs to our Python reference, and the port we would have suspected first is the correct one. Naming an implementation “the reference” is a statement about our workflow, not about its conformance.

Why the existing test suite missed it

Our verifier parity harness runs on every build of this site and had never failed, because every shipped sample is all-integer — a fact the port's own source comment records. The claim it supports was therefore true and correctly scoped; what was wrong was the width of the corpus, not the honesty of the claim. A parity suite proves agreement over the cases it contains and nothing beyond them, which is the entire argument for comparisons run by someone other than the author.

What we changed, and what we did not

We did not touch either verifier. The round's assigned values were sealed by hash before it opened, and editing an implementation to make a divergence disappear would defeat the instrument; the vector stays in the corpus as an unscored probe, and any verdict change belongs to a later round with a new commitment.

What we added is a bound on the input. A producer-side guard rejects non-integer numbers and integers beyond 253−1 in signed payloads, and the site build now fails if any shipped sample leaves that range — turning an accidental property of the corpus into an enforced one. The alternative, implementing the ECMAScript number-to-string algorithm in Python, is a larger commitment than this format needs; a format that carries money and identifiers has no good reason to carry binary floating point at all.

The corpus, the rules and the sealed commitment are public in the aep-sandbox repository under interlab/round0. The assigned values are withheld until the round closes; only their SHA-256 is published. Independent verdicts, and findings against us, are what the round is for.

Tyche Labs →