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', () => {
|
describe('Testing the SmartWeave client', () => {
|
||||||
let contractSrc: string;
|
let contractSrc: string;
|
||||||
let initialState: string;
|
|
||||||
|
|
||||||
let wallet: JWKInterface;
|
let wallet: JWKInterface;
|
||||||
let walletAddress: string;
|
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;
|
const { arweave } = this.smartweave;
|
||||||
|
|
||||||
if (this._evaluationOptions.internalWrites) {
|
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 callStack: ContractCallStack = this.getCallStack();
|
||||||
const innerWrites = this._innerWritesEvaluator.eval(callStack);
|
const innerWrites = this._innerWritesEvaluator.eval(callStack);
|
||||||
this.logger.debug('Input', input);
|
this.logger.debug('Input', input);
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ export class DefaultEvaluationOptions implements EvaluationOptions {
|
|||||||
|
|
||||||
maxCallDepth = 7; // your lucky number...
|
maxCallDepth = 7; // your lucky number...
|
||||||
|
|
||||||
|
maxInteractionEvaluationTimeSeconds = 60;
|
||||||
|
|
||||||
stackTrace = {
|
stackTrace = {
|
||||||
saveState: false
|
saveState: false
|
||||||
};
|
};
|
||||||
@@ -103,7 +105,7 @@ export interface EvaluationOptions {
|
|||||||
// smart contract's source code.
|
// smart contract's source code.
|
||||||
internalWrites: boolean;
|
internalWrites: boolean;
|
||||||
|
|
||||||
// maximum call depth between contracts
|
// the maximum call depth between contracts
|
||||||
// eg. ContractA calls ContractB,
|
// eg. ContractA calls ContractB,
|
||||||
// then ContractB calls ContractC,
|
// then ContractB calls ContractC,
|
||||||
// then ContractC calls ContractD
|
// then ContractC calls ContractD
|
||||||
@@ -111,6 +113,9 @@ export interface EvaluationOptions {
|
|||||||
// this is added as a protection from "stackoverflow" errors
|
// this is added as a protection from "stackoverflow" errors
|
||||||
maxCallDepth: number;
|
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
|
// a set of options that control the behaviour of the stack trace generator
|
||||||
stackTrace: {
|
stackTrace: {
|
||||||
// whether output state should be saved for each interaction in the stack trace (may result in huuuuge json files!)
|
// 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,
|
InteractionResult,
|
||||||
LoggerFactory,
|
LoggerFactory,
|
||||||
RedStoneLogger,
|
RedStoneLogger,
|
||||||
SmartWeaveGlobal
|
SmartWeaveGlobal,
|
||||||
|
timeout
|
||||||
} from '@smartweave';
|
} from '@smartweave';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import * as clarity from '@weavery/clarity';
|
import * as clarity from '@weavery/clarity';
|
||||||
|
|
||||||
export class ContractHandlerApi<State> implements HandlerApi<State> {
|
export class ContractHandlerApi<State> implements HandlerApi<State> {
|
||||||
private readonly contractLogger: RedStoneLogger;
|
private readonly contractLogger: RedStoneLogger;
|
||||||
private readonly logger = LoggerFactory.INST.create('ContractHandler');
|
private readonly logger = LoggerFactory.INST.create('ContractHandlerApi');
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly swGlobal: SmartWeaveGlobal,
|
private readonly swGlobal: SmartWeaveGlobal,
|
||||||
@@ -40,6 +41,10 @@ export class ContractHandlerApi<State> implements HandlerApi<State> {
|
|||||||
): Promise<InteractionResult<State, Result>> {
|
): Promise<InteractionResult<State, Result>> {
|
||||||
const contractLogger = LoggerFactory.INST.create('Contract');
|
const contractLogger = LoggerFactory.INST.create('Contract');
|
||||||
|
|
||||||
|
const { timeoutId, timeoutPromise } = timeout(
|
||||||
|
executionContext.evaluationOptions.maxInteractionEvaluationTimeSeconds
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { interaction, interactionTx, currentTx } = interactionData;
|
const { interaction, interactionTx, currentTx } = interactionData;
|
||||||
|
|
||||||
@@ -57,7 +62,8 @@ export class ContractHandlerApi<State> implements HandlerApi<State> {
|
|||||||
this.assignWrite(executionContext, currentTx);
|
this.assignWrite(executionContext, currentTx);
|
||||||
this.assignRefreshState(executionContext);
|
this.assignRefreshState(executionContext);
|
||||||
|
|
||||||
const handlerResult = await handler(stateCopy, interaction);
|
const handlerResult = await Promise.race([timeoutPromise, handler(stateCopy, interaction)]);
|
||||||
|
|
||||||
this.logger.debug('handlerResult', handlerResult);
|
this.logger.debug('handlerResult', handlerResult);
|
||||||
|
|
||||||
if (handlerResult && (handlerResult.state || handlerResult.result)) {
|
if (handlerResult && (handlerResult.state || handlerResult.result)) {
|
||||||
@@ -86,11 +92,19 @@ export class ContractHandlerApi<State> implements HandlerApi<State> {
|
|||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
type: 'exception',
|
type: 'exception',
|
||||||
errorMessage: `${(err && err.stack) || (err && err.message)}`,
|
errorMessage: `${(err && err.stack) || (err && err.message) || err}`,
|
||||||
state: currentResult.state,
|
state: currentResult.state,
|
||||||
result: null
|
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) {
|
if (!executionContext.evaluationOptions.internalWrites) {
|
||||||
throw new Error("Internal writes feature switched off. Change EvaluationOptions.internalWrites flag to 'true'");
|
throw new Error("Internal writes feature switched off. Change EvaluationOptions.internalWrites flag to 'true'");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug('swGlobal.write call:', {
|
this.logger.debug('swGlobal.write call:', {
|
||||||
from: this.contractDefinition.txId,
|
from: this.contractDefinition.txId,
|
||||||
to: contractTxId,
|
to: contractTxId,
|
||||||
|
|||||||
@@ -20,3 +20,17 @@ export const mapReplacer = (key: unknown, value: unknown) => {
|
|||||||
export const asc = (a: number, b: number) => a - b;
|
export const asc = (a: number, b: number) => a - b;
|
||||||
|
|
||||||
export const desc = (a: number, b: number) => b - a;
|
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"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
|
||||||
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
|
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
|
||||||
|
|
||||||
tslog@^3.2.2:
|
tslog@^3.2.1:
|
||||||
version "3.2.2"
|
version "3.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/tslog/-/tslog-3.2.2.tgz#5bbaa1fab685c4273e59b38064227321a69a0694"
|
resolved "https://registry.yarnpkg.com/tslog/-/tslog-3.2.2.tgz#5bbaa1fab685c4273e59b38064227321a69a0694"
|
||||||
integrity sha512-8dwb1cYpj3/w/MZTrSkPrdlA44loUodGT8N6ULMojqV4YByVM7ynhvVs9JwcIYxhhHf4bz1C5O3NKIPehnGp/w==
|
integrity sha512-8dwb1cYpj3/w/MZTrSkPrdlA44loUodGT8N6ULMojqV4YByVM7ynhvVs9JwcIYxhhHf4bz1C5O3NKIPehnGp/w==
|
||||||
|
|||||||
Reference in New Issue
Block a user