Skip to content

indexer: pushWithTombstone create+remove dedup is order-sensitive within a single transaction #653

Description

@aristidesstaffieri

Summary

pushWithTombstone (internal/indexer/indexer_buffer.go) nets a create+remove of the same key within one ledger to nothing via a tombstone. That is correct only if the CREATE is folded before the REMOVE. Within a single transaction, changes are appended while ranging the opsParticipants map in processTransaction (internal/indexer/indexer.go), so the relative order of a CREATE-op change and a REMOVE-op change is nondeterministic — which flips the result.

Behavior (for a tx that both creates and removes the same key: CreateAccount+AccountMerge; ChangeTrust add + ChangeTrust limit-0)

  • CREATE folds first → REMOVE hits isNoopRemove, key deleted + tombstone recorded → nets to nothing ✓
  • REMOVE folds first → no existing entry, so isNoopRemove (guarded by exists) never fires; REMOVE stored as a phantom with no tombstone, and the lower-order CREATE is then dropped at the order(existing) > order(change) check → a spurious REMOVE persists ✗

Same ledger + input, different output between runs. Cross-transaction (create in tx1, remove in tx2 of the same ledger) is safe — results fold in tx-index order.

Impact

Nondeterministic ingestion output for identical input (reproducibility / backfill consistency). The downstream effect of the spurious REMOVE depends on how REMOVE changes are applied (may be an idempotent no-op).

Scope note

Pre-existing in the tombstone code introduced by #641; not introduced or worsened by #648 (which preserves the within-tx map-iteration order — its "dedup/tombstone semantics unchanged" claim holds for this hazard). Filed separately so it's tracked against the owning code.

Fix direction

Fold each change slice in ascending order value before dedup (sort by SortKey/OperationID), or emit changes ordered from processTransaction, so CREATE always precedes REMOVE. Add a regression test with a create+remove-in-one-tx ledger — the map iteration means a naive test passes most runs.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions