feat: feature switch for internal writes

This commit is contained in:
ppedziwiatr
2021-10-27 11:51:26 +02:00
committed by Piotr Pędziwiatr
parent 5791c81b4f
commit fbbbb26c56
15 changed files with 169 additions and 76 deletions

View File

@@ -96,8 +96,18 @@ describe('Testing internal writes', () => {
src: contractBSrc
});
contractA = smartweave.contract(contractATxId).connect(wallet);
contractB = smartweave.contract(contractBTxId).connect(wallet);
contractA = smartweave
.contract(contractATxId)
.setEvaluationOptions({
internalWrites: true
})
.connect(wallet);
contractB = smartweave
.contract(contractBTxId)
.setEvaluationOptions({
internalWrites: true
})
.connect(wallet);
await mine();
}

View File

@@ -11,7 +11,6 @@ interface ExampleContractState {
counter: number;
}
/**
* The most basic example of writes between contracts.
* In this suite "User" is calling CallingContract.writeContract
@@ -97,8 +96,18 @@ describe('Testing internal writes', () => {
src: callingContractSrc
});
calleeContract = smartweave.contract<ExampleContractState>(calleeTxId).connect(wallet);
callingContract = smartweave.contract(callingTxId).connect(wallet);
calleeContract = smartweave
.contract<ExampleContractState>(calleeTxId)
.setEvaluationOptions({
internalWrites: true
})
.connect(wallet);
callingContract = smartweave
.contract(callingTxId)
.setEvaluationOptions({
internalWrites: true
})
.connect(wallet);
await mine();
}

View File

@@ -7,7 +7,6 @@ import { Contract, LoggerFactory, SmartWeave, SmartWeaveNodeFactory } from '@sma
import path from 'path';
import { TsLogFactory } from '../../../logging/node/TsLogFactory';
/**
* This verifies whether combination of read and write state works properly.
* 1. User calls ContractA.writeContractCheck(amount)
@@ -109,8 +108,18 @@ describe('Testing internal writes', () => {
src: callingContractSrc
});
calleeContract = smartweave.contract(calleeTxId).connect(wallet);
callingContract = smartweave.contract(callingTxId).connect(wallet);
calleeContract = smartweave
.contract(calleeTxId)
.setEvaluationOptions({
internalWrites: true
})
.connect(wallet);
callingContract = smartweave
.contract(callingTxId)
.setEvaluationOptions({
internalWrites: true
})
.connect(wallet);
await mine();
}

View File

@@ -116,9 +116,24 @@ describe('Testing internal writes', () => {
src: contractBSrc
});
contractA = smartweave.contract(contractATxId).connect(wallet);
contractB = smartweave.contract(contractBTxId).connect(wallet);
contractC = smartweave.contract(contractCTxId).connect(wallet);
contractA = smartweave
.contract(contractATxId)
.setEvaluationOptions({
internalWrites: true
})
.connect(wallet);
contractB = smartweave
.contract(contractBTxId)
.setEvaluationOptions({
internalWrites: true
})
.connect(wallet);
contractC = smartweave
.contract(contractCTxId)
.setEvaluationOptions({
internalWrites: true
})
.connect(wallet);
await mine();
}

View File

@@ -61,7 +61,6 @@ describe('Testing internal writes', () => {
let contractBTxId;
let contractCTxId;
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!)
@@ -111,9 +110,9 @@ describe('Testing internal writes', () => {
src: contractBSrc
});
contractA = smartweave.contract(contractATxId).connect(wallet);
contractB = smartweave.contract(contractBTxId).connect(wallet);
contractC = smartweave.contract(contractCTxId).connect(wallet);
contractA = smartweave.contract(contractATxId).setEvaluationOptions({ internalWrites: true }).connect(wallet);
contractB = smartweave.contract(contractBTxId).setEvaluationOptions({ internalWrites: true }).connect(wallet);
contractC = smartweave.contract(contractCTxId).setEvaluationOptions({ internalWrites: true }).connect(wallet);
await mine();
}

View File

@@ -79,8 +79,14 @@ describe('Testing internal writes', () => {
src: stakingContractSrc
});
tokenContract = smartweave.contract(tokenContractTxId).connect(wallet);
stakingContract = smartweave.contract(stakingContractTxId).connect(wallet);
tokenContract = smartweave
.contract(tokenContractTxId)
.setEvaluationOptions({ internalWrites: true })
.connect(wallet);
stakingContract = smartweave
.contract(stakingContractTxId)
.setEvaluationOptions({ internalWrites: true })
.connect(wallet);
await mine();
}
@@ -129,12 +135,11 @@ describe('Testing internal writes', () => {
});
await mine();
expect((await stakingContract.readState()).state.stakes).toEqual({
});
expect((await stakingContract.readState()).state.stakes).toEqual({});
const tokenState = (await tokenContract.readState()).state;
expect(tokenState.balances).toEqual({
[walletAddress]: 10000,
[walletAddress]: 10000
});
});
@@ -178,8 +183,6 @@ describe('Testing internal writes', () => {
[stakingContractTxId]: 1000
});
});
});
describe('with read states at the end', () => {
@@ -240,7 +243,6 @@ describe('Testing internal writes', () => {
}
});
});
});
async function mine() {

View File

@@ -146,20 +146,22 @@ export class HandlerBasedContract<State> implements Contract<State> {
}
const { arweave } = this.smartweave;
await this.callContract(input, undefined, tags, transfer);
const callStack: ContractCallStack = this.getCallStack();
const innerWrites = this.innerWritesEvaluator.eval(callStack);
this.logger.debug('Input', input);
this.logger.debug('Callstack', callStack.print());
if (this.evaluationOptions.internalWrites) {
await this.callContract(input, undefined, tags, transfer);
const callStack: ContractCallStack = this.getCallStack();
const innerWrites = this.innerWritesEvaluator.eval(callStack);
this.logger.debug('Input', input);
this.logger.debug('Callstack', callStack.print());
innerWrites.forEach((contractTxId) => {
tags.push({
name: SmartWeaveTags.INTERACT_WRITE,
value: contractTxId
innerWrites.forEach((contractTxId) => {
tags.push({
name: SmartWeaveTags.INTERACT_WRITE,
value: contractTxId
});
});
});
this.logger.debug('Tags with inner calls', tags);
this.logger.debug('Tags with inner calls', tags);
}
const interactionTx = await createTx(
this.smartweave.arweave,
@@ -287,7 +289,12 @@ export class HandlerBasedContract<State> implements Contract<State> {
// (eg. if contract is calling different contracts on different block heights).
// This basically limits the amount of interactions with Arweave GraphQL endpoint -
// each such interaction takes at least ~500ms.
interactionsLoader.load(contractTxId, cachedBlockHeight + 1, this.rootBlockHeight || this.networkInfo.height)
interactionsLoader.load(
contractTxId,
cachedBlockHeight + 1,
this.rootBlockHeight || this.networkInfo.height,
this.evaluationOptions
)
]);
this.logger.debug('contract and interactions load', benchmark.elapsed());
sortedInteractions = await interactionsSorter.sort(interactions);
@@ -338,7 +345,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
if (cachedBlockHeight != blockHeight) {
[contractDefinition, interactions] = await Promise.all([
definitionLoader.load<State>(contractTxId),
await interactionsLoader.load(contractTxId, 0, blockHeight)
await interactionsLoader.load(contractTxId, 0, blockHeight, this.evaluationOptions)
]);
sortedInteractions = await interactionsSorter.sort(interactions);
} else {
@@ -466,7 +473,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
const interaction: ContractInteraction<Input> = {
input,
caller: this.callingContract.txId()//executionContext.caller
caller: this.callingContract.txId() //executionContext.caller
};
const interactionData: InteractionData<Input> = {

View File

@@ -1,4 +1,4 @@
import { GQLEdgeInterface } from '@smartweave';
import { EvaluationOptions, GQLEdgeInterface } from '@smartweave';
/**
* Implementors of this interface add functionality of loading contract's interaction transactions.
@@ -7,5 +7,10 @@ import { GQLEdgeInterface } from '@smartweave';
* Note: InteractionsLoaders are not responsible for sorting interaction transactions!
*/
export interface InteractionsLoader {
load(contractId: string, fromBlockHeight: number, toBlockHeight: number): Promise<GQLEdgeInterface[]>;
load(
contractId: string,
fromBlockHeight: number,
toBlockHeight: number,
evaluationOptions: EvaluationOptions
): Promise<GQLEdgeInterface[]>;
}

