Files
warp/src/plugins/CacheableStateEvaluator.ts
2021-08-26 10:09:33 +02:00

118 lines
4.5 KiB
TypeScript

import { BlockHeightCacheResult, BlockHeightKey, BlockHeightSwCache } from '@smartweave/cache';
import {
DefaultStateEvaluator,
EvalStateResult,
ExecutionContext,
ExecutionContextModifier,
HandlerApi
} from '@smartweave/core';
import Arweave from 'arweave';
import { GQLNodeInterface } from '@smartweave/legacy';
import { LoggerFactory } from '@smartweave/logging';
const logger = LoggerFactory.INST.create(__filename);
/**
* An implementation of DefaultStateEvaluator that adds caching capabilities
*/
export class CacheableStateEvaluator extends DefaultStateEvaluator {
constructor(
arweave: Arweave,
private readonly cache: BlockHeightSwCache<EvalStateResult<unknown>>,
executionContextModifiers: ExecutionContextModifier[] = []
) {
super(arweave, executionContextModifiers);
}
async eval<State, Api>(
executionContext: ExecutionContext<State>,
currentTx: { interactionTxId: string; contractTxId: string }[]
): Promise<EvalStateResult<State>> {
const requestedBlockHeight = executionContext.blockHeight;
logger.debug(`Requested state block height: ${requestedBlockHeight}`);
let cachedState: BlockHeightCacheResult<EvalStateResult<State>> | null = null;
const sortedInteractionsUpToBlock = executionContext.sortedInteractions.filter((tx) => {
return tx.node.block.height <= executionContext.blockHeight;
});
let missingInteractions = sortedInteractionsUpToBlock.slice();
// if there was anything to cache...
if (sortedInteractionsUpToBlock.length > 0) {
// get latest available cache for the requested block height
cachedState = this.cache.getLessOrEqual(
executionContext.contractDefinition.txId,
requestedBlockHeight
) as BlockHeightCacheResult<EvalStateResult<State>>;
if (cachedState != null) {
logger.debug(`Cached state for ${executionContext.contractDefinition.txId}`, {
block: cachedState.cachedHeight,
requestedBlockHeight
});
// verify if for the requested block height there are any interactions
// with higher block height than latest value stored in cache - basically if there are any non-cached interactions.
missingInteractions = sortedInteractionsUpToBlock.filter(
({ node }) => node.block.height > cachedState.cachedHeight && node.block.height <= requestedBlockHeight
);
}
logger.debug(`Interactions until [${requestedBlockHeight}]`, {
total: sortedInteractionsUpToBlock.length,
cached: sortedInteractionsUpToBlock.length - missingInteractions.length
});
// I (still) have no idea what I'm doing....
// TODO: this probably should be removed, as it seems to protect from
// some specific contract's implementation flaws
// (i.e. inner calls between two contracts that lead to inf. call loop - circular dependency).
// Instead - some kind of stack trace should be generated and "stackoverflow"
// exception should be thrown during contract's execution.
for (const entry of currentTx || []) {
if (entry.contractTxId === executionContext.contractDefinition.txId) {
const index = missingInteractions.findIndex((tx) => tx.node.id === entry.interactionTxId);
if (index !== -1) {
logger.debug('Inf. Loop fix - removing interaction', {
contractTxId: entry.contractTxId,
interactionTxId: entry.interactionTxId
});
missingInteractions.splice(index, 1);
}
}
}
// if cache is up-to date - return immediately to speed-up the whole process
if (missingInteractions.length === 0 && cachedState) {
logger.debug(`State up to requested height [${requestedBlockHeight}] fully cached!`);
return cachedState.cachedValue;
}
}
const baseState =
cachedState == null ? executionContext.contractDefinition.initState : cachedState.cachedValue.state;
const baseValidity = cachedState == null ? {} : cachedState.cachedValue.validity;
// eval state for the missing transactions - starting from latest value from cache.
return await this.doReadState(
missingInteractions,
new EvalStateResult(baseState, baseValidity),
executionContext,
currentTx
);
}
onStateUpdate<State>(
currentInteraction: GQLNodeInterface,
executionContext: ExecutionContext<State>,
state: EvalStateResult<State>
) {
this.cache.put(
new BlockHeightKey(executionContext.contractDefinition.txId, currentInteraction.block.height),
state
);
}
}