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

Implement Effective Reorg Handling Mechanism #48

Merged
merged 2 commits into from
Oct 25, 2024
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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ lerna-debug.log*

# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
Expand Down
2 changes: 2 additions & 0 deletions config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ db:
synchronize: false
app:
port: <port>
verbose: <boolean>
theanmolsharma marked this conversation as resolved.
Show resolved Hide resolved
debug: <boolean>
network: <network>
requestRetry:
delay: <number> # delay in Milliseconds
Expand Down
11 changes: 6 additions & 5 deletions config/dev.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ db:
synchronize: true
app:
port: 3000
verbose: false
debug: true
network: regtest
requestRetry:
delay: 3000
count: 3
providerType: ESPLORA
providerType: BITCOIN_CORE_RPC
esplora:
url: https://blockstream.info
batchSize: 5
bitcoinCore:
protocol: http
rpcHost: 127.0.0.1
rpcPass: polarpass
rpcUser: polaruser
rpcPort: 18445

rpcPass: password
rpcUser: admin
rpcPort: 18443
2 changes: 2 additions & 0 deletions config/e2e.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ db:
synchronize: true
app:
port: 3000
verbose: false
debug: true
network: regtest
requestRetry:
delay: 500
Expand Down
35 changes: 34 additions & 1 deletion src/block-data-providers/base-block-data-provider.abstract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ import {
TransactionInput,
TransactionOutput,
} from '@/indexer/indexer.service';
import { ConfigService } from '@nestjs/config';
import { BlockStateService } from '@/block-state/block-state.service';
import { BlockState } from '@/block-state/block-state.entity';

export abstract class BaseBlockDataProvider<OperationState> {
protected abstract readonly logger: Logger;
protected abstract readonly operationStateKey: string;

protected constructor(
protected readonly configService: ConfigService,
private readonly indexerService: IndexerService,
private readonly operationStateService: OperationStateService,
protected readonly blockStateService: BlockStateService,
) {}

async indexTransaction(
Expand All @@ -39,10 +44,38 @@ export abstract class BaseBlockDataProvider<OperationState> {
)?.state;
}

async setState(state: OperationState): Promise<void> {
async setState(
state: OperationState,
blockState: BlockState,
): Promise<void> {
await this.operationStateService.setOperationState(
this.operationStateKey,
state,
);

await this.blockStateService.addBlockState(blockState);
}

abstract getBlockHash(height: number): Promise<string>;

async traceReorg(): Promise<number> {
let state = await this.blockStateService.getCurrentBlockState();

if (!state) return null;

while (state) {
const fetchedBlockHash = await this.getBlockHash(state.blockHeight);

if (state.blockHash === fetchedBlockHash) return state.blockHeight;

await this.blockStateService.removeState(state);

this.logger.log(
`Reorg found at height: ${state.blockHeight}, Wrong hash: ${state.blockHash}, Correct hash: ${fetchedBlockHash}`,
);
state = await this.blockStateService.getCurrentBlockState();
}

throw new Error('Cannot Reorgs, blockchain state exhausted');
}
}
1 change: 0 additions & 1 deletion src/block-data-providers/bitcoin-core/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export interface Output {
}

export type BitcoinCoreOperationState = {
currentBlockHeight: number;
indexedBlockHeight: number;
};

Expand Down
9 changes: 7 additions & 2 deletions src/block-data-providers/bitcoin-core/provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
rawTransactions,
} from '@/block-data-providers/bitcoin-core/provider-fixtures';
import { Test, TestingModule } from '@nestjs/testing';
import { BlockStateService } from '@/block-state/block-state.service';

describe('Bitcoin Core Provider', () => {
let provider: BitcoinCoreProvider;
Expand Down Expand Up @@ -46,6 +47,10 @@ describe('Bitcoin Core Provider', () => {
getOperationState: jest.fn(),
},
},
{
provide: BlockStateService,
useClass: jest.fn(),
},
],
}).compile();

Expand All @@ -72,8 +77,8 @@ describe('Bitcoin Core Provider', () => {

it('should process each transaction of a block appropriately', async () => {
const result = await provider.processBlock(3, 2);
expect(result).toHaveLength(1);
expect(result).toEqual(
expect(result[0]).toHaveLength(1);
expect(result[0]).toEqual(
expect.arrayContaining([...parsedTransactions.values()]),
);
});
Expand Down
63 changes: 42 additions & 21 deletions src/block-data-providers/bitcoin-core/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
import { AxiosRequestConfig } from 'axios';
import * as currency from 'currency.js';
import { AxiosRetryConfig, makeRequest } from '@/common/request';
import { BlockStateService } from '@/block-state/block-state.service';

@Injectable()
export class BitcoinCoreProvider
Expand All @@ -41,11 +42,17 @@ export class BitcoinCoreProvider
private retryConfig: AxiosRetryConfig;

public constructor(
private readonly configService: ConfigService,
configService: ConfigService,
indexerService: IndexerService,
operationStateService: OperationStateService,
blockStateService: BlockStateService,
) {
super(indexerService, operationStateService);
super(
configService,
indexerService,
operationStateService,
blockStateService,
);

const { protocol, rpcPort, rpcHost } =
configService.get<BitcoinCoreConfig>('bitcoinCore');
Expand All @@ -57,25 +64,32 @@ export class BitcoinCoreProvider
}

async onApplicationBootstrap() {
const getState = await this.getState();
if (getState) {
const currentState = await this.getState();
if (currentState) {
this.logger.log(
`Restoring state from previous run: ${JSON.stringify(
getState,
currentState,
)}`,
);
} else {
this.logger.log('No previous state found. Starting from scratch.');
const state: BitcoinCoreOperationState = {
currentBlockHeight: 0,
indexedBlockHeight:
this.configService.get<BitcoinNetwork>('app.network') ===
BitcoinNetwork.MAINNET
? TAPROOT_ACTIVATION_HEIGHT - 1
: 0,
};

await this.setState(state);

const blockHeight =
this.configService.get<BitcoinNetwork>('app.network') ===
BitcoinNetwork.MAINNET
? TAPROOT_ACTIVATION_HEIGHT - 1
: 0;
const blockHash = await this.getBlockHash(blockHeight);

await this.setState(
{
indexedBlockHeight: blockHeight,
},
{
blockHash,
blockHeight,
},
);
}
}

Expand All @@ -85,12 +99,14 @@ export class BitcoinCoreProvider
this.isSyncing = true;

const state = await this.getState();

if (!state) {
throw new Error('State not found');
}

try {
const tipHeight = await this.getTipHeight();

if (tipHeight <= state.indexedBlockHeight) {
this.logger.debug(
`No new blocks found. Current tip height: ${tipHeight}`,
Expand All @@ -102,9 +118,11 @@ export class BitcoinCoreProvider
const networkInfo = await this.getNetworkInfo();
const verbosityLevel = this.versionToVerbosity(networkInfo.version);

let height = state.indexedBlockHeight + 1;
let height =
((await this.traceReorg()) ?? state.indexedBlockHeight) + 1;

for (height; height <= tipHeight; height++) {
const transactions = await this.processBlock(
const [transactions, blockHash] = await this.processBlock(
height,
verbosityLevel,
);
Expand All @@ -122,7 +140,10 @@ export class BitcoinCoreProvider
}

state.indexedBlockHeight = height;
await this.setState(state);
await this.setState(state, {
blockHash: blockHash,
blockHeight: height,
});
}
} finally {
this.isSyncing = false;
Expand All @@ -143,7 +164,7 @@ export class BitcoinCoreProvider
});
}

private async getBlockHash(height: number): Promise<string> {
async getBlockHash(height: number): Promise<string> {
return this.request({
method: 'getblockhash',
params: [height],
Expand All @@ -170,7 +191,7 @@ export class BitcoinCoreProvider
public async processBlock(
height: number,
verbosityLevel: number,
): Promise<Transaction[]> {
): Promise<[Transaction[], string]> {
const parsedTransactionList: Transaction[] = [];
const blockHash = await this.getBlockHash(height);
this.logger.debug(
Expand All @@ -188,7 +209,7 @@ export class BitcoinCoreProvider
parsedTransactionList.push(parsedTransaction);
}

return parsedTransactionList;
return [parsedTransactionList, blockHash];
}

private async parseTransaction(
Expand Down
19 changes: 17 additions & 2 deletions src/block-data-providers/block-provider.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,46 @@ import { IndexerService } from '@/indexer/indexer.service';
import { ProviderType } from '@/common/enum';
import { BitcoinCoreProvider } from '@/block-data-providers/bitcoin-core/provider';
import { EsploraProvider } from '@/block-data-providers/esplora/provider';
import { BlockStateService } from '@/block-state/block-state.service';
import { BlockStateModule } from '@/block-state/block-state.module';

@Module({
imports: [OperationStateModule, IndexerModule, ConfigModule],
imports: [
OperationStateModule,
IndexerModule,
ConfigModule,
BlockStateModule,
],
controllers: [],
providers: [
{
provide: 'BlockDataProvider',
inject: [ConfigService, IndexerService, OperationStateService],
inject: [
ConfigService,
IndexerService,
OperationStateService,
BlockStateService,
],
useFactory: (
configService: ConfigService,
indexerService: IndexerService,
operationStateService: OperationStateService,
blockStateService: BlockStateService,
) => {
switch (configService.get<ProviderType>('providerType')) {
case ProviderType.ESPLORA:
return new EsploraProvider(
configService,
indexerService,
operationStateService,
blockStateService,
);
case ProviderType.BITCOIN_CORE_RPC:
return new BitcoinCoreProvider(
configService,
indexerService,
operationStateService,
blockStateService,
);
default:
throw Error('unrecognised provider type in config');
Expand Down
4 changes: 2 additions & 2 deletions src/block-data-providers/esplora/interface.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export type EsploraOperationState = {
export interface EsploraOperationState {
currentBlockHeight: number;
indexedBlockHeight: number;
lastProcessedTxIndex: number;
};
}

type EsploraTransactionInput = {
txid: string;
Expand Down
Loading