From 5dce3e77170595d654e28a9d41f9ffc374232ba9 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Thu, 6 Feb 2025 16:01:23 +0100 Subject: [PATCH 1/2] Refactored WithdrawalQueue to be stateless --- .../src/protocol/baselayer/BaseLayer.ts | 4 +- .../src/protocol/baselayer/NoopBaseLayer.ts | 21 +-- .../src/settlement/BridgingModule.ts | 29 +++- .../settlement/messages/WithdrawalQueue.ts | 147 ++++++++++-------- 4 files changed, 116 insertions(+), 85 deletions(-) diff --git a/packages/sequencer/src/protocol/baselayer/BaseLayer.ts b/packages/sequencer/src/protocol/baselayer/BaseLayer.ts index 175c2fd15..ef0d64f10 100644 --- a/packages/sequencer/src/protocol/baselayer/BaseLayer.ts +++ b/packages/sequencer/src/protocol/baselayer/BaseLayer.ts @@ -5,12 +5,12 @@ import { } from "@proto-kit/common"; import { IncomingMessageAdapter } from "../../settlement/messages/IncomingMessageAdapter"; -import type { OutgoingMessageQueue } from "../../settlement/messages/WithdrawalQueue"; +import type { OutgoingMessageAdapter } from "../../settlement/messages/WithdrawalQueue"; export interface BaseLayerDependencyRecord extends DependencyRecord { IncomingMessageAdapter: DependencyDeclaration; // TODO Move that to Database? - OutgoingMessageQueue: DependencyDeclaration; + OutgoingMessageQueue: DependencyDeclaration; } export interface BaseLayer extends DependencyFactory { diff --git a/packages/sequencer/src/protocol/baselayer/NoopBaseLayer.ts b/packages/sequencer/src/protocol/baselayer/NoopBaseLayer.ts index 9d84abf7f..81c59f8c4 100644 --- a/packages/sequencer/src/protocol/baselayer/NoopBaseLayer.ts +++ b/packages/sequencer/src/protocol/baselayer/NoopBaseLayer.ts @@ -1,5 +1,5 @@ import { noop } from "@proto-kit/common"; -import { PublicKey } from "o1js"; +import { Field, PublicKey } from "o1js"; import { Withdrawal } from "@proto-kit/protocol"; import { @@ -10,7 +10,7 @@ import { IncomingMessageAdapter } from "../../settlement/messages/IncomingMessag import { PendingTransaction } from "../../mempool/PendingTransaction"; import { OutgoingMessage, - OutgoingMessageQueue, + OutgoingMessageAdapter, } from "../../settlement/messages/WithdrawalQueue"; import { BaseLayer, BaseLayerDependencyRecord } from "./BaseLayer"; @@ -35,16 +35,11 @@ class NoopIncomingMessageAdapter implements IncomingMessageAdapter { } } -class NoopOutgoingMessageQueue implements OutgoingMessageQueue { - length(): number { - return 0; - } - - peek(num: number): OutgoingMessage[] { - return []; - } - - pop(num: number): OutgoingMessage[] { +class NoopOutgoingMessageAdapter implements OutgoingMessageAdapter { + async fetchWithdrawals( + tokenId: Field, + offset: number + ): Promise[]> { return []; } } @@ -65,7 +60,7 @@ export class NoopBaseLayer extends SequencerModule implements BaseLayer { useClass: NoopIncomingMessageAdapter, }, OutgoingMessageQueue: { - useClass: NoopOutgoingMessageQueue, + useClass: NoopOutgoingMessageAdapter, }, }; } diff --git a/packages/sequencer/src/settlement/BridgingModule.ts b/packages/sequencer/src/settlement/BridgingModule.ts index a1308208f..156afa13d 100644 --- a/packages/sequencer/src/settlement/BridgingModule.ts +++ b/packages/sequencer/src/settlement/BridgingModule.ts @@ -44,7 +44,7 @@ import { AsyncMerkleTreeStore } from "../state/async/AsyncMerkleTreeStore"; import { FeeStrategy } from "../protocol/baselayer/fees/FeeStrategy"; import type { MinaBaseLayer } from "../protocol/baselayer/MinaBaseLayer"; -import type { OutgoingMessageQueue } from "./messages/WithdrawalQueue"; +import type { OutgoingMessageAdapter } from "./messages/WithdrawalQueue"; import type { SettlementModule } from "./SettlementModule"; import { SettlementUtils } from "./utils/SettlementUtils"; import { MinaTransactionSender } from "./transactions/MinaTransactionSender"; @@ -73,7 +73,7 @@ export class BridgingModule extends SequencerModule { @inject("SettlementModule") private readonly settlementModule: SettlementModule, @inject("OutgoingMessageQueue") - private readonly outgoingMessageQueue: OutgoingMessageQueue, + private readonly outgoingMessageQueue: OutgoingMessageAdapter, @inject("AsyncMerkleStore") private readonly merkleTreeStore: AsyncMerkleTreeStore, @inject("FeeStrategy") @@ -326,7 +326,6 @@ export class BridgingModule extends SequencerModule { tx: Transaction; }[] > { - const length = this.outgoingMessageQueue.length(); const { feepayer } = this.settlementModule.config; let { nonce } = options; @@ -364,9 +363,27 @@ export class BridgingModule extends SequencerModule { this.getBridgingModuleConfig().withdrawalStatePath.split("."); const basePath = Path.fromProperty(withdrawalModule, withdrawalStateName); + // TODO Not sure if we should re-fetch the account state here + const outgoingMessageCursor = parseInt( + bridgeContract.outgoingMessageCursor.get().toString(), + 10 + ); + + const pendingWithdrawals = await this.outgoingMessageQueue.fetchWithdrawals( + tokenId, + outgoingMessageCursor + ); + // Create withdrawal batches and send them as L1 transactions - for (let i = 0; i < length; i += OUTGOING_MESSAGE_BATCH_SIZE) { - const batch = this.outgoingMessageQueue.peek(OUTGOING_MESSAGE_BATCH_SIZE); + for ( + let i = 0; + i < pendingWithdrawals.length; + i += OUTGOING_MESSAGE_BATCH_SIZE + ) { + const batch = pendingWithdrawals.slice( + i, + i + OUTGOING_MESSAGE_BATCH_SIZE + ); const keys = batch.map((x) => Path.fromKey(basePath, OutgoingMessageKey, { @@ -427,8 +444,6 @@ export class BridgingModule extends SequencerModule { "included" ); - this.outgoingMessageQueue.pop(OUTGOING_MESSAGE_BATCH_SIZE); - txs.push({ tx: signedTx, }); diff --git a/packages/sequencer/src/settlement/messages/WithdrawalQueue.ts b/packages/sequencer/src/settlement/messages/WithdrawalQueue.ts index 1b286fa0f..a75bb29ac 100644 --- a/packages/sequencer/src/settlement/messages/WithdrawalQueue.ts +++ b/packages/sequencer/src/settlement/messages/WithdrawalQueue.ts @@ -1,7 +1,6 @@ import { inject, injectable } from "tsyringe"; import { Withdrawal } from "@proto-kit/protocol"; import { Field, Struct } from "o1js"; -import { splitArray } from "@proto-kit/common"; import type { BlockTriggerBase } from "../../protocol/production/trigger/BlockTrigger"; import { SettlementModule } from "../SettlementModule"; @@ -9,6 +8,12 @@ import { SequencerModule } from "../../sequencer/builder/SequencerModule"; import { Sequencer } from "../../sequencer/executor/Sequencer"; import { Block } from "../../storage/model/Block"; import { BridgingModule } from "../BridgingModule"; +import { + BlockStorage, + HistoricalBlockStorage, +} from "../../storage/repositories/BlockStorage"; +import { SettlementStorage } from "../../storage/repositories/SettlementStorage"; +import { HistoricalBatchStorage } from "../../storage/repositories/BatchStorage"; export interface OutgoingMessage { index: number; @@ -34,21 +39,18 @@ export class WithdrawalEvent extends Struct({ * In the future, this interface should be flexibly typed so that the * outgoing message type is not limited to Withdrawals */ -export interface OutgoingMessageQueue { - peek: (num: number) => OutgoingMessage[]; - pop: (num: number) => OutgoingMessage[]; - length: () => number; +export interface OutgoingMessageAdapter { + fetchWithdrawals( + tokenId: Field, + offset: number + ): Promise[]>; } @injectable() export class WithdrawalQueue extends SequencerModule - implements OutgoingMessageQueue + implements OutgoingMessageAdapter { - private lockedQueue: Block[] = []; - - private unlockedQueue: OutgoingMessage[] = []; - private outgoingWithdrawalEvents: string[] = []; public constructor( @@ -56,31 +58,90 @@ export class WithdrawalQueue private readonly sequencer: Sequencer<{ BlockTrigger: typeof BlockTriggerBase; SettlementModule: typeof SettlementModule; - }> + }>, + @inject("BlockStorage") + private readonly blockStorage: BlockStorage & HistoricalBlockStorage, + @inject("BatchStorage") + private readonly batchStorage: HistoricalBatchStorage, + @inject("SettlementStorage") + private readonly settlementStorage: SettlementStorage ) { super(); } - public peek(num: number): OutgoingMessage[] { - return this.unlockedQueue.slice(0, num); + private extractEventsFromBlock(block: Block) { + return block.transactions.flatMap((result) => + result.events + .filter((event) => + this.outgoingWithdrawalEvents.includes(event.eventName) + ) + .map((event) => WithdrawalEvent.fromFields(event.data)) + ); } - public pop(num: number): OutgoingMessage[] { - const slice = this.peek(num); - this.unlockedQueue = this.unlockedQueue.slice(num); - return slice; + private async getLatestSettledBlock(): Promise { + const settlement = await this.settlementStorage.getLatestSettlement(); + if (settlement !== undefined) { + const lastBatch = settlement.batches.at(-1); + if (lastBatch !== undefined) { + const batch = await this.batchStorage.getBatchAt(lastBatch); + if (batch !== undefined) { + const blockHash = batch.blockHashes.at(-1); + if (blockHash !== undefined) { + return await this.blockStorage.getBlock(blockHash); + } + } + } + } + return undefined; } - public length() { - return this.unlockedQueue.length; + private async findBlockWithEvent(tokenId: Field, index: number) { + let block = await this.getLatestSettledBlock(); + + // Casting to defined here is fine, bcs in all cases where that could be undefined, + // we break and return undefined all together. + const blockHistory = [block!]; + + while (block !== undefined) { + const events = this.extractEventsFromBlock(block); + const found = events.find((withdrawalEvent) => { + return withdrawalEvent.key.tokenId + .equals(tokenId) + .and(withdrawalEvent.key.index.equals(index)) + .toBoolean(); + }); + if (found !== undefined) { + return blockHistory.reverse(); + } + if (block.previousBlockHash !== undefined) { + // eslint-disable-next-line no-await-in-loop + block = await this.blockStorage.getBlock( + block.previousBlockHash.toString() + ); + blockHistory.push(block!); + } + } + return undefined; + } + + public async fetchWithdrawals( + tokenId: Field, + offset: number + ): Promise[]> { + const blocks = await this.findBlockWithEvent(tokenId, offset); + + const events = blocks + ?.flatMap((block) => this.extractEventsFromBlock(block)) + ?.map((event) => ({ + index: parseInt(event.key.index.toString(), 10), + value: event.value, + })); + return events ?? []; } public async start(): Promise { // Hacky workaround for this cyclic dependency - const settlementModule = this.sequencer.resolveOrFail( - "SettlementModule", - SettlementModule - ); const bridgingModule = this.sequencer.resolveOrFail( "BridgingModule", BridgingModule @@ -88,45 +149,5 @@ export class WithdrawalQueue const { withdrawalEventName } = bridgingModule.getBridgingModuleConfig(); this.outgoingWithdrawalEvents = [withdrawalEventName]; - - this.sequencer.events.on("block-produced", (block) => { - this.lockedQueue.push(block); - }); - - // TODO Add event settlement-included and register it there - settlementModule.events.on("settlement-submitted", (batch) => { - // This finds out which blocks are contained in this batch and extracts only from those - const { inBatch, notInBatch } = splitArray(this.lockedQueue, (block) => - batch.blockHashes.includes(block.hash.toString()) - ? "inBatch" - : "notInBatch" - ); - - const withdrawals = (inBatch ?? []).flatMap((block) => { - return block.transactions - .flatMap((tx) => - tx.events - .filter( - (event) => event.eventName === this.outgoingWithdrawalEvents[0] - ) - .map((event) => { - return { - tx, - event, - }; - }) - ) - .map>(({ tx, event }) => { - const withdrawalEvent = WithdrawalEvent.fromFields(event.data); - - return { - index: Number(withdrawalEvent.key.index.toString()), - value: withdrawalEvent.value, - }; - }); - }); - this.unlockedQueue.push(...withdrawals); - this.lockedQueue = notInBatch ?? []; - }); } } From 76b88db53461bfda8ecd0362a63dbcae7e6fe4b5 Mon Sep 17 00:00:00 2001 From: Raphael Panic Date: Thu, 6 Feb 2025 16:04:11 +0100 Subject: [PATCH 2/2] Added TODO Note --- packages/sequencer/src/settlement/messages/WithdrawalQueue.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/sequencer/src/settlement/messages/WithdrawalQueue.ts b/packages/sequencer/src/settlement/messages/WithdrawalQueue.ts index a75bb29ac..e8770e7bc 100644 --- a/packages/sequencer/src/settlement/messages/WithdrawalQueue.ts +++ b/packages/sequencer/src/settlement/messages/WithdrawalQueue.ts @@ -79,6 +79,8 @@ export class WithdrawalQueue ); } + // TODO Not really efficient right now in regards to DB trips, can be + // easily built as a join query though private async getLatestSettledBlock(): Promise { const settlement = await this.settlementStorage.getLatestSettlement(); if (settlement !== undefined) {