Skip to content

Commit 3e2fe7c

Browse files
committed
tapdb: dedupe legacy duplicate events during migration 62 backfill
The bug this PR closes -- restart-triggered re-fires of mint events inserting duplicate supply_update_events rows -- could have already fired against an unupgraded database. Two rows with identical content hash to the same event_key, so the second SetSupplyUpdateEventKey in the migration 62 backfill would violate the unique index added in migration 61 and abort the migration. Track hashes seen during the backfill loop and delete any row whose hash a prior row already claimed. The deleted rows are by definition the same logical event as the one that survives. A new DeleteSupplyUpdateEvent sqlc query supports the delete-by-event_id that the dedupe needs. TestMigration62BackfillDedupesLegacyDuplicates seeds three identical rows pre-backfill and asserts the migration leaves exactly one with the expected hash, never erroring out.
1 parent 221ed86 commit 3e2fe7c

5 files changed

Lines changed: 110 additions & 0 deletions

File tree

tapdb/migrations_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package tapdb
22

33
import (
4+
"bytes"
45
"context"
56
"database/sql"
67
"encoding/hex"
@@ -1491,3 +1492,60 @@ func TestMigration62BackfillSupplyUpdateEventKeys(t *testing.T) {
14911492
require.NoError(t, postRows.Close())
14921493
require.Equal(t, len(seeds), seen)
14931494
}
1495+
1496+
// TestMigration62BackfillDedupesLegacyDuplicates simulates the legacy
1497+
// failure mode this PR closes: pre-migration databases could contain
1498+
// multiple supply_update_events rows with identical content. The
1499+
// migration 62 backfill must drop the duplicates rather than fail on
1500+
// the unique index added in migration 61.
1501+
func TestMigration62BackfillDedupesLegacyDuplicates(t *testing.T) {
1502+
ctx := context.Background()
1503+
1504+
db := NewTestDBWithVersion(t, 61)
1505+
1506+
groupKey := bytes.Repeat([]byte{0x42}, 32)
1507+
payload := []byte("event-payload-duplicate")
1508+
1509+
// Insert the same logical event three times. NULL event_key is
1510+
// distinct from NULL under both backends, so all three rows
1511+
// land without tripping the unique index.
1512+
for i := 0; i < 3; i++ {
1513+
_, err := db.InsertSupplyUpdateEvent(
1514+
ctx, sqlc.InsertSupplyUpdateEventParams{
1515+
GroupKey: groupKey,
1516+
TransitionID: sql.NullInt64{},
1517+
UpdateTypeID: 0,
1518+
EventData: payload,
1519+
EventKey: nil,
1520+
},
1521+
)
1522+
require.NoError(t, err)
1523+
}
1524+
1525+
var preCount int
1526+
require.NoError(t, db.QueryRowContext(ctx, `
1527+
SELECT COUNT(*) FROM supply_update_events
1528+
`).Scan(&preCount))
1529+
require.Equal(t, 3, preCount)
1530+
1531+
// Run the backfill. The unique index added in migration 61
1532+
// would reject the naive UPDATE for the second and third
1533+
// rows; the backfill must dedupe before writing.
1534+
err := db.ExecuteMigrations(TargetLatest, WithProgrammaticMigrations(
1535+
makeProgrammaticMigrations(db, programmaticMigrations, true),
1536+
))
1537+
require.NoError(t, err)
1538+
1539+
// Exactly one row should survive, and its event_key should
1540+
// match the hash of the duplicated content.
1541+
var postCount int
1542+
var survivingKey []byte
1543+
require.NoError(t, db.QueryRowContext(ctx, `
1544+
SELECT COUNT(*), MAX(event_key) FROM supply_update_events
1545+
`).Scan(&postCount, &survivingKey))
1546+
require.Equal(t, 1, postCount)
1547+
require.Equal(t,
1548+
supplyUpdateEventKey(groupKey, 0, payload),
1549+
survivingKey,
1550+
)
1551+
}

tapdb/programmatic_migrations.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,13 @@ func insertAssetBurns(ctx context.Context, q sqlc.Querier) error {
342342
// in migration 000061) and stores it in the new column. After this
343343
// migration runs every row holds a hash, and the unique index on
344344
// event_key enforces the no-duplicates invariant for new inserts.
345+
//
346+
// Legacy databases may already contain duplicate rows (the bug this
347+
// PR fixes -- restart re-fires of the same logical event). Two rows
348+
// with identical content hash to the same key, so the second
349+
// SetSupplyUpdateEventKey would violate the unique index added in
350+
// migration 000061. We dedupe in-memory by tracking the hashes we've
351+
// already assigned and dropping any row whose hash we've seen.
345352
func backfillSupplyUpdateEventKeys(ctx context.Context,
346353
q sqlc.Querier) error {
347354

@@ -354,11 +361,32 @@ func backfillSupplyUpdateEventKeys(ctx context.Context,
354361
log.Debugf("Backfilling event_key for %d supply update events",
355362
len(rows))
356363

364+
seen := make(map[string]struct{}, len(rows))
357365
for _, row := range rows {
358366
key := supplyUpdateEventKey(
359367
row.GroupKey, row.UpdateTypeID, row.EventData,
360368
)
361369

370+
if _, dup := seen[string(key)]; dup {
371+
// A prior row in this loop already claimed this
372+
// hash, so the current row is a duplicate of an
373+
// earlier logical event. Drop it; the unique
374+
// index in migration 000061 would otherwise
375+
// reject the UPDATE below.
376+
log.Debugf("Dropping duplicate supply update "+
377+
"event %d during backfill", row.EventID)
378+
379+
err := q.DeleteSupplyUpdateEvent(ctx, row.EventID)
380+
if err != nil {
381+
return fmt.Errorf("error deleting "+
382+
"duplicate event %d: %w",
383+
row.EventID, err)
384+
}
385+
386+
continue
387+
}
388+
seen[string(key)] = struct{}{}
389+
362390
err := q.SetSupplyUpdateEventKey(
363391
ctx, sqlc.SetSupplyUpdateEventKeyParams{
364392
EventKey: key,

tapdb/sqlc/querier.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tapdb/sqlc/queries/supply_commit.sql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,13 @@ WHERE transition_id = @transition_id;
233233
DELETE FROM supply_update_events
234234
WHERE transition_id = @transition_id;
235235

236+
-- name: DeleteSupplyUpdateEvent :exec
237+
-- Deletes a single supply update event row identified by its
238+
-- event_id. Used by the migration 62 backfill to drop duplicate
239+
-- rows that hash to the same event_key as an earlier row.
240+
DELETE FROM supply_update_events
241+
WHERE event_id = @event_id;
242+
236243
-- name: FetchUnspentSupplyPreCommits :many
237244
-- Fetch unspent supply pre-commitment outputs. Each pre-commitment output
238245
-- comes from a mint anchor transaction and relates to an asset issuance

tapdb/sqlc/supply_commit.sql.go

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)