Skip to content

fix(transactions): widen amount + fee cols to numeric(38,0) so post-fork OS values cannot overflow#863

Merged
tcsenpai merged 1 commit into
stabilisationfrom
fix/widen-tx-money-cols-to-numeric
May 26, 2026
Merged

fix(transactions): widen amount + fee cols to numeric(38,0) so post-fork OS values cannot overflow#863
tcsenpai merged 1 commit into
stabilisationfrom
fix/widen-tx-money-cols-to-numeric

Conversation

@tcsenpai
Copy link
Copy Markdown
Contributor

@tcsenpai tcsenpai commented May 26, 2026

Summary

A 10% transfer out of a 10^18 DEM genesis-funded account is 10^17 DEM × 10^9 OS/DEM = 10^26 OSthree orders of magnitude past PG bigint (int8, max ≈ 9.22 × 10^18).

Pre-fix, validateTransaction accepted the tx (getAccountBalance reads gcr_main.balance as numeric(38, 0)) but insertBlock crashed the consensus loop:

QueryFailedError: value "100000000000000000000000000" is out of range for type bigint

Fix

The four money-shaped columns on transactions (amount, networkFee, rpcFee, additionalFee) are widened to numeric(38, 0) to match gcr_main.balance. They share its bigintNumericTransformer so the application-level type stays bigint end-to-end — readers and writers don't change shape.

Why this matters

OS-magnitude wire values already land in amount post-fork (see DemosTransactions.pay where wireAmount = amountOs.toString()), and any genesis-funded account big enough to be useful (founder + incentives wallets) trips this on first transfer. Without the fix, validators crash on the first non-trivial transfer.

Why 9 decimals (not 18)

Demos: 1 DEM = 10^9 OS. The overflow comes from supply magnitude (founder accounts pre-funded at 10^18 DEM), not decimal count. Same structural issue Ethereum-style chains hit with bigint — Geth uses byte arrays for the same reason.

Migration

1779834000000-WidenTransactionsMoneyColsToNumeric:

  • ALTER COLUMN … TYPE numeric(38, 0) — implicit cast from bigint is lossless (every bigint fits a numeric(38, 0))
  • Defaults restored explicitly after the TYPE change (Postgres drops them)
  • down() reverses to bigint, but narrowing is lossy on any row with a post-fork-magnitude value — operator guidance in the migration log clarifies they must wipe before reverting

Test plan — devnet repro

Booted local 2-node devnet with funded-genesis fixture (5 identities, 1e18 DEM each, osDenomination active from block 0).

  • Pre-fix: confirm() returns valid: true, broadcast lands tx in reference_block=2, node-1 crashes at insertBlock:

    value "100000000000000000000000000" is out of range for type bigint
    

    Chain stops at block 2, no receiver-side balance update visible.

  • Post-fix: chain advances to block 9+, receiver balance moves from 1e27 OS (genesis) → 1.1e27 OS (genesis + 1e26 OS transfer). GCREdit hash compare still passes (PR fix(forks,validation): post-fork by default on fresh chains + GCREdit hash mismatch #861 prerequisite).

Type-check

bun run type-check-ts
# new code clean; pre-existing duplicate-import errors in chainBlocks.ts + worker-threads-test unchanged

Summary by CodeRabbit

  • Refactor
    • Improved numeric precision for transaction monetary fields. Transaction amounts and associated fees (network, RPC, additional) now support larger values with enhanced accuracy for better financial data integrity.

Review Change Stack

…38,0) so post-fork OS values cannot overflow at INSERT

Concretely: a 10 % transfer out of a 10^18 DEM genesis-funded account
is 10^17 DEM × 10^9 OS/DEM = 10^26 OS — three orders of magnitude past
PG `bigint` (int8, max ≈ 9.22 × 10^18).

Pre-this-fix `validateTransaction` accepted the tx
(`getAccountBalance` already reads `gcr_main.balance` as
`numeric(38, 0)`) but `insertBlock` crashed the consensus loop:

    QueryFailedError: value "100000000000000000000000000" is out of
                      range for type bigint

The four money-shaped columns on `transactions` (`amount`,
`networkFee`, `rpcFee`, `additionalFee`) are widened to
`numeric(38, 0)` to match `gcr_main.balance`. They share its
`bigintNumericTransformer` so the application-level type stays
`bigint` end-to-end — readers / writers don't change shape.

Why not just sub-DEM amounts (which fit)? OS-magnitude wire values
already land in `amount` post-fork (see DemosTransactions.pay where
`wireAmount = amountOs.toString()` post-fork), and any genesis-funded
account big enough to be useful (founder + incentives wallets) will
trip this on first transfer.

