Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New fork detection for probabilistic finalization chains #217

Merged
merged 6 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/node/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Different method for detecting block forks for chains with probabalistic finalization (#217)

## [3.3.6] - 2023-11-23
### Fixed
Expand Down
2 changes: 1 addition & 1 deletion packages/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"@nestjs/schedule": "^3.0.1",
"@subql/common": "^3.3.0",
"@subql/common-ethereum": "workspace:*",
"@subql/node-core": "^6.4.2",
"@subql/node-core": "^7.0.0",
"@subql/testing": "^2.0.2",
"@subql/types-ethereum": "workspace:*",
"cacheable-lookup": "6",
Expand Down
5 changes: 5 additions & 0 deletions packages/node/src/configure/NodeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { IConfig, NodeConfig } from '@subql/node-core';
export interface IEthereumConfig extends IConfig {
skipTransactions: boolean;
blockConfirmations: number;
blockForkReindex: number;
}

export class EthereumNodeConfig extends NodeConfig<IEthereumConfig> {
Expand All @@ -32,4 +33,8 @@ export class EthereumNodeConfig extends NodeConfig<IEthereumConfig> {
get blockConfirmations(): number {
return this._config.blockConfirmations;
}

get blockForkReindex(): number {
return this._config.blockForkReindex;
}
}
9 changes: 6 additions & 3 deletions packages/node/src/ethereum/api.ethereum.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright 2020-2023 SubQuery Pte Ltd authors & contributors
// SPDX-License-Identifier: GPL-3.0

import assert from 'assert';
import fs from 'fs';
import http from 'http';
import https from 'https';
Expand Down Expand Up @@ -99,7 +98,11 @@ export class EthereumApi implements ApiWrapper {
private name: string;

// Ethereum POS
private supportsFinalization = true;
private _supportsFinalization = true;

get supportsFinalization(): boolean {
return this._supportsFinalization;
}

/**
* @param {string} endpoint - The endpoint of the RPC provider
Expand Down Expand Up @@ -164,7 +167,7 @@ export class EthereumApi implements ApiWrapper {
]);

this.genesisBlock = genesisBlock;
this.supportsFinalization = supportsFinalization && supportsSafe;
this._supportsFinalization = supportsFinalization && supportsSafe;
this.chainId = network.chainId;
this.name = network.name;
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ describe('CeloJsonRpcProvider', () => {
const block = formatBlock(
await provider.send('eth_getBlockByNumber', ['latest', true]),
);
expect(BigNumber.from(block.gasLimit)).toEqual(BigNumber.from(0x01e84800));
expect(BigNumber.from(block.gasLimit).gte(constants.Zero)).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ describe('CeloJsonRpcProvider', () => {
const block = formatBlock(
await provider.send('eth_getBlockByNumber', ['latest', true]),
);
expect(BigNumber.from(block.gasLimit)).toEqual(BigNumber.from(0x01e84800));
expect(BigNumber.from(block.gasLimit).gte(constants.Zero)).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ describe('CeloJsonRpcProvider', () => {
const block = formatBlock(
await provider.send('eth_getBlockByNumber', ['latest', true]),
);
expect(BigNumber.from(block.gasLimit)).toEqual(BigNumber.from(0x01e84800));
expect(BigNumber.from(block.gasLimit).gte(constants.Zero)).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export class BlockDispatcherService
smartBatchService: SmartBatchService,
storeService: StoreService,
storeCacheService: StoreCacheService,
poiService: PoiService,
poiSyncService: PoiSyncService,
@Inject('ISubqueryProject') project: SubqueryProject,
dynamicDsService: DynamicDsService,
Expand All @@ -57,7 +56,6 @@ export class BlockDispatcherService
smartBatchService,
storeService,
storeCacheService,
poiService,
poiSyncService,
project,
dynamicDsService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export class WorkerBlockDispatcherService
cacheService: InMemoryCacheService,
storeService: StoreService,
storeCacheService: StoreCacheService,
poiService: PoiService,
poiSyncService: PoiSyncService,
@Inject('ISubqueryProject') project: SubqueryProject,
dynamicDsService: DynamicDsService,
Expand All @@ -63,7 +62,6 @@ export class WorkerBlockDispatcherService
smartBatchService,
storeService,
storeCacheService,
poiService,
poiSyncService,
project,
dynamicDsService,
Expand Down
4 changes: 0 additions & 4 deletions packages/node/src/indexer/fetch.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ import { UnfinalizedBlocksService } from './unfinalizedBlocks.service';
cacheService: InMemoryCacheService,
storeService: StoreService,
storeCacheService: StoreCacheService,
poiService: PoiService,
poiSyncService: PoiSyncService,
project: SubqueryProject,
dynamicDsService: DynamicDsService,
Expand All @@ -110,7 +109,6 @@ import { UnfinalizedBlocksService } from './unfinalizedBlocks.service';
cacheService,
storeService,
storeCacheService,
poiService,
poiSyncService,
project,
dynamicDsService,
Expand All @@ -127,7 +125,6 @@ import { UnfinalizedBlocksService } from './unfinalizedBlocks.service';
smartBatchService,
storeService,
storeCacheService,
poiService,
poiSyncService,
project,
dynamicDsService,
Expand All @@ -143,7 +140,6 @@ import { UnfinalizedBlocksService } from './unfinalizedBlocks.service';
InMemoryCacheService,
StoreService,
StoreCacheService,
PoiService,
PoiSyncService,
'ISubqueryProject',
DynamicDsService,
Expand Down
10 changes: 9 additions & 1 deletion packages/node/src/indexer/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const { version: packageVersion } = require('../../package.json');
@Injectable()
export class ProjectService extends BaseProjectService<
EthereumApiService,
EthereumProjectDs
EthereumProjectDs,
UnfinalizedBlocksService
> {
protected packageVersion = packageVersion;

Expand Down Expand Up @@ -75,4 +76,11 @@ export class ProjectService extends BaseProjectService<
// TODO update this when implementing skipBlock feature for Eth
this.apiService.updateBlockFetching();
}

protected async initUnfinalized(): Promise<number | undefined> {
return this.unfinalizedBlockService.init(
this.reindex.bind(this),
this.apiService.api.supportsFinalization,
);
}
}
214 changes: 214 additions & 0 deletions packages/node/src/indexer/unfinalizedBlocks.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
// Copyright 2020-2023 SubQuery Pte Ltd authors & contributors
// SPDX-License-Identifier: GPL-3.0

import { hexZeroPad } from '@ethersproject/bytes';
import {
ApiService,
CacheMetadataModel,
Header,
NodeConfig,
PoiBlock,
StoreCacheService,
METADATA_UNFINALIZED_BLOCKS_KEY,
METADATA_LAST_FINALIZED_PROCESSED_KEY,
} from '@subql/node-core';
import { EthereumNodeConfig } from '../configure/NodeConfig';
import { UnfinalizedBlocksService } from './unfinalizedBlocks.service';

// Adds 0 padding so we can convert to POI block
const hexify = (input: string) => hexZeroPad(input, 4);

const makeHeader = (height: number, finalized?: boolean): Header => ({
blockHeight: height,
blockHash: hexify(`0xABC${height}${finalized ? 'f' : ''}`),
parentHash: hexify(`0xABC${height - 1}${finalized ? 'f' : ''}`),
});

const getMockApi = (): ApiService => {
return {
api: {
getBlockByHeightOrHash: (hash: string | number) => {
const num =
typeof hash === 'number'
? hash
: Number(
hash
.toString()
.replace('0x', '')
.replace('ABC', '')
.replace('f', ''),
);
return Promise.resolve({
number: num,
hash: typeof hash === 'number' ? hexify(`0xABC${hash}f`) : hash,
parentHash: hexify(`0xABC${num - 1}f`),
});
},
getFinalizedBlock: jest.fn(() => ({
number: 110,
hash: '0xABC110f',
parentHash: '0xABC109f',
})),
},
} as any;
};

function getMockMetadata(): any {
const data: Record<string, any> = {};
return {
upsert: ({ key, value }: any) => (data[key] = value),
findOne: ({ where: { key } }: any) => ({ value: data[key] }),
findByPk: (key: string) => data[key],
find: (key: string) => data[key],
} as any;
}

function mockStoreCache(): StoreCacheService {
return {
metadata: new CacheMetadataModel(getMockMetadata()),
poi: {
getPoiBlocksBefore: jest.fn(() => [
PoiBlock.create(99, hexify('0xABC99f'), new Uint8Array(), ''),
]),
},
} as any as StoreCacheService;
}

describe('UnfinalizedBlockService', () => {
let unfinalizedBlocks: UnfinalizedBlocksService;
let storeCache: StoreCacheService;

beforeEach(() => {
storeCache = mockStoreCache();

unfinalizedBlocks = new UnfinalizedBlocksService(
getMockApi(),
new NodeConfig({
unfinalizedBlocks: true,
blockForkReindex: 1000,
} as any) as EthereumNodeConfig,
storeCache,
);
});

it('handles a block fork', async () => {
await unfinalizedBlocks.init(jest.fn());

(unfinalizedBlocks as any)._unfinalizedBlocks = [
makeHeader(100),
makeHeader(101),
makeHeader(102),
makeHeader(103, true), // Where the fork started
makeHeader(104),
makeHeader(105),
makeHeader(106),
makeHeader(107),
makeHeader(108),
makeHeader(109),
makeHeader(110),
];

const rewind = await unfinalizedBlocks.processUnfinalizedBlockHeader(
makeHeader(111, true),
);

expect(rewind).toEqual(103);
});

it('uses POI blocks if there are not enough cached unfinalized blocks', async () => {
await unfinalizedBlocks.init(jest.fn());

(unfinalizedBlocks as any)._unfinalizedBlocks = [
makeHeader(100),
makeHeader(101),
makeHeader(102),
makeHeader(103),
makeHeader(104),
makeHeader(105),
makeHeader(106),
makeHeader(107),
makeHeader(108),
makeHeader(109),
makeHeader(110),
];

const spy = jest.spyOn(storeCache.poi as any, 'getPoiBlocksBefore');

const rewind = await unfinalizedBlocks.processUnfinalizedBlockHeader(
makeHeader(111, true),
);

expect(rewind).toEqual(99);
expect(spy).toHaveBeenCalled();
});

// The finalized block is after the cached unfinalized blocks, they should be rechecked
it('startup, correctly checks for forks after cached unfinalized blocks', async () => {
storeCache.metadata.set(
METADATA_UNFINALIZED_BLOCKS_KEY,
JSON.stringify(<Header[]>[
makeHeader(99, true),
makeHeader(100),
makeHeader(101),
]),
);

storeCache.metadata.set(METADATA_LAST_FINALIZED_PROCESSED_KEY, 99);

const rewind = jest.fn();

await unfinalizedBlocks.init(rewind);

// It should fall back to poi in this case
expect(rewind).toHaveBeenCalledWith(99);
});

it('startup, correctly checks for forks within cached unfinalized blocks', async () => {
storeCache.metadata.set(
METADATA_UNFINALIZED_BLOCKS_KEY,
JSON.stringify(<Header[]>[
makeHeader(110),
makeHeader(111),
makeHeader(112),
]),
);

storeCache.metadata.set(METADATA_LAST_FINALIZED_PROCESSED_KEY, 109);

const rewind = jest.fn();

await unfinalizedBlocks.init(rewind);

// It should fall back to poi in this case
expect(rewind).toHaveBeenCalledWith(99);
});

it('doesnt throw if there are no unfinalized blocks on startup', async () => {
storeCache.metadata.set(METADATA_LAST_FINALIZED_PROCESSED_KEY, 109);

await expect(unfinalizedBlocks.init(jest.fn())).resolves.not.toThrow();
});

it('rewinds using blockForkReindex value if poi is not enabled', async () => {
// Do this to "disable" poi
(storeCache as any).poi = null;

storeCache.metadata.set(
METADATA_UNFINALIZED_BLOCKS_KEY,
JSON.stringify(<Header[]>[
makeHeader(110),
makeHeader(111),
makeHeader(112),
]),
);

storeCache.metadata.set(METADATA_LAST_FINALIZED_PROCESSED_KEY, 109);

const rewind = jest.fn();

await unfinalizedBlocks.init(rewind);

// It should fall back to poi in this case
expect(rewind).toHaveBeenCalledWith(0);
});
});
Loading
Loading