Skip to content

v2.3.0 — canonical number formatting

Choose a tag to compare

@AdmWalker AdmWalker released this 21 Sep 07:22
· 8 commits to master since this release

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, and PHP_INT_MAX is ~9.2e18 — so 1e19 produced -8446744073709551616. A positive input became a negative number, with a warning nobody reads in a signing path.
  • Rust's as i64 saturates: 1e20 became 9223372036854775807.
  • C# had the same shape via (long), undefined above long.MaxValue. JVM's toLong() 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.