mirror of
https://github.com/alexgo-io/stacks-blockchain-api.git
synced 2026-06-10 23:19:35 +08:00
fix: convert chain_tip materialized view into a table (#1751)
* feat: chain tip table * fix: handle reorgs
This commit is contained in:
142
migrations/1700071472495_chain-tip-table.js
Normal file
142
migrations/1700071472495_chain-tip-table.js
Normal file
@@ -0,0 +1,142 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
exports.shorthands = undefined;
|
||||
|
||||
exports.up = pgm => {
|
||||
pgm.dropMaterializedView('chain_tip');
|
||||
pgm.createTable('chain_tip', {
|
||||
id: {
|
||||
type: 'bool',
|
||||
primaryKey: true,
|
||||
default: true,
|
||||
},
|
||||
block_height: {
|
||||
type: 'integer',
|
||||
notNull: true,
|
||||
},
|
||||
block_count: {
|
||||
type: 'integer',
|
||||
notNull: true,
|
||||
},
|
||||
block_hash: {
|
||||
type: 'bytea',
|
||||
notNull: true,
|
||||
},
|
||||
index_block_hash: {
|
||||
type: 'bytea',
|
||||
notNull: true,
|
||||
},
|
||||
burn_block_height: {
|
||||
type: 'integer',
|
||||
notNull: true,
|
||||
},
|
||||
microblock_hash: {
|
||||
type: 'bytea',
|
||||
},
|
||||
microblock_sequence: {
|
||||
type: 'integer',
|
||||
},
|
||||
microblock_count: {
|
||||
type: 'integer',
|
||||
notNull: true,
|
||||
},
|
||||
tx_count: {
|
||||
type: 'integer',
|
||||
notNull: true,
|
||||
},
|
||||
tx_count_unanchored: {
|
||||
type: 'integer',
|
||||
notNull: true,
|
||||
},
|
||||
});
|
||||
pgm.addConstraint('chain_tip', 'chain_tip_one_row', 'CHECK(id)');
|
||||
pgm.sql(`
|
||||
WITH block_tip AS (
|
||||
SELECT block_height, block_hash, index_block_hash, burn_block_height
|
||||
FROM blocks
|
||||
WHERE block_height = (SELECT MAX(block_height) FROM blocks WHERE canonical = TRUE)
|
||||
),
|
||||
microblock_tip AS (
|
||||
SELECT microblock_hash, microblock_sequence
|
||||
FROM microblocks, block_tip
|
||||
WHERE microblocks.parent_index_block_hash = block_tip.index_block_hash
|
||||
AND microblock_canonical = true AND canonical = true
|
||||
ORDER BY microblock_sequence DESC
|
||||
LIMIT 1
|
||||
),
|
||||
microblock_count AS (
|
||||
SELECT COUNT(*)::INTEGER AS microblock_count
|
||||
FROM microblocks
|
||||
WHERE canonical = TRUE AND microblock_canonical = TRUE
|
||||
),
|
||||
tx_count AS (
|
||||
SELECT COUNT(*)::INTEGER AS tx_count
|
||||
FROM txs
|
||||
WHERE canonical = TRUE AND microblock_canonical = TRUE
|
||||
AND block_height <= (SELECT MAX(block_height) FROM blocks WHERE canonical = TRUE)
|
||||
),
|
||||
tx_count_unanchored AS (
|
||||
SELECT COUNT(*)::INTEGER AS tx_count_unanchored
|
||||
FROM txs
|
||||
WHERE canonical = TRUE AND microblock_canonical = TRUE
|
||||
)
|
||||
INSERT INTO chain_tip (block_height, block_hash, index_block_hash, burn_block_height,
|
||||
block_count, microblock_hash, microblock_sequence, microblock_count, tx_count,
|
||||
tx_count_unanchored)
|
||||
VALUES (
|
||||
COALESCE((SELECT block_height FROM block_tip), 0),
|
||||
COALESCE((SELECT block_hash FROM block_tip), ''),
|
||||
COALESCE((SELECT index_block_hash FROM block_tip), ''),
|
||||
COALESCE((SELECT burn_block_height FROM block_tip), 0),
|
||||
COALESCE((SELECT block_height FROM block_tip), 0),
|
||||
(SELECT microblock_hash FROM microblock_tip),
|
||||
(SELECT microblock_sequence FROM microblock_tip),
|
||||
COALESCE((SELECT microblock_count FROM microblock_count), 0),
|
||||
COALESCE((SELECT tx_count FROM tx_count), 0),
|
||||
COALESCE((SELECT tx_count_unanchored FROM tx_count_unanchored), 0)
|
||||
)
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = pgm => {
|
||||
pgm.dropTable('chain_tip');
|
||||
pgm.createMaterializedView('chain_tip', {}, `
|
||||
WITH block_tip AS (
|
||||
SELECT block_height, block_hash, index_block_hash, burn_block_height
|
||||
FROM blocks
|
||||
WHERE block_height = (SELECT MAX(block_height) FROM blocks WHERE canonical = TRUE)
|
||||
),
|
||||
microblock_tip AS (
|
||||
SELECT microblock_hash, microblock_sequence
|
||||
FROM microblocks, block_tip
|
||||
WHERE microblocks.parent_index_block_hash = block_tip.index_block_hash
|
||||
AND microblock_canonical = true AND canonical = true
|
||||
ORDER BY microblock_sequence DESC
|
||||
LIMIT 1
|
||||
),
|
||||
microblock_count AS (
|
||||
SELECT COUNT(*)::INTEGER AS microblock_count
|
||||
FROM microblocks
|
||||
WHERE canonical = TRUE AND microblock_canonical = TRUE
|
||||
),
|
||||
tx_count AS (
|
||||
SELECT COUNT(*)::INTEGER AS tx_count
|
||||
FROM txs
|
||||
WHERE canonical = TRUE AND microblock_canonical = TRUE
|
||||
AND block_height <= (SELECT MAX(block_height) FROM blocks WHERE canonical = TRUE)
|
||||
),
|
||||
tx_count_unanchored AS (
|
||||
SELECT COUNT(*)::INTEGER AS tx_count_unanchored
|
||||
FROM txs
|
||||
WHERE canonical = TRUE AND microblock_canonical = TRUE
|
||||
)
|
||||
SELECT *, block_tip.block_height AS block_count
|
||||
FROM block_tip
|
||||
LEFT JOIN microblock_tip ON TRUE
|
||||
LEFT JOIN microblock_count ON TRUE
|
||||
LEFT JOIN tx_count ON TRUE
|
||||
LEFT JOIN tx_count_unanchored ON TRUE
|
||||
LIMIT 1
|
||||
`);
|
||||
pgm.createIndex('chain_tip', 'block_height', { unique: true });
|
||||
};
|
||||
@@ -252,13 +252,13 @@ async function calculateETag(
|
||||
switch (etagType) {
|
||||
case ETagType.chainTip:
|
||||
try {
|
||||
const chainTip = await db.getUnanchoredChainTip();
|
||||
if (!chainTip.found) {
|
||||
const chainTip = await db.getChainTip();
|
||||
if (chainTip.block_height === 0) {
|
||||
// This should never happen unless the API is serving requests before it has synced any
|
||||
// blocks.
|
||||
return;
|
||||
}
|
||||
return chainTip.result.microblockHash ?? chainTip.result.indexBlockHash;
|
||||
return chainTip.microblock_hash ?? chainTip.index_block_hash;
|
||||
} catch (error) {
|
||||
logger.error(error, 'Unable to calculate chain_tip ETag');
|
||||
return;
|
||||
|
||||
@@ -18,15 +18,15 @@ export function createStatusRouter(db: PgStore): express.Router {
|
||||
response.pox_v1_unlock_height = poxForceUnlockHeights.result.pox1UnlockHeight as number;
|
||||
response.pox_v2_unlock_height = poxForceUnlockHeights.result.pox2UnlockHeight as number;
|
||||
}
|
||||
const chainTip = await db.getUnanchoredChainTip();
|
||||
if (chainTip.found) {
|
||||
const chainTip = await db.getChainTip();
|
||||
if (chainTip.block_height > 0) {
|
||||
response.chain_tip = {
|
||||
block_height: chainTip.result.blockHeight,
|
||||
block_hash: chainTip.result.blockHash,
|
||||
index_block_hash: chainTip.result.indexBlockHash,
|
||||
microblock_hash: chainTip.result.microblockHash,
|
||||
microblock_sequence: chainTip.result.microblockSequence,
|
||||
burn_block_height: chainTip.result.burnBlockHeight,
|
||||
block_height: chainTip.block_height,
|
||||
block_hash: chainTip.block_hash,
|
||||
index_block_hash: chainTip.index_block_hash,
|
||||
microblock_hash: chainTip.microblock_hash,
|
||||
microblock_sequence: chainTip.microblock_sequence,
|
||||
burn_block_height: chainTip.burn_block_height,
|
||||
};
|
||||
}
|
||||
setETagCacheHeaders(res);
|
||||
|
||||
@@ -744,15 +744,6 @@ export type BlockIdentifier =
|
||||
| { burnBlockHash: string }
|
||||
| { burnBlockHeight: number };
|
||||
|
||||
export interface DbChainTip {
|
||||
blockHeight: number;
|
||||
indexBlockHash: string;
|
||||
blockHash: string;
|
||||
microblockHash?: string;
|
||||
microblockSequence?: number;
|
||||
burnBlockHeight: number;
|
||||
}
|
||||
|
||||
export interface BlockQueryResult {
|
||||
block_hash: string;
|
||||
index_block_hash: string;
|
||||
@@ -1461,10 +1452,16 @@ export interface SmartContractInsertValues {
|
||||
}
|
||||
|
||||
export interface DbChainTip {
|
||||
blockHeight: number;
|
||||
blockHash: string;
|
||||
indexBlockHash: string;
|
||||
burnBlockHeight: number;
|
||||
block_height: number;
|
||||
block_count: number;
|
||||
block_hash: string;
|
||||
index_block_hash: string;
|
||||
burn_block_height: number;
|
||||
microblock_hash?: string;
|
||||
microblock_sequence?: number;
|
||||
microblock_count: number;
|
||||
tx_count: number;
|
||||
tx_count_unanchored: number;
|
||||
}
|
||||
|
||||
export enum IndexesState {
|
||||
|
||||
@@ -202,26 +202,20 @@ export class PgStore extends BasePgStore {
|
||||
});
|
||||
}
|
||||
|
||||
async getChainTip(sql: PgSqlClient): Promise<{
|
||||
blockHeight: number;
|
||||
blockHash: string;
|
||||
indexBlockHash: string;
|
||||
burnBlockHeight: number;
|
||||
}> {
|
||||
const currentTipBlock = await sql<
|
||||
{
|
||||
block_height: number;
|
||||
block_hash: string;
|
||||
index_block_hash: string;
|
||||
burn_block_height: number;
|
||||
}[]
|
||||
>`SELECT block_height, block_hash, index_block_hash, burn_block_height FROM chain_tip`;
|
||||
const height = currentTipBlock[0]?.block_height ?? 0;
|
||||
async getChainTip(): Promise<DbChainTip> {
|
||||
const tipResult = await this.sql<DbChainTip[]>`SELECT * FROM chain_tip`;
|
||||
const tip = tipResult[0];
|
||||
return {
|
||||
blockHeight: height,
|
||||
blockHash: currentTipBlock[0]?.block_hash ?? '',
|
||||
indexBlockHash: currentTipBlock[0]?.index_block_hash ?? '',
|
||||
burnBlockHeight: currentTipBlock[0]?.burn_block_height ?? 0,
|
||||
block_height: tip?.block_height ?? 0,
|
||||
block_count: tip?.block_count ?? 0,
|
||||
block_hash: tip?.block_hash ?? '',
|
||||
index_block_hash: tip?.index_block_hash ?? '',
|
||||
burn_block_height: tip?.burn_block_height ?? 0,
|
||||
microblock_hash: tip?.microblock_hash ?? undefined,
|
||||
microblock_sequence: tip?.microblock_sequence ?? undefined,
|
||||
microblock_count: tip?.microblock_count ?? 0,
|
||||
tx_count: tip?.tx_count ?? 0,
|
||||
tx_count_unanchored: tip?.tx_count_unanchored ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -316,33 +310,6 @@ export class PgStore extends BasePgStore {
|
||||
return this.getPoxForcedUnlockHeightsInternal(this.sql);
|
||||
}
|
||||
|
||||
async getUnanchoredChainTip(): Promise<FoundOrNot<DbChainTip>> {
|
||||
const result = await this.sql<
|
||||
{
|
||||
block_height: number;
|
||||
index_block_hash: string;
|
||||
block_hash: string;
|
||||
microblock_hash: string | null;
|
||||
microblock_sequence: number | null;
|
||||
burn_block_height: number;
|
||||
}[]
|
||||
>`SELECT block_height, index_block_hash, block_hash, microblock_hash, microblock_sequence, burn_block_height
|
||||
FROM chain_tip`;
|
||||
if (result.length === 0) {
|
||||
return { found: false } as const;
|
||||
}
|
||||
const row = result[0];
|
||||
const chainTipResult: DbChainTip = {
|
||||
blockHeight: row.block_height,
|
||||
indexBlockHash: row.index_block_hash,
|
||||
blockHash: row.block_hash,
|
||||
microblockHash: row.microblock_hash === null ? undefined : row.microblock_hash,
|
||||
microblockSequence: row.microblock_sequence === null ? undefined : row.microblock_sequence,
|
||||
burnBlockHeight: row.burn_block_height,
|
||||
};
|
||||
return { found: true, result: chainTipResult };
|
||||
}
|
||||
|
||||
async getBlock(blockIdentifer: BlockIdentifier): Promise<FoundOrNot<DbBlock>> {
|
||||
return this.getBlockInternal(this.sql, blockIdentifer);
|
||||
}
|
||||
@@ -626,8 +593,8 @@ export class PgStore extends BasePgStore {
|
||||
|
||||
async getUnanchoredTxsInternal(sql: PgSqlClient): Promise<{ txs: DbTx[] }> {
|
||||
// Get transactions that have been streamed in microblocks but not yet accepted or rejected in an anchor block.
|
||||
const { blockHeight } = await this.getChainTip(sql);
|
||||
const unanchoredBlockHeight = blockHeight + 1;
|
||||
const { block_height } = await this.getChainTip();
|
||||
const unanchoredBlockHeight = block_height + 1;
|
||||
const query = await sql<ContractTxQueryResult[]>`
|
||||
SELECT ${unsafeCols(sql, [...TX_COLUMNS, abiColumn()])}
|
||||
FROM txs
|
||||
@@ -1372,11 +1339,11 @@ export class PgStore extends BasePgStore {
|
||||
sql: PgSqlClient,
|
||||
{ includeUnanchored }: { includeUnanchored: boolean }
|
||||
): Promise<number> {
|
||||
const chainTip = await this.getChainTip(sql);
|
||||
const chainTip = await this.getChainTip();
|
||||
if (includeUnanchored) {
|
||||
return chainTip.blockHeight + 1;
|
||||
return chainTip.block_height + 1;
|
||||
} else {
|
||||
return chainTip.blockHeight;
|
||||
return chainTip.block_height;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2159,9 +2126,9 @@ export class PgStore extends BasePgStore {
|
||||
|
||||
async getStxBalanceAtBlock(stxAddress: string, blockHeight: number): Promise<DbStxBalance> {
|
||||
return await this.sqlTransaction(async sql => {
|
||||
const chainTip = await this.getChainTip(sql);
|
||||
const chainTip = await this.getChainTip();
|
||||
const blockHeightToQuery =
|
||||
blockHeight > chainTip.blockHeight ? chainTip.blockHeight : blockHeight;
|
||||
blockHeight > chainTip.block_height ? chainTip.block_height : blockHeight;
|
||||
const blockQuery = await this.getBlockByHeightInternal(sql, blockHeightToQuery);
|
||||
if (!blockQuery.found) {
|
||||
throw new Error(`Could not find block at height: ${blockHeight}`);
|
||||
|
||||
@@ -141,33 +141,6 @@ export class PgWriteStore extends PgStore {
|
||||
return store;
|
||||
}
|
||||
|
||||
async getChainTip(sql: PgSqlClient, useMaterializedView = true): Promise<DbChainTip> {
|
||||
if (!this.isEventReplay && useMaterializedView) {
|
||||
return super.getChainTip(sql);
|
||||
}
|
||||
// The `chain_tip` materialized view is not available during event replay.
|
||||
// Since `getChainTip()` is used heavily during event ingestion, we'll fall back to
|
||||
// a classic query.
|
||||
const currentTipBlock = await sql<
|
||||
{
|
||||
block_height: number;
|
||||
block_hash: string;
|
||||
index_block_hash: string;
|
||||
burn_block_height: number;
|
||||
}[]
|
||||
>`
|
||||
SELECT block_height, block_hash, index_block_hash, burn_block_height
|
||||
FROM blocks
|
||||
WHERE canonical = true AND block_height = (SELECT MAX(block_height) FROM blocks)
|
||||
`;
|
||||
return {
|
||||
blockHeight: currentTipBlock[0]?.block_height ?? 0,
|
||||
blockHash: currentTipBlock[0]?.block_hash ?? '',
|
||||
indexBlockHash: currentTipBlock[0]?.index_block_hash ?? '',
|
||||
burnBlockHeight: currentTipBlock[0]?.burn_block_height ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
async storeRawEventRequest(eventPath: string, payload: PgJsonb): Promise<void> {
|
||||
// To avoid depending on the DB more than once and to allow the query transaction to settle,
|
||||
// we'll take the complete insert result and move that to the output TSV file instead of taking
|
||||
@@ -198,10 +171,10 @@ export class PgWriteStore extends PgStore {
|
||||
const contractLogEvents: DbSmartContractEvent[] = [];
|
||||
|
||||
await this.sqlWriteTransaction(async sql => {
|
||||
const chainTip = await this.getChainTip(sql, false);
|
||||
await this.handleReorg(sql, data.block, chainTip.blockHeight);
|
||||
const chainTip = await this.getChainTip();
|
||||
await this.handleReorg(sql, data.block, chainTip.block_height);
|
||||
// If the incoming block is not of greater height than current chain tip, then store data as non-canonical.
|
||||
const isCanonical = data.block.block_height > chainTip.blockHeight;
|
||||
const isCanonical = data.block.block_height > chainTip.block_height;
|
||||
if (!isCanonical) {
|
||||
data.block = { ...data.block, canonical: false };
|
||||
data.microblocks = data.microblocks.map(mb => ({ ...mb, canonical: false }));
|
||||
@@ -417,12 +390,27 @@ export class PgWriteStore extends PgStore {
|
||||
const mempoolStats = await this.getMempoolStatsInternal({ sql });
|
||||
this.eventEmitter.emit('mempoolStatsUpdate', mempoolStats);
|
||||
}
|
||||
if (isCanonical)
|
||||
await sql`
|
||||
WITH new_tx_count AS (
|
||||
SELECT tx_count + ${data.txs.length} AS tx_count FROM chain_tip
|
||||
)
|
||||
UPDATE chain_tip SET
|
||||
block_height = ${data.block.block_height},
|
||||
block_hash = ${data.block.block_hash},
|
||||
index_block_hash = ${data.block.index_block_hash},
|
||||
burn_block_height = ${data.block.burn_block_height},
|
||||
microblock_hash = NULL,
|
||||
microblock_sequence = NULL,
|
||||
block_count = ${data.block.block_height},
|
||||
tx_count = (SELECT tx_count FROM new_tx_count),
|
||||
tx_count_unanchored = (SELECT tx_count FROM new_tx_count)
|
||||
`;
|
||||
});
|
||||
// Do we have an IBD height defined in ENV? If so, check if this block update reached it.
|
||||
const ibdHeight = getIbdBlockHeight();
|
||||
this.isIbdBlockHeightReached = ibdHeight ? data.block.block_height > ibdHeight : true;
|
||||
|
||||
await this.refreshMaterializedView('chain_tip');
|
||||
await this.refreshMaterializedView('mempool_digest');
|
||||
|
||||
// Skip sending `PgNotifier` updates altogether if we're in the genesis block since this block is the
|
||||
@@ -597,12 +585,12 @@ export class PgWriteStore extends PgStore {
|
||||
const contractLogEvents: DbSmartContractEvent[] = [];
|
||||
|
||||
await this.sqlWriteTransaction(async sql => {
|
||||
// Sanity check: ensure incoming microblocks have a `parent_index_block_hash` that matches the API's
|
||||
// current known canonical chain tip. We assume this holds true so incoming microblock data is always
|
||||
// treated as being built off the current canonical anchor block.
|
||||
const chainTip = await this.getChainTip(sql, false);
|
||||
// Sanity check: ensure incoming microblocks have a `parent_index_block_hash` that matches the
|
||||
// API's current known canonical chain tip. We assume this holds true so incoming microblock
|
||||
// data is always treated as being built off the current canonical anchor block.
|
||||
const chainTip = await this.getChainTip();
|
||||
const nonCanonicalMicroblock = data.microblocks.find(
|
||||
mb => mb.parent_index_block_hash !== chainTip.indexBlockHash
|
||||
mb => mb.parent_index_block_hash !== chainTip.index_block_hash
|
||||
);
|
||||
// Note: the stacks-node event emitter can send old microblocks that have already been processed by a previous anchor block.
|
||||
// Log warning and return, nothing to do.
|
||||
@@ -610,13 +598,13 @@ export class PgWriteStore extends PgStore {
|
||||
logger.info(
|
||||
`Failure in microblock ingestion, microblock ${nonCanonicalMicroblock.microblock_hash} ` +
|
||||
`points to parent index block hash ${nonCanonicalMicroblock.parent_index_block_hash} rather ` +
|
||||
`than the current canonical tip's index block hash ${chainTip.indexBlockHash}.`
|
||||
`than the current canonical tip's index block hash ${chainTip.index_block_hash}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// The block height is just one after the current chain tip height
|
||||
const blockHeight = chainTip.blockHeight + 1;
|
||||
const blockHeight = chainTip.block_height + 1;
|
||||
dbMicroblocks = data.microblocks.map(mb => {
|
||||
const dbMicroBlock: DbMicroblock = {
|
||||
canonical: true,
|
||||
@@ -629,8 +617,8 @@ export class PgWriteStore extends PgStore {
|
||||
parent_burn_block_hash: mb.parent_burn_block_hash,
|
||||
parent_burn_block_time: mb.parent_burn_block_time,
|
||||
block_height: blockHeight,
|
||||
parent_block_height: chainTip.blockHeight,
|
||||
parent_block_hash: chainTip.blockHash,
|
||||
parent_block_height: chainTip.block_height,
|
||||
parent_block_hash: chainTip.block_hash,
|
||||
index_block_hash: '', // Empty until microblock is confirmed in an anchor block
|
||||
block_hash: '', // Empty until microblock is confirmed in an anchor block
|
||||
};
|
||||
@@ -642,7 +630,7 @@ export class PgWriteStore extends PgStore {
|
||||
// block with that data doesn't yet exist.
|
||||
const dbTx: DbTxRaw = {
|
||||
...entry.tx,
|
||||
parent_block_hash: chainTip.blockHash,
|
||||
parent_block_hash: chainTip.block_hash,
|
||||
block_height: blockHeight,
|
||||
};
|
||||
|
||||
@@ -722,9 +710,20 @@ export class PgWriteStore extends PgStore {
|
||||
const mempoolStats = await this.getMempoolStatsInternal({ sql });
|
||||
this.eventEmitter.emit('mempoolStatsUpdate', mempoolStats);
|
||||
}
|
||||
if (currentMicroblockTip.microblock_canonical)
|
||||
await sql`
|
||||
UPDATE chain_tip SET
|
||||
microblock_hash = ${currentMicroblockTip.microblock_hash},
|
||||
microblock_sequence = ${currentMicroblockTip.microblock_sequence},
|
||||
microblock_count = microblock_count + ${data.microblocks.length},
|
||||
tx_count_unanchored = ${
|
||||
currentMicroblockTip.microblock_sequence === 0
|
||||
? sql`tx_count + ${data.txs.length}`
|
||||
: sql`tx_count_unanchored + ${data.txs.length}`
|
||||
}
|
||||
`;
|
||||
});
|
||||
|
||||
await this.refreshMaterializedView('chain_tip');
|
||||
await this.refreshMaterializedView('mempool_digest');
|
||||
|
||||
if (this.notifier) {
|
||||
@@ -1728,7 +1727,7 @@ export class PgWriteStore extends PgStore {
|
||||
anchor_mode: tx.anchor_mode,
|
||||
status: tx.status,
|
||||
receipt_time: tx.receipt_time,
|
||||
receipt_block_height: chainTip.blockHeight,
|
||||
receipt_block_height: chainTip.block_height,
|
||||
post_conditions: tx.post_conditions,
|
||||
nonce: tx.nonce,
|
||||
fee_rate: tx.fee_rate,
|
||||
@@ -1767,7 +1766,7 @@ export class PgWriteStore extends PgStore {
|
||||
async updateMempoolTxs({ mempoolTxs: txs }: { mempoolTxs: DbMempoolTxRaw[] }): Promise<void> {
|
||||
const updatedTxIds: string[] = [];
|
||||
await this.sqlWriteTransaction(async sql => {
|
||||
const chainTip = await this.getChainTip(sql, false);
|
||||
const chainTip = await this.getChainTip();
|
||||
for (const tx of txs) {
|
||||
const inserted = await this.insertDbMempoolTx(tx, chainTip, sql);
|
||||
if (inserted) {
|
||||
@@ -2245,6 +2244,12 @@ export class PgWriteStore extends PgStore {
|
||||
});
|
||||
}
|
||||
|
||||
// Update unanchored tx count in `chain_tip` table
|
||||
const txCountDelta = updatedMbTxs.length * (args.isMicroCanonical ? 1 : -1);
|
||||
await sql`
|
||||
UPDATE chain_tip SET tx_count_unanchored = tx_count_unanchored + ${txCountDelta}
|
||||
`;
|
||||
|
||||
return { updatedTxs: updatedMbTxs };
|
||||
}
|
||||
|
||||
@@ -2860,6 +2865,14 @@ export class PgWriteStore extends PgStore {
|
||||
await this.restoreOrphanedChain(sql, parentResult[0].index_block_hash, updatedEntities);
|
||||
this.logReorgResultInfo(updatedEntities);
|
||||
}
|
||||
// Reflect updated transaction totals in `chain_tip` table.
|
||||
const txCountDelta =
|
||||
updatedEntities.markedCanonical.txs - updatedEntities.markedNonCanonical.txs;
|
||||
await sql`
|
||||
UPDATE chain_tip SET
|
||||
tx_count = tx_count + ${txCountDelta},
|
||||
tx_count_unanchored = tx_count_unanchored + ${txCountDelta}
|
||||
`;
|
||||
}
|
||||
return updatedEntities;
|
||||
}
|
||||
@@ -2944,13 +2957,8 @@ export class PgWriteStore extends PgStore {
|
||||
* Called when a full event import is complete.
|
||||
*/
|
||||
async finishEventReplay() {
|
||||
if (!this.isEventReplay) {
|
||||
return;
|
||||
}
|
||||
await this.sqlWriteTransaction(async sql => {
|
||||
await this.refreshMaterializedView('chain_tip', sql, false);
|
||||
await this.refreshMaterializedView('mempool_digest', sql, false);
|
||||
});
|
||||
if (!this.isEventReplay) return;
|
||||
await this.refreshMaterializedView('mempool_digest', this.sql, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -845,8 +845,8 @@ export async function startEventServer(opts: {
|
||||
if (ibdHeight) {
|
||||
app.use(IBD_PRUNABLE_ROUTES, async (req, res, next) => {
|
||||
try {
|
||||
const chainTip = await db.getChainTip(db.sql, false);
|
||||
if (chainTip.blockHeight > ibdHeight) {
|
||||
const chainTip = await db.getChainTip();
|
||||
if (chainTip.block_height > ibdHeight) {
|
||||
next();
|
||||
} else {
|
||||
handleRawEventRequest(req, res, next);
|
||||
|
||||
@@ -28,13 +28,12 @@ describe('import/export tests', () => {
|
||||
test('event import and export cycle', async () => {
|
||||
// Import from mocknet TSV
|
||||
await importEventsFromTsv('src/tests-event-replay/tsv/mocknet.tsv', 'archival', true, true);
|
||||
const chainTip = await db.getUnanchoredChainTip();
|
||||
expect(chainTip.found).toBe(true);
|
||||
expect(chainTip.result?.blockHeight).toBe(28);
|
||||
expect(chainTip.result?.indexBlockHash).toBe(
|
||||
const chainTip = await db.getChainTip();
|
||||
expect(chainTip.block_height).toBe(28);
|
||||
expect(chainTip.index_block_hash).toBe(
|
||||
'0x76cd67a65c0dfd5ea450bb9efe30da89fa125bfc077c953802f718353283a533'
|
||||
);
|
||||
expect(chainTip.result?.blockHash).toBe(
|
||||
expect(chainTip.block_hash).toBe(
|
||||
'0x7682af212d3c1ef62613412f9b5a727269b4548f14eca2e3f941f7ad8b3c11b2'
|
||||
);
|
||||
|
||||
@@ -51,13 +50,12 @@ describe('import/export tests', () => {
|
||||
// Re-import with exported TSV and check that chain tip matches.
|
||||
try {
|
||||
await importEventsFromTsv(`${tmpDir}/export.tsv`, 'archival', true, true);
|
||||
const newChainTip = await db.getUnanchoredChainTip();
|
||||
expect(newChainTip.found).toBe(true);
|
||||
expect(newChainTip.result?.blockHeight).toBe(28);
|
||||
expect(newChainTip.result?.indexBlockHash).toBe(
|
||||
const newChainTip = await db.getChainTip();
|
||||
expect(newChainTip.block_height).toBe(28);
|
||||
expect(newChainTip.index_block_hash).toBe(
|
||||
'0x76cd67a65c0dfd5ea450bb9efe30da89fa125bfc077c953802f718353283a533'
|
||||
);
|
||||
expect(newChainTip.result?.blockHash).toBe(
|
||||
expect(newChainTip.block_hash).toBe(
|
||||
'0x7682af212d3c1ef62613412f9b5a727269b4548f14eca2e3f941f7ad8b3c11b2'
|
||||
);
|
||||
} finally {
|
||||
@@ -198,30 +196,14 @@ describe('IBD', () => {
|
||||
process.env.IBD_MODE_UNTIL_BLOCK = '1000';
|
||||
// TSV has 1 microblock message.
|
||||
await expect(getIbdInterceptCountFromTsvEvents()).resolves.toBe(1);
|
||||
await expect(db.getChainTip(client, false)).resolves.toHaveProperty('blockHeight', 28);
|
||||
await expect(db.getChainTip()).resolves.toHaveProperty('block_height', 28);
|
||||
});
|
||||
|
||||
test('IBD mode does NOT block certain API routes once the threshold number of blocks are ingested', async () => {
|
||||
process.env.IBD_MODE_UNTIL_BLOCK = '1';
|
||||
// Microblock processed normally.
|
||||
await expect(getIbdInterceptCountFromTsvEvents()).resolves.toBe(0);
|
||||
await expect(db.getChainTip(client, false)).resolves.toHaveProperty('blockHeight', 28);
|
||||
});
|
||||
|
||||
test('IBD mode prevents refreshing materialized views', async () => {
|
||||
process.env.IBD_MODE_UNTIL_BLOCK = '1000';
|
||||
await getIbdInterceptCountFromTsvEvents();
|
||||
await db.refreshMaterializedView('chain_tip', client);
|
||||
const res = await db.sql<{ block_height: number }[]>`SELECT * FROM chain_tip`;
|
||||
expect(res.count).toBe(0);
|
||||
});
|
||||
|
||||
test('IBD mode allows refreshing materialized views after height has passed', async () => {
|
||||
process.env.IBD_MODE_UNTIL_BLOCK = '10';
|
||||
await getIbdInterceptCountFromTsvEvents();
|
||||
await db.refreshMaterializedView('chain_tip', client);
|
||||
const res = await db.sql<{ block_height: number }[]>`SELECT * FROM chain_tip`;
|
||||
expect(res[0].block_height).toBe(28);
|
||||
await expect(db.getChainTip()).resolves.toHaveProperty('block_height', 28);
|
||||
});
|
||||
|
||||
test('IBD mode covers prune mode', async () => {
|
||||
|
||||
@@ -25,22 +25,21 @@ describe('poison microblock for height 80743', () => {
|
||||
true
|
||||
);
|
||||
const poisonTxId = '0x58ffe62029f94f7101b959536ea4953b9bce0ec3f6e2a06254c511bdd5cfa9e7';
|
||||
const chainTip = await db.getUnanchoredChainTip();
|
||||
const chainTip = await db.getChainTip();
|
||||
// query the txs table and check the transaction type
|
||||
const searchResult = await db.searchHash({ hash: poisonTxId });
|
||||
let entityData: any;
|
||||
if (searchResult.result?.entity_data) {
|
||||
entityData = searchResult.result?.entity_data;
|
||||
}
|
||||
expect(chainTip.found).toBe(true);
|
||||
// check the transaction type to be contract call for this poison block
|
||||
expect(entityData.type_id).toBe(DbTxTypeId.ContractCall);
|
||||
expect(searchResult.found).toBe(true);
|
||||
expect(chainTip.result?.blockHeight).toBe(1);
|
||||
expect(chainTip.result?.indexBlockHash).toBe(
|
||||
expect(chainTip.block_height).toBe(1);
|
||||
expect(chainTip.index_block_hash).toBe(
|
||||
'0x05ca75b9949195da435e6e36d731dbaa10bb75fda576a52263e25164990bfdaa'
|
||||
);
|
||||
expect(chainTip.result?.blockHash).toBe(
|
||||
expect(chainTip.block_hash).toBe(
|
||||
'0x6b83b44571365e6e530d679536578c71d6c376b07666f3671786b6fd8fac049c'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -318,13 +318,12 @@ describe('cache-control tests', () => {
|
||||
],
|
||||
});
|
||||
|
||||
const chainTip2 = await db.getUnanchoredChainTip();
|
||||
expect(chainTip2.found).toBeTruthy();
|
||||
expect(chainTip2.result?.blockHash).toBe(block1.block_hash);
|
||||
expect(chainTip2.result?.blockHeight).toBe(block1.block_height);
|
||||
expect(chainTip2.result?.indexBlockHash).toBe(block1.index_block_hash);
|
||||
expect(chainTip2.result?.microblockHash).toBe(mb1.microblock_hash);
|
||||
expect(chainTip2.result?.microblockSequence).toBe(mb1.microblock_sequence);
|
||||
const chainTip2 = await db.getChainTip();
|
||||
expect(chainTip2.block_hash).toBe(block1.block_hash);
|
||||
expect(chainTip2.block_height).toBe(block1.block_height);
|
||||
expect(chainTip2.index_block_hash).toBe(block1.index_block_hash);
|
||||
expect(chainTip2.microblock_hash).toBe(mb1.microblock_hash);
|
||||
expect(chainTip2.microblock_sequence).toBe(mb1.microblock_sequence);
|
||||
|
||||
const expectedResp2 = {
|
||||
burn_block_time: 1594647996,
|
||||
|
||||
@@ -3733,6 +3733,7 @@ describe('postgres datastore', () => {
|
||||
contract_name: 'pox',
|
||||
};
|
||||
|
||||
// Start canonical chain
|
||||
await db.update({
|
||||
block: block1,
|
||||
microblocks: [],
|
||||
@@ -3953,6 +3954,7 @@ describe('postgres datastore', () => {
|
||||
abi: '{"thing":1}',
|
||||
};
|
||||
|
||||
// Insert non-canonical block
|
||||
await db.update({
|
||||
block: block2b,
|
||||
microblocks: [],
|
||||
@@ -4050,12 +4052,18 @@ describe('postgres datastore', () => {
|
||||
|
||||
const blockQuery1 = await db.getBlock({ hash: block2b.block_hash });
|
||||
expect(blockQuery1.result?.canonical).toBe(false);
|
||||
const chainTip1 = await db.getChainTip(client);
|
||||
const chainTip1 = await db.getChainTip();
|
||||
expect(chainTip1).toEqual({
|
||||
blockHash: '0x33',
|
||||
blockHeight: 3,
|
||||
indexBlockHash: '0xcc',
|
||||
burnBlockHeight: 123,
|
||||
block_hash: '0x33',
|
||||
block_height: 3,
|
||||
index_block_hash: '0xcc',
|
||||
burn_block_height: 123,
|
||||
block_count: 3,
|
||||
microblock_count: 0,
|
||||
microblock_hash: undefined,
|
||||
microblock_sequence: undefined,
|
||||
tx_count: 2, // Tx from block 2b does not count
|
||||
tx_count_unanchored: 2,
|
||||
});
|
||||
const namespaces = await db.getNamespaceList({ includeUnanchored: false });
|
||||
expect(namespaces.results.length).toBe(1);
|
||||
@@ -4109,12 +4117,19 @@ describe('postgres datastore', () => {
|
||||
await db.update({ block: block3b, microblocks: [], minerRewards: [], txs: [] });
|
||||
const blockQuery2 = await db.getBlock({ hash: block3b.block_hash });
|
||||
expect(blockQuery2.result?.canonical).toBe(false);
|
||||
const chainTip2 = await db.getChainTip(client);
|
||||
// Chain tip doesn't change yet.
|
||||
const chainTip2 = await db.getChainTip();
|
||||
expect(chainTip2).toEqual({
|
||||
blockHash: '0x33',
|
||||
blockHeight: 3,
|
||||
indexBlockHash: '0xcc',
|
||||
burnBlockHeight: 123,
|
||||
block_hash: '0x33',
|
||||
block_height: 3,
|
||||
index_block_hash: '0xcc',
|
||||
burn_block_height: 123,
|
||||
block_count: 3,
|
||||
microblock_count: 0,
|
||||
microblock_hash: undefined,
|
||||
microblock_sequence: undefined,
|
||||
tx_count: 2,
|
||||
tx_count_unanchored: 2,
|
||||
});
|
||||
|
||||
const block4b: DbBlock = {
|
||||
@@ -4152,12 +4167,18 @@ describe('postgres datastore', () => {
|
||||
|
||||
const blockQuery3 = await db.getBlock({ hash: block3b.block_hash });
|
||||
expect(blockQuery3.result?.canonical).toBe(true);
|
||||
const chainTip3 = await db.getChainTip(client);
|
||||
const chainTip3 = await db.getChainTip();
|
||||
expect(chainTip3).toEqual({
|
||||
blockHash: '0x44bb',
|
||||
blockHeight: 4,
|
||||
indexBlockHash: '0xddbb',
|
||||
burnBlockHeight: 123,
|
||||
block_count: 4,
|
||||
block_hash: '0x44bb',
|
||||
block_height: 4,
|
||||
burn_block_height: 123,
|
||||
index_block_hash: '0xddbb',
|
||||
microblock_count: 0,
|
||||
microblock_hash: undefined,
|
||||
microblock_sequence: undefined,
|
||||
tx_count: 2, // Tx from block 2b now counts, but compensates with tx from block 2
|
||||
tx_count_unanchored: 2,
|
||||
});
|
||||
|
||||
const b1 = await db.getBlock({ hash: block1.block_hash });
|
||||
|
||||
@@ -1539,7 +1539,7 @@ describe('mempool tests', () => {
|
||||
// Simulate the bug with a txs being in the mempool at confirmed at the same time by
|
||||
// directly inserting the mempool-tx and mined-tx, bypassing the normal update functions.
|
||||
await db.updateBlock(db.sql, dbBlock1);
|
||||
const chainTip = await db.getChainTip(db.sql);
|
||||
const chainTip = await db.getChainTip();
|
||||
await db.insertDbMempoolTx(mempoolTx, chainTip, db.sql);
|
||||
await db.updateTx(db.sql, dbTx1);
|
||||
|
||||
|
||||
@@ -384,13 +384,12 @@ describe('microblock tests', () => {
|
||||
],
|
||||
});
|
||||
|
||||
const chainTip1 = await db.getUnanchoredChainTip();
|
||||
expect(chainTip1.found).toBeTruthy();
|
||||
expect(chainTip1.result?.blockHash).toBe(block1.block_hash);
|
||||
expect(chainTip1.result?.blockHeight).toBe(block1.block_height);
|
||||
expect(chainTip1.result?.indexBlockHash).toBe(block1.index_block_hash);
|
||||
expect(chainTip1.result?.microblockHash).toBeUndefined();
|
||||
expect(chainTip1.result?.microblockSequence).toBeUndefined();
|
||||
const chainTip1 = await db.getChainTip();
|
||||
expect(chainTip1.block_hash).toBe(block1.block_hash);
|
||||
expect(chainTip1.block_height).toBe(block1.block_height);
|
||||
expect(chainTip1.index_block_hash).toBe(block1.index_block_hash);
|
||||
expect(chainTip1.microblock_hash).toBeUndefined();
|
||||
expect(chainTip1.microblock_sequence).toBeUndefined();
|
||||
|
||||
const mb1: DbMicroblockPartial = {
|
||||
microblock_hash: '0xff01',
|
||||
@@ -546,13 +545,12 @@ describe('microblock tests', () => {
|
||||
],
|
||||
});
|
||||
|
||||
const chainTip2 = await db.getUnanchoredChainTip();
|
||||
expect(chainTip2.found).toBeTruthy();
|
||||
expect(chainTip2.result?.blockHash).toBe(block1.block_hash);
|
||||
expect(chainTip2.result?.blockHeight).toBe(block1.block_height);
|
||||
expect(chainTip2.result?.indexBlockHash).toBe(block1.index_block_hash);
|
||||
expect(chainTip2.result?.microblockHash).toBe(mb1.microblock_hash);
|
||||
expect(chainTip2.result?.microblockSequence).toBe(mb1.microblock_sequence);
|
||||
const chainTip2 = await db.getChainTip();
|
||||
expect(chainTip2.block_hash).toBe(block1.block_hash);
|
||||
expect(chainTip2.block_height).toBe(block1.block_height);
|
||||
expect(chainTip2.index_block_hash).toBe(block1.index_block_hash);
|
||||
expect(chainTip2.microblock_hash).toBe(mb1.microblock_hash);
|
||||
expect(chainTip2.microblock_sequence).toBe(mb1.microblock_sequence);
|
||||
|
||||
const txListResult1 = await supertest(api.server).get(`/extended/v1/tx`);
|
||||
const { body: txListBody1 }: { body: TransactionResults } = txListResult1;
|
||||
|
||||
Reference in New Issue
Block a user