Skip to content

fix(genesis): overlay genesisData.balances on snapshot rows#858

Merged
tcsenpai merged 1 commit into
stabilisationfrom
fix/genesis-balance-merge-snapshot
May 25, 2026
Merged

fix(genesis): overlay genesisData.balances on snapshot rows#858
tcsenpai merged 1 commit into
stabilisationfrom
fix/genesis-balance-merge-snapshot

Conversation

@tcsenpai
Copy link
Copy Markdown
Contributor

@tcsenpai tcsenpai commented May 25, 2026

Summary

When data/snapshot/ is present, restoreSnapshot owns every gcr_main row and the legacy genesisData.balances array was silently dropped (per chainGenesis.ts:131-138 historical comment). An operator who edited data/genesis.json to add an address would see block-0 hash change (extra.genesisData carries the raw JSON) but the address balance would stay at whatever the snapshot row said — or 0 once ensureGCRForUser later materialised the row.

Repro

  • Address 0xd17624…d98a listed in data/genesis.json.balances with non-zero balance
  • Chain accepts the resulting block-0 hash
  • getAddressInfo reports balance: "0"
  • Same for 3 independent identities — all "funded in genesis on paper", all 0 on chain
  • Disk diverged from what the hash committed to

Fix

mergeGenesisBalances() runs after restoreSnapshot inside the same transaction. genesisData.balances wins on conflict — operator intent is "snapshot is yesterday's state, genesis.balances is today's top-up". Only the balance column is overwritten; identities, points, referralInfo, flags are preserved. Missing pubkeys get a fresh row with the same defaults HandleGCR.createAccount would assign.

Consensus impact

None. Block-0 hash is computed from serializeBlockContent whose extra.genesisData already includes the balances array. Every honest node parsing the same genesis.json now derives the same gcr_main state, which is what the hash already committed to. The fix closes the gap between hash and on-disk state; it does not change the hash.

Validation

Balances parsed to bigint OS up front; rejects negative / fractional / NaN / wrong-shaped entries. Silent BigInt(0) on a typo would re-introduce the same class of "operator wrote it but chain ignored it" bug.

Test plan

  • 18 unit cases in testing/genesis/mergeGenesisBalances.test.ts:
    • no-op paths (undefined/null/empty)
    • validation rejects (non-array, malformed tuple, empty pubkey, negative, fractional, NaN, wrong type)
    • coercion (string / number / bigint)
    • dedup last-wins on duplicate pubkey
    • UPDATE preserves snapshot identity columns
    • INSERT writes defaults (identities, referralCode, points, flags, dates)
    • mixed batch counts (total / updated / inserted)
    • zero balance still applied (operator intent)
  • Manual: wipe data_* PG volume, boot with data/snapshot/ + data/genesis.json.balances containing extra addresses, verify getAddressInfo returns the genesis-declared balance for those addresses.

Summary by CodeRabbit

  • Bug Fixes

    • Genesis account restoration now properly aligns account balances with committed genesis data during snapshot recovery, ensuring consistency between snapshot-derived accounts and on-chain genesis records.
  • Tests

    • Added comprehensive test suite for genesis balance validation and overlay operations.

Review Change Stack

…or top-ups actually land

When data/snapshot/ is present, restoreSnapshot owns every gcr_main row
and the legacy genesisData.balances array was silently dropped (per
chainGenesis.ts:131-138 historical comment). An operator who edited
data/genesis.json to add an address would see block-0 hash change
(extra.genesisData carries the raw JSON) but the address balance would
remain at whatever the snapshot row said — or 0 once ensureGCRForUser
later materialised the row.

Concretely: address 0xd17624...d98a was listed in data/genesis.json with
a non-zero balance, the chain accepted the resulting block-0 hash, but
gcr_main reported balance "0". Three independent identities, all funded
in genesis on paper, all 0 on chain. The disk diverged from what the
hash committed to.

Fix: new mergeGenesisBalances() runs after restoreSnapshot inside the
same transaction. genesisData.balances wins on conflict — operator
intent is "the snapshot is yesterday's state, the genesis.balances
overlay is today's top-up". Only the balance column is overwritten;
identities, points, referralInfo, flags from the snapshot row are
preserved. Missing pubkeys get a fresh row with the same defaults
HandleGCR.createAccount would assign, so the row is indistinguishable
from one materialised organically.

