diff --git a/CHANGELOG.md b/CHANGELOG.md index fe9fd131e..ebd3e402f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## Unreleased +- Added RecursiveProofModule [#516](https://github.com/proto-kit/framework/pull/516) ### Added - Added createdAt timestamp to block, batch and settlement models.[#502](https://github.com/proto-kit/framework/pull/502) diff --git a/packages/sequencer/src/sequencer/output/RecursiveProofModule.ts b/packages/sequencer/src/sequencer/output/RecursiveProofModule.ts new file mode 100644 index 000000000..dc0d3fadc --- /dev/null +++ b/packages/sequencer/src/sequencer/output/RecursiveProofModule.ts @@ -0,0 +1,121 @@ +import { + filterNonUndefined, + mapSequential, + noop, + range, +} from "@proto-kit/common"; +import { inject } from "tsyringe"; +import { BlockProof } from "@proto-kit/protocol"; +import { JsonProof } from "o1js"; + +import { sequencerModule, SequencerModule } from "../builder/SequencerModule"; +import { BatchMergingFlow } from "../../settlement/tasks/BatchMergingFlow"; +import { PropertyStorage } from "../../storage/repositories/PropertyStorage"; +import { BatchStorage } from "../../storage/repositories/BatchStorage"; +import { Batch } from "../../storage/model/Batch"; +import { BatchProducerModule } from "../../protocol/production/BatchProducerModule"; +import { BlockProofSerializer } from "../../protocol/production/tasks/serializers/BlockProofSerializer"; + +export interface RecursiveProofJson { + toBatchHeight: number; + proof: JsonProof; +} + +export interface RecursiveProof { + toBatchHeight: number; + proof: BlockProof; +} + +const BATCH_STORAGE_KEY = "batch-recursive"; + +@sequencerModule() +// implements ProofOutputModule +export class RecursiveProofModule extends SequencerModule { + public constructor( + @inject("PropertyStorage") + private readonly propertyStorage: PropertyStorage, + @inject("BatchStorage") + private readonly batchStorage: BatchStorage, + private readonly batchMergingFlow: BatchMergingFlow, + @inject("BatchProducerModule") + private readonly batchProducerModule: BatchProducerModule, + private readonly blockProofSerializer: BlockProofSerializer + ) { + super(); + } + + public async start() { + noop(); + } + + public async storeProof(proof: RecursiveProofJson) { + const str = JSON.stringify(proof); + + await this.propertyStorage.set(BATCH_STORAGE_KEY, str); + } + + public async readProof(): Promise { + const json = await this.propertyStorage.get(BATCH_STORAGE_KEY); + + if (json === undefined) { + return undefined; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const jsonObject: RecursiveProofJson = JSON.parse(json); + + const proof = await this.blockProofSerializer + .getBlockProofSerializer() + .fromJSONProof(jsonObject.proof); + + return { + toBatchHeight: jsonObject.toBatchHeight, + proof, + }; + } + + public async proveRecursively() { + const previous = await this.readProof(); + let previousBatch: Batch | undefined = undefined; + if (previous !== undefined) { + previousBatch = await this.batchStorage.getBatchAt( + previous.toBatchHeight + ); + } + + const currentHeight = await this.batchStorage.getCurrentBatchHeight(); + const previousBatchesResults = await Promise.all( + range((previous?.toBatchHeight ?? -1) + 1, currentHeight).map((height) => + this.batchStorage.getBatchAt(height) + ) + ); + + const previousBatches = previousBatchesResults.filter(filterNonUndefined); + if (previousBatches.length < previousBatchesResults.length) { + throw new Error("Some previous batch retrieval failed"); + } + + const batches = [previousBatch, ...previousBatches].filter( + filterNonUndefined + ); + // TODO This is very expensive (loads of DB-trips) + const settleableBatches = await mapSequential( + batches, + async (batch) => + await this.batchProducerModule.recoverSettleableBatch(batch) + ); + + const batch = await this.batchMergingFlow.mergeBatches(settleableBatches); + + await this.storeProof({ proof: batch.proof, toBatchHeight: batch.height }); + + return batch; + } + + // public async settleBatch(batches: SettleableBatch[]): Promise { + // + // return { + // + // } + // } +} diff --git a/packages/sequencer/src/settlement/tasks/BatchMergingFlow.ts b/packages/sequencer/src/settlement/tasks/BatchMergingFlow.ts index 7b9d43533..bdd9e9d71 100644 --- a/packages/sequencer/src/settlement/tasks/BatchMergingFlow.ts +++ b/packages/sequencer/src/settlement/tasks/BatchMergingFlow.ts @@ -51,7 +51,7 @@ export class BatchMergingFlow { return { proof: await serializer.toJSONProof(result), - height: batches[0].height, + height: batches.at(-1)!.height, blockHashes: batches.flatMap((batch) => batch.blockHashes), createdAt: Date.now(), fromNetworkState: batches[0].fromNetworkState, diff --git a/packages/sequencer/test/integration/RecursiveProofModules.test.ts b/packages/sequencer/test/integration/RecursiveProofModules.test.ts new file mode 100644 index 000000000..a9ac73539 --- /dev/null +++ b/packages/sequencer/test/integration/RecursiveProofModules.test.ts @@ -0,0 +1,153 @@ +import { log } from "@proto-kit/common"; +import { VanillaProtocolModules } from "@proto-kit/library"; +import { Runtime } from "@proto-kit/module"; +import { Protocol } from "@proto-kit/protocol"; +import { Bool, PrivateKey, UInt64 } from "o1js"; +import "reflect-metadata"; +import { container } from "tsyringe"; +import { afterEach, expect } from "@jest/globals"; + +import { + Sequencer, + VanillaTaskWorkerModules, + AppChain, + InMemoryDatabase, +} from "../../src"; +import { + DefaultTestingSequencerModules, + testingSequencerModules, +} from "../TestingSequencer"; +import { RecursiveProofModule } from "../../src/sequencer/output/RecursiveProofModule"; + +import { Balance } from "./mocks/Balance"; +import { BlockTestService } from "./services/BlockTestService"; + +describe("RecursiveProofModule", () => { + let sequencer: Sequencer< + DefaultTestingSequencerModules & { + Database: typeof InMemoryDatabase; + RecursiveProofModule: typeof RecursiveProofModule; + } + >; + + let appChain: AppChain; + + let test: BlockTestService; + + beforeEach(async () => { + const runtimeClass = Runtime.from({ + Balance, + }); + + const sequencerClass = Sequencer.from({ + ...testingSequencerModules({}), + Database: InMemoryDatabase, + RecursiveProofModule, + }); + + // TODO Analyze how we can get rid of the library import for mandatory modules + const protocolClass = Protocol.from( + VanillaProtocolModules.mandatoryModules({}) + ); + + const app = AppChain.from({ + Runtime: runtimeClass, + Sequencer: sequencerClass, + Protocol: protocolClass, + }); + + app.configure({ + Sequencer: { + Database: {}, + BlockTrigger: {}, + Mempool: {}, + BatchProducerModule: {}, + BlockProducerModule: {}, + WorkerModule: VanillaTaskWorkerModules.defaultConfig(), + BaseLayer: {}, + TaskQueue: {}, + FeeStrategy: {}, + SequencerStartupModule: {}, + RecursiveProofModule: {}, + }, + Runtime: { + Balance: {}, + }, + Protocol: { + ...Protocol.defaultConfig(), + }, + }); + + // Start AppChain + await app.start(false, container.createChildContainer()); + + appChain = app; + + ({ sequencer } = app); + + test = app.sequencer.dependencyContainer.resolve(BlockTestService); + }, 30000); + + afterEach(async () => { + await appChain.close(); + }); + + it("should produce a dummy block proof", async () => { + log.setLevel("DEBUG"); + expect.assertions(12); + + const outputModule = sequencer.resolve("RecursiveProofModule"); + + const privateKey = PrivateKey.random(); + const publicKey = privateKey.toPublicKey(); + + await test.addTransaction({ + method: ["Balance", "setBalanceIf"], + privateKey, + args: [publicKey, UInt64.from(100), Bool(true)], + }); + + // let [block, batch] = await blockTrigger.produceBlockAndBatch(); + const block = await test.produceBlock(); + + expect(block).toBeDefined(); + expect(block!.transactions).toHaveLength(1); + + const batch = await test.produceBatch(); + + expect(batch).toBeDefined(); + expect(batch!.blockHashes).toHaveLength(1); + + const recursiveBatch = await outputModule.proveRecursively(); + expect(recursiveBatch.proof.publicInput).toStrictEqual( + batch!.proof.publicInput + ); + + // Second tx + await test.addTransaction({ + method: ["Balance", "addBalanceToSelf"], + privateKey, + args: [UInt64.from(100), UInt64.from(1)], + }); + + log.info("Starting second block"); + + const [block2, batch2] = await test.produceBlockAndBatch(); + + expect(block2).toBeDefined(); + expect(block2!.transactions).toHaveLength(1); + expect(batch2!.blockHashes).toHaveLength(1); + + const recursiveBatch2 = await outputModule.proveRecursively(); + expect(recursiveBatch2.proof.publicInput).toStrictEqual( + batch!.proof.publicInput + ); + expect(recursiveBatch2.proof.publicOutput).toStrictEqual( + batch2!.proof.publicOutput + ); + + const storedProperty = await outputModule.readProof(); + expect(storedProperty).toBeDefined(); + expect(storedProperty!.toBatchHeight).toBe(1); + }, 60_000); +});