bitcore-node
Version:
A blockchain indexing node with extended capabilities using bitcore
364 lines • 15.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EVMP2pWorker = void 0;
const events_1 = require("events");
const os = __importStar(require("os"));
const __1 = require("../../");
const logger_1 = require("../../../../logger");
const logger_2 = __importDefault(require("../../../../logger"));
const state_1 = require("../../../../models/state");
const p2p_1 = require("../../../../services/p2p");
const utils_1 = require("../../../../utils");
const csp_1 = require("../api/csp");
const block_1 = require("../models/block");
const transaction_1 = require("../models/transaction");
const rpcs_1 = require("./rpcs");
const sync_1 = require("./sync");
class EVMP2pWorker extends p2p_1.BaseP2PWorker {
constructor({ chain, network, chainConfig, blockModel = block_1.EVMBlockStorage, txModel = transaction_1.EVMTransactionStorage }) {
super({ chain, network, chainConfig, blockModel });
this.chain = chain || 'ETH';
this.network = network;
this.chainConfig = chainConfig;
this.syncing = false;
this.initialSyncComplete = false;
this.blockModel = blockModel;
this.txModel = txModel;
this.provider = new csp_1.BaseEVMStateProvider(this.chain);
this.events = new events_1.EventEmitter();
this.invCache = {};
this.invCacheLimits = {
TX: 100000
};
this.disconnecting = false;
this.multiThreadSync = new sync_1.MultiThreadSync({ chain, network });
}
cacheInv(type, hash) {
if (!this.invCache[type]) {
this.invCache[type] = [];
}
if (this.invCache[type].length > this.invCacheLimits[type]) {
this.invCache[type].shift();
}
this.invCache[type].push(hash);
}
isCachedInv(type, hash) {
if (!this.invCache[type]) {
this.invCache[type] = [];
}
return this.invCache[type].includes(hash);
}
async setupListeners() {
const { host, port } = this.chainConfig.provider || this.chainConfig.providers[0];
this.events.on('disconnected', async () => {
logger_2.default.warn(`${(0, logger_1.timestamp)()} | Not connected to peer: ${host}:${port} | Chain: ${this.chain} | Network: ${this.network}`);
});
this.events.on('connected', async () => {
this.txSubscription = await this.web3.eth.subscribe('pendingTransactions');
this.txSubscription.subscribe(async (_err, txid) => {
if (!this.isCachedInv('TX', txid)) {
this.cacheInv('TX', txid);
const tx = (await this.web3.eth.getTransaction(txid));
if (tx) {
await this.processTransaction(tx);
this.events.emit('transaction', tx);
}
}
});
this.blockSubscription = await this.web3.eth.subscribe('newBlockHeaders');
this.blockSubscription.subscribe((_err, block) => {
this.events.emit('block', block);
if (!this.syncing) {
this.sync();
}
});
});
this.multiThreadSync.once('INITIALSYNCDONE', () => {
this.initialSyncComplete = true;
this.events.emit('SYNCDONE');
});
}
async disconnect() {
this.disconnecting = true;
try {
if (this.txSubscription) {
this.txSubscription.unsubscribe();
}
if (this.blockSubscription) {
this.blockSubscription.unsubscribe();
}
}
catch (e) {
console.error(e);
}
}
async getWeb3() {
return this.provider.getWeb3(this.network);
}
async getClient() {
try {
const nodeVersion = await this.web3.eth.getNodeInfo();
const client = nodeVersion.split('/')[0].toLowerCase();
if (client !== 'erigon' && client !== 'geth') {
// assume it's a geth fork, or at least more like geth.
// this is helpful when using a dev solution like ganache.
return 'geth';
}
return client;
}
catch (e) {
console.error(e);
return 'geth';
}
}
async handleReconnects() {
this.disconnecting = false;
let firstConnect = true;
let connected = false;
let disconnected = false;
const { host, port } = this.chainConfig.provider || this.chainConfig.providers[0];
while (!this.disconnecting && !this.stopping) {
try {
if (!this.web3) {
const { web3 } = await this.getWeb3();
this.web3 = web3;
}
try {
if (!this.client || !this.rpc) {
this.client = await this.getClient();
this.rpc = new rpcs_1.Rpcs[this.client](this.web3);
}
connected = await this.web3.eth.net.isListening();
}
catch (e) {
connected = false;
}
if (!connected) {
this.web3 = undefined;
this.client = undefined;
this.events.emit('disconnected');
}
else if (disconnected || firstConnect) {
this.events.emit('connected');
}
if (disconnected && connected && !firstConnect) {
logger_2.default.warn(`${(0, logger_1.timestamp)()} | Reconnected to peer: ${host}:${port} | Chain: ${this.chain} | Network: ${this.network}`);
}
if (connected && firstConnect) {
firstConnect = false;
logger_2.default.info(`${(0, logger_1.timestamp)()} | Connected to peer: ${host}:${port} | Chain: ${this.chain} | Network: ${this.network}`);
}
disconnected = !connected;
}
catch (e) { }
await (0, utils_1.wait)(2000);
}
}
async connect() {
this.handleReconnects();
return new Promise(resolve => this.events.once('connected', resolve));
}
async getBlock(height) {
return this.rpc.getBlock(height);
}
async processBlock(block, transactions) {
await this.blockModel.addBlock({
chain: this.chain,
network: this.network,
forkHeight: this.chainConfig.forkHeight,
parentChain: this.chainConfig.parentChain,
initialSyncComplete: this.initialSyncComplete,
block,
transactions
});
if (!this.syncing) {
logger_2.default.info(`Added block ${block.hash}`, {
chain: this.chain,
network: this.network
});
}
}
async processTransaction(tx) {
const now = new Date();
const convertedTx = this.txModel.convertRawTx(this.chain, this.network, tx);
this.txModel.batchImport({
chain: this.chain,
network: this.network,
txs: [convertedTx],
height: -1,
mempoolTime: now,
blockTime: now,
blockTimeNormalized: now,
initialSyncComplete: true
});
}
useMultiThread() {
if (this.chainConfig.threads == null) {
// use multithread by default if there are >2 threads in the CPU
return os.cpus().length > 2;
}
return this.chainConfig.threads > 0;
}
async sync() {
if (this.syncing) {
return false;
}
if (!this.initialSyncComplete && this.useMultiThread()) {
return this.multiThreadSync.sync();
}
const { chain, chainConfig, network } = this;
const { parentChain, forkHeight = 0 } = chainConfig;
this.syncing = true;
const state = await state_1.StateStorage.collection.findOne({});
this.initialSyncComplete =
state && state.initialSyncComplete && state.initialSyncComplete.includes(`${chain}:${network}`);
let tip = await __1.ChainStateProvider.getLocalTip({ chain, network });
if (parentChain && (!tip || tip.height < forkHeight)) {
let parentTip = await __1.ChainStateProvider.getLocalTip({ chain: parentChain, network });
while (!parentTip || parentTip.height < forkHeight) {
logger_2.default.info(`Waiting until ${parentChain} syncs before ${chain} ${network}`);
await new Promise(resolve => {
setTimeout(resolve, 5000);
});
parentTip = await __1.ChainStateProvider.getLocalTip({ chain: parentChain, network });
}
}
const startHeight = tip ? tip.height : chainConfig.syncStartHeight || 0;
const startTime = Date.now();
try {
let bestBlock = await this.web3.eth.getBlockNumber();
let lastLog = 0;
let currentHeight = tip ? tip.height : chainConfig.syncStartHeight || 0;
logger_2.default.info(`Syncing ${bestBlock - currentHeight} blocks for ${chain} ${network}`);
while (currentHeight <= bestBlock) {
const block = await this.getBlock(currentHeight);
if (!block) {
await (0, utils_1.wait)(1000);
continue;
}
const { convertedBlock, convertedTxs } = await this.convertBlock(block);
await this.processBlock(convertedBlock, convertedTxs);
if (currentHeight === bestBlock) {
bestBlock = await this.web3.eth.getBlockNumber();
}
tip = await __1.ChainStateProvider.getLocalTip({ chain, network });
currentHeight = tip ? tip.height + 1 : 0;
const oneSecond = 1000;
const now = Date.now();
if (now - lastLog > oneSecond) {
const blocksProcessed = currentHeight - startHeight;
const elapsedMinutes = (now - startTime) / (60 * oneSecond);
logger_2.default.info(`${(0, logger_1.timestamp)()} | Syncing... | Chain: ${chain} | Network: ${network} |${(blocksProcessed / elapsedMinutes)
.toFixed(2)
.padStart(8)} blocks/min | Height: ${currentHeight.toString().padStart(7)}`);
lastLog = Date.now();
}
}
}
catch (err) {
logger_2.default.error(`Error syncing ${chain} ${network} -- %o`, err);
await (0, utils_1.wait)(2000);
this.syncing = false;
return this.sync();
}
logger_2.default.info(`${chain}:${network} up to date.`);
this.syncing = false;
state_1.StateStorage.collection.findOneAndUpdate({}, { $addToSet: { initialSyncComplete: `${chain}:${network}` } }, { upsert: true });
this.events.emit('SYNCDONE');
return true;
}
async syncDone() {
return new Promise(resolve => this.events.once('SYNCDONE', resolve));
}
getBlockReward(block) {
// TODO: implement block reward
block;
return 0;
}
async convertBlock(block) {
const blockTime = Number(block.timestamp) * 1000;
const hash = block.hash;
const height = block.number;
const reward = this.getBlockReward(block);
const convertedBlock = {
chain: this.chain,
network: this.network,
height,
hash,
coinbase: Buffer.from(block.miner),
merkleRoot: Buffer.from(block.transactionsRoot),
time: new Date(blockTime),
timeNormalized: new Date(blockTime),
nonce: Buffer.from(block.extraData),
previousBlockHash: block.parentHash,
difficulty: block.difficulty,
totalDifficulty: block.totalDifficulty,
nextBlockHash: '',
transactionCount: block.transactions.length,
size: block.size,
reward,
logsBloom: Buffer.from(block.logsBloom),
sha3Uncles: Buffer.from(block.sha3Uncles),
receiptsRoot: Buffer.from(block.receiptsRoot),
processed: false,
gasLimit: block.gasLimit,
gasUsed: block.gasUsed,
stateRoot: Buffer.from(block.stateRoot)
};
const convertedTxs = block.transactions.map(t => this.txModel.convertRawTx(this.chain, this.network, t, convertedBlock));
const traceTxs = await this.rpc.getTransactionsFromBlock(convertedBlock.height);
this.rpc.reconcileTraces(convertedBlock, convertedTxs, traceTxs);
this.txModel.addEffectsToTxs(convertedTxs);
return { convertedBlock, convertedTxs };
}
async stop() {
this.stopping = true;
this.multiThreadSync.stop();
logger_2.default.debug(`Stopping worker for chain ${this.chain} ${this.network}`);
await this.disconnect();
}
async start() {
logger_2.default.debug(`Started worker for chain ${this.chain} ${this.network}`);
this.setupListeners();
await this.connect();
this.sync();
}
}
exports.EVMP2pWorker = EVMP2pWorker;
//# sourceMappingURL=p2p.js.map