feat: introduce InteractionType for explicit marking of contract interactions as 'write' or 'view'

This allows for some optimizations in WASM code: 'view' interactions do not require cloning of contract state

gh-309
This commit is contained in:
robal
2023-03-03 16:15:11 +01:00
parent 554bbc5d89
commit 6cd1bf1ce4
6 changed files with 58 additions and 26 deletions

View File

@@ -6,7 +6,8 @@ import {
ContractInteraction, ContractInteraction,
HandlerApi, HandlerApi,
InteractionData, InteractionData,
InteractionResult InteractionResult,
InteractionType
} from '../core/modules/impl/HandlerExecutorFactory'; } from '../core/modules/impl/HandlerExecutorFactory';
import { LexicographicalInteractionsSorter } from '../core/modules/impl/LexicographicalInteractionsSorter'; import { LexicographicalInteractionsSorter } from '../core/modules/impl/LexicographicalInteractionsSorter';
import { InteractionsSorter } from '../core/modules/InteractionsSorter'; import { InteractionsSorter } from '../core/modules/InteractionsSorter';
@@ -212,7 +213,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
transfer: ArTransfer = emptyTransfer transfer: ArTransfer = emptyTransfer
): Promise<InteractionResult<State, View>> { ): Promise<InteractionResult<State, View>> {
this.logger.info('View state for', this._contractTxId); this.logger.info('View state for', this._contractTxId);
return await this.callContract<Input, View>(input, undefined, undefined, tags, transfer); return await this.callContract<Input, View>(input, 'view', undefined, undefined, tags, transfer);
} }
async viewStateForTx<Input, View>( async viewStateForTx<Input, View>(
@@ -220,7 +221,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
interactionTx: GQLNodeInterface interactionTx: GQLNodeInterface
): Promise<InteractionResult<State, View>> { ): Promise<InteractionResult<State, View>> {
this.logger.info(`View state for ${this._contractTxId}`); this.logger.info(`View state for ${this._contractTxId}`);
return await this.doApplyInputOnTx<Input, View>(input, interactionTx); return await this.doApplyInputOnTx<Input, View>(input, interactionTx, 'view');
} }
async dryWrite<Input>( async dryWrite<Input>(
@@ -230,12 +231,12 @@ export class HandlerBasedContract<State> implements Contract<State> {
transfer?: ArTransfer transfer?: ArTransfer
): Promise<InteractionResult<State, unknown>> { ): Promise<InteractionResult<State, unknown>> {
this.logger.info('Dry-write for', this._contractTxId); this.logger.info('Dry-write for', this._contractTxId);
return await this.callContract<Input>(input, caller, undefined, tags, transfer); return await this.callContract<Input>(input, 'write', caller, undefined, tags, transfer);
} }
async applyInput<Input>(input: Input, transaction: GQLNodeInterface): Promise<InteractionResult<State, unknown>> { async applyInput<Input>(input: Input, transaction: GQLNodeInterface): Promise<InteractionResult<State, unknown>> {
this.logger.info(`Apply-input from transaction ${transaction.id} for ${this._contractTxId}`); this.logger.info(`Apply-input from transaction ${transaction.id} for ${this._contractTxId}`);
return await this.doApplyInputOnTx<Input>(input, transaction); return await this.doApplyInputOnTx<Input>(input, transaction, 'write');
} }
async writeInteraction<Input>( async writeInteraction<Input>(
@@ -370,7 +371,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
reward?: string reward?: string
) { ) {
if (this._evaluationOptions.internalWrites) { if (this._evaluationOptions.internalWrites) {
// it modifies tags // it modifies tags
await this.discoverInternalWrites<Input>(input, tags, transfer, strict, vrf); await this.discoverInternalWrites<Input>(input, tags, transfer, strict, vrf);
} }
@@ -400,7 +401,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
this.signature.type == 'arweave' this.signature.type == 'arweave'
? await arweave.wallets.ownerToAddress(interactionTx.owner) ? await arweave.wallets.ownerToAddress(interactionTx.owner)
: interactionTx.owner; : interactionTx.owner;
const handlerResult = await this.callContract(input, caller, undefined, tags, transfer, strict, vrf); const handlerResult = await this.callContract(input, 'write', caller, undefined, tags, transfer, strict, vrf);
if (handlerResult.type !== 'ok') { if (handlerResult.type !== 'ok') {
throw Error(`Cannot create interaction: ${handlerResult.errorMessage}`); throw Error(`Cannot create interaction: ${handlerResult.errorMessage}`);
} }
@@ -630,6 +631,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
private async callContract<Input, View = unknown>( private async callContract<Input, View = unknown>(
input: Input, input: Input,
interactionType: InteractionType,
caller?: string, caller?: string,
sortKey?: string, sortKey?: string,
tags: Tags = [], tags: Tags = [],
@@ -673,7 +675,8 @@ export class HandlerBasedContract<State> implements Contract<State> {
// create interaction transaction // create interaction transaction
const interaction: ContractInteraction<Input> = { const interaction: ContractInteraction<Input> = {
input, input,
caller: executionContext.caller caller: executionContext.caller,
interactionType
}; };
this.logger.debug('interaction', interaction); this.logger.debug('interaction', interaction);
@@ -723,7 +726,8 @@ export class HandlerBasedContract<State> implements Contract<State> {
private async doApplyInputOnTx<Input, View = unknown>( private async doApplyInputOnTx<Input, View = unknown>(
input: Input, input: Input,
interactionTx: GQLNodeInterface interactionTx: GQLNodeInterface,
interactionType: InteractionType
): Promise<InteractionResult<State, View>> { ): Promise<InteractionResult<State, View>> {
this.maybeResetRootContract(); this.maybeResetRootContract();
@@ -748,7 +752,8 @@ export class HandlerBasedContract<State> implements Contract<State> {
const interaction: ContractInteraction<Input> = { const interaction: ContractInteraction<Input> = {
input, input,
caller: this._parentContract.txId() caller: this._parentContract.txId(),
interactionType
}; };
const interactionData: InteractionData<Input> = { const interactionData: InteractionData<Input> = {
@@ -990,7 +995,17 @@ export class HandlerBasedContract<State> implements Contract<State> {
strict: boolean, strict: boolean,
vrf: boolean vrf: boolean
) { ) {
const handlerResult = await this.callContract(input, undefined, undefined, tags, transfer, strict, vrf, false); const handlerResult = await this.callContract(
input,
'write',
undefined,
undefined,
tags,
transfer,
strict,
vrf,
false
);
if (strict && handlerResult.type !== 'ok') { if (strict && handlerResult.type !== 'ok') {
throw Error(`Cannot create interaction: ${handlerResult.errorMessage}`); throw Error(`Cannot create interaction: ${handlerResult.errorMessage}`);

View File

@@ -213,7 +213,8 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
const interaction: ContractInteraction<unknown> = { const interaction: ContractInteraction<unknown> = {
input, input,
caller: missingInteraction.owner.address caller: missingInteraction.owner.address,
interactionType: 'write'
}; };
const interactionData = { const interactionData = {

View File

@@ -243,7 +243,7 @@ async function getWasmModule(wasmResponse: Response, binary: Buffer): Promise<We
} }
export interface InteractionData<Input> { export interface InteractionData<Input> {
interaction?: ContractInteraction<Input>; interaction: ContractInteraction<Input>;
interactionTx: GQLNodeInterface; interactionTx: GQLNodeInterface;
} }
@@ -281,9 +281,12 @@ export type InteractionResult<State, Result> = HandlerResult<State, Result> & {
originalErrorMessages?: Record<string, string>; originalErrorMessages?: Record<string, string>;
}; };
export type InteractionType = 'view' | 'write';
export type ContractInteraction<Input> = { export type ContractInteraction<Input> = {
input: Input; input: Input;
caller: string; caller: string;
interactionType: InteractionType;
}; };
export type InteractionResultType = 'ok' | 'error' | 'exception'; export type InteractionResultType = 'ok' | 'error' | 'exception';

View File

@@ -52,9 +52,10 @@ export class JsHandlerApi<State> extends AbstractContractHandler<State> {
executionContext: ExecutionContext<State> executionContext: ExecutionContext<State>
): Promise<State> { ): Promise<State> {
if (this.contractDefinition.manifest?.evaluationOptions?.useConstructor) { if (this.contractDefinition.manifest?.evaluationOptions?.useConstructor) {
const interaction = { const interaction: ContractInteraction<Input> = {
input: { function: INIT_FUNC_NAME, args: initialState } as Input, input: { function: INIT_FUNC_NAME, args: initialState } as Input,
caller: this.contractDefinition.owner caller: this.contractDefinition.owner,
interactionType: 'write'
}; };
const interactionTx = { const interactionTx = {

View File

@@ -3,7 +3,7 @@ import { ContractDefinition } from '../../../../core/ContractDefinition';
import { ExecutionContext } from '../../../../core/ExecutionContext'; import { ExecutionContext } from '../../../../core/ExecutionContext';
import { EvalStateResult } from '../../../../core/modules/StateEvaluator'; import { EvalStateResult } from '../../../../core/modules/StateEvaluator';
import { SmartWeaveGlobal } from '../../../../legacy/smartweave-global'; import { SmartWeaveGlobal } from '../../../../legacy/smartweave-global';
import { InteractionData, InteractionResult } from '../HandlerExecutorFactory'; import { ContractInteraction, InteractionData, InteractionResult } from '../HandlerExecutorFactory';
import { AbstractContractHandler } from './AbstractContractHandler'; import { AbstractContractHandler } from './AbstractContractHandler';
export class WasmHandlerApi<State> extends AbstractContractHandler<State> { export class WasmHandlerApi<State> extends AbstractContractHandler<State> {
@@ -89,17 +89,20 @@ export class WasmHandlerApi<State> extends AbstractContractHandler<State> {
} }
} }
private async doHandle(action: any): Promise<any> { private async doHandle(action: ContractInteraction<unknown>): Promise<any> {
switch (this.contractDefinition.srcWasmLang) { switch (this.contractDefinition.srcWasmLang) {
case 'rust': { case 'rust': {
let handleResult = await this.wasmExports.handle(action.input);
if (!handleResult) { const handleResult = action.interactionType === 'write' ? await this.wasmExports.warpContractWrite(action.input) : await this.wasmExports.warpContractView(action.input);
return;
if (Object.prototype.hasOwnProperty.call(handleResult, 'WriteResponse')) {
return handleResult.WriteResponse;
} }
if (Object.prototype.hasOwnProperty.call(handleResult, 'Ok')) { if (Object.prototype.hasOwnProperty.call(handleResult, 'ViewResponse')) {
return handleResult.Ok; return handleResult.ViewResponse;
} else { }
this.logger.debug('Error from rust', handleResult.Err); {
this.logger.error('Error from rust', handleResult);
let errorKey; let errorKey;
let errorArgs = ''; let errorArgs = '';
if (typeof handleResult.Err === 'string' || handleResult.Err instanceof String) { if (typeof handleResult.Err === 'string' || handleResult.Err instanceof String) {

View File

@@ -585,11 +585,20 @@ export const rustWasmImports = (swGlobal, wbindgenImports, wasmInstance, dtorVal
* @param {any} interaction * @param {any} interaction
* @returns {Promise<any>} * @returns {Promise<any>}
*/ */
module.handle = function (interaction) { module.warpContractWrite = function (interaction) {
var ret = wasmInstance.exports.handle(addHeapObject(interaction)); var ret = wasmInstance.exports.warpContractWrite(addHeapObject(interaction));
return takeObject(ret); return takeObject(ret);
}; };
/**
* @param {any} interaction
* @returns {Promise<any>}
*/
module.warpContractView = function (interaction) {
var ret = wasmInstance.exports.warpContractView(addHeapObject(interaction));
return takeObject(ret);
};
let stack_pointer = 32; let stack_pointer = 32;
function addBorrowedObject(obj) { function addBorrowedObject(obj) {