feat: transactional writes from the callee point of view

This commit is contained in:
ppedziwiatr
2023-07-19 10:55:54 +02:00
committed by just_ppe
parent 014c933956
commit 9f466ca130
4 changed files with 100 additions and 7 deletions

View File

@@ -9,10 +9,14 @@ export async function handle(state, action) {
if (action.input.function === 'add') {
state.counter++;
if (action.input.throw) {
throw new ContractError('Error from "add" function');
}
return { state };
}
if (action.input.function === 'addAndWrite') {
console.log("addAndWrite");
const result = await SmartWeave.contracts.write(action.input.contractId, {
function: 'addAmount',
amount: action.input.amount
@@ -51,4 +55,5 @@ export async function handle(state, action) {
if (action.input.function === 'justThrow') {
throw new ContractError('Error from justThrow function');
}
}

View File

@@ -124,6 +124,8 @@ export async function handle(state, action) {
}
if (action.input.function === 'addAmount') {
console.log("addAmount", action.input);
state.counter += action.input.amount;
return { state };
}

View File

@@ -11,6 +11,7 @@ import { WarpFactory } from '../../../core/WarpFactory';
import { LoggerFactory } from '../../../logging/LoggerFactory';
import { DeployPlugin } from 'warp-contracts-plugin-deploy';
import { VM2Plugin } from 'warp-contracts-plugin-vm2';
import { MemoryLevel } from 'memory-level';
interface ExampleContractState {
counter: number;
@@ -213,6 +214,81 @@ describe('Testing internal writes', () => {
});
});
describe('should properly commit states', () => {
beforeAll(async () => {
await deployContracts();
});
async function currentContractEntries(contractTxId: string): Promise<[[string, string]]> {
const storage: MemoryLevel<string, any> = await warp.stateEvaluator.getCache().storage();
const sub = storage.sublevel(contractTxId, { valueEncoding: "json" });
return await sub.iterator().all();
}
it('should write to cache the initial state', async () => {
expect((await calleeContract.readState()).cachedValue.state.counter).toEqual(555);
await mineBlock(warp);
const entries = await currentContractEntries(calleeContract.txId());
expect(entries.length).toEqual(1);
});
it('should write to cache at the end of evaluation (if no interactions with other contracts)', async () => {
await calleeContract.writeInteraction({ function: 'add' });
await calleeContract.writeInteraction({ function: 'add' });
await calleeContract.writeInteraction({ function: 'add' });
await calleeContract.writeInteraction({ function: 'add' });
await mineBlock(warp);
const entries1 = await currentContractEntries(calleeContract.txId());
expect(entries1.length).toEqual(1);
await calleeContract.readState();
const entries2 = await currentContractEntries(calleeContract.txId());
expect(entries2.length).toEqual(2);
await calleeContract.writeInteraction({ function: 'add' });
await calleeContract.writeInteraction({ function: 'add' });
await mineBlock(warp);
const entries3 = await currentContractEntries(calleeContract.txId());
expect(entries3.length).toEqual(2);
await calleeContract.readState();
const entries4 = await currentContractEntries(calleeContract.txId());
expect(entries4.length).toEqual(3);
});
// i.e. it should write the state from previous sort key under the sort key of the last interaction
it('should rollback state', async () => {
await calleeContract.writeInteraction({ function: 'add' });
await mineBlock(warp);
const result1 = await calleeContract.readState();
const entries1 = await currentContractEntries(calleeContract.txId());
expect(entries1.length).toEqual(4);
await calleeContract.writeInteraction({ function: 'add', throw: true });
await mineBlock(warp);
await calleeContract.readState();
const entries2 = await currentContractEntries(calleeContract.txId());
expect(entries2.length).toEqual(5);
const lastCacheValue = await warp.stateEvaluator.getCache().getLast(calleeContract.txId());
expect(lastCacheValue.cachedValue.state).toEqual(result1.cachedValue.state);
expect(Object.keys(result1.cachedValue.errorMessages).length + 1).toEqual(Object.keys(lastCacheValue.cachedValue.errorMessages).length);
const blockHeight = (await warp.arweave.network.getInfo()).height;
expect(lastCacheValue.sortKey).toContain(`${blockHeight}`.padStart(12, '0'));
});
it('should write to cache at the end of interaction (if interaction with other contracts)', async () => {
await calleeContract.writeInteraction({ function: 'addAndWrite', contractId: callingContract.txId(), amount: 1 });
await calleeContract.writeInteraction({ function: 'add' });
await mineBlock(warp);
await calleeContract.readState();
const entries2 = await currentContractEntries(calleeContract.txId());
expect(entries2.length).toEqual(7);
});
});
describe('with read state at the end', () => {
beforeAll(async () => {
await deployContracts();

View File

@@ -152,11 +152,12 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
/**
Reading the state of the writing contract.
This in turn will cause the state of THIS contract to be
updated in uncommitted state
updated in 'interaction state'
*/
let newState: EvalStateResult<unknown> = null;
let writingContractState: SortKeyCacheResult<EvalStateResult<unknown>> = null;
try {
await writingContract.readState(missingInteraction.sortKey);
writingContractState = await writingContract.readState(missingInteraction.sortKey);
newState = contract.interactionState().get(contract.txId(), missingInteraction.sortKey);
} catch (e) {
// ppe: not sure why we're not handling all ContractErrors here...
@@ -179,15 +180,24 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
}
}
if (newState !== null) {
currentState = newState.state as State;
if (newState !== null && writingContractState !== null) {
const parentValidity = writingContractState.cachedValue.validity[missingInteraction.id];
if (parentValidity) {
currentState = newState.state as State;
}
// we need to update the state in the wasm module
// TODO: opt - reuse wasm handlers...
executionContext?.handler.initState(currentState);
validity[missingInteraction.id] = newState.validity[missingInteraction.id];
if (newState.errorMessages?.[missingInteraction.id]) {
errorMessages[missingInteraction.id] = newState.errorMessages[missingInteraction.id];
if (parentValidity) {
validity[missingInteraction.id] = newState.validity[missingInteraction.id];
if (newState.errorMessages?.[missingInteraction.id]) {
errorMessages[missingInteraction.id] = newState.errorMessages[missingInteraction.id];
}
} else {
validity[missingInteraction.id] = false;
errorMessages[missingInteraction.id] =
writingContractState.cachedValue.errorMessages[missingInteraction.id];
}
const toCache = new EvalStateResult(currentState, validity, errorMessages);