diff --git a/src/main.ts b/src/main.ts index 577ce66..338fdf5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,6 +12,10 @@ program .option('-c, --config [path to config.json]', 'Path to config.json file', 'config.json') .action(async (options) => { const config: TelosEvmConfig = JSON.parse(readFileSync(options.config).toString()); + + if (!config.elasticIndexDocsAmount) + config.elasticIndexDocsAmount = 1e7; + const rpc: TelosEVMRPC = new TelosEVMRPC(config); console.log("Starting Telos EVM RPC..."); diff --git a/src/routes/evm/index.ts b/src/routes/evm/index.ts index a744540..c12bc3f 100644 --- a/src/routes/evm/index.ts +++ b/src/routes/evm/index.ts @@ -200,8 +200,7 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { } } - async function getVRS(receiptDoc): Promise { - let receipt = receiptDoc["@raw"]; + function getVRS(receipt): {v: string, r: string, s: string} { const v = removeLeftZeros(BigInt(receipt.v).toString(16), true); const r = removeLeftZeros(receipt.r, true); const s = removeLeftZeros(receipt.s, true); @@ -292,8 +291,8 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { } function adjustBlockNum(num: number): number { - // convert to native block num and divide over index size 10 million - return Math.floor((num + opts.blockNumberDelta) / 1e7); + // convert to native block num and divide over dos per index + return Math.floor((num + opts.blockNumberDelta) / opts.elasticIndexDocsAmount); } function indexSuffixForBlock(blockNumber: number): string { @@ -302,7 +301,7 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { } async function getDeltaDocFromNumber(blockNumber: number, retry: number = 0) { - const indexSuffix = indexSuffixForBlock(blockNumber); + const indexSuffix = indexSuffixForBlock(blockNumber + opts.blockNumberDelta); const results = await fastify.elastic.search({ index: `${opts.elasticIndexPrefix}-delta-${opts.elasticIndexVersion}-${indexSuffix}`, size: 1, @@ -325,7 +324,67 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { return blockDelta; } - async function emptyBlockFromDelta(blockDelta: any) { + function getIndexSuffixesForBlocks(blockNumbers: number[]) { + const blocksPerSuff = new Map(); + for (const blockNum of blockNumbers) { + const indexSuff = indexSuffixForBlock(blockNum + opts.blockNumberDelta); + const blocks = [blockNum]; + if (blocksPerSuff.has(indexSuff)) + blocksPerSuff.get(indexSuff).push(...blocks); + else + blocksPerSuff.set(indexSuff, blocks); + } + return blocksPerSuff; + } + + async function getMultipleReceipts(blockNumbers: number[]) { + const receipts = []; + for (const [suffix, blocks] of getIndexSuffixesForBlocks(blockNumbers).entries()) { + const results = await fastify.elastic.search({ + index: `${opts.elasticIndexPrefix}-action-${opts.elasticIndexVersion}-${suffix}`, + size: 2000, + query: { + terms: {'@raw.block': blocks} + } + }).catch(err => { + console.error(err); + throw err; + }); + receipts.push( + ...results.hits.hits.map(h => h._source)); + } + return receipts; + } + + async function getMultipleDeltaDocsFromNumbers(blockNumbers: number[]) { + const blocksWithReceipts = []; + const blocks = []; + for (const [suffix, blockNums] of getIndexSuffixesForBlocks(blockNumbers).entries()) { + const searchBlock = await fastify.elastic.search({ + index: `${opts.elasticIndexPrefix}-delta-${opts.elasticIndexVersion}-${suffix}`, + size: blockNums.length, + query: { + terms: {"@global.block_num": blockNums} + } + }).catch(error => { + console.error(error); + throw error; + }); + for (const hit of searchBlock.hits.hits) { + const doc: any = hit._source; + if (doc.txAmount) + blocksWithReceipts.push(doc['@global'].block_num) + blocks.push(doc); + } + } + let receipts = []; + if (blocksWithReceipts.length > 0) + receipts = await getMultipleReceipts(blocksWithReceipts); + + return [blocks, receipts]; + } + + function emptyBlockFromDelta(blockDelta: any) { const blockNumberHex = addHexPrefix(blockDelta['@global'].block_num.toString(16)); const timestamp = new Date(blockDelta['@timestamp']).getTime() / 1000; const parentHash = addHexPrefix(blockDelta['@evmPrevBlockHash']); @@ -350,7 +409,7 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { if (!blockDelta) return null; - return await emptyBlockFromDelta(blockDelta); + return emptyBlockFromDelta(blockDelta); } catch (e) { Logger.error(e); return null; @@ -373,7 +432,7 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { return null; } - return await emptyBlockFromDelta(blockDelta); + return emptyBlockFromDelta(blockDelta); } catch (e) { Logger.error(e); return null; @@ -381,7 +440,7 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { } - async function reconstructBlockFromReceipts(receipts: any[], full: boolean, client: any) { + function reconstructBlockFromReceipts(block, receipts: any[], full: boolean, client: any) { try { let blockHash; let blockHex: string; @@ -390,9 +449,8 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { let bloom = new Bloom(); const trxs = []; //Logger.debug(`Reconstructing block from receipts: ${JSON.stringify(receipts)}`) - for (const receiptDoc of receipts) { - const {v, r, s} = await getVRS(receiptDoc._source); - const receipt = receiptDoc._source['@raw']; + for (const receipt of receipts) { + const {v, r, s} = getVRS(receipt); if (!blockHash) { blockHash = addHexPrefix(receipt['block_hash']); @@ -431,11 +489,6 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { } } - const block = await getDeltaDocFromNumber(blockNum); - if(!block){ - Logger.error("Could not find block for receipts"); - return null; - } const timestamp = new Date(block['@timestamp']).getTime() / 1000; const gasUsedBlock = addHexPrefix(removeLeftZeros(new BN(block['gasUsed']).toString('hex'))); const extraData = addHexPrefix(block['@blockHash']); @@ -473,7 +526,7 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { size: 2000, query: { bool: { must: [{ term: termStruct }] } } }); - return results?.hits?.hits; + return results?.hits?.hits.map(h => h._source['@raw']); } async function getCurrentBlockNumber(indexed: boolean = false, retry: number = 0) { @@ -514,7 +567,7 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { if (indices.length > 1) { const docsCount = parseInt(index['docs.count']); const adjustedNum = indexToSuffixNum(index.index); - lastBlockNum = (adjustedNum * 1e7) + docsCount - opts.blockNumberDelta - 1; + lastBlockNum = (adjustedNum * opts.elasticIndexDocsAmount) + docsCount - opts.blockNumberDelta - 1; } else if (index?.index) { const results = await fastify.elastic.search({ index: `${index.index}`, @@ -1173,7 +1226,7 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { // lookup raw action const receiptAction = await searchActionByHash(trxHash, client); if (!receiptAction) return null; - const {v, r, s} = await getVRS(receiptAction); + const {v, r, s} = getVRS(receiptAction); const receipt = receiptAction['@raw']; // lookup receipt delta @@ -1210,10 +1263,20 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { _hash = _hash.slice(2); } const receipts = await getReceiptsByTerm("@raw.block_hash", _hash); - const block = receipts.length > 0 ? await reconstructBlockFromReceipts(receipts, true, client) : await emptyBlockFromHash(_hash); - if(!block || block.transactions?.length === 0) return null; + let block; + if (receipts.length == 0) + block = await emptyBlockFromHash(_hash); + if (!block) return null; + + else { + const blockNum = receipts[0].block; + const delta = await getDeltaDocFromNumber(blockNum); + if (!delta) return null; + block = reconstructBlockFromReceipts(delta, receipts, true, client); + } + const trxIndex = parseInt(trxIndexHex, 16); - if(!block.transactions[trxIndex]) return null; + if(!block.transactions[trxIndex]) return null; let trx = block.transactions[trxIndex]; trx.type = "0x0"; // TODO: Determine type with 1559 trx.chainId = CHAIN_ID_HEX; @@ -1229,7 +1292,6 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { if (blockNumber === 0) return GENESIS_BLOCK; - try { const blockDelta = await getDeltaDocFromNumber(blockNumber); if(!blockDelta){ @@ -1238,6 +1300,10 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { } if (blockDelta['@transactionsRoot'] === NULL_TRIE) return emptyBlockFromDelta(blockDelta); + else { + const receipts = await getReceiptsByTerm("@raw.block", blockNumber); + return receipts.length > 0 ? reconstructBlockFromReceipts(blockDelta, receipts, full, client) : await emptyBlockFromNumber(blockNumber); + } } catch (e) { Logger.error(`Could not find block from block number ${blockNumber}: ${e}`); if(e?.message.startsWith("index_not_found_exception")){ @@ -1245,9 +1311,6 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { } throw(e); } - - const receipts = await getReceiptsByTerm("@raw.block", blockNumber); - return receipts.length > 0 ? await reconstructBlockFromReceipts(receipts, full, client) : await emptyBlockFromNumber(blockNumber); }); /** @@ -1262,7 +1325,16 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { _hash = _hash.slice(2); } const receipts = await getReceiptsByTerm("@raw.block_hash", _hash); - return receipts.length > 0 ? await reconstructBlockFromReceipts(receipts, full, client) : await emptyBlockFromHash(_hash); + let block; + if (receipts.length == 0) + block = await emptyBlockFromHash(_hash); + + else { + const blockNum = receipts[0].block; + const delta = await getDeltaDocFromNumber(blockNum - opts.blockNumberDelta); + block = reconstructBlockFromReceipts(delta, receipts, true, client); + } + return block; }); /** @@ -1648,8 +1720,7 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { throw new Error("trace_replayBlockTransactions only supports the \"trace\" type of trace (not vmTrace or stateDiff"); const blockNumber = parseInt(await toBlockNumber(block), 16); - const receiptHits = await getReceiptsByTerm("@raw.block", blockNumber); - const receipts = receiptHits.map(r => r._source["@raw"]); + const receipts = await getReceiptsByTerm("@raw.block", blockNumber); const sortedReceipts = receipts.sort((a, b) => { return a.trx_index - b.trx_index; }) @@ -1666,8 +1737,7 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { methods.set('trace_block', async ([block]) => { const blockNumber = parseInt(await toBlockNumber(block), 16); - const receiptHits = await getReceiptsByTerm("@raw.block", blockNumber); - const receipts = receiptHits.map(r => r._source["@raw"]); + const receipts = await getReceiptsByTerm("@raw.block", blockNumber); const sortedReceipts = receipts.sort((a, b) => { return a.trx_index - b.trx_index; }) @@ -1796,12 +1866,51 @@ export default async function (fastify: FastifyInstance, opts: TelosEvmConfig) { const tRef = process.hrtime.bigint(); - let promises = []; + const promises = []; + // gather all getBlockByNumber requests + const requestedBlockNums = []; + // used to order responses after async processing + const respMap = new Map(); + + // generate promises for request resolution for (let i = 0; i < payload.length; i++) { - let promise = doRpcMethod(payload[i], clientInfo, reply); - promises.push(promise); + let { jsonrpc, id, method, params } = payload[i]; + if (method === 'eth_getBlockByNumber') { + // gather block request info for later + requestedBlockNums.push({params, index: i}); + } else { + // normal request + promises.push(doRpcMethod(payload[i], clientInfo, reply).then(resp => respMap.set(i, resp))); + } + } + + if (requestedBlockNums.length > 0) { + // perform optimized batch get block by number request + promises.push( + getMultipleDeltaDocsFromNumbers(requestedBlockNums.map(req => req.params[0])).then( + blockData => { + const [blocks, allReceipts] = blockData; + for (const delta of blocks) { + const blockNum = delta['@global'].block_num; + const request = requestedBlockNums.find(req => req.params[0] == blockNum); + if (!delta.txAmount) { + respMap.set(request.index, emptyBlockFromDelta(delta)); + continue; + } + + const receipts = allReceipts.find(r => r["@raw"].block == blockNum); + respMap.set(request.index, reconstructBlockFromReceipts(delta, receipts, request.params[1], clientInfo)); + } + } + )); } - let responses = await Promise.all(promises); + + await Promise.all(promises); + + // gather results in same order user requested + const responses = [] + for (let i = 0; i < respMap.size; i++) + responses.push(respMap.get(i)); const duration = ((Number(process.hrtime.bigint()) - Number(tRef)) / 1000).toFixed(3); Logger.log(`RPCREQUESTBATCH: ${new Date().toISOString()} - ${duration} μs - ${ip} (${usage}/${limit}) - ${origin} - BATCH OF ${responses.length}`); diff --git a/src/types/index.d.ts b/src/types/index.d.ts index de3de1b..322f67f 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -31,6 +31,7 @@ export interface TelosEvmConfig { elasticPass: string; elasticIndexPrefix: string; elasticIndexVersion: string; + elasticIndexDocsAmount: number; orderNonces: boolean; orderNonceRetryTimeout: number; syncingThreshhold: number;