From c8bb9d4cbe40c9a52b81c6402d1bbbf4adfd26a8 Mon Sep 17 00:00:00 2001 From: kyscott18 <43524469+kyscott18@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:27:12 +0000 Subject: [PATCH] fix(contracts): free memory after each mutation in execute execute copies the arguments of each mutation into memory, and Solidity never frees memory. Memory grew with each mutation in a batch. With 10,000 concurrent mutations, the harness benchmark settled a batch that went above the 8 MiB memory limit per transaction on Monad, and the transaction halted with OutOfGas(MemoryLimit). execute now saves the free memory pointer before each mutation and sets it back after the mutation. The harness benchmark now sends 500 concurrent mutations, not 10,000. The runtime settles all pending mutations in one transaction, and 10,000 Credit mutations need about 220M gas. Monad accepts a maximum of 30M gas per transaction. The benchmark also waits until all mutations are included before it closes, and it prints the mean settlement gas for each mutation. Regenerate the Token and OrderBook declarations for the new AST ids. --- .../contracts/src/OrderBook.sol.d.ts | 84 +++++++++---------- apps/token/contracts/src/Token.sol.d.ts | 10 +-- packages/typewriter/src/Typewriter.sol | 16 ++++ packages/typewriter/test/harness.benchmark.ts | 44 +++++++++- 4 files changed, 105 insertions(+), 49 deletions(-) diff --git a/apps/order-book/contracts/src/OrderBook.sol.d.ts b/apps/order-book/contracts/src/OrderBook.sol.d.ts index 9dbe47a6..add2f86f 100644 --- a/apps/order-book/contracts/src/OrderBook.sol.d.ts +++ b/apps/order-book/contracts/src/OrderBook.sol.d.ts @@ -28,12 +28,12 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_uint256"; }, { - readonly astId: 43903; + readonly astId: 43935; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "state"; readonly offset: 0; readonly slot: "3"; - readonly type: "t_struct(State)43420_storage"; + readonly type: "t_struct(State)43452_storage"; }, ]; readonly types: { @@ -48,11 +48,11 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly numberOfBytes: "32"; readonly base: "t_struct(Credential)15_storage"; }; - readonly "t_array(t_struct(Order)43440_storage)dyn_storage": { + readonly "t_array(t_struct(Order)43472_storage)dyn_storage": { readonly encoding: "dynamic_array"; readonly label: "struct Order[]"; readonly numberOfBytes: "32"; - readonly base: "t_struct(Order)43440_storage"; + readonly base: "t_struct(Order)43472_storage"; }; readonly "t_array(t_struct(QueuedMutation)657_storage)dyn_storage": { readonly encoding: "dynamic_array"; @@ -89,12 +89,12 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly numberOfBytes: "32"; readonly value: "t_struct(Account)26_storage"; }; - readonly "t_mapping(t_bytes32,t_struct(Account)43429_storage)": { + readonly "t_mapping(t_bytes32,t_struct(Account)43461_storage)": { readonly encoding: "mapping"; readonly key: "t_bytes32"; readonly label: "mapping(bytes32 => struct Account)"; readonly numberOfBytes: "32"; - readonly value: "t_struct(Account)43429_storage"; + readonly value: "t_struct(Account)43461_storage"; }; readonly "t_mapping(t_uint192,t_uint64)": { readonly encoding: "mapping"; @@ -103,19 +103,19 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly numberOfBytes: "32"; readonly value: "t_uint64"; }; - readonly "t_mapping(t_uint64,t_struct(Instrument)43463_storage)": { + readonly "t_mapping(t_uint64,t_struct(Instrument)43495_storage)": { readonly encoding: "mapping"; readonly key: "t_uint64"; readonly label: "mapping(uint64 => struct Instrument)"; readonly numberOfBytes: "32"; - readonly value: "t_struct(Instrument)43463_storage"; + readonly value: "t_struct(Instrument)43495_storage"; }; - readonly "t_mapping(t_uint64,t_struct(Tick)43474_storage)": { + readonly "t_mapping(t_uint64,t_struct(Tick)43506_storage)": { readonly encoding: "mapping"; readonly key: "t_uint64"; readonly label: "mapping(uint64 => struct Tick)"; readonly numberOfBytes: "32"; - readonly value: "t_struct(Tick)43474_storage"; + readonly value: "t_struct(Tick)43506_storage"; }; readonly "t_struct(Account)26_storage": { readonly encoding: "inplace"; @@ -148,13 +148,13 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ }, ]; }; - readonly "t_struct(Account)43429_storage": { + readonly "t_struct(Account)43461_storage": { readonly encoding: "inplace"; readonly label: "struct Account"; readonly numberOfBytes: "64"; readonly members: readonly [ { - readonly astId: 43424; + readonly astId: 43456; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "balances"; readonly offset: 0; @@ -162,12 +162,12 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_mapping(t_address,t_uint256)"; }, { - readonly astId: 43428; + readonly astId: 43460; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "orders"; readonly offset: 0; readonly slot: "1"; - readonly type: "t_array(t_struct(Order)43440_storage)dyn_storage"; + readonly type: "t_array(t_struct(Order)43472_storage)dyn_storage"; }, ]; }; @@ -210,13 +210,13 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ }, ]; }; - readonly "t_struct(Instrument)43463_storage": { + readonly "t_struct(Instrument)43495_storage": { readonly encoding: "inplace"; readonly label: "struct Instrument"; readonly numberOfBytes: "160"; readonly members: readonly [ { - readonly astId: 43442; + readonly astId: 43474; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "base"; readonly offset: 0; @@ -224,7 +224,7 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_address"; }, { - readonly astId: 43444; + readonly astId: 43476; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "quote"; readonly offset: 0; @@ -232,7 +232,7 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_address"; }, { - readonly astId: 43446; + readonly astId: 43478; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "baseLotExp"; readonly offset: 20; @@ -240,7 +240,7 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_uint8"; }, { - readonly astId: 43448; + readonly astId: 43480; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "quoteLotExp"; readonly offset: 21; @@ -248,7 +248,7 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_uint8"; }, { - readonly astId: 43450; + readonly astId: 43482; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "bestBid"; readonly offset: 22; @@ -256,7 +256,7 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_uint64"; }, { - readonly astId: 43452; + readonly astId: 43484; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "bestAsk"; readonly offset: 0; @@ -264,30 +264,30 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_uint64"; }, { - readonly astId: 43457; + readonly astId: 43489; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "bids"; readonly offset: 0; readonly slot: "3"; - readonly type: "t_mapping(t_uint64,t_struct(Tick)43474_storage)"; + readonly type: "t_mapping(t_uint64,t_struct(Tick)43506_storage)"; }, { - readonly astId: 43462; + readonly astId: 43494; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "asks"; readonly offset: 0; readonly slot: "4"; - readonly type: "t_mapping(t_uint64,t_struct(Tick)43474_storage)"; + readonly type: "t_mapping(t_uint64,t_struct(Tick)43506_storage)"; }, ]; }; - readonly "t_struct(Order)43440_storage": { + readonly "t_struct(Order)43472_storage": { readonly encoding: "inplace"; readonly label: "struct Order"; readonly numberOfBytes: "32"; readonly members: readonly [ { - readonly astId: 43431; + readonly astId: 43463; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "quantity"; readonly offset: 0; @@ -295,7 +295,7 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_uint64"; }, { - readonly astId: 43433; + readonly astId: 43465; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "instrumentId"; readonly offset: 8; @@ -303,7 +303,7 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_uint64"; }, { - readonly astId: 43435; + readonly astId: 43467; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "price"; readonly offset: 16; @@ -311,7 +311,7 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_uint64"; }, { - readonly astId: 43437; + readonly astId: 43469; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "tickVolume"; readonly offset: 24; @@ -319,7 +319,7 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_uint32"; }, { - readonly astId: 43439; + readonly astId: 43471; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "side"; readonly offset: 28; @@ -367,36 +367,36 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ }, ]; }; - readonly "t_struct(State)43420_storage": { + readonly "t_struct(State)43452_storage": { readonly encoding: "inplace"; readonly label: "struct State"; readonly numberOfBytes: "64"; readonly members: readonly [ { - readonly astId: 43414; + readonly astId: 43446; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "accounts"; readonly offset: 0; readonly slot: "0"; - readonly type: "t_mapping(t_bytes32,t_struct(Account)43429_storage)"; + readonly type: "t_mapping(t_bytes32,t_struct(Account)43461_storage)"; }, { - readonly astId: 43419; + readonly astId: 43451; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "instruments"; readonly offset: 0; readonly slot: "1"; - readonly type: "t_mapping(t_uint64,t_struct(Instrument)43463_storage)"; + readonly type: "t_mapping(t_uint64,t_struct(Instrument)43495_storage)"; }, ]; }; - readonly "t_struct(Tick)43474_storage": { + readonly "t_struct(Tick)43506_storage": { readonly encoding: "inplace"; readonly label: "struct Tick"; readonly numberOfBytes: "64"; readonly members: readonly [ { - readonly astId: 43465; + readonly astId: 43497; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "quantity"; readonly offset: 0; @@ -404,7 +404,7 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_uint64"; }, { - readonly astId: 43467; + readonly astId: 43499; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "remainingQuantity"; readonly offset: 8; @@ -412,7 +412,7 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_uint64"; }, { - readonly astId: 43469; + readonly astId: 43501; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "volume"; readonly offset: 16; @@ -420,7 +420,7 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_uint32"; }, { - readonly astId: 43471; + readonly astId: 43503; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "prev"; readonly offset: 20; @@ -428,7 +428,7 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_uint64"; }, { - readonly astId: 43473; + readonly astId: 43505; readonly contract: "src/OrderBook.sol:OrderBook"; readonly label: "next"; readonly offset: 0; diff --git a/apps/token/contracts/src/Token.sol.d.ts b/apps/token/contracts/src/Token.sol.d.ts index 129c0bd1..4003f733 100644 --- a/apps/token/contracts/src/Token.sol.d.ts +++ b/apps/token/contracts/src/Token.sol.d.ts @@ -28,12 +28,12 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_uint256"; }, { - readonly astId: 42145; + readonly astId: 42177; readonly contract: "src/Token.sol:Token"; readonly label: "state"; readonly offset: 0; readonly slot: "3"; - readonly type: "t_struct(State)42134_storage"; + readonly type: "t_struct(State)42166_storage"; }, ]; readonly types: { @@ -194,13 +194,13 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ }, ]; }; - readonly "t_struct(State)42134_storage": { + readonly "t_struct(State)42166_storage": { readonly encoding: "inplace"; readonly label: "struct State"; readonly numberOfBytes: "64"; readonly members: readonly [ { - readonly astId: 42129; + readonly astId: 42161; readonly contract: "src/Token.sol:Token"; readonly label: "totalSupply"; readonly offset: 0; @@ -208,7 +208,7 @@ declare const entrypoint: import("typewriter").TypewriterSolidityEntrypoint<{ readonly type: "t_uint256"; }, { - readonly astId: 42133; + readonly astId: 42165; readonly contract: "src/Token.sol:Token"; readonly label: "balances"; readonly offset: 0; diff --git a/packages/typewriter/src/Typewriter.sol b/packages/typewriter/src/Typewriter.sol index 64aa77fd..d53bc02f 100644 --- a/packages/typewriter/src/Typewriter.sol +++ b/packages/typewriter/src/Typewriter.sol @@ -194,7 +194,9 @@ abstract contract Typewriter { revert LengthMismatch(); } for (uint256 i; i < batch.mutations.length; i++) { + uint256 freeMemoryPointer = _getFreeMemoryPointer(); _execute(batch.mutations[i], batch.mutationData[i], batch.authorizationData[i]); + _setFreeMemoryPointer(freeMemoryPointer); executionIndex++; } } @@ -205,13 +207,27 @@ abstract contract Typewriter { if (queued.enqueuedBlock == 0) revert ForceInclusionAlreadyExecuted(index); + uint256 freeMemoryPointer = _getFreeMemoryPointer(); _execute(queued.mutation, queued.mutationData, queued.authorizationData); + _setFreeMemoryPointer(freeMemoryPointer); executionIndex++; delete queue[index]; } } + function _getFreeMemoryPointer() private pure returns (uint256 pointer) { + assembly ("memory-safe") { + pointer := mload(0x40) + } + } + + function _setFreeMemoryPointer(uint256 pointer) private pure { + assembly ("memory-safe") { + mstore(0x40, pointer) + } + } + function enqueue(uint8 mutation, bytes calldata mutationData, bytes calldata authorizationData) external returns (uint256) diff --git a/packages/typewriter/test/harness.benchmark.ts b/packages/typewriter/test/harness.benchmark.ts index f607f7c6..f72f9362 100644 --- a/packages/typewriter/test/harness.benchmark.ts +++ b/packages/typewriter/test/harness.benchmark.ts @@ -1,10 +1,12 @@ import { test } from "bun:test"; +import type { Hex } from "viem"; import { anvil } from "viem/chains"; import type { TypewriterConfig } from "../src"; import Harness from "./contracts/src/Harness.sol"; import { SCHEDULER_ACCOUNT, TEST_DB_URL, + TEST_PUBLIC_CLIENT, TEST_RPC_URL, USER_ACCOUNT, USER_PRIVATE_KEY, @@ -23,7 +25,8 @@ Object.assign(process.env, { }); const LATENCY_MUTATION_COUNT = 1_000; -const THROUGHPUT_MUTATION_COUNT = 10_000; +// Keep the throughput phase below Monad's 30M gas limit per transaction. +const THROUGHPUT_MUTATION_COUNT = 500; test("harness benchmark", async () => { const { createTypewriter } = await import("../src"); @@ -65,6 +68,25 @@ test("harness benchmark", async () => { const throughputMutations = mutations.slice(LATENCY_MUTATION_COUNT); const typewriter = await createTypewriter(Harness, config); + + // `execute` resolves when the runtime accepts a mutation. Wait for every + // mutation to be included before closing, so no settlement transaction is + // in flight when the test database is dropped. + const allIncluded = Promise.withResolvers(); + let pendingCount = 1 + mutations.length; + const unsubscribe = typewriter.on("mutation", (event) => { + if (event.status === "included" && --pendingCount === 0) { + allIncluded.resolve(); + } + }); + + const settlementHashes: Hex[] = []; + const unsubscribeBlock = typewriter.on("block", (event) => { + if (event.status === "included") { + settlementHashes.push(event.transactionHash); + } + }); + await typewriter.execute(createAccount); const latenciesMs: number[] = []; @@ -80,6 +102,22 @@ test("harness benchmark", async () => { ); const throughputDurationMs = performance.now() - start; + await allIncluded.promise; + unsubscribe(); + unsubscribeBlock(); + await typewriter.close(); + + const receipts = await Promise.all( + settlementHashes.map((hash) => + TEST_PUBLIC_CLIENT.getTransactionReceipt({ hash }), + ), + ); + const totalGas = receipts.reduce( + (total, receipt) => total + receipt.gasUsed, + 0n, + ); + const meanGas = Number(totalGas) / (1 + mutations.length); + latenciesMs.sort((a, b) => a - b); const averageMs = latenciesMs.reduce((total, latency) => total + latency, 0) / @@ -94,5 +132,7 @@ Harness benchmark single user, ${LATENCY_MUTATION_COUNT.toLocaleString()} sequential mutations: average ${averageMs.toFixed(2)}ms, p50 ${p50Ms.toFixed(2)}ms, p99 ${p99Ms.toFixed(2)}ms max throughput, ${THROUGHPUT_MUTATION_COUNT.toLocaleString()} concurrent mutations: - ${Math.round(mutationsPerSecond).toLocaleString()} mutations/s`); + ${Math.round(mutationsPerSecond).toLocaleString()} mutations/s +settlement gas: + ${Math.round(meanGas).toLocaleString()} per mutation`); }, 300_000);