feat: make cache puts configurable
This commit is contained in:
committed by
Piotr Pędziwiatr
parent
02b58217f7
commit
d858c08b72
12
README.md
12
README.md
@@ -13,6 +13,7 @@ The base motivation behind rewriting the original SDK (and roadmap proposal) has
|
|||||||
To further improve contract state evaluation time, one can additionally use AWS CloudFront based Arweave cache described [here](https://github.com/redstone-finance/redstone-smartweave-contracts/blob/main/docs/CACHE.md).
|
To further improve contract state evaluation time, one can additionally use AWS CloudFront based Arweave cache described [here](https://github.com/redstone-finance/redstone-smartweave-contracts/blob/main/docs/CACHE.md).
|
||||||
|
|
||||||
- [Architecture](#architecture)
|
- [Architecture](#architecture)
|
||||||
|
- [State evaluation diagram](#state-evaluation-diagram)
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Installation and import](#installation-and-import)
|
- [Installation and import](#installation-and-import)
|
||||||
- [Examples](#examples)
|
- [Examples](#examples)
|
||||||
@@ -46,6 +47,17 @@ This modular architecture has several advantages:
|
|||||||
2. The SmartWeave client can be customized depending on user needs (e.g. different type of caches for web and node environment)
|
2. The SmartWeave client can be customized depending on user needs (e.g. different type of caches for web and node environment)
|
||||||
3. It makes it easier to add new features on top of the core protocol - without the risk of breaking the functionality of the core layer.
|
3. It makes it easier to add new features on top of the core protocol - without the risk of breaking the functionality of the core layer.
|
||||||
|
|
||||||
|
## State evaluation diagram
|
||||||
|

|
||||||
|
|
||||||
|
In order to perform contract state evaluation (at given block height), SDK performs certain operations.
|
||||||
|
The diagram above and description assume the most basic “mem-cached” SDK client.
|
||||||
|
1. Users who are interacting with the contract, call the “readState” method.
|
||||||
|
2. Interactions Loader and Contract Definition Loader modules are then called in parallel - to load all the data required for state evaluation. Both Interactions Loader and Contract Definition Loader first check its corresponding cache whether data is already loaded - and load from Arweave only the missing part.
|
||||||
|
3. With interactions and contract definition loaded - Executor Factory creates a handle to the SmartWeave contract main function (or loads it from its own cache)
|
||||||
|
4. With all the interactions and a contract handle - the State Evaluator evaluates the state from the lastly cached value - and returns the result to User.
|
||||||
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
PRs are welcome! :-) Also, feel free to submit [issues](https://github.com/redstone-finance/redstone-smartcontracts/issues) - with both bugs and feature proposals.
|
PRs are welcome! :-) Also, feel free to submit [issues](https://github.com/redstone-finance/redstone-smartcontracts/issues) - with both bugs and feature proposals.
|
||||||
In case of creating a PR - please use [semantic commit messages](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716).
|
In case of creating a PR - please use [semantic commit messages](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716).
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ In functional programming terms it acts as a `fold` function - the state of the
|
|||||||
2. A contract function
|
2. A contract function
|
||||||
3. An ordered list of actions
|
3. An ordered list of actions
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
In order to evaluate contract state, SmartWeave Protocol client:
|
In order to evaluate contract state, SmartWeave Protocol client:
|
||||||
1. Loads all the contract's interaction transactions up to the requested block height.
|
1. Loads all the contract's interaction transactions up to the requested block height.
|
||||||
2. Sorts the interaction transactions. The order of the interactions is determined firstly by interaction
|
2. Sorts the interaction transactions. The order of the interactions is determined firstly by interaction
|
||||||
|
|||||||
BIN
docs/img/protocol.png
Normal file
BIN
docs/img/protocol.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
BIN
docs/img/readstate.png
Normal file
BIN
docs/img/readstate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 224 KiB |
@@ -65,7 +65,7 @@
|
|||||||
"@types/node": "^16.7.1",
|
"@types/node": "^16.7.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.29.2",
|
"@typescript-eslint/eslint-plugin": "^4.29.2",
|
||||||
"@typescript-eslint/parser": "^4.29.2",
|
"@typescript-eslint/parser": "^4.29.2",
|
||||||
"arlocal": "^1.0.43",
|
"arlocal": "^1.0.44",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^3.4.1",
|
"eslint-plugin-prettier": "^3.4.1",
|
||||||
@@ -78,6 +78,7 @@
|
|||||||
"ts-node": "^10.2.1",
|
"ts-node": "^10.2.1",
|
||||||
"tsc-alias": "^1.3.9",
|
"tsc-alias": "^1.3.9",
|
||||||
"tsconfig-paths": "^3.10.1",
|
"tsconfig-paths": "^3.10.1",
|
||||||
"typescript": "^4.3.5"
|
"typescript": "^4.3.5",
|
||||||
|
"cors": "^2.8.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const arweave = Arweave.init({
|
|||||||
});
|
});
|
||||||
|
|
||||||
LoggerFactory.INST.logLevel('fatal');
|
LoggerFactory.INST.logLevel('fatal');
|
||||||
const smartWeave = SmartWeaveNodeFactory.memCached(arweave);
|
const smartWeave = SmartWeaveNodeFactory.memCached(arweave, 10);
|
||||||
|
|
||||||
const testCases: string[] = JSON.parse(fs.readFileSync(path.join(__dirname, 'test-cases.json'), 'utf-8'));
|
const testCases: string[] = JSON.parse(fs.readFileSync(path.join(__dirname, 'test-cases.json'), 'utf-8'));
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@ describe.each(chunked)('.suite %#', (contracts: string[]) => {
|
|||||||
const result = await readContract(arweave, contractTxId);
|
const result = await readContract(arweave, contractTxId);
|
||||||
const resultString = JSON.stringify(result).trim();
|
const resultString = JSON.stringify(result).trim();
|
||||||
|
|
||||||
|
console.log('readState', contractTxId);
|
||||||
const result2 = await smartWeave
|
const result2 = await smartWeave
|
||||||
.contract(contractTxId)
|
.contract(contractTxId)
|
||||||
.setEvaluationOptions({
|
.setEvaluationOptions({
|
||||||
|
|||||||
7
src/cache/impl/RemoteBlockHeightCache.ts
vendored
7
src/cache/impl/RemoteBlockHeightCache.ts
vendored
@@ -4,6 +4,9 @@ import axios, { AxiosInstance } from 'axios';
|
|||||||
/**
|
/**
|
||||||
* A {@link BlockHeightSwCache} implementation that delegates all its methods
|
* A {@link BlockHeightSwCache} implementation that delegates all its methods
|
||||||
* to remote endpoints.
|
* to remote endpoints.
|
||||||
|
*
|
||||||
|
* TODO: this could be further optimised - i.e. with the help of "level 1" memory cache
|
||||||
|
* that would store max X elements - and would be backed up by the "level 2" remote cache.
|
||||||
*/
|
*/
|
||||||
export class RemoteBlockHeightCache<V = any> implements BlockHeightSwCache<V> {
|
export class RemoteBlockHeightCache<V = any> implements BlockHeightSwCache<V> {
|
||||||
private axios: AxiosInstance;
|
private axios: AxiosInstance;
|
||||||
@@ -39,9 +42,13 @@ export class RemoteBlockHeightCache<V = any> implements BlockHeightSwCache<V> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* TODO: data should "flushed" in batches...
|
||||||
* PUT '/:type/:key/:blockHeight' {data: value}
|
* PUT '/:type/:key/:blockHeight' {data: value}
|
||||||
*/
|
*/
|
||||||
async put({ cacheKey, blockHeight }: BlockHeightKey, value: V): Promise<void> {
|
async put({ cacheKey, blockHeight }: BlockHeightKey, value: V): Promise<void> {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await this.axios.put(`/${this.type}/${cacheKey}/${blockHeight}`, value);
|
await this.axios.put(`/${this.type}/${cacheKey}/${blockHeight}`, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* wallet connected to this contract
|
* wallet connected to this contract
|
||||||
* @protected
|
|
||||||
*/
|
*/
|
||||||
protected wallet?: ArWallet;
|
protected wallet?: ArWallet;
|
||||||
private evaluationOptions: EvaluationOptions = new DefaultEvaluationOptions();
|
private evaluationOptions: EvaluationOptions = new DefaultEvaluationOptions();
|
||||||
@@ -87,7 +86,8 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
const executionContext = await this.createExecutionContext(this.contractTxId, blockHeight);
|
const executionContext = await this.createExecutionContext(this.contractTxId, blockHeight);
|
||||||
this.logger.info('Execution Context', {
|
this.logger.info('Execution Context', {
|
||||||
blockHeight: executionContext.blockHeight,
|
blockHeight: executionContext.blockHeight,
|
||||||
srcTxId: executionContext.contractDefinition.srcTxId
|
srcTxId: executionContext.contractDefinition.srcTxId,
|
||||||
|
missingInteractions: executionContext.sortedInteractions.length
|
||||||
});
|
});
|
||||||
this.logger.debug('context', benchmark.elapsed());
|
this.logger.debug('context', benchmark.elapsed());
|
||||||
benchmark.reset();
|
benchmark.reset();
|
||||||
@@ -149,7 +149,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
// call one of the contract's view method
|
// call one of the contract's view method
|
||||||
const handleResult = await executionContext.handler.handle<Input, View>(
|
const handleResult = await executionContext.handler.handle<Input, View>(
|
||||||
executionContext,
|
executionContext,
|
||||||
evalStateResult.state,
|
evalStateResult,
|
||||||
interaction,
|
interaction,
|
||||||
{
|
{
|
||||||
id: null,
|
id: null,
|
||||||
@@ -192,7 +192,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
|||||||
|
|
||||||
return await executionContext.handler.handle<Input, View>(
|
return await executionContext.handler.handle<Input, View>(
|
||||||
executionContext,
|
executionContext,
|
||||||
evalStateResult.state,
|
evalStateResult,
|
||||||
interaction,
|
interaction,
|
||||||
transaction,
|
transaction,
|
||||||
[]
|
[]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ExecutionContext, GQLEdgeInterface, GQLNodeInterface } from '@smartweave';
|
import { ExecutionContext, GQLNodeInterface, InteractionTx } from '@smartweave';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementors of this class are responsible for evaluating contract's state
|
* Implementors of this class are responsible for evaluating contract's state
|
||||||
@@ -10,11 +10,32 @@ export interface StateEvaluator {
|
|||||||
currentTx: { interactionTxId: string; contractTxId: string }[]
|
currentTx: { interactionTxId: string; contractTxId: string }[]
|
||||||
): Promise<EvalStateResult<State>>;
|
): Promise<EvalStateResult<State>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a hook that is called on each state update (i.e. after evaluating state for each interaction)
|
||||||
|
*/
|
||||||
onStateUpdate<State>(
|
onStateUpdate<State>(
|
||||||
currentInteraction: GQLNodeInterface,
|
currentInteraction: GQLNodeInterface,
|
||||||
executionContext: ExecutionContext<State>,
|
executionContext: ExecutionContext<State>,
|
||||||
state: EvalStateResult<State>
|
state: EvalStateResult<State>
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a hook that is called after state has been fully evaluated
|
||||||
|
*/
|
||||||
|
onStateEvaluated<State>(
|
||||||
|
lastInteraction: GQLNodeInterface,
|
||||||
|
executionContext: ExecutionContext<State>,
|
||||||
|
state: EvalStateResult<State>
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a hook that is called before communicating with other contract
|
||||||
|
*/
|
||||||
|
onContractCall<State>(
|
||||||
|
currentInteraction: InteractionTx,
|
||||||
|
executionContext: ExecutionContext<State>,
|
||||||
|
state: EvalStateResult<State>
|
||||||
|
): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EvalStateResult<State> {
|
export class EvalStateResult<State> {
|
||||||
@@ -32,6 +53,8 @@ export class DefaultEvaluationOptions implements EvaluationOptions {
|
|||||||
waitForConfirmation = false;
|
waitForConfirmation = false;
|
||||||
|
|
||||||
fcpOptimization = false;
|
fcpOptimization = false;
|
||||||
|
|
||||||
|
updateCacheForEachInteraction = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// an interface for the contract EvaluationOptions - can be used to change the behaviour of some of the features.
|
// an interface for the contract EvaluationOptions - can be used to change the behaviour of some of the features.
|
||||||
@@ -45,4 +68,9 @@ export interface EvaluationOptions {
|
|||||||
|
|
||||||
// experimental optimization for contracts that utilize the Foreign Call Protocol
|
// experimental optimization for contracts that utilize the Foreign Call Protocol
|
||||||
fcpOptimization: boolean;
|
fcpOptimization: boolean;
|
||||||
|
|
||||||
|
// whether cache should be updated after evaluating each interaction transaction.
|
||||||
|
// this can be switched off to speed up cache writes (ie. for some contracts (with flat structure)
|
||||||
|
// and caches it maybe more suitable to cache only after state has been fully evaluated)
|
||||||
|
updateCacheForEachInteraction: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
GQLTagInterface,
|
GQLTagInterface,
|
||||||
HandlerApi,
|
HandlerApi,
|
||||||
InteractionResult,
|
InteractionResult,
|
||||||
|
InteractionTx,
|
||||||
LoggerFactory,
|
LoggerFactory,
|
||||||
MemCache,
|
MemCache,
|
||||||
StateEvaluator,
|
StateEvaluator,
|
||||||
@@ -50,30 +51,25 @@ export class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
): Promise<EvalStateResult<State>> {
|
): Promise<EvalStateResult<State>> {
|
||||||
const stateEvaluationBenchmark = Benchmark.measure();
|
const stateEvaluationBenchmark = Benchmark.measure();
|
||||||
const { ignoreExceptions } = executionContext.evaluationOptions;
|
const { ignoreExceptions } = executionContext.evaluationOptions;
|
||||||
|
const { contractDefinition, sortedInteractions } = executionContext;
|
||||||
|
|
||||||
let currentState = baseState.state;
|
let currentState = baseState.state;
|
||||||
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 ${executionContext.contractDefinition.txId}
|
||||||
|
[${missingInteractions.length} non-cached of ${executionContext.sortedInteractions.length} all]`
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.trace(
|
let lastEvaluatedInteraction = null;
|
||||||
'missingInteractions',
|
|
||||||
missingInteractions.map((int) => {
|
|
||||||
return int.node.id;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this.logger.trace('Init state', JSON.stringify(baseState.state));
|
|
||||||
|
|
||||||
for (const missingInteraction of missingInteractions) {
|
for (const missingInteraction of missingInteractions) {
|
||||||
const currentInteraction: GQLNodeInterface = missingInteraction.node;
|
const currentInteraction: GQLNodeInterface = missingInteraction.node;
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`[${executionContext.contractDefinition.txId}][${missingInteraction.node.id}]: ${
|
`[${contractDefinition.txId}][${missingInteraction.node.id}][${missingInteraction.node.block.height}]: ${
|
||||||
missingInteractions.indexOf(missingInteraction) + 1
|
missingInteractions.indexOf(missingInteraction) + 1
|
||||||
}/${missingInteractions.length} [of all:${executionContext.sortedInteractions.length}]`
|
}/${missingInteractions.length} [of all:${sortedInteractions.length}]`
|
||||||
);
|
);
|
||||||
|
|
||||||
const state = await this.onNextIteration(currentInteraction, executionContext);
|
const state = await this.onNextIteration(currentInteraction, executionContext);
|
||||||
@@ -84,7 +80,7 @@ export class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
} else {
|
} else {
|
||||||
const singleInteractionBenchmark = Benchmark.measure();
|
const singleInteractionBenchmark = Benchmark.measure();
|
||||||
|
|
||||||
const inputTag = this.tagsParser.getInputTag(missingInteraction, executionContext.contractDefinition.txId);
|
const inputTag = this.tagsParser.getInputTag(missingInteraction, contractDefinition.txId);
|
||||||
if (!inputTag) {
|
if (!inputTag) {
|
||||||
this.logger.error(`Skipping tx with missing or invalid Input tag - ${currentInteraction.id}`);
|
this.logger.error(`Skipping tx with missing or invalid Input tag - ${currentInteraction.id}`);
|
||||||
continue;
|
continue;
|
||||||
@@ -103,7 +99,7 @@ export class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
|
|
||||||
const result = await executionContext.handler.handle(
|
const result = await executionContext.handler.handle(
|
||||||
executionContext,
|
executionContext,
|
||||||
currentState,
|
new EvalStateResult(currentState, validity),
|
||||||
interaction,
|
interaction,
|
||||||
currentInteraction,
|
currentInteraction,
|
||||||
currentTx
|
currentTx
|
||||||
@@ -115,10 +111,6 @@ export class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
throw new Error(`Exception while processing ${JSON.stringify(interaction)}:\n${result.result}`);
|
throw new Error(`Exception while processing ${JSON.stringify(interaction)}:\n${result.result}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.type === 'exception') {
|
|
||||||
this.logger.error('Credit:', (currentState as any).credit);
|
|
||||||
}
|
|
||||||
|
|
||||||
validity[currentInteraction.id] = result.type === 'ok';
|
validity[currentInteraction.id] = result.type === 'ok';
|
||||||
// strangely - state is for some reason modified for some contracts (eg. YLVpmhSq5JmLltfg6R-5fL04rIRPrlSU22f6RQ6VyYE)
|
// strangely - state is for some reason modified for some contracts (eg. YLVpmhSq5JmLltfg6R-5fL04rIRPrlSU22f6RQ6VyYE)
|
||||||
// when calling any async (even simple timeout) function here...
|
// when calling any async (even simple timeout) function here...
|
||||||
@@ -126,6 +118,10 @@ export class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
// see https://github.com/ArweaveTeam/SmartWeave/pull/92 for more details
|
// see https://github.com/ArweaveTeam/SmartWeave/pull/92 for more details
|
||||||
currentState = deepCopy(result.state);
|
currentState = deepCopy(result.state);
|
||||||
|
|
||||||
|
// cannot simply take last element of the missingInteractions
|
||||||
|
// as there is no certainty that it has been evaluated (e.g. issues with input tag).
|
||||||
|
lastEvaluatedInteraction = currentInteraction;
|
||||||
|
|
||||||
this.logger.debug('Interaction evaluation', singleInteractionBenchmark.elapsed());
|
this.logger.debug('Interaction evaluation', singleInteractionBenchmark.elapsed());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +138,15 @@ export class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.logger.debug('State evaluation total:', stateEvaluationBenchmark.elapsed());
|
this.logger.debug('State evaluation total:', stateEvaluationBenchmark.elapsed());
|
||||||
return new EvalStateResult<State>(currentState, validity);
|
const result = new EvalStateResult<State>(currentState, validity);
|
||||||
|
|
||||||
|
// state could have been full retrieved from cache
|
||||||
|
// or there were no interactions below requested block height
|
||||||
|
if (lastEvaluatedInteraction !== null) {
|
||||||
|
await this.onStateEvaluated(lastEvaluatedInteraction, executionContext, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private logResult<State>(
|
private logResult<State>(
|
||||||
@@ -199,4 +203,20 @@ export class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
return deepCopy(cachedState as EvalStateResult<State>);
|
return deepCopy(cachedState as EvalStateResult<State>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onContractCall<State>(
|
||||||
|
currentInteraction: InteractionTx,
|
||||||
|
executionContext: ExecutionContext<State, unknown>,
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import * as clarity from '@weavery/clarity';
|
|||||||
import {
|
import {
|
||||||
ContractDefinition,
|
ContractDefinition,
|
||||||
deepCopy,
|
deepCopy,
|
||||||
|
EvalStateResult,
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
ExecutorFactory,
|
ExecutorFactory,
|
||||||
InteractionTx,
|
InteractionTx,
|
||||||
@@ -17,7 +18,7 @@ import {
|
|||||||
export interface HandlerApi<State> {
|
export interface HandlerApi<State> {
|
||||||
handle<Input, Result>(
|
handle<Input, Result>(
|
||||||
executionContext: ExecutionContext<State>,
|
executionContext: ExecutionContext<State>,
|
||||||
state: State,
|
currentResult: EvalStateResult<State>,
|
||||||
interaction: ContractInteraction<Input>,
|
interaction: ContractInteraction<Input>,
|
||||||
interactionTx: InteractionTx,
|
interactionTx: InteractionTx,
|
||||||
currentTx: { interactionTxId: string; contractTxId: string }[]
|
currentTx: { interactionTxId: string; contractTxId: string }[]
|
||||||
@@ -54,7 +55,7 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
|
|||||||
return {
|
return {
|
||||||
async handle<Input, Result>(
|
async handle<Input, Result>(
|
||||||
executionContext: ExecutionContext<State>,
|
executionContext: ExecutionContext<State>,
|
||||||
state: State,
|
currentResult: EvalStateResult<State>,
|
||||||
interaction: ContractInteraction<Input>,
|
interaction: ContractInteraction<Input>,
|
||||||
interactionTx: InteractionTx,
|
interactionTx: InteractionTx,
|
||||||
currentTx: { interactionTxId: string; contractTxId: string }[]
|
currentTx: { interactionTxId: string; contractTxId: string }[]
|
||||||
@@ -65,11 +66,19 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
|
|||||||
Input,
|
Input,
|
||||||
Result
|
Result
|
||||||
>;
|
>;
|
||||||
const stateCopy = JSON.parse(JSON.stringify(state));
|
const stateCopy = JSON.parse(JSON.stringify(currentResult.state));
|
||||||
swGlobal._activeTx = interactionTx;
|
swGlobal._activeTx = interactionTx;
|
||||||
self.logger.trace(`SmartWeave.contract.id:`, swGlobal.contract.id);
|
self.logger.trace(`SmartWeave.contract.id:`, swGlobal.contract.id);
|
||||||
|
|
||||||
self.assignReadContractState<Input, State>(swGlobal, contractDefinition, executionContext, currentTx);
|
// TODO: refactor - too many arguments
|
||||||
|
self.assignReadContractState<Input, State>(
|
||||||
|
swGlobal,
|
||||||
|
contractDefinition,
|
||||||
|
executionContext,
|
||||||
|
currentTx,
|
||||||
|
currentResult,
|
||||||
|
interactionTx
|
||||||
|
);
|
||||||
self.assignViewContractState<Input, State>(swGlobal, contractDefinition, executionContext);
|
self.assignViewContractState<Input, State>(swGlobal, contractDefinition, executionContext);
|
||||||
|
|
||||||
const handlerResult = await handler(stateCopy, interaction);
|
const handlerResult = await handler(stateCopy, interaction);
|
||||||
@@ -78,7 +87,7 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
|
|||||||
return {
|
return {
|
||||||
type: 'ok',
|
type: 'ok',
|
||||||
result: handlerResult.result,
|
result: handlerResult.result,
|
||||||
state: handlerResult.state || state
|
state: handlerResult.state || currentResult.state
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +99,7 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
|
|||||||
return {
|
return {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
errorMessage: err.message,
|
errorMessage: err.message,
|
||||||
state,
|
state: currentResult.state,
|
||||||
// note: previous version was writing error message to a "result" field,
|
// note: previous version was writing error message to a "result" field,
|
||||||
// which fucks-up the HandlerResult type definition -
|
// which fucks-up the HandlerResult type definition -
|
||||||
// HandlerResult.result had to be declared as 'Result | string' - and that led to a poor dev exp.
|
// HandlerResult.result had to be declared as 'Result | string' - and that led to a poor dev exp.
|
||||||
@@ -101,7 +110,7 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
|
|||||||
return {
|
return {
|
||||||
type: 'exception',
|
type: 'exception',
|
||||||
errorMessage: `${(err && err.stack) || (err && err.message)}`,
|
errorMessage: `${(err && err.stack) || (err && err.message)}`,
|
||||||
state,
|
state: currentResult.state,
|
||||||
result: null
|
result: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -133,7 +142,9 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
|
|||||||
swGlobal: SmartWeaveGlobal,
|
swGlobal: SmartWeaveGlobal,
|
||||||
contractDefinition: ContractDefinition<State>,
|
contractDefinition: ContractDefinition<State>,
|
||||||
executionContext: ExecutionContext<State>,
|
executionContext: ExecutionContext<State>,
|
||||||
currentTx: { interactionTxId: string; contractTxId: string }[]
|
currentTx: { interactionTxId: string; contractTxId: string }[],
|
||||||
|
currentResult: EvalStateResult<State>,
|
||||||
|
interactionTx: InteractionTx
|
||||||
) {
|
) {
|
||||||
swGlobal.contracts.readContractState = async (contractTxId: string, height?: number, returnValidity?: boolean) => {
|
swGlobal.contracts.readContractState = async (contractTxId: string, height?: number, returnValidity?: boolean) => {
|
||||||
const requestedHeight = height || swGlobal.block.height;
|
const requestedHeight = height || swGlobal.block.height;
|
||||||
@@ -143,10 +154,14 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
|
|||||||
height: requestedHeight,
|
height: requestedHeight,
|
||||||
transaction: swGlobal.transaction.id
|
transaction: swGlobal.transaction.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { stateEvaluator } = executionContext.smartweave;
|
||||||
const childContract = executionContext.smartweave
|
const childContract = executionContext.smartweave
|
||||||
.contract(contractTxId, executionContext.contract)
|
.contract(contractTxId, executionContext.contract)
|
||||||
.setEvaluationOptions(executionContext.evaluationOptions);
|
.setEvaluationOptions(executionContext.evaluationOptions);
|
||||||
|
|
||||||
|
await stateEvaluator.onContractCall(interactionTx, executionContext, currentResult);
|
||||||
|
|
||||||
const stateWithValidity = await childContract.readState(requestedHeight, [
|
const stateWithValidity = await childContract.readState(requestedHeight, [
|
||||||
...(currentTx || []),
|
...(currentTx || []),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export class SmartWeaveWebFactory {
|
|||||||
/**
|
/**
|
||||||
* Returns a fully configured {@link SmartWeave} that is using mem cache for all layers.
|
* Returns a fully configured {@link SmartWeave} that is using mem cache for all layers.
|
||||||
*/
|
*/
|
||||||
static memCached(arweave: Arweave): SmartWeave {
|
static memCached(arweave: Arweave, maxStoredBlockHeights: number = Number.MAX_SAFE_INTEGER): SmartWeave {
|
||||||
return this.memCachedBased(arweave).build();
|
return this.memCachedBased(arweave).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,19 +72,21 @@ export class SmartWeaveWebFactory {
|
|||||||
* Returns a preconfigured, memCached {@link SmartWeaveBuilder}, that allows for customization of the SmartWeave instance.
|
* Returns a preconfigured, memCached {@link SmartWeaveBuilder}, that allows for customization of the SmartWeave instance.
|
||||||
* Use {@link SmartWeaveBuilder.build()} to finish the configuration.
|
* Use {@link SmartWeaveBuilder.build()} to finish the configuration.
|
||||||
*/
|
*/
|
||||||
static memCachedBased(arweave: Arweave): SmartWeaveBuilder {
|
static memCachedBased(arweave: Arweave, maxStoredBlockHeights: number = Number.MAX_SAFE_INTEGER): SmartWeaveBuilder {
|
||||||
const definitionLoader = new ContractDefinitionLoader(arweave, new MemCache());
|
const definitionLoader = new ContractDefinitionLoader(arweave, new MemCache());
|
||||||
|
|
||||||
const interactionsLoader = new CacheableContractInteractionsLoader(
|
const interactionsLoader = new CacheableContractInteractionsLoader(
|
||||||
new ContractInteractionsLoader(arweave),
|
new ContractInteractionsLoader(arweave),
|
||||||
new MemBlockHeightSwCache()
|
new MemBlockHeightSwCache(maxStoredBlockHeights)
|
||||||
);
|
);
|
||||||
|
|
||||||
const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache());
|
const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache());
|
||||||
|
|
||||||
const stateEvaluator = new CacheableStateEvaluator(arweave, new MemBlockHeightSwCache<EvalStateResult<unknown>>(), [
|
const stateEvaluator = new CacheableStateEvaluator(
|
||||||
new Evolve(definitionLoader, executorFactory)
|
arweave,
|
||||||
]);
|
new MemBlockHeightSwCache<EvalStateResult<unknown>>(maxStoredBlockHeights),
|
||||||
|
[new Evolve(definitionLoader, executorFactory)]
|
||||||
|
);
|
||||||
|
|
||||||
const interactionsSorter = new LexicographicalInteractionsSorter(arweave);
|
const interactionsSorter = new LexicographicalInteractionsSorter(arweave);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BlockHeightCacheResult, BlockHeightKey, BlockHeightSwCache, MemCache } from '@smartweave/cache';
|
import { BlockHeightCacheResult, BlockHeightKey, BlockHeightSwCache } from '@smartweave/cache';
|
||||||
import {
|
import {
|
||||||
DefaultStateEvaluator,
|
DefaultStateEvaluator,
|
||||||
EvalStateResult,
|
EvalStateResult,
|
||||||
@@ -7,9 +7,8 @@ import {
|
|||||||
HandlerApi
|
HandlerApi
|
||||||
} from '@smartweave/core';
|
} from '@smartweave/core';
|
||||||
import Arweave from 'arweave';
|
import Arweave from 'arweave';
|
||||||
import { GQLEdgeInterface, GQLNodeInterface } from '@smartweave/legacy';
|
import { GQLNodeInterface, InteractionTx } from '@smartweave/legacy';
|
||||||
import { Benchmark, LoggerFactory } from '@smartweave/logging';
|
import { Benchmark, LoggerFactory } from '@smartweave/logging';
|
||||||
import { deepCopy } from '@smartweave/utils';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of DefaultStateEvaluator that adds caching capabilities
|
* An implementation of DefaultStateEvaluator that adds caching capabilities
|
||||||
@@ -34,12 +33,16 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
|||||||
|
|
||||||
let cachedState: BlockHeightCacheResult<EvalStateResult<State>> | null = null;
|
let cachedState: BlockHeightCacheResult<EvalStateResult<State>> | null = null;
|
||||||
|
|
||||||
|
this.cLogger.debug('executionContext.sortedInteractions', executionContext.sortedInteractions.length);
|
||||||
|
|
||||||
const sortedInteractionsUpToBlock = executionContext.sortedInteractions.filter((tx) => {
|
const sortedInteractionsUpToBlock = executionContext.sortedInteractions.filter((tx) => {
|
||||||
return tx.node.block.height <= executionContext.blockHeight;
|
return tx.node.block.height <= executionContext.blockHeight;
|
||||||
});
|
});
|
||||||
|
|
||||||
let missingInteractions = sortedInteractionsUpToBlock.slice();
|
let missingInteractions = sortedInteractionsUpToBlock.slice();
|
||||||
|
|
||||||
|
this.cLogger.debug('missingInteractions', missingInteractions.length);
|
||||||
|
|
||||||
// if there was anything to cache...
|
// if there was anything to cache...
|
||||||
if (sortedInteractionsUpToBlock.length > 0) {
|
if (sortedInteractionsUpToBlock.length > 0) {
|
||||||
// get latest available cache for the requested block height
|
// get latest available cache for the requested block height
|
||||||
@@ -52,7 +55,7 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
|||||||
|
|
||||||
if (cachedState != null) {
|
if (cachedState != null) {
|
||||||
this.cLogger.debug(`Cached state for ${executionContext.contractDefinition.txId}`, {
|
this.cLogger.debug(`Cached state for ${executionContext.contractDefinition.txId}`, {
|
||||||
block: cachedState.cachedHeight,
|
cachedHeight: cachedState.cachedHeight,
|
||||||
requestedBlockHeight
|
requestedBlockHeight
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,16 +110,30 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onStateUpdate<State>(
|
async onStateEvaluated<State>(
|
||||||
currentInteraction: GQLNodeInterface,
|
lastInteraction: GQLNodeInterface,
|
||||||
executionContext: ExecutionContext<State>,
|
executionContext: ExecutionContext<State>,
|
||||||
state: EvalStateResult<State>
|
state: EvalStateResult<State>
|
||||||
) {
|
): Promise<void> {
|
||||||
await super.onStateUpdate(currentInteraction, executionContext, state);
|
this.cLogger.debug(
|
||||||
|
`onStateEvaluated: cache update for contract ${executionContext.contractDefinition.txId} [${lastInteraction.block.height}]`
|
||||||
|
);
|
||||||
|
await this.cache.put(
|
||||||
|
new BlockHeightKey(executionContext.contractDefinition.txId, lastInteraction.block.height),
|
||||||
|
state
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onStateUpdate<State>(
|
||||||
|
currentInteraction: GQLNodeInterface,
|
||||||
|
executionContext: ExecutionContext<State, unknown>,
|
||||||
|
state: EvalStateResult<State>
|
||||||
|
): Promise<void> {
|
||||||
|
if (executionContext.evaluationOptions.updateCacheForEachInteraction) {
|
||||||
await this.cache.put(
|
await this.cache.put(
|
||||||
new BlockHeightKey(executionContext.contractDefinition.txId, currentInteraction.block.height),
|
new BlockHeightKey(executionContext.contractDefinition.txId, currentInteraction.block.height),
|
||||||
state
|
state
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
const { MemBlockHeightSwCache } = require('../lib/cjs/cache/impl/MemBlockHeightCache');
|
const { MemBlockHeightSwCache } = require('../lib/cjs/cache/impl/MemBlockHeightCache');
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 3000;
|
const port = 3000;
|
||||||
|
|
||||||
console.log(MemBlockHeightSwCache);
|
app.use(cors());
|
||||||
|
app.use(express.json({ limit: "50mb", extended: true }));
|
||||||
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
const caches = {
|
const caches = {
|
||||||
STATE: new MemBlockHeightSwCache(1),
|
STATE: new MemBlockHeightSwCache(1),
|
||||||
|
|||||||
20
yarn.lock
20
yarn.lock
@@ -1475,10 +1475,10 @@ argparse@^1.0.7:
|
|||||||
dependencies:
|
dependencies:
|
||||||
sprintf-js "~1.0.2"
|
sprintf-js "~1.0.2"
|
||||||
|
|
||||||
arlocal@^1.0.43:
|
arlocal@^1.0.44:
|
||||||
version "1.0.43"
|
version "1.0.44"
|
||||||
resolved "https://registry.yarnpkg.com/arlocal/-/arlocal-1.0.43.tgz#ce45ed2882a75293990aadd57dea1c085b7f345f"
|
resolved "https://registry.yarnpkg.com/arlocal/-/arlocal-1.0.44.tgz#21f0d206b6d539aff04a24c2002f4c42ad467419"
|
||||||
integrity sha512-9G5QAtuDrD/8DvkrLk7O8uCIIVwo2tW4+1XCx90OrDfOh3QH0/7peKLcpE2dWYkl3684lvyJ/ku/kRJcQhQvNA==
|
integrity sha512-NviY1QzOb4v66rh0zTwNV8eoYu4my1hJbLE+RvuPgqrg32nBS3Snv53CyZ36czs4ffpDkpHv01p4dpFIrox1wA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@koa/cors" "^3.1.0"
|
"@koa/cors" "^3.1.0"
|
||||||
apollo-server-koa "^2.25.1"
|
apollo-server-koa "^2.25.1"
|
||||||
@@ -2163,6 +2163,14 @@ core-util-is@^1.0.2, core-util-is@~1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||||
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
||||||
|
|
||||||
|
cors@^2.8.5:
|
||||||
|
version "2.8.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
|
||||||
|
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
|
||||||
|
dependencies:
|
||||||
|
object-assign "^4"
|
||||||
|
vary "^1"
|
||||||
|
|
||||||
create-require@^1.1.0:
|
create-require@^1.1.0:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||||
@@ -4794,7 +4802,7 @@ oauth-sign@~0.9.0:
|
|||||||
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
|
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
|
||||||
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
|
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
|
||||||
|
|
||||||
object-assign@^4.1.0:
|
object-assign@^4, object-assign@^4.1.0:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||||
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
||||||
@@ -6230,7 +6238,7 @@ v8-to-istanbul@^8.0.0:
|
|||||||
convert-source-map "^1.6.0"
|
convert-source-map "^1.6.0"
|
||||||
source-map "^0.7.3"
|
source-map "^0.7.3"
|
||||||
|
|
||||||
vary@^1.1.2, vary@~1.1.2:
|
vary@^1, vary@^1.1.2, vary@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||||
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
|
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
|
||||||
|
|||||||
Reference in New Issue
Block a user