diff --git a/src/__tests__/integration/data/example-contract.js b/src/__tests__/integration/data/example-contract.js index e39a497..4fe05ee 100644 --- a/src/__tests__/integration/data/example-contract.js +++ b/src/__tests__/integration/data/example-contract.js @@ -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'); } + } diff --git a/src/__tests__/integration/data/writing-contract.js b/src/__tests__/integration/data/writing-contract.js index b201bf1..708de2d 100644 --- a/src/__tests__/integration/data/writing-contract.js +++ b/src/__tests__/integration/data/writing-contract.js @@ -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 }; } diff --git a/src/__tests__/integration/internal-writes/internal-write-callee.test.ts b/src/__tests__/integration/internal-writes/internal-write-callee.test.ts index aa8ccfd..f9bd437 100644 --- a/src/__tests__/integration/internal-writes/internal-write-callee.test.ts +++ b/src/__tests__/integration/internal-writes/internal-write-callee.test.ts @@ -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 = 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(); diff --git a/src/core/modules/impl/DefaultStateEvaluator.ts b/src/core/modules/impl/DefaultStateEvaluator.ts index d72c374..144b5b6 100644 --- a/src/core/modules/impl/DefaultStateEvaluator.ts +++ b/src/core/modules/impl/DefaultStateEvaluator.ts @@ -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 = null; + let writingContractState: SortKeyCacheResult> = 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);