feat: poor's man version of max interaction evaluation time protection
This commit is contained in:
committed by
Piotr Pędziwiatr
parent
d63ab29129
commit
02b0651fb3
1114
new_state_fix.json
1114
new_state_fix.json
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,6 @@ interface ExampleContractState {
|
||||
}
|
||||
describe('Testing the SmartWeave client', () => {
|
||||
let contractSrc: string;
|
||||
let initialState: string;
|
||||
|
||||
let wallet: JWKInterface;
|
||||
let walletAddress: string;
|
||||
|
||||
29
src/__tests__/integration/data/inf-loop-contract.js
Normal file
29
src/__tests__/integration/data/inf-loop-contract.js
Normal 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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
103
src/__tests__/integration/inf-loop.test.ts
Normal file
103
src/__tests__/integration/inf-loop.test.ts
Normal 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');
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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!)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
127
tools/stake.ts
Normal 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));
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user