feat: enable inner contract calls for kv storage

This commit is contained in:
ppe
2023-03-14 11:02:58 +01:00
committed by Tadeuchi
parent be52a03a23
commit 9fdcd2e3fa
16 changed files with 302 additions and 180 deletions

View File

@@ -2,7 +2,7 @@ import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import {JWKInterface} from 'arweave/node/lib/wallet';
import path from 'path';
import { mineBlock } from '../_helpers';
import { PstContract, PstState } from '../../../contract/PstContract';
@@ -33,8 +33,8 @@ describe('Testing unsafe client in nested contracts with "skip" option', () => {
warp = WarpFactory.forLocal(1667).use(new DeployPlugin());
warpUnsafe = WarpFactory.forLocal(1667).use(new DeployPlugin());
({ arweave } = warp);
({ jwk: wallet, address: walletAddress } = await warp.generateWallet());
({arweave} = warp);
({jwk: wallet, address: walletAddress} = await warp.generateWallet());
safeContractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst.js'), 'utf8');
const stateFromFile: PstState = JSON.parse(fs.readFileSync(path.join(__dirname, '../data/token-pst.json'), 'utf8'));
@@ -50,7 +50,7 @@ describe('Testing unsafe client in nested contracts with "skip" option', () => {
}
};
({ contractTxId } = await warp.createContract.deploy({
({contractTxId} = await warp.createContract.deploy({
wallet,
initState: JSON.stringify(initialState),
src: safeContractSrc
@@ -62,14 +62,14 @@ describe('Testing unsafe client in nested contracts with "skip" option', () => {
pst.connect(wallet);
unsafeContractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst-unsafe.js'), 'utf8');
({ contractTxId: foreignUnsafeContractTxId } = await warp.createContract.deploy({
({contractTxId: foreignUnsafeContractTxId} = await warp.createContract.deploy({
wallet,
initState: JSON.stringify(initialState),
src: unsafeContractSrc
}));
await mineBlock(warp);
({ contractTxId: foreignSafeContractTxId } = await warp.createContract.deploy({
({contractTxId: foreignSafeContractTxId} = await warp.createContract.deploy({
wallet,
initState: JSON.stringify(initialState),
src: safeContractSrc

View File

@@ -1,4 +1,4 @@
import { DEFAULT_LEVEL_DB_LOCATION } from '../../core/WarpFactory';
import {DEFAULT_LEVEL_DB_LOCATION, WarpFactory} from '../../core/WarpFactory';
import fs from 'fs';
import { SmartWeaveGlobal } from '../../legacy/smartweave-global';
import Arweave from 'arweave';
@@ -6,16 +6,19 @@ import { DefaultEvaluationOptions } from '../../core/modules/StateEvaluator';
import { LevelDbCache } from '../../cache/impl/LevelDbCache';
import { GQLNodeInterface } from '../../legacy/gqlResult';
import { CacheKey } from '../../cache/SortKeyCache';
import {ContractInteractionState} from "../../contract/states/ContractInteractionState";
describe('KV database', () => {
describe('with the SmartWeave Global KV implementation', () => {
const arweave = Arweave.init({});
const db = new LevelDbCache({
inMemory: false,
dbLocation: `${DEFAULT_LEVEL_DB_LOCATION}/kv/KV_TRIE_TEST_SW_GLOBAL`
dbLocation: `${DEFAULT_LEVEL_DB_LOCATION}/kv/ldb/KV_TRIE_TEST_SW_GLOBAL`
});
const sut = new SmartWeaveGlobal(arweave, { id: 'a', owner: '' }, new DefaultEvaluationOptions(), db);
const interactionState = new ContractInteractionState(WarpFactory.forTestnet());
const sut = new SmartWeaveGlobal(arweave, { id: 'KV_TRIE_TEST_SW_GLOBAL', owner: '' }, new DefaultEvaluationOptions(), interactionState, db);
it('should set values', async () => {
sut._activeTx = { sortKey: '123' } as GQLNodeInterface;
@@ -26,18 +29,28 @@ describe('KV database', () => {
expect(await sut.kv.get('one')).toEqual({ val: 1 });
expect(await sut.kv.get('ninety')).toBeNull();
await sut.kv.commit();
await db.close();
await interactionState.commit(sut._activeTx);
await db.open();
expect(await sut.kv.get('one')).toEqual({ val: 1 });
sut._activeTx = { sortKey: '222' } as GQLNodeInterface;
await sut.kv.put('one', '1');
await sut.kv.put('three', 3);
await sut.kv.commit();
await db.close();
await interactionState.commit(sut._activeTx);
await db.open();
sut._activeTx = { sortKey: '330' } as GQLNodeInterface;
await sut.kv.put('one', { val: [1] });
await sut.kv.put('33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA', 23111222);
await sut.kv.commit();
await db.close();
await interactionState.commit(sut._activeTx);
await db.open();
expect(await sut.kv.get('foo')).toEqual('bar');
expect(await sut.kv.get('one')).toEqual({ val: [1] });

View File

@@ -6,6 +6,7 @@ import { GQLNodeInterface } from '../legacy/gqlResult';
import { ArTransfer, Tags, ArWallet } from './deploy/CreateContract';
import { CustomSignature } from './Signature';
import { EvaluationOptionsEvaluator } from './EvaluationOptionsEvaluator';
import { InteractionState } from './states/InteractionState';
export type BenchmarkStats = { gatewayCommunication: number; stateEvaluation: number; total: number };
@@ -245,7 +246,9 @@ export interface Contract<State = unknown> {
getStorageValues(keys: string[]): Promise<SortKeyCacheResult<Map<string, unknown>>>;
getUncommittedState(contractTxId: string): EvalStateResult<unknown>;
interactionState(): InteractionState;
/* getUncommittedState(contractTxId: string): EvalStateResult<unknown>;
setUncommittedState(contractTxId: string, result: EvalStateResult<unknown>): void;
@@ -253,5 +256,5 @@ export interface Contract<State = unknown> {
resetUncommittedState(): void;
commitStates(interaction: GQLNodeInterface): Promise<void>;
commitStates(interaction: GQLNodeInterface): Promise<void>;*/
}

View File

@@ -37,6 +37,8 @@ import { EvaluationOptionsEvaluator } from './EvaluationOptionsEvaluator';
import { WarpFetchWrapper } from '../core/WarpFetchWrapper';
import { Mutex } from 'async-mutex';
import { TransactionStatusResponse } from '../utils/types/arweave-types';
import { InteractionState } from './states/InteractionState';
import { ContractInteractionState } from './states/ContractInteractionState';
import { Crypto } from 'warp-isomorphic';
/**
@@ -51,29 +53,28 @@ export class HandlerBasedContract<State> implements Contract<State> {
// TODO: refactor: extract execution context logic to a separate class
private readonly ecLogger = LoggerFactory.INST.create('ExecutionContext');
private readonly _innerWritesEvaluator = new InnerWritesEvaluator();
private readonly _callDepth: number;
private readonly _arweaveWrapper: ArweaveWrapper;
private readonly _mutex = new Mutex();
private _callStack: ContractCallRecord;
private _evaluationOptions: EvaluationOptions;
private _eoEvaluator: EvaluationOptionsEvaluator; // this is set after loading Contract Definition for the root contract
private readonly _innerWritesEvaluator = new InnerWritesEvaluator();
private readonly _callDepth: number;
private _benchmarkStats: BenchmarkStats = null;
private readonly _arweaveWrapper: ArweaveWrapper;
private _sorter: InteractionsSorter;
private _rootSortKey: string;
private signature: Signature;
private warpFetchWrapper: WarpFetchWrapper;
private _signature: Signature;
private _warpFetchWrapper: WarpFetchWrapper;
private _children: HandlerBasedContract<unknown>[] = [];
private _uncommittedStates = new Map<string, EvalStateResult<unknown>>();
private _interactionState;
private _dreStates = new Map<string, SortKeyCacheResult<EvalStateResult<State>>>();
private readonly mutex = new Mutex();
constructor(
private readonly _contractTxId: string,
protected readonly warp: Warp,
private readonly _parentContract: Contract<unknown> = null,
private readonly _parentContract: Contract = null,
private readonly _innerCallData: InnerCallData = null
) {
this.waitForConfirmation = this.waitForConfirmation.bind(this);
@@ -81,9 +82,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
this._sorter = new LexicographicalInteractionsSorter(warp.arweave);
if (_parentContract != null) {
this._evaluationOptions = this.getRoot().evaluationOptions();
if (_parentContract.evaluationOptions().useKVStorage) {
throw new Error('Foreign writes or reads are forbidden for kv storage contracts');
}
this._callDepth = _parentContract.callDepth() + 1;
const callingInteraction: InteractionCall = _parentContract
.getCallStack()
@@ -125,10 +123,11 @@ export class HandlerBasedContract<State> implements Contract<State> {
this._rootSortKey = null;
this._evaluationOptions = new DefaultEvaluationOptions();
this._children = [];
this._interactionState = new ContractInteractionState(warp);
}
this.getCallStack = this.getCallStack.bind(this);
this.warpFetchWrapper = new WarpFetchWrapper(this.warp);
this._warpFetchWrapper = new WarpFetchWrapper(this.warp);
}
async readState(
@@ -149,8 +148,8 @@ export class HandlerBasedContract<State> implements Contract<State> {
? this._sorter.generateLastSortKey(sortKeyOrBlockHeight)
: sortKeyOrBlockHeight;
if (sortKey && !this.isRoot() && this.hasUncommittedState(this.txId())) {
const result = this.getUncommittedState(this.txId());
if (sortKey && !this.isRoot() && this.interactionState().has(this.txId())) {
const result = this.interactionState().get(this.txId());
return {
sortKey,
cachedValue: result as EvalStateResult<State>
@@ -159,7 +158,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
// TODO: not sure if we should synchronize on a contract instance or contractTxId
// in the latter case, the warp instance should keep a map contractTxId -> mutex
const releaseMutex = await this.mutex.acquire();
const releaseMutex = await this._mutex.acquire();
try {
const initBenchmark = Benchmark.measure();
this.maybeResetRootContract();
@@ -191,7 +190,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
});
if (sortKey && !this.isRoot()) {
this.setUncommittedState(this.txId(), result.cachedValue);
this.interactionState().update(this.txId(), result.cachedValue);
}
return result;
@@ -245,7 +244,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
options?: WriteInteractionOptions
): Promise<WriteInteractionResponse | null> {
this.logger.info('Write interaction', { input, options });
if (!this.signature) {
if (!this._signature) {
throw new Error("Wallet not connected. Use 'connect' method first.");
}
const { arweave, interactionsLoader, environment } = this.warp;
@@ -263,7 +262,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
const bundleInteraction = interactionsLoader.type() == 'warp' && !effectiveDisableBundling;
this.signature.checkNonArweaveSigningAvailability(bundleInteraction);
this._signature.checkNonArweaveSigningAvailability(bundleInteraction);
if (
bundleInteraction &&
@@ -334,7 +333,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
options.vrf
);
const response = this.warpFetchWrapper.fetch(
const response = this._warpFetchWrapper.fetch(
`${stripTrailingSlash(this._evaluationOptions.sequencerUrl)}/gateway/sequencer/register`,
{
method: 'POST',
@@ -376,7 +375,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
const interactionTx = await createInteractionTx(
this.warp.arweave,
this.signature.signer,
this._signature.signer,
this._contractTxId,
input,
tags,
@@ -390,7 +389,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
if (!this._evaluationOptions.internalWrites && strict) {
const { arweave } = this.warp;
const caller =
this.signature.type == 'arweave'
this._signature.type == 'arweave'
? await arweave.wallets.ownerToAddress(interactionTx.owner)
: interactionTx.owner;
const handlerResult = await this.callContract(input, 'write', caller, undefined, tags, transfer, strict, vrf);
@@ -411,7 +410,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
}
connect(signature: ArWallet | CustomSignature): Contract<State> {
this.signature = new Signature(this.warp, signature);
this._signature = new Signature(this.warp, signature);
return this;
}
@@ -526,15 +525,13 @@ export class HandlerBasedContract<State> implements Contract<State> {
contractEvaluationOptions = this.resolveEvaluationOptions(contractDefinition.manifest?.evaluationOptions);
}
if (!this.isRoot() && contractEvaluationOptions.useKVStorage) {
throw new Error('Foreign read/writes cannot be performed on kv storage contracts');
}
this.ecLogger.debug(`Evaluation options ${contractTxId}:`, contractEvaluationOptions);
handler = (await this.warp.executorFactory.create(
contractDefinition,
contractEvaluationOptions,
this.warp
this.warp,
this.interactionState()
)) as HandlerApi<State>;
}
@@ -571,7 +568,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
}
private async fetchRemoteContractState(contractId: string): Promise<DREContractStatusResponse<State> | null> {
return this.warpFetchWrapper
return this._warpFetchWrapper
.fetch(`${this._evaluationOptions.remoteStateSyncSource}?id=${contractId}&events=false`)
.then((res) => {
return res.ok ? res.json() : Promise.reject(res);
@@ -616,7 +613,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
this._rootSortKey = null;
this.warp.interactionsLoader.clearCache();
this._children = [];
this._uncommittedStates = new Map();
this._interactionState = new ContractInteractionState(this.warp);
this._dreStates = new Map();
}
}
@@ -634,7 +631,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
): Promise<InteractionResult<State, View>> {
this.logger.info('Call contract input', input);
this.maybeResetRootContract();
if (!this.signature) {
if (!this._signature) {
this.logger.warn('Wallet not set.');
}
const { arweave, stateEvaluator } = this.warp;
@@ -648,8 +645,8 @@ export class HandlerBasedContract<State> implements Contract<State> {
let effectiveCaller;
if (caller) {
effectiveCaller = caller;
} else if (this.signature) {
effectiveCaller = await this.signature.getAddress();
} else if (this._signature) {
effectiveCaller = await this._signature.getAddress();
} else {
effectiveCaller = '';
}
@@ -674,7 +671,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
this.logger.debug('interaction', interaction);
const tx = await createInteractionTx(
arweave,
sign ? this.signature?.signer : undefined,
sign ? this._signature?.signer : undefined,
this._contractTxId,
input,
tags,
@@ -727,14 +724,14 @@ export class HandlerBasedContract<State> implements Contract<State> {
const executionContext = await this.createExecutionContextFromTx(this._contractTxId, interactionTx);
if (!this.isRoot() && this.hasUncommittedState(this.txId())) {
if (!this.isRoot() && this.interactionState().has(this.txId())) {
evalStateResult = {
sortKey: interactionTx.sortKey,
cachedValue: this.getUncommittedState(this.txId()) as EvalStateResult<State>
cachedValue: this.interactionState().get(this.txId()) as EvalStateResult<State>
};
} else {
evalStateResult = await this.warp.stateEvaluator.eval<State>(executionContext);
this.setUncommittedState(this.txId(), evalStateResult.cachedValue);
this.interactionState().update(this.txId(), evalStateResult.cachedValue);
}
this.logger.debug('callContractForTx - evalStateResult', {
@@ -819,7 +816,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- params can be anything
async syncState(externalUrl: string, params?: any): Promise<Contract> {
const { stateEvaluator } = this.warp;
const response = await this.warpFetchWrapper
const response = await this._warpFetchWrapper
.fetch(
`${externalUrl}?${new URLSearchParams({
id: this._contractTxId,
@@ -884,38 +881,20 @@ export class HandlerBasedContract<State> implements Contract<State> {
}
}
getUncommittedState(contractTxId: string): EvalStateResult<unknown> {
return this.getRoot()._uncommittedStates.get(contractTxId);
interactionState(): InteractionState {
return this.getRoot()._interactionState;
}
setUncommittedState(contractTxId: string, result: EvalStateResult<unknown>): void {
this.getRoot()._uncommittedStates.set(contractTxId, result);
}
hasUncommittedState(contractTxId: string): boolean {
return this.getRoot()._uncommittedStates.has(contractTxId);
}
resetUncommittedState(): void {
this.getRoot()._uncommittedStates = new Map();
}
async commitStates(interaction: GQLNodeInterface): Promise<void> {
const uncommittedStates = this.getRoot()._uncommittedStates;
try {
// i.e. if more than root contract state is in uncommitted state
// - without this check, we would effectively cache state for each evaluated interaction
// - which is not storage-effective
if (uncommittedStates.size > 1) {
for (const [k, v] of uncommittedStates) {
await this.warp.stateEvaluator.putInCache(k, interaction, v);
}
}
} finally {
this.resetUncommittedState();
getRoot(): HandlerBasedContract<unknown> {
let result: Contract = this;
while (!result.isRoot()) {
result = result.parent();
}
return result as HandlerBasedContract<unknown>;
}
private async maybeSyncStateWithRemoteSource(
remoteState: SortKeyCacheResult<EvalStateResult<State>>,
upToSortKey: string,
@@ -965,15 +944,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
return this.getRoot()._dreStates.has(contractTxId);
}
private getRoot(): HandlerBasedContract<unknown> {
let result: Contract = this;
while (!result.isRoot()) {
result = result.parent();
}
return result as HandlerBasedContract<unknown>;
}
// Call contract and verify if there are any internal writes:
// 1. Evaluate current contract state
// 2. Apply input as "dry-run" transaction

View File

@@ -0,0 +1,102 @@
import { InteractionState } from './InteractionState';
import { BatchDBOp } from '../../cache/SortKeyCache';
import { EvalStateResult } from '../../core/modules/StateEvaluator';
import { GQLNodeInterface } from '../../legacy/gqlResult';
import { Warp } from '../../core/Warp';
export class ContractInteractionState implements InteractionState {
private readonly _json = new Map<string, EvalStateResult<unknown>>();
private readonly _initialJson = new Map<string, EvalStateResult<unknown>>();
private readonly _kv = new Map<string, BatchDBOp<unknown>[]>();
constructor(private readonly _warp: Warp) {}
has(contractTx): boolean {
return this._json.has(contractTx);
}
get(contractTxId: string): EvalStateResult<unknown> {
return this._json.get(contractTxId) || null;
}
getKV<T>(contractTxId: string): BatchDBOp<T>[] | null {
return this._kv.get(contractTxId) as BatchDBOp<T>[] || null;
}
// TODO. TWL good luck with this one :-)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getKVRange(contractTxId: string, key: string): unknown {
throw new Error('Method not implemented.');
}
async commit(interaction: GQLNodeInterface): Promise<void> {
if (interaction.dry) {
return;
}
try {
await this.doStoreJson(this._json, interaction);
await this.doStoreKV();
} finally {
this.reset();
}
}
async commitKV(): Promise<void> {
await this.doStoreKV();
this._kv.clear();
}
async rollback(interaction: GQLNodeInterface): Promise<void> {
try {
await this.doStoreJson(this._initialJson, interaction);
} finally {
this.reset();
}
}
setInitial(contractTxId: string, state: EvalStateResult<unknown>): void {
// think twice here.
this._initialJson.set(contractTxId, state);
this._json.set(contractTxId, state);
}
update(contractTxId: string, state: EvalStateResult<unknown>): void {
this._json.set(contractTxId, state);
}
updateKV(contractTxId: string, ops: BatchDBOp<unknown>[]): void {
if (!this._kv.has(contractTxId)) {
this._kv.set(contractTxId, ops);
} else {
this._kv.set(contractTxId, this._kv.get(contractTxId).concat(ops));
}
}
private reset(): void {
this._json.clear();
this._initialJson.clear();
this._kv.clear();
}
private async doStoreJson(states: Map<string, EvalStateResult<unknown>>, interaction: GQLNodeInterface) {
if (states.size > 1) {
for (const [k, v] of states) {
await this._warp.stateEvaluator.putInCache(k, interaction, v);
}
}
}
private async doStoreKV(): Promise<void> {
for (const [contractTxId, batch] of this._kv) {
const storage = this._warp.kvStorageFactory(contractTxId);
try {
await storage.open();
await storage.batch(batch);
} finally {
await storage.close();
}
}
}
}

View File

@@ -0,0 +1,50 @@
import { BatchDBOp } from '../../cache/SortKeyCache';
import { EvalStateResult } from '../../core/modules/StateEvaluator';
import { GQLNodeInterface } from '../../legacy/gqlResult';
// Handles contracts state (both the json-based and kv-based) during interaction evaluation
export interface InteractionState {
/**
* Sets the state for a given contract as it is at the beginning of the interaction evaluation.
* If the interaction evaluation of the root contract will fail (i.e. its result type is != 'ok')
* - this initial state will be committed to the cache for this interaction.
* In other words - all changes made during evaluation of this interaction will be rollbacked.
*/
setInitial(contractTxId: string, state: EvalStateResult<unknown>): void;
/**
* Updates the json-state for a given contract during interaction evaluation - e.g. as a result of an internal write
*/
update(contractTxId: string, state: EvalStateResult<unknown>): void;
/**
* Updates the kv-state for a given contract during interaction evaluation
*/
updateKV(contractTxId: string, ops: BatchDBOp<unknown>[]): void;
/**
* commits all the state changes made for all contracts within given interaction evaluation.
* Called by the {@link DefaultStateEvaluator} at the end every root's contract interaction evaluation
* - IFF the result.type == 'ok'.
*/
commit(interaction: GQLNodeInterface): Promise<void>;
commitKV(): Promise<void>;
/**
* rollbacks all the state changes made for all contracts within given interaction evaluation.
* Called by the {@link DefaultStateEvaluator} at the end every root's contract interaction evaluation
* - IFF the result.type != 'ok'.
* This ensures atomicity of state changes withing any given interaction - also in case of internal contract calls.
*/
rollback(interaction: GQLNodeInterface): Promise<void>;
has(contractTxId: string): boolean;
get(contractTxId: string): EvalStateResult<unknown> | null;
getKV<T>(contractTxId: string): BatchDBOp<T>[] | null;
// TODO
getKVRange(contractTxId: string, key: string): unknown | null;
}

View File

@@ -1,7 +1,5 @@
import Arweave from 'arweave';
import { LevelDbCache } from '../cache/impl/LevelDbCache';
import { MemCache } from '../cache/impl/MemCache';
import { CacheableExecutorFactory } from '../plugins/CacheableExecutorFactory';
import { Evolve } from '../plugins/Evolve';
import { CacheableStateEvaluator } from './modules/impl/CacheableStateEvaluator';
import { HandlerExecutorFactory } from './modules/impl/HandlerExecutorFactory';
@@ -122,7 +120,7 @@ export class WarpFactory {
dbLocation: `${cacheOptions.dbLocation}/state`
});
const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache());
const executorFactory = new HandlerExecutorFactory(arweave);
const stateEvaluator = new CacheableStateEvaluator(arweave, stateCache, [new Evolve()]);
return Warp.builder(arweave, stateCache, environment)

View File

@@ -1,6 +1,7 @@
import { ContractDefinition } from '../../core/ContractDefinition';
import { EvaluationOptions } from './StateEvaluator';
import { Warp } from '../Warp';
import { InteractionState } from '../../contract/states/InteractionState';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ContractApi {}
@@ -13,6 +14,7 @@ export interface ExecutorFactory<Api> {
create<State>(
contractDefinition: ContractDefinition<State>,
evaluationOptions: EvaluationOptions,
warp: Warp
warp: Warp,
interactionState: InteractionState
): Promise<Api>;
}

View File

@@ -1,14 +1,14 @@
import Arweave from 'arweave';
import { SortKeyCache, SortKeyCacheResult, CacheKey } from '../../../cache/SortKeyCache';
import { ExecutionContext } from '../../../core/ExecutionContext';
import { ExecutionContextModifier } from '../../../core/ExecutionContextModifier';
import { GQLNodeInterface } from '../../../legacy/gqlResult';
import { LoggerFactory } from '../../../logging/LoggerFactory';
import { indent } from '../../../utils/utils';
import { EvalStateResult } from '../StateEvaluator';
import { DefaultStateEvaluator } from './DefaultStateEvaluator';
import { HandlerApi } from './HandlerExecutorFactory';
import { genesisSortKey } from './LexicographicalInteractionsSorter';
import {CacheKey, SortKeyCache, SortKeyCacheResult} from '../../../cache/SortKeyCache';
import {ExecutionContext} from '../../../core/ExecutionContext';
import {ExecutionContextModifier} from '../../../core/ExecutionContextModifier';
import {GQLNodeInterface} from '../../../legacy/gqlResult';
import {LoggerFactory} from '../../../logging/LoggerFactory';
import {indent} from '../../../utils/utils';
import {EvalStateResult} from '../StateEvaluator';
import {DefaultStateEvaluator} from './DefaultStateEvaluator';
import {HandlerApi} from './HandlerExecutorFactory';
import {genesisSortKey} from './LexicographicalInteractionsSorter';
/**
* An implementation of DefaultStateEvaluator that adds caching capabilities.
@@ -32,7 +32,7 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
async eval<State>(
executionContext: ExecutionContext<State, HandlerApi<State>>
): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
const cachedState = executionContext.cachedState;
const {cachedState, contract} = executionContext;
const missingInteractions = executionContext.sortedInteractions;
if (cachedState && cachedState.sortKey == executionContext.requestedSortKey && !missingInteractions?.length) {
@@ -59,22 +59,22 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
executionContext.contractDefinition.initState,
executionContext
);
await contract.interactionState().commitKV();
}
if (missingInteractions.length == 0) {
this.cLogger.info(`No missing interactions ${contractTxId}`);
if (!isFirstEvaluation) {
executionContext.handler?.initState(cachedState.cachedValue.state);
return cachedState;
} else {
if (isFirstEvaluation) {
executionContext.handler?.initState(baseState);
this.cLogger.debug('Inserting initial state into cache');
const stateToCache = new EvalStateResult(baseState, {}, {});
// no real sort-key - as we're returning the initial state
await this.cache.put(new CacheKey(contractTxId, genesisSortKey), stateToCache);
return new SortKeyCacheResult<EvalStateResult<State>>(genesisSortKey, stateToCache);
} else {
executionContext.handler?.initState(cachedState.cachedValue.state);
return cachedState;
}
}
// eval state for the missing transactions - starting from the latest value from cache.
@@ -123,11 +123,9 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
contractTxId: string,
sortKey?: string
): Promise<SortKeyCacheResult<EvalStateResult<State>> | null> {
this.cLogger.debug('Searching for', { contractTxId, sortKey });
this.cLogger.debug('Searching for', {contractTxId, sortKey});
if (sortKey) {
const stateCache = (await this.cache.getLessOrEqual(contractTxId, sortKey)) as SortKeyCacheResult<
EvalStateResult<State>
>;
const stateCache = (await this.cache.getLessOrEqual(contractTxId, sortKey)) as SortKeyCacheResult<EvalStateResult<State>>;
if (stateCache) {
this.cLogger.debug(`Latest available state at ${contractTxId}: ${stateCache.sortKey}`);
}

View File

@@ -94,7 +94,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
break;
}
contract.setUncommittedState(contract.txId(), new EvalStateResult(currentState, validity, errorMessages));
contract.interactionState().setInitial(contract.txId(), new EvalStateResult(currentState, validity, errorMessages));
const missingInteraction = missingInteractions[i];
const singleInteractionBenchmark = Benchmark.measure();
@@ -150,7 +150,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
let newState: EvalStateResult<unknown> = null;
try {
await writingContract.readState(missingInteraction.sortKey);
newState = contract.getUncommittedState(contract.txId());
newState = contract.interactionState().get(contract.txId());
} catch (e) {
if (e.name == 'ContractError' && e.subtype == 'unsafeClientSkip') {
this.logger.warn('Skipping unsafe contract in internal write');
@@ -290,12 +290,16 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
if (contract.isRoot()) {
// update the uncommitted state of the root contract
if (lastConfirmedTxState) {
contract.setUncommittedState(contract.txId(), lastConfirmedTxState.state);
await contract.commitStates(missingInteraction);
contract.interactionState().update(contract.txId(), lastConfirmedTxState.state);
if (validity[missingInteraction.id]) {
await contract.interactionState().commit(missingInteraction);
} else {
await contract.interactionState().rollback(missingInteraction);
}
}
} else {
// if that's an inner contract call - only update the state in the uncommitted states
contract.setUncommittedState(contract.txId(), new EvalStateResult(currentState, validity, errorMessages));
contract.interactionState().update(contract.txId(), new EvalStateResult(currentState, validity, errorMessages));
}
}
const evalStateResult = new EvalStateResult<State>(currentState, validity, errorMessages);

View File

@@ -1,7 +1,6 @@
import Arweave from 'arweave';
import { rustWasmImports, WarpContractsCrateVersion } from './wasm/rust-wasm-imports';
import * as vm2 from 'vm2';
import { WarpCache } from '../../../cache/WarpCache';
import { ContractDefinition } from '../../../core/ContractDefinition';
import { ExecutionContext } from '../../../core/ExecutionContext';
import { GQLNodeInterface } from '../../../legacy/gqlResult';
@@ -13,10 +12,10 @@ import { EvalStateResult, EvaluationOptions } from '../StateEvaluator';
import { JsHandlerApi } from './handler/JsHandlerApi';
import { WasmHandlerApi } from './handler/WasmHandlerApi';
import { normalizeContractSource } from './normalize-source';
import { MemCache } from '../../../cache/impl/MemCache';
import { Warp } from '../../Warp';
import { isBrowser } from '../../../utils/utils';
import { Buffer } from 'warp-isomorphic';
import { InteractionState } from '../../../contract/states/InteractionState';
// 'require' to fix esbuild adding same lib in both cjs and esm format
// https://github.com/evanw/esbuild/issues/1950
@@ -37,15 +36,13 @@ export class ContractError<T> extends Error {
export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknown>> {
private readonly logger = LoggerFactory.INST.create('HandlerExecutorFactory');
// TODO: cache compiled wasm binaries here.
private readonly cache: WarpCache<string, WebAssembly.Module> = new MemCache();
constructor(private readonly arweave: Arweave) {}
async create<State>(
contractDefinition: ContractDefinition<State>,
evaluationOptions: EvaluationOptions,
warp: Warp
warp: Warp,
interactionState: InteractionState
): Promise<HandlerApi<State>> {
if (warp.hasPlugin('contract-blacklist')) {
const blacklistPlugin = warp.loadPlugin<string, Promise<boolean>>('contract-blacklist');
@@ -75,6 +72,7 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
owner: contractDefinition.owner
},
evaluationOptions,
interactionState,
kvStorage
);

View File

@@ -75,7 +75,7 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
? `Internal write auto error for call [${JSON.stringify(debugData)}]: ${result.errorMessage}`
: result.errorMessage;
calleeContract.setUncommittedState(calleeContract.txId(), {
calleeContract.interactionState().update(calleeContract.txId(), {
state: result.state as State,
validity: {
...result.originalValidity,
@@ -145,19 +145,21 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
// (by simply using destructuring operator)...
// but this (i.e. returning always stateWithValidity from here) would break backwards compatibility
// in current contract's source code..:/
return returnValidity
const result = returnValidity
? {
state: deepCopy(stateWithValidity.cachedValue.state),
validity: stateWithValidity.cachedValue.validity,
errorMessages: stateWithValidity.cachedValue.errorMessages
}
: deepCopy(stateWithValidity.cachedValue.state);
return result;
};
}
protected assignRefreshState(executionContext: ExecutionContext<State>) {
this.swGlobal.contracts.refreshState = async () => {
return executionContext.contract.getUncommittedState(this.swGlobal.contract.id)?.state;
return executionContext.contract.interactionState().get(this.swGlobal.contract.id)?.state;
};
}
}

View File

@@ -3,6 +3,7 @@ import Arweave from 'arweave';
import { EvaluationOptions } from '../core/modules/StateEvaluator';
import { GQLNodeInterface, GQLTagInterface, VrfData } from './gqlResult';
import { BatchDBOp, CacheKey, PutBatch, SortKeyCache } from '../cache/SortKeyCache';
import {InteractionState} from "../contract/states/InteractionState";
/**
*
@@ -63,6 +64,7 @@ export class SmartWeaveGlobal {
arweave: Arweave,
contract: { id: string; owner: string },
evaluationOptions: EvaluationOptions,
interactionState: InteractionState,
storage: SortKeyCache<any> | null
) {
this.gasUsed = 0;
@@ -104,7 +106,7 @@ export class SmartWeaveGlobal {
this.extensions = {};
this.kv = new KV(storage, this.transaction);
this.kv = new KV(storage, interactionState, this.transaction, this.contract.id);
}
useGas(gas: number) {
@@ -257,7 +259,11 @@ export class SWVrf {
export class KV {
private _kvBatch: BatchDBOp<any>[] = [];
constructor(private readonly _storage: SortKeyCache<any> | null, private readonly _transaction: SWTransaction) {}
constructor(
private readonly _storage: SortKeyCache<any> | null,
private readonly _interactionState: InteractionState,
private readonly _transaction: SWTransaction,
private readonly _contractTxId: string) {}
async put(key: string, value: any): Promise<void> {
this.checkStorageAvailable();
@@ -272,13 +278,20 @@ export class KV {
this.checkStorageAvailable();
const sortKey = this._transaction.sortKey;
// first we're checking if the value exists in changes registered for the activeTx
if (this._kvBatch.length > 0) {
const putBatches = this._kvBatch.filter((batchOp) => batchOp.type === 'put') as PutBatch<any>[];
const matchingPutBatch = putBatches.reverse().find((batchOp) => {
return batchOp.key.key === key && batchOp.key.sortKey === sortKey;
}) as PutBatch<V>;
if (matchingPutBatch !== undefined) {
return matchingPutBatch.value;
const activeTxValue = this.findInBatches<V>(this._kvBatch, key, sortKey);
if (activeTxValue !== undefined) {
return activeTxValue;
}
}
// then we're checking if the values exists in the interactionState
const interactionStateBatch = this._interactionState.getKV(this._contractTxId);
if (interactionStateBatch?.length > 0) {
const interactionStateValue = this.findInBatches<V>(interactionStateBatch, key, sortKey);
if (interactionStateValue !== undefined) {
return interactionStateValue;
}
}
@@ -286,11 +299,18 @@ export class KV {
return result?.cachedValue || null;
}
private findInBatches<V>(batches: BatchDBOp<any>[], key: string, sortKey: string): V | undefined {
const putBatches = batches.filter((batchOp) => batchOp.type === 'put') as PutBatch<any>[];
const matchingPutBatch = putBatches.reverse().find((batchOp) => {
return batchOp.key.key === key && batchOp.key.sortKey === sortKey;
}) as PutBatch<V>;
return matchingPutBatch?.value;
}
async commit(): Promise<void> {
if (this._storage) {
if (!this._transaction.dryRun) {
await this._storage.batch(this._kvBatch);
}
this._interactionState.updateKV(this._contractTxId, [...this._kvBatch])
this._kvBatch = [];
}
}

View File

@@ -1,41 +0,0 @@
import Arweave from 'arweave';
import { LoggerFactory } from '../logging/LoggerFactory';
import { WarpCache } from '../cache/WarpCache';
import { ContractDefinition } from '../core/ContractDefinition';
import { ExecutorFactory } from '../core/modules/ExecutorFactory';
import { EvaluationOptions } from '../core/modules/StateEvaluator';
import { Warp } from '../core/Warp';
/**
* An implementation of ExecutorFactory that adds caching capabilities
*/
export class CacheableExecutorFactory<Api> implements ExecutorFactory<Api> {
private readonly logger = LoggerFactory.INST.create('CacheableExecutorFactory');
constructor(
private readonly arweave: Arweave,
private readonly baseImplementation: ExecutorFactory<Api>,
private readonly cache: WarpCache<string, Api>
) {}
async create<State>(
contractDefinition: ContractDefinition<State>,
evaluationOptions: EvaluationOptions,
warp: Warp
): Promise<Api> {
return await this.baseImplementation.create(contractDefinition, evaluationOptions, warp);
// warn: do not cache on the contractDefinition.srcTxId. This might look like a good optimisation
// (as many contracts share the same source code), but unfortunately this is causing issues
// with the same SwGlobal object being cached for all contracts with the same source code
// (eg. SwGlobal.contract.id field - which of course should have different value for different contracts
// that share the same source).
// warn#2: cache key MUST be a combination of both txId and srcTxId -
// as "evolve" feature changes the srcTxId for the given txId...
// switching off caching for now
// - https://github.com/redstone-finance/redstone-smartcontracts/issues/53
// probably should be cached on a lower level - i.e. either handler function (for js contracts)
// or wasm module.
}
}

View File

@@ -2,10 +2,11 @@ import { ContractDefinition } from '../core/ContractDefinition';
import { ExecutorFactory } from '../core/modules/ExecutorFactory';
import { EvaluationOptions } from '../core/modules/StateEvaluator';
import { Warp } from '../core/Warp';
import { InteractionState } from '../contract/states/InteractionState';
/**
* An ExecutorFactory that allows to substitute original contract's source code.
* Useful for debugging purposes (eg. to quickly add some console.logs in contract
* Useful for debugging purposes (e.g. to quickly add some console.logs in contract
* or to test a fix or a new feature - without the need of redeploying a new contract on Arweave);
*
* Not meant to be used in production env! ;-)
@@ -20,7 +21,8 @@ export class DebuggableExecutorFactory<Api> implements ExecutorFactory<Api> {
async create<State>(
contractDefinition: ContractDefinition<State>,
evaluationOptions: EvaluationOptions,
warp: Warp
warp: Warp,
interactionState: InteractionState
): Promise<Api> {
if (Object.prototype.hasOwnProperty.call(this.sourceCode, contractDefinition.txId)) {
contractDefinition = {
@@ -29,6 +31,6 @@ export class DebuggableExecutorFactory<Api> implements ExecutorFactory<Api> {
};
}
return await this.baseImplementation.create(contractDefinition, evaluationOptions, warp);
return await this.baseImplementation.create(contractDefinition, evaluationOptions, warp, interactionState);
}
}

View File

@@ -41,7 +41,8 @@ export class Evolve implements ExecutionContextModifier {
const newHandler = (await executorFactory.create<State>(
newContractDefinition,
executionContext.evaluationOptions,
executionContext.warp
executionContext.warp,
executionContext.contract.interactionState()
)) as HandlerApi<State>;
//FIXME: side-effect...