Consensus impact: none. Block-0 hash is computed from
serializeBlockContent whose extra.genesisData already includes the
balances array. Every honest node parsing the same genesis.json now
derives the same gcr_main state, which is what the hash already
committed to. The fix closes the gap between hash and on-disk state,
it does not change the hash.

Validation: balances are parsed to bigint OS up front and rejected if
negative, fractional, NaN, or wrong-shaped — silent BigInt(0) on a typo
would re-introduce the same class of "operator wrote it but the chain
ignored it" bug.

Tests: 18 unit cases covering no-op paths, validation rejects, coercion
(string|number|bigint), dedup last-wins, UPDATE preserves snapshot
identity columns, INSERT writes defaults, mixed batch counts.
@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 20fa141 into stabilisation May 25, 2026
1 of 2 checks passed
@tcsenpai tcsenpai deleted the fix/genesis-balance-merge-snapshot branch May 25, 2026 13:25
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 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: 18ca17cb-6cbd-4c0a-bac8-2d60e7d8ca31

📥 Commits

Reviewing files that changed from the base of the PR and between 6899bcf and 055b987.

📒 Files selected for processing (3)
  • src/libs/blockchain/chainGenesis.ts
  • src/libs/blockchain/genesis/mergeGenesisBalances.ts
  • testing/genesis/mergeGenesisBalances.test.ts

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


Walkthrough

This PR adds a new genesis balance overlay feature that applies committed genesis balance data to snapshot-restored accounts. It introduces a new mergeGenesisBalances module with strict entry parsing, deduplication, and database merging logic, then integrates it into the snapshot-restore flow in chainGenesis with comprehensive test coverage.

Changes

Genesis balance overlay during snapshot restore

Layer / File(s) Summary
Balance entry parsing and validation
src/libs/blockchain/genesis/mergeGenesisBalances.ts
parseGenesisBalanceEntry validates and coerces raw [pubkey, balance] tuples into [string, bigint] pairs, supporting bigint, integer number, and string input forms; rejects malformed entries, negative balances, and coercion failures with descriptive errors.
Result type and account creation defaults
src/libs/blockchain/genesis/mergeGenesisBalances.ts
MergeGenesisBalancesResult interface reports total, updated, and inserted counts; defaultEmptyAccountFields factory creates new accounts with default timestamps, identity/points, referral code generation, and account flags.
Merge orchestration and database updates
src/libs/blockchain/genesis/mergeGenesisBalances.ts
mergeGenesisBalances parses all balance entries into a deduplicated map (last-wins by pubkey), then iterates each [pubkey, balance] to update existing gcr_main rows or insert new ones with defaults, returning counts and logging progress.
Snapshot restore integration in chainGenesis
src/libs/blockchain/chainGenesis.ts
Imports and calls mergeGenesisBalances in a transactional step after snapshot restoration to overlay genesis-committed balances onto restored account rows.
Comprehensive test coverage
testing/genesis/mergeGenesisBalances.test.ts
Bun unit tests with an in-memory EntityManager mock exercise input validation, balance coercion, deduplication semantics, both UPDATE and INSERT paths, mixed-batch accounting, and zero-balance regression testing.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A rabbit hops through genesis blocks so tall,
Balances overlaid upon them all! 🐰✨
Snapshots merge with gently tested grace,
Deduplication finds its perfect place.

✨ 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/genesis-balance-merge-snapshot

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 25, 2026

Greptile Summary

This PR fixes a long-standing silent bug where genesisData.balances entries in data/genesis.json were dropped when a snapshot was present, leaving operator-declared addresses with a zero balance despite the balance being committed in the block-0 hash. The new mergeGenesisBalances module overlays those balances inside the existing snapshot transaction, closing the gap between hash and on-disk state without changing consensus.

  • mergeGenesisBalances validates and deduplicates the balance array up-front (failing loudly on malformed entries), then UPSERTs: existing snapshot rows get only their balance overwritten, missing pubkeys get fresh rows with the same defaults HandleGCR.createAccount would assign.
  • Wired into chainGenesis.ts in the snapshot branch only, inside the same dataSource.transaction that owns restoreSnapshot, so a crash mid-overlay rolls the whole genesis bootstrap back.
  • 18 unit tests cover validation rejection, coercion, dedup, UPDATE/INSERT paths, and mixed-batch counts via an in-memory EntityManager mock.

