Store initial state and refactor IPLDBlocks related code (#76)

* Process hooks for the first block in the watchers

* Store initial state from a hook in an IPLDBlock for eden-watcher

* Extract watcher method to prepare IPLDBlocks in util

* Extract common IPLDBlocks related code in util

* Move IPFSClient to util

* Use constants for state kind
This commit is contained in:
prathamesh0
2021-12-28 16:08:05 +05:30
committed by nabarun
parent 4c422e3ea2
commit cb2fe2aa45
23 changed files with 877 additions and 942 deletions
+3
View File
@@ -14,3 +14,6 @@ export * from './src/indexer';
export * from './src/job-runner';
export * from './src/ipld-helper';
export * from './src/graph-decimal';
export * from './src/ipld-indexer';
export * from './src/ipld-database';
export * from './src/ipfs';
+5
View File
@@ -23,3 +23,8 @@ export const UNKNOWN_EVENT_NAME = '__unknown__';
export const KIND_ACTIVE = 'active';
export const KIND_LAZY = 'lazy';
export const STATE_KIND_INIT = 'init';
export const STATE_KIND_DIFF_STAGED = 'diff_staged';
export const STATE_KIND_DIFF = 'diff';
export const STATE_KIND_CHECKPOINT = 'checkpoint';
+17
View File
@@ -0,0 +1,17 @@
//
// Copyright 2021 Vulcanize, Inc.
//
import { create, IPFSHTTPClient } from 'ipfs-http-client';
export class IPFSClient {
_client: IPFSHTTPClient
constructor (url: string) {
this._client = create({ url });
}
async push (data: any): Promise<void> {
await this._client.dag.put(data, { format: 'dag-cbor', hashAlg: 'sha2-256' });
}
}
+154
View File
@@ -0,0 +1,154 @@
//
// Copyright 2021 Vulcanize, Inc.
//
import { FindConditions, MoreThan, Repository } from 'typeorm';
import { IPLDBlockInterface, HookStatusInterface } from './types';
import { Database } from './database';
import { MAX_REORG_DEPTH, STATE_KIND_DIFF_STAGED, STATE_KIND_DIFF } from './constants';
export class IPLDDatabase extends Database {
async getLatestIPLDBlock (repo: Repository<IPLDBlockInterface>, contractAddress: string, kind: string | null, blockNumber?: number): Promise<IPLDBlockInterface | undefined> {
let queryBuilder = repo.createQueryBuilder('ipld_block')
.leftJoinAndSelect('ipld_block.block', 'block')
.where('block.is_pruned = false')
.andWhere('ipld_block.contract_address = :contractAddress', { contractAddress })
.orderBy('block.block_number', 'DESC');
// Filter out blocks after the provided block number.
if (blockNumber) {
queryBuilder.andWhere('block.block_number <= :blockNumber', { blockNumber });
}
// Filter using kind if specified else order by id to give preference to checkpoint.
queryBuilder = kind
? queryBuilder.andWhere('ipld_block.kind = :kind', { kind })
: queryBuilder.andWhere('ipld_block.kind != :kind', { kind: STATE_KIND_DIFF_STAGED })
.addOrderBy('ipld_block.id', 'DESC');
return queryBuilder.getOne();
}
async getPrevIPLDBlock (repo: Repository<IPLDBlockInterface>, blockHash: string, contractAddress: string, kind?: string): Promise<IPLDBlockInterface | undefined> {
const heirerchicalQuery = `
WITH RECURSIVE cte_query AS
(
SELECT
b.block_hash,
b.block_number,
b.parent_hash,
1 as depth,
i.id,
i.kind
FROM
block_progress b
LEFT JOIN
ipld_block i ON i.block_id = b.id
AND i.contract_address = $2
WHERE
b.block_hash = $1
UNION ALL
SELECT
b.block_hash,
b.block_number,
b.parent_hash,
c.depth + 1,
i.id,
i.kind
FROM
block_progress b
LEFT JOIN
ipld_block i
ON i.block_id = b.id
AND i.contract_address = $2
INNER JOIN
cte_query c ON c.parent_hash = b.block_hash
WHERE
c.depth < $3
)
SELECT
block_number, id, kind
FROM
cte_query
ORDER BY block_number DESC, id DESC
`;
// Fetching block and id for previous IPLDBlock in frothy region.
const queryResult = await repo.query(heirerchicalQuery, [blockHash, contractAddress, MAX_REORG_DEPTH]);
const latestRequiredResult = kind
? queryResult.find((obj: any) => obj.kind === kind)
: queryResult.find((obj: any) => obj.id);
let result: IPLDBlockInterface | undefined;
if (latestRequiredResult) {
result = await repo.findOne(latestRequiredResult.id, { relations: ['block'] });
} else {
// If IPLDBlock not found in frothy region get latest IPLDBlock in the pruned region.
// Filter out IPLDBlocks from pruned blocks.
const canonicalBlockNumber = queryResult.pop().block_number + 1;
let queryBuilder = repo.createQueryBuilder('ipld_block')
.leftJoinAndSelect('ipld_block.block', 'block')
.where('block.is_pruned = false')
.andWhere('ipld_block.contract_address = :contractAddress', { contractAddress })
.andWhere('block.block_number <= :canonicalBlockNumber', { canonicalBlockNumber })
.orderBy('block.block_number', 'DESC');
// Filter using kind if specified else order by id to give preference to checkpoint.
queryBuilder = kind
? queryBuilder.andWhere('ipld_block.kind = :kind', { kind })
: queryBuilder.addOrderBy('ipld_block.id', 'DESC');
result = await queryBuilder.getOne();
}
return result;
}
async getIPLDBlocks (repo: Repository<IPLDBlockInterface>, where: FindConditions<IPLDBlockInterface>): Promise<IPLDBlockInterface[]> {
return repo.find({ where, relations: ['block'] });
}
async getDiffIPLDBlocksByBlocknumber (repo: Repository<IPLDBlockInterface>, contractAddress: string, blockNumber: number): Promise<IPLDBlockInterface[]> {
return repo.find({
relations: ['block'],
where: {
contractAddress,
kind: STATE_KIND_DIFF,
block: {
isPruned: false,
blockNumber: MoreThan(blockNumber)
}
},
order: {
block: 'ASC'
}
});
}
async saveOrUpdateIPLDBlock (repo: Repository<IPLDBlockInterface>, ipldBlock: IPLDBlockInterface): Promise<IPLDBlockInterface> {
return repo.save(ipldBlock);
}
async getHookStatus (repo: Repository<HookStatusInterface>): Promise<HookStatusInterface | undefined> {
return repo.findOne();
}
async updateHookStatusProcessedBlock (repo: Repository<HookStatusInterface>, blockNumber: number, force?: boolean): Promise<HookStatusInterface> {
let entity = await repo.findOne();
if (!entity) {
entity = repo.create({
latestProcessedBlockNumber: blockNumber
});
}
if (force || blockNumber > entity.latestProcessedBlockNumber) {
entity.latestProcessedBlockNumber = blockNumber;
}
return repo.save(entity);
}
}
+396
View File
@@ -0,0 +1,396 @@
//
// Copyright 2021 Vulcanize, Inc.
//
import assert from 'assert';
import { ethers } from 'ethers';
import { sha256 } from 'multiformats/hashes/sha2';
import { CID } from 'multiformats/cid';
import _ from 'lodash';
import { EthClient } from '@vulcanize/ipld-eth-client';
import * as codec from '@ipld/dag-cbor';
import {
IPLDDatabaseInterface,
IndexerInterface,
BlockProgressInterface,
IPLDBlockInterface,
HookStatusInterface
} from './types';
import { Indexer } from './indexer';
import { ServerConfig } from './config';
import { IPFSClient } from './ipfs';
import {
STATE_KIND_INIT,
STATE_KIND_DIFF_STAGED,
STATE_KIND_DIFF,
STATE_KIND_CHECKPOINT
} from './constants';
import { JobQueue } from './job-queue';
export class IPLDIndexer extends Indexer {
_serverConfig: ServerConfig;
_ipldDb: IPLDDatabaseInterface;
_ipfsClient: IPFSClient;
constructor (
serverConfig: ServerConfig,
ipldDb: IPLDDatabaseInterface,
ethClient: EthClient,
postgraphileClient: EthClient,
ethProvider: ethers.providers.BaseProvider,
jobQueue: JobQueue,
ipfsClient: IPFSClient
) {
super(ipldDb, ethClient, postgraphileClient, ethProvider, jobQueue);
this._serverConfig = serverConfig;
this._ipldDb = ipldDb;
this._ipfsClient = ipfsClient;
}
getIPLDData (ipldBlock: IPLDBlockInterface): any {
return codec.decode(Buffer.from(ipldBlock.data));
}
async pushToIPFS (data: any): Promise<void> {
await this._ipfsClient.push(data);
}
isIPFSConfigured (): boolean {
const ipfsAddr = this._serverConfig.ipfsApiAddr;
// Return false if ipfsAddr is undefined | null | empty string.
return (ipfsAddr !== undefined && ipfsAddr !== null && ipfsAddr !== '');
}
async getLatestHooksProcessedBlock (hookStatus: HookStatusInterface): Promise<BlockProgressInterface> {
const blocksAtHeight = await this.getBlocksAtHeight(hookStatus.latestProcessedBlockNumber, false);
// There can exactly one block at hookStatus.latestProcessedBlockNumber height.
assert(blocksAtHeight.length === 1);
return blocksAtHeight[0];
}
async processCheckpoint (indexer: IndexerInterface, blockHash: string, checkpointInterval: number): Promise<void> {
// Get all the contracts.
assert(this._ipldDb.getContracts);
const contracts = await this._ipldDb.getContracts();
// For each contract, merge the diff till now to create a checkpoint.
for (const contract of contracts) {
// Check if contract has checkpointing on.
if (contract.checkpoint) {
await this.createCheckpoint(indexer, contract.address, blockHash, null, checkpointInterval);
}
}
}
async processCLICheckpoint (indexer: IndexerInterface, contractAddress: string, blockHash?: string): Promise<string | undefined> {
const checkpointBlockHash = await this.createCheckpoint(indexer, contractAddress, blockHash);
assert(checkpointBlockHash);
// Push checkpoint to IPFS if configured.
if (this.isIPFSConfigured()) {
const block = await this.getBlockProgress(checkpointBlockHash);
const checkpointIPLDBlocks = await this._ipldDb.getIPLDBlocks({ block, contractAddress, kind: STATE_KIND_CHECKPOINT });
// There can be at most one IPLDBlock for a (block, contractAddress, kind) combination.
assert(checkpointIPLDBlocks.length <= 1);
const checkpointIPLDBlock = checkpointIPLDBlocks[0];
const checkpointData = this.getIPLDData(checkpointIPLDBlock);
await this.pushToIPFS(checkpointData);
}
return checkpointBlockHash;
}
async createInit (
indexer: IndexerInterface,
blockHash: string,
blockNumber: number
): Promise<void> {
// Get all the contracts.
assert(this._ipldDb.getContracts);
const contracts = await this._ipldDb.getContracts();
// Create an initial state for each contract.
for (const contract of contracts) {
// Check if contract has checkpointing on.
if (contract.checkpoint) {
// Check if a 'diff' | 'checkpoint' ipldBlock already exists or blockNumber is < to startingBlock.
const existingIpldBlock = await this._ipldDb.getLatestIPLDBlock(contract.address, null);
if (existingIpldBlock || blockNumber < contract.startingBlock) {
continue;
}
// Call initial state hook.
assert(indexer.createInitialState);
const stateData = await indexer.createInitialState(contract.address, blockHash);
const block = await this.getBlockProgress(blockHash);
assert(block);
const ipldBlock = await this.prepareIPLDBlock(block, contract.address, stateData, STATE_KIND_INIT);
await this.saveOrUpdateIPLDBlock(ipldBlock);
// Push initial state to IPFS if configured.
if (this.isIPFSConfigured()) {
const ipldData = this.getIPLDData(ipldBlock);
await this.pushToIPFS(ipldData);
}
}
}
}
async createDiffStaged (contractAddress: string, blockHash: string, data: any): Promise<void> {
const block = await this.getBlockProgress(blockHash);
assert(block);
// Create a staged diff block.
const ipldBlock = await this.prepareIPLDBlock(block, contractAddress, data, STATE_KIND_DIFF_STAGED);
await this.saveOrUpdateIPLDBlock(ipldBlock);
}
async finalizeDiffStaged (blockHash: string): Promise<void> {
const block = await this.getBlockProgress(blockHash);
assert(block);
// Get all the staged diff blocks for the given blockHash.
const stagedBlocks = await this._ipldDb.getIPLDBlocks({ block, kind: STATE_KIND_DIFF_STAGED });
// For each staged block, create a diff block.
for (const stagedBlock of stagedBlocks) {
const data = codec.decode(Buffer.from(stagedBlock.data));
await this.createDiff(stagedBlock.contractAddress, stagedBlock.block.blockHash, data);
}
// Remove all the staged diff blocks for current blockNumber.
await this.removeIPLDBlocks(block.blockNumber, STATE_KIND_DIFF_STAGED);
}
async createDiff (contractAddress: string, blockHash: string, data: any): Promise<void> {
const block = await this.getBlockProgress(blockHash);
assert(block);
// Fetch the latest checkpoint for the contract.
const checkpoint = await this._ipldDb.getLatestIPLDBlock(contractAddress, STATE_KIND_CHECKPOINT);
// There should be an initial state at least.
if (!checkpoint) {
// Fetch the initial state for the contract.
const initState = await this._ipldDb.getLatestIPLDBlock(contractAddress, STATE_KIND_INIT);
assert(initState, 'No initial state found');
} else {
// Check if the latest checkpoint is in the same block.
assert(checkpoint.block.blockHash !== block.blockHash, 'Checkpoint already created for the block hash');
}
const ipldBlock = await this.prepareIPLDBlock(block, contractAddress, data, STATE_KIND_DIFF);
await this.saveOrUpdateIPLDBlock(ipldBlock);
}
async createCheckpoint (indexer: IndexerInterface, contractAddress: string, blockHash?: string, data?: any, checkpointInterval?: number): Promise<string | undefined> {
// Get current hookStatus.
const hookStatus = await this._ipldDb.getHookStatus();
assert(hookStatus);
// Getting the current block.
let currentBlock;
if (blockHash) {
currentBlock = await this.getBlockProgress(blockHash);
} else {
// In case of empty blockHash from checkpoint CLI, get the latest processed block from hookStatus for the checkpoint.
currentBlock = await this.getLatestHooksProcessedBlock(hookStatus);
}
assert(currentBlock);
// Data is passed in case of checkpoint hook.
if (data) {
// Create a checkpoint from the hook data without being concerned about diffs.
const ipldBlock = await this.prepareIPLDBlock(currentBlock, contractAddress, data, STATE_KIND_CHECKPOINT);
await this.saveOrUpdateIPLDBlock(ipldBlock);
return;
}
// If data is not passed, create from previous 'checkpoint' | 'init' and diffs after that.
// Make sure the block is marked complete.
assert(currentBlock.isComplete, 'Block for a checkpoint should be marked as complete');
// Make sure the hooks have been processed for the block.
assert(currentBlock.blockNumber <= hookStatus.latestProcessedBlockNumber, 'Block for a checkpoint should have hooks processed');
// Call state checkpoint hook and check if default checkpoint is disabled.
assert(indexer.createStateCheckpoint);
const disableDefaultCheckpoint = await indexer.createStateCheckpoint(contractAddress, currentBlock.blockHash);
if (disableDefaultCheckpoint) {
// Return if default checkpoint is disabled.
// Return block hash for checkpoint CLI.
return currentBlock.blockHash;
}
// Fetch the latest 'checkpoint' | 'init' for the contract.
let prevNonDiffBlock: IPLDBlockInterface;
let getDiffBlockNumber: number;
const checkpointBlock = await this._ipldDb.getLatestIPLDBlock(contractAddress, STATE_KIND_CHECKPOINT, currentBlock.blockNumber);
if (checkpointBlock) {
prevNonDiffBlock = checkpointBlock;
getDiffBlockNumber = checkpointBlock.block.blockNumber;
// Check (only if checkpointInterval is passed) if it is time for a new checkpoint.
if (checkpointInterval && checkpointBlock.block.blockNumber > (currentBlock.blockNumber - checkpointInterval)) {
return;
}
} else {
// There should be an initial state at least.
const initBlock = await this._ipldDb.getLatestIPLDBlock(contractAddress, STATE_KIND_INIT);
assert(initBlock, 'No initial state found');
prevNonDiffBlock = initBlock;
// Take block number previous to initial state block to get diffs after that.
getDiffBlockNumber = initBlock.block.blockNumber - 1;
}
// Fetching all diff blocks after the latest 'checkpoint' | 'init'.
const diffBlocks = await this._ipldDb.getDiffIPLDBlocksByBlocknumber(contractAddress, getDiffBlockNumber);
const prevNonDiffBlockData = codec.decode(Buffer.from(prevNonDiffBlock.data)) as any;
data = {
state: prevNonDiffBlockData.state
};
for (const diffBlock of diffBlocks) {
const diff = codec.decode(Buffer.from(diffBlock.data)) as any;
data.state = _.merge(data.state, diff.state);
}
const ipldBlock = await this.prepareIPLDBlock(currentBlock, contractAddress, data, STATE_KIND_CHECKPOINT);
await this.saveOrUpdateIPLDBlock(ipldBlock);
return currentBlock.blockHash;
}
async prepareIPLDBlock (block: BlockProgressInterface, contractAddress: string, data: any, kind: string):Promise<any> {
assert(_.includes([
STATE_KIND_INIT,
STATE_KIND_DIFF_STAGED,
STATE_KIND_DIFF,
STATE_KIND_CHECKPOINT
], kind));
// Get an existing 'init' | 'diff' | 'diff_staged' | 'checkpoint' IPLDBlock for current block, contractAddress.
const currentIPLDBlocks = await this._ipldDb.getIPLDBlocks({ block, contractAddress, kind });
// There can be at most one IPLDBlock for a (block, contractAddress, kind) combination.
assert(currentIPLDBlocks.length <= 1);
const currentIPLDBlock = currentIPLDBlocks[0];
// Update currentIPLDBlock of same kind if it exists.
let ipldBlock;
if (currentIPLDBlock) {
ipldBlock = currentIPLDBlock;
// Update the data field.
const oldData = codec.decode(Buffer.from(currentIPLDBlock.data));
data = _.merge(oldData, data);
} else {
ipldBlock = this._ipldDb.getNewIPLDBlock();
// Fetch the parent IPLDBlock.
const parentIPLDBlock = await this._ipldDb.getLatestIPLDBlock(contractAddress, null, block.blockNumber);
// Setting the meta-data for an IPLDBlock (done only once per block).
data.meta = {
id: contractAddress,
kind,
parent: {
'/': parentIPLDBlock ? parentIPLDBlock.cid : null
},
ethBlock: {
cid: {
'/': block.cid
},
num: block.blockNumber
}
};
}
// Encoding the data using dag-cbor codec.
const bytes = codec.encode(data);
// Calculating sha256 (multi)hash of the encoded data.
const hash = await sha256.digest(bytes);
// Calculating the CID: v1, code: dag-cbor, hash.
const cid = CID.create(1, codec.code, hash);
// Update ipldBlock with new data.
ipldBlock = Object.assign(ipldBlock, {
block,
contractAddress,
cid: cid.toString(),
kind: data.meta.kind,
data: Buffer.from(bytes)
});
return ipldBlock;
}
async getIPLDBlocksByHash (blockHash: string): Promise<IPLDBlockInterface[]> {
const block = await this.getBlockProgress(blockHash);
assert(block);
return this._ipldDb.getIPLDBlocks({ block });
}
async getIPLDBlockByCid (cid: string): Promise<IPLDBlockInterface | undefined> {
const ipldBlocks = await this._ipldDb.getIPLDBlocks({ cid });
// There can be only one IPLDBlock with a particular cid.
assert(ipldBlocks.length <= 1);
return ipldBlocks[0];
}
async saveOrUpdateIPLDBlock (ipldBlock: IPLDBlockInterface): Promise<IPLDBlockInterface> {
const dbTx = await this._db.createTransactionRunner();
let res;
try {
res = await this._ipldDb.saveOrUpdateIPLDBlock(dbTx, ipldBlock);
await dbTx.commitTransaction();
} catch (error) {
await dbTx.rollbackTransaction();
throw error;
} finally {
await dbTx.release();
}
return res;
}
async removeIPLDBlocks (blockNumber: number, kind: string): Promise<void> {
const dbTx = await this._db.createTransactionRunner();
try {
await this._ipldDb.removeIPLDBlocks(dbTx, blockNumber, kind);
await dbTx.commitTransaction();
} catch (error) {
await dbTx.rollbackTransaction();
throw error;
} finally {
await dbTx.release();
}
}
}
+27 -1
View File
@@ -31,6 +31,11 @@ export interface SyncStatusInterface {
latestCanonicalBlockNumber: number;
}
export interface HookStatusInterface {
id: number;
latestProcessedBlockNumber: number
}
export interface EventInterface {
id: number;
block: BlockProgressInterface;
@@ -51,6 +56,15 @@ export interface ContractInterface {
checkpoint: boolean;
}
export interface IPLDBlockInterface {
id: number;
block: BlockProgressInterface;
contractAddress: string;
cid: string;
kind: string;
data: Buffer;
}
export interface IndexerInterface {
getBlockProgress (blockHash: string): Promise<BlockProgressInterface | undefined>
getBlockProgressEntities (where: FindConditions<BlockProgressInterface>, options: FindManyOptions<BlockProgressInterface>): Promise<BlockProgressInterface[]>
@@ -72,9 +86,11 @@ export interface IndexerInterface {
parseEventNameAndArgs?: (kind: string, logObj: any) => any;
isWatchedContract?: (address: string) => Promise<ContractInterface | undefined>;
cacheContract?: (contract: ContractInterface) => void;
createDiffStaged?: (contractAddress: string, blockHash: string, data: any) => Promise<void>
watchContract?: (address: string, kind: string, checkpoint: boolean, startingBlock: number) => Promise<void>
getEntityTypesMap?: () => Map<string, { [key: string]: string }>
createDiffStaged?: (contractAddress: string, blockHash: string, data: any) => Promise<void>
createInitialState?: (contractAddress: string, blockHash: string) => Promise<any>
createStateCheckpoint?: (contractAddress: string, blockHash: string) => Promise<boolean>
}
export interface EventWatcherInterface {
@@ -108,3 +124,13 @@ export interface DatabaseInterface {
getContracts?: () => Promise<ContractInterface[]>
saveContract?: (queryRunner: QueryRunner, contractAddress: string, kind: string, checkpoint: boolean, startingBlock: number) => Promise<ContractInterface>
}
export interface IPLDDatabaseInterface extends DatabaseInterface {
getLatestIPLDBlock (contractAddress: string, kind: string | null, blockNumber?: number): Promise<IPLDBlockInterface | undefined>
getIPLDBlocks (where: FindConditions<IPLDBlockInterface>): Promise<IPLDBlockInterface[]>
getDiffIPLDBlocksByBlocknumber (contractAddress: string, blockNumber: number): Promise<IPLDBlockInterface[]>
getNewIPLDBlock (): IPLDBlockInterface
removeIPLDBlocks(dbTx: QueryRunner, blockNumber: number, kind: string): Promise<void>
saveOrUpdateIPLDBlock (dbTx: QueryRunner, ipldBlock: IPLDBlockInterface): Promise<IPLDBlockInterface>
getHookStatus (): Promise<HookStatusInterface | undefined>
}