Skip to content

Commit eb3fa47

Browse files
authored
Merge pull request #477 from hack-a-chain-software/blocks-from-height
refactor: improved blocks-from-height query
2 parents 770aac8 + 1c9a122 commit eb3fa47

18 files changed

+37713
-6251
lines changed

indexer/src/kadena-server/repository/infra/repository/block-db-repository.ts

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,18 @@ export default class BlockDbRepository implements BlockRepository {
193193
}
194194

195195
/**
196-
* Retrieves blocks within a specific height range
196+
* Retrieves blocks within a specific height range using a sliding window approach
197197
*
198198
* This method fetches blocks between the specified start and end heights,
199199
* supporting cursor-based pagination and chain filtering for efficient
200-
* navigation through large result sets.
200+
* navigation through large result sets. To prevent large database queries,
201+
* it implements a sliding window approach with a default window size of 50.
202+
*
203+
* Sliding window behavior:
204+
* - If endHeight is not provided or is too far from the current cursor position,
205+
* the query is limited to a window of 50 heights from the current position
206+
* - As pagination progresses (via 'after' cursor), the window slides forward
207+
* - This prevents unnecessary large queries when users request broad ranges
201208
*
202209
* Uses keyset pagination with compound cursors (height:id) for better performance
203210
* when navigating through large datasets.
@@ -228,8 +235,70 @@ export default class BlockDbRepository implements BlockRepository {
228235
last,
229236
});
230237

238+
const heightQuery = `SELECT max("height") FROM "Blocks"`;
239+
240+
const { rows: maxHeightRows } = await rootPgPool.query(heightQuery);
241+
242+
const maxHeight = maxHeightRows[0].max;
243+
// Default window size to prevent large database queries
244+
const DEFAULT_WINDOW_SIZE = 50;
245+
246+
// Calculate effective window range
247+
let effectiveStartHeight = startHeight;
248+
let effectiveEndHeight: number;
249+
250+
// Determine current position from cursor for window calculation
251+
let currentHeight = startHeight;
252+
if (after) {
253+
const [height] = after.split(':');
254+
const afterHeight = parseInt(height, 10);
255+
if (!isNaN(afterHeight)) {
256+
currentHeight = afterHeight;
257+
}
258+
} else if (before) {
259+
const [height] = before.split(':');
260+
const beforeHeight = parseInt(height, 10);
261+
if (!isNaN(beforeHeight)) {
262+
currentHeight = beforeHeight;
263+
}
264+
}
265+
266+
// If no endHeight provided or endHeight is too far from current position,
267+
// use sliding window approach
268+
if (!endHeight || endHeight - currentHeight > DEFAULT_WINDOW_SIZE) {
269+
if (after) {
270+
// Forward pagination: window starts from cursor position
271+
effectiveStartHeight = currentHeight;
272+
effectiveEndHeight = effectiveStartHeight + DEFAULT_WINDOW_SIZE;
273+
} else if (before) {
274+
// Backward pagination: window ends at cursor position
275+
effectiveEndHeight = currentHeight;
276+
effectiveStartHeight = Math.max(startHeight, effectiveEndHeight - DEFAULT_WINDOW_SIZE);
277+
} else {
278+
// Initial request: check if it's 'last' pagination (backward from end)
279+
if (last) {
280+
// For 'last' without cursor, use the maximum height across all chains
281+
const actualEndHeight = endHeight ? Math.min(endHeight, maxHeight) : maxHeight;
282+
effectiveEndHeight = actualEndHeight;
283+
effectiveStartHeight = Math.max(startHeight, actualEndHeight - DEFAULT_WINDOW_SIZE);
284+
} else {
285+
// Forward pagination: window starts from startHeight
286+
effectiveStartHeight = startHeight;
287+
effectiveEndHeight = effectiveStartHeight + DEFAULT_WINDOW_SIZE;
288+
}
289+
}
290+
291+
// If original endHeight is provided and within window, respect it (except for 'last' mode)
292+
if (endHeight && endHeight <= effectiveEndHeight && !last) {
293+
effectiveEndHeight = endHeight;
294+
}
295+
} else {
296+
// Use the provided endHeight when it's within reasonable range
297+
effectiveEndHeight = endHeight;
298+
}
299+
231300
const order = orderParam === 'ASC' ? 'DESC' : 'ASC';
232-
const queryParams: (string | number | string[])[] = [limit, startHeight];
301+
const queryParams: (string | number | string[])[] = [limit];
233302
let conditions = '';
234303

235304
if (before) {
@@ -243,7 +312,7 @@ export default class BlockDbRepository implements BlockRepository {
243312
}
244313

245314
queryParams.push(beforeHeight, beforeId);
246-
conditions += `\nAND (b.height, b.id) > ($${queryParams.length - 1}, $${queryParams.length})`;
315+
conditions += `\nAND (b.height, b.id) < ($${queryParams.length - 1}, $${queryParams.length})`;
247316
}
248317

249318
if (after) {
@@ -256,19 +325,26 @@ export default class BlockDbRepository implements BlockRepository {
256325
throw new Error('Invalid after cursor');
257326
}
258327
queryParams.push(afterHeight, afterId);
259-
conditions += `\nAND (b.height, b.id) < ($${queryParams.length - 1}, $${queryParams.length})`;
328+
conditions += `\nAND (b.height, b.id) > ($${queryParams.length - 1}, $${queryParams.length})`;
260329
}
261330

262331
if (chainIds?.length) {
263332
queryParams.push(chainIds);
264333
conditions += `\nAND b."chainId" = ANY($${queryParams.length})`;
265334
}
266335

267-
if (endHeight) {
268-
queryParams.push(endHeight);
269-
conditions += `\nAND b."height" <= $${queryParams.length}`;
336+
// Apply height range constraints
337+
// Only add start height constraint if we don't have an 'after' cursor
338+
// (cursor condition already handles the lower bound for 'after')
339+
// For 'before' cursor, we still need the lower bound
340+
if (!after) {
341+
queryParams.push(effectiveStartHeight);
342+
conditions += `\nAND b."height" >= $${queryParams.length}`;
270343
}
271344

345+
queryParams.push(effectiveEndHeight);
346+
conditions += `\nAND b."height" <= $${queryParams.length}`;
347+
272348
const query = `
273349
SELECT b.id,
274350
b.hash,
@@ -288,7 +364,7 @@ export default class BlockDbRepository implements BlockRepository {
288364
b."transactionsCount" as "transactionsCount",
289365
b."totalGasUsed" as "totalGasUsed"
290366
FROM "Blocks" b
291-
WHERE b.height ${before ? '<=' : '>='} $2
367+
WHERE 1=1
292368
${conditions}
293369
ORDER BY b.height ${order}, b.id ${order}
294370
LIMIT $1;
@@ -301,7 +377,7 @@ export default class BlockDbRepository implements BlockRepository {
301377
node: blockValidator.validate(row),
302378
}));
303379

304-
const pageInfo = getPageInfo({ edges, order, limit, after, before });
380+
const pageInfo = getPageInfo({ edges, order: orderParam, limit, after, before });
305381
return pageInfo;
306382
}
307383

indexer/src/kadena-server/repository/infra/repository/network-db-repository.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export default class NetworkDbRepository implements NetworkRepository {
224224

225225
async getCurrentChainHeights(): Promise<CurrentChainHeights> {
226226
const heightsQuery = `
227-
SELECT "chainId", "canonicalBlocks" FROM "Counters" ORDER BY "canonicalBlocks"
227+
SELECT "chainId", "canonicalBlocks" FROM "Counters"
228228
`;
229229

230230
const { rows } = await rootPgPool.query(heightsQuery);
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
/**
2-
* Resolver for the totalCount field of the QueryEventsConnection type.
3-
* This module counts blockchain events based on various filter criteria.
2+
* Resolver for the totalCount field of the QueryBlocksFromHeightConnection type.
3+
* This module counts blockchain blocks based on various filter criteria.
44
*/
55
import { ResolverContext } from '../../../config/apollo-server-config';
66
import { QueryBlocksFromHeightConnectionResolvers } from '../../../config/graphql-types';
77
import zod from 'zod';
88

99
/**
10-
* Zod schema for validating event query parameters.
11-
* Defines the structure and types of filter parameters used to count events.
12-
* Note that qualifiedEventName is required, while other filters are optional.
10+
* Zod schema for validating block query parameters.
11+
* Defines the structure and types of filter parameters used to count blocks.
1312
*/
1413
const schema = zod.object({
1514
chainIds: zod.array(zod.string()).optional(),
@@ -18,13 +17,83 @@ const schema = zod.object({
1817
});
1918

2019
/**
21-
* Resolver function for the totalCount field of the QueryEventsConnection type.
22-
* Retrieves the total count of events matching the provided filter criteria.
20+
* Interface for genesis height information
21+
*/
22+
interface GenesisHeight {
23+
chainId: string;
24+
height: number;
25+
}
26+
27+
/**
28+
* Interface for current chain heights
29+
*/
30+
interface CurrentChainHeights {
31+
[chainId: string]: number;
32+
}
33+
34+
/**
35+
* Calculates the total count of blocks based on chain IDs and height ranges.
36+
*
37+
* Logic:
38+
* - Chains 0-9: Start from genesis height (typically 0) to current/end height
39+
* - Chains 10-19: Start from their specific genesis height to current/end height
40+
*
41+
* @param startHeight - The starting height for the query
42+
* @param endHeight - The ending height for the query (null means use current heights)
43+
* @param chainIds - Array of chain IDs to include (empty means all chains)
44+
* @param genesisHeights - Array of genesis height information for each chain
45+
* @param currentChainHeights - Current maximum height for each chain
46+
* @returns The total count of blocks across all specified chains
47+
*/
48+
function calculateTotalBlockCount(
49+
startHeight: number,
50+
endHeight: number | null,
51+
chainIds: string[],
52+
genesisHeights: GenesisHeight[],
53+
currentChainHeights: CurrentChainHeights,
54+
): number {
55+
// Create a map for quick lookup of genesis heights
56+
const genesisHeightMap = genesisHeights.reduce(
57+
(map, { chainId, height }) => {
58+
map[chainId] = height;
59+
return map;
60+
},
61+
{} as Record<string, number>,
62+
);
63+
64+
return chainIds.reduce((totalCount, chainId) => {
65+
const genesisHeight = genesisHeightMap[chainId] ?? 0;
66+
const currentMaxHeight = currentChainHeights[chainId] ?? 0;
67+
68+
// Use the minimum of provided endHeight and actual max height in database
69+
const endHeightToUse =
70+
endHeight === null || endHeight === undefined
71+
? currentMaxHeight
72+
: Math.min(endHeight, currentMaxHeight);
73+
74+
// Ensure we don't go below the genesis height for any chain
75+
const effectiveStartHeight = Math.max(startHeight, genesisHeight);
76+
77+
// If the effective start height is greater than the end height, no blocks exist in this range
78+
if (effectiveStartHeight > endHeightToUse) {
79+
return totalCount; // No blocks to add for this chain
80+
}
81+
82+
// Calculate the count for this chain
83+
const chainBlockCount = endHeightToUse - effectiveStartHeight + 1;
84+
85+
return totalCount + chainBlockCount;
86+
}, 0);
87+
}
88+
89+
/**
90+
* Resolver function for the totalCount field of the QueryBlocksFromHeightConnection type.
91+
* Retrieves the total count of blocks matching the provided filter criteria.
2392
*
2493
* @param parent - The parent object containing filter parameters
2594
* @param _args - GraphQL arguments (unused in this resolver)
2695
* @param context - The resolver context containing repositories and services
27-
* @returns The total count of events matching the criteria
96+
* @returns The total count of blocks matching the criteria
2897
*/
2998
export const totalCountQueryBlocksFromHeightConnectionResolver: QueryBlocksFromHeightConnectionResolvers<ResolverContext>['totalCount'] =
3099
async (parent, _args, context) => {
@@ -42,22 +111,16 @@ export const totalCountQueryBlocksFromHeightConnectionResolver: QueryBlocksFromH
42111
chainIds = [...networkInfo.nodeChains];
43112
}
44113

114+
const networkInfo = await context.networkRepository.getAllInfo();
45115
const currentChainHeights = await context.networkRepository.getCurrentChainHeights();
46-
const startHeightOfChainsFrom9to19 = 852054;
47-
const totalCount = chainIds.reduce((acum, chainId) => {
48-
const endHeightToUse = endHeight ?? currentChainHeights[chainId];
49-
let totalOfChain = endHeightToUse - startHeight + 1;
50-
if (Number(chainId) > 9) {
51-
if (startHeight >= startHeightOfChainsFrom9to19) {
52-
totalOfChain = endHeightToUse - startHeight + 1;
53-
} else if (endHeightToUse < startHeightOfChainsFrom9to19) {
54-
totalOfChain = 0;
55-
} else {
56-
totalOfChain = endHeightToUse - startHeightOfChainsFrom9to19 + 1;
57-
}
58-
}
59-
return totalOfChain + acum;
60-
}, 0);
116+
117+
const totalCount = calculateTotalBlockCount(
118+
startHeight,
119+
endHeight ?? null,
120+
chainIds,
121+
networkInfo.genesisHeights,
122+
currentChainHeights,
123+
);
61124

62125
return totalCount;
63126
};

indexer/tests/integration/builders/blocks-from-height.builder.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { gql } from 'graphql-request';
22

3-
export const getBlocksFromHeightQuery = (params: any): string => {
3+
const buildBlocksFromHeightQuery = (params: any, includeTotalCount: boolean = true): string => {
44
if (Object.keys(params).length === 0) {
5-
throw new Error('No parameters provided to getBlocksFromHeightQuery.');
5+
throw new Error('No parameters provided to buildBlocksFromHeightQuery.');
66
}
77

88
const query = Object.entries(params)
@@ -14,9 +14,12 @@ export const getBlocksFromHeightQuery = (params: any): string => {
1414
})
1515
.join(', ');
1616

17+
const totalCountField = includeTotalCount ? 'totalCount' : '';
18+
1719
const queryGql = gql`
1820
query {
1921
blocksFromHeight(${query}) {
22+
${totalCountField}
2023
edges {
2124
cursor
2225
node {
@@ -54,7 +57,6 @@ export const getBlocksFromHeightQuery = (params: any): string => {
5457
id
5558
minerAccount {
5659
accountName
57-
balance
5860
chainId
5961
fungibleName
6062
guard {
@@ -101,3 +103,11 @@ export const getBlocksFromHeightQuery = (params: any): string => {
101103

102104
return queryGql;
103105
};
106+
107+
export const getBlocksFromHeightQuery = (params: any): string => {
108+
return buildBlocksFromHeightQuery(params, true);
109+
};
110+
111+
export const getBlocksFromHeightWithoutTotalCountQuery = (params: any): string => {
112+
return buildBlocksFromHeightQuery(params, false);
113+
};

0 commit comments

Comments
 (0)