From e2feb0837ff0e1022c02dd5698f1f0258914156a Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Thu, 16 May 2024 15:55:52 +0200 Subject: [PATCH] Fixed bugs with async state --- package.json | 2 +- packages/cli/test/chain.config.ts | 4 +- .../library/src/hooks/TransactionFeeHook.ts | 8 +- packages/library/src/runtime/Balances.ts | 42 +++-- packages/library/test/math/State.test.ts | 34 ++++ packages/module/src/method/runtimeMethod.ts | 2 +- .../module/src/state/InMemoryStateService.ts | 8 +- packages/module/test/modules/Balances.test.ts | 34 ++-- packages/module/test/modules/Balances.ts | 14 +- packages/module/test/modules/State.test.ts | 4 +- .../src/services/prisma/PrismaStateService.ts | 6 +- packages/persistance/test/connection.test.ts | 4 +- packages/protocol/src/state/State.ts | 1 + packages/protocol/src/state/StateService.ts | 2 +- packages/protocol/src/state/assert/assert.ts | 12 +- .../context/RuntimeMethodExecutionContext.ts | 5 +- packages/protocol/test/State.test.ts | 12 +- packages/sdk/src/appChain/AppChain.ts | 1 + .../sdk/src/query/StateServiceQueryModule.ts | 4 +- packages/sdk/test/TestingAppChain.test.ts | 8 +- packages/sdk/test/XYK/TestBalances.ts | 4 +- packages/sdk/test/XYK/XYK.test.ts | 4 +- packages/sdk/test/XYK/XYK.ts | 49 +++--- packages/sdk/test/blockProof/TestBalances.ts | 2 +- packages/sdk/test/fees.test.ts | 10 +- packages/sdk/test/networkstate/Balance.ts | 20 +-- packages/sdk/test/parameters.test.ts | 6 +- packages/sequencer/src/index.ts | 2 - .../production/TransactionTraceService.ts | 60 +++---- .../unproven/RuntimeMethodExecution.ts | 156 ------------------ .../unproven/TransactionExecutionService.ts | 130 ++++----------- .../src/state/async/AsyncStateService.ts | 4 +- .../src/state/state/CachedStateService.ts | 33 ++-- .../src/state/state/DummyStateService.ts | 8 +- .../src/state/state/RecordingStateService.ts | 53 ++++++ .../src/state/state/SyncCachedStateService.ts | 45 ----- .../test/integration/BlockProduction.test.ts | 32 ++-- .../integration/StorageIntegration.test.ts | 6 +- .../test/integration/mocks/Balance.ts | 38 ++--- .../mocks/ProtocolStateTestHook.ts | 11 +- .../test/integration/mocks/Withdrawals.ts | 10 +- .../test/protocol/KeyExtraction.test.ts | 111 ------------- .../state/state/CachedStateService.test.ts | 21 +-- packages/stack/src/scripts/graphql/server.ts | 12 +- 44 files changed, 384 insertions(+), 650 deletions(-) create mode 100644 packages/library/test/math/State.test.ts delete mode 100644 packages/sequencer/src/protocol/production/unproven/RuntimeMethodExecution.ts create mode 100644 packages/sequencer/src/state/state/RecordingStateService.ts delete mode 100644 packages/sequencer/src/state/state/SyncCachedStateService.ts delete mode 100644 packages/sequencer/test/protocol/KeyExtraction.test.ts diff --git a/package.json b/package.json index 0c7bf281..d7dbe70e 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "npx lerna run dev", "build": "npx lerna run build", - "lint": "npx lerna run lint", + "lint": "npx lerna run lint --parallel", "lint:staged": "eslint", "test": "npx lerna run test -- --passWithNoTests", "test:ci": "npx lerna run test -- --passWithNoTests --forceExit", diff --git a/packages/cli/test/chain.config.ts b/packages/cli/test/chain.config.ts index f9ef17fb..4dab7015 100644 --- a/packages/cli/test/chain.config.ts +++ b/packages/cli/test/chain.config.ts @@ -27,12 +27,12 @@ export class Balances extends RuntimeModule { @runtimeMethod() public async getBalance(address: PublicKey): Promise> { - return this.balances.get(address); + return await this.balances.get(address); } @runtimeMethod() public async setBalance(address: PublicKey, balance: UInt64) { - this.balances.set(address, balance); + await this.balances.set(address, balance); } } diff --git a/packages/library/src/hooks/TransactionFeeHook.ts b/packages/library/src/hooks/TransactionFeeHook.ts index 93e1dab3..758651cd 100644 --- a/packages/library/src/hooks/TransactionFeeHook.ts +++ b/packages/library/src/hooks/TransactionFeeHook.ts @@ -27,7 +27,7 @@ interface Balances { from: PublicKey, to: PublicKey, amount: Balance - ) => void; + ) => Promise; } export interface TransactionFeeHookConfig @@ -95,8 +95,8 @@ export class TransactionFeeHook extends ProvableTransactionHook Balance ); - public getBalance(tokenId: TokenId, address: PublicKey): Balance { + public async getBalance( + tokenId: TokenId, + address: PublicKey + ): Promise { const key = new BalancesKey({ tokenId, address }); - const balanceOption = this.balances.get(key); + const balanceOption = await this.balances.get(key); return Balance.Unsafe.fromField(balanceOption.value.value); } - public setBalance(tokenId: TokenId, address: PublicKey, amount: Balance) { + public async setBalance( + tokenId: TokenId, + address: PublicKey, + amount: Balance + ) { const key = new BalancesKey({ tokenId, address }); - this.balances.set(key, amount); + await this.balances.set(key, amount); } - public transfer( + public async transfer( tokenId: TokenId, from: PublicKey, to: PublicKey, amount: Balance ) { - const fromBalance = this.getBalance(tokenId, from); - const toBalance = this.getBalance(tokenId, to); + const fromBalance = await this.getBalance(tokenId, from); + const toBalance = await this.getBalance(tokenId, to); const fromBalanceIsSufficient = fromBalance.greaterThanOrEqual(amount); @@ -78,20 +85,21 @@ export class Balances const newFromBalance = fromBalance.sub(amount); const newToBalance = toBalance.add(amount); - this.setBalance(tokenId, from, newFromBalance); - this.setBalance(tokenId, to, newToBalance); + await this.setBalance(tokenId, from, newFromBalance); + await this.setBalance(tokenId, to, newToBalance); } - public mint(tokenId: TokenId, address: PublicKey, amount: Balance) { - const balance = this.getBalance(tokenId, address); + public async mint(tokenId: TokenId, address: PublicKey, amount: Balance) { + const balance = await this.getBalance(tokenId, address); const newBalance = balance.add(amount); - this.setBalance(tokenId, address, newBalance); + await this.setBalance(tokenId, address, newBalance); } - public burn(tokenId: TokenId, address: PublicKey, amount: Balance) { - const balance = this.getBalance(tokenId, address); + public async burn(tokenId: TokenId, address: PublicKey, amount: Balance) { + const balance = await this.getBalance(tokenId, address); + Provable.log("Balance", balance, amount); const newBalance = balance.sub(amount); - this.setBalance(tokenId, address, newBalance); + await this.setBalance(tokenId, address, newBalance); } @runtimeMethod() @@ -103,6 +111,6 @@ export class Balances ) { assert(this.transaction.sender.value.equals(from), errors.senderNotFrom()); - this.transfer(tokenId, from, to, amount); + await this.transfer(tokenId, from, to, amount); } } diff --git a/packages/library/test/math/State.test.ts b/packages/library/test/math/State.test.ts new file mode 100644 index 00000000..369fc78b --- /dev/null +++ b/packages/library/test/math/State.test.ts @@ -0,0 +1,34 @@ +import "reflect-metadata"; +import { + State, + StateServiceProvider, + RuntimeMethodExecutionContext, + RuntimeTransaction, + NetworkState, +} from "@proto-kit/protocol"; +import { UInt64, Field } from "o1js"; +import { InMemoryStateService } from "@proto-kit/module"; +import { container } from "tsyringe"; + +describe("interop uint <-> state", () => { + it("should deserialize as a correct class instance coming from state", async () => { + const state = new State(UInt64); + const service = new InMemoryStateService(); + const provider = new StateServiceProvider(); + state.path = Field(0); + state.stateServiceProvider = provider; + provider.setCurrentStateService(service); + + await service.set(state.path, [Field(10)]); + + const context = container.resolve(RuntimeMethodExecutionContext); + context.setup({ + transaction: RuntimeTransaction.dummyTransaction(), + networkState: NetworkState.empty(), + }); + + const uint = await state.get(); + const uint2 = uint.value.add(5); + expect(uint2.toString()).toStrictEqual("15"); + }); +}); diff --git a/packages/module/src/method/runtimeMethod.ts b/packages/module/src/method/runtimeMethod.ts index 42b36047..0dc63a86 100644 --- a/packages/module/src/method/runtimeMethod.ts +++ b/packages/module/src/method/runtimeMethod.ts @@ -270,7 +270,7 @@ function runtimeMethodInternal(options: { let result: unknown; try { - result = Reflect.apply(simulatedMethod, this, args); + result = await Reflect.apply(simulatedMethod, this, args); } finally { executionContext.afterMethod(); } diff --git a/packages/module/src/state/InMemoryStateService.ts b/packages/module/src/state/InMemoryStateService.ts index f4906004..ba23c697 100644 --- a/packages/module/src/state/InMemoryStateService.ts +++ b/packages/module/src/state/InMemoryStateService.ts @@ -1,21 +1,21 @@ import { Field } from "o1js"; -import { StateService } from "@proto-kit/protocol"; +import { SimpleAsyncStateService } from "@proto-kit/protocol"; /** * Naive implementation of an in-memory variant of the StateService interface */ -export class InMemoryStateService implements StateService { +export class InMemoryStateService implements SimpleAsyncStateService { /** * This mapping container null values if the specific entry has been deleted. * This is used by the CachedState service to keep track of deletions */ public values: Record = {}; - public get(key: Field): Field[] | undefined { + public async get(key: Field): Promise { return this.values[key.toString()] ?? undefined; } - public set(key: Field, value: Field[] | undefined) { + public async set(key: Field, value: Field[] | undefined) { if (value === undefined) { this.values[key.toString()] = null; } else { diff --git a/packages/module/test/modules/Balances.test.ts b/packages/module/test/modules/Balances.test.ts index d9b2281a..8d388691 100644 --- a/packages/module/test/modules/Balances.test.ts +++ b/packages/module/test/modules/Balances.test.ts @@ -5,7 +5,7 @@ import { type ProvableStateTransition, Path, MethodPublicOutput, - StateService, + SimpleAsyncStateService, RuntimeMethodExecutionContext, RuntimeTransaction, NetworkState, @@ -20,19 +20,19 @@ import { Admin } from "./Admin.js"; describe("balances", () => { let balances: Balances; - let state: StateService; + let state: SimpleAsyncStateService; let runtime: Runtime<{ Admin: typeof Admin; Balances: typeof Balances; }>; - function getStateValue(path: Field | undefined) { + async function getStateValue(path: Field | undefined) { if (!path) { throw new Error("Path not found"); } - const stateValue = state.get(path); + const stateValue = await state.get(path); if (!stateValue) { throw new Error("stateValue is undefined"); @@ -82,7 +82,7 @@ describe("balances", () => { await runtime.zkProgrammable.zkProgram.compile(); - balances.getTotalSupply(); + await balances.getTotalSupply(); const { result } = executionContext.current(); @@ -107,7 +107,7 @@ describe("balances", () => { describe("state transitions", () => { let stateTransitions: ProvableStateTransition[]; - beforeEach(() => { + beforeEach(async () => { const executionContext = container.resolve( RuntimeMethodExecutionContext ); @@ -115,7 +115,7 @@ describe("balances", () => { transaction: RuntimeTransaction.dummyTransaction(), networkState: NetworkState.empty(), }); - balances.getTotalSupply(); + await balances.getTotalSupply(); stateTransitions = executionContext .current() @@ -139,13 +139,13 @@ describe("balances", () => { ); }); - it("should produce a from-only state transition", () => { + it("should produce a from-only state transition", async () => { expect.assertions(3); const [stateTransition] = stateTransitions; const value = UInt64.fromFields( - getStateValue(balances.totalSupply.path) + await getStateValue(balances.totalSupply.path) ); const treeValue = Poseidon.hash(value.toFields()); @@ -166,7 +166,7 @@ describe("balances", () => { state.set(balances.totalSupply.path!, undefined); }); - beforeEach(() => { + beforeEach(async () => { const executionContext = container.resolve( RuntimeMethodExecutionContext ); @@ -175,7 +175,7 @@ describe("balances", () => { networkState: NetworkState.empty(), }); - balances.getTotalSupply(); + await balances.getTotalSupply(); stateTransitions = executionContext .current() @@ -221,7 +221,7 @@ describe("balances", () => { describe("state transitions", () => { let stateTransitions: ProvableStateTransition[]; - beforeEach(() => { + beforeEach(async () => { const executionContext = container.resolve( RuntimeMethodExecutionContext ); @@ -230,7 +230,7 @@ describe("balances", () => { networkState: NetworkState.empty(), }); - balances.setTotalSupply(); + await balances.setTotalSupply(); stateTransitions = executionContext .current() @@ -254,12 +254,12 @@ describe("balances", () => { ); }); - it("should produce a from-to state transition", () => { + it("should produce a from-to state transition", async () => { expect.assertions(4); const [stateTransition] = stateTransitions; const fromValue = UInt64.fromFields( - getStateValue(balances.totalSupply.path) + await getStateValue(balances.totalSupply.path) ); const fromTreeValue = Poseidon.hash(fromValue.toFields()); @@ -286,7 +286,7 @@ describe("balances", () => { let stateTransitions: ProvableStateTransition[]; const address = PrivateKey.random().toPublicKey(); - beforeEach(() => { + beforeEach(async () => { const executionContext = container.resolve( RuntimeMethodExecutionContext ); @@ -295,7 +295,7 @@ describe("balances", () => { networkState: NetworkState.empty(), }); - balances.getBalance(address); + await balances.getBalance(address); stateTransitions = executionContext .current() diff --git a/packages/module/test/modules/Balances.ts b/packages/module/test/modules/Balances.ts index 10327343..67916592 100644 --- a/packages/module/test/modules/Balances.ts +++ b/packages/module/test/modules/Balances.ts @@ -29,26 +29,26 @@ export class Balances extends RuntimeModule { @runtimeMethod() public async getTotalSupply() { - this.totalSupply.get(); + await this.totalSupply.get(); } @runtimeMethod() public async setTotalSupply() { - this.totalSupply.set(UInt64.from(20)); + await this.totalSupply.set(UInt64.from(20)); await this.admin.isAdmin(this.transaction.sender.value); } @runtimeMethod() public async getBalance(address: PublicKey) { - this.balances.get(address).orElse(UInt64.zero); + (await this.balances.get(address)).orElse(UInt64.zero); } @runtimeMethod() public async transientState() { - const totalSupply = this.totalSupply.get(); - this.totalSupply.set(totalSupply.orElse(UInt64.zero).add(100)); + const totalSupply = await this.totalSupply.get(); + await this.totalSupply.set(totalSupply.orElse(UInt64.zero).add(100)); - const totalSupply2 = this.totalSupply.get(); - this.totalSupply.set(totalSupply2.orElse(UInt64.zero).add(100)); + const totalSupply2 = await this.totalSupply.get(); + await this.totalSupply.set(totalSupply2.orElse(UInt64.zero).add(100)); } } diff --git a/packages/module/test/modules/State.test.ts b/packages/module/test/modules/State.test.ts index 64ae6d70..8f5f5553 100644 --- a/packages/module/test/modules/State.test.ts +++ b/packages/module/test/modules/State.test.ts @@ -51,7 +51,7 @@ describe("state", () => { }); describe("transient state", () => { - it("should track previously set state", () => { + it("should track previously set state", async () => { expect.assertions(2); const executionContext = container.resolve(RuntimeMethodExecutionContext); @@ -59,7 +59,7 @@ describe("state", () => { networkState: NetworkState.empty(), transaction: RuntimeTransaction.dummyTransaction(), }); - balances.transientState(); + await balances.transientState(); const stateTransitions = executionContext .current() diff --git a/packages/persistance/src/services/prisma/PrismaStateService.ts b/packages/persistance/src/services/prisma/PrismaStateService.ts index e8997090..32798700 100644 --- a/packages/persistance/src/services/prisma/PrismaStateService.ts +++ b/packages/persistance/src/services/prisma/PrismaStateService.ts @@ -53,7 +53,7 @@ export class PrismaStateService implements AsyncStateService { this.cache = []; } - public async getAsync(keys: Field[]): Promise { + public async getMany(keys: Field[]): Promise { const records = await this.connection.prismaClient.state.findMany({ where: { AND: [ @@ -78,8 +78,8 @@ export class PrismaStateService implements AsyncStateService { noop(); } - public async getSingleAsync(key: Field): Promise { - const state = await this.getAsync([key]); + public async get(key: Field): Promise { + const state = await this.getMany([key]); return state.at(-1)?.value; } diff --git a/packages/persistance/test/connection.test.ts b/packages/persistance/test/connection.test.ts index 07a64dad..ef83f454 100644 --- a/packages/persistance/test/connection.test.ts +++ b/packages/persistance/test/connection.test.ts @@ -71,7 +71,7 @@ describe.skip("prisma", () => { ]); await service.commit(); - const result = await service.getSingleAsync(Field(5)); + const result = await service.get(Field(5)); console.log(`Received ${result?.map((x) => x.toString())}`); expectDefined(result); @@ -87,7 +87,7 @@ describe.skip("prisma", () => { ]); await service.commit(); - const result2 = await service.getSingleAsync(Field(5)); + const result2 = await service.get(Field(5)); expect(result2).toBeUndefined(); await db.prismaClient.$disconnect(); diff --git a/packages/protocol/src/state/State.ts b/packages/protocol/src/state/State.ts index 0d8cca7d..36400d8c 100644 --- a/packages/protocol/src/state/State.ts +++ b/packages/protocol/src/state/State.ts @@ -76,6 +76,7 @@ export class State extends Mixin(WithPath, WithStateServiceProvider) { .resolve(RuntimeMethodExecutionContext) .current().result; + // TODO Use Stateservice for this // First try to find a match inside already created stateTransitions let previousMutatingTransitions: StateTransition[] = []; previousMutatingTransitions = stateTransitions.filter((transition) => diff --git a/packages/protocol/src/state/StateService.ts b/packages/protocol/src/state/StateService.ts index 203f3adf..70275d49 100644 --- a/packages/protocol/src/state/StateService.ts +++ b/packages/protocol/src/state/StateService.ts @@ -2,5 +2,5 @@ import { Field } from "o1js"; export interface SimpleAsyncStateService { get: (key: Field) => Promise; - set: (key: Field, value: Field[] | undefined) => void; + set: (key: Field, value: Field[] | undefined) => Promise; } diff --git a/packages/protocol/src/state/assert/assert.ts b/packages/protocol/src/state/assert/assert.ts index e2bff37b..0971bd0d 100644 --- a/packages/protocol/src/state/assert/assert.ts +++ b/packages/protocol/src/state/assert/assert.ts @@ -23,13 +23,11 @@ export function assert(condition: Bool, message?: string | (() => string)) { if (!executionContext.current().isSimulated) { log.debug("Assertion failed: ", message); } - let messageString: string | undefined = undefined; - if (message !== undefined && typeof message === "function") { - messageString = message(); - } else { - messageString = message; - } - executionContext.setStatusMessage(messageString); + const messageString = + message !== undefined && typeof message === "function" + ? message() + : message; + executionContext.setStatusMessage(messageString, new Error().stack); } }); diff --git a/packages/protocol/src/state/context/RuntimeMethodExecutionContext.ts b/packages/protocol/src/state/context/RuntimeMethodExecutionContext.ts index c2c2dba5..9138f9b3 100644 --- a/packages/protocol/src/state/context/RuntimeMethodExecutionContext.ts +++ b/packages/protocol/src/state/context/RuntimeMethodExecutionContext.ts @@ -22,6 +22,8 @@ export class RuntimeProvableMethodExecutionResult extends ProvableMethodExecutio public status: Bool = Bool(true); public statusMessage?: string; + + public stackTrace?: string; } export interface RuntimeMethodExecutionData { @@ -74,12 +76,13 @@ export class RuntimeMethodExecutionContext extends ProvableMethodExecutionContex /** * @param message - Status message to acompany the current status */ - public setStatusMessage(message?: string) { + public setStatusMessage(message?: string, stackTrace?: string) { this.assertSetupCalled(); if (this.isSimulated) { return; } this.result.statusMessage ??= message; + this.result.stackTrace ??= stackTrace; } /** diff --git a/packages/protocol/test/State.test.ts b/packages/protocol/test/State.test.ts index c7d59f61..cc51bc5d 100644 --- a/packages/protocol/test/State.test.ts +++ b/packages/protocol/test/State.test.ts @@ -8,7 +8,7 @@ import { RuntimeMethodExecutionContext, RuntimeTransaction, State, - StateService, + SimpleAsyncStateService, StateServiceProvider, } from "../src"; @@ -22,14 +22,14 @@ describe("state", () => { }); }); - it("should decode state correctly", () => { + it("should decode state correctly", async () => { expect.assertions(2); const state = State.from(UInt64); - const stateService: StateService = { - get: () => [Field(123)], + const stateService: SimpleAsyncStateService = { + get: async () => [Field(123)], - set: () => { + set: async () => { noop(); }, }; @@ -37,7 +37,7 @@ describe("state", () => { state.stateServiceProvider.setCurrentStateService(stateService); state.path = Field(1); - const retrieved = state.get(); + const retrieved = await state.get(); expect(retrieved.isSome).toStrictEqual(Bool(true)); expect(retrieved.value).toStrictEqual(UInt64.from(123)); diff --git a/packages/sdk/src/appChain/AppChain.ts b/packages/sdk/src/appChain/AppChain.ts index d7e0bed6..ccbbacf1 100644 --- a/packages/sdk/src/appChain/AppChain.ts +++ b/packages/sdk/src/appChain/AppChain.ts @@ -227,6 +227,7 @@ export class AppChain< transaction: RuntimeTransaction.dummyTransaction(), networkState: NetworkState.empty(), }); + executionContext.setSimulated(true); const stateServiceProvider = this.container.resolve( "StateServiceProvider" diff --git a/packages/sdk/src/query/StateServiceQueryModule.ts b/packages/sdk/src/query/StateServiceQueryModule.ts index 8226c384..ec78f44a 100644 --- a/packages/sdk/src/query/StateServiceQueryModule.ts +++ b/packages/sdk/src/query/StateServiceQueryModule.ts @@ -33,8 +33,8 @@ export class StateServiceQueryModule return this.sequencer.dependencyContainer.resolve("AsyncMerkleStore"); } - public async get(key: Field) { - return await this.asyncStateService.getSingleAsync(key); + public get(key: Field) { + return this.asyncStateService.get(key); } public async merkleWitness( diff --git a/packages/sdk/test/TestingAppChain.test.ts b/packages/sdk/test/TestingAppChain.test.ts index f2c67ce5..9caf0b19 100644 --- a/packages/sdk/test/TestingAppChain.test.ts +++ b/packages/sdk/test/TestingAppChain.test.ts @@ -45,7 +45,7 @@ class CustomBalances extends Balances { @runtimeMethod() public async addBalance(address: PublicKey, balance: UInt64) { - const totalSupply = this.totalSupply.get(); + const totalSupply = await this.totalSupply.get(); // TODO Fix UInt issues to remove new UInt() const newTotalSupply = UInt64.Unsafe.fromField(totalSupply.value.value).add( @@ -55,7 +55,7 @@ class CustomBalances extends Balances { this.config.totalSupply ); - this.totalSupply.set(newTotalSupply); + await this.totalSupply.set(newTotalSupply); assert( isSupplyNotOverflown, @@ -65,7 +65,7 @@ class CustomBalances extends Balances { const isSender = this.transaction.sender.value.equals(address); assert(isSender, "Address is not the sender"); - const currentBalance = this.balances.get( + const currentBalance = await this.balances.get( new BalancesKey({ tokenId: TokenId.from(0n), address }) ); @@ -74,7 +74,7 @@ class CustomBalances extends Balances { balance ); - this.balances.set( + await this.balances.set( new BalancesKey({ tokenId: TokenId.from(0n), address }), newBalance ); diff --git a/packages/sdk/test/XYK/TestBalances.ts b/packages/sdk/test/XYK/TestBalances.ts index 8919c5fd..3e3e36da 100644 --- a/packages/sdk/test/XYK/TestBalances.ts +++ b/packages/sdk/test/XYK/TestBalances.ts @@ -6,8 +6,8 @@ import { PublicKey } from "o1js"; export class TestBalances extends Balances { @runtimeMethod() public async mint(tokenId: TokenId, address: PublicKey, amount: Balance) { - const balance = this.getBalance(tokenId, address); + const balance = await this.getBalance(tokenId, address); const newBalance = balance.add(amount); - this.setBalance(tokenId, address, newBalance); + await this.setBalance(tokenId, address, newBalance); } } diff --git a/packages/sdk/test/XYK/XYK.test.ts b/packages/sdk/test/XYK/XYK.test.ts index 65a27ce9..142d20b9 100644 --- a/packages/sdk/test/XYK/XYK.test.ts +++ b/packages/sdk/test/XYK/XYK.test.ts @@ -92,8 +92,8 @@ describe("xyk", () => { const balanceIn = await getBalance(tokenInId, alice); const balanceOut = await getBalance(tokenOutId, alice); - expect(balanceIn?.toBigInt()).toBe(balanceToMint); - expect(balanceOut?.toBigInt()).toBe(balanceToMint); + expect(balanceIn?.toString()).toBe(balanceToMint.toString()); + expect(balanceOut?.toString()).toBe(balanceToMint.toString()); }, 30_000); it("should create a pool", async () => { diff --git a/packages/sdk/test/XYK/XYK.ts b/packages/sdk/test/XYK/XYK.ts index 8710deb8..1ee48d76 100644 --- a/packages/sdk/test/XYK/XYK.ts +++ b/packages/sdk/test/XYK/XYK.ts @@ -65,15 +65,15 @@ export class XYK extends RuntimeModule> { super(); } - public poolExists(tokenIdIn: TokenId, tokenIdOut: TokenId) { + public async poolExists(tokenIdIn: TokenId, tokenIdOut: TokenId) { const key = PoolKey.fromTokenIdPair(tokenIdIn, tokenIdOut); - const pool = this.pools.get(key); + const pool = await this.pools.get(key); return pool.isSome; } - public assertPoolExists(tokenIdIn: TokenId, tokenIdOut: TokenId) { - assert(this.poolExists(tokenIdIn, tokenIdOut), errors.poolExists()); + public async assertPoolExists(tokenIdIn: TokenId, tokenIdOut: TokenId) { + assert(await this.poolExists(tokenIdIn, tokenIdOut), errors.poolExists()); } @runtimeMethod() @@ -84,31 +84,34 @@ export class XYK extends RuntimeModule> { tokenOutAmount: Balance ) { assert(tokenIdIn.equals(tokenIdOut).not(), errors.tokensMatch()); - assert(this.poolExists(tokenIdIn, tokenIdOut).not(), errors.poolExists()); + assert( + (await this.poolExists(tokenIdIn, tokenIdOut)).not(), + errors.poolExists() + ); const key = PoolKey.fromTokenIdPair(tokenIdIn, tokenIdOut); - this.pools.set(key, XYK.defaultPoolValue); + await this.pools.set(key, XYK.defaultPoolValue); const creator = this.transaction.sender.value; const pool = PoolKey.fromTokenIdPair(tokenIdIn, tokenIdOut); - this.balances.transfer(tokenIdIn, creator, pool, tokenInAmount); - this.balances.transfer(tokenIdOut, creator, pool, tokenOutAmount); + await this.balances.transfer(tokenIdIn, creator, pool, tokenInAmount); + await this.balances.transfer(tokenIdOut, creator, pool, tokenOutAmount); // mint LP token const lpTokenId = LPTokenId.fromTokenIdPair(tokenIdIn, tokenIdOut); - this.balances.mint(lpTokenId, creator, tokenInAmount); + await this.balances.mint(lpTokenId, creator, tokenInAmount); } - public calculateTokenOutAmountOut( + public async calculateTokenOutAmountOut( tokenIdIn: TokenId, tokenIdOut: TokenId, tokenInAmountIn: Balance ) { const pool = PoolKey.fromTokenIdPair(tokenIdIn, tokenIdOut); - const tokenInReserve = this.balances.getBalance(tokenIdIn, pool); - const tokenOutReserve = this.balances.getBalance(tokenIdOut, pool); + const tokenInReserve = await this.balances.getBalance(tokenIdIn, pool); + const tokenOutReserve = await this.balances.getBalance(tokenIdOut, pool); return this.calculateTokenOutAmountOutFromReserves( tokenInReserve, @@ -128,15 +131,15 @@ export class XYK extends RuntimeModule> { return numerator.div(denominator); } - public calculateTokenInAmountIn( + public async calculateTokenInAmountIn( tokenIdIn: TokenId, tokenIdOut: TokenId, tokenOutAmountOut: Balance ) { const pool = PoolKey.fromTokenIdPair(tokenIdIn, tokenIdOut); - const tokenInReserve = this.balances.getBalance(tokenIdIn, pool); - const tokenOutReserve = this.balances.getBalance(tokenIdOut, pool); + const tokenInReserve = await this.balances.getBalance(tokenIdIn, pool); + const tokenOutReserve = await this.balances.getBalance(tokenIdOut, pool); return this.calculateTokenInAmountInFromReserves( tokenInReserve, tokenOutReserve, @@ -184,10 +187,10 @@ export class XYK extends RuntimeModule> { tokenInAmountIn: Balance, minTokenOutAmountOut: Balance ) { - this.assertPoolExists(tokenIdIn, tokenIdOut); + await this.assertPoolExists(tokenIdIn, tokenIdOut); const pool = PoolKey.fromTokenIdPair(tokenIdIn, tokenIdOut); - const tokenOutAmountOut = this.calculateTokenOutAmountOut( + const tokenOutAmountOut = await this.calculateTokenOutAmountOut( tokenIdIn, tokenIdOut, tokenInAmountIn @@ -203,14 +206,14 @@ export class XYK extends RuntimeModule> { assert(isTokenOutAmountTooLow, errors.tokenOutAmountTooLow()); - this.balances.transfer( + await this.balances.transfer( tokenIdIn, this.transaction.sender.value, pool, tokenInAmountIn ); - this.balances.transfer( + await this.balances.transfer( tokenIdOut, pool, this.transaction.sender.value, @@ -225,10 +228,10 @@ export class XYK extends RuntimeModule> { tokenOutAmountOut: Balance, maxTokenInAmountIn: Balance ) { - this.assertPoolExists(tokenIdIn, tokenIdOut); + await this.assertPoolExists(tokenIdIn, tokenIdOut); const pool = PoolKey.fromTokenIdPair(tokenIdIn, tokenIdOut); - const tokenInAmountIn = this.calculateTokenInAmountIn( + const tokenInAmountIn = await this.calculateTokenInAmountIn( tokenIdIn, tokenIdOut, tokenOutAmountOut @@ -244,14 +247,14 @@ export class XYK extends RuntimeModule> { assert(isTokenInInAmountTooHigh, errors.tokenInAmountTooHigh()); - this.balances.transfer( + await this.balances.transfer( tokenIdOut, pool, this.transaction.sender.value, tokenOutAmountOut ); - this.balances.transfer( + await this.balances.transfer( tokenIdIn, this.transaction.sender.value, pool, diff --git a/packages/sdk/test/blockProof/TestBalances.ts b/packages/sdk/test/blockProof/TestBalances.ts index 19470b88..e502abf6 100644 --- a/packages/sdk/test/blockProof/TestBalances.ts +++ b/packages/sdk/test/blockProof/TestBalances.ts @@ -17,6 +17,6 @@ export class TestBalances extends Balances { address: PublicKey, amount: Balance ) { - super.setBalance(tokenId, address, amount); + await super.setBalance(tokenId, address, amount); } } diff --git a/packages/sdk/test/fees.test.ts b/packages/sdk/test/fees.test.ts index 85eeb556..e2b6a2d5 100644 --- a/packages/sdk/test/fees.test.ts +++ b/packages/sdk/test/fees.test.ts @@ -20,8 +20,8 @@ class Faucet extends RuntimeModule { @runtimeMethod() public async drip() { - this.balances.mint( - new TokenId(0), + await this.balances.mint( + TokenId.from(0), this.transaction.sender.value, Balance.from(1000) ); @@ -36,7 +36,11 @@ class Pit extends RuntimeModule { @runtimeMethod() public async burn(amount: Balance) { - this.balances.burn(TokenId.from(0), this.transaction.sender.value, amount); + await this.balances.burn( + TokenId.from(0), + this.transaction.sender.value, + amount + ); } } diff --git a/packages/sdk/test/networkstate/Balance.ts b/packages/sdk/test/networkstate/Balance.ts index 0cc81da6..1e1977d4 100644 --- a/packages/sdk/test/networkstate/Balance.ts +++ b/packages/sdk/test/networkstate/Balance.ts @@ -6,7 +6,7 @@ import { UInt64, } from "@proto-kit/library"; import { runtimeMethod, runtimeModule, state } from "@proto-kit/module"; -import { log, Presets, range } from "@proto-kit/common"; +import { log, Presets, range, mapSequential } from "@proto-kit/common"; import { Bool, Field, PublicKey } from "o1js"; import { Admin } from "@proto-kit/module/test/modules/Admin"; import { State, assert } from "@proto-kit/protocol"; @@ -28,7 +28,7 @@ export class BalanceChild extends Balances { tokenId: TokenId.from(0), address, }); - const balance = this.balances.get(balancesKey); + const balance = await this.balances.get(balancesKey); log.provable.debug("Sender:", address); log.provable.debug("Balance:", balance.isSome, balance.value); @@ -41,22 +41,22 @@ export class BalanceChild extends Balances { ); const newBalance = balance.value.add(value); - this.balances.set(balancesKey, newBalance); + await this.balances.set(balancesKey, newBalance); } @runtimeMethod() public async lotOfSTs() { - range(0, 10).forEach((index) => { + await mapSequential(range(0, 10), async (index) => { const pk = PublicKey.from({ x: Field(index % 5), isOdd: Bool(false) }); const balancesKey = new BalancesKey({ address: pk, tokenId: TokenId.from(0), }); - const value = this.balances.get(balancesKey); - this.balances.set(balancesKey, value.orElse(UInt64.zero).add(100)); + const value = await this.balances.get(balancesKey); + await this.balances.set(balancesKey, value.orElse(UInt64.zero).add(100)); - const supply = this.totalSupply.get().orElse(UInt64.zero); - this.totalSupply.set(supply.add(UInt64.from(100))); + const supply = (await this.totalSupply.get()).orElse(UInt64.zero); + await this.totalSupply.set(supply.add(UInt64.from(100))); }); } @@ -67,7 +67,7 @@ export class BalanceChild extends Balances { } @runtimeMethod() - public async getUserBalance( + public getUserBalance( tokenId: TokenId, address: PublicKey ): Promise { @@ -80,6 +80,6 @@ export class BalanceChild extends Balances { address: PublicKey, amount: Balance ) { - super.setBalance(tokenId, address, amount); + await super.setBalance(tokenId, address, amount); } } diff --git a/packages/sdk/test/parameters.test.ts b/packages/sdk/test/parameters.test.ts index a154a19d..ab710b31 100644 --- a/packages/sdk/test/parameters.test.ts +++ b/packages/sdk/test/parameters.test.ts @@ -84,9 +84,9 @@ class TestRuntime extends RuntimeModule { TestStruct.toFields(struct) ); assert(valid, "Signature invalid"); - this.test1.set(field); - this.ballots.get(Field(1)); - this.ballots.set(Field(1), ballot); + await this.test1.set(field); + await this.ballots.get(Field(1)); + await this.ballots.set(Field(1), ballot); const [root, key] = witness.computeRootAndKey(Field(0)); const knownRoot = Provable.witness(Field, () => map.getRoot()); diff --git a/packages/sequencer/src/index.ts b/packages/sequencer/src/index.ts index dd3dc3c5..b2a9c92b 100644 --- a/packages/sequencer/src/index.ts +++ b/packages/sequencer/src/index.ts @@ -32,7 +32,6 @@ export * from "./protocol/production/trigger/TimedBlockTrigger"; export * from "./protocol/production/BlockProducerModule"; export * from "./protocol/production/BlockTaskFlowService"; export * from "./protocol/production/TransactionTraceService"; -export * from "./protocol/production/unproven/RuntimeMethodExecution"; export * from "./protocol/production/unproven/TransactionExecutionService"; export * from "./protocol/production/unproven/UnprovenProducerModule"; export * from "./protocol/production/flow/ReductionTaskFlow"; @@ -63,7 +62,6 @@ export * from "./state/async/AsyncStateService"; export * from "./state/merkle/CachedMerkleTreeStore"; export * from "./state/merkle/SyncCachedMerkleTreeStore"; export * from "./state/state/DummyStateService"; -export * from "./state/state/SyncCachedStateService"; export * from "./state/state/CachedStateService"; export * from "./state/MerkleStoreWitnessProvider"; export * from "./settlement/SettlementModule"; diff --git a/packages/sequencer/src/protocol/production/TransactionTraceService.ts b/packages/sequencer/src/protocol/production/TransactionTraceService.ts index 7f4b68eb..54b44571 100644 --- a/packages/sequencer/src/protocol/production/TransactionTraceService.ts +++ b/packages/sequencer/src/protocol/production/TransactionTraceService.ts @@ -40,32 +40,38 @@ export class TransactionTraceService { } private async collectStartingState( - stateService: CachedStateService, stateTransitions: UntypedStateTransition[] ): Promise { - const keys = this.allKeys(stateTransitions); - await stateService.preloadKeys(keys); - - return keys.reduce((state, key) => { - const stateValue = stateService.get(key); - if (stateValue !== undefined) { - state[key.toString()] = stateValue; - } - return state; - }, {}); + const stateEntries = stateTransitions + // Filter distinct + .filter( + (st, index, array) => + array.findIndex( + (st2) => st2.path.toBigInt() === st.path.toBigInt() + ) === index + ) + // Filter out STs that have isSome: false as precondition, because this means + // "state hasn't been set before" and has to correlate to a precondition on Field(0) + // and for that the state has to be undefined + .filter((st) => st.fromValue.isSome.toBoolean()) + .map((st) => [st.path.toString(), st.fromValue.value]); + + return Object.fromEntries(stateEntries); } - private applyTransitions( + private async applyTransitions( stateService: CachedStateService, stateTransitions: UntypedStateTransition[] - ): void { + ): Promise { // Use updated stateTransitions since only they will have the // right values - stateTransitions + const writes = stateTransitions .filter((st) => st.toValue.isSome.toBoolean()) - .forEach((st) => { - stateService.set(st.path, st.toValue.toFields()); + .map((st) => { + return { key: st.path, value: st.toValue.toFields() }; }); + stateService.writeStates(writes); + await stateService.commit(); } public async createBlockTrace( @@ -80,16 +86,13 @@ export class TransactionTraceService { ): Promise { const stateTransitions = block.metadata.blockStateTransitions; - const startingState = await this.collectStartingState( - stateServices.stateService, - stateTransitions - ); + const startingState = await this.collectStartingState(stateTransitions); let stParameters: StateTransitionProofParameters[]; let fromStateRoot: Field; if (stateTransitions.length > 0) { - this.applyTransitions(stateServices.stateService, stateTransitions); + await this.applyTransitions(stateServices.stateService, stateTransitions); ({ stParameters, fromStateRoot } = await this.createMerkleTrace( stateServices.merkleStore, @@ -179,20 +182,19 @@ export class TransactionTraceService { executionResult; // Collect starting state - const protocolStartingState = await this.collectStartingState( + const protocolStartingState = + await this.collectStartingState(protocolTransitions); + + await this.applyTransitions( stateServices.stateService, protocolTransitions ); - this.applyTransitions(stateServices.stateService, protocolTransitions); - - const runtimeStartingState = await this.collectStartingState( - stateServices.stateService, - stateTransitions - ); + const runtimeStartingState = + await this.collectStartingState(stateTransitions); if (status.toBoolean()) { - this.applyTransitions(stateServices.stateService, stateTransitions); + await this.applyTransitions(stateServices.stateService, stateTransitions); } // Step 3 diff --git a/packages/sequencer/src/protocol/production/unproven/RuntimeMethodExecution.ts b/packages/sequencer/src/protocol/production/unproven/RuntimeMethodExecution.ts deleted file mode 100644 index e89f5e2a..00000000 --- a/packages/sequencer/src/protocol/production/unproven/RuntimeMethodExecution.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { - RuntimeMethodExecutionContext, - RuntimeMethodExecutionData, - StateTransition, - ProtocolEnvironment, -} from "@proto-kit/protocol"; -import { RuntimeEnvironment } from "@proto-kit/module"; -import _ from "lodash"; -import { Field } from "o1js"; - -import { AsyncStateService } from "../../../state/async/AsyncStateService"; -import { CachedStateService } from "../../../state/state/CachedStateService"; -import { distinctByString } from "../../../helpers/utils"; -import { SyncCachedStateService } from "../../../state/state/SyncCachedStateService"; - -export class RuntimeMethodExecution { - public constructor( - private readonly runtime: RuntimeEnvironment, - private readonly protocol: ProtocolEnvironment, - private readonly executionContext: RuntimeMethodExecutionContext - ) {} - - private async executeMethodWithKeys( - method: () => Promise, - contextInputs: RuntimeMethodExecutionData, - parentStateService: CachedStateService - ): Promise[]> { - const { executionContext, runtime, protocol } = this; - - executionContext.setup(contextInputs); - executionContext.setSimulated(true); - - const stateService = new SyncCachedStateService(parentStateService); - runtime.stateServiceProvider.setCurrentStateService(stateService); - protocol.stateServiceProvider.setCurrentStateService(stateService); - - // Execute method - await method(); - - const { stateTransitions } = executionContext.current().result; - - // Clear executionContext - executionContext.afterMethod(); - executionContext.clear(); - - runtime.stateServiceProvider.popCurrentStateService(); - protocol.stateServiceProvider.popCurrentStateService(); - - return stateTransitions; - } - - /** - * Simulates a certain Context-aware method through multiple rounds. - * - * For a method that emits n Statetransitions, we execute it n times, - * where for every i-th iteration, we collect the i-th ST that has - * been emitted and preload the corresponding key. - */ - - public async simulateMultiRound( - method: () => Promise, - contextInputs: RuntimeMethodExecutionData, - parentStateService: AsyncStateService - ): Promise[]> { - let numberMethodSTs: number | undefined; - let collectedSTs = 0; - - const touchedKeys: string[] = []; - - let lastRuntimeResult: StateTransition[]; - - const preloadingStateService = new CachedStateService(parentStateService); - - /* eslint-disable no-await-in-loop */ - do { - const stateTransitions = await this.executeMethodWithKeys( - method, - contextInputs, - preloadingStateService - ); - - if (numberMethodSTs === undefined) { - numberMethodSTs = stateTransitions.length; - } - - if (collectedSTs === 0) { - // Do a full run with all keys and see if keys have changed - // (i.e. if no dynamic keys are used, just take that result) - // If that is the case, fast-forward to the first dynamic key - const keys = stateTransitions - .map((st) => st.path.toString()) - .filter(distinctByString); - const optimisticRunStateService = new CachedStateService( - parentStateService - ); - await optimisticRunStateService.preloadKeys( - keys.map((fieldString) => Field(fieldString)) - ); - const stateTransitionsFullRun = await this.executeMethodWithKeys( - method, - contextInputs, - optimisticRunStateService - ); - - const firstDiffIndex = _.zip( - stateTransitions, - stateTransitionsFullRun - ).findIndex( - ([st1, st2]) => st1?.path.toString() !== st2?.path.toString() - ); - - if (firstDiffIndex === -1) { - // Abort bcs no dynamic keys are used => use then 1:1 - return stateTransitionsFullRun; - } - // here push all known keys up to the first dynamic key - // touchedkeys is empty, so we don't care about that - const additionalKeys = stateTransitionsFullRun - .slice(0, firstDiffIndex) - .map((st) => st.path.toString()) - .filter(distinctByString); - - // Preload eligible keys - touchedKeys.push(...additionalKeys); - - await preloadingStateService.preloadKeys( - additionalKeys.map((key) => Field(key)) - ); - - collectedSTs = firstDiffIndex - 1; - lastRuntimeResult = stateTransitions; - // eslint-disable-next-line no-continue - continue; - } - - const latestST = stateTransitions.at(collectedSTs); - - if ( - latestST !== undefined && - !touchedKeys.includes(latestST.path.toString()) - ) { - touchedKeys.push(latestST.path.toString()); - - await preloadingStateService.preloadKey(latestST.path); - } - - collectedSTs += 1; - - lastRuntimeResult = stateTransitions; - } while (collectedSTs < numberMethodSTs); - - /* eslint-enable no-await-in-loop */ - - return lastRuntimeResult; - } -} diff --git a/packages/sequencer/src/protocol/production/unproven/TransactionExecutionService.ts b/packages/sequencer/src/protocol/production/unproven/TransactionExecutionService.ts index 09ad1674..eebf0420 100644 --- a/packages/sequencer/src/protocol/production/unproven/TransactionExecutionService.ts +++ b/packages/sequencer/src/protocol/production/unproven/TransactionExecutionService.ts @@ -38,7 +38,6 @@ import { import { PendingTransaction } from "../../../mempool/PendingTransaction"; import { CachedStateService } from "../../../state/state/CachedStateService"; import { distinctByString } from "../../../helpers/utils"; -import { AsyncStateService } from "../../../state/async/AsyncStateService"; import { CachedMerkleTreeStore } from "../../../state/merkle/CachedMerkleTreeStore"; import { AsyncMerkleTreeStore } from "../../../state/async/AsyncMerkleTreeStore"; import { @@ -50,8 +49,6 @@ import { import { UntypedStateTransition } from "../helpers/UntypedStateTransition"; import type { StateRecord } from "../BlockProducerModule"; -import { RuntimeMethodExecution } from "./RuntimeMethodExecution"; - const errors = { methodIdNotFound: (methodId: string) => new Error(`Can't find runtime method with id ${methodId}`), @@ -66,8 +63,6 @@ export class TransactionExecutionService { private readonly blockHooks: ProvableBlockHook[]; - private readonly runtimeMethodExecution: RuntimeMethodExecution; - public constructor( @inject("Runtime") private readonly runtime: Runtime, @inject("Protocol") @@ -84,12 +79,6 @@ export class TransactionExecutionService { ); this.blockHooks = protocol.dependencyContainer.resolveAll("ProvableBlockHook"); - - this.runtimeMethodExecution = new RuntimeMethodExecution( - this.runtime, - this.protocol, - container.resolve(RuntimeMethodExecutionContext) - ); } private allKeys(stateTransitions: StateTransition[]): Field[] { @@ -98,6 +87,22 @@ export class TransactionExecutionService { return stateTransitions.map((st) => st.path).filter(distinctByString); } + // TODO Use RecordingStateservice for this + private async applyTransitions( + stateService: CachedStateService, + stateTransitions: StateTransition[] + ): Promise { + // Use updated stateTransitions since only they will have the + // right values + const writes = stateTransitions + .filter((st) => st.toValue.isSome.toBoolean()) + .map((st) => { + return { key: st.path, value: st.toValue.toFields() }; + }); + stateService.writeStates(writes); + await stateService.commit(); + } + private collectStateDiff( stateTransitions: UntypedStateTransition[] ): StateRecord { @@ -163,11 +168,12 @@ export class TransactionExecutionService { ): Promise< Pick< RuntimeProvableMethodExecutionResult, - "stateTransitions" | "status" | "statusMessage" + "stateTransitions" | "status" | "statusMessage" | "stackTrace" > > { // Set up context const executionContext = container.resolve(RuntimeMethodExecutionContext); + executionContext.setup(contextInputs); executionContext.setSimulated(runSimulated); @@ -221,7 +227,7 @@ export class TransactionExecutionService { * attached that is needed for tracing */ public async createUnprovenBlock( - stateService: AsyncStateService, + stateService: CachedStateService, transactions: PendingTransaction[], lastBlockWithMetadata: UnprovenBlockWithMetadata, allowEmptyBlocks: boolean @@ -421,73 +427,13 @@ export class TransactionExecutionService { }; } - private async applyTransitions( - stateService: CachedStateService, - stateTransitions: StateTransition[] - ): Promise { - const writes = stateTransitions - .filter((st) => st.to.isSome.toBoolean()) - .map((st) => - // Use updated stateTransitions since only they will have the - // right values - ({ key: st.path, value: st.to.toFields() }) - ); - // Maybe replace with stateService.set() because its cached anyways? - stateService.writeStates(writes); - } - - // TODO Here exists a edge-case, where the protocol hooks set - // some state that is then consumed by the runtime and used as a key. - // In this case, runtime would generate a wrong key here. - private async extractAccessedKeys( - method: SomeRuntimeMethod, - args: unknown[], - runtimeContextInputs: RuntimeMethodExecutionData, - blockContextInputs: BlockProverExecutionData, - parentStateService: AsyncStateService - ): Promise<{ - runtimeKeys: Field[]; - protocolKeys: Field[]; - }> { - // TODO unsafe to re-use params here? - const stateTransitions = - await this.runtimeMethodExecution.simulateMultiRound( - async () => { - await method(...args); - }, - runtimeContextInputs, - parentStateService - ); - - const protocolTransitions = - await this.runtimeMethodExecution.simulateMultiRound( - async () => { - await mapSequential( - this.transactionHooks, - async (transactionHook) => { - await transactionHook.onTransaction(blockContextInputs); - } - ); - }, - runtimeContextInputs, - parentStateService - ); - - log.debug(`Got ${stateTransitions.length} StateTransitions`); - log.debug(`Got ${protocolTransitions.length} ProtocolStateTransitions`); - - return { - runtimeKeys: this.allKeys(stateTransitions), - protocolKeys: this.allKeys(protocolTransitions), - }; - } - private async createExecutionTrace( - asyncStateService: AsyncStateService, + asyncStateService: CachedStateService, tx: PendingTransaction, networkState: NetworkState ): Promise { - const cachedStateService = new CachedStateService(asyncStateService); + // TODO Use RecordingStateService -> async asProver needed + const recordingStateService = new CachedStateService(asyncStateService); const { method, args, module } = await this.decodeTransaction(tx); @@ -507,22 +453,8 @@ export class TransactionExecutionService { networkState: blockContextInputs.networkState, }; - const { runtimeKeys, protocolKeys } = await this.extractAccessedKeys( - method, - args, - runtimeContextInputs, - blockContextInputs, - asyncStateService - ); - - // Preload keys - await cachedStateService.preloadKeys( - runtimeKeys.concat(protocolKeys).filter(distinctByString) - ); - - // Execute second time with preloaded state. The following steps - // generate and apply the correct STs with the right values - this.stateServiceProvider.setCurrentStateService(cachedStateService); + // The following steps generate and apply the correct STs with the right values + this.stateServiceProvider.setCurrentStateService(recordingStateService); const protocolResult = await this.executeProtocolHooks( runtimeContextInputs, @@ -530,11 +462,14 @@ export class TransactionExecutionService { ); if (!protocolResult.status.toBoolean()) { - throw new Error( + const error = new Error( `Protocol hooks not executable: ${ protocolResult.statusMessage ?? "unknown" }` ); + log.debug("Protocol hook error stack trace:", protocolResult.stackTrace); + // Propagate stack trace from the assertion + throw error; } log.trace( @@ -548,7 +483,7 @@ export class TransactionExecutionService { // Apply protocol STs await this.applyTransitions( - cachedStateService, + recordingStateService, protocolResult.stateTransitions ); @@ -569,20 +504,21 @@ export class TransactionExecutionService { // Apply runtime STs (only if the tx succeeded) if (runtimeResult.status.toBoolean()) { + // Apply protocol STs await this.applyTransitions( - cachedStateService, + recordingStateService, runtimeResult.stateTransitions ); } + await recordingStateService.mergeIntoParent(); + // Reset global stateservice this.stateServiceProvider.popCurrentStateService(); // Reset proofs enabled appChain.setProofsEnabled(previousProofsEnabled); - await cachedStateService.mergeIntoParent(); - return { tx, status: runtimeResult.status, diff --git a/packages/sequencer/src/state/async/AsyncStateService.ts b/packages/sequencer/src/state/async/AsyncStateService.ts index d239a02e..b5420abc 100644 --- a/packages/sequencer/src/state/async/AsyncStateService.ts +++ b/packages/sequencer/src/state/async/AsyncStateService.ts @@ -17,7 +17,7 @@ export interface AsyncStateService { writeStates: (entries: StateEntry[]) => void; - getAsync: (keys: Field[]) => Promise; + getMany: (keys: Field[]) => Promise; - getSingleAsync: (key: Field) => Promise; + get: (key: Field) => Promise; } diff --git a/packages/sequencer/src/state/state/CachedStateService.ts b/packages/sequencer/src/state/state/CachedStateService.ts index 9bc7b533..16ce311c 100644 --- a/packages/sequencer/src/state/state/CachedStateService.ts +++ b/packages/sequencer/src/state/state/CachedStateService.ts @@ -1,6 +1,7 @@ import { Field } from "o1js"; -import { log, noop } from "@proto-kit/common"; +import { log, noop, mapSequential } from "@proto-kit/common"; import { InMemoryStateService } from "@proto-kit/module"; +import { SimpleAsyncStateService } from "@proto-kit/protocol"; import { AsyncStateService, StateEntry } from "../async/AsyncStateService"; @@ -10,16 +11,14 @@ const errors = { export class CachedStateService extends InMemoryStateService - implements AsyncStateService + implements AsyncStateService, SimpleAsyncStateService { + private writes: StateEntry[] = []; + public constructor(private readonly parent: AsyncStateService | undefined) { super(); } - public get(key: Field): Field[] | undefined { - return super.get(key); - } - /** * Works like get(), but if a value is in this store, * but is known to be empty, this will return null @@ -37,13 +36,13 @@ export class CachedStateService } public writeStates(entries: StateEntry[]): void { - entries.forEach(({ key, value }) => { - this.set(key, value); - }); + this.writes.push(...entries); } public async commit(): Promise { - noop(); + await mapSequential(this.writes, async ({ key, value }) => { + await this.set(key, value); + }); } public async openTransaction(): Promise { @@ -59,7 +58,7 @@ export class CachedStateService // Only preload it if it hasn't been preloaded previously // TODO Not safe for deletes const keysToBeLoaded = keys.filter((key) => this.get(key) === undefined); - const loaded = await this.parent.getAsync(keysToBeLoaded); + const loaded = await this.parent.getMany(keysToBeLoaded); log.trace( `Preloaded: ${loaded.map( @@ -67,13 +66,13 @@ export class CachedStateService )}` ); - loaded.forEach(({ key, value }) => { - this.set(key, value); + await mapSequential(loaded, async ({ key, value }) => { + await this.set(key, value); }); } } - public async getAsync(keys: Field[]): Promise { + public async getMany(keys: Field[]): Promise { const remoteKeys: Field[] = []; const local: StateEntry[] = []; @@ -87,13 +86,13 @@ export class CachedStateService } }); - const remote = await this.parent?.getAsync(remoteKeys); + const remote = await this.parent?.getMany(remoteKeys); return local.concat(remote ?? []); } - public async getSingleAsync(key: Field): Promise { - const entries = await this.getAsync([key]); + public async get(key: Field): Promise { + const entries = await this.getMany([key]); return entries.at(0)?.value; } diff --git a/packages/sequencer/src/state/state/DummyStateService.ts b/packages/sequencer/src/state/state/DummyStateService.ts index 274f87b3..acb29b97 100644 --- a/packages/sequencer/src/state/state/DummyStateService.ts +++ b/packages/sequencer/src/state/state/DummyStateService.ts @@ -1,13 +1,13 @@ import { Field } from "o1js"; -import { StateService } from "@proto-kit/protocol"; +import { SimpleAsyncStateService } from "@proto-kit/protocol"; import { noop } from "@proto-kit/common"; -export class DummyStateService implements StateService { - public get(key: Field): Field[] | undefined { +export class DummyStateService implements SimpleAsyncStateService { + public async get(key: Field): Promise { return undefined; } - public set(key: Field, value: Field[] | undefined): void { + public async set(key: Field, value: Field[] | undefined): Promise { noop(); } } diff --git a/packages/sequencer/src/state/state/RecordingStateService.ts b/packages/sequencer/src/state/state/RecordingStateService.ts new file mode 100644 index 00000000..8d7809c7 --- /dev/null +++ b/packages/sequencer/src/state/state/RecordingStateService.ts @@ -0,0 +1,53 @@ +import { InMemoryStateService } from "@proto-kit/module"; +import { SimpleAsyncStateService } from "@proto-kit/protocol"; +import { Field } from "o1js"; +import { mapSequential } from "@proto-kit/common"; + +/** + * A simple stateservice that records the retrieved and written state. + * The values fields from the extended InMemoryStateService serves as the memory. + * After merging, the memory will not be deleted. + * Therefore, this service should not be used twice, except consectively + */ +export class RecordingStateService + extends InMemoryStateService + implements SimpleAsyncStateService +{ + public constructor(private readonly parent: SimpleAsyncStateService) { + super(); + } + + /** + * Works like get(), but if a value is in this store, + * but is known to be empty, this will return null + */ + protected getNullAware(key: Field): Field[] | null | undefined { + return this.values[key.toString()]; + } + + public async get(key: Field): Promise { + const remembered = this.getNullAware(key); + if (remembered !== undefined) { + return remembered ?? undefined; + } + const fetched = await this.parent.get(key); + if (fetched !== undefined) { + await super.set(key, fetched); + } + return fetched; + } + + public async set(key: Field, value: Field[] | undefined): Promise { + await super.set(key, value); + } + + public getRecorded() { + return this.values; + } + + public async mergeIntoParent() { + await mapSequential(Object.entries(this.values), async ([key, values]) => { + await this.parent.set(Field(key), values ?? undefined); + }); + } +} diff --git a/packages/sequencer/src/state/state/SyncCachedStateService.ts b/packages/sequencer/src/state/state/SyncCachedStateService.ts deleted file mode 100644 index 2b30a7cf..00000000 --- a/packages/sequencer/src/state/state/SyncCachedStateService.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { InMemoryStateService } from "@proto-kit/module"; -import { Field } from "o1js"; -import { StateService } from "@proto-kit/protocol"; - -const errors = { - parentIsUndefined: () => new Error("Parent StateService is undefined"), -}; - -export class SyncCachedStateService - extends InMemoryStateService - implements StateService -{ - public constructor(private readonly parent: StateService | undefined) { - super(); - } - - public get(key: Field): Field[] | undefined { - return super.get(key) ?? this.parent?.get(key); - } - - private assertParentNotNull( - parent: StateService | undefined - ): asserts parent is StateService { - if (parent === undefined) { - throw errors.parentIsUndefined(); - } - } - - /** - * Merges all caches set() operation into the parent and - * resets this instance to the parent's state (by clearing the cache and - * defaulting to the parent) - */ - public async mergeIntoParent() { - const { parent, values } = this; - this.assertParentNotNull(parent); - - // Set all cached values on parent - Object.entries(values).forEach((value) => { - parent.set(Field(value[0]), value[1] ?? undefined); - }); - // Clear cache - this.values = {}; - } -} diff --git a/packages/sequencer/test/integration/BlockProduction.test.ts b/packages/sequencer/test/integration/BlockProduction.test.ts index a8f7f986..cc14fd7e 100644 --- a/packages/sequencer/test/integration/BlockProduction.test.ts +++ b/packages/sequencer/test/integration/BlockProduction.test.ts @@ -116,6 +116,8 @@ describe("block production", () => { it("should produce a dummy block proof", async () => { expect.assertions(25); + log.setLevel("TRACE"); + const privateKey = PrivateKey.random(); const publicKey = privateKey.toPublicKey(); @@ -178,15 +180,14 @@ describe("block production", () => { balanceModule.balances.keyType, publicKey ); - const newState = await stateService.getSingleAsync(balancesPath); - const newUnprovenState = - await unprovenStateService.getSingleAsync(balancesPath); + const newState = await stateService.get(balancesPath); + const newUnprovenState = await unprovenStateService.get(balancesPath); expect(newState).toBeDefined(); expect(newUnprovenState).toBeDefined(); - expect(UInt64.fromFields(newState!)).toStrictEqual(UInt64.from(100)); - expect(UInt64.fromFields(newUnprovenState!)).toStrictEqual( - UInt64.from(100) + expect(UInt64.fromFields(newState!).toString()).toStrictEqual("100"); + expect(UInt64.fromFields(newUnprovenState!).toString()).toStrictEqual( + "100" ); // Check that nonce has been set @@ -196,7 +197,7 @@ describe("block production", () => { accountModule.accountState.keyType, publicKey ); - const newAccountState = await stateService.getSingleAsync(accountStatePath); + const newAccountState = await stateService.get(accountStatePath); expect(newAccountState).toBeDefined(); expect(AccountState.fromFields(newAccountState!).nonce.toBigInt()).toBe(1n); @@ -227,7 +228,7 @@ describe("block production", () => { expect(batch!.bundles).toHaveLength(1); expect(batch!.proof.proof).toBe("mock-proof"); - const state2 = await stateService.getSingleAsync(balancesPath); + const state2 = await stateService.get(balancesPath); expect(state2).toBeDefined(); expect(UInt64.fromFields(state2!)).toStrictEqual(UInt64.from(200)); @@ -274,9 +275,8 @@ describe("block production", () => { balanceModule.balances.keyType, PublicKey.empty() ); - const unprovenState = - await unprovenStateService.getSingleAsync(balancesPath); - const newState = await stateService.getSingleAsync(balancesPath); + const unprovenState = await unprovenStateService.get(balancesPath); + const newState = await stateService.get(balancesPath); // Assert that state is not set expect(unprovenState).toBeUndefined(); @@ -341,7 +341,7 @@ describe("block production", () => { balanceModule.balances.keyType, publicKey ); - const newState = await stateService.getSingleAsync(balancesPath); + const newState = await stateService.get(balancesPath); expect(newState).toBeDefined(); expect(UInt64.fromFields(newState!)).toStrictEqual( @@ -393,7 +393,7 @@ describe("block production", () => { balanceModule.balances.keyType, pk1.toPublicKey() ); - const newState1 = await stateService.getSingleAsync(balancesPath1); + const newState1 = await stateService.get(balancesPath1); expect(newState1).toBeUndefined(); @@ -402,7 +402,7 @@ describe("block production", () => { balanceModule.balances.keyType, pk2.toPublicKey() ); - const newState2 = await stateService.getSingleAsync(balancesPath2); + const newState2 = await stateService.get(balancesPath2); expect(newState2).toBeDefined(); expect(UInt64.fromFields(newState2!)).toStrictEqual(UInt64.from(100)); @@ -505,7 +505,7 @@ describe("block production", () => { "AsyncStateService" ); const supplyPath = Path.fromProperty("Balance", "totalSupply"); - const newState = await stateService.getSingleAsync(supplyPath); + const newState = await stateService.get(supplyPath); expect(newState).toBeDefined(); expect(UInt64.fromFields(newState!)).toStrictEqual( @@ -521,7 +521,7 @@ describe("block production", () => { pk2 ); - const newBalance = await stateService.getSingleAsync(balancesPath); + const newBalance = await stateService.get(balancesPath); expect(newBalance).toBeDefined(); expect(UInt64.fromFields(newBalance!)).toStrictEqual(UInt64.from(200)); diff --git a/packages/sequencer/test/integration/StorageIntegration.test.ts b/packages/sequencer/test/integration/StorageIntegration.test.ts index 6385c74b..a19bcbe9 100644 --- a/packages/sequencer/test/integration/StorageIntegration.test.ts +++ b/packages/sequencer/test/integration/StorageIntegration.test.ts @@ -174,16 +174,14 @@ describe.each([["InMemory", InMemoryDatabase]])( ) ); - const state = await unprovenState.getAsync( + const state = await unprovenState.getMany( Object.keys(stateDiff).map(Field) ); expect(checkStateDiffEquality(stateDiff, state)).toBe(true); expect(state.length).toBeGreaterThanOrEqual(1); - await expect( - provenState.getSingleAsync(state[0].key) - ).resolves.toBeUndefined(); + await expect(provenState.get(state[0].key)).resolves.toBeUndefined(); }); it("test proven block prod", async () => { diff --git a/packages/sequencer/test/integration/mocks/Balance.ts b/packages/sequencer/test/integration/mocks/Balance.ts index 718d0507..1fe0c945 100644 --- a/packages/sequencer/test/integration/mocks/Balance.ts +++ b/packages/sequencer/test/integration/mocks/Balance.ts @@ -5,7 +5,7 @@ import { RuntimeModule, state, } from "@proto-kit/module"; -import { log, Presets, range } from "@proto-kit/common"; +import { log, Presets, range, mapSequential } from "@proto-kit/common"; import { Bool, Field, PublicKey, UInt64 } from "o1js"; import { Admin } from "@proto-kit/module/test/modules/Admin"; import { Option, State, StateMap, assert, Deposit } from "@proto-kit/protocol"; @@ -27,13 +27,13 @@ export class Balance extends RuntimeModule { @runtimeMessage() public async deposit(deposit: Deposit) { - const balance = this.balances.get(deposit.address); - this.balances.set(deposit.address, balance.value.add(deposit.amount)); + const balance = await this.balances.get(deposit.address); + await this.balances.set(deposit.address, balance.value.add(deposit.amount)); } @runtimeMethod() public async getTotalSupply() { - this.totalSupply.get(); + await this.totalSupply.get(); } // @runtimeMethod() @@ -41,13 +41,13 @@ export class Balance extends RuntimeModule { @runtimeMethod() public async setTotalSupply() { - this.totalSupply.set(UInt64.from(20)); - this.admin.isAdmin(this.transaction.sender.value); + await this.totalSupply.set(UInt64.from(20)); + await this.admin.isAdmin(this.transaction.sender.value); } @runtimeMethod() public async getBalance(address: PublicKey): Promise> { - return this.balances.get(address); + return await this.balances.get(address); } @runtimeMethod() @@ -57,26 +57,26 @@ export class Balance extends RuntimeModule { condition: Bool ) { assert(condition, "Condition not met"); - this.balances.set(address, value); + await this.balances.set(address, value); } @runtimeMethod() public async addBalance(address: PublicKey, value: UInt64) { - const totalSupply = this.totalSupply.get(); - this.totalSupply.set(totalSupply.orElse(UInt64.zero).add(value)); + const totalSupply = await this.totalSupply.get(); + await this.totalSupply.set(totalSupply.orElse(UInt64.zero).add(value)); - const balance = this.balances.get(address); + const balance = await this.balances.get(address); log.provable.debug("Balance:", balance.isSome, balance.value); const newBalance = balance.orElse(UInt64.zero).add(value); - this.balances.set(address, newBalance); + await this.balances.set(address, newBalance); } @runtimeMethod() public async addBalanceToSelf(value: UInt64, blockHeight: UInt64) { const address = this.transaction.sender.value; - const balance = this.balances.get(address); + const balance = await this.balances.get(address); log.provable.debug("Sender:", address); log.provable.debug("Balance:", balance.isSome, balance.value); @@ -89,20 +89,20 @@ export class Balance extends RuntimeModule { ); const newBalance = balance.value.add(value); - this.balances.set(address, newBalance); + await this.balances.set(address, newBalance); } @runtimeMethod() public async lotOfSTs(randomArg: Field) { - range(0, 10).forEach((index) => { + await mapSequential(range(0, 10), async (index) => { const pk = PublicKey.from({ x: randomArg.add(Field(index % 5)), isOdd: Bool(false), }); - const value = this.balances.get(pk); - this.balances.set(pk, value.orElse(UInt64.zero).add(100)); - const supply = this.totalSupply.get().orElse(UInt64.zero); - this.totalSupply.set(supply.add(UInt64.from(100))); + const value = await this.balances.get(pk); + await this.balances.set(pk, value.orElse(UInt64.zero).add(100)); + const supply = (await this.totalSupply.get()).orElse(UInt64.zero); + await this.totalSupply.set(supply.add(UInt64.from(100))); }); } } diff --git a/packages/sequencer/test/integration/mocks/ProtocolStateTestHook.ts b/packages/sequencer/test/integration/mocks/ProtocolStateTestHook.ts index c8fbef06..33ce0163 100644 --- a/packages/sequencer/test/integration/mocks/ProtocolStateTestHook.ts +++ b/packages/sequencer/test/integration/mocks/ProtocolStateTestHook.ts @@ -12,9 +12,14 @@ import { Field } from "o1js"; export class ProtocolStateTestHook extends ProvableTransactionHook { @protocolState() methodIdInvocations = StateMap.from(Field, Field); - public onTransaction(executionData: BlockProverExecutionData): void { + public async onTransaction( + executionData: BlockProverExecutionData + ): Promise { const { methodId } = executionData.transaction; - const invocations = this.methodIdInvocations.get(methodId); - this.methodIdInvocations.set(methodId, invocations.orElse(Field(0)).add(1)); + const invocations = await this.methodIdInvocations.get(methodId); + await this.methodIdInvocations.set( + methodId, + invocations.orElse(Field(0)).add(1) + ); } } diff --git a/packages/sequencer/test/integration/mocks/Withdrawals.ts b/packages/sequencer/test/integration/mocks/Withdrawals.ts index 68efc088..735ed3f6 100644 --- a/packages/sequencer/test/integration/mocks/Withdrawals.ts +++ b/packages/sequencer/test/integration/mocks/Withdrawals.ts @@ -20,12 +20,12 @@ export class Withdrawals extends RuntimeModule { super(); } - protected queueWithdrawal(withdrawal: Withdrawal) { - const counter = this.withdrawalCounter.get().orElse(Field(0)); + protected async queueWithdrawal(withdrawal: Withdrawal) { + const counter = (await this.withdrawalCounter.get()).orElse(Field(0)); - this.withdrawalCounter.set(counter.add(1)); + await this.withdrawalCounter.set(counter.add(1)); - this.withdrawals.set(counter, withdrawal); + await this.withdrawals.set(counter, withdrawal); } @runtimeMethod() @@ -43,7 +43,7 @@ export class Withdrawals extends RuntimeModule { // eslint-disable-next-line max-len // this.balances.setBalanceIf(address, UInt64.from(balance.value.value).sub(amount), Bool(true)); - this.queueWithdrawal( + await this.queueWithdrawal( new Withdrawal({ address, amount, diff --git a/packages/sequencer/test/protocol/KeyExtraction.test.ts b/packages/sequencer/test/protocol/KeyExtraction.test.ts deleted file mode 100644 index bc8f40e1..00000000 --- a/packages/sequencer/test/protocol/KeyExtraction.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import "reflect-metadata"; -import { beforeAll, describe } from "@jest/globals"; -import { - runtimeMethod, - runtimeModule, - RuntimeModule, - state, -} from "@proto-kit/module"; -import { - NetworkState, - RuntimeMethodExecutionContext, - RuntimeTransaction, - State, - StateMap, -} from "@proto-kit/protocol"; -import { TestingAppChain } from "@proto-kit/sdk"; -import { Field, PublicKey, UInt64 } from "o1js"; - -import { RuntimeMethodExecution, CachedStateService } from "../../src"; - -@runtimeModule() -export class TestModule extends RuntimeModule { - @state() state1 = State.from(Field); - - @state() state2 = State.from(Field); - - @state() map = StateMap.from(Field, Field); - - @runtimeMethod() - public async performAction(inputKey: Field) { - this.map.get(inputKey); - this.map.set(inputKey, Field(1)); - - const state1 = this.state1.get(); - const state2 = this.state2.get(); - - const compKey = state1.value.add(state2.value); - const value = this.map.get(compKey); - this.map.set(compKey, value.value.add(Field(10))); - } -} - -describe("test the correct key extraction for runtime methods", () => { - const stateService = new CachedStateService(undefined); - - let context: RuntimeMethodExecutionContext; - let execution: RuntimeMethodExecution; - let module: TestModule; - - beforeAll(async () => { - const appchain = TestingAppChain.fromRuntime({ - TestModule, - }); - - appchain.configurePartial({ - Runtime: { - TestModule: {}, - Balances: {}, - }, - }); - - await appchain.start(); - - module = appchain.runtime.resolve("TestModule"); - stateService.set(module.state1.path!, [Field(5)]); - stateService.set(module.state2.path!, [Field(10)]); - - context = appchain.runtime.dependencyContainer.resolve( - RuntimeMethodExecutionContext - ); - execution = new RuntimeMethodExecution( - appchain.runtime, - appchain.protocol, - context - ); - }); - - it("test if simulation is done correctly", async () => { - expect.assertions(1); - - const contextInputs = { - networkState: new NetworkState({ - block: { height: UInt64.one }, - previous: { rootHash: Field(1) }, - }), - - transaction: RuntimeTransaction.fromTransaction({ - sender: PublicKey.empty(), - nonce: UInt64.zero, - methodId: Field(0), - argsHash: Field(0), - }), - }; - - console.time("Simulating..."); - - const sts = await execution.simulateMultiRound( - async () => { - await module.performAction(Field(5)); - }, - contextInputs, - new CachedStateService(stateService) - ); - - console.timeEnd("Simulating..."); - - const path = module.map.getPath(Field(15)); - - expect(sts[4].path.toString()).toBe(path.toString()); - }); -}); diff --git a/packages/sequencer/test/state/state/CachedStateService.test.ts b/packages/sequencer/test/state/state/CachedStateService.test.ts index 4f5bc51e..e4fda9fb 100644 --- a/packages/sequencer/test/state/state/CachedStateService.test.ts +++ b/packages/sequencer/test/state/state/CachedStateService.test.ts @@ -9,7 +9,7 @@ describe("cachedStateService", () => { let mask1: CachedStateService; let mask2: CachedStateService; - beforeEach(() => { + beforeEach(async () => { baseService = new CachedStateService(undefined); baseService.writeStates([ @@ -18,6 +18,7 @@ describe("cachedStateService", () => { value: [Field(1), Field(2)], }, ]); + await baseService.commit(); mask1 = new CachedStateService(baseService); mask2 = new CachedStateService(mask1); @@ -26,7 +27,7 @@ describe("cachedStateService", () => { it("should preload through multiple layers of services", async () => { await mask2.preloadKey(Field(5)); - const record = mask2.get(Field(5)); + const record = await mask2.get(Field(5)); expectDefined(record); expect(record).toHaveLength(2); @@ -35,14 +36,14 @@ describe("cachedStateService", () => { }); it("should set through multiple layers of services with merging", async () => { - mask2.set(Field(6), [Field(3)]); + await mask2.set(Field(6), [Field(3)]); await mask2.mergeIntoParent(); - expect(baseService.getSingleAsync(Field(6))).resolves.toBeUndefined(); + await expect(baseService.get(Field(6))).resolves.toBeUndefined(); await mask1.mergeIntoParent(); - const record = await baseService.getSingleAsync(Field(6)); + const record = await baseService.get(Field(6)); expectDefined(record); expect(record).toHaveLength(1); @@ -52,21 +53,21 @@ describe("cachedStateService", () => { it("should delete correctly through multiple layers of services", async () => { await mask2.preloadKey(Field(5)); - mask2.set(Field(5), undefined); + await mask2.set(Field(5), undefined); await mask1.preloadKey(Field(5)); - expect(mask1.get(Field(5))).toHaveLength(2); + await expect(mask1.get(Field(5))).resolves.toHaveLength(2); await mask2.mergeIntoParent(); await mask1.mergeIntoParent(); - const value = await baseService.getSingleAsync(Field(5)); + const value = await baseService.get(Field(5)); expect(value).toBeUndefined(); }); it("should delete correctly when deleting in the middle", async () => { - mask1.set(Field(5), undefined); + await mask1.set(Field(5), undefined); - await expect(mask2.getSingleAsync(Field(5))).resolves.toBeUndefined(); + await expect(mask2.get(Field(5))).resolves.toBeUndefined(); }); }); diff --git a/packages/stack/src/scripts/graphql/server.ts b/packages/stack/src/scripts/graphql/server.ts index 22e9951f..825748c4 100644 --- a/packages/stack/src/scripts/graphql/server.ts +++ b/packages/stack/src/scripts/graphql/server.ts @@ -62,7 +62,7 @@ export class TestBalances extends Balances { tokenId: TokenId, address: PublicKey ): Promise { - return super.getBalance(tokenId, address); + return await super.getBalance(tokenId, address); } @runtimeMethod() @@ -71,11 +71,13 @@ export class TestBalances extends Balances { address: PublicKey, balance: UInt64 ) { - const totalSupply = this.totalSupply.get(); - this.totalSupply.set(totalSupply.orElse(UInt64.zero).add(balance)); + const totalSupply = await this.totalSupply.get(); + await this.totalSupply.set(totalSupply.orElse(UInt64.zero).add(balance)); - const previous = this.balances.get(new BalancesKey({ tokenId, address })); - this.balances.set( + const previous = await this.balances.get( + new BalancesKey({ tokenId, address }) + ); + await this.balances.set( new BalancesKey({ tokenId, address }), previous.orElse(UInt64.zero).add(balance) );