Repository navigation
v2.3.0 — canonical number formatting
Important
Anyone on the previous release has this defect. Six SDKs formatted numbers differently from the ledger, which verifies against a re-stringified $tx — its crypto package calls JSON.stringify on the object its HTTP layer already parsed. JavaScript's formatting is therefore the specification, and a number written differently produces a signature rejected as 1220 "Signature Incorrect", with nothing in the message about numbers.
| input | JS (spec) | PHP | Python | Go | Rust | C# | JVM |
|---|---|---|---|---|---|---|---|
1e21 |
1e+21 |
1.0e+21 |
1000…000 |
1e+21 |
1000…000 |
1E+21 |
1.0E21 |
1e-7 |
1e-7 |
1.0e-7 |
1e-07 |
1e-07 |
0.0000001 |
1E-07 |
1.0E-7 |
-0 |
0 |
0 |
0 |
-0 |
0 |
0 |
0 |
Two were data corruption, not formatting
- PHP cast whole floats to
int, andPHP_INT_MAXis ~9.2e18 — so1e19produced-8446744073709551616. A positive input became a negative number, with a warning nobody reads in a signing path. - Rust's
as i64saturates:1e20became9223372036854775807. - C# had the same shape via
(long), undefined abovelong.MaxValue. JVM'stoLong()saturates likewise.
Reachable in ordinary use
A nanosecond timestamp is ~1.76e18 — well past 2^53. Integers now take double precision because JavaScript has no integer type: 9007199254740993 becomes …992 before the ledger verifies anything, so signing the exact integer could never match.
The rule
ECMA-262 Number::toString — shortest round-trip digits, plain decimal while -6 < n <= 21, exponent form outside it with no leading zeros and an explicit +. Each SDK exposes the formatter publicly so a value can be checked before building a transaction.
Verified against JSON.stringify on 6139 doubles including every power of ten from 1e-330 to 1e308, plus a 24-case number-vectors.json covering both sides of both boundaries.
Why it survived: the existing float vector was 1, 0.1, -2.5, 0 — all inside the range where every language already agrees.