feat: poor's man version of max interaction evaluation time protection

This commit is contained in:
ppedziwiatr
2021-10-28 15:23:48 +02:00
committed by Piotr Pędziwiatr
parent d63ab29129
commit 02b0651fb3
10 changed files with 365 additions and 1061 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,6 @@ interface ExampleContractState {
}
describe('Testing the SmartWeave client', () => {
let contractSrc: string;
let initialState: string;
let wallet: JWKInterface;
let walletAddress: string;

View File

@@ -0,0 +1,29 @@
export async function handle(state, action) {
if (state.counter === undefined) {
state.counter = 0;
}
if (action.input.function === 'loop') {
let i = 0;
// well, not really an inf. loop, as Jest will cry here
// that async operations were not stopped.
while (i++ < 2) {
await timeout(1000);
state.counter++;
}
return { state };
}
if (action.input.function === 'add') {
state.counter += 10;
return { state };
}
function timeout(delay) {
return new Promise(function(resolve) {
setTimeout(resolve, delay);
});
}
}

View File

@@ -0,0 +1,103 @@
import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { Contract, HandlerBasedContract, LoggerFactory, SmartWeave, SmartWeaveNodeFactory, timeout } from '@smartweave';
import path from 'path';
let arweave: Arweave;
let arlocal: ArLocal;
let smartweave: SmartWeave;
let contract: Contract<ExampleContractState>;
interface ExampleContractState {
counter: number;
}
describe('Testing the SmartWeave client', () => {
let contractSrc: string;
let wallet: JWKInterface;
let walletAddress: string;
beforeAll(async () => {
// note: each tests suit (i.e. file with tests that Jest is running concurrently
// with another files has to have ArLocal set to a different port!)
arlocal = new ArLocal(1830, false);
await arlocal.start();
arweave = Arweave.init({
host: 'localhost',
port: 1830,
protocol: 'http'
});
LoggerFactory.INST.logLevel('debug');
smartweave = SmartWeaveNodeFactory.memCached(arweave);
wallet = await arweave.wallets.generate();
walletAddress = await arweave.wallets.jwkToAddress(wallet);
contractSrc = fs.readFileSync(path.join(__dirname, 'data/inf-loop-contract.js'), 'utf8');
// deploying contract using the new SDK.
const contractTxId = await smartweave.createContract.deploy({
wallet,
initState: JSON.stringify({
counter: 10
}),
src: contractSrc
});
contract = smartweave
.contract<ExampleContractState>(contractTxId)
.setEvaluationOptions({
maxInteractionEvaluationTimeSeconds: 1
})
.connect(wallet);
await mine();
});
afterAll(async () => {
await arlocal.stop();
});
it('should properly deploy contract with initial state', async () => {
expect(await contract.readState()).not.toBeUndefined();
});
it('should run the non blocking function', async () => {
await contract.writeInteraction({
function: 'add'
});
await mine();
expect((await contract.readState()).state.counter).toEqual(20);
});
it('should exit long running function', async () => {
await contract.writeInteraction({
function: 'loop'
});
await mine();
await contract.writeInteraction({
function: 'add'
});
await mine();
// wait for a while for the "inf-loop" to finish
// otherwise Jest will complain that there are unresolved promises
// after finishing the tests
try {
await timeout(2).timeoutPromise;
} catch {}
expect((await contract.readState()).state.counter).toEqual(30);
});
});
async function mine() {
await arweave.api.get('mine');
}

View File

@@ -166,7 +166,10 @@ export class HandlerBasedContract<State> implements Contract<State> {
const { arweave } = this.smartweave;
if (this._evaluationOptions.internalWrites) {
await this.callContract(input, undefined, tags, transfer);
const handlerResult = await this.callContract(input, undefined, tags, transfer);
/*if (handlerResult.type !== "ok") {
throw Error(`Cannot create interaction: ${handlerResult.errorMessage}`);
}*/
const callStack: ContractCallStack = this.getCallStack();
const innerWrites = this._innerWritesEvaluator.eval(callStack);
this.logger.debug('Input', input);

View File

@@ -76,6 +76,8 @@ export class DefaultEvaluationOptions implements EvaluationOptions {
maxCallDepth = 7; // your lucky number...
maxInteractionEvaluationTimeSeconds = 60;
stackTrace = {
saveState: false
};
@@ -103,7 +105,7 @@ export interface EvaluationOptions {
// smart contract's source code.
internalWrites: boolean;
// maximum call depth between contracts
// the maximum call depth between contracts
// eg. ContractA calls ContractB,
// then ContractB calls ContractC,
// then ContractC calls ContractD
@@ -111,6 +113,9 @@ export interface EvaluationOptions {
// this is added as a protection from "stackoverflow" errors
maxCallDepth: number;
// the maximum evaluation time of a single interaction transaction
maxInteractionEvaluationTimeSeconds: number;
// 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!)

View File

@@ -11,14 +11,15 @@ import {
InteractionResult,
LoggerFactory,
RedStoneLogger,
SmartWeaveGlobal
SmartWeaveGlobal,
timeout
} from '@smartweave';
import BigNumber from 'bignumber.js';
import * as clarity from '@weavery/clarity';
export class ContractHandlerApi<State> implements HandlerApi<State> {
private readonly contractLogger: RedStoneLogger;
private readonly logger = LoggerFactory.INST.create('ContractHandler');
private readonly logger = LoggerFactory.INST.create('ContractHandlerApi');
constructor(
private readonly swGlobal: SmartWeaveGlobal,
@@ -40,6 +41,10 @@ export class ContractHandlerApi<State> implements HandlerApi<State> {
): Promise<InteractionResult<State, Result>> {
const contractLogger = LoggerFactory.INST.create('Contract');
const { timeoutId, timeoutPromise } = timeout(
executionContext.evaluationOptions.maxInteractionEvaluationTimeSeconds
);
try {
const { interaction, interactionTx, currentTx } = interactionData;
@@ -57,7 +62,8 @@ export class ContractHandlerApi<State> implements HandlerApi<State> {
this.assignWrite(executionContext, currentTx);
this.assignRefreshState(executionContext);
const handlerResult = await handler(stateCopy, interaction);
const handlerResult = await Promise.race([timeoutPromise, handler(stateCopy, interaction)]);
this.logger.debug('handlerResult', handlerResult);
if (handlerResult && (handlerResult.state || handlerResult.result)) {
@@ -86,11 +92,19 @@ export class ContractHandlerApi<State> implements HandlerApi<State> {
default:
return {
type: 'exception',
errorMessage: `${(err && err.stack) || (err && err.message)}`,
errorMessage: `${(err && err.stack) || (err && err.message) || err}`,
state: currentResult.state,
result: null
};
}
} finally {
if (timeoutId !== null) {
// it is important to clear the timeout promise
// - promise.race won't "cancel" it automatically if the "handler" promise "wins"
// - and this would ofc. cause a waste in cpu cycles
// (+ Jest complains about async operations not being stopped properly).
clearTimeout(timeoutId);
}
}
}
@@ -99,7 +113,7 @@ export class ContractHandlerApi<State> implements HandlerApi<State> {
if (!executionContext.evaluationOptions.internalWrites) {
throw new Error("Internal writes feature switched off. Change EvaluationOptions.internalWrites flag to 'true'");
}
this.logger.debug('swGlobal.write call:', {
from: this.contractDefinition.txId,
to: contractTxId,

View File

@@ -20,3 +20,17 @@ export const mapReplacer = (key: unknown, value: unknown) => {
export const asc = (a: number, b: number) => a - b;
export const desc = (a: number, b: number) => b - a;
export function timeout(s: number): { timeoutId: number; timeoutPromise: Promise<any> } {
let timeoutId = null;
const timeoutPromise = new Promise((resolve, reject) => {
timeoutId = setTimeout(() => {
clearTimeout(timeoutId);
reject('timeout');
}, s * 1000);
});
return {
timeoutId,
timeoutPromise
};
}

127
tools/stake.ts Normal file
View File

@@ -0,0 +1,127 @@
/* eslint-disable */
import Arweave from 'arweave';
import { Contract, LoggerFactory, SmartWeave, SmartWeaveNodeFactory } from '../src';
import { TsLogFactory } from '../src/logging/node/TsLogFactory';
import fs from 'fs';
import path from 'path';
import ArLocal from 'arlocal';
import { JWKInterface } from 'arweave/node/lib/wallet';
async function main() {
let tokenContractSrc: string;
let tokenContractInitialState: string;
let tokenContract: Contract<any>;
let tokenContractTxId;
let stakingContractSrc: string;
let stakingContractInitialState: string;
let stakingContract: Contract<any>;
let stakingContractTxId;
let wallet: JWKInterface;
let walletAddress: string;
let smartweave: SmartWeave;
LoggerFactory.use(new TsLogFactory());
LoggerFactory.INST.logLevel('debug');
/*
LoggerFactory.INST.logLevel('debug', 'HandlerBasedContract');
LoggerFactory.INST.logLevel('debug', 'DefaultStateEvaluator');
LoggerFactory.INST.logLevel('debug', 'CacheableStateEvaluator');
LoggerFactory.INST.logLevel('debug', 'ContractHandler');
LoggerFactory.INST.logLevel('debug', 'MemBlockHeightSwCache');
*/ const logger = LoggerFactory.INST.create('stake');
const arlocal = new ArLocal(1982, false);
await arlocal.start();
const arweave = Arweave.init({
host: 'localhost',
port: 1982,
protocol: 'http'
});
try {
smartweave = SmartWeaveNodeFactory.memCached(arweave);
wallet = await arweave.wallets.generate();
walletAddress = await arweave.wallets.jwkToAddress(wallet);
tokenContractSrc = fs.readFileSync(
path.join(__dirname, '../src/__tests__/integration/', 'data/staking/token-allowance.js'),
'utf8'
);
tokenContractInitialState = fs.readFileSync(
path.join(__dirname, '../src/__tests__/integration/', 'data/staking/token-allowance.json'),
'utf8'
);
stakingContractSrc = fs.readFileSync(
path.join(__dirname, '../src/__tests__/integration/', 'data/staking/staking-contract.js'),
'utf8'
);
stakingContractInitialState = fs.readFileSync(
path.join(__dirname, '../src/__tests__/integration/', 'data/staking/staking-contract.json'),
'utf8'
);
tokenContractTxId = await smartweave.createContract.deploy({
wallet,
initState: JSON.stringify({
...JSON.parse(tokenContractInitialState),
owner: walletAddress
}),
src: tokenContractSrc
});
stakingContractTxId = await smartweave.createContract.deploy({
wallet,
initState: JSON.stringify({
...JSON.parse(stakingContractInitialState),
tokenTxId: tokenContractTxId
}),
src: stakingContractSrc
});
tokenContract = smartweave
.contract(tokenContractTxId)
.setEvaluationOptions({ internalWrites: true })
.connect(wallet);
stakingContract = smartweave
.contract(stakingContractTxId)
.setEvaluationOptions({ internalWrites: true })
.connect(wallet);
await mine();
await tokenContract.writeInteraction({
function: 'mint',
account: walletAddress,
amount: 10000
});
await mine();
await tokenContract.writeInteraction({
function: 'approve',
spender: stakingContractTxId,
amount: 9999
});
await stakingContract.writeInteraction({
function: 'stake',
amount: 1000
});
await mine();
//const tokenState = (await tokenContract.readState()).state;
//logger.info('token stakes:', tokenState.state.stakes);
//logger.info('token balances:', tokenState.state.balances);
//logger.info('Staking state:', (await stakingContract.readState()).state.stakes);
} finally {
await arlocal.stop();
}
async function mine() {
await arweave.api.get('mine');
}
}
main().catch((e) => console.error(e));

View File

@@ -6380,7 +6380,7 @@ tslib@~2.1.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
tslog@^3.2.2:
tslog@^3.2.1:
version "3.2.2"
resolved "https://registry.yarnpkg.com/tslog/-/tslog-3.2.2.tgz#5bbaa1fab685c4273e59b38064227321a69a0694"
integrity sha512-8dwb1cYpj3/w/MZTrSkPrdlA44loUodGT8N6ULMojqV4YByVM7ynhvVs9JwcIYxhhHf4bz1C5O3NKIPehnGp/w==