Migration: 1779834000000-WidenTransactionsMoneyColsToNumeric. Idempotent
ALTER COLUMN … TYPE numeric(38, 0) USING (implicit cast); defaults
restored explicitly after the TYPE change (Postgres drops them).
`down()` reverses to `bigint` — narrowing is lossy on any row with a
post-fork-magnitude value, so the migration log clarifies operators
must wipe before reverting.

Manual repro on a fresh devnet booted with the funded-genesis fixture:
- pre-fix: confirm() returns valid=true, broadcast lands tx in
  reference_block=2, then node-1 crashes at insertBlock with
  "value '1e26' is out of range for type bigint", chain stops at
  block 2, no receiver-side balance update visible.
- post-fix: chain advances to block 9+, receiver balance moves from
  10^27 OS (genesis) to 1.1×10^27 OS (genesis + 10^26 OS transfer).
  GCREdit hash compare still passes (PR #861 is the predecessor here).

Why 9 decimals and not 18 (Ethereum-style): Demos uses 1 DEM = 10^9 OS.
The overflow comes from supply magnitude (founder accounts pre-funded
at 10^18 DEM), not decimal count.
@qodo-code-review
Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@tcsenpai tcsenpai merged commit 4fff076 into stabilisation May 26, 2026
1 of 2 checks passed
@tcsenpai tcsenpai deleted the fix/widen-tx-money-cols-to-numeric branch May 26, 2026 14:19
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 08591ff4-b4bc-4318-a0b4-3039330bbd60

📥 Commits

Reviewing files that changed from the base of the PR and between 963ca5f and 8d8f3a7.

📒 Files selected for processing (2)
  • src/migrations/1779834000000-WidenTransactionsMoneyColsToNumeric.ts
  • src/model/entities/Transactions.ts

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


Walkthrough

The PR widens four transaction money columns (amount, networkFee, rpcFee, additionalFee) from Postgres bigint to numeric(38, 0) using a TypeORM entity update paired with a reversible database migration. The entity adds a numeric transformer to handle type conversions.

Changes

Transaction Money Column Type Widening

Layer / File(s) Summary
Entity column type and transformer updates
src/model/entities/Transactions.ts
Import of bigintNumericTransformer and four column decorators updated from bigint to numeric(38, 0) with transformer attachment; fee defaults changed to string literals "0" to match the numeric column type.
Database migration for numeric widening
src/migrations/1779834000000-WidenTransactionsMoneyColsToNumeric.ts
New TypeORM migration that widens the four columns in up() by dropping fee defaults before type conversion and restoring them after, then reverts with explicit ::bigint casts and default re-application in down().

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 Columns grow taller, numeric and wide,
Bigints trade places, no data's denied,
Defaults and transformers dance hand in hand,
Migration reversals make schemas so grand!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/widen-tx-money-cols-to-numeric

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 26, 2026

Greptile Summary

Widens the four money-shaped columns on transactions (amount, networkFee, rpcFee, additionalFee) from PG bigint to numeric(38, 0) via a new TypeORM migration, adding the shared bigintNumericTransformer to the entity so the application-level type stays bigint end-to-end. The fix unblocks the consensus INSERT crash for post-fork OS-magnitude transfers (e.g. 10^26 OS from a 10^18 DEM genesis account).

  • The migration up() is a lossless widening; down() uses explicit USING ::bigint casts and is documented as lossy for rows already carrying OS-magnitude values.
  • The entity changes correctly mirror gcr_main.balance's column definition and transformer, keeping write paths consistent.

Confidence Score: 3/5

The INSERT crash is resolved, but four unguarded DB read functions in chainTransactions.ts will throw on the first post-fork OS-magnitude transaction lookup, leaving peer-sync and RPC tx queries broken for the very rows this migration enables.

The migration and entity changes are correct and the write path is fully fixed. However, fromEntityToWireNumber — already in the codebase — explicitly throws for values above Number.MAX_SAFE_INTEGER, and getTxByHash, getBlockTransactions, getTransactionsFromHashes, and getTransactionFromHash all call fromRawTransaction without any guard. Storing OS-magnitude rows activates this throw path for every transaction read, which could crash consensus-adjacent operations, peer sync, and RPC queries that weren't broken before simply because no such rows existed.

src/libs/blockchain/transaction.ts (fromEntityToWireNumber guard) and src/libs/blockchain/chainTransactions.ts (all four fromRawTransaction call sites) need attention before this is safe for a network that will process post-fork OS-magnitude transactions.

Important Files Changed

Filename Overview
src/migrations/1779834000000-WidenTransactionsMoneyColsToNumeric.ts New migration widens amount, networkFee, rpcFee, additionalFee from bigint to numeric(38,0). up() is lossless (widening cast); down() correctly uses USING ::bigint and documents the lossy-revert risk. Defaults are dropped and restored explicitly around type changes for fee columns.
src/model/entities/Transactions.ts Four money columns converted from @Column("bigint") to @Column({ type: "numeric", precision: 38, scale: 0, transformer: bigintNumericTransformer }). Transformer is reused from gcr_main.balance, keeping the application-level bigint type consistent. Pre-existing amount: bigint vs nullable: true type mismatch is unchanged.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Node as Node (consensus)
    participant ORM as TypeORM Entity
    participant PG as Postgres

    Client->>Node: "broadcast tx (amount = 1e26 OS)"
    Node->>Node: validateTransaction (reads gcr_main.balance as numeric ✓)
    Node->>ORM: toTransactionsEntity(rawTx) → bigint via toEntityBigint
    ORM->>ORM: bigintNumericTransformer.to(1e26n) → "100000000000000000000000000"
    ORM->>PG: INSERT INTO transactions (amount) VALUES ('100000000000000000000000000')
    Note over PG: numeric(38,0) accepts value ✓ (pre-fix: bigint overflow ✗)

    Note over Node,ORM: Read path — UNGUARDED after this fix
    Node->>ORM: getTxByHash / getBlockTransactions
    ORM->>PG: SELECT amount FROM transactions
    PG->>ORM: "100000000000000000000000000" (string)
    ORM->>ORM: bigintNumericTransformer.from → 1e26n
    ORM->>Node: fromRawTransaction(entity)
    Node->>Node: fromEntityToWireNumber(1e26n)
    Note over Node: throws Error: exceeds MAX_SAFE_INTEGER ✗
Loading

Reviews (1): Last reviewed commit: "fix(transactions): widen amount + fee co..." | Re-trigger Greptile

Comment on lines +55 to +59
await queryRunner.query(
`ALTER TABLE "transactions" ALTER COLUMN "networkFee" SET DEFAULT 0`,
)
await queryRunner.query(
`ALTER TABLE "transactions" ALTER COLUMN "rpcFee" DROP DEFAULT`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 amount column type change lacks USING clause consistency with fee columns

The up() migration changes amount with a bare ALTER COLUMN … TYPE numeric(38, 0) and relies on Postgres's implicit bigint→numeric cast. This is safe in practice (every bigint is representable as numeric(38,0)) but is inconsistent with the fee columns, which explicitly DROP DEFAULT before the TYPE change to avoid a Postgres implicit-rewrite edge case. Since amount has no default (so no drop-restore sequence is needed), this difference is only a minor style inconsistency — but worth noting for symmetry with the rest of the migration.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

tcsenpai added a commit that referenced this pull request May 27, 2026
…ansactions + batch aggregator (#873)

Companion to #863 (top-level `transactions` widening) and #872
(`fromEntityToWireNumber` post-fork string fallback). Audit of every
remaining `Number.MAX_SAFE_INTEGER` and `Number(bigint)` site that
touches a money field found two more places that silently corrupt or
crash on post-fork OS values:

1) `L2PSTransactions.amount` was `@Column("bigint")`, same `int8` cap
   as the top-level `transactions.amount` widened in #863. L2PS
   replays per-tx amounts inside aggregated L1 batches, so a 10 %
   move out of a 10^18 DEM wallet (10^26 OS) overflows the same way
   the top-level table did before #863. Widened to `numeric(38, 0)`
   with the shared `bigintNumericTransformer`. New migration
   `1779834500000-WidenL2PSTransactionsAmountToNumeric` matches the
   shape of `1779834000000-WidenTransactionsMoneyColsToNumeric`.

2) `L2PSBatchAggregator.zkTransactions` was doing
   `BigInt(Math.floor(Number(rawAmount)))` for every tx amount. The
   wire shape is `string | number | bigint`; routing the post-fork
   decimal-string path through `Number()` first silently truncates to
   the nearest double-precision value (10^26 OS becomes 10^26 minus
   ~70 low bits of junk) before `BigInt()` is even reached. Rewrote
   the coercion to handle each input type directly: `bigint` and
   `number` pass through, decimal-integer strings go straight to
   `BigInt()`, fractional strings are truncated at the decimal point.
   No lossy `Number()` round-trip on the wide path.

Both are pre-existing post-fork overflow bombs that would have fired
the first time an L2PS batch carried a real founder-wallet transfer.

Audit of remaining `MAX_SAFE_INTEGER` sites confirms the others are
either dead code (`subOperations.transferNative` — no live caller),
seed values for `reduce()` (`l2ps_hashes.ts`), block-number caps
(`omniprotocol/sync.ts`), or legacy-GCR migration internals that no
longer ship as a live runtime path.

Co-authored-by: tcsenpai <tcsenpai@discus.sh>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant