bitcore-node
Version:
A blockchain indexing node with extended capabilities using bitcore
280 lines (248 loc) • 9.7 kB
text/typescript
import { CryptoRpc } from 'crypto-rpc';
import { EventEmitter } from 'events';
import * as os from 'os';
import { threadId, Worker as Thread } from 'worker_threads';
import { ChainStateProvider } from '../../';
import Config from '../../../../config';
import logger, { timestamp } from '../../../../logger';
import { StateStorage } from '../../../../models/state';
import { IEVMNetworkConfig } from '../../../../types/Config';
import { wait } from '../../../../utils';
import { EVMBlockStorage } from '../models/block';
export class MultiThreadSync extends EventEmitter {
private chain: string;
private network: string;
private syncInterval?: NodeJS.Timeout;
private threads: Thread[] = [];
private syncingThreads: number = 0;
private bestBlock: number = 0;
private syncHeight: number = 0;
private stopping: boolean = false;
private syncQueue: number[] = [];
private syncing: boolean = false;
private config: IEVMNetworkConfig;
private resolvingGaps: boolean = false;
private gapsLength: number = 0;
private mtSyncTipPad: number;
protected currentHeight: number = 0;
constructor({ chain, network }) {
super();
this.chain = chain || 'ETH';
this.network = network || 'mainnet';
this.config = Config.chains[chain][network];
this.mtSyncTipPad = this.config.mtSyncTipPad || 100;
}
async addBlockToQueue(blockNum) {
this.syncQueue.push(blockNum);
}
getRpc() {
const providerIdx = threadId % (this.config.providers || []).length;
const providerConfig = this.config.provider || this.config.providers![providerIdx];
const rpcConfig = { ...providerConfig, chain: this.chain, currencyConfig: {} };
const rpc = new CryptoRpc(rpcConfig, {}).get(this.chain);
return rpc;
}
async sync() {
if (this.syncing) {
return false;
}
const { chain, network } = this;
const { parentChain, forkHeight = 0 } = this.config;
this.syncing = true;
try {
let tip = await ChainStateProvider.getLocalTip({ chain, network });
if (parentChain && (!tip || tip.height < forkHeight)) {
let parentTip = await ChainStateProvider.getLocalTip({ chain: parentChain, network });
while (!parentTip || parentTip.height < forkHeight) {
logger.info(`Waiting until ${parentChain} syncs before ${chain} ${network}`);
await new Promise(resolve => {
setTimeout(resolve, 5000);
});
parentTip = await ChainStateProvider.getLocalTip({ chain: parentChain, network });
}
}
let startHeight = tip ? tip.height : this.config.syncStartHeight || 0;
const rpc = this.getRpc();
this.bestBlock = await rpc.web3!.eth.getBlockNumber();
this.currentHeight = tip ? tip.height : this.config.syncStartHeight || 0;
this.syncHeight = this.currentHeight;
startHeight = this.currentHeight;
logger.info(`Syncing ${this.bestBlock - this.currentHeight} blocks for ${chain} ${network}`);
await this.initializeThreads();
const startTime = Date.now();
const oneSecond = 1000;
this.syncInterval = setInterval(() => {
if (this.resolvingGaps) {
logger.info(
`${timestamp()} | Filling gaps... | Chain: ${chain} | Network: ${network} | On gap ${this.gapsLength -
this.syncQueue.length} of ${this.gapsLength} | Height: ${
this.syncQueue[0] ? this.syncQueue[0].toString().padStart(7) : this.syncHeight
}`
);
} else {
const blocksProcessed = this.currentHeight - startHeight;
const elapsedMinutes = (Date.now() - startTime) / (60 * oneSecond);
logger.info(
`${timestamp()} | Syncing... | Chain: ${chain} | Network: ${network} | ${(blocksProcessed / elapsedMinutes)
.toFixed(2)
.padStart(8)} blocks/min | Height: ${this.currentHeight.toString().padStart(7)}`
);
}
}, oneSecond);
this.syncingThreads = this.threads.length;
// Kick off threads syncing
for (let i = 0; i < this.threads.length; i++) {
if (this.syncQueue.length > 0) {
this.threads[i].postMessage({ blockNum: this.syncQueue.shift() });
} else {
this.threads[i].postMessage({ blockNum: this.currentHeight++ });
}
}
} catch (err: any) {
logger.error(`Error syncing ${chain} ${network} :: ${err.message}`);
await wait(2000);
this.syncing = false;
return this.sync();
}
return true;
}
threadMessageHandler(thread: Thread) {
const self = this;
return function(msg) {
logger.debug('Received sync thread message: ' + JSON.stringify(msg));
switch (msg.message) {
case 'ready':
self.emit('THREADREADY');
break;
case 'sync':
default:
self.threadSync(thread, msg);
}
};
}
async threadSync(thread: Thread, msg: { blockNum: number; notFound?: boolean; error?: Error }) {
if (msg.error) {
logger.warn(`Syncing thread ${thread.threadId} returned an error: ${msg.error}`);
}
const gimmeAnotherBlock = !msg.notFound;
const atTip = await this.areWeAtTheTip();
const moreBlocksToGive = !atTip || this.syncQueue.length > 0;
// If last block was found and there's more to sync
if (gimmeAnotherBlock && moreBlocksToGive) {
// If queue is empty, then !atTip must be true, so add next block to queue
if (this.syncQueue.length === 0) {
this.addBlockToQueue(this.syncHeight++);
}
const blockNum = this.syncQueue.shift();
thread.postMessage({ message: 'sync', blockNum });
this.currentHeight = Math.max(msg.blockNum, this.currentHeight);
// If the thread didn't find the block for some reason, but we know it exists
} else if (msg.blockNum < this.bestBlock && !atTip) {
logger.debug('Known block not found by thread: %o. Retrying.', msg.blockNum);
thread.postMessage({ message: 'sync', blockNum: msg.blockNum });
// Otherwise, decrement active syncing threads counter
} else {
this.syncingThreads--;
if (!this.syncingThreads) {
this.finishSync();
}
}
}
async areWeAtTheTip() {
if (this.bestBlock > this.syncHeight + this.mtSyncTipPad) {
return false;
}
const rpc = this.getRpc();
this.bestBlock = await rpc.web3!.eth.getBlockNumber();
if (this.bestBlock > this.syncHeight + this.mtSyncTipPad) {
return false;
}
return true;
}
getWorkerThread(workerData): Thread {
return new Thread(__dirname + '/syncWorker.js', {
workerData
});
}
async initializeThreads() {
if (this.threads.length > 0) {
return;
}
const self = this;
let threadCnt = this.config.threads || os.cpus().length - 1; // Subtract 1 for this process/thread
if (threadCnt <= 0) {
throw new Error('Invalid number of syncing threads.');
}
logger.info(`Initializing ${threadCnt} syncing threads.`);
const workerData = { chain: this.chain, network: this.network };
for (let i = 0; i < threadCnt; i++) {
const thread = this.getWorkerThread(workerData);
this.threads.push(thread);
thread.on('message', this.threadMessageHandler(thread));
thread.on('exit', function(code) {
self.syncingThreads--;
self.threads.splice(
self.threads.findIndex(t => t.threadId === thread.threadId),
1
);
if (code !== 0) {
logger.error('Thread exited with non-zero code: %o', code);
}
if (self.threads.length === 0) {
logger.info('All syncing threads stopped.');
}
});
thread.postMessage({ message: 'start' });
}
await new Promise(resolve => this.once('THREADREADY', resolve));
logger.info('Syncing threads ready.');
}
async getVerifiedBlockHeight() {
const state = await StateStorage.collection.findOne({}, { sort: { _id: -1 } });
const savedStartHeight = state?.verifiedBlockHeight?.[this.chain]?.[this.network] || 0;
return Math.max(savedStartHeight, this.config.syncStartHeight || 0);
}
async finishSync() {
clearInterval(this.syncInterval as NodeJS.Timeout);
if (this.stopping) {
return;
}
const verifiedBlockHeight = await this.getVerifiedBlockHeight();
logger.info(
`Verifying ${this.currentHeight - verifiedBlockHeight} ${this.chain}:${this.network} blocks for consistency.`
);
const gaps = await EVMBlockStorage.getBlockSyncGaps({
chain: this.chain,
network: this.network,
startHeight: verifiedBlockHeight
});
logger.info(`Verification of ${this.chain}:${this.network} blocks finished.`);
if (gaps.length) {
logger.info(`Gaps found. Attempting to fill ${gaps.length} block gaps.`);
this.resolvingGaps = true;
this.gapsLength = gaps.length;
this.syncingThreads = this.threads.length;
for (let blockNum of gaps) {
this.addBlockToQueue(blockNum);
}
this.syncing = false;
this.sync();
} else {
logger.info(`${this.chain}:${this.network} multi-thread sync is finished. Switching to main process sync.`);
await StateStorage.setVerifiedBlockHeight({ chain: this.chain, network: this.network, height: this.currentHeight });
this.emit('INITIALSYNCDONE');
this.shutdownThreads();
this.syncing = false;
}
}
shutdownThreads() {
for (let thread of this.threads) {
thread.postMessage({ message: 'shutdown' });
}
clearInterval(this.syncInterval as NodeJS.Timeout);
}
stop() {
this.shutdownThreads();
this.stopping = true;
}
}