From cf27e78efe4cb485797f9bf9d200d35e1f5d1db0 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Sat, 12 Apr 2025 20:09:58 +0200 Subject: [PATCH] Added tps test (courtesy @maht0rz) --- .../test-integration/benchmarks/tps.test.ts | 288 ++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 packages/sequencer/test-integration/benchmarks/tps.test.ts diff --git a/packages/sequencer/test-integration/benchmarks/tps.test.ts b/packages/sequencer/test-integration/benchmarks/tps.test.ts new file mode 100644 index 000000000..32c27c45f --- /dev/null +++ b/packages/sequencer/test-integration/benchmarks/tps.test.ts @@ -0,0 +1,288 @@ +import "reflect-metadata"; +import { + AppChain, + BlockStorageNetworkStateModule, + InMemorySigner, + InMemoryTransactionSender, + StateServiceQueryModule, +} from "@proto-kit/sdk"; +import { Runtime, runtimeMethod, runtimeModule } from "@proto-kit/module"; +import { Protocol, state, State } from "@proto-kit/protocol"; +import { + Balance, + VanillaProtocolModules, + VanillaRuntimeModules, + Balances as BaseBalances, + TokenId, +} from "@proto-kit/library"; +import { PrismaRedisDatabase } from "@proto-kit/persistance"; +import { NoConfig, log } from "@proto-kit/common"; +import { PrivateKey, PublicKey } from "o1js"; +import { container } from "tsyringe"; +import { afterEach } from "@jest/globals"; + +import { + BlockProducerModule, + DatabasePruneModule, + ManualBlockTrigger, + Sequencer, +} from "../../src"; +// we import PrivateMempool from dist to satisfy constraints of InMemoryTransactionSender +import { PrivateMempool } from "../../dist"; + +@runtimeModule() +export class Balances extends BaseBalances { + @state() public circulatingSupply = State.from(Balance); + + @runtimeMethod() + public async addBalance( + tokenId: TokenId, + address: PublicKey, + amount: Balance + ): Promise { + await this.mint(tokenId, address, amount); + } +} + +export async function duration(cb: () => Promise) { + const startTime = performance.now(); + const result = await cb(); + + return { + duration: performance.now() - startTime, + result, + }; +} + +export async function createAppChain() { + const appChain = AppChain.from({ + Runtime: Runtime.from({ + modules: VanillaRuntimeModules.with({ + Balances, + }), + }), + Protocol: Protocol.from({ + modules: VanillaProtocolModules.with({}), + }), + Sequencer: Sequencer.from({ + modules: { + DatabasePruneModule, + Database: PrismaRedisDatabase, + Mempool: PrivateMempool, + BlockProducerModule: BlockProducerModule, + BlockTrigger: ManualBlockTrigger, + }, + }), + modules: { + Signer: InMemorySigner, + TransactionSender: InMemoryTransactionSender, + QueryTransportModule: StateServiceQueryModule, + NetworkStateTransportModule: BlockStorageNetworkStateModule, + }, + }); + + appChain.configure({ + Runtime: { + Balances: {}, + }, + Protocol: { + ...VanillaProtocolModules.defaultConfig(), + }, + Sequencer: { + DatabasePruneModule: { + pruneOnStartup: true, + }, + Database: { + redis: { + host: "localhost", + port: 6379, + password: "password", + }, + prisma: { + connection: + "postgresql://admin:password@localhost:5432/protokit?schema=public", + }, + }, + BlockProducerModule: { + maximumBlockSize: 100, + }, + BlockTrigger: {}, + Mempool: {}, + }, + Signer: { + signer: PrivateKey.random(), + }, + QueryTransportModule: {}, + TransactionSender: {}, + NetworkStateTransportModule: {}, + }); + + await appChain.start(false, container.createChildContainer()); + + return appChain; +} + +const timeout = 600000; + +describe.skip("tps", () => { + let appChain: Awaited>; + let privateKeys: PrivateKey[] = []; + let balances: Balances; + + async function mint(signer: PrivateKey, amount: number, nonce: number = 0) { + appChain.resolve("Signer").config.signer = signer; + + const address = signer.toPublicKey(); + const tx = await appChain.transaction( + address, + async () => { + await balances.addBalance( + TokenId.from(0), + address, + Balance.from(amount) + ); + }, + { + nonce, + } + ); + + await tx.sign(); + await tx.send(); + } + + async function fundKeys(totalKeys: number) { + const batchSize = 20; + + for (let i = 0; i < totalKeys / batchSize; i++) { + for (let j = 0; j < batchSize; j++) { + const privateKey = PrivateKey.random(); + privateKeys.push(privateKey); + await mint(privateKey, 100_000); + } + await appChain.sequencer.resolve("BlockTrigger").produceBlock(); + } + } + + beforeEach(async () => { + try { + log.setLevel("DEBUG"); + appChain = await createAppChain(); + + balances = appChain.runtime.resolve("Balances"); + + await fundKeys(200); + // log.enableTiming(); + } catch (e) { + console.error(e); + throw e; + } + }, timeout); + + afterEach(async () => { + privateKeys = []; + await appChain.close(); + }); + + it("should produce an empty block", async () => { + try { + const produceBlockDuration = await duration(async () => { + return await appChain.sequencer.resolve("BlockTrigger").produceBlock(); + }); + + expect(produceBlockDuration.duration).toBeLessThan(200); + } catch (e) { + console.error(e); + throw e; + } + }); + + it( + "should produce a block with unique txs", + async () => { + console.log("should produce a block with unique txs"); + const transactionCount = 100; + for (let i = 0; i < transactionCount; i++) { + const fromPrivateKey = privateKeys[0]; + const toPrivateKey = privateKeys[1]; + privateKeys.splice(0, 2); + + appChain.resolve("Signer").config.signer = fromPrivateKey; + + const from = fromPrivateKey.toPublicKey(); + const to = toPrivateKey.toPublicKey(); + + const tx = await appChain.transaction( + from, + // eslint-disable-next-line @typescript-eslint/no-loop-func + async () => { + await balances.transferSigned( + TokenId.from(0), + from, + to, + Balance.from(1) + ); + } + ); + + await tx.sign(); + await tx.send(); + } + + const produceBlockDuration = await duration(async () => { + return await appChain.sequencer.resolve("BlockTrigger").produceBlock(); + }); + + console.log("txs", produceBlockDuration.result?.transactions.length); + + const tps = transactionCount / (produceBlockDuration.duration / 1000); + + console.log("duration multiple txs", produceBlockDuration.duration); + console.log("tps with state unique txs", tps); + + expect(tps).toBeGreaterThan(2.5); + }, + timeout + ); + + it("should produce a block with state cachable txs", async () => { + const transactionCount = 100; + + const fromPrivateKey = privateKeys[0]; + const from = fromPrivateKey.toPublicKey(); + const toPrivateKey = privateKeys[1]; + const to = toPrivateKey.toPublicKey(); + + appChain.resolve("Signer").config.signer = fromPrivateKey; + + for (let i = 0; i < transactionCount; i++) { + const tx = await appChain.transaction( + from, + // eslint-disable-next-line @typescript-eslint/no-loop-func + async () => { + await balances.transferSigned( + TokenId.from(0), + from, + to, + Balance.from(1) + ); + }, + { nonce: i } + ); + + await tx.sign(); + await tx.send(); + } + + const produceBlockDuration = await duration(async () => { + return await appChain.sequencer.resolve("BlockTrigger").produceBlock(); + }); + + const tps = transactionCount / (produceBlockDuration.duration / 1000); + + console.log("duration multiple txs", produceBlockDuration.duration); + console.log("tps with state cachable txs", tps); + + expect(tps).toBeGreaterThan(10); + }, 600000); +});