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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions