Skip to content

Commit 943e2d1

Browse files
authored
fix: optimize block endpoint (#1190)
* refactor: optimized block endpoint * refactor: made suggested changes * fix: removed unnecessary string to buffer conversions * fix: made suggested changes
1 parent 8ee1da8 commit 943e2d1

File tree

5 files changed

+172
-16
lines changed

5 files changed

+172
-16
lines changed

src/api/controllers/db-controller.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,20 @@ export async function getMicroblocksFromDataStore(args: {
412412
};
413413
}
414414

415+
export async function getBlocksWithMetadata(args: { limit: number; offset: number; db: PgStore }) {
416+
const blocks = await args.db.getBlocksWithMetadata({ limit: args.limit, offset: args.offset });
417+
const results = blocks.results.map(block =>
418+
parseDbBlock(
419+
block.block,
420+
block.txs,
421+
block.microblocks_accepted,
422+
block.microblocks_streamed,
423+
block.microblock_tx_count
424+
)
425+
);
426+
return { results, total: blocks.total };
427+
}
428+
415429
export async function getBlockFromDataStore({
416430
blockIdentifer,
417431
db,

src/api/routes/block.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as express from 'express';
22
import * as Bluebird from 'bluebird';
33
import { BlockListResponse } from '@stacks/stacks-blockchain-api-types';
44

5-
import { getBlockFromDataStore } from '../controllers/db-controller';
5+
import { getBlockFromDataStore, getBlocksWithMetadata } from '../controllers/db-controller';
66
import { timeout, waiter, has0xPrefix } from '../../helpers';
77
import { InvalidRequestError, InvalidRequestErrorType } from '../../errors';
88
import { parseLimitQuery, parsePagingQueryInput } from '../pagination';
@@ -28,19 +28,7 @@ export function createBlockRouter(db: PgStore): express.Router {
2828
const limit = parseBlockQueryLimit(req.query.limit ?? 20);
2929
const offset = parsePagingQueryInput(req.query.offset ?? 0);
3030

31-
// TODO: use getBlockWithMetadata or similar to avoid transaction integrity issues from lazy resolving block tx data (primarily the contract-call ABI data)
32-
const { results: blocks, total } = await db.getBlocks({ offset, limit });
33-
// TODO: fix duplicate pg queries
34-
const results = await Bluebird.mapSeries(blocks, async block => {
35-
const blockQuery = await getBlockFromDataStore({
36-
blockIdentifer: { hash: block.block_hash },
37-
db,
38-
});
39-
if (!blockQuery.found) {
40-
throw new Error('unexpected block not found -- fix block enumeration query');
41-
}
42-
return blockQuery.result;
43-
});
31+
const { results, total } = await getBlocksWithMetadata({ offset, limit, db });
4432
setETagCacheHeaders(res);
4533
// TODO: block schema validation
4634
const response: BlockListResponse = { limit, offset, total, results };

src/datastore/common.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,17 @@ export interface TransferQueryResult {
761761
amount: string;
762762
}
763763

764+
export interface BlocksWithMetadata {
765+
results: {
766+
block: DbBlock;
767+
txs: string[];
768+
microblocks_accepted: string[];
769+
microblocks_streamed: string[];
770+
microblock_tx_count: Record<string, number>;
771+
}[];
772+
total: number;
773+
}
774+
764775
export interface NonFungibleTokenMetadataQueryResult {
765776
token_uri: string;
766777
name: string;

src/datastore/pg-store.ts

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@ import { ChainID, ClarityAbi } from '@stacks/transactions';
77
import { getTxTypeId } from '../api/controllers/db-controller';
88
import {
99
assertNotNullish,
10+
FoundOrNot,
11+
hexToBuffer,
12+
unwrapOptional,
1013
bnsHexValueToName,
1114
bnsNameCV,
12-
FoundOrNot,
1315
getBnsSmartContractId,
14-
unwrapOptional,
1516
} from '../helpers';
1617
import { PgStoreEventEmitter } from './pg-store-event-emitter';
1718
import {
1819
AddressNftEventIdentifier,
1920
BlockIdentifier,
2021
BlockQueryResult,
22+
BlocksWithMetadata,
2123
ContractTxQueryResult,
2224
DbAssetEventTypeId,
2325
DbBlock,
@@ -403,6 +405,96 @@ export class PgStore {
403405
});
404406
}
405407

408+
/**
409+
* Returns Block information with metadata, including accepted and streamed microblocks hash
410+
* @returns `BlocksWithMetadata` object including list of Blocks with metadata and total count.
411+
*/
412+
async getBlocksWithMetadata({
413+
limit,
414+
offset,
415+
}: {
416+
limit: number;
417+
offset: number;
418+
}): Promise<BlocksWithMetadata> {
419+
return await this.sql.begin(async sql => {
420+
// get block list
421+
const { results: blocks, total: block_count } = await this.getBlocks({ limit, offset });
422+
const blockHashValues: string[] = [];
423+
const indexBlockHashValues: string[] = [];
424+
blocks.forEach(block => {
425+
const indexBytea = block.index_block_hash;
426+
const parentBytea = block.parent_index_block_hash;
427+
indexBlockHashValues.push(indexBytea, parentBytea);
428+
blockHashValues.push(indexBytea);
429+
});
430+
431+
// get txs in those blocks
432+
const txs = await sql<{ tx_id: string; index_block_hash: string }[]>`
433+
SELECT tx_id, index_block_hash
434+
FROM txs
435+
WHERE index_block_hash IN ${sql(blockHashValues)}
436+
AND canonical = true AND microblock_canonical = true
437+
ORDER BY microblock_sequence DESC, tx_index DESC
438+
`;
439+
440+
// get microblocks in those blocks
441+
const microblocksQuery = await sql<
442+
{
443+
parent_index_block_hash: string;
444+
index_block_hash: string;
445+
microblock_hash: string;
446+
transaction_count: number;
447+
}[]
448+
>`
449+
SELECT parent_index_block_hash, index_block_hash, microblock_hash, (
450+
SELECT COUNT(tx_id)::integer as transaction_count
451+
FROM txs
452+
WHERE txs.microblock_hash = microblocks.microblock_hash
453+
AND canonical = true AND microblock_canonical = true
454+
)
455+
FROM microblocks
456+
WHERE parent_index_block_hash IN ${sql(indexBlockHashValues)}
457+
AND microblock_canonical = true
458+
ORDER BY microblock_sequence DESC
459+
`;
460+
// parse data to return
461+
const blocksMetadata = blocks.map(block => {
462+
const transactions = txs
463+
.filter(tx => tx.index_block_hash === block.index_block_hash)
464+
.map(tx => tx.tx_id);
465+
const microblocksAccepted = microblocksQuery
466+
.filter(
467+
microblock => block.parent_index_block_hash === microblock.parent_index_block_hash
468+
)
469+
.map(mb => {
470+
return {
471+
microblock_hash: mb.microblock_hash,
472+
transaction_count: mb.transaction_count,
473+
};
474+
});
475+
const microblocksStreamed = microblocksQuery
476+
.filter(microblock => block.parent_index_block_hash === microblock.index_block_hash)
477+
.map(mb => mb.microblock_hash);
478+
const microblock_tx_count: Record<string, number> = {};
479+
microblocksAccepted.forEach(mb => {
480+
microblock_tx_count[mb.microblock_hash] = mb.transaction_count;
481+
});
482+
return {
483+
block,
484+
txs: transactions,
485+
microblocks_accepted: microblocksAccepted.map(mb => mb.microblock_hash),
486+
microblocks_streamed: microblocksStreamed,
487+
microblock_tx_count,
488+
};
489+
});
490+
const results: BlocksWithMetadata = {
491+
results: blocksMetadata,
492+
total: block_count,
493+
};
494+
return results;
495+
});
496+
}
497+
406498
async getBlockTxs(indexBlockHash: string) {
407499
const result = await this.sql<{ tx_id: string; tx_index: number }[]>`
408500
SELECT tx_id, tx_index

src/tests/api-tests.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10848,6 +10848,57 @@ describe('api tests', () => {
1084810848
expect(result.body.txs[0]).toEqual(tx_id);
1084910849
});
1085010850

10851+
test('/block', async () => {
10852+
const block_hash = '0x1234',
10853+
index_block_hash = '0xabcd',
10854+
tx_id = '0x12ff';
10855+
10856+
const block1 = new TestBlockBuilder({
10857+
block_hash,
10858+
index_block_hash,
10859+
// parent_index_block_hash: genesis_index_block_hash,
10860+
block_height: 1,
10861+
})
10862+
.addTx({ block_hash, tx_id, index_block_hash })
10863+
.build();
10864+
const microblock = new TestMicroblockStreamBuilder()
10865+
.addMicroblock({ parent_index_block_hash: index_block_hash })
10866+
.build();
10867+
await db.update(block1);
10868+
await db.updateMicroblocks(microblock);
10869+
const expectedResp = {
10870+
limit: 20,
10871+
offset: 0,
10872+
total: 1,
10873+
results: [
10874+
{
10875+
canonical: true,
10876+
height: 1,
10877+
hash: block_hash,
10878+
parent_block_hash: '0x',
10879+
burn_block_time: 94869286,
10880+
burn_block_time_iso: '1973-01-03T00:34:46.000Z',
10881+
burn_block_hash: '0xf44f44',
10882+
burn_block_height: 713000,
10883+
miner_txid: '0x4321',
10884+
parent_microblock_hash: '0x00',
10885+
parent_microblock_sequence: 0,
10886+
txs: [tx_id],
10887+
microblocks_accepted: [],
10888+
microblocks_streamed: [microblock.microblocks[0].microblock_hash],
10889+
execution_cost_read_count: 0,
10890+
execution_cost_read_length: 0,
10891+
execution_cost_runtime: 0,
10892+
execution_cost_write_count: 0,
10893+
execution_cost_write_length: 0,
10894+
microblock_tx_count: {},
10895+
},
10896+
],
10897+
};
10898+
const result = await supertest(api.server).get(`/extended/v1/block/`);
10899+
expect(result.body).toEqual(expectedResp);
10900+
});
10901+
1085110902
afterEach(async () => {
1085210903
await api.terminate();
1085310904
await db?.close();

0 commit comments

Comments
 (0)