Confidence Score: 4/5

The change is safe to merge — it closes a real on-disk/hash divergence at genesis and runs inside an existing transaction, so partial failures roll back cleanly.

The logic is well-scoped, the validation path fails loudly on malformed input, and the row-level merge correctly preserves snapshot-owned columns. The row-by-row SELECT+SAVE loop introduces no correctness problems but does not scale for large balances arrays. The result.total JSDoc is mildly misleading about deduplication. Both are quality observations rather than behavioral defects.

src/libs/blockchain/genesis/mergeGenesisBalances.ts — specifically the N+1 loop at lines 207–226 if the balances list ever grows beyond a handful of founder addresses.

Important Files Changed

Filename Overview
src/libs/blockchain/genesis/mergeGenesisBalances.ts New module that validates, deduplicates, and applies genesisData.balances over snapshot rows; uses a row-by-row SELECT+SAVE loop (N+1) and sets result.total to the post-dedup count
src/libs/blockchain/chainGenesis.ts Wires mergeGenesisBalances into the snapshot transaction path after restoreSnapshot and seedValidators; non-snapshot path is unchanged
testing/genesis/mergeGenesisBalances.test.ts 18 unit tests covering validation, coercion, dedup, UPDATE/INSERT paths, and mixed-batch counts using an in-memory EntityManager mock

Sequence Diagram

sequenceDiagram
    participant G as generateGenesisBlock
    participant TX as dataSource.transaction
    participant RS as restoreSnapshot
    participant SV as seedValidators
    participant MGB as mergeGenesisBalances
    participant AF as applyForksAtGenesis
    participant DB as gcr_main (DB)

    G->>TX: begin transaction
    TX->>RS: restoreSnapshot(em, snapshot)
    RS->>DB: bulk-insert snapshot rows
    TX->>SV: seedValidators(em, validators)
    SV->>DB: insert validator rows
    TX->>MGB: mergeGenesisBalances(em, genesisData.balances)
    loop for each parsed pubkey
        MGB->>DB: findOne(pubkey)
        alt row exists (snapshot row)
            MGB->>DB: "save(existing, balance=new)"
        else row missing
            MGB->>DB: save(fresh row with defaults)
        end
    end
    TX->>AF: applyForksAtGenesis(em, forks)
    AF->>DB: apply fork migrations
    TX-->>G: commit (or rollback on any throw)
Loading

Reviews (1): Last reviewed commit: "fix(genesis): overlay genesisData.balanc..." | Re-trigger Greptile

Comment on lines +207 to +226
for (const [pubkey, balance] of parsed) {
const existing = await repo.findOne({ where: { pubkey } })
if (existing) {
// Only the balance column is overwritten; everything else
// (identities, points, referralInfo, flagged, …) belongs to
// the snapshot row and we have no business stomping it.
existing.balance = balance
existing.updatedAt = new Date()
await repo.save(existing)
result.updated++
} else {
const fresh = repo.create({
pubkey,
balance,
...defaultEmptyAccountFields(pubkey),
})
await repo.save(fresh)
result.inserted++
}
}
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 N+1 query loop at genesis bootstrap

The main loop issues a repo.findOne plus a repo.save for every distinct pubkey, sequentially. For a genesisData.balances that carries tens or hundreds of entries this results in that many individual SELECT + UPDATE/INSERT round-trips inside the transaction. A bulk approach — one SELECT ... WHERE pubkey IN (...) to split entries into existing/missing sets, then an INSERT ... ON CONFLICT DO UPDATE or a batched save([...]) for each set — would reduce this to two queries regardless of input size. At genesis time this is a one-shot operation so correctness is unaffected, but the current implementation does not scale if the balances list grows.

Comment on lines +151 to +152
/** Total entries processed from genesisData.balances */
total: number
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 The total field is assigned parsed.size (unique pubkeys after deduplication) but its JSDoc says "Total entries processed from genesisData.balances" and the preceding log line reports the raw balances.length. If a balances array has duplicate pubkeys, total will be less than the number of array items, which is surprising for a caller reading both the log and the result. Consider updating the JSDoc to clarify that it counts distinct pubkeys.

Suggested change
/** Total entries processed from genesisData.balances */
total: number
/** Distinct pubkeys processed from genesisData.balances (duplicates are last-wins deduped) */
total: number

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