Skip to content

Commit 50587d5

Browse files
authored
Merge pull request #349 from hack-a-chain-software/orphan-blocks
feat: orphan blocks mechanism
2 parents 82f92f6 + bf56960 commit 50587d5

File tree

18 files changed

+1151
-115
lines changed

18 files changed

+1151
-115
lines changed

indexer/src/index.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@ import { closeDatabase } from './config/database';
1919
import { initializeDatabase } from './config/init';
2020
import { startGraphqlServer } from './kadena-server/server';
2121
import { backfillBalances } from './services/balances';
22-
import { startMissingBlocks } from './services/missing';
2322
import { startStreaming } from './services/streaming';
2423
import { backfillPairEvents } from './services/pair';
2524
import { setupAssociations } from './models/setup-associations';
26-
import { PriceUpdaterService } from './services/price/price-updater.service';
25+
import { PriceUpdaterService } from '@/services/price/price-updater.service';
2726

2827
/**
2928
* Command-line interface configuration using Commander.
@@ -33,7 +32,6 @@ program
3332
.option('-s, --streaming', 'Start streaming blockchain data')
3433
.option('-t, --graphql', 'Start GraphQL server based on kadena schema')
3534
.option('-f, --guards', 'Backfill the guards')
36-
.option('-m, --missing', 'Missing blocks')
3735
.option('-z, --database', 'Init the database')
3836
.option('-p, --backfillPairs', 'Backfill the pairs');
3937

@@ -58,7 +56,7 @@ const options = program.opts();
5856
async function main() {
5957
try {
6058
setupAssociations();
61-
await PriceUpdaterService.getInstance();
59+
PriceUpdaterService.getInstance();
6260

6361
if (options.database) {
6462
await initializeDatabase();
@@ -72,20 +70,16 @@ async function main() {
7270
await backfillBalances();
7371
await closeDatabase();
7472
process.exit(0);
75-
} else if (options.missing) {
76-
await startMissingBlocks();
77-
process.exit(0);
7873
} else if (options.graphql) {
7974
await startGraphqlServer();
8075
} else if (options.backfillPairs) {
81-
PriceUpdaterService.getInstance();
8276
await backfillPairEvents();
8377
} else {
8478
console.info('[INFO][BIZ][BIZ_FLOW] No specific task requested.');
8579
}
8680
} catch (error) {
8781
console.error('[ERROR][INFRA][INFRA_CONFIG] Initialization failed:', error);
88-
// TODO: [OPTIMIZATION] Implement more detailed error logging and possibly retry mechanisms
82+
process.exit(1);
8983
}
9084
}
9185

indexer/src/kadena-server/config/graphql-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export type Scalars = {
3030
/** A unit of information that stores a set of verified transactions. */
3131
export type Block = Node & {
3232
__typename?: 'Block';
33+
canonical: Scalars['Boolean']['output'];
3334
chainId: Scalars['BigInt']['output'];
3435
coinbase: Scalars['String']['output'];
3536
creationTime: Scalars['DateTime']['output'];
@@ -1983,6 +1984,7 @@ export type BlockResolvers<
19831984
ContextType = any,
19841985
ParentType extends ResolversParentTypes['Block'] = ResolversParentTypes['Block'],
19851986
> = {
1987+
canonical?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
19861988
chainId?: Resolver<ResolversTypes['BigInt'], ParentType, ContextType>;
19871989
coinbase?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
19881990
creationTime?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;

indexer/src/kadena-server/config/schema.graphql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,8 @@ type Block implements Node {
620620
"""
621621
powHash: String!
622622

623+
canonical: Boolean!
624+
623625
parent: Block @complexity(value: 1)
624626
"""
625627
Default page size is 20.

indexer/src/kadena-server/repository/application/block-repository.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,14 @@ export interface GetLatestBlocksParams {
2626
chainIds?: string[];
2727
}
2828

29+
export interface UpdateCanonicalStatusParams {
30+
blocks: { hash: string; canonical: boolean }[];
31+
}
32+
2933
export type BlockOutput = Omit<Block, 'parent' | 'events' | 'minerAccount' | 'transactions'> & {
3034
parentHash: string;
3135
blockId: number;
36+
canonical: boolean;
3237
};
3338

3439
export type FungibleChainAccountOutput = Omit<
@@ -37,7 +42,7 @@ export type FungibleChainAccountOutput = Omit<
3742
>;
3843

3944
export default interface BlockRepository {
40-
getBlockByHash(hash: string): Promise<BlockOutput>;
45+
getBlockByHash(hash: string): Promise<BlockOutput | null>;
4146
getBlocksFromDepth(params: GetBlocksFromDepthParams): Promise<{
4247
pageInfo: PageInfo;
4348
edges: ConnectionEdge<BlockOutput>[];
@@ -75,6 +80,11 @@ export default interface BlockRepository {
7580
id?: string,
7681
): Promise<BlockOutput[]>;
7782

83+
getBlockNParent(depth: number, hash: string): Promise<string | undefined>;
84+
getBlocksWithSameHeight(height: number, chainId: string): Promise<BlockOutput[]>;
85+
getBlocksWithHeightHigherThan(height: number, chainId: string): Promise<BlockOutput[]>;
86+
updateCanonicalStatus(params: UpdateCanonicalStatusParams): Promise<void>;
87+
7888
// dataloader
7989
getBlocksByEventIds(eventIds: string[]): Promise<BlockOutput[]>;
8090
getBlocksByTransactionIds(transactionIds: string[]): Promise<BlockOutput[]>;

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

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@
1414
* - Optimized batch retrieval through DataLoader patterns
1515
*/
1616

17-
import { FindOptions, Op, QueryTypes } from 'sequelize';
17+
import { Op, QueryTypes } from 'sequelize';
1818
import { rootPgPool, sequelize } from '../../../../config/database';
19-
import BlockModel, { BlockAttributes } from '../../../../models/block';
19+
import BlockModel from '../../../../models/block';
2020
import BlockRepository, {
2121
BlockOutput,
2222
GetBlocksBetweenHeightsParams,
2323
GetBlocksFromDepthParams,
2424
GetCompletedBlocksParams,
2525
GetLatestBlocksParams,
26+
UpdateCanonicalStatusParams,
2627
} from '../../application/block-repository';
2728
import { getPageInfo, getPaginationParams } from '../../pagination';
2829
import { blockValidator } from '../schema-validator/block-schema-validator';
@@ -53,14 +54,12 @@ export default class BlockDbRepository implements BlockRepository {
5354
* @returns Promise resolving to the block data if found
5455
* @throws Error if the block is not found
5556
*/
56-
async getBlockByHash(hash: string) {
57+
async getBlockByHash(hash: string): Promise<BlockOutput | null> {
5758
const block = await BlockModel.findOne({
5859
where: { hash },
5960
});
6061

61-
if (!block) {
62-
throw new Error('Block not found.');
63-
}
62+
if (!block) return null;
6463

6564
return blockValidator.mapFromSequelize(block);
6665
}
@@ -314,7 +313,8 @@ export default class BlockDbRepository implements BlockRepository {
314313
b.target as "target",
315314
b.coinbase as "coinbase",
316315
b.adjacents as "adjacents",
317-
b.parent as "parent"
316+
b.parent as "parent",
317+
b.canonical as "canonical"
318318
FROM "Blocks" b
319319
WHERE b.height >= $2
320320
${conditions}
@@ -594,6 +594,7 @@ export default class BlockDbRepository implements BlockRepository {
594594
b.coinbase as "coinbase",
595595
b.adjacents as "adjacents",
596596
b.parent as "parent",
597+
b.canonical as "canonical",
597598
t.id as "transactionId"
598599
FROM "Blocks" b
599600
JOIN "Transactions" t ON b.id = t."blockId"
@@ -641,7 +642,8 @@ export default class BlockDbRepository implements BlockRepository {
641642
b.target as "target",
642643
b.coinbase as "coinbase",
643644
b.adjacents as "adjacents",
644-
b.parent as "parent"
645+
b.parent as "parent",
646+
b.canonical as "canonical"
645647
FROM "Blocks" b
646648
WHERE b.hash = ANY($1::text[])`,
647649
[hashes],
@@ -923,4 +925,84 @@ export default class BlockDbRepository implements BlockRepository {
923925

924926
return Object.assign({}, ...maxHeightsArray);
925927
}
928+
929+
async getBlocksWithSameHeight(height: number, chainId: string): Promise<BlockOutput[]> {
930+
const query = `
931+
SELECT b.*
932+
FROM "Blocks" b
933+
WHERE b."height" = $1 AND b."chainId" = $2
934+
`;
935+
936+
const { rows } = await rootPgPool.query(query, [height, chainId]);
937+
938+
return rows.map(row => blockValidator.validate(row));
939+
}
940+
941+
async getBlockNParent(depth: number, hash: string): Promise<string | undefined> {
942+
const query = `
943+
WITH RECURSIVE BlockAncestors AS (
944+
SELECT hash, parent, 1 AS depth, height
945+
FROM "Blocks"
946+
WHERE hash = $1
947+
UNION ALL
948+
SELECT b.hash, b.parent, d.depth + 1 AS depth, b.height
949+
FROM BlockAncestors d
950+
JOIN "Blocks" b ON d.parent = b.hash
951+
WHERE d.depth < $2
952+
)
953+
SELECT parent as hash, depth
954+
FROM BlockAncestors
955+
ORDER BY depth DESC
956+
LIMIT 1;
957+
`;
958+
const { rows } = await rootPgPool.query(query, [hash, depth]);
959+
960+
return rows?.[0]?.hash;
961+
}
962+
963+
async getBlocksWithHeightHigherThan(height: number, chainId: string): Promise<BlockOutput[]> {
964+
const query = `
965+
SELECT b.*
966+
FROM "Blocks" b
967+
WHERE b.height > $1 AND b."chainId" = $2;
968+
`;
969+
970+
const { rows } = await rootPgPool.query(query, [height, chainId]);
971+
972+
return rows.map(row => blockValidator.validate(row));
973+
}
974+
975+
async updateCanonicalStatus(params: UpdateCanonicalStatusParams) {
976+
const canonicalHashes = params.blocks
977+
.filter(change => change.canonical)
978+
.map(change => change.hash);
979+
const nonCanonicalHashes = params.blocks
980+
.filter(change => !change.canonical)
981+
.map(change => change.hash);
982+
983+
await rootPgPool.query('BEGIN');
984+
try {
985+
if (canonicalHashes.length > 0) {
986+
const canonicalQuery = `
987+
UPDATE "Blocks"
988+
SET "canonical" = true
989+
WHERE hash = ANY($1)
990+
`;
991+
await rootPgPool.query(canonicalQuery, [canonicalHashes]);
992+
}
993+
994+
if (nonCanonicalHashes.length > 0) {
995+
const nonCanonicalQuery = `
996+
UPDATE "Blocks"
997+
SET "canonical" = false
998+
WHERE hash = ANY($1)
999+
`;
1000+
await rootPgPool.query(nonCanonicalQuery, [nonCanonicalHashes]);
1001+
}
1002+
await rootPgPool.query('COMMIT');
1003+
} catch (error) {
1004+
await rootPgPool.query('ROLLBACK');
1005+
throw error;
1006+
}
1007+
}
9261008
}

indexer/src/kadena-server/repository/infra/schema-validator/block-schema-validator.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const schema = zod.object({
4141
adjacents: zod.record(zod.any()),
4242
parent: zod.string(),
4343
coinbase: zod.any(),
44+
canonical: zod.boolean().nullable(),
4445
});
4546

4647
/**
@@ -75,13 +76,15 @@ const getBase64ID = (hash: string): string => {
7576
*/
7677
const validate = (row: any): BlockOutput => {
7778
const res = schema.parse(row);
79+
const isCanonicalNull = res.canonical === null;
80+
const canonicalValue = res.canonical ?? false;
7881
return {
7982
id: getBase64ID(res.hash),
8083
parentHash: res.parent,
8184
creationTime: convertStringToDate(res.creationTime),
8285
epoch: convertStringToDate(res.epochStart),
8386
flags: int64ToUint64String(res.featureFlags),
84-
powHash: '...', // TODO (STREAMING)
87+
powHash: '',
8588
hash: res.hash,
8689
height: res.height,
8790
nonce: res.nonce,
@@ -90,6 +93,7 @@ const validate = (row: any): BlockOutput => {
9093
coinbase: JSON.stringify(res.coinbase),
9194
weight: res.weight,
9295
chainId: res.chainId,
96+
canonical: isCanonicalNull ? true : canonicalValue,
9397
difficulty: Number(calculateBlockDifficulty(res.target)),
9498
neighbors: Object.entries(res.adjacents).map(([chainId, hash]) => ({
9599
chainId,
@@ -110,13 +114,16 @@ const validate = (row: any): BlockOutput => {
110114
* @returns A transformed BlockOutput object
111115
*/
112116
const mapFromSequelize = (blockModel: BlockAttributes): BlockOutput => {
117+
const isCanonicalNull = blockModel.canonical === null;
118+
const canonicalValue = blockModel.canonical ?? false;
113119
return {
114120
id: getBase64ID(blockModel.hash),
115121
hash: blockModel.hash,
116122
parentHash: blockModel.parent,
117123
chainId: blockModel.chainId,
124+
canonical: isCanonicalNull ? true : canonicalValue,
118125
creationTime: convertStringToDate(blockModel.creationTime),
119-
powHash: '...', // TODO (STREAMING)
126+
powHash: '',
120127
difficulty: Number(calculateBlockDifficulty(blockModel.target)),
121128
epoch: convertStringToDate(blockModel.epochStart),
122129
flags: int64ToUint64String(blockModel.featureFlags),

indexer/src/kadena-server/resolvers/fields/transfer/block-transfer-resolver.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,9 @@ export const blockTransferResolver: TransferResolvers<ResolverContext>['block']
3232
const { blockHash } = schema.parse(parent);
3333
const output = await context.blockRepository.getBlockByHash(blockHash);
3434

35+
if (!output) {
36+
throw new Error('[ERROR][DB][DATA_MISSING] Block transfer not found.');
37+
}
38+
3539
return buildBlockOutput(output);
3640
};

indexer/src/kadena-server/resolvers/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ export const resolvers: Resolvers<ResolverContext> = {
263263
* @returns The GraphQL type name as a string, or null if unrecognized
264264
*/
265265
__resolveType(obj: any) {
266-
if (obj.difficulty && obj.powHash) {
266+
if (obj.difficulty && obj.hash) {
267267
return 'Block';
268268
}
269269

indexer/src/kadena-server/resolvers/node-utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ export const getNode = async (context: ResolverContext, id: string) => {
5959
if (type === 'Block') {
6060
// Resolve Block node - only requires the block hash as a parameter
6161
const output = await context.blockRepository.getBlockByHash(params);
62+
if (!output) {
63+
throw new Error('[ERROR][DB][DATA_MISSING] Block not found.');
64+
}
6265
return buildBlockOutput(output);
6366
}
6467

indexer/src/kadena-server/resolvers/query/block-query-resolver.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,9 @@ export const blockQueryResolver: QueryResolvers<ResolverContext>['block'] = asyn
3333
const { hash } = args;
3434
const output = await context.blockRepository.getBlockByHash(hash);
3535

36+
if (!output) {
37+
throw new Error('[ERROR][DB][DATA_MISSING] Block not found.');
38+
}
39+
3640
return buildBlockOutput(output);
3741
};

0 commit comments

Comments
 (0)