Skip to content

Commit 04b71cc

Browse files
authored
fix: convert chain_tip materialized view into a table (#1751)
* feat: chain tip table * fix: handle reorgs
1 parent 3a2e1ea commit 04b71cc

File tree

13 files changed

+313
-200
lines changed

13 files changed

+313
-200
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
@@ -252,13 +252,13 @@ async function calculateETag(
252252
switch (etagType) {
253253
case ETagType.chainTip:
254254
try {
255-
const chainTip = await db.getUnanchoredChainTip();
256-
if (!chainTip.found) {
255+
const chainTip = await db.getChainTip();
256+
if (chainTip.block_height === 0) {
257257
// This should never happen unless the API is serving requests before it has synced any
258258
// blocks.
259259
return;
260260
}
261-
return chainTip.result.microblockHash ?? chainTip.result.indexBlockHash;
261+
return chainTip.microblock_hash ?? chainTip.index_block_hash;
262262
} catch (error) {
263263
logger.error(error, 'Unable to calculate chain_tip ETag');
264264
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
@@ -744,15 +744,6 @@ export type BlockIdentifier =
744744
| { burnBlockHash: string }
745745
| { burnBlockHeight: number };
746746

747-
export interface DbChainTip {
748-
blockHeight: number;
749-
indexBlockHash: string;
750-
blockHash: string;
751-
microblockHash?: string;
752-
microblockSequence?: number;
753-
burnBlockHeight: number;
754-
}
755-
756747
export interface BlockQueryResult {
757748
block_hash: string;
758749
index_block_hash: string;
@@ -1461,10 +1452,16 @@ export interface SmartContractInsertValues {
14611452
}
14621453

14631454
export interface DbChainTip {
1464-
blockHeight: number;
1465-
blockHash: string;
1466-
indexBlockHash: string;
1467-
burnBlockHeight: number;
1455+
block_height: number;
1456+
block_count: number;
1457+
block_hash: string;
1458+
index_block_hash: string;
1459+
burn_block_height: number;
1460+
microblock_hash?: string;
1461+
microblock_sequence?: number;
1462+
microblock_count: number;
1463+
tx_count: number;
1464+
tx_count_unanchored: number;
14681465
}
14691466

14701467
export enum IndexesState {

src/datastore/pg-store.ts

Lines changed: 20 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -202,26 +202,20 @@ export class PgStore extends BasePgStore {
202202
});
203203
}
204204

205-
async getChainTip(sql: PgSqlClient): Promise<{
206-
blockHeight: number;
207-
blockHash: string;
208-
indexBlockHash: string;
209-
burnBlockHeight: number;
210-
}> {
211-
const currentTipBlock = await sql<
212-
{
213-
block_height: number;
214-
block_hash: string;
215-
index_block_hash: string;
216-
burn_block_height: number;
217-
}[]
218-
>`SELECT block_height, block_hash, index_block_hash, burn_block_height FROM chain_tip`;
219-
const height = currentTipBlock[0]?.block_height ?? 0;
205+
async getChainTip(): Promise<DbChainTip> {
206+
const tipResult = await this.sql<DbChainTip[]>`SELECT * FROM chain_tip`;
207+
const tip = tipResult[0];
220208
return {
221-
blockHeight: height,
222-
blockHash: currentTipBlock[0]?.block_hash ?? '',
223-
indexBlockHash: currentTipBlock[0]?.index_block_hash ?? '',
224-
burnBlockHeight: currentTipBlock[0]?.burn_block_height ?? 0,
209+
block_height: tip?.block_height ?? 0,
210+
block_count: tip?.block_count ?? 0,
211+
block_hash: tip?.block_hash ?? '',
212+
index_block_hash: tip?.index_block_hash ?? '',
213+
burn_block_height: tip?.burn_block_height ?? 0,
214+
microblock_hash: tip?.microblock_hash ?? undefined,
215+
microblock_sequence: tip?.microblock_sequence ?? undefined,
216+
microblock_count: tip?.microblock_count ?? 0,
217+
tx_count: tip?.tx_count ?? 0,
218+
tx_count_unanchored: tip?.tx_count_unanchored ?? 0,
225219
};
226220
}
227221

@@ -316,33 +310,6 @@ export class PgStore extends BasePgStore {
316310
return this.getPoxForcedUnlockHeightsInternal(this.sql);
317311
}
318312

319-
async getUnanchoredChainTip(): Promise<FoundOrNot<DbChainTip>> {
320-
const result = await this.sql<
321-
{
322-
block_height: number;
323-
index_block_hash: string;
324-
block_hash: string;
325-
microblock_hash: string | null;
326-
microblock_sequence: number | null;
327-
burn_block_height: number;
328-
}[]
329-
>`SELECT block_height, index_block_hash, block_hash, microblock_hash, microblock_sequence, burn_block_height
330-
FROM chain_tip`;
331-
if (result.length === 0) {
332-
return { found: false } as const;
333-
}
334-
const row = result[0];
335-
const chainTipResult: DbChainTip = {
336-
blockHeight: row.block_height,
337-
indexBlockHash: row.index_block_hash,
338-
blockHash: row.block_hash,
339-
microblockHash: row.microblock_hash === null ? undefined : row.microblock_hash,
340-
microblockSequence: row.microblock_sequence === null ? undefined : row.microblock_sequence,
341-
burnBlockHeight: row.burn_block_height,
342-
};
343-
return { found: true, result: chainTipResult };
344-
}
345-
346313
async getBlock(blockIdentifer: BlockIdentifier): Promise<FoundOrNot<DbBlock>> {
347314
return this.getBlockInternal(this.sql, blockIdentifer);
348315
}
@@ -626,8 +593,8 @@ export class PgStore extends BasePgStore {
626593

627594
async getUnanchoredTxsInternal(sql: PgSqlClient): Promise<{ txs: DbTx[] }> {
628595
// Get transactions that have been streamed in microblocks but not yet accepted or rejected in an anchor block.
629-
const { blockHeight } = await this.getChainTip(sql);
630-
const unanchoredBlockHeight = blockHeight + 1;
596+
const { block_height } = await this.getChainTip();
597+
const unanchoredBlockHeight = block_height + 1;
631598
const query = await sql<ContractTxQueryResult[]>`
632599
SELECT ${unsafeCols(sql, [...TX_COLUMNS, abiColumn()])}
633600
FROM txs
@@ -1372,11 +1339,11 @@ export class PgStore extends BasePgStore {
13721339
sql: PgSqlClient,
13731340
{ includeUnanchored }: { includeUnanchored: boolean }
13741341
): Promise<number> {
1375-
const chainTip = await this.getChainTip(sql);
1342+
const chainTip = await this.getChainTip();
13761343
if (includeUnanchored) {
1377-
return chainTip.blockHeight + 1;
1344+
return chainTip.block_height + 1;
13781345
} else {
1379-
return chainTip.blockHeight;
1346+
return chainTip.block_height;
13801347
}
13811348
}
13821349

@@ -2159,9 +2126,9 @@ export class PgStore extends BasePgStore {
21592126

21602127
async getStxBalanceAtBlock(stxAddress: string, blockHeight: number): Promise<DbStxBalance> {
21612128
return await this.sqlTransaction(async sql => {
2162-
const chainTip = await this.getChainTip(sql);
2129+
const chainTip = await this.getChainTip();
21632130
const blockHeightToQuery =
2164-
blockHeight > chainTip.blockHeight ? chainTip.blockHeight : blockHeight;
2131+
blockHeight > chainTip.block_height ? chainTip.block_height : blockHeight;
21652132
const blockQuery = await this.getBlockByHeightInternal(sql, blockHeightToQuery);
21662133
if (!blockQuery.found) {
21672134
throw new Error(`Could not find block at height: ${blockHeight}`);

0 commit comments

Comments
 (0)