F6: fix two replay bugs — genesis-index users FK, created_tx rewrite on edit - #63
Merged
Conversation
The creator's network row FK-references users(account), but createCommunity never created that row — prod only worked because backend sign-ups had pre-populated users. On a from-genesis index of a fresh DB the network insert violated network_account_id_fkey, the swallowed rollback dropped the whole community, and the next netlink crashed the process (network_community_id_fkey -> unhandledRejection -> exit -> crash-loop). Mirror netlink's idempotent INSERT ... ON CONFLICT DO NOTHING inside the same transaction, before the network/role inserts.
…n edit
The upsertObjective/upsertAction update paths carried created_block /
created_tx / created_at / created_eos_account into the save() UPDATE, so
every edit rewrote the row's create provenance with the edit's values.
That destroyed the create-path replay guard (count({created_tx})): after
any create+edit, replaying the range no longer recognized the create and
re-executed it — resolveCreated*Id found no unknown chain id, fell back
to the serial, and the insert hit community_objectives_pkey, aborting the
block transaction and crash-looping the indexer.
Keep the created_* fields create-only; update paths now touch only
mutable fields. The backend 20260702120000 migration comment cited the
rewrite only as the reason no unique index on created_tx was added — it
documents the old behavior, it is not a rationale for keeping it.
Verified end-to-end on the local chain: create+edit leaves created_tx /
created_block untouched, and a ledger-cleared replay of the range skips
the creates via the guard (row counts and created_tx unchanged).
Resolves the conflict with F2/F3: the created_* provenance stays on the create path only (this PR's point — the replay guard keys on created_tx, so an edit must not rewrite it), and every timestamp this PR newly writes now goes through toUTC, matching the convention master just adopted. That covers the creator's users row in createCommunity and both create-path provenance blocks. Also drops src/config/fresh.js, a local reproduction config whose own header says it is not meant to be committed; it was swept into master by a careless `git add -A` while landing F0. Added to .gitignore so it stays local. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
F6 — Two pre-existing replay bugs: both confirmed, both fixed
From the follow-up handoff — two second-hand reports from the escrow indexer work. Both reproduce; evidence and fixes below. Two commits:
e982794(bug 1),b120dd4(bug 2).yarn formatclean.Bug 1 — community creator missing from
userson a fresh genesis index: CONFIRMEDReproduction (before): fresh schema-only DB, index from block 1. Local chain's first real action is
cambiatus.cm::createat block 104 (creatoralice):Result: 0 users, 0 communities, 0 network rows — the whole
createCommunitytransaction rolled back (FK failure swallowed), and the very nextnetlinkcrashed the process (crash-loop under pm2). Prod never sees this because backend sign-ups pre-populateusers; a genesis index of a fresh DB has no such seeding.Fix:
createCommunitynow upserts the creator intousers(INSERT ... ON CONFLICT DO NOTHING, same pattern asnetlink) inside the same transaction, before thenetwork/network_rolesinserts.Acceptance (after): fresh DB from block 1 — 0 exit events; 6 users, 5 communities, 19 network, 20 network_roles;
alicepresent withcreated_block=104; all 5 communities indexed.Bug 2 —
upsertObjective/upsertActionrewritecreated_txon edit: CONFIRMEDReproduction (before): created objective id=2 (tx
f0332fb6…, block 32590) and action id=3 (txbbebf474…, block 32850), then edited both (block 32950). After indexing, both rows carried the EDIT'screated_block/created_tx. Then a safe rewind (UPDATE _index_state SET block_number=32589 … WHERE id=1+ targeted_processed_actionsdelete, perscripts/reindex-runbook.md) and replay:Exactly the reported chain: edit destroyed
created_tx→ create guardcount({created_tx})=0on replay → create re-executes → chain-id resolution finds nothing unknown → serial fallback → pkey violation → aborted block tx → process exit → crash-loop.On the migration comment (a decision, not silent compliance): backend migration
20260702120000_add_idempotency_backstops.exssays "the event-source update path rewrites created_tx on every edit…" — but only to justify not adding a unique index; it simultaneously relies on "the created_tx create-path guards", which the rewrite defeats. It documents current (buggy) behavior, not a design goal. The fix is therefore to stop the rewrite; the no-unique-index decision still stands on its own (two creates in one tx would legitimately sharecreated_tx). The backend comment itself may want a follow-up touch — flagged, not changed here.Fix: in
upsertObjectiveandupsertAction,created_block/created_tx/created_at/created_eos_accountmoved out of the shareddatainto the create path only; update paths write just mutable fields.Acceptance (after):
created_tx/created_blockunchanged.created_txvalues intact, 8 ledger rows re-claimed.Same-class observations (report only, not fixed)
created_tx— their create guard is still broken. The_processed_actionsledger protects them in normal operation, but any ledger-cleared replay over their create blocks re-triggers the pkey crash-loop. Consider a one-time remediation restoringobjectives/actions.created_tx(+block/at) from chain history.upsertRolealso rewritescreated_txon every edit — same class, but benign: its guard keys on the(name, community_id)natural key..catch(logError)) — the known "claimed ledger row without applied writes" window from the runbook. Pre-existing, documented, untouched.block_hash=''skips the handler's seek so the reader re-walks from block 1 (ledger makes it no-ops), and the non-monotoniccambiatus.tkhistory can leave_index_stateat a stale low block after catch-up.