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

View File

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

View File

@@ -11,6 +11,7 @@ import { WarpFactory } from '../../../core/WarpFactory';
import { LoggerFactory } from '../../../logging/LoggerFactory'; import { LoggerFactory } from '../../../logging/LoggerFactory';
import { DeployPlugin } from 'warp-contracts-plugin-deploy'; import { DeployPlugin } from 'warp-contracts-plugin-deploy';
import { VM2Plugin } from 'warp-contracts-plugin-vm2'; import { VM2Plugin } from 'warp-contracts-plugin-vm2';
import { MemoryLevel } from 'memory-level';
interface ExampleContractState { interface ExampleContractState {
counter: number; 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', () => { describe('with read state at the end', () => {
beforeAll(async () => { beforeAll(async () => {
await deployContracts(); await deployContracts();

View File

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