diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 5ab438fe..f81f2d33 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -619,8 +619,8 @@ export class HandlerBasedContract implements Contract { const handleResult = await this.evalInteraction( { - interaction, - interactionTx: dummyTx, + action: interaction, + interaction: dummyTx, currentTx: [] }, executionContext, @@ -658,8 +658,8 @@ export class HandlerBasedContract implements Contract { }; const interactionData: InteractionData = { - interaction, - interactionTx, + action: interaction, + interaction: interactionTx, currentTx }; @@ -689,7 +689,6 @@ export class HandlerBasedContract implements Contract { interactionCall.update({ cacheHit: false, - outputState: this._evaluationOptions.stackTrace.saveState ? result.state : undefined, executionTime: benchmark.elapsed(true) as number, valid: result.type === 'ok', errorMessage: result.errorMessage, diff --git a/src/core/ContractCallRecord.ts b/src/core/ContractCallRecord.ts index a827e0a6..97797e76 100644 --- a/src/core/ContractCallRecord.ts +++ b/src/core/ContractCallRecord.ts @@ -11,23 +11,23 @@ export class ContractCallRecord { } addInteractionData(interactionData: InteractionData): InteractionCall { - const { interaction, interactionTx } = interactionData; + const { action, interaction } = interactionData; const interactionCall = InteractionCall.create( new InteractionInput( - interactionTx.id, - interactionTx.sortKey, - interactionTx.block.height, - interactionTx.block.timestamp, - interaction?.caller, - interaction?.input.function, - interaction?.input, - interactionTx.dry, + interaction.id, + interaction.sortKey, + interaction.block.height, + interaction.block.timestamp, + action?.caller, + action?.input.function, + action?.input, + interaction.dry, {} ) ); - this.interactions[interactionTx.id] = interactionCall; + this.interactions[interaction.id] = interactionCall; return interactionCall; } @@ -72,7 +72,6 @@ export class InteractionInput { export class InteractionOutput { constructor( public readonly cacheHit: boolean, - public readonly outputState: any, public readonly executionTime: number, public readonly valid: boolean, public readonly errorMessage: string = '', diff --git a/src/core/WarpPlugin.ts b/src/core/WarpPlugin.ts index e15f9a8d..1a26bc0a 100644 --- a/src/core/WarpPlugin.ts +++ b/src/core/WarpPlugin.ts @@ -13,3 +13,10 @@ export interface WarpPlugin { process(input: T): R; } + +export type EvaluationProgressInput = { + contractTxId: string; + currentInteraction: number; + allInteractions: number; + lastInteractionProcessingTime: string; +} diff --git a/src/core/modules/impl/DefaultStateEvaluator.ts b/src/core/modules/impl/DefaultStateEvaluator.ts index 30387434..89292703 100644 --- a/src/core/modules/impl/DefaultStateEvaluator.ts +++ b/src/core/modules/impl/DefaultStateEvaluator.ts @@ -13,8 +13,9 @@ import { LoggerFactory } from '../../../logging/LoggerFactory'; import { indent } from '../../../utils/utils'; import { EvalStateResult, StateEvaluator } from '../StateEvaluator'; import { ContractInteraction, HandlerApi, InteractionResult } from './HandlerExecutorFactory'; -import { canBeCached } from './StateCache'; +import { isConfirmedInteraction } from './StateCache'; import { TagsParser } from './TagsParser'; +import {EvaluationProgressInput} from "../../WarpPlugin"; const EC = new elliptic.ec('secp256k1'); @@ -48,14 +49,14 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { } protected async doReadState( - missingInteractions: GQLNodeInterface[], + interactions: GQLNodeInterface[], baseState: EvalStateResult, executionContext: ExecutionContext>, currentTx: CurrentTx[] ): Promise>> { const { ignoreExceptions, stackTrace, internalWrites, cacheEveryNInteractions } = executionContext.evaluationOptions; - const { contract, contractDefinition, sortedInteractions, warp } = executionContext; + const { contract, contractDefinition, sortedInteractions, warp, handler } = executionContext; let currentState = baseState.state; let currentSortKey = null; @@ -67,48 +68,48 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { const depth = executionContext.contract.callDepth(); this.logger.info( - `${indent(depth)}Evaluating state for ${contractDefinition.txId} [${missingInteractions.length} non-cached of ${ + `${indent(depth)}Evaluating state for ${contractDefinition.txId} [${interactions.length} non-cached of ${ sortedInteractions.length } all]` ); let errorMessage = null; - let lastConfirmedTxState: { tx: GQLNodeInterface; state: EvalStateResult } = null; - - const missingInteractionsLength = missingInteractions.length; - executionContext.handler.initState(currentState); + //let lastConfirmedTxState: { tx: GQLNodeInterface; state: EvalStateResult } = null; + let lastConfirmedTx: GQLNodeInterface = null; + let lastConfirmedState: EvalStateResult = null; + const interactionsLength = interactions.length; const evmSignatureVerificationPlugin = warp.hasPlugin('evm-signature-verification') ? warp.loadPlugin>('evm-signature-verification') : null; const progressPlugin = warp.hasPlugin('evaluation-progress') - ? warp.loadPlugin< - { - contractTxId: string; - currentInteraction: number; - allInteractions: number; - lastInteractionProcessingTime: string; - }, - void - >('evaluation-progress') + ? warp.loadPlugin('evaluation-progress') : null; - for (let i = 0; i < missingInteractionsLength; i++) { - const missingInteraction = missingInteractions[i]; + + for (let i = interactionsLength - 1; i >= 0; i--) { + if (isConfirmedInteraction(interactions[i])) { + lastConfirmedTx = interactions[i]; + break; + } + } + + for (let i = 0; i < interactionsLength; i++) { + const interaction = interactions[i]; const singleInteractionBenchmark = Benchmark.measure(); - currentSortKey = missingInteraction.sortKey; + currentSortKey = interaction.sortKey; - if (missingInteraction.vrf) { - if (!this.verifyVrf(missingInteraction.vrf, missingInteraction.sortKey, this.arweave)) { + if (interaction.vrf) { + if (!this.verifyVrf(interaction.vrf, interaction.sortKey, this.arweave)) { throw new Error('Vrf verification failed.'); } } - if (evmSignatureVerificationPlugin && this.tagsParser.isEvmSigned(missingInteraction)) { + if (evmSignatureVerificationPlugin && this.tagsParser.isEvmSigned(interaction)) { try { - if (!(await evmSignatureVerificationPlugin.process(missingInteraction))) { - this.logger.warn(`Interaction ${missingInteraction.id} was not verified, skipping.`); + if (!(await evmSignatureVerificationPlugin.process(interaction))) { + this.logger.warn(`Interaction ${interaction.id} was not verified, skipping.`); continue; } } catch (e) { @@ -118,161 +119,154 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { } this.logger.debug( - `${indent(depth)}[${contractDefinition.txId}][${missingInteraction.id}][${missingInteraction.block.height}]: ${ - missingInteractions.indexOf(missingInteraction) + 1 - }/${missingInteractions.length} [of all:${sortedInteractions.length}]` + `${indent(depth)}[${contractDefinition.txId}][${interaction.id}][${interaction.block.height}]: ${ + interactions.indexOf(interaction) + 1 + }/${interactions.length} [of all:${sortedInteractions.length}]` ); - const isInteractWrite = this.tagsParser.isInteractWrite(missingInteraction, contractDefinition.txId); + const isInteractWrite = this.tagsParser.isInteractWrite(interaction, contractDefinition.txId); // other contract makes write ("writing contract") on THIS contract if (isInteractWrite && internalWrites) { // evaluating txId of the contract that is writing on THIS contract - const writingContractTxId = this.tagsParser.getContractTag(missingInteraction); + const writingContractTxId = this.tagsParser.getContractTag(interaction); this.logger.debug(`${indent(depth)}Internal Write - Loading writing contract`, writingContractTxId); const interactionCall: InteractionCall = contract .getCallStack() - .addInteractionData({ interaction: null, interactionTx: missingInteraction, currentTx }); + .addInteractionData({ action: null, interaction: interaction, currentTx }); // creating a Contract instance for the "writing" contract const writingContract = executionContext.warp.contract(writingContractTxId, executionContext.contract, { - callingInteraction: missingInteraction, + callingInteraction: interaction, callType: 'read' }); await this.onContractCall( - missingInteraction, + interaction, executionContext, new EvalStateResult(currentState, validity, errorMessages) ); - this.logger.debug(`${indent(depth)}Reading state of the calling contract at`, missingInteraction.sortKey); + this.logger.debug(`${indent(depth)}Reading state of the calling contract at`, interaction.sortKey); /** Reading the state of the writing contract. This in turn will cause the state of THIS contract to be updated in cache - see {@link ContractHandlerApi.assignWrite} */ - await writingContract.readState(missingInteraction.sortKey, [ + await writingContract.readState(interaction.sortKey, [ ...(currentTx || []), { contractTxId: contractDefinition.txId, //not: writingContractTxId! - interactionTxId: missingInteraction.id + interactionTxId: interaction.id } ]); // loading latest state of THIS contract from cache - const newState = await this.internalWriteState(contractDefinition.txId, missingInteraction.sortKey); + const newState = await this.internalWriteState(contractDefinition.txId, interaction.sortKey); if (newState !== null) { currentState = newState.cachedValue.state; // we need to update the state in the wasm module executionContext?.handler.initState(currentState); - validity[missingInteraction.id] = newState.cachedValue.validity[missingInteraction.id]; - if (newState.cachedValue.errorMessages?.[missingInteraction.id]) { - errorMessages[missingInteraction.id] = newState.cachedValue.errorMessages[missingInteraction.id]; + validity[interaction.id] = newState.cachedValue.validity[interaction.id]; + if (newState.cachedValue.errorMessages?.[interaction.id]) { + errorMessages[interaction.id] = newState.cachedValue.errorMessages[interaction.id]; } const toCache = new EvalStateResult(currentState, validity, errorMessages); - await this.onStateUpdate(missingInteraction, executionContext, toCache); - if (canBeCached(missingInteraction)) { - lastConfirmedTxState = { - tx: missingInteraction, - state: toCache - }; + await this.onStateUpdate(interaction, executionContext, toCache); + if (interaction.id == lastConfirmedTx.id) { + lastConfirmedState = toCache; } } else { - validity[missingInteraction.id] = false; + validity[interaction.id] = false; } interactionCall.update({ cacheHit: false, - outputState: stackTrace.saveState ? currentState : undefined, executionTime: singleInteractionBenchmark.elapsed(true) as number, - valid: validity[missingInteraction.id], + valid: validity[interaction.id], errorMessage: errorMessage, gasUsed: 0 // TODO... }); } else { // "direct" interaction with this contract - "standard" processing - const inputTag = this.tagsParser.getInputTag(missingInteraction, executionContext.contractDefinition.txId); + const inputTag = this.tagsParser.getInputTag(interaction, contractDefinition.txId); if (!inputTag) { - this.logger.error(`${indent(depth)}Skipping tx - Input tag not found for ${missingInteraction.id}`); + this.logger.error(`${indent(depth)}Skipping tx - Input tag not found for ${interaction.id}`); continue; } const input = this.parseInput(inputTag); if (!input) { - this.logger.error(`${indent(depth)}Skipping tx - invalid Input tag - ${missingInteraction.id}`); + this.logger.error(`${indent(depth)}Skipping tx - invalid Input tag - ${interaction.id}`); continue; } - const interaction: ContractInteraction = { + const action: ContractInteraction = { input, - caller: missingInteraction.owner.address - }; - - const interactionData = { - interaction, - interactionTx: missingInteraction, - currentTx + caller: interaction.owner.address }; + const interactionData = { action, interaction, currentTx }; const interactionCall: InteractionCall = contract.getCallStack().addInteractionData(interactionData); - const result = await executionContext.handler.handle( - executionContext, - new EvalStateResult(currentState, validity, errorMessages), - interactionData - ); + const result = await handler.handle(executionContext, new EvalStateResult(currentState, validity, errorMessages), interactionData); errorMessage = result.errorMessage; if (result.type !== 'ok') { - errorMessages[missingInteraction.id] = errorMessage; + errorMessages[interaction.id] = errorMessage; } - this.logResult(result, missingInteraction, executionContext); + this.logResult(result, interaction, executionContext); this.logger.debug(`${indent(depth)}Interaction evaluation`, singleInteractionBenchmark.elapsed()); interactionCall.update({ cacheHit: false, - outputState: stackTrace.saveState ? currentState : undefined, executionTime: singleInteractionBenchmark.elapsed(true) as number, - valid: validity[missingInteraction.id], + valid: validity[interaction.id], errorMessage: errorMessage, gasUsed: result.gasUsed }); if (result.type === 'exception' && ignoreExceptions !== true) { - throw new Error(`Exception while processing ${JSON.stringify(interaction)}:\n${result.errorMessage}`); + throw new Error(`Exception while processing ${JSON.stringify(action)}:\n${result.errorMessage}`); } - validity[missingInteraction.id] = result.type === 'ok'; - currentState = result.state; + validity[interaction.id] = result.type === 'ok'; + + if (result.state) { + currentState = result.state; - const toCache = new EvalStateResult(currentState, validity, errorMessages); - if (canBeCached(missingInteraction)) { - lastConfirmedTxState = { - tx: missingInteraction, - state: toCache - }; + const toCache = new EvalStateResult(currentState, validity, errorMessages); + if (interaction.id == lastConfirmedTx.id) { + lastConfirmedState = toCache; + } + await this.onStateUpdate( + interaction, + executionContext, + toCache, + cacheEveryNInteractions % i == 0 //TODO: will not work for WASM + ); + } else { + currentState = null; + if (interaction.id == lastConfirmedTx.id) { + const toCache = new EvalStateResult(handler.currentState(), validity, errorMessages); + lastConfirmedState = toCache; + } } - await this.onStateUpdate( - missingInteraction, - executionContext, - toCache, - cacheEveryNInteractions % i == 0 - ); } if (progressPlugin) { progressPlugin.process({ contractTxId: contractDefinition.txId, - allInteractions: missingInteractionsLength, + allInteractions: interactionsLength, currentInteraction: i, lastInteractionProcessingTime: singleInteractionBenchmark.elapsed() as string }); } + // TODO: this obviously not work with the current state opt for WASM for (const { modify } of this.executionContextModifiers) { executionContext = await modify(currentState, executionContext); } @@ -281,8 +275,9 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { // state could have been fully retrieved from cache // or there were no interactions below requested block height - if (lastConfirmedTxState !== null) { - await this.onStateEvaluated(lastConfirmedTxState.tx, executionContext, lastConfirmedTxState.state); + // or all interaction used for evaluation were not confirmed + if (lastConfirmedTx !== null && lastConfirmedState !== null) { + await this.onStateEvaluated(lastConfirmedTx, executionContext, lastConfirmedState); } return new SortKeyCacheResult(currentSortKey, evalStateResult); diff --git a/src/core/modules/impl/HandlerExecutorFactory.ts b/src/core/modules/impl/HandlerExecutorFactory.ts index 09298aa0..31d7d1ea 100644 --- a/src/core/modules/impl/HandlerExecutorFactory.ts +++ b/src/core/modules/impl/HandlerExecutorFactory.ts @@ -230,8 +230,8 @@ async function getWasmModule(wasmResponse: Response, binary: Buffer): Promise { - interaction?: ContractInteraction; - interactionTx: GQLNodeInterface; + action?: ContractInteraction; + interaction: GQLNodeInterface; currentTx: { interactionTxId: string; contractTxId: string }[]; } @@ -246,6 +246,8 @@ export interface HandlerApi { ): Promise>; initState(state: State): void; + + currentState(): State; } export type HandlerFunction = ( diff --git a/src/core/modules/impl/StateCache.ts b/src/core/modules/impl/StateCache.ts index 11c4368c..b9b7d99f 100644 --- a/src/core/modules/impl/StateCache.ts +++ b/src/core/modules/impl/StateCache.ts @@ -1,8 +1,7 @@ import { GQLNodeInterface } from '../../../legacy/gqlResult'; -//export type StateCache = Array>; -export function canBeCached(tx: GQLNodeInterface): boolean { - // in case of using non-redstone gateway +export function isConfirmedInteraction(tx: GQLNodeInterface): boolean { + // in case of using non-warp gateway if (tx.confirmationStatus === undefined) { return true; } else { diff --git a/src/core/modules/impl/handler/AbstractContractHandler.ts b/src/core/modules/impl/handler/AbstractContractHandler.ts index 0c4a82ff..5c7f9b28 100644 --- a/src/core/modules/impl/handler/AbstractContractHandler.ts +++ b/src/core/modules/impl/handler/AbstractContractHandler.ts @@ -28,6 +28,7 @@ export abstract class AbstractContractHandler implements HandlerApi>; abstract initState(state: State): void; + abstract currentState(): State; async dispose(): Promise { // noop by default; @@ -132,7 +133,9 @@ export abstract class AbstractContractHandler implements HandlerApi extends AbstractContractHandler { ); try { - const { interaction, interactionTx, currentTx } = interactionData; + const { action, interaction, currentTx } = interactionData; const stateCopy = deepCopy(currentResult.state); - this.swGlobal._activeTx = interactionTx; - this.swGlobal.caller = interaction.caller; // either contract tx id (for internal writes) or transaction.owner - this.assignReadContractState(executionContext, currentTx, currentResult, interactionTx); + this.swGlobal._activeTx = interaction; + this.swGlobal.caller = action.caller; // either contract tx id (for internal writes) or transaction.owner + this.assignReadContractState(executionContext, currentTx, currentResult, interaction); this.assignViewContractState(executionContext); this.assignWrite(executionContext, currentTx); this.assignRefreshState(executionContext); @@ -46,7 +46,7 @@ export class JsHandlerApi extends AbstractContractHandler { } }); - const handlerResult = await Promise.race([timeoutPromise, this.contractFunction(stateCopy, interaction)]); + const handlerResult = await Promise.race([timeoutPromise, this.contractFunction(stateCopy, action)]); if (handlerResult && (handlerResult.state !== undefined || handlerResult.result !== undefined)) { return { @@ -93,4 +93,8 @@ export class JsHandlerApi extends AbstractContractHandler { initState(state: State): void { // nth to do in this impl... } + + currentState(): State { + throw new Error('Should not be used for JS Handlers'); + } } diff --git a/src/core/modules/impl/handler/WasmHandlerApi.ts b/src/core/modules/impl/handler/WasmHandlerApi.ts index 7ec7cd2f..62922c01 100644 --- a/src/core/modules/impl/handler/WasmHandlerApi.ts +++ b/src/core/modules/impl/handler/WasmHandlerApi.ts @@ -23,17 +23,17 @@ export class WasmHandlerApi extends AbstractContractHandler { interactionData: InteractionData ): Promise> { try { - const { interaction, interactionTx, currentTx } = interactionData; + const { action, interaction, currentTx } = interactionData; - this.swGlobal._activeTx = interactionTx; - this.swGlobal.caller = interaction.caller; // either contract tx id (for internal writes) or transaction.owner + this.swGlobal._activeTx = interaction; + this.swGlobal.caller = action.caller; // either contract tx id (for internal writes) or transaction.owner this.swGlobal.gasLimit = executionContext.evaluationOptions.gasLimit; this.swGlobal.gasUsed = 0; - this.assignReadContractState(executionContext, currentTx, currentResult, interactionTx); + this.assignReadContractState(executionContext, currentTx, currentResult, interaction); this.assignWrite(executionContext, currentTx); - const handlerResult = await this.doHandle(interaction); + const handlerResult = await this.doHandle(action); return { type: 'ok', @@ -156,4 +156,8 @@ export class WasmHandlerApi extends AbstractContractHandler { } } } + + currentState(): State { + return this.doGetCurrentState(); + } }