Skip to content

Commit 20bf561

Browse files
authored
fix!: duplicate block re-org scenario in /transactions (#1135)
* test: replicate re-org scenario * fix: add index_block_hash and microblock_hash to reorgs * style: remove old comment * fix: canonical where * fix: use unique constraint to join on txs * test: restore correct test cases
1 parent f5c4da7 commit 20bf561

File tree

4 files changed

+127
-61
lines changed

4 files changed

+127
-61
lines changed

src/datastore/postgres-store.ts

Lines changed: 43 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1701,6 +1701,10 @@ export class PgDataStore
17011701
microblocks: string[];
17021702
}
17031703
): Promise<{ updatedTxs: DbTx[] }> {
1704+
const bufIndexBlockHash = hexToBuffer(args.indexBlockHash);
1705+
const bufBlockHash = hexToBuffer(args.blockHash);
1706+
const bufMicroblockHashes = args.microblocks.map(mb => hexToBuffer(mb));
1707+
17041708
// Flag orphaned microblock rows as `microblock_canonical=false`
17051709
const updatedMicroblocksQuery = await client.query(
17061710
`
@@ -1711,9 +1715,9 @@ export class PgDataStore
17111715
[
17121716
args.isMicroCanonical,
17131717
args.isCanonical,
1714-
hexToBuffer(args.indexBlockHash),
1715-
hexToBuffer(args.blockHash),
1716-
args.microblocks.map(mb => hexToBuffer(mb)),
1718+
bufIndexBlockHash,
1719+
bufBlockHash,
1720+
bufMicroblockHashes,
17171721
]
17181722
);
17191723
if (updatedMicroblocksQuery.rowCount !== args.microblocks.length) {
@@ -1734,10 +1738,10 @@ export class PgDataStore
17341738
[
17351739
args.isMicroCanonical,
17361740
args.isCanonical,
1737-
hexToBuffer(args.indexBlockHash),
1738-
hexToBuffer(args.blockHash),
1741+
bufIndexBlockHash,
1742+
bufBlockHash,
17391743
args.burnBlockTime,
1740-
args.microblocks.map(mb => hexToBuffer(mb)),
1744+
bufMicroblockHashes,
17411745
]
17421746
);
17431747
// Any txs restored need to be pruned from the mempool
@@ -1757,8 +1761,8 @@ export class PgDataStore
17571761
const updatedAssociatedTableParams = [
17581762
args.isMicroCanonical,
17591763
args.isCanonical,
1760-
hexToBuffer(args.indexBlockHash),
1761-
args.microblocks.map(mb => hexToBuffer(mb)),
1764+
bufIndexBlockHash,
1765+
bufMicroblockHashes,
17621766
updatedMbTxs.map(tx => hexToBuffer(tx.tx_id)),
17631767
];
17641768
for (const associatedTableName of TX_METADATA_TABLES) {
@@ -1774,16 +1778,14 @@ export class PgDataStore
17741778
);
17751779
}
17761780

1777-
// TODO: [bug] This is can end up with incorrect canonical state due to missing the `index_block_hash` column
1778-
// which is required for the way micro-reorgs are handled. Queries against this table can work around the
1779-
// bug by using the `txs` table canonical state in the JOIN condition.
1780-
17811781
// Update `principal_stx_txs`
17821782
await client.query(
17831783
`UPDATE principal_stx_txs
1784-
SET canonical = $1, microblock_canonical = $2
1785-
WHERE tx_id = ANY ($3)`,
1786-
[args.isCanonical, args.isMicroCanonical, updatedMbTxs.map(tx => hexToBuffer(tx.tx_id))]
1784+
SET microblock_canonical = $1, canonical = $2, index_block_hash = $3
1785+
WHERE microblock_hash = ANY($4)
1786+
AND (index_block_hash = $3 OR index_block_hash = '\\x'::bytea)
1787+
AND tx_id = ANY($5)`,
1788+
updatedAssociatedTableParams
17871789
);
17881790

17891791
return { updatedTxs: updatedMbTxs };
@@ -2327,9 +2329,9 @@ export class PgDataStore
23272329
// Update `principal_stx_txs`
23282330
await client.query(
23292331
`UPDATE principal_stx_txs
2330-
SET canonical = $1
2331-
WHERE tx_id = ANY ($2)`,
2332-
[canonical, txIds.map(tx => hexToBuffer(tx.tx_id))]
2332+
SET canonical = $2
2333+
WHERE tx_id = ANY($3) AND index_block_hash = $1 AND canonical != $2`,
2334+
[indexBlockHash, canonical, txIds.map(tx => hexToBuffer(tx.tx_id))]
23332335
);
23342336

23352337
const minerRewardResults = await client.query(
@@ -4537,8 +4539,8 @@ export class PgDataStore
45374539
}
45384540
eventsQueries.push(`
45394541
SELECT
4540-
tx_id, event_index, tx_index, block_height, locked_address as sender, NULL as recipient,
4541-
locked_amount as amount, unlock_height, NULL as asset_identifier, NULL as contract_identifier,
4542+
tx_id, event_index, tx_index, block_height, locked_address as sender, NULL as recipient,
4543+
locked_amount as amount, unlock_height, NULL as asset_identifier, NULL as contract_identifier,
45424544
'0'::bytea as value, NULL as topic,
45434545
${DbEventTypeId.StxLock} as event_type_id, 0 as asset_event_type_id
45444546
FROM stx_lock_events
@@ -4550,8 +4552,8 @@ export class PgDataStore
45504552
}
45514553
eventsQueries.push(`
45524554
SELECT
4553-
tx_id, event_index, tx_index, block_height, sender, recipient,
4554-
amount, 0 as unlock_height, NULL as asset_identifier, NULL as contract_identifier,
4555+
tx_id, event_index, tx_index, block_height, sender, recipient,
4556+
amount, 0 as unlock_height, NULL as asset_identifier, NULL as contract_identifier,
45554557
'0'::bytea as value, NULL as topic,
45564558
${DbEventTypeId.StxAsset} as event_type_id, asset_event_type_id
45574559
FROM stx_events
@@ -4563,8 +4565,8 @@ export class PgDataStore
45634565
}
45644566
eventsQueries.push(`
45654567
SELECT
4566-
tx_id, event_index, tx_index, block_height, sender, recipient,
4567-
amount, 0 as unlock_height, asset_identifier, NULL as contract_identifier,
4568+
tx_id, event_index, tx_index, block_height, sender, recipient,
4569+
amount, 0 as unlock_height, asset_identifier, NULL as contract_identifier,
45684570
'0'::bytea as value, NULL as topic,
45694571
${DbEventTypeId.FungibleTokenAsset} as event_type_id, asset_event_type_id
45704572
FROM ft_events
@@ -4576,8 +4578,8 @@ export class PgDataStore
45764578
}
45774579
eventsQueries.push(`
45784580
SELECT
4579-
tx_id, event_index, tx_index, block_height, sender, recipient,
4580-
0 as amount, 0 as unlock_height, asset_identifier, NULL as contract_identifier,
4581+
tx_id, event_index, tx_index, block_height, sender, recipient,
4582+
0 as amount, 0 as unlock_height, asset_identifier, NULL as contract_identifier,
45814583
value, NULL as topic,
45824584
${DbEventTypeId.NonFungibleTokenAsset} as event_type_id, asset_event_type_id
45834585
FROM nft_events
@@ -4589,8 +4591,8 @@ export class PgDataStore
45894591
}
45904592
eventsQueries.push(`
45914593
SELECT
4592-
tx_id, event_index, tx_index, block_height, NULL as sender, NULL as recipient,
4593-
0 as amount, 0 as unlock_height, NULL as asset_identifier, contract_identifier,
4594+
tx_id, event_index, tx_index, block_height, NULL as sender, NULL as recipient,
4595+
0 as amount, 0 as unlock_height, NULL as asset_identifier, contract_identifier,
45944596
value, topic,
45954597
${DbEventTypeId.SmartContractLog} as event_type_id, 0 as asset_event_type_id
45964598
FROM contract_logs
@@ -4605,7 +4607,7 @@ export class PgDataStore
46054607
`WITH events AS ( ` +
46064608
eventsQueries.join(`\nUNION\n`) +
46074609
`)
4608-
SELECT *
4610+
SELECT *
46094611
FROM events JOIN txs USING(tx_id)
46104612
WHERE txs.canonical = true AND txs.microblock_canonical = true
46114613
ORDER BY events.block_height DESC, microblock_sequence DESC, events.tx_index DESC, event_index DESC
@@ -4738,9 +4740,11 @@ export class PgDataStore
47384740
*/
47394741
async updatePrincipalStxTxs(client: ClientBase, tx: DbTx, events: DbStxEvent[]) {
47404742
const txIdBuffer = hexToBuffer(tx.tx_id);
4743+
const indexBlockHashBuffer = hexToBuffer(tx.index_block_hash);
4744+
const microblockHashBuffer = hexToBuffer(tx.microblock_hash);
47414745
const insertPrincipalStxTxs = async (principals: string[]) => {
47424746
principals = [...new Set(principals)]; // Remove duplicates
4743-
const columnCount = 7;
4747+
const columnCount = 9;
47444748
const insertParams = this.generateParameterizedInsertString({
47454749
rowCount: principals.length,
47464750
columnCount,
@@ -4751,31 +4755,21 @@ export class PgDataStore
47514755
principal,
47524756
txIdBuffer,
47534757
tx.block_height,
4758+
indexBlockHashBuffer,
4759+
microblockHashBuffer,
47544760
tx.microblock_sequence,
47554761
tx.tx_index,
47564762
tx.canonical,
47574763
tx.microblock_canonical
47584764
);
47594765
}
4760-
// If there was already an existing (`tx_id`, `principal`) pair in the table, we will update
4761-
// the entry's data to reflect the newer transaction state.
47624766
const insertQuery = `
47634767
INSERT INTO principal_stx_txs
4764-
(principal, tx_id, block_height, microblock_sequence, tx_index, canonical, microblock_canonical)
4768+
(principal, tx_id,
4769+
block_height, index_block_hash, microblock_hash, microblock_sequence, tx_index,
4770+
canonical, microblock_canonical)
47654771
VALUES ${insertParams}
4766-
ON CONFLICT ON CONSTRAINT unique_principal_tx_id
4767-
DO UPDATE
4768-
SET block_height = EXCLUDED.block_height,
4769-
microblock_sequence = EXCLUDED.microblock_sequence,
4770-
tx_index = EXCLUDED.tx_index,
4771-
canonical = EXCLUDED.canonical,
4772-
microblock_canonical = EXCLUDED.microblock_canonical
4773-
WHERE EXCLUDED.block_height > principal_stx_txs.block_height
4774-
OR EXCLUDED.microblock_sequence > principal_stx_txs.microblock_sequence
4775-
OR EXCLUDED.tx_index > principal_stx_txs.tx_index
4776-
OR EXCLUDED.canonical != principal_stx_txs.canonical
4777-
OR EXCLUDED.microblock_canonical != principal_stx_txs.microblock_canonical
4778-
`;
4772+
ON CONFLICT ON CONSTRAINT unique_principal_tx_id_index_block_hash_microblock_hash DO NOTHING`;
47794773
const insertQueryName = `insert-batch-principal_stx_txs_${columnCount}x${principals.length}`;
47804774
const insertQueryConfig: QueryConfig = {
47814775
name: insertQueryName,
@@ -5868,21 +5862,17 @@ export class PgDataStore
58685862
// join against `txs` to get the full transaction objects only for that page.
58695863
`
58705864
WITH stx_txs AS (
5871-
SELECT tx_id, ${countOverColumn()}
5865+
SELECT tx_id, index_block_hash, microblock_hash, ${countOverColumn()}
58725866
FROM principal_stx_txs
58735867
WHERE principal = $1 AND ${blockCond}
5868+
AND canonical = TRUE AND microblock_canonical = TRUE
58745869
ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC
58755870
LIMIT $2
58765871
OFFSET $3
58775872
)
58785873
SELECT ${txColumns()}, ${abiColumn()}, count
58795874
FROM stx_txs
5880-
INNER JOIN txs
5881-
ON (stx_txs.tx_id = txs.tx_id
5882-
AND txs.canonical = TRUE
5883-
AND txs.microblock_canonical = TRUE)
5884-
ORDER BY txs.block_height DESC, txs.microblock_sequence DESC, txs.tx_index DESC
5885-
LIMIT $2
5875+
INNER JOIN txs USING (tx_id, index_block_hash, microblock_hash)
58865876
`,
58875877
[args.stxAddress, args.limit, args.offset, args.blockHeight]
58885878
);

src/migrations/1640651533899_principal_stx_txs.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ export async function up(pgm: MigrationBuilder): Promise<void> {
2525
type: 'integer',
2626
notNull: true,
2727
},
28+
index_block_hash: {
29+
type: 'bytea',
30+
notNull: true,
31+
},
32+
microblock_hash: {
33+
type: 'bytea',
34+
notNull: true,
35+
},
2836
microblock_sequence: {
2937
type: 'integer',
3038
notNull: true,
@@ -51,5 +59,9 @@ export async function up(pgm: MigrationBuilder): Promise<void> {
5159
{ name: 'tx_index', sort: 'DESC' }
5260
]);
5361

54-
pgm.addConstraint('principal_stx_txs', 'unique_principal_tx_id', `UNIQUE(principal, tx_id)`);
62+
pgm.addConstraint(
63+
'principal_stx_txs',
64+
'unique_principal_tx_id_index_block_hash_microblock_hash',
65+
`UNIQUE(principal, tx_id, index_block_hash, microblock_hash)`
66+
);
5567
}

0 commit comments

Comments
 (0)