Define canonical JSON encoding for unsigned 64-bit integers #343

Open
robwoodgate wants to merge 2 commits from robwoodgate/json-int into main
robwoodgate commented 2026-02-18 21:31:30 +00:00 (Migrated from github.com)

The NUTs define amounts as unsigned 64-bit integers, but the JSON wire encoding is ambiguous.

JSON has no integer type, and many clients parse JSON numbers via float-like defaults. This can cause cross-language divergence and precision loss for large values (e.g, in environments where JSON numbers use IEEE-754).

This PR defines a canonical UInt64Json format, which is backwards compatible and precision safe across runtimes.

Simply:

UInt64Json MAY be represented as a JSON integer number or a base-10 decimal string.

Implementations MUST accept both forms.

Values greater than 9007199254740991 (2^53-1) MUST be represented as decimal strings.

This applies to int / u64 fields across the protocol, including amounts in Proofs, Blinded Messages, and request/response payloads.

Fee fields are unchanged to keep the change minimal, but we could include all int/u64 JSON fields for consistency.

The NUTs define amounts as unsigned 64-bit integers, but the JSON wire encoding is ambiguous. JSON has no integer type, and many clients parse JSON numbers via float-like defaults. This can cause cross-language divergence and precision loss for large values (e.g, in environments where JSON numbers use IEEE-754). This PR defines a canonical `UInt64Json` format, which is backwards compatible and precision safe across runtimes. Simply: > `UInt64Json` **MAY** be represented as a JSON integer number or a base-10 decimal string. > > Implementations **MUST** accept both forms. > > Values greater than `9007199254740991 (2^53-1)` **MUST** be represented as decimal strings. This applies to `int` / `u64` fields across the protocol, including amounts in Proofs, Blinded Messages, and request/response payloads. Fee fields are unchanged to keep the change minimal, but we could include all int/u64 JSON fields for consistency.
prusnak commented 2026-02-18 22:13:33 +00:00 (Migrated from github.com)

When exactly are the amounts bigger than (2^53-1)?

>>> 21e6 * 1e8 < 2**53
True
When exactly are the amounts bigger than `(2^53-1)`? ``` >>> 21e6 * 1e8 < 2**53 True ```
robwoodgate commented 2026-02-18 22:40:42 +00:00 (Migrated from github.com)

When exactly are the amounts bigger than (2^53-1)?

>>> 21e6 * 1e8 < 2**53
True

Just look at Minibits key amounts: https://mint.minibits.cash/Bitcoin/v1/keys

Keys 9007199254740992 up to 9223372036854775808 are all over the safe limit (9007199254740991)

Amounts in millisats (eg melt quotes, amounts in unit msat) can go over the limit.

Even if not used 99.99% of the time, the wire support must be solid.

> When exactly are the amounts bigger than `(2^53-1)`? > > ``` > >>> 21e6 * 1e8 < 2**53 > True > ``` Just look at Minibits key amounts: https://mint.minibits.cash/Bitcoin/v1/keys Keys `9007199254740992` up to `9223372036854775808` are all over the safe limit (`9007199254740991`) Amounts in millisats (eg melt quotes, amounts in unit `msat`) can go over the limit. Even if not used 99.99% of the time, the wire support must be solid.
prusnak commented 2026-02-18 22:42:18 +00:00 (Migrated from github.com)

Amounts in millisats (eg melt quotes, amounts in unit msat) can go over the limit.

Yeah, I agree that if amounts are listed in msats, then we need this.

> Amounts in millisats (eg melt quotes, amounts in unit `msat`) can go over the limit. Yeah, I agree that if amounts are listed in msats, then we need this.
robwoodgate (Migrated from github.com) reviewed 2026-02-21 16:30:52 +00:00
@ -64,0 +68,4 @@
For JSON fields, `int` / `u64` values may be encoded as a JSON integer number or a base-10 decimal string.
Implementations **MUST** accept both forms.
robwoodgate (Migrated from github.com) commented 2026-02-21 16:30:52 +00:00

If we want to encourage a single standard (decimal string) over time, we could tweak:

Implementations **MUST** accept both forms, and **SHOULD** encode values as decimal strings.

We already consistently use string amounts in the /v1/keys api, so this would be a continuation of that precedent.

If we want to encourage a single standard (decimal string) over time, we could tweak: ```suggestion Implementations **MUST** accept both forms, and **SHOULD** encode values as decimal strings. ``` We already consistently use string amounts in the `/v1/keys` api, so this would be a continuation of that precedent.
callebtc commented 2026-02-26 15:43:26 +00:00 (Migrated from github.com)

I think this is too excessive. NUT-00 could state that all amounts are uint64 and we could leave the rest as is.

I think this is too excessive. NUT-00 could state that all amounts are uint64 and we could leave the rest as is.
SatsAndSports commented 2026-02-26 16:33:49 +00:00 (Migrated from github.com)

NUT-00 could state that all amounts are uint64

When you say 'uint64', do you mean non-quoted integers?

As mentioned by others, key amounts are quoted currently , but fees are non-quoted. Both of those, I think, make sense as fees can't realistically get very big

$ curl https://mint.minibits.cash/Bitcoin/v1/keys | jq .                                                                                             
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current                                                                                              
                                 Dload  Upload   Total   Spent    Left  Speed                                                                                                
100  5341  100  5341    0     0   1045      0  0:00:05  0:00:05 --:--:--  1100                                                                                               
{                                                                                                                                                                            
  "keysets": [                                                                                                                                                               
    {                                                                                                                                                                        
      "id": "00107937db0cc865",                                                                                                                                              
      "unit": "sat",                                                                                                                                                         
      "active": true,                                                                                                                                                        
      "input_fee_ppk": 0,                                                                                                                                                    
      "keys": {                                                                                                                                                              
        "1": "03be63a0f422f8db6297fb3018bc3e626751010434c19c9c8c990e1c4e438f03dc",  
...
        "9223372036854775808": "02d900df693b1505cc5cbdef13a7ee793b89ebfedb628f4dc5fbbe49a7ef8cd07a"
> NUT-00 could state that all amounts are uint64 When you say 'uint64', do you mean _non-quoted_ integers? As mentioned by others, key amounts are quoted currently , but fees are non-quoted. Both of those, I think, make sense as fees can't realistically get very big ``` $ curl https://mint.minibits.cash/Bitcoin/v1/keys | jq . % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 5341 100 5341 0 0 1045 0 0:00:05 0:00:05 --:--:-- 1100 { "keysets": [ { "id": "00107937db0cc865", "unit": "sat", "active": true, "input_fee_ppk": 0, "keys": { "1": "03be63a0f422f8db6297fb3018bc3e626751010434c19c9c8c990e1c4e438f03dc", ... "9223372036854775808": "02d900df693b1505cc5cbdef13a7ee793b89ebfedb628f4dc5fbbe49a7ef8cd07a" ```
SatsAndSports commented 2026-02-26 17:08:27 +00:00 (Migrated from github.com)

Ignore this comment. I oversimplified. See Rob's response just below

(Maybe I've missed some detail/context, but I think this will break stuff)

The CDK code currently requires the amounts (in the amount-publickey mapping in a keyset) to be quoted, i.e. "8" not 8.

In this screenshot, 'key' means the 'amount' and 'value' means the mint's public key for that amount.

So basically, I don't think we can change anything about existing fields

=======

An AI helped me find this example. (Apologies if I'm wrong, and have been misled by AI)

image image
***Ignore this comment. I oversimplified. See Rob's response just below*** (Maybe I've missed some detail/context, but I think this will break stuff) The CDK code currently requires the amounts (in the amount-publickey mapping in a keyset) to be quoted, i.e. `"8"` not `8`. In this screenshot, 'key' means the 'amount' and 'value' means the mint's public key for that amount. So basically, I don't think we can change anything about existing fields ======= An AI helped me find this example. (Apologies if I'm wrong, and have been misled by AI) <img width="1381" height="555" alt="image" src="https://github.com/user-attachments/assets/6c075ce0-4b2b-4748-bafb-c0d60b750087" /> <img width="1403" height="226" alt="image" src="https://github.com/user-attachments/assets/1e6773d4-e291-4a08-85b8-c4e7f372d535" />
robwoodgate commented 2026-02-26 20:03:19 +00:00 (Migrated from github.com)

(Maybe I've missed some detail/context, but I think this will break stuff)

The CDK code currently requires the amounts (in the amount-publickey mapping in a keyset) to be quoted, i.e. "8" not 8.

In this screenshot, 'key' means the 'amount' and 'value' means the mint's public key for that amount.

So basically, I don't think we can change anything about existing fields

=======

An AI helped me find this example. (Apologies if I'm wrong, and have been misled by AI)

Key amounts ARE all strings, and that is perfect. Not suggesting a change there at all.

In a perfect world, ALL u64 amounts would be represented as strings in JSON.

The issue is "amounts" (sums of key amounts), which currently transport as numeric digits, and which CAN be > 2^53-1 because a) some keys are already larger than that, and b) combinations of proofs could go over the limit in total.

Especially with millisat (or in future, nanosat) amounts.

Fee amounts can stay as numeric digits, as it would be crazy if fees went over the limit.

> (Maybe I've missed some detail/context, but I think this will break stuff) > > The CDK code currently requires the amounts (in the amount-publickey mapping in a keyset) to be quoted, i.e. `"8"` not `8`. > > In this screenshot, 'key' means the 'amount' and 'value' means the mint's public key for that amount. > > So basically, I don't think we can change anything about existing fields > > ======= > > An AI helped me find this example. (Apologies if I'm wrong, and have been misled by AI) > Key amounts ARE all strings, and that is perfect. Not suggesting a change there at all. In a perfect world, ALL u64 amounts would be represented as strings in JSON. The issue is "amounts" (sums of key amounts), which currently transport as numeric digits, and which CAN be > 2^53-1 because a) some keys are already larger than that, and b) combinations of proofs could go over the limit in total. Especially with millisat (or in future, nanosat) amounts. Fee amounts can stay as numeric digits, as it would be crazy if fees went over the limit.
robwoodgate commented 2026-04-07 15:33:17 +00:00 (Migrated from github.com)

I think this is too excessive. NUT-00 could state that all amounts are uint64 and we could leave the rest as is.

I have trimmed it down to essentially the NUT-00 change. LMK if you want other changes.

> I think this is too excessive. NUT-00 could state that all amounts are uint64 and we could leave the rest as is. I have trimmed it down to essentially the NUT-00 change. LMK if you want other changes.
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin robwoodgate/json-int:robwoodgate/json-int
git switch robwoodgate/json-int

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff robwoodgate/json-int
git switch robwoodgate/json-int
git rebase main
git switch main
git merge --ff-only robwoodgate/json-int
git switch robwoodgate/json-int
git rebase main
git switch main
git merge --no-ff robwoodgate/json-int
git switch main
git merge --squash robwoodgate/json-int
git switch main
git merge --ff-only robwoodgate/json-int
git switch main
git merge robwoodgate/json-int
git push origin main
Sign in to join this conversation.
No description provided.