Skip to content

fix(core): dedupe chain events (CORE-08) - #199

Merged
seanpatrickmay merged 1 commit into
mainfrom
seanpatrickmay/core-08-event-dedupe
Oct 25, 2026
Merged

seanpatrickmay merged 1 commit into
mainfrom
seanpatrickmay/core-08-event-dedupe

Conversation

@seanpatrickmay

Copy link
Copy Markdown
Collaborator

Last open item of #60 (CORE-08): chain events were not deduplicated (packages/core/API.md said so). Atomic writes, the cross-process lock and the encrypted preimage store already landed in #140.

Key: (txHash, kind, projectId, milestoneId), hash compared case-insensitively. Not the hash alone: one tx can back several events that are all real history (another kind, another project's view of a shared tx, or one hash per milestone), so only an exact repeat of the same fact collapses. Today every event has its own hash: Batch escrows use their inner EscrowCreate hashes, and preceding txs are not recorded. The wider key keeps it that way if that ever changes. First recording wins: a later reconcile of the same tx (result: "reconciled_loan_repaid" / "reconciled_escrow_consumed") does not replace it. Events without a txHash are never collapsed.

Where: recordChainEvents(db, events) in store.ts is now the only way core appends a ChainEvent: all 21 sites in service.ts and the prestage import. Routes still return the events from their own call.

Found while testing: two writers on one FileStore (the keeper process and the server) can both reconcile the same ambiguous refund LoanPay. Before this PR that recorded the event twice and also booked the repayment twice. commitRefund now books a milestone's escrow refund once.

Tests (test/event-dedupe.test.ts, plus one in prestage-import.test.ts):

  • the helper: a repeat collapses and the first recording wins, case-insensitive hash, events that differ in kind/project/milestone are kept, one hash across N milestones gives N events, events without a txHash are never collapsed
  • a loan close with N escrows records N escrow_created events plus one loan_set
  • an ambiguous refund LoanPay reconciled by two services on one store gives 1 event and 1 repayment (this test fails on main with 2 events)
  • recording a committed close's events again adds nothing
  • a prestage file that logs a tx twice, imported twice, gives one event per key

pnpm -r typecheck && pnpm -r test green (core 158 tests).

Closes #60

…Id) (#60)

Every ChainEvent append now goes through recordChainEvents (store.ts): an
event whose key is already stored is skipped, first recording wins, events
without a txHash are never collapsed. A milestone's escrow refund is booked in
repayments once, even when two writers reconcile the same ambiguous LoanPay.
@seanpatrickmay
seanpatrickmay merged commit 0e1c5a6 into main Oct 25, 2026
1 check passed
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.

CORE-08 · Store hardening

1 participant