Skip to content

Commit 0211932

Browse files
authored
fix: convert chain_tip materialized view into a table (#1789)
* fix: convert `chain_tip` materialized view into a table (#1751) * feat: chain tip table * fix: handle reorgs * fix: lint
1 parent f461c42 commit 0211932

File tree

13 files changed

+313
-202
lines changed

13 files changed

+313
-202
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/* eslint-disable camelcase */
2+
3+
exports.shorthands = undefined;
4+
5+
exports.up = pgm => {
6+
pgm.dropMaterializedView('chain_tip');
7+
pgm.createTable('chain_tip', {
8+
id: {
9+
type: 'bool',
10+
primaryKey: true,
11+
default: true,
12+
},
13+
block_height: {
14+
type: 'integer',
15+
notNull: true,
16+
},
17+
block_count: {
18+
type: 'integer',
19+
notNull: true,
20+
},
21+
block_hash: {
22+
type: 'bytea',
23+
notNull: true,
24+
},
25+
index_block_hash: {
26+
type: 'bytea',
27+
notNull: true,
28+
},
29+
burn_block_height: {
30+
type: 'integer',
31+
notNull: true,
32+
},
33+
microblock_hash: {
34+
type: 'bytea',
35+
},
36+
microblock_sequence: {
37+
type: 'integer',
38+
},
39+
microblock_count: {
40+
type: 'integer',
41+
notNull: true,
42+
},
43+
tx_count: {
44+
type: 'integer',
45+
notNull: true,
46+
},
47+
tx_count_unanchored: {
48+
type: 'integer',
49+
notNull: true,
50+
},
51+
});
52+
pgm.addConstraint('chain_tip', 'chain_tip_one_row', 'CHECK(id)');
53+
pgm.sql(`
54+
WITH block_tip AS (
55+
SELECT block_height, block_hash, index_block_hash, burn_block_height
56+
FROM blocks
57+
WHERE block_height = (SELECT MAX(block_height) FROM blocks WHERE canonical = TRUE)
58+
),
59+
microblock_tip AS (
60+
SELECT microblock_hash, microblock_sequence
61+
FROM microblocks, block_tip
62+
WHERE microblocks.parent_index_block_hash = block_tip.index_block_hash
63+
AND microblock_canonical = true AND canonical = true
64+
ORDER BY microblock_sequence DESC
65+
LIMIT 1
66+
),
67+
microblock_count AS (
68+
SELECT COUNT(*)::INTEGER AS microblock_count
69+
FROM microblocks
70+
WHERE canonical = TRUE AND microblock_canonical = TRUE
71+
),
72+
tx_count AS (
73+
SELECT COUNT(*)::INTEGER AS tx_count
74+
FROM txs
75+
WHERE canonical = TRUE AND microblock_canonical = TRUE
76+
AND block_height <= (SELECT MAX(block_height) FROM blocks WHERE canonical = TRUE)
77+
),
78+
tx_count_unanchored AS (
79+
SELECT COUNT(*)::INTEGER AS tx_count_unanchored
80+
FROM txs
81+
WHERE canonical = TRUE AND microblock_canonical = TRUE
82+
)
83+
INSERT INTO chain_tip (block_height, block_hash, index_block_hash, burn_block_height,
84+
block_count, microblock_hash, microblock_sequence, microblock_count, tx_count,
85+
tx_count_unanchored)
86+
VALUES (
87+
COALESCE((SELECT block_height FROM block_tip), 0),
88+
COALESCE((SELECT block_hash FROM block_tip), ''),
89+
COALESCE((SELECT index_block_hash FROM block_tip), ''),
90+
COALESCE((SELECT burn_block_height FROM block_tip), 0),
91+
COALESCE((SELECT block_height FROM block_tip), 0),
92+
(SELECT microblock_hash FROM microblock_tip),
93+
(SELECT microblock_sequence FROM microblock_tip),
94+
COALESCE((SELECT microblock_count FROM microblock_count), 0),
95+
COALESCE((SELECT tx_count FROM tx_count), 0),
96+
COALESCE((SELECT tx_count_unanchored FROM tx_count_unanchored), 0)
97+
)
98+
`);
99+
};
100+
101+
exports.down = pgm => {
102+
pgm.dropTable('chain_tip');
103+
pgm.createMaterializedView('chain_tip', {}, `
104+
WITH block_tip AS (
105+
SELECT block_height, block_hash, index_block_hash, burn_block_height
106+
FROM blocks
107+
WHERE block_height = (SELECT MAX(block_height) FROM blocks WHERE canonical = TRUE)
108+
),
109+
microblock_tip AS (
110+
SELECT microblock_hash, microblock_sequence
111+
FROM microblocks, block_tip
112+
WHERE microblocks.parent_index_block_hash = block_tip.index_block_hash
113+
AND microblock_canonical = true AND canonical = true
114+
ORDER BY microblock_sequence DESC
115+
LIMIT 1
116+
),
117+
microblock_count AS (
118+
SELECT COUNT(*)::INTEGER AS microblock_count
119+
FROM microblocks
120+
WHERE canonical = TRUE AND microblock_canonical = TRUE
121+
),
122+
tx_count AS (
123+
SELECT COUNT(*)::INTEGER AS tx_count
124+
FROM txs
125+
WHERE canonical = TRUE AND microblock_canonical = TRUE
126+
AND block_height <= (SELECT MAX(block_height) FROM blocks WHERE canonical = TRUE)
127+
),
128+
tx_count_unanchored AS (
129+
SELECT COUNT(*)::INTEGER AS tx_count_unanchored
130+
FROM txs
131+
WHERE canonical = TRUE AND microblock_canonical = TRUE
132+
)
133+
SELECT *, block_tip.block_height AS block_count
134+
FROM block_tip
135+
LEFT JOIN microblock_tip ON TRUE
136+
LEFT JOIN microblock_count ON TRUE
137+
LEFT JOIN tx_count ON TRUE
138+
LEFT JOIN tx_count_unanchored ON TRUE
139+
LIMIT 1
140+
`);
141+
pgm.createIndex('chain_tip', 'block_height', { unique: true });
142+
};

src/api/controllers/cache-controller.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,13 +251,13 @@ async function calculateETag(
251251
switch (etagType) {
252252
case ETagType.chainTip:
253253
try {
254-
const chainTip = await db.getUnanchoredChainTip();
255-
if (!chainTip.found) {
254+
const chainTip = await db.getChainTip();
255+
if (chainTip.block_height === 0) {
256256
// This should never happen unless the API is serving requests before it has synced any
257257
// blocks.
258258
return;
259259
}
260-
return chainTip.result.microblockHash ?? chainTip.result.indexBlockHash;
260+
return chainTip.microblock_hash ?? chainTip.index_block_hash;
261261
} catch (error) {
262262
logger.error(error, 'Unable to calculate chain_tip ETag');
263263
return;

src/api/routes/status.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ export function createStatusRouter(db: PgStore): express.Router {
1818
response.pox_v1_unlock_height = poxForceUnlockHeights.result.pox1UnlockHeight as number;
1919
response.pox_v2_unlock_height = poxForceUnlockHeights.result.pox2UnlockHeight as number;
2020
}
21-
const chainTip = await db.getUnanchoredChainTip();
22-
if (chainTip.found) {
21+
const chainTip = await db.getChainTip();
22+
if (chainTip.block_height > 0) {
2323
response.chain_tip = {
24-
block_height: chainTip.result.blockHeight,
25-
block_hash: chainTip.result.blockHash,
26-
index_block_hash: chainTip.result.indexBlockHash,
27-
microblock_hash: chainTip.result.microblockHash,
28-
microblock_sequence: chainTip.result.microblockSequence,
29-
burn_block_height: chainTip.result.burnBlockHeight,
24+
block_height: chainTip.block_height,
25+
block_hash: chainTip.block_hash,
26+
index_block_hash: chainTip.index_block_hash,
27+
microblock_hash: chainTip.microblock_hash,
28+
microblock_sequence: chainTip.microblock_sequence,
29+
burn_block_height: chainTip.burn_block_height,
3030
};
3131
}
3232
setETagCacheHeaders(res);

src/datastore/common.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -743,15 +743,6 @@ export type BlockIdentifier =
743743
| { burnBlockHash: string }
744744
| { burnBlockHeight: number };
745745

746-
export interface DbChainTip {
747-
blockHeight: number;
748-
indexBlockHash: string;
749-
blockHash: string;
750-
microblockHash?: string;
751-
microblockSequence?: number;
752-
burnBlockHeight: number;
753-
}
754-
755746
export interface BlockQueryResult {
756747
block_hash: string;
757748
index_block_hash: string;
@@ -1455,8 +1446,14 @@ export interface SmartContractInsertValues {
14551446
}
14561447

14571448
export interface DbChainTip {
1458-
blockHeight: number;
1459-
blockHash: string;
1460-
indexBlockHash: string;
1461-
burnBlockHeight: number;
1449+
block_height: number;
1450+
block_count: number;
1451+
block_hash: string;
1452+
index_block_hash: string;
1453+
burn_block_height: number;
1454+
microblock_hash?: string;
1455+
microblock_sequence?: number;
1456+
microblock_count: number;
1457+
tx_count: number;
1458+
tx_count_unanchored: number;
14621459
}

src/datastore/pg-store.ts

Lines changed: 20 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -252,28 +252,20 @@ export class PgStore {
252252
});
253253
}
254254

255-
async getChainTip(
256-
sql: PgSqlClient
257-
): Promise<{
258-
blockHeight: number;
259-
blockHash: string;
260-
indexBlockHash: string;
261-
burnBlockHeight: number;
262-
}> {
263-
const currentTipBlock = await sql<
264-
{
265-
block_height: number;
266-
block_hash: string;
267-
index_block_hash: string;
268-
burn_block_height: number;
269-
}[]
270-
>`SELECT block_height, block_hash, index_block_hash, burn_block_height FROM chain_tip`;
271-
const height = currentTipBlock[0]?.block_height ?? 0;
255+
async getChainTip(): Promise<DbChainTip> {
256+
const tipResult = await this.sql<DbChainTip[]>`SELECT * FROM chain_tip`;
257+
const tip = tipResult[0];
272258
return {
273-
blockHeight: height,
274-
blockHash: currentTipBlock[0]?.block_hash ?? '',
275-
indexBlockHash: currentTipBlock[0]?.index_block_hash ?? '',
276-
burnBlockHeight: currentTipBlock[0]?.burn_block_height ?? 0,
259+
block_height: tip?.block_height ?? 0,
260+
block_count: tip?.block_count ?? 0,
261+
block_hash: tip?.block_hash ?? '',
262+
index_block_hash: tip?.index_block_hash ?? '',
263+
burn_block_height: tip?.burn_block_height ?? 0,
264+
microblock_hash: tip?.microblock_hash ?? undefined,
265+
microblock_sequence: tip?.microblock_sequence ?? undefined,
266+
microblock_count: tip?.microblock_count ?? 0,
267+
tx_count: tip?.tx_count ?? 0,
268+
tx_count_unanchored: tip?.tx_count_unanchored ?? 0,
277269
};
278270
}
279271

@@ -368,33 +360,6 @@ export class PgStore {
368360
return this.getPoxForcedUnlockHeightsInternal(this.sql);
369361
}
370362

371-
async getUnanchoredChainTip(): Promise<FoundOrNot<DbChainTip>> {
372-
const result = await this.sql<
373-
{
374-
block_height: number;
375-
index_block_hash: string;
376-
block_hash: string;
377-
microblock_hash: string | null;
378-
microblock_sequence: number | null;
379-
burn_block_height: number;
380-
}[]
381-
>`SELECT block_height, index_block_hash, block_hash, microblock_hash, microblock_sequence, burn_block_height
382-
FROM chain_tip`;
383-
if (result.length === 0) {
384-
return { found: false } as const;
385-
}
386-
const row = result[0];
387-
const chainTipResult: DbChainTip = {
388-
blockHeight: row.block_height,
389-
indexBlockHash: row.index_block_hash,
390-
blockHash: row.block_hash,
391-
microblockHash: row.microblock_hash === null ? undefined : row.microblock_hash,
392-
microblockSequence: row.microblock_sequence === null ? undefined : row.microblock_sequence,
393-
burnBlockHeight: row.burn_block_height,
394-
};
395-
return { found: true, result: chainTipResult };
396-
}
397-
398363
async getBlock(blockIdentifer: BlockIdentifier): Promise<FoundOrNot<DbBlock>> {
399364
return this.getBlockInternal(this.sql, blockIdentifer);
400365
}
@@ -678,8 +643,8 @@ export class PgStore {
678643

679644
async getUnanchoredTxsInternal(sql: PgSqlClient): Promise<{ txs: DbTx[] }> {
680645
// Get transactions that have been streamed in microblocks but not yet accepted or rejected in an anchor block.
681-
const { blockHeight } = await this.getChainTip(sql);
682-
const unanchoredBlockHeight = blockHeight + 1;
646+
const { block_height } = await this.getChainTip();
647+
const unanchoredBlockHeight = block_height + 1;
683648
const query = await sql<ContractTxQueryResult[]>`
684649
SELECT ${unsafeCols(sql, [...TX_COLUMNS, abiColumn()])}
685650
FROM txs
@@ -1426,11 +1391,11 @@ export class PgStore {
14261391
sql: PgSqlClient,
14271392
{ includeUnanchored }: { includeUnanchored: boolean }
14281393
): Promise<number> {
1429-
const chainTip = await this.getChainTip(sql);
1394+
const chainTip = await this.getChainTip();
14301395
if (includeUnanchored) {
1431-
return chainTip.blockHeight + 1;
1396+
return chainTip.block_height + 1;
14321397
} else {
1433-
return chainTip.blockHeight;
1398+
return chainTip.block_height;
14341399
}
14351400
}
14361401

@@ -2213,9 +2178,9 @@ export class PgStore {
22132178

22142179
async getStxBalanceAtBlock(stxAddress: string, blockHeight: number): Promise<DbStxBalance> {
22152180
return await this.sqlTransaction(async sql => {
2216-
const chainTip = await this.getChainTip(sql);
2181+
const chainTip = await this.getChainTip();
22172182
const blockHeightToQuery =
2218-
blockHeight > chainTip.blockHeight ? chainTip.blockHeight : blockHeight;
2183+
blockHeight > chainTip.block_height ? chainTip.block_height : blockHeight;
22192184
const blockQuery = await this.getBlockByHeightInternal(sql, blockHeightToQuery);
22202185
if (!blockQuery.found) {
22212186
throw new Error(`Could not find block at height: ${blockHeight}`);

0 commit comments

Comments
 (0)