View File

@@ -72,11 +72,11 @@ export class DefaultEvaluationOptions implements EvaluationOptions {
updateCacheForEachInteraction = true;
enhancedValidity = false;
stackTrace = {
saveState: false
};
internalWrites: false;
}
// an interface for the contract EvaluationOptions - can be used to change the behaviour of some of the features.
@@ -96,12 +96,13 @@ export interface EvaluationOptions {
// 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;
};
// a new, experimental enhancement of the protocol that allows for interactWrites from
// smart contract's source code.
internalWrites: boolean;
}

View File

@@ -96,6 +96,10 @@ export class ContractHandlerApi<State> implements HandlerApi<State> {
private assignWrite(executionContext: ExecutionContext<State>, currentTx: CurrentTx[]) {
this.swGlobal.contracts.write = async <Input = unknown>(contractTxId: string, input: Input) => {
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

@@ -1,5 +1,5 @@
import {
Benchmark,
Benchmark, EvaluationOptions,
GQLEdgeInterface,
GQLResultInterface,
GQLTransactionsResultInterface,
@@ -64,8 +64,13 @@ export class ContractInteractionsLoader implements InteractionsLoader {
constructor(private readonly arweave: Arweave) {}
async load(contractTxId: string, fromBlockHeight: number, toBlockHeight: number): Promise<GQLEdgeInterface[]> {
this.logger.debug('Loading interactions for', contractTxId);
async load(
contractId: string,
fromBlockHeight: number,
toBlockHeight: number,
evaluationOptions: EvaluationOptions
): Promise<GQLEdgeInterface[]> {
this.logger.debug('Loading interactions for', contractId);
const mainTransactionsVariables: ReqVariables = {
tags: [
{
@@ -74,7 +79,7 @@ export class ContractInteractionsLoader implements InteractionsLoader {
},
{
name: SmartWeaveTags.CONTRACT_TX_ID,
values: [contractTxId]
values: [contractId]
}
],
blockFilter: {
@@ -83,35 +88,35 @@ export class ContractInteractionsLoader implements InteractionsLoader {
},
first: MAX_REQUEST
};
const mainInteractions = await this.loadPages(mainTransactionsVariables);
this.logger.debug('Main Interactions:', mainInteractions);
this.logger.debug('Main interactions length:', mainInteractions.length);
let innerWritesVariables: ReqVariables = {
tags: [
{
name: SmartWeaveTags.INTERACT_WRITE,
values: [contractTxId]
}
],
blockFilter: {
min: fromBlockHeight,
max: toBlockHeight
},
first: MAX_REQUEST
};
const innerWritesInteractions = await this.loadPages(innerWritesVariables);
this.logger.debug('Inner Writes Interactions', innerWritesInteractions);
this.logger.debug('Inner writes interactions length:', innerWritesInteractions.length);
const allInteractions = mainInteractions.concat(innerWritesInteractions);
let interactions = await this.loadPages(mainTransactionsVariables);
if (evaluationOptions.internalWrites) {
let innerWritesVariables: ReqVariables = {
tags: [
{
name: SmartWeaveTags.INTERACT_WRITE,
values: [contractId]
}
],
blockFilter: {
min: fromBlockHeight,
max: toBlockHeight
},
first: MAX_REQUEST
};
const innerWritesInteractions = await this.loadPages(innerWritesVariables);
this.logger.debug('Inner writes interactions length:', innerWritesInteractions.length);
interactions = interactions.concat(innerWritesInteractions);
}
this.logger.debug('All loaded interactions:', {
from: fromBlockHeight,
to: toBlockHeight,
loaded: allInteractions.length
loaded: interactions.length
});
return allInteractions;
return interactions;
}
private async loadPages(variables: ReqVariables) {

View File

@@ -1,4 +1,11 @@
import { BlockHeightKey, BlockHeightSwCache, GQLEdgeInterface, InteractionsLoader, LoggerFactory } from '@smartweave';
import {
BlockHeightKey,
BlockHeightSwCache,
EvaluationOptions,
GQLEdgeInterface,
InteractionsLoader,
LoggerFactory
} from '@smartweave';
/**
* This implementation of the {@link InteractionsLoader} tries to limit the amount of interactions
@@ -13,7 +20,12 @@ export class CacheableContractInteractionsLoader implements InteractionsLoader {
private readonly cache: BlockHeightSwCache<GQLEdgeInterface[]>
) {}
async load(contractId: string, fromBlockHeight: number, toBlockHeight: number): Promise<GQLEdgeInterface[]> {
async load(
contractId: string,
fromBlockHeight: number,
toBlockHeight: number,
evaluationOptions: EvaluationOptions
): Promise<GQLEdgeInterface[]> {
this.logger.debug('Loading interactions', {
contractId,
fromBlockHeight,
@@ -38,7 +50,12 @@ export class CacheableContractInteractionsLoader implements InteractionsLoader {
cachedValue
});
const missingInteractions = await this.baseImplementation.load(contractId, cachedHeight + 1, toBlockHeight);
const missingInteractions = await this.baseImplementation.load(
contractId,
cachedHeight + 1,
toBlockHeight,
evaluationOptions
);
const result = cachedValue
.filter((interaction: GQLEdgeInterface) => interaction.node.block.height >= fromBlockHeight)

View File

@@ -47,7 +47,12 @@ export class FromFileInteractionsLoader implements InteractionsLoader {
});
}
async load(contractId: string, fromBlockHeight: number, toBlockHeight: number): Promise<GQLEdgeInterface[]> {
async load(
contractId: string,
fromBlockHeight: number,
toBlockHeight: number,
evaluationOptions: EvaluationOptions
): Promise<GQLEdgeInterface[]> {
return this.transactions;
}
}

View File

@@ -110,7 +110,7 @@ async function main() {
console.log(
`\n[${contractTxs.indexOf(contractTx) + 1} / ${contractTxs.length}] loading interactions of the ${contractTxId}`
);
const interactions = await transactionsLoader.load(contractTxId, 0, 779826);
const interactions = await transactionsLoader.load(contractTxId, 0, 779826, evaluationOptions);
console.log(`${contractTxId}: ${interactions.length}`);
result[contractTxId] = interactions.length;

View File

@@ -21,7 +21,12 @@ async function main() {
const transactionsLoader = new ContractInteractionsLoader(arweave);
const result = await transactionsLoader.load('Daj-MNSnH55TDfxqC7v4eq0lKzVIwh98srUaWqyuZtY', 0, 779820);
const result = await transactionsLoader.load(
'Daj-MNSnH55TDfxqC7v4eq0lKzVIwh98srUaWqyuZtY',
0,
779820,
evaluationOptions
);
console.log(result.length);