feat: generate 'stacktrace' from all the contract interactions #21

This commit is contained in:
ppedziwiatr
2021-10-04 19:00:54 +02:00
committed by Piotr Pędziwiatr
parent fb0b108b29
commit dc0191edd0
14 changed files with 24320 additions and 94 deletions

View File

@@ -7,6 +7,8 @@ import {
InteractionTx,
Tags
} from '@smartweave';
import { NetworkInfoInterface } from 'arweave/node/network';
import { ContractCallStack } from '../core/ContractCallStack';
/**
* A base interface to be implemented by SmartWeave Contracts clients
@@ -100,4 +102,8 @@ export interface Contract<State = unknown> {
* @param transfer - additional {@link ArTransfer} than can be attached to the interaction transaction
*/
writeInteraction<Input = unknown>(input: Input, tags?: Tags, transfer?: ArTransfer): Promise<string | null>;
getCallStack(): ContractCallStack;
getNetworkInfo(): NetworkInfoInterface;
}

View File

@@ -11,6 +11,7 @@ import {
EvaluationOptions,
ExecutionContext,
HandlerApi,
InteractionData,
InteractionResult,
InteractionTx,
LoggerFactory,
@@ -20,6 +21,7 @@ import {
} from '@smartweave';
import { TransactionStatusResponse } from 'arweave/node/transactions';
import { NetworkInfoInterface } from 'arweave/node/network';
import { ContractCallStack, InteractionCall } from '../core/ContractCallStack';
/**
* An implementation of {@link Contract} that is backwards compatible with current style
@@ -30,10 +32,7 @@ import { NetworkInfoInterface } from 'arweave/node/network';
export class HandlerBasedContract<State> implements Contract<State> {
private readonly logger = LoggerFactory.INST.create('HandlerBasedContract');
/**
* wallet connected to this contract
*/
protected wallet?: ArWallet;
private callStack: ContractCallStack;
private evaluationOptions: EvaluationOptions = new DefaultEvaluationOptions();
/**
@@ -41,45 +40,44 @@ export class HandlerBasedContract<State> implements Contract<State> {
* Only the 'root' contract call should read this data from Arweave - all the inner calls ("child" contracts)
* should reuse this data from the parent ("calling) contract.
*/
public networkInfo?: NetworkInfoInterface = null;
private networkInfo?: NetworkInfoInterface = null;
/**
* wallet connected to this contract
*/
protected wallet?: ArWallet;
constructor(
readonly contractTxId: string,
protected readonly smartweave: SmartWeave,
// note: this will be probably used for creating contract's
// call hierarchy and generating some sort of "stack trace"
private readonly callingContract: Contract = null
private readonly callingContract: Contract = null,
private readonly callingInteraction: InteractionTx = null
) {
this.waitForConfirmation = this.waitForConfirmation.bind(this);
if (callingContract != null) {
this.networkInfo = (callingContract as HandlerBasedContract<State>).networkInfo;
this.networkInfo = callingContract.getNetworkInfo();
//callingContract.getCallStack().
// sanity-check...
if (this.networkInfo == null) {
throw Error('Calling contract should have the network info already set!');
}
const interaction: InteractionCall = callingContract.getCallStack().getInteraction(callingInteraction.id);
const callStack = new ContractCallStack(contractTxId);
interaction.interactionInput.foreignContractCalls.set(contractTxId, callStack);
this.callStack = callStack;
} else {
this.callStack = new ContractCallStack(contractTxId);
}
}
connect(wallet: ArWallet): Contract<State> {
this.wallet = wallet;
return this;
}
setEvaluationOptions(options: Partial<EvaluationOptions>): Contract<State> {
this.evaluationOptions = {
...this.evaluationOptions,
...options
};
return this;
}
async readState(
blockHeight?: number,
currentTx?: { interactionTxId: string; contractTxId: string }[]
): Promise<EvalStateResult<State>> {
this.logger.info('Read state for', this.contractTxId);
this.maybeClearNetworkInfo();
this.maybeClear();
const { stateEvaluator } = this.smartweave;
const benchmark = Benchmark.measure();
@@ -103,7 +101,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
transfer: ArTransfer = emptyTransfer
): Promise<InteractionResult<State, View>> {
this.logger.info('View state for', this.contractTxId);
this.maybeClearNetworkInfo();
this.maybeClear();
if (!this.wallet) {
this.logger.warn('Wallet not set.');
}
@@ -147,11 +145,9 @@ export class HandlerBasedContract<State> implements Contract<State> {
// creating a real transaction, with multiple calls to Arweave, seems like a huge waste.
// call one of the contract's view method
const handleResult = await executionContext.handler.handle<Input, View>(
executionContext,
evalStateResult,
const handleResult = await executionContext.handler.handle<Input, View>(executionContext, evalStateResult, {
interaction,
{
interactionTx: {
id: null,
recipient: transfer.target,
owner: {
@@ -164,8 +160,8 @@ export class HandlerBasedContract<State> implements Contract<State> {
},
block: executionContext.currentBlockData
},
[]
);
currentTx: []
});
if (handleResult.type !== 'ok') {
this.logger.fatal('Error while interacting with contract', {
@@ -177,12 +173,15 @@ export class HandlerBasedContract<State> implements Contract<State> {
return handleResult;
}
async viewStateForTx<Input, View>(input: Input, transaction: InteractionTx): Promise<InteractionResult<State, View>> {
this.logger.info(`Vies state for ${this.contractTxId}`, transaction);
this.maybeClearNetworkInfo();
async viewStateForTx<Input, View>(
input: Input,
interactionTx: InteractionTx
): Promise<InteractionResult<State, View>> {
this.logger.info(`Vies state for ${this.contractTxId}`, interactionTx);
this.maybeClear();
const { stateEvaluator } = this.smartweave;
const executionContext = await this.createExecutionContextFromTx(this.contractTxId, transaction);
const executionContext = await this.createExecutionContextFromTx(this.contractTxId, interactionTx);
const evalStateResult = await stateEvaluator.eval<State>(executionContext, []);
const interaction: ContractInteraction<Input> = {
@@ -190,13 +189,11 @@ export class HandlerBasedContract<State> implements Contract<State> {
caller: executionContext.caller
};
return await executionContext.handler.handle<Input, View>(
executionContext,
evalStateResult,
return await executionContext.handler.handle<Input, View>(executionContext, evalStateResult, {
interaction,
transaction,
[]
);
interactionTx,
currentTx: []
});
}
async writeInteraction<Input>(
@@ -207,7 +204,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
if (!this.wallet) {
throw new Error("Wallet not connected. Use 'connect' method first.");
}
this.maybeClearNetworkInfo();
this.maybeClear();
const { arweave } = this.smartweave;
const interactionTx = await createTx(
@@ -346,14 +343,36 @@ export class HandlerBasedContract<State> implements Contract<State> {
};
}
private maybeClearNetworkInfo() {
private maybeClear() {
if (this.callingContract == null) {
this.logger.debug('Clearing network info for the root contract');
this.logger.debug('Clearing network info and call stack for the root contract');
this.networkInfo = null;
this.callStack = new ContractCallStack(this.txId());
}
}
txId(): string {
return this.contractTxId;
}
getCallStack(): ContractCallStack {
return this.callStack;
}
getNetworkInfo(): NetworkInfoInterface {
return this.networkInfo;
}
connect(wallet: ArWallet): Contract<State> {
this.wallet = wallet;
return this;
}
setEvaluationOptions(options: Partial<EvaluationOptions>): Contract<State> {
this.evaluationOptions = {
...this.evaluationOptions,
...options
};
return this;
}
}

View File

@@ -0,0 +1,67 @@
import { InteractionData } from '@smartweave';
export class ContractCallStack {
readonly interactions: Map<string, InteractionCall> = new Map();
constructor(public readonly contractTxId: string, public readonly label: string = '') {}
addInteractionData(interactionData: InteractionData<any>): InteractionCall {
const { interaction, interactionTx } = interactionData;
const interactionCall = InteractionCall.create(
new InteractionInput(
interactionTx.id,
interactionTx.block.height,
interactionTx.block.timestamp,
interaction.caller,
interaction.input.function,
interaction.input,
new Map()
)
);
this.interactions.set(interactionTx.id, interactionCall);
return interactionCall;
}
getInteraction(txId: string) {
return this.interactions.get(txId);
}
}
export class InteractionCall {
interactionOutput: InteractionOutput;
private constructor(readonly interactionInput: InteractionInput) {}
static create(interactionInput: InteractionInput): InteractionCall {
return new InteractionCall(interactionInput);
}
update(interactionOutput: InteractionOutput) {
this.interactionOutput = interactionOutput;
}
}
export class InteractionInput {
constructor(
public readonly txId: string,
public readonly blockHeight: number,
public readonly blockTimestamp: number,
public readonly caller: string,
public readonly functionName: string,
public readonly functionArguments: [],
public readonly foreignContractCalls: Map<string, ContractCallStack> = new Map()
) {}
}
export class InteractionOutput {
constructor(
public readonly cacheHit: boolean,
public readonly intermediaryCacheHit: boolean,
public readonly outputState: any,
public readonly executionTime: number,
public readonly valid: boolean,
public readonly errorMessage: string = ''
) {}
}

View File

@@ -11,6 +11,7 @@ import {
} from '@smartweave/core';
import Arweave from 'arweave';
import { Contract, HandlerBasedContract, PstContract, PstContractImpl } from '@smartweave/contract';
import { InteractionTx } from '@smartweave/legacy';
/**
* The SmartWeave "motherboard" ;-).
@@ -43,8 +44,8 @@ export class SmartWeave {
* @param contractTxId
* @param callingContract
*/
contract<State>(contractTxId: string, callingContract?: Contract): Contract<State> {
return new HandlerBasedContract<State>(contractTxId, this, callingContract);
contract<State>(contractTxId: string, callingContract?: Contract, callingInteraction?: InteractionTx): Contract<State> {
return new HandlerBasedContract<State>(contractTxId, this, callingContract, callingInteraction);
}
/**

View File

@@ -30,6 +30,14 @@ export interface StateEvaluator {
/**
* a hook that is called before communicating with other contract
* note to myself: putting values into cache only "onContractCall" may degrade performance.
* For example"
* block 722317 - contract A calls B
* block 722727 - contract A calls B
* block 722695 - contract B calls A
* If we update cache only on contract call - for the last above call (B->A)
* we would retrieve state cached for 722317. If there are any transactions
* between 722317 and 722695 - the performance will be degraded.
*/
onContractCall<State>(
currentInteraction: InteractionTx,
@@ -55,6 +63,12 @@ export class DefaultEvaluationOptions implements EvaluationOptions {
fcpOptimization = false;
updateCacheForEachInteraction = true;
enhancedValidity = false;
stackTrace = {
saveState: false
}
}
// an interface for the contract EvaluationOptions - can be used to change the behaviour of some of the features.
@@ -73,4 +87,13 @@ export interface EvaluationOptions {
// 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;
// enhanced validity report with error/exception messages included
enhancedValidity: boolean;
// a set of options that control the behaviour of the stack trace generator
stackTrace: {
// whether output state should be saved for each interaction in the stack trace (may result in huuuuge json files!)
saveState: boolean;
}
}

View File

@@ -17,6 +17,7 @@ import {
TagsParser
} from '@smartweave';
import Arweave from 'arweave';
import { InteractionCall } from '../../ContractCallStack';
// FIXME: currently this is tightly coupled with the HandlerApi
export class DefaultStateEvaluator implements StateEvaluator {
@@ -50,8 +51,8 @@ export class DefaultStateEvaluator implements StateEvaluator {
currentTx: { interactionTxId: string; contractTxId: string }[]
): Promise<EvalStateResult<State>> {
const stateEvaluationBenchmark = Benchmark.measure();
const { ignoreExceptions } = executionContext.evaluationOptions;
const { contractDefinition, sortedInteractions } = executionContext;
const { ignoreExceptions, stackTrace } = executionContext.evaluationOptions;
const { contract, contractDefinition, sortedInteractions } = executionContext;
let currentState = baseState.state;
let validity = deepCopy(baseState.validity);
@@ -61,9 +62,12 @@ export class DefaultStateEvaluator implements StateEvaluator {
);
let lastEvaluatedInteraction = null;
let errorMessage = null;
for (const missingInteraction of missingInteractions) {
const currentInteraction: GQLNodeInterface = missingInteraction.node;
const singleInteractionBenchmark = Benchmark.measure();
const interactionTx: GQLNodeInterface = missingInteraction.node;
this.logger.debug(
`[${contractDefinition.txId}][${missingInteraction.node.id}][${missingInteraction.node.block.height}]: ${
@@ -71,46 +75,53 @@ export class DefaultStateEvaluator implements StateEvaluator {
}/${missingInteractions.length} [of all:${sortedInteractions.length}]`
);
const state = await this.onNextIteration(currentInteraction, executionContext);
if (state !== null) {
this.logger.debug('Found in cache');
currentState = state.state;
validity = state.validity;
} else {
const singleInteractionBenchmark = Benchmark.measure();
const inputTag = this.tagsParser.getInputTag(missingInteraction, contractDefinition.txId);
const state = await this.onNextIteration(interactionTx, executionContext);
const inputTag = this.tagsParser.getInputTag(missingInteraction, executionContext.contractDefinition.txId);
if (!inputTag) {
this.logger.error(`Skipping tx with missing or invalid Input tag - ${currentInteraction.id}`);
this.logger.error(`Skipping tx - Input tag not found for ${interactionTx.id}`);
continue;
}
const input = this.parseInput(inputTag);
if (!input) {
this.logger.error(`Skipping tx with missing or invalid Input tag - ${currentInteraction.id}`);
this.logger.error(`Skipping tx - invalid Input tag - ${interactionTx.id}`);
continue;
}
const interaction: ContractInteraction<unknown> = {
input,
caller: currentInteraction.owner.address
caller: interactionTx.owner.address
};
let intermediaryCacheHit = false;
const interactionData = {
interaction,
interactionTx,
currentTx
};
const interactionCall: InteractionCall = contract.getCallStack().addInteractionData(interactionData);
if (state !== null) {
this.logger.debug('Found in intermediary cache');
intermediaryCacheHit = true;
currentState = state.state;
validity = state.validity;
} else {
const result = await executionContext.handler.handle(
executionContext,
new EvalStateResult(currentState, validity),
interaction,
currentInteraction,
currentTx
interactionData
);
errorMessage = result.errorMessage;
this.logResult<State>(result, currentInteraction, executionContext);
this.logResult<State>(result, interactionTx, executionContext);
if (result.type === 'exception' && ignoreExceptions !== true) {
throw new Error(`Exception while processing ${JSON.stringify(interaction)}:\n${result.result}`);
}
validity[currentInteraction.id] = result.type === 'ok';
validity[interactionTx.id] = result.type === 'ok';
// strangely - state is for some reason modified for some contracts (eg. YLVpmhSq5JmLltfg6R-5fL04rIRPrlSU22f6RQ6VyYE)
// when calling any async (even simple timeout) function here...
// that's (ie. deepCopy) a dumb workaround for this issue
@@ -119,16 +130,21 @@ export class DefaultStateEvaluator implements StateEvaluator {
// 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;
lastEvaluatedInteraction = interactionTx;
this.logger.debug('Interaction evaluation', singleInteractionBenchmark.elapsed());
}
await this.onStateUpdate<State>(
currentInteraction,
executionContext,
new EvalStateResult(currentState, validity)
);
interactionCall.update({
cacheHit: false,
intermediaryCacheHit,
outputState: stackTrace.saveState ? currentState : undefined,
executionTime: singleInteractionBenchmark.elapsed(true) as number,
valid: validity[interactionTx.id],
errorMessage: errorMessage
});
await this.onStateUpdate<State>(interactionTx, executionContext, new EvalStateResult(currentState, validity));
// I'm really NOT a fan of this "modify" feature, but I don't have idea how to better
// implement the "evolve" feature
@@ -137,15 +153,15 @@ export class DefaultStateEvaluator implements StateEvaluator {
}
}
this.logger.debug('State evaluation total:', stateEvaluationBenchmark.elapsed());
const result = new EvalStateResult<State>(currentState, validity);
const evalStateResult = 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);
await this.onStateEvaluated(lastEvaluatedInteraction, executionContext, evalStateResult);
}
return result;
return evalStateResult;
}
private logResult<State>(

View File

@@ -11,6 +11,13 @@ import {
LoggerFactory,
SmartWeaveGlobal
} from '@smartweave';
import { InteractionCall } from '../../ContractCallStack';
export interface InteractionData<Input> {
interaction: ContractInteraction<Input>,
interactionTx: InteractionTx,
currentTx: { interactionTxId: string; contractTxId: string }[]
}
/**
* A handle that effectively runs contract's code.
@@ -19,9 +26,7 @@ export interface HandlerApi<State> {
handle<Input, Result>(
executionContext: ExecutionContext<State>,
currentResult: EvalStateResult<State>,
interaction: ContractInteraction<Input>,
interactionTx: InteractionTx,
currentTx: { interactionTxId: string; contractTxId: string }[]
interactionData: InteractionData<Input>,
): Promise<InteractionResult<State, Result>>;
}
@@ -56,11 +61,11 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
async handle<Input, Result>(
executionContext: ExecutionContext<State>,
currentResult: EvalStateResult<State>,
interaction: ContractInteraction<Input>,
interactionTx: InteractionTx,
currentTx: { interactionTxId: string; contractTxId: string }[]
interactionData: InteractionData<Input>
): Promise<InteractionResult<State, Result>> {
try {
const { interaction, interactionTx, currentTx } = interactionData;
const handler = contractFunction(swGlobal, BigNumber, clarity, contractLogger) as HandlerFunction<
State,
Input,
@@ -77,7 +82,7 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
executionContext,
currentTx,
currentResult,
interactionTx
interactionTx,
);
self.assignViewContractState<Input, State>(swGlobal, contractDefinition, executionContext);
@@ -157,7 +162,7 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
const { stateEvaluator } = executionContext.smartweave;
const childContract = executionContext.smartweave
.contract(contractTxId, executionContext.contract)
.contract(contractTxId, executionContext.contract, interactionTx)
.setEvaluationOptions(executionContext.evaluationOptions);
await stateEvaluator.onContractCall(interactionTx, executionContext, currentResult);
@@ -220,7 +225,7 @@ export type HandlerResult<State, Result> = {
};
export type InteractionResult<State, Result> = HandlerResult<State, Result> & {
type: 'ok' | 'error' | 'exception';
type: InteractionResultType;
errorMessage?: string;
};
@@ -228,3 +233,6 @@ export type ContractInteraction<Input> = {
input: Input;
caller: string;
};
export type InteractionResultType = 'ok' | 'error' | 'exception';

View File

@@ -8,14 +8,19 @@ export class Benchmark {
}
private start = Date.now();
private end = null;
public reset() {
this.start = Date.now();
this.end = null;
}
public elapsed(rawValue = false): string | number {
const end = Date.now();
const result = end - this.start;
return rawValue ? result : `${(end - this.start).toFixed(0)}ms`;
if (this.end === null) {
this.end = Date.now();
}
const result = this.end - this.start;
return rawValue ? result : `${(this.end - this.start).toFixed(0)}ms`;
}
}

View File

@@ -5,3 +5,14 @@ export const sleep = (ms: number) => {
export const deepCopy = (input: unknown) => {
return JSON.parse(JSON.stringify(input));
};
export const mapReplacer = (key: unknown, value: unknown) => {
if (value instanceof Map) {
return {
dataType: 'Map',
value: Array.from(value.entries())
};
} else {
return value;
}
};

39
tools/call-stack.ts Normal file
View File

@@ -0,0 +1,39 @@
import Arweave from 'arweave';
import { LoggerFactory, mapReplacer } from '../src';
import { TsLogFactory } from '../src/logging/node/TsLogFactory';
import fs from 'fs';
import path from 'path';
import { SmartWeaveWebFactory } from '../src/core/web/SmartWeaveWebFactory';
async function main() {
LoggerFactory.use(new TsLogFactory());
LoggerFactory.INST.logLevel('debug');
const arweave = Arweave.init({
host: 'arweave.net', // Hostname or IP address for a Arweave host
port: 443, // Port
protocol: 'https', // Network protocol http or https
timeout: 60000, // Network request timeouts in milliseconds
logging: false // Enable network request logging
});
const contractTxId = 'LppT1p3wri4FCKzW5buohsjWxpJHC58_rgIO-rYTMB8';
const smartweave = SmartWeaveWebFactory.memCached(arweave);
const contract = smartweave.contract(contractTxId)
.setEvaluationOptions({
ignoreExceptions: false,
stackTrace: {
saveState: false
}
});
const { state, validity } = await contract.readState();
const callStack = contract.getCallStack();
fs.writeFileSync(path.join(__dirname, 'data', 'call_stack.json'), JSON.stringify(callStack, mapReplacer));
}
main().catch((e) => console.error(e));

22020
tools/data/call_stack.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff