Skip to content

Kupmios/Kupo: 64-bit UTxO amounts decoded as JS number, rounding values above 2^53 #454

Description

@solidsnakedev

Summary

The Kupo provider decodes on-chain lovelace and asset quantities as JS numbers, but a JS
number is exact only to 2^53-1. Cardano amounts are uint64, so any value above 2^53 is
silently rounded. Kupo returns these amounts as unquoted JSON integers, so the rounding
happens at JSON.parse time — before the schema — and the later BigInt(...) conversion just
preserves the already-corrupted value. Blockfrost and Koios avoid this on decode by typing the
same fields as strings. Fail closed: no fund loss; a consumer can display a wrong balance, and a
corrupted UTxO fed into evaluation produces a tx that is rejected on-chain.

Affected

packages/evolution/src/sdk/provider/internal/Kupo.ts

  • L4 ValueSchema coins: S.Number
  • L5 ValueSchema assets value: S.Number
  • L49-50 Delegation rewards/deposit lovelace: S.Number

packages/evolution/src/sdk/provider/internal/KupmiosEffects.ts

  • L79 BigInt(value.coins) // wraps an already-rounded number
  • L85 BigInt(value.assets[unit]) // wraps an already-rounded number

transport: HttpUtils.get -> response.json -> JSON.parse truncates above 2^53 before the schema runs.
Kupo API: value.coins and asset quantities are type: integer (unquoted JSON numbers), per the Kupo OpenAPI spec.
contrast (correct): Blockfrost.ts inbound quantity: Schema.String -> BigInt; Koios.ts value/quantity: Schema.String -> BigInt.

Fix

Because JSON.parse rounds before the schema, changing S.Number to a bigint schema is not
enough — the inbound amount fields need a big-int-aware JSON parser on the Kupo response
(json-bigint style, or parse coins/assets from the raw text), then carry them as bigint into
CoreAssets.fromLovelace / addByHex (drop the intermediate JS number entirely).

Regression test

  • given: a Kupo matches response with coins = 2^53+1 and an asset quantity = 2^64-1 as raw JSON integers
  • before fix: decoded amounts are corrupted (2^53+1 -> 2^53, 2^64-1 -> 18446744073709552000)
  • after fix: CoreAssets holds the exact bigint values, round-tripping unchanged
    Must FAIL on main today and PASS after the fix.

Reference

Reported informally (large-value UTxO precision loss). Related: #406 (same bug class, Ogmios outbound path).
Correction to the original triage: the report also named Blockfrost, and that is partly right —
Blockfrost is safe on decode (amounts are strings) but IS affected on its OUTBOUND evaluate
serialization (BlockfrostEffect.ts toBlockfrostValue, Number() at L89/L98). That Blockfrost
outbound path is a separate finding, tracked in its own issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingexternal-reviewFrom external review batch

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions