// // Copyright 2021 Vulcanize, Inc. // import { IPLDBlockInterface, StateKind } from '@cerc-io/util'; import assert from 'assert'; import * as codec from '@ipld/dag-cbor'; import _ from 'lodash'; import { Indexer, ResultEvent } from './indexer'; const IPLD_BATCH_BLOCKS = 10000; /** * Hook function to store an initial state. * @param indexer Indexer instance. * @param blockHash Hash of the concerned block. * @param contractAddress Address of the concerned contract. * @returns Data block to be stored. */ export async function createInitialState (indexer: Indexer, contractAddress: string, blockHash: string): Promise { assert(indexer); assert(blockHash); assert(contractAddress); // Store an empty state in an IPLDBlock. const ipldBlockData: any = { state: {} }; // Return initial state data to be saved. return ipldBlockData; } /** * Hook function to create state diff. * @param indexer Indexer instance that contains methods to fetch the contract varaiable values. * @param blockHash Block hash of the concerned block. */ export async function createStateDiff (indexer: Indexer, blockHash: string): Promise { assert(indexer); assert(blockHash); // Use indexer.createDiff() method to save custom state diff(s). } /** * Hook function to create state checkpoint * @param indexer Indexer instance. * @param contractAddress Address of the concerned contract. * @param blockHash Block hash of the concerned block. * @returns Whether to disable default checkpoint. If false, the state from this hook is updated with that from default checkpoint. */ export async function createStateCheckpoint (indexer: Indexer, contractAddress: string, blockHash: string): Promise { assert(indexer); assert(blockHash); assert(contractAddress); // TODO: Pass blockProgress instead of blockHash to hook method. const block = await indexer.getBlockProgress(blockHash); assert(block); // Fetch the latest 'checkpoint' | 'init' for the contract to fetch diffs after it. let prevNonDiffBlock: IPLDBlockInterface; let diffStartBlockNumber: number; const checkpointBlock = await indexer.getLatestIPLDBlock(contractAddress, StateKind.Checkpoint, block.blockNumber - 1); if (checkpointBlock) { const checkpointBlockNumber = checkpointBlock.block.blockNumber; prevNonDiffBlock = checkpointBlock; diffStartBlockNumber = checkpointBlockNumber; // Update IPLD status map with the latest checkpoint info. // Essential while importing state as checkpoint at the snapshot block is added by import-state CLI. // (job-runner won't have the updated ipld status) indexer.updateIPLDStatusMap(contractAddress, { checkpoint: checkpointBlockNumber }); } else { // There should be an initial state at least. const initBlock = await indexer.getLatestIPLDBlock(contractAddress, StateKind.Init); assert(initBlock, 'No initial state found'); prevNonDiffBlock = initBlock; // Take block number previous to initial state block to include any diff state at that block. diffStartBlockNumber = initBlock.block.blockNumber - 1; } const prevNonDiffBlockData = codec.decode(Buffer.from(prevNonDiffBlock.data)) as any; const data = { state: prevNonDiffBlockData.state }; console.time('time:hooks#createStateCheckpoint'); // Fetching and merging all diff blocks after the latest 'checkpoint' | 'init' in batch. for (let i = diffStartBlockNumber; i < block.blockNumber;) { const endBlockHeight = Math.min(i + IPLD_BATCH_BLOCKS, block.blockNumber); console.time(`time:hooks#createStateCheckpoint-batch-merge-diff-${i}-${endBlockHeight}`); const diffBlocks = await indexer.getDiffIPLDBlocksInRange(contractAddress, i, endBlockHeight); // Merge all diff blocks after previous checkpoint. for (const diffBlock of diffBlocks) { const diff = codec.decode(Buffer.from(diffBlock.data)) as any; data.state = _.merge(data.state, diff.state); } console.timeEnd(`time:hooks#createStateCheckpoint-batch-merge-diff-${i}-${endBlockHeight}`); i = endBlockHeight; } console.time('time:hooks#createStateCheckpoint-db-save-checkpoint'); await indexer.createStateCheckpoint(contractAddress, blockHash, data); console.timeEnd('time:hooks#createStateCheckpoint-db-save-checkpoint'); console.timeEnd('time:hooks#createStateCheckpoint'); return true; } /** * Event hook function. * @param indexer Indexer instance that contains methods to fetch and update the contract values in the database. * @param eventData ResultEvent object containing event information. */ export async function handleEvent (indexer: Indexer, eventData: ResultEvent): Promise { assert(indexer); assert(eventData); }