refactor: state cache #51
This commit is contained in:
committed by
Piotr Pędziwiatr
parent
5f1834347b
commit
fd7a63db6d
2
src/cache/BlockHeightSwCache.ts
vendored
2
src/cache/BlockHeightSwCache.ts
vendored
@@ -4,7 +4,7 @@
|
|||||||
*
|
*
|
||||||
* @typeParam V - type of value stored in cache, defaults to `any`.
|
* @typeParam V - type of value stored in cache, defaults to `any`.
|
||||||
*/
|
*/
|
||||||
export interface BlockHeightSwCache<V = any> {
|
export interface BlockHeightSwCache<V> {
|
||||||
/**
|
/**
|
||||||
* returns cached value for the highest available in cache block that is not higher than `blockHeight`.
|
* returns cached value for the highest available in cache block that is not higher than `blockHeight`.
|
||||||
*/
|
*/
|
||||||
|
|||||||
6
src/cache/impl/MemBlockHeightCache.ts
vendored
6
src/cache/impl/MemBlockHeightCache.ts
vendored
@@ -8,10 +8,14 @@ import { LoggerFactory } from '@smartweave/logging';
|
|||||||
export class MemBlockHeightSwCache<V = any> implements BlockHeightSwCache<V> {
|
export class MemBlockHeightSwCache<V = any> implements BlockHeightSwCache<V> {
|
||||||
private readonly logger = LoggerFactory.INST.create('MemBlockHeightSwCache');
|
private readonly logger = LoggerFactory.INST.create('MemBlockHeightSwCache');
|
||||||
|
|
||||||
private storage: { [key: string]: Map<number, V> } = {};
|
protected storage: { [key: string]: Map<number, V> } = {};
|
||||||
|
|
||||||
constructor(private maxStoredBlockHeights: number = Number.MAX_SAFE_INTEGER) {}
|
constructor(private maxStoredBlockHeights: number = Number.MAX_SAFE_INTEGER) {}
|
||||||
|
|
||||||
|
flush(): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
async getLast(key: string): Promise<BlockHeightCacheResult<V> | null> {
|
async getLast(key: string): Promise<BlockHeightCacheResult<V> | null> {
|
||||||
if (!(await this.contains(key))) {
|
if (!(await this.contains(key))) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -328,6 +328,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
]);
|
]);
|
||||||
this.logger.debug('contract and interactions load', benchmark.elapsed());
|
this.logger.debug('contract and interactions load', benchmark.elapsed());
|
||||||
sortedInteractions = await interactionsSorter.sort(interactions);
|
sortedInteractions = await interactionsSorter.sort(interactions);
|
||||||
|
this.logger.debug('Sorted interactions', sortedInteractions);
|
||||||
handler = (await executorFactory.create(contractDefinition)) as HandlerApi<State>;
|
handler = (await executorFactory.create(contractDefinition)) as HandlerApi<State>;
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug('State fully cached, not loading interactions.');
|
this.logger.debug('State fully cached, not loading interactions.');
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export * from './modules/impl/LexicographicalInteractionsSorter';
|
|||||||
export * from './modules/impl/DefaultCreateContract';
|
export * from './modules/impl/DefaultCreateContract';
|
||||||
export * from './modules/impl/TagsParser';
|
export * from './modules/impl/TagsParser';
|
||||||
export * from './modules/impl/normalize-source';
|
export * from './modules/impl/normalize-source';
|
||||||
|
export * from './modules/impl/StateCache';
|
||||||
|
|
||||||
export * from './ExecutionContextModifier';
|
export * from './ExecutionContextModifier';
|
||||||
export * from './SmartWeaveTags';
|
export * from './SmartWeaveTags';
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ export interface StateEvaluator {
|
|||||||
eval<State>(executionContext: ExecutionContext<State>, currentTx: CurrentTx[]): Promise<EvalStateResult<State>>;
|
eval<State>(executionContext: ExecutionContext<State>, currentTx: CurrentTx[]): Promise<EvalStateResult<State>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a hook that is called on each state update (i.e. after evaluating state for each interaction)
|
* a hook that is called on each state update (i.e. after evaluating state for each interaction transaction)
|
||||||
*/
|
*/
|
||||||
onStateUpdate<State>(
|
onStateUpdate<State>(
|
||||||
currentInteraction: GQLNodeInterface,
|
transaction: GQLNodeInterface,
|
||||||
executionContext: ExecutionContext<State>,
|
executionContext: ExecutionContext<State>,
|
||||||
state: EvalStateResult<State>
|
state: EvalStateResult<State>
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
@@ -20,42 +20,56 @@ export interface StateEvaluator {
|
|||||||
* a hook that is called after state has been fully evaluated
|
* a hook that is called after state has been fully evaluated
|
||||||
*/
|
*/
|
||||||
onStateEvaluated<State>(
|
onStateEvaluated<State>(
|
||||||
lastInteraction: GQLNodeInterface,
|
transaction: GQLNodeInterface,
|
||||||
executionContext: ExecutionContext<State>,
|
executionContext: ExecutionContext<State>,
|
||||||
state: EvalStateResult<State>
|
state: EvalStateResult<State>
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a hook that is called after performing internal write between contracts
|
||||||
|
*/
|
||||||
onInternalWriteStateUpdate<State>(
|
onInternalWriteStateUpdate<State>(
|
||||||
currentInteraction: GQLNodeInterface,
|
transaction: GQLNodeInterface,
|
||||||
contractTxId: string,
|
contractTxId: string,
|
||||||
state: EvalStateResult<State>
|
state: EvalStateResult<State>
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a hook that is called before communicating with other contract
|
* a hook that is called before communicating with other contract.
|
||||||
* note to myself: putting values into cache only "onContractCall" may degrade performance.
|
* note to myself: putting values into cache only "onContractCall" may degrade performance.
|
||||||
* For example"
|
* For example:
|
||||||
* block 722317 - contract A calls B
|
* 1. block 722317 - contract A calls B
|
||||||
* block 722727 - contract A calls B
|
* 2. block 722727 - contract A calls B
|
||||||
* block 722695 - contract B calls A
|
* 3. block 722695 - contract B calls A
|
||||||
* If we update cache only on contract call - for the last above call (B->A)
|
* If we update cache only on contract call - for the last above call (B->A)
|
||||||
* we would retrieve state cached for 722317. If there are any transactions
|
* we would retrieve state cached for 722317. If there are any transactions
|
||||||
* between 722317 and 722695 - the performance will be degraded.
|
* between 722317 and 722695 - the performance will be degraded.
|
||||||
*/
|
*/
|
||||||
onContractCall<State>(
|
onContractCall<State>(
|
||||||
currentInteraction: GQLNodeInterface,
|
transaction: GQLNodeInterface,
|
||||||
executionContext: ExecutionContext<State>,
|
executionContext: ExecutionContext<State>,
|
||||||
state: EvalStateResult<State>
|
state: EvalStateResult<State>
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loads latest available state for given contract for given blockHeight.
|
||||||
|
* - implementors should be aware that there might multiple interactions
|
||||||
|
* for single block - and sort them according to protocol specification.
|
||||||
|
*/
|
||||||
latestAvailableState<State>(
|
latestAvailableState<State>(
|
||||||
contractTxId: string,
|
contractTxId: string,
|
||||||
blockHeight: number
|
blockHeight: number
|
||||||
): Promise<BlockHeightCacheResult<EvalStateResult<State>> | null>;
|
): Promise<BlockHeightCacheResult<EvalStateResult<State>> | null>;
|
||||||
|
|
||||||
|
transactionState<State>(transaction: GQLNodeInterface, contractTxId: string): Promise<EvalStateResult<State> | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EvalStateResult<State> {
|
export class EvalStateResult<State> {
|
||||||
constructor(readonly state: State, readonly validity: Record<string, boolean>) {}
|
constructor(
|
||||||
|
readonly state: State,
|
||||||
|
readonly validity: Record<string, boolean>,
|
||||||
|
readonly transactionId?: string,
|
||||||
|
readonly blockId?: string) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DefaultEvaluationOptions implements EvaluationOptions {
|
export class DefaultEvaluationOptions implements EvaluationOptions {
|
||||||
|
|||||||
@@ -4,22 +4,30 @@ import {
|
|||||||
EvalStateResult,
|
EvalStateResult,
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
ExecutionContextModifier,
|
ExecutionContextModifier,
|
||||||
HandlerApi
|
HandlerApi,
|
||||||
|
LexicographicalInteractionsSorter,
|
||||||
|
StateCache
|
||||||
} from '@smartweave/core';
|
} from '@smartweave/core';
|
||||||
import Arweave from 'arweave';
|
import Arweave from 'arweave';
|
||||||
import { GQLNodeInterface } from '@smartweave/legacy';
|
import { GQLNodeInterface } from '@smartweave/legacy';
|
||||||
import { LoggerFactory } from '@smartweave/logging';
|
import { LoggerFactory } from '@smartweave/logging';
|
||||||
import { CurrentTx } from '@smartweave/contract';
|
import { CurrentTx } from '@smartweave/contract';
|
||||||
|
import { mapReplacer } from '@smartweave/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of DefaultStateEvaluator that adds caching capabilities
|
* An implementation of DefaultStateEvaluator that adds caching capabilities.
|
||||||
|
*
|
||||||
|
* The main responsibility of this class is to compute whether there are
|
||||||
|
* any interaction transactions, for which the state hasn't been evaluated yet -
|
||||||
|
* if so - it generates a list of such transactions and evaluates the state
|
||||||
|
* for them - taking as an input state the last cached state.
|
||||||
*/
|
*/
|
||||||
export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
||||||
private readonly cLogger = LoggerFactory.INST.create('CacheableStateEvaluator');
|
private readonly cLogger = LoggerFactory.INST.create('CacheableStateEvaluator');
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
arweave: Arweave,
|
arweave: Arweave,
|
||||||
private readonly cache: BlockHeightSwCache<EvalStateResult<unknown>>,
|
private readonly cache: BlockHeightSwCache<StateCache<unknown>>,
|
||||||
executionContextModifiers: ExecutionContextModifier[] = []
|
executionContextModifiers: ExecutionContextModifier[] = []
|
||||||
) {
|
) {
|
||||||
super(arweave, executionContextModifiers);
|
super(arweave, executionContextModifiers);
|
||||||
@@ -105,61 +113,132 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onStateEvaluated<State>(
|
async onStateEvaluated<State>(
|
||||||
lastInteraction: GQLNodeInterface,
|
transaction: GQLNodeInterface,
|
||||||
executionContext: ExecutionContext<State>,
|
executionContext: ExecutionContext<State>,
|
||||||
state: EvalStateResult<State>
|
state: EvalStateResult<State>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (lastInteraction.dry) {
|
if (transaction.dry) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.cLogger.debug(
|
const contractTxId = executionContext.contractDefinition.txId;
|
||||||
`onStateEvaluated: cache update for contract ${executionContext.contractDefinition.txId} [${lastInteraction.block.height}]`
|
|
||||||
);
|
this.cLogger.debug(`onStateEvaluated: cache update for contract ${contractTxId} [${transaction.block.height}]`);
|
||||||
await this.cache.put(
|
await this.putInCache(contractTxId, transaction, state);
|
||||||
new BlockHeightKey(executionContext.contractDefinition.txId, lastInteraction.block.height),
|
|
||||||
state
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onStateUpdate<State>(
|
async onStateUpdate<State>(
|
||||||
currentInteraction: GQLNodeInterface,
|
transaction: GQLNodeInterface,
|
||||||
executionContext: ExecutionContext<State>,
|
executionContext: ExecutionContext<State>,
|
||||||
state: EvalStateResult<State>
|
state: EvalStateResult<State>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (currentInteraction.dry) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (executionContext.evaluationOptions.updateCacheForEachInteraction) {
|
if (executionContext.evaluationOptions.updateCacheForEachInteraction) {
|
||||||
await this.cache.put(
|
await this.putInCache(executionContext.contractDefinition.txId, transaction, state);
|
||||||
new BlockHeightKey(executionContext.contractDefinition.txId, currentInteraction.block.height),
|
|
||||||
state
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
await super.onStateUpdate(currentInteraction, executionContext, state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async latestAvailableState<State>(
|
async latestAvailableState<State>(
|
||||||
contractTxId: string,
|
contractTxId: string,
|
||||||
blockHeight: number
|
blockHeight: number
|
||||||
): Promise<BlockHeightCacheResult<EvalStateResult<State>> | null> {
|
): Promise<BlockHeightCacheResult<EvalStateResult<State>> | null> {
|
||||||
return (await this.cache.getLessOrEqual(contractTxId, blockHeight)) as BlockHeightCacheResult<
|
const stateCache = (await this.cache.getLessOrEqual(contractTxId, blockHeight)) as BlockHeightCacheResult<
|
||||||
EvalStateResult<State>
|
StateCache<State>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
if (stateCache == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*if (stateCache.cachedValue.length == 1) {
|
||||||
|
this.cLogger.debug('CacheValue size 1', stateCache.cachedValue.values().next().value);
|
||||||
|
return new BlockHeightCacheResult<EvalStateResult<State>>(
|
||||||
|
stateCache.cachedHeight,
|
||||||
|
stateCache.cachedValue.values().next().value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorter = new LexicographicalInteractionsSorter(this.arweave);
|
||||||
|
|
||||||
|
this.cLogger.debug('State cache', JSON.stringify(stateCache.cachedValue, mapReplacer));
|
||||||
|
const toSort = await Promise.all(
|
||||||
|
[...stateCache.cachedValue.values()].map(async (k) => {
|
||||||
|
return {
|
||||||
|
transactionId: k.transactionId,
|
||||||
|
sortKey: await sorter.createSortKey(k.blockId, k.transactionId, blockHeight)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const sorted = toSort.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
|
||||||
|
this.cLogger.debug('sorted:', sorted);
|
||||||
|
|
||||||
|
const lastKey = sorted.pop();
|
||||||
|
|
||||||
|
this.cLogger.debug('Last key: ', lastKey);*/
|
||||||
|
|
||||||
|
return new BlockHeightCacheResult<EvalStateResult<State>>(
|
||||||
|
stateCache.cachedHeight,
|
||||||
|
[...stateCache.cachedValue].pop()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onInternalWriteStateUpdate<State>(
|
async onInternalWriteStateUpdate<State>(
|
||||||
currentInteraction: GQLNodeInterface,
|
transaction: GQLNodeInterface,
|
||||||
contractTxId: string,
|
contractTxId: string,
|
||||||
state: EvalStateResult<State>
|
state: EvalStateResult<State>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (currentInteraction.dry) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.cLogger.debug('Internal write state update:', {
|
this.cLogger.debug('Internal write state update:', {
|
||||||
height: currentInteraction.block.height,
|
height: transaction.block.height,
|
||||||
contractTxId,
|
contractTxId,
|
||||||
state
|
state
|
||||||
});
|
});
|
||||||
await this.cache.put(new BlockHeightKey(contractTxId, currentInteraction.block.height), state);
|
await this.putInCache(contractTxId, transaction, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onContractCall<State>(
|
||||||
|
transaction: GQLNodeInterface,
|
||||||
|
executionContext: ExecutionContext<State>,
|
||||||
|
state: EvalStateResult<State>
|
||||||
|
): Promise<void> {
|
||||||
|
await this.putInCache(executionContext.contractDefinition.txId, transaction, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
async transactionState<State>(
|
||||||
|
transaction: GQLNodeInterface,
|
||||||
|
contractTxId: string
|
||||||
|
): Promise<EvalStateResult<State> | undefined> {
|
||||||
|
const stateCache = (await this.cache.get(contractTxId, transaction.block.height)) as BlockHeightCacheResult<
|
||||||
|
StateCache<State>
|
||||||
|
>;
|
||||||
|
|
||||||
|
if (stateCache == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stateCache.cachedValue.find((sc) => {
|
||||||
|
return sc.transactionId === transaction.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async putInCache<State>(
|
||||||
|
contractTxId: string,
|
||||||
|
transaction: GQLNodeInterface,
|
||||||
|
state: EvalStateResult<State>
|
||||||
|
): Promise<void> {
|
||||||
|
if (transaction.dry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const transactionId = transaction.id;
|
||||||
|
const blockHeight = transaction.block.height;
|
||||||
|
this.cLogger.debug('putInCache:', {
|
||||||
|
state: state.state,
|
||||||
|
transactionId
|
||||||
|
});
|
||||||
|
const stateToCache = new EvalStateResult(state.state, state.validity, transactionId, transaction.block.id);
|
||||||
|
const stateCache = await this.cache.get(contractTxId, blockHeight);
|
||||||
|
if (stateCache != null) {
|
||||||
|
stateCache.cachedValue.push(stateToCache);
|
||||||
|
await this.cache.put(new BlockHeightKey(contractTxId, blockHeight), stateCache.cachedValue);
|
||||||
|
} else {
|
||||||
|
await this.cache.put(new BlockHeightKey(contractTxId, blockHeight), [stateToCache]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,7 @@ export class ContractInteractionsLoader implements InteractionsLoader {
|
|||||||
this.logger.debug('GQL page load:', benchmark.elapsed());
|
this.logger.debug('GQL page load:', benchmark.elapsed());
|
||||||
|
|
||||||
while (response.status === 403) {
|
while (response.status === 403) {
|
||||||
this.logger.debug(`GQL rate limiting, waiting ${ContractInteractionsLoader._30seconds}ms before next try.`);
|
this.logger.warn(`GQL rate limiting, waiting ${ContractInteractionsLoader._30seconds}ms before next try.`);
|
||||||
|
|
||||||
await sleep(ContractInteractionsLoader._30seconds);
|
await sleep(ContractInteractionsLoader._30seconds);
|
||||||
|
|
||||||
|
|||||||
@@ -14,22 +14,25 @@ import {
|
|||||||
InteractionCall,
|
InteractionCall,
|
||||||
InteractionResult,
|
InteractionResult,
|
||||||
LoggerFactory,
|
LoggerFactory,
|
||||||
MemCache,
|
|
||||||
StateEvaluator,
|
StateEvaluator,
|
||||||
TagsParser
|
TagsParser
|
||||||
} from '@smartweave';
|
} from '@smartweave';
|
||||||
import Arweave from 'arweave';
|
import Arweave from 'arweave';
|
||||||
|
|
||||||
// FIXME: currently this is tightly coupled with the HandlerApi
|
/**
|
||||||
export class DefaultStateEvaluator implements StateEvaluator {
|
* This class contains the base functionality of evaluating the contracts state - according
|
||||||
|
* to the SmartWeave protocol.
|
||||||
|
* Marked as abstract - as without help of any cache - the evaluation in real-life applications
|
||||||
|
* would be really slow - so using this class without any caching ({@link CacheableStateEvaluator})
|
||||||
|
* mechanism built on top makes no sense.
|
||||||
|
*/
|
||||||
|
export abstract class DefaultStateEvaluator implements StateEvaluator {
|
||||||
private readonly logger = LoggerFactory.INST.create('DefaultStateEvaluator');
|
private readonly logger = LoggerFactory.INST.create('DefaultStateEvaluator');
|
||||||
|
|
||||||
private readonly transactionStateCache: MemCache<EvalStateResult<unknown>> = new MemCache();
|
|
||||||
|
|
||||||
private readonly tagsParser = new TagsParser();
|
private readonly tagsParser = new TagsParser();
|
||||||
|
|
||||||
constructor(
|
protected constructor(
|
||||||
private readonly arweave: Arweave,
|
protected readonly arweave: Arweave,
|
||||||
private readonly executionContextModifiers: ExecutionContextModifier[] = []
|
private readonly executionContextModifiers: ExecutionContextModifier[] = []
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -59,7 +62,7 @@ export class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
let validity = deepCopy(baseState.validity);
|
let validity = deepCopy(baseState.validity);
|
||||||
|
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`Evaluating state for ${executionContext.contractDefinition.txId} [${missingInteractions.length} non-cached of ${executionContext.sortedInteractions.length} all]`
|
`Evaluating state for ${contractDefinition.txId} [${missingInteractions.length} non-cached of ${sortedInteractions.length} all]`
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.debug('Base state:', baseState.state);
|
this.logger.debug('Base state:', baseState.state);
|
||||||
@@ -78,7 +81,8 @@ export class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
}/${missingInteractions.length} [of all:${sortedInteractions.length}]`
|
}/${missingInteractions.length} [of all:${sortedInteractions.length}]`
|
||||||
);
|
);
|
||||||
|
|
||||||
const state = await this.onNextIteration(interactionTx, executionContext);
|
// verifying whether state isn't already available for this exact interaction.
|
||||||
|
const state = await this.transactionState<State>(interactionTx, contractDefinition.txId);
|
||||||
const isInteractWrite = this.tagsParser.isInteractWrite(missingInteraction, contractDefinition.txId);
|
const isInteractWrite = this.tagsParser.isInteractWrite(missingInteraction, contractDefinition.txId);
|
||||||
|
|
||||||
this.logger.debug('interactWrite?:', isInteractWrite);
|
this.logger.debug('interactWrite?:', isInteractWrite);
|
||||||
@@ -116,7 +120,7 @@ export class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// loading latest state of THIS contract from cache
|
// loading latest state of THIS contract from cache
|
||||||
const newState = await this.latestAvailableState(contractDefinition.txId, interactionTx.block.height);
|
const newState = await this.latestAvailableState<State>(contractDefinition.txId, interactionTx.block.height);
|
||||||
this.logger.debug('New state:', {
|
this.logger.debug('New state:', {
|
||||||
height: interactionTx.block.height,
|
height: interactionTx.block.height,
|
||||||
newState,
|
newState,
|
||||||
@@ -126,10 +130,11 @@ export class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
if (newState !== null) {
|
if (newState !== null) {
|
||||||
currentState = deepCopy(newState.cachedValue.state);
|
currentState = deepCopy(newState.cachedValue.state);
|
||||||
validity[interactionTx.id] = newState.cachedValue.validity[interactionTx.id];
|
validity[interactionTx.id] = newState.cachedValue.validity[interactionTx.id];
|
||||||
|
await this.onStateUpdate<State>(interactionTx, executionContext, new EvalStateResult(currentState, validity));
|
||||||
|
lastEvaluatedInteraction = interactionTx;
|
||||||
} else {
|
} else {
|
||||||
validity[interactionTx.id] = false;
|
validity[interactionTx.id] = false;
|
||||||
}
|
}
|
||||||
lastEvaluatedInteraction = interactionTx;
|
|
||||||
|
|
||||||
interactionCall.update({
|
interactionCall.update({
|
||||||
cacheHit: false,
|
cacheHit: false,
|
||||||
@@ -171,8 +176,8 @@ export class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
|
|
||||||
const interactionCall: InteractionCall = contract.getCallStack().addInteractionData(interactionData);
|
const interactionCall: InteractionCall = contract.getCallStack().addInteractionData(interactionData);
|
||||||
|
|
||||||
if (state !== null) {
|
if (state) {
|
||||||
this.logger.debug('Found in intermediary cache');
|
this.logger.debug('Found in cache');
|
||||||
intermediaryCacheHit = true;
|
intermediaryCacheHit = true;
|
||||||
currentState = state.state;
|
currentState = state.state;
|
||||||
validity = state.validity;
|
validity = state.validity;
|
||||||
@@ -208,9 +213,11 @@ export class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
valid: validity[interactionTx.id],
|
valid: validity[interactionTx.id],
|
||||||
errorMessage: errorMessage
|
errorMessage: errorMessage
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
await this.onStateUpdate<State>(interactionTx, executionContext, new EvalStateResult(currentState, validity));
|
await this.onStateUpdate<State>(interactionTx, executionContext, new EvalStateResult(currentState, validity));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// I'm really NOT a fan of this "modify" feature, but I don't have idea how to better
|
// I'm really NOT a fan of this "modify" feature, but I don't have idea how to better
|
||||||
// implement the "evolve" feature
|
// implement the "evolve" feature
|
||||||
for (const { modify } of this.executionContextModifiers) {
|
for (const { modify } of this.executionContextModifiers) {
|
||||||
@@ -257,61 +264,37 @@ export class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onStateUpdate<State>(
|
abstract latestAvailableState<State>(
|
||||||
currentInteraction: GQLNodeInterface,
|
|
||||||
executionContext: ExecutionContext<State>,
|
|
||||||
state: EvalStateResult<State>
|
|
||||||
): Promise<void> {
|
|
||||||
if (executionContext.evaluationOptions.fcpOptimization && !currentInteraction.dry) {
|
|
||||||
this.transactionStateCache.put(
|
|
||||||
`${executionContext.contractDefinition.txId}|${currentInteraction.id}`,
|
|
||||||
deepCopy(state)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async onNextIteration<State>(
|
|
||||||
currentInteraction: GQLNodeInterface,
|
|
||||||
executionContext: ExecutionContext<State>
|
|
||||||
): Promise<EvalStateResult<State>> {
|
|
||||||
const cacheKey = `${executionContext.contractDefinition.txId}|${currentInteraction.id}`;
|
|
||||||
const cachedState = this.transactionStateCache.get(cacheKey);
|
|
||||||
|
|
||||||
if (cachedState == null) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return deepCopy(cachedState as EvalStateResult<State>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onContractCall<State>(
|
|
||||||
currentInteraction: GQLNodeInterface,
|
|
||||||
executionContext: ExecutionContext<State>,
|
|
||||||
state: EvalStateResult<State>
|
|
||||||
): Promise<void> {
|
|
||||||
return Promise.resolve(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
onStateEvaluated<State>(
|
|
||||||
lastInteraction: GQLNodeInterface,
|
|
||||||
executionContext: ExecutionContext<State>,
|
|
||||||
state: EvalStateResult<State>
|
|
||||||
): Promise<void> {
|
|
||||||
return Promise.resolve(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
async latestAvailableState<State>(
|
|
||||||
contractTxId: string,
|
contractTxId: string,
|
||||||
blockHeight: number
|
blockHeight: number
|
||||||
): Promise<BlockHeightCacheResult<EvalStateResult<State>> | null> {
|
): Promise<BlockHeightCacheResult<EvalStateResult<State>> | null>;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
onInternalWriteStateUpdate<State>(
|
abstract onContractCall<State>(
|
||||||
currentInteraction: GQLNodeInterface,
|
transaction: GQLNodeInterface,
|
||||||
|
executionContext: ExecutionContext<State>,
|
||||||
|
state: EvalStateResult<State>
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
abstract onInternalWriteStateUpdate<State>(
|
||||||
|
transaction: GQLNodeInterface,
|
||||||
contractTxId: string,
|
contractTxId: string,
|
||||||
state: EvalStateResult<State>
|
state: EvalStateResult<State>
|
||||||
): Promise<void> {
|
): Promise<void>;
|
||||||
return Promise.resolve(undefined);
|
|
||||||
}
|
abstract onStateEvaluated<State>(
|
||||||
|
transaction: GQLNodeInterface,
|
||||||
|
executionContext: ExecutionContext<State>,
|
||||||
|
state: EvalStateResult<State>
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
abstract onStateUpdate<State>(
|
||||||
|
transaction: GQLNodeInterface,
|
||||||
|
executionContext: ExecutionContext<State>,
|
||||||
|
state: EvalStateResult<State>
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
abstract transactionState<State>(
|
||||||
|
transaction: GQLNodeInterface,
|
||||||
|
contractTxId: string
|
||||||
|
): Promise<EvalStateResult<State> | undefined>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,16 @@ export class LexicographicalInteractionsSorter implements InteractionsSorter {
|
|||||||
private async addSortKey(txInfo: GQLEdgeInterface) {
|
private async addSortKey(txInfo: GQLEdgeInterface) {
|
||||||
const { node } = txInfo;
|
const { node } = txInfo;
|
||||||
|
|
||||||
const blockHashBytes = this.arweave.utils.b64UrlToBuffer(node.block.id);
|
txInfo.sortKey = await this.createSortKey(node.block.id, node.id, node.block.height);
|
||||||
const txIdBytes = this.arweave.utils.b64UrlToBuffer(node.id);
|
}
|
||||||
|
|
||||||
|
public async createSortKey(blockId: string, transactionId: string, blockHeight: number) {
|
||||||
|
const blockHashBytes = this.arweave.utils.b64UrlToBuffer(blockId);
|
||||||
|
const txIdBytes = this.arweave.utils.b64UrlToBuffer(transactionId);
|
||||||
const concatenated = this.arweave.utils.concatBuffers([blockHashBytes, txIdBytes]);
|
const concatenated = this.arweave.utils.concatBuffers([blockHashBytes, txIdBytes]);
|
||||||
const hashed = arrayToHex(await this.arweave.crypto.hash(concatenated));
|
const hashed = arrayToHex(await this.arweave.crypto.hash(concatenated));
|
||||||
const blockHeight = `000000${node.block.height}`.slice(-12);
|
const blockHeightString = `000000${blockHeight}`.slice(-12);
|
||||||
|
|
||||||
txInfo.sortKey = `${blockHeight},${hashed}`;
|
return `${blockHeightString},${hashed}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/core/modules/impl/StateCache.ts
Normal file
3
src/core/modules/impl/StateCache.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { EvalStateResult } from '@smartweave';
|
||||||
|
|
||||||
|
export type StateCache<State> = Array<EvalStateResult<State>>;
|
||||||
@@ -8,12 +8,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
ContractDefinitionLoader,
|
ContractDefinitionLoader,
|
||||||
ContractInteractionsLoader,
|
ContractInteractionsLoader,
|
||||||
DefaultStateEvaluator,
|
|
||||||
EvalStateResult,
|
|
||||||
HandlerExecutorFactory,
|
HandlerExecutorFactory,
|
||||||
LexicographicalInteractionsSorter,
|
LexicographicalInteractionsSorter,
|
||||||
SmartWeave,
|
SmartWeave,
|
||||||
SmartWeaveBuilder
|
SmartWeaveBuilder,
|
||||||
|
StateCache
|
||||||
} from '@smartweave/core';
|
} from '@smartweave/core';
|
||||||
import { MemBlockHeightSwCache, MemCache, RemoteBlockHeightCache } from '@smartweave/cache';
|
import { MemBlockHeightSwCache, MemCache, RemoteBlockHeightCache } from '@smartweave/cache';
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ export class SmartWeaveWebFactory {
|
|||||||
|
|
||||||
const stateEvaluator = new CacheableStateEvaluator(
|
const stateEvaluator = new CacheableStateEvaluator(
|
||||||
arweave,
|
arweave,
|
||||||
new RemoteBlockHeightCache<EvalStateResult<unknown>>('STATE', cacheBaseURL),
|
new RemoteBlockHeightCache<StateCache<unknown>>('STATE', cacheBaseURL),
|
||||||
[new Evolve(definitionLoader, executorFactory)]
|
[new Evolve(definitionLoader, executorFactory)]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -84,7 +83,7 @@ export class SmartWeaveWebFactory {
|
|||||||
|
|
||||||
const stateEvaluator = new CacheableStateEvaluator(
|
const stateEvaluator = new CacheableStateEvaluator(
|
||||||
arweave,
|
arweave,
|
||||||
new MemBlockHeightSwCache<EvalStateResult<unknown>>(maxStoredBlockHeights),
|
new MemBlockHeightSwCache<StateCache<unknown>>(maxStoredBlockHeights),
|
||||||
[new Evolve(definitionLoader, executorFactory)]
|
[new Evolve(definitionLoader, executorFactory)]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -97,31 +96,4 @@ export class SmartWeaveWebFactory {
|
|||||||
.setExecutorFactory(executorFactory)
|
.setExecutorFactory(executorFactory)
|
||||||
.setStateEvaluator(stateEvaluator);
|
.setStateEvaluator(stateEvaluator);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a fully configured, nonCached {@link SmartWeave}.
|
|
||||||
*/
|
|
||||||
static nonCached(arweave: Arweave): SmartWeave {
|
|
||||||
return this.nonCachedBased(arweave).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a preconfigured {@link SmartWeave} that (yup, you've guessed it!) does not use any caches.
|
|
||||||
* This one is gonna be slooow!
|
|
||||||
* Use {@link SmartWeaveBuilder.build()} to finish the configuration.
|
|
||||||
*/
|
|
||||||
static nonCachedBased(arweave: Arweave): SmartWeaveBuilder {
|
|
||||||
const definitionLoader = new ContractDefinitionLoader(arweave);
|
|
||||||
const interactionsLoader = new ContractInteractionsLoader(arweave);
|
|
||||||
const executorFactory = new HandlerExecutorFactory(arweave);
|
|
||||||
const stateEvaluator = new DefaultStateEvaluator(arweave, [new Evolve(definitionLoader, executorFactory)]);
|
|
||||||
const interactionsSorter = new LexicographicalInteractionsSorter(arweave);
|
|
||||||
|
|
||||||
return SmartWeave.builder(arweave)
|
|
||||||
.setDefinitionLoader(definitionLoader)
|
|
||||||
.setInteractionsLoader(interactionsLoader)
|
|
||||||
.setInteractionsSorter(interactionsSorter)
|
|
||||||
.setExecutorFactory(executorFactory)
|
|
||||||
.setStateEvaluator(stateEvaluator);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export interface GQLNodeInterface {
|
|||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
dry?: boolean;
|
dry?: boolean;
|
||||||
|
sortKey?: string; //added dynamically by the LexicographicalInteractionsSorter
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GQLEdgeInterface {
|
export interface GQLEdgeInterface {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export * from './CacheableContractInteractionsLoader';
|
export * from './CacheableContractInteractionsLoader';
|
||||||
export * from './CacheableExecutorFactory';
|
export * from './CacheableExecutorFactory';
|
||||||
export * from './CacheableStateEvaluator';
|
export * from '../core/modules/impl/CacheableStateEvaluator';
|
||||||
export * from './DebuggableExecutorFactor';
|
export * from './DebuggableExecutorFactor';
|
||||||
export * from './Evolve';
|
export * from './Evolve';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export const sleep = (ms: number): Promise<void> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const deepCopy = (input: unknown): any => {
|
export const deepCopy = (input: unknown): any => {
|
||||||
return JSON.parse(JSON.stringify(input));
|
return JSON.parse(JSON.stringify(input, mapReplacer), mapReviver);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapReplacer = (key: unknown, value: unknown) => {
|
export const mapReplacer = (key: unknown, value: unknown) => {
|
||||||
@@ -18,6 +18,15 @@ export const mapReplacer = (key: unknown, value: unknown) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mapReviver = (key: unknown, value: any) => {
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
if (value.dataType === 'Map') {
|
||||||
|
return new Map(value.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
export const asc = (a: number, b: number): number => a - b;
|
export const asc = (a: number, b: number): number => a - b;
|
||||||
|
|
||||||
export const desc = (a: number, b: number): number => b - a;
|
export const desc = (a: number, b: number): number => b - a;
|
||||||
|
|||||||
@@ -66,17 +66,15 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
calleeContract = smartweave.contract(calleeTxId).connect(wallet).setEvaluationOptions({
|
calleeContract = smartweave.contract(calleeTxId).connect(wallet).setEvaluationOptions({
|
||||||
ignoreExceptions: false
|
ignoreExceptions: false,
|
||||||
|
internalWrites: true,
|
||||||
});
|
});
|
||||||
callingContract = smartweave.contract(callingTxId).connect(wallet).setEvaluationOptions({
|
callingContract = smartweave.contract(callingTxId).connect(wallet).setEvaluationOptions({
|
||||||
ignoreExceptions: false
|
ignoreExceptions: false,
|
||||||
|
internalWrites: true
|
||||||
});
|
});
|
||||||
await mine();
|
await mine();
|
||||||
|
|
||||||
await calleeContract.writeInteraction({ function: 'add' });
|
|
||||||
await calleeContract.writeInteraction({ function: 'add' });
|
|
||||||
await mine(); // 102
|
|
||||||
|
|
||||||
await calleeContract.writeInteraction({ function: 'add' });
|
await calleeContract.writeInteraction({ function: 'add' });
|
||||||
await callingContract.writeInteraction({ function: 'writeContract', contractId: calleeTxId, amount: 10 });
|
await callingContract.writeInteraction({ function: 'writeContract', contractId: calleeTxId, amount: 10 });
|
||||||
await mine(); // 113
|
await mine(); // 113
|
||||||
@@ -85,7 +83,6 @@ async function main() {
|
|||||||
logger.info('Read state 1', result1.state);*/
|
logger.info('Read state 1', result1.state);*/
|
||||||
|
|
||||||
await callingContract.writeInteraction({ function: 'writeContract', contractId: calleeTxId, amount: 10 });
|
await callingContract.writeInteraction({ function: 'writeContract', contractId: calleeTxId, amount: 10 });
|
||||||
await mine(); // 123
|
|
||||||
await calleeContract.writeInteraction({ function: 'add' });
|
await calleeContract.writeInteraction({ function: 'add' });
|
||||||
await mine(); //124
|
await mine(); //124
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { TsLogFactory } from '../src/logging/node/TsLogFactory';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { ContractInteractionsLoader } from '../src/core/modules/impl/ContractInteractionsLoader';
|
import { ContractInteractionsLoader } from '../src/core/modules/impl/ContractInteractionsLoader';
|
||||||
|
import { DefaultEvaluationOptions } from '../../smartweave-loot/.yalc/redstone-smartweave';
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
LoggerFactory.use(new TsLogFactory());
|
LoggerFactory.use(new TsLogFactory());
|
||||||
@@ -25,7 +26,7 @@ async function main() {
|
|||||||
'Daj-MNSnH55TDfxqC7v4eq0lKzVIwh98srUaWqyuZtY',
|
'Daj-MNSnH55TDfxqC7v4eq0lKzVIwh98srUaWqyuZtY',
|
||||||
0,
|
0,
|
||||||
779820,
|
779820,
|
||||||
evaluationOptions
|
new DefaultEvaluationOptions()
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(result.length);
|
console.log(result.length);
|
||||||
|
|||||||
Reference in New Issue
Block a user