diff --git a/packages/sequencer/src/mempool/sorting/MempoolSorter.ts b/packages/sequencer/src/mempool/sorting/MempoolSorter.ts new file mode 100644 index 000000000..0aa933e75 --- /dev/null +++ b/packages/sequencer/src/mempool/sorting/MempoolSorter.ts @@ -0,0 +1,81 @@ +import { injectable } from "tsyringe"; + +import { PendingTransaction } from "../PendingTransaction"; + +import { + MempoolSortingConfig, + TransactionGroup, + TransactionQueueItem, +} from "./types"; + +@injectable() +export class MempoolSorter { + constructor(private config: MempoolSortingConfig) {} + + public sortTransactions( + transactions: PendingTransaction[] + ): PendingTransaction[] { + const groups = this.groupTransactionsBySender(transactions); + + // sort transactions within each group by nonce just in case + groups.forEach((group) => { + group.transactions.sort((a, b) => { + const nonceA = BigInt(a.transaction.nonce.toString()); + const nonceB = BigInt(b.transaction.nonce.toString()); + + if (nonceA < nonceB) return -1; + if (nonceA > nonceB) return 1; + return 0; + }); + }); + + if (this.config.maxTransactionsPerSender != null) { + groups.forEach((group) => { + group.transactions = group.transactions.slice( + 0, + this.config.maxTransactionsPerSender + ); + }); + } + + if (this.config.minGasFee != null) { + groups.forEach((group) => { + group.transactions = group.transactions.filter((tx) => { + const gasFee = this.getGasFee(tx.transaction); + return gasFee >= this.config.minGasFee!; + }); + }); + } + + const sortedGroups = this.config.sortingStrategy(groups); + + return sortedGroups.map((item) => item.transaction); + } + + private groupTransactionsBySender( + transactions: PendingTransaction[] + ): TransactionGroup[] { + const groupMap = new Map(); + + let order = 0; + transactions.forEach((tx) => { + const sender = tx.sender.toBase58(); + if (!groupMap.has(sender)) { + groupMap.set(sender, []); + } + groupMap.get(sender)!.push({ transaction: tx, order }); + order += 1; + }); + + const groups: TransactionGroup[] = []; + groupMap.forEach((txs, sender) => { + groups.push({ sender, transactions: txs }); + }); + + return groups; + } + + private getGasFee(tx: PendingTransaction): bigint { + return 0n; // Placeholder + } +} diff --git a/packages/sequencer/src/mempool/sorting/strategies.ts b/packages/sequencer/src/mempool/sorting/strategies.ts new file mode 100644 index 000000000..258a42a6f --- /dev/null +++ b/packages/sequencer/src/mempool/sorting/strategies.ts @@ -0,0 +1,79 @@ +import type { PendingTransaction } from "../PendingTransaction"; + +import type { SortingStrategy, TransactionQueueItem } from "./types"; + +function getGasFee(tx: PendingTransaction): bigint { + return 0n; // Placeholder +} + +export const byFIFO: SortingStrategy = (groups) => { + return groups + .sort((a, b) => { + const firstOrderA = a.transactions[0]?.order ?? Number.MAX_SAFE_INTEGER; + const firstOrderB = b.transactions[0]?.order ?? Number.MAX_SAFE_INTEGER; + return firstOrderA - firstOrderB; + }) + .flatMap((group) => group.transactions); +}; + +// eslint-disable-next-line sonarjs/cognitive-complexity +export const byGreedyFee: SortingStrategy = (groups) => { + const result: TransactionQueueItem[] = []; + const workingGroups = groups.map((g) => ({ + sender: g.sender, + transactions: [...g.transactions], + })); + + let hasRemaining = true; + while (hasRemaining) { + hasRemaining = false; + + let bestGroupIndex = -1; + let bestFee = -1n; + let bestOrder = Number.MAX_SAFE_INTEGER; + + for (let i = 0; i < workingGroups.length; i++) { + const group = workingGroups[i]; + if (group.transactions.length > 0) { + hasRemaining = true; + const tx = group.transactions[0]; + const fee = getGasFee(tx.transaction); + + if (fee > bestFee || (fee === bestFee && tx.order < bestOrder)) { + bestFee = fee; + bestOrder = tx.order; + bestGroupIndex = i; + } + } + } + + if (bestGroupIndex >= 0) { + const tx = workingGroups[bestGroupIndex].transactions.shift()!; + result.push(tx); + } + } + + return result; +}; + +export const byRoundRobin: SortingStrategy = (groups) => { + const result: TransactionQueueItem[] = []; + const workingGroups = groups.map((g) => ({ + sender: g.sender, + transactions: [...g.transactions], + })); + + let hasRemaining = true; + while (hasRemaining) { + hasRemaining = false; + for (const group of workingGroups) { + if (group.transactions.length > 0) { + hasRemaining = true; + const tx = group.transactions.shift()!; + result.push(tx); + } + } + } + + return result; +}; diff --git a/packages/sequencer/src/mempool/sorting/types.ts b/packages/sequencer/src/mempool/sorting/types.ts new file mode 100644 index 000000000..8ad879e51 --- /dev/null +++ b/packages/sequencer/src/mempool/sorting/types.ts @@ -0,0 +1,21 @@ +import { PendingTransaction } from "../PendingTransaction"; + +export interface TransactionGroup { + sender: string; + transactions: TransactionQueueItem[]; +} + +export type SortingStrategy = ( + groups: TransactionGroup[] +) => TransactionQueueItem[]; + +export interface MempoolSortingConfig { + sortingStrategy: SortingStrategy; + maxTransactionsPerSender?: number; + minGasFee?: bigint; +} + +export interface TransactionQueueItem { + transaction: PendingTransaction; + order: number; +}