diff --git a/packages/rollup-core/src/app/data/consumers/block-batch-processor.ts b/packages/rollup-core/src/app/data/consumers/block-batch-processor.ts deleted file mode 100644 index bf64157c2c2..00000000000 --- a/packages/rollup-core/src/app/data/consumers/block-batch-processor.ts +++ /dev/null @@ -1,196 +0,0 @@ -/* External Imports */ -import { - BaseQueuedPersistedProcessor, - DB, - EthereumListener, -} from '@eth-optimism/core-db' -import { getLogger, Logger } from '@eth-optimism/core-utils' - -import { Block, Provider, TransactionResponse } from 'ethers/providers' -import { Log } from 'ethers/providers/abstract-provider' - -/* Internal Imports */ -import { - BlockBatches, - BlockBatchListener, - BatchLogParserContext, - L1Batch, -} from '../../../types' -import { addressesAreEqual } from '../../utils' - -const log: Logger = getLogger('block-batch-processor') - -export class BlockBatchProcessor - extends BaseQueuedPersistedProcessor - implements EthereumListener { - public static readonly persistenceKey = 'BlockBatchProcessor' - - private readonly topics: string[] - private readonly topicMap: Map - - /** - * Creates a BlockBatchProcessor that subscribes to blocks, processes all - * L1ToL2Transaction events, parses L1ToL2Transactions and submits them to L2. - * - * @param db The DB to use to persist the queue of L1ToL2Transaction[] objects. - * @param l1Provider The provider to use to connect to L1 to subscribe & fetch block / tx / log data. - * @param logContexts The collection of L1ToL2TransactionLogParserContext that uniquely identify the log event and - * provide the ability to create L2 transactions from the L1 transaction that emitted it. - * @param listeners The downstream subscribers to the L1ToL2TransactionBatch objects this processor creates. - * @param persistenceKey The persistence key to use for this instance within the provided DB. - */ - public static async create( - db: DB, - l1Provider: Provider, - logContexts: BatchLogParserContext[], - listeners: BlockBatchListener[], - persistenceKey: string = BlockBatchProcessor.persistenceKey - ): Promise { - const processor = new BlockBatchProcessor( - db, - l1Provider, - logContexts, - listeners, - persistenceKey - ) - await processor.init() - return processor - } - - private constructor( - db: DB, - private readonly l1Provider: Provider, - logContexts: BatchLogParserContext[], - private readonly listeners: BlockBatchListener[], - persistenceKey: string = BlockBatchProcessor.persistenceKey - ) { - super(db, persistenceKey) - this.topicMap = new Map( - logContexts.map((x) => [x.topic, x]) - ) - this.topics = Array.from(this.topicMap.keys()) - } - - /** - * @inheritDoc - */ - public async handle(block: Block): Promise { - log.debug( - `Received block ${block.number}. Searching for any contained L1toL2Transactions.` - ) - - const logs: Log[] = await this.l1Provider.getLogs({ - blockHash: block.hash, - topics: this.topics, - }) - log.debug( - `Got ${logs.length} logs from block ${block.number}: ${JSON.stringify( - logs - )}` - ) - - logs.sort((a, b) => a.logIndex - b.logIndex) - - let batches: L1Batch[] = await Promise.all( - logs.map((l) => this.getBatchFromLog(l)) - ) - - batches = batches.filter((x) => x.length > 0) - - if (!batches.length) { - log.debug(`There were no L1toL2Transactions in block ${block.number}.`) - } else { - log.debug( - `Parsed ${batches.length} batches from block ${ - block.number - }: ${JSON.stringify(batches)}` - ) - } - - this.add(block.number, { - blockNumber: block.number, - timestamp: block.timestamp, - batches, - }) - } - - /** - * @inheritDoc - */ - public async onSyncCompleted(syncIdentifier?: string): Promise { - // TODO: Turn off processing of CannonicalTransactionChainBatch events here - } - - /** - * @inheritDoc - */ - protected async handleNextItem( - blockNumber: number, - blockBatches: BlockBatches - ): Promise { - try { - if (!!blockBatches.batches.length) { - this.listeners.map((x) => x.handleBlockBatches(blockBatches)) - } - await this.markProcessed(blockNumber) - } catch (e) { - this.logError( - `Error processing L1ToL2Transactions. Txs: ${JSON.stringify( - blockBatches - )}`, - e - ) - // Can't properly sync from L1 to L2, and need to do so in order. This is fatal. - process.exit(1) - } - } - - /** - * @inheritDoc - */ - protected async serializeItem(item: BlockBatches): Promise { - return Buffer.from(JSON.stringify(item), 'utf-8') - } - - /** - * @inheritDoc - */ - protected async deserializeItem(itemBuffer: Buffer): Promise { - return JSON.parse(itemBuffer.toString('utf-8')) - } - - private async getBatchFromLog(l: Log): Promise { - const matchedTopics: string[] = l.topics.filter( - (x) => this.topics.indexOf(x) >= 0 - ) - if (matchedTopics.length === 0) { - log.error( - `Received log with topics: ${l.topics.join( - ',' - )} for subscription to topics: ${this.topics.join(',')}. Transaction: ${ - l.transactionHash - }` - ) - return [] - } - - const transaction: TransactionResponse = await this.l1Provider.getTransaction( - l.transactionHash - ) - log.debug( - `Fetched tx by hash ${l.transactionHash}: ${JSON.stringify(transaction)}` - ) - - const parsedBatch: L1Batch = [] - for (const topic of matchedTopics) { - const context = this.topicMap.get(topic) - if (!addressesAreEqual(l.address, context.contractAddress)) { - continue - } - const transactions = await context.parseL1Batch(l, transaction) - parsedBatch.push(...transactions) - } - - return parsedBatch - } -} diff --git a/packages/rollup-core/src/app/data/consumers/index.ts b/packages/rollup-core/src/app/data/consumers/index.ts index 2234d9446fa..ff39044bb38 100644 --- a/packages/rollup-core/src/app/data/consumers/index.ts +++ b/packages/rollup-core/src/app/data/consumers/index.ts @@ -1,3 +1,3 @@ -export * from './block-batch-submitter' -export * from './block-batch-processor' +export * from './l2-batch-creator' +export * from './l2-batch-submitter' export * from './verifier' diff --git a/packages/rollup-core/src/app/data/consumers/l2-batch-creator.ts b/packages/rollup-core/src/app/data/consumers/l2-batch-creator.ts new file mode 100644 index 00000000000..0391d02771e --- /dev/null +++ b/packages/rollup-core/src/app/data/consumers/l2-batch-creator.ts @@ -0,0 +1,55 @@ +/* External Imports */ +import { getLogger, logError, ScheduledTask } from '@eth-optimism/core-utils' + +/* Internal Imports */ +import { DataService, L1BatchRecord } from '../../../types/data' + +const log = getLogger('l2-batch-creator') + +/** + * Polls the DB to create a batch of L2 Transactions, when one is ready. + */ +export class L2BatchCreator extends ScheduledTask { + constructor( + private readonly dataService: DataService, + periodMilliseconds = 10_000 + ) { + super(periodMilliseconds) + } + + /** + * @inheritDoc + * + * Creates L2 batches from L2 Transactions in the DB, either when: + * 1. Unsubmitted & unverified transactions in the L2 tx DB match the oldest unverified L1 batch in size + * 2. Unsubmitted & unverified transactions in the L2 tx DB have multiple timestamps (multiple batches exist) + * + */ + public async runTask(): Promise { + try { + const l1BatchRecord: L1BatchRecord = await this.dataService.getOldestUnverifiedL1TransactionBatch() + if (!l1BatchRecord) { + const l2OnlyBatchBuilt: number = await this.dataService.tryBuildL2OnlyBatch() + if (l2OnlyBatchBuilt !== undefined && l2OnlyBatchBuilt >= 0) { + log.debug(`L2-only batch with number ${l2OnlyBatchBuilt} was built!`) + } + return + } + + const batchBuilt: number = await this.dataService.tryBuildL2BatchToMatchL1( + l1BatchRecord.batchNumber, + l1BatchRecord.batchSize + ) + if (batchBuilt !== undefined && batchBuilt >= 0) { + log.debug( + `L2 batch to match L1 batch of size ${l1BatchRecord} was built. Batch number: ${batchBuilt}.` + ) + return + } + + log.debug(`No L2 batches built... sad.`) + } catch (e) { + logError(log, `Error running L2BatchCreator! Continuing...`, e) + } + } +} diff --git a/packages/rollup-core/src/app/data/consumers/l2-batch-submitter.ts b/packages/rollup-core/src/app/data/consumers/l2-batch-submitter.ts new file mode 100644 index 00000000000..61a81b6f459 --- /dev/null +++ b/packages/rollup-core/src/app/data/consumers/l2-batch-submitter.ts @@ -0,0 +1,70 @@ +/* External Imports */ +import { + getLogger, + logError, + ScheduledTask, +} from '@eth-optimism/core-utils/build' + +/* Internal Imports */ +import { L1DataService } from '../../../types/data' +import { BlockBatches, L2NodeService } from '../../../types' + +const log = getLogger('l2-batch-submitter') + +/** + * Polls the database for new Rollup Transactions that were submitted to L1 that + * have not yet been processed by L2 and submits them one-by-one to L2. + */ +export class L2BatchSubmitter extends ScheduledTask { + constructor( + private readonly l1DataService: L1DataService, + private readonly l2NodeService: L2NodeService, + periodMilliseconds: number = 10_000 + ) { + super(periodMilliseconds) + } + + /** + * @inheritDoc + */ + public async runTask(): Promise { + let blockBatches: BlockBatches + try { + blockBatches = await this.l1DataService.getNextBatchForL2Submission() + } catch (e) { + logError(log, `Error fetching next batch for L2 submission!`, e) + return + } + + if (!blockBatches) { + log.debug(`No batches ready for submission to L2.`) + return + } + + try { + await this.l2NodeService.sendBlockBatches(blockBatches) + } catch (e) { + logError( + log, + `Error sending batch to BlockBatchSubmitter! Block Batches: ${JSON.stringify( + blockBatches + )}`, + e + ) + return + } + + try { + await this.l1DataService.markL1BatchSubmittedToL2( + blockBatches.batchNumber + ) + } catch (e) { + logError( + log, + `Error marking L1 Batch as Submitted to L2. L1 Batch Number: ${blockBatches.batchNumber}`, + e + ) + return + } + } +} diff --git a/packages/rollup-core/src/app/data/consumers/verifier.ts b/packages/rollup-core/src/app/data/consumers/verifier.ts index b2ee99885bf..310d88a8e59 100644 --- a/packages/rollup-core/src/app/data/consumers/verifier.ts +++ b/packages/rollup-core/src/app/data/consumers/verifier.ts @@ -11,7 +11,8 @@ import { const log = getLogger('verifier') /** - * Polls the DB for VerificationCandidates to ensure that L1 rollup state roots match L2 state roots. + * Polls the DB for VerificationCandidates to ensure that L1 rollup Txs match L2 Txs. + * */ export class Verifier extends ScheduledTask { private static readonly ALERT_EVERY: number = 6 diff --git a/packages/rollup-core/src/app/data/data-service.ts b/packages/rollup-core/src/app/data/data-service.ts index 3009f475a1f..b708ea065f7 100644 --- a/packages/rollup-core/src/app/data/data-service.ts +++ b/packages/rollup-core/src/app/data/data-service.ts @@ -6,22 +6,24 @@ import { Block, TransactionResponse } from 'ethers/providers' /* Internal Imports */ import { + BlockBatches, DataService, + L1BatchRecord, RollupTransaction, TransactionAndRoot, VerificationCandidate, } from '../../types' import { - blockInsertStatement, - getBlockInsertValue, + l1BlockInsertStatement, + getL1BlockInsertValue, getL2TransactionInsertValue, - getRollupStateRootInsertValue, - getRollupTransactionInsertValue, - getTransactionInsertValue, + getL1RollupStateRootInsertValue, + getL1RollupTransactionInsertValue, + getL1TransactionInsertValue, l2TransactionInsertStatement, - rollupStateRootInsertStatement, - rollupTxInsertStatement, - txInsertStatement, + l1RollupStateRootInsertStatement, + l1RollupTxInsertStatement, + l1TxInsertStatement, } from './query-utils' const log = getLogger('data-service') @@ -35,12 +37,12 @@ export class DefaultDataService implements DataService { /** * @inheritDoc */ - public async insertBlock( + public async insertL1Block( block: Block, processed: boolean = false ): Promise { return this.rdb.execute( - `${blockInsertStatement} VALUES (${getBlockInsertValue( + `${l1BlockInsertStatement} VALUES (${getL1BlockInsertValue( block, processed )})` @@ -50,30 +52,30 @@ export class DefaultDataService implements DataService { /** * @inheritDoc */ - public async insertTransactions( + public async insertL1Transactions( transactions: TransactionResponse[] ): Promise { if (!transactions || !transactions.length) { return } const values: string[] = transactions.map( - (x) => `(${getTransactionInsertValue(x)})` + (x) => `(${getL1TransactionInsertValue(x)})` ) - return this.rdb.execute(`${txInsertStatement} VALUES ${values.join(',')}`) + return this.rdb.execute(`${l1TxInsertStatement} VALUES ${values.join(',')}`) } /** * @inheritDoc */ - public async insertBlockAndTransactions( + public async insertL1BlockAndTransactions( block: Block, txs: TransactionResponse[], processed: boolean = false ): Promise { await this.rdb.startTransaction() try { - await this.insertBlock(block, processed) - await this.insertTransactions(txs) + await this.insertL1Block(block, processed) + await this.insertL1Transactions(txs) } catch (e) { await this.rdb.rollback() throw e @@ -84,7 +86,7 @@ export class DefaultDataService implements DataService { /** * @inheritDoc */ - public async insertRollupTransactions( + public async insertL1RollupTransactions( l1TxHash: string, rollupTransactions: RollupTransaction[] ): Promise { @@ -100,10 +102,10 @@ export class DefaultDataService implements DataService { ) const values: string[] = rollupTransactions.map( - (x) => `(${getRollupTransactionInsertValue(x, batchNumber)})` + (x) => `(${getL1RollupTransactionInsertValue(x, batchNumber)})` ) await this.rdb.execute( - `${rollupTxInsertStatement} VALUES ${values.join(',')}` + `${l1RollupTxInsertStatement} VALUES ${values.join(',')}` ) await this.rdb.commit() @@ -123,7 +125,7 @@ export class DefaultDataService implements DataService { /** * @inheritDoc */ - public async insertRollupStateRoots( + public async insertL1RollupStateRoots( l1TxHash: string, stateRoots: string[] ): Promise { @@ -137,10 +139,11 @@ export class DefaultDataService implements DataService { batchNumber = await this.insertNewL1StateRootBatch(l1TxHash) const values: string[] = stateRoots.map( - (root, i) => `(${getRollupStateRootInsertValue(root, batchNumber, i)})` + (root, i) => + `(${getL1RollupStateRootInsertValue(root, batchNumber, i)})` ) await this.rdb.execute( - `${rollupStateRootInsertStatement} VALUES ${values.join(',')}` + `${l1RollupStateRootInsertStatement} VALUES ${values.join(',')}` ) await this.rdb.commit() @@ -155,6 +158,92 @@ export class DefaultDataService implements DataService { } } + /** + * @inheritDoc + */ + public async getOldestUnverifiedL1TransactionBatch(): Promise { + const res: Row[] = await this.rdb.select(` + SELECT COUNT(*) as batch_size, batch_number, block_timestamp + FROM next_l1_verification_batch + GROUP BY batch_number, block_timestamp + ORDER BY batch_number ASC + `) // note batch_number should be the same, just ordering in case + + if (!res || !res.length || !res[0].columns['batch_size']) { + return undefined + } + return { + batchSize: res[0].columns['batch_size'], + batchNumber: res[0].columns['batch_number'], + blockTimestamp: res[0].columns['block_timestamp'], + } + } + + /** + * @inheritDoc + */ + public async getNextBatchForL2Submission(): Promise { + const res: Row[] = await this.rdb.select(` + SELECT batch_number, target, calldata, block_timestamp, block_number, l1_tx_hash, queue_origin, sender, l1_message_sender, gas_limit, nonce, signature + FROM next_l2_submission_batch + `) + + if (!res || !res.length) { + return undefined + } + + const batchNumber = res[0].columns['batch_number'] + const timestamp = res[0].columns['block_timestamp'] + const blockNumber = res[0].columns['block_number'] + + return { + batchNumber, + timestamp, + blockNumber, + batches: [ + res.map((row: Row, batchIndex: number) => { + const tx: RollupTransaction = { + batchIndex, + target: row.columns['target'], + calldata: row.columns['calldata'], // TODO: may have to format Buffer => string + l1Timestamp: row.columns['block_timestamp'], + l1BlockNumber: row.columns['block_number'], + l1TxHash: row.columns['l1_tx_hash'], + queueOrigin: row.columns['queue_origin'], + } + + if (!!row.columns['sender']) { + tx.sender = row.columns['sender'] + } + if (!!row.columns['l1MessageSender']) { + tx.l1MessageSender = row.columns['l1_message_sender'] + } + if (!!row.columns['gas_limit']) { + tx.gasLimit = row.columns['gas_limit'] + } + if (!!row.columns['nonce']) { + tx.nonce = row.columns['nonce'] + } + if (!!row.columns['signature']) { + tx.nonce = row.columns['signature'] + } + return tx + }), + ], + } + } + + /** + * @inheritDoc + */ + public async markL1BatchSubmittedToL2(batchNumber: number): Promise { + return this.rdb.execute( + `UPDATE l1_tx_batch + SET status = 'SUBMITTED_TO_L2' + WHERE batch_number = ${batchNumber}` + ) + } + /** * @inheritDoc */ @@ -180,6 +269,116 @@ export class DefaultDataService implements DataService { ) } + /** + * @inheritDoc + */ + public async tryBuildL2OnlyBatch(): Promise { + const timestampRes = await this.rdb.select( + `SELECT DISTINCT block_timestamp + FROM l2_tx + WHERE status = 'UNBATCHED' + ORDER BY block_timestamp ASC + ` + ) + + if (!timestampRes || timestampRes.length < 2) { + return -1 + } + + const batchTimestamp = timestampRes[0].columns['block_timestamp'] + + await this.rdb.startTransaction() + try { + const batchNumber = await this.insertNewL2TransactionBatch() + await this.rdb.execute(` + UPDATE l2_tx + SET status = 'BATCHED', batch_number = ${batchNumber} + WHERE status = 'UNBATCHED' AND block_timestamp = ${batchTimestamp} + `) + + await this.rdb.commit() + return batchNumber + } catch (e) { + logError(log, `Error building L2 Batch!`, e) + await this.rdb.rollback() + throw Error(e) + } + } + + public async tryBuildL2BatchToMatchL1( + l1BatchSize: number, + l1BatchNumber: number + ): Promise { + const maxL2BatchNumber = await this.getMaxL2TxBatchNumber() + if (maxL2BatchNumber >= l1BatchNumber) { + log.debug( + `Not attempting to build batch because max L2 batch number is ${maxL2BatchNumber} and provided L1 batchNumber is ${l1BatchNumber}` + ) + return -1 + } + + const transactionsToBatchRes = await this.rdb.select(` + SELECT COUNT(*) as batchable_tx_count, block_timestamp + FROM l2_tx + WHERE status = 'UNBATCHED' + GROUP BY block_timestamp + ORDER BY block_timestamp ASC + `) + + if ( + !transactionsToBatchRes || + !transactionsToBatchRes.length || + !transactionsToBatchRes[0].columns['batchable_tx_count'] + ) { + return -1 + } + + const batchableTxCount = + transactionsToBatchRes[0].columns['batchable_tx-count'] + if (batchableTxCount < l1BatchSize && transactionsToBatchRes.length > 1) { + const msg = `L2 transactions do not match L1 transactions! Cannot and will not be able to build an L2 batch until this is fixed! Expected L1 batch size ${l1BatchSize}, got multiple L2 batches with the oldest unbatched being of size ${batchableTxCount}` + log.error(msg) + throw Error(msg) + } + + if (batchableTxCount < l1BatchSize) { + return -1 + } + + await this.rdb.startTransaction() + try { + const batchNumber = await this.insertNewL2TransactionBatch() + if (batchNumber !== l1BatchNumber) { + log.error( + `Created L2 batch number ${batchNumber} does not match expected L1 batch number ${l1BatchNumber}. This probably shouldn't happen.` + ) + await this.rdb.rollback() + return -1 + } + await this.rdb.execute(` + UPDATE l2_tx l + SET l.status = 'BATCHED', l.batch_number = ${batchNumber} + FROM ( + SELECT * + FROM l2_tx + WHERE status = 'UNBATCHED' + LIMIT ${l1BatchSize} + ) t + WHERE l.id = t.id + `) + await this.rdb.commit() + return batchNumber + } catch (e) { + logError( + log, + `Error creating L2 batch to match L1 batch of size ${l1BatchSize}.`, + e + ) + await this.rdb.rollback() + throw Error(e) + } + } + /************ * VERIFIER * ************/ @@ -190,8 +389,8 @@ export class DefaultDataService implements DataService { public async getVerificationCandidate(): Promise { const rows: Row[] = await this.rdb.select(` SELECT l1.batch_number as l1_batch, l2.batch_number as l2_batch, l1.batch_index, l1.state_root as l1_root, l2.state_root as l2_root - FROM next_l1_batch l1 - LEFT OUTER JOIN next_l2_batch l2 + FROM next_l1_verification_batch l1 + LEFT OUTER JOIN next_l2_verification_batch l2 ON l1.batch_number = l2.batch_number AND l1.batch_index = l2.batch_index ORDER BY l1.batch_index ASC `) @@ -277,6 +476,51 @@ export class DefaultDataService implements DataService { return batchNumber } + /** + * @inheritDoc + */ + protected async insertNewL2TransactionBatch(): Promise { + let batchNumber: number + + let retries = 3 + // This should never fail, but adding in retries anyway + while (retries > 0) { + try { + batchNumber = (await this.getMaxL2TxBatchNumber()) + 1 + await this.rdb.execute(` + INSERT INTO l2_tx_batch(batch_number) + VALUES (${batchNumber})`) + break + } catch (e) { + retries-- + } + } + + return batchNumber + } + + /** + * Fetches the max L2 tx batch number for use in inserting a new tx batch + * @returns The max batch number at the time of this query. + */ + protected async getMaxL2TxBatchNumber(): Promise { + const rows = await this.rdb.select( + `SELECT MAX(batch_number) as batch_number + FROM l2_tx_batch` + ) + if ( + rows && + !!rows.length && + !!rows[0].columns && + !!rows[0].columns['batch_number'] + ) { + // TODO: make sure we don't need to cast + return rows[0].columns['batch_number'] + } + + return -1 + } + /** * Fetches the max L1 tx batch number for use in inserting a new tx batch * @returns The max batch number at the time of this query. diff --git a/packages/rollup-core/src/app/data/index.ts b/packages/rollup-core/src/app/data/index.ts index 6a16ee2149e..54c30899e58 100644 --- a/packages/rollup-core/src/app/data/index.ts +++ b/packages/rollup-core/src/app/data/index.ts @@ -1,2 +1,4 @@ export * from './consumers' export * from './producers' + +export * from './data-service' diff --git a/packages/rollup-core/src/app/data/producers/l1-chain-data-persister.ts b/packages/rollup-core/src/app/data/producers/l1-chain-data-persister.ts index 3947a50acd2..54db401b567 100644 --- a/packages/rollup-core/src/app/data/producers/l1-chain-data-persister.ts +++ b/packages/rollup-core/src/app/data/producers/l1-chain-data-persister.ts @@ -2,12 +2,7 @@ import { DB } from '@eth-optimism/core-db' import { getLogger, Logger } from '@eth-optimism/core-utils' -import { - Block, - Provider, - TransactionReceipt, - TransactionResponse, -} from 'ethers/providers' +import { Block, Provider, TransactionResponse } from 'ethers/providers' import { Log } from 'ethers/providers/abstract-provider' /* Internal Imports */ @@ -119,7 +114,7 @@ export class L1ChainDataPersister extends ChainDataProcessor { relevantLogs.map((l) => this.l1Provider.getTransaction(l.transactionHash)) ) - await this.l1DataService.insertBlockAndTransactions(block, txs, false) + await this.l1DataService.insertL1BlockAndTransactions(block, txs, false) for (let i = 0; i < relevantLogs.length; i++) { const current_log = relevantLogs[i] diff --git a/packages/rollup-core/src/app/data/query-utils.ts b/packages/rollup-core/src/app/data/query-utils.ts index 0257a925cca..0fa50995dcc 100644 --- a/packages/rollup-core/src/app/data/query-utils.ts +++ b/packages/rollup-core/src/app/data/query-utils.ts @@ -4,8 +4,10 @@ import { Block, TransactionResponse } from 'ethers/providers' /* Internal Imports */ import { RollupTransaction, TransactionAndRoot } from '../../types' -export const txInsertStatement = `INSERT INTO l1_tx(block_number, block_hash, hash, from_address, to_address, nonce, gas_limit, gas_price, calldata, v, r, s) ` -export const getTransactionInsertValue = (tx: TransactionResponse): string => { +export const l1TxInsertStatement = `INSERT INTO l1_tx(block_number, block_hash, hash, from_address, to_address, nonce, gas_limit, gas_price, calldata, v, r, s) ` +export const getL1TransactionInsertValue = ( + tx: TransactionResponse +): string => { return `${tx.blockNumber}, '${tx.blockHash}', '${tx.hash}', '${tx.from}', '${ tx.to }', ${tx.nonce}, ${bigNumOrNull(tx.gasLimit)}, ${bigNumOrNull( @@ -13,8 +15,8 @@ export const getTransactionInsertValue = (tx: TransactionResponse): string => { )}, '${tx.data}', ${numOrNull(tx.v)}, ${numOrNull(tx.r)}, ${numOrNull(tx.s)}` } -export const blockInsertStatement = `INSERT INTO l1_block(block_hash, parent_hash, block_number, block_timestamp, gas_limit, gas_used, processed) ` -export const getBlockInsertValue = ( +export const l1BlockInsertStatement = `INSERT INTO l1_block(block_hash, parent_hash, block_number, block_timestamp, gas_limit, gas_used, processed) ` +export const getL1BlockInsertValue = ( block: Block, processed: boolean ): string => { @@ -25,8 +27,8 @@ export const getBlockInsertValue = ( )}` } -export const rollupTxInsertStatement = `INSERT INTO rollup_tx(sender, l1_message_sender, target, calldata, queue_origin, nonce, gas_limit, signature, batch_number, batch_index) ` -export const getRollupTransactionInsertValue = ( +export const l1RollupTxInsertStatement = `INSERT INTO rollup_tx(sender, l1_message_sender, target, calldata, queue_origin, nonce, gas_limit, signature, batch_number, batch_index) ` +export const getL1RollupTransactionInsertValue = ( tx: RollupTransaction, batchNumber: number ): string => { @@ -37,8 +39,8 @@ export const getRollupTransactionInsertValue = ( )}, ${stringOrNull(tx.signature)}, ${batchNumber}, ${tx.batchIndex}` } -export const rollupStateRootInsertStatement = `INSERT into l1_state_root(state_root, batch_number, batch_index) ` -export const getRollupStateRootInsertValue = ( +export const l1RollupStateRootInsertStatement = `INSERT into l1_state_root(state_root, batch_number, batch_index) ` +export const getL1RollupStateRootInsertValue = ( stateRoot: string, batchNumber: number, batchIndex: number diff --git a/packages/rollup-core/src/app/index.ts b/packages/rollup-core/src/app/index.ts index d842ca5b1d4..bf5a82410ef 100644 --- a/packages/rollup-core/src/app/index.ts +++ b/packages/rollup-core/src/app/index.ts @@ -3,4 +3,5 @@ export * from './serialization' export * from './util' export * from './constants' +export * from './l2-node-service' export * from './utils' diff --git a/packages/rollup-core/src/app/data/consumers/block-batch-submitter.ts b/packages/rollup-core/src/app/l2-node-service.ts similarity index 76% rename from packages/rollup-core/src/app/data/consumers/block-batch-submitter.ts rename to packages/rollup-core/src/app/l2-node-service.ts index f42ec2623db..2e54e0b1d7e 100644 --- a/packages/rollup-core/src/app/data/consumers/block-batch-submitter.ts +++ b/packages/rollup-core/src/app/l2-node-service.ts @@ -5,11 +5,11 @@ import { JsonRpcProvider } from 'ethers/providers' import { Wallet } from 'ethers' /* Internal Imports */ -import { BlockBatches, BlockBatchListener } from '../../../types' +import { BlockBatches, L2NodeService } from '../types' const log: Logger = getLogger('block-batch-submitter') -export class BlockBatchSubmitter implements BlockBatchListener { +export class DefaultL2NodeService implements L2NodeService { // params: [blockBatchesJSONString, signedBlockBatchesJSONString] // -- note all numbers are replaces with hex strings when serialized public static readonly sendBlockBatchesMethod: string = 'eth_sendBlockBatches' @@ -23,7 +23,7 @@ export class BlockBatchSubmitter implements BlockBatchListener { /** * @inheritDoc */ - public async handleBlockBatches(blockBatches: BlockBatches): Promise { + public async sendBlockBatches(blockBatches: BlockBatches): Promise { if (!blockBatches) { const msg = `Received undefined Block Batch!.` log.error(msg) @@ -31,7 +31,7 @@ export class BlockBatchSubmitter implements BlockBatchListener { } if (!blockBatches.batches || !blockBatches.batches.length) { - log.debug(`Moving past empty block ${blockBatches.blockNumber}.`) + log.error(`Received empty block batch: ${JSON.stringify(blockBatches)}`) return } @@ -43,7 +43,7 @@ export class BlockBatchSubmitter implements BlockBatchListener { }) const signedPayload: string = await this.l2Wallet.signMessage(payload) - await this.l2Provider.send(BlockBatchSubmitter.sendBlockBatchesMethod, [ + await this.l2Provider.send(DefaultL2NodeService.sendBlockBatchesMethod, [ payload, signedPayload, ]) diff --git a/packages/rollup-core/src/types/data/index.ts b/packages/rollup-core/src/types/data/index.ts index 5f4b66f0e26..4d3cdfa8686 100644 --- a/packages/rollup-core/src/types/data/index.ts +++ b/packages/rollup-core/src/types/data/index.ts @@ -1,4 +1,5 @@ export * from './data-service' export * from './l1-data-service' export * from './l2-data-service' +export * from './types' export * from './verifier-data-service' diff --git a/packages/rollup-core/src/types/data/l1-data-service.ts b/packages/rollup-core/src/types/data/l1-data-service.ts index 535b8c7ab9d..5b198616f48 100644 --- a/packages/rollup-core/src/types/data/l1-data-service.ts +++ b/packages/rollup-core/src/types/data/l1-data-service.ts @@ -2,7 +2,14 @@ import { Block, TransactionResponse } from 'ethers/providers' /* Internal Imports */ -import { RollupTransaction, TransactionAndRoot } from '../types' +import { + BlockBatches, + L1Batch, + RollupTransaction, + TransactionAndRoot, +} from '../types' +import { L1BatchRecord } from './types' +import { Row } from '@eth-optimism/core-db/build/src' export interface L1DataService { /** @@ -12,7 +19,7 @@ export interface L1DataService { * @param processed Whether or not the Block is completely processed and ready for use by other parts of the system. * @throws An error if there is a DB error. */ - insertBlock(block: Block, processed: boolean): Promise + insertL1Block(block: Block, processed: boolean): Promise /** * Atomically inserts the provided transactions into the associated RDB. @@ -20,7 +27,7 @@ export interface L1DataService { * @param transactions The transactions to insert. * @throws An error if there is a DB error. */ - insertTransactions(transactions: TransactionResponse[]): Promise + insertL1Transactions(transactions: TransactionResponse[]): Promise /** * Atomically inserts the provided block & contained transactions of interest. @@ -30,7 +37,7 @@ export interface L1DataService { * @param processed Whether or not the Block is completely processed and ready for use by other parts of the system. * @throws An error if there is a DB error. */ - insertBlockAndTransactions( + insertL1BlockAndTransactions( block: Block, txs: TransactionResponse[], processed: boolean @@ -53,7 +60,7 @@ export interface L1DataService { * @returns The inserted transaction batch number. * @throws An error if there is a DB error. */ - insertRollupTransactions( + insertL1RollupTransactions( l1TxHash: string, rollupTransactions: RollupTransaction[] ): Promise @@ -66,8 +73,31 @@ export interface L1DataService { * @returns The inserted state root batch number. * @throws An error if there is a DB error. */ - insertRollupStateRoots( + insertL1RollupStateRoots( l1TxHash: string, stateRoots: string[] ): Promise + + /** + * Fetches the next batch from L1 to submit to L2, if there is one. + * + * @returns The fetched batch or undefined if one is not present in the DB. + */ + getNextBatchForL2Submission(): Promise + + /** + * Marks the provided L1 batch as submitted to L2. + * + * @params batchNumber The L1 batch number to mark as submitted to L2. + * @throws An error if there is a DB error. + */ + markL1BatchSubmittedToL2(batchNumber: number): Promise + + /** + * Gets the oldest unverified L1 transaction batch. + * + * @returns The L1BatchRecord representing the oldest unverified batch + * @throws An error if there is a DB error. + */ + getOldestUnverifiedL1TransactionBatch(): Promise } diff --git a/packages/rollup-core/src/types/data/l2-data-service.ts b/packages/rollup-core/src/types/data/l2-data-service.ts index 878ac0b7fc7..77f9716b0d8 100644 --- a/packages/rollup-core/src/types/data/l2-data-service.ts +++ b/packages/rollup-core/src/types/data/l2-data-service.ts @@ -9,4 +9,24 @@ export interface L2DataService { * @throws An error if there is a DB error. */ insertL2Transaction(transaction: TransactionAndRoot): Promise + + /** + * Builds an L2-only batch if there are unbatched L2 Transactions with different timestamps. + * + * @returns The number of the L2 Batch that was built, or -1 if one wasn't built. + */ + tryBuildL2OnlyBatch(): Promise + + /** + * Builds an L2 batch of the provided size matching the provided batch number + * if there are enough L2 transactions to support it. + * @param batchNumber The expected batch number + * @param batchSize The expected batch size + * @throws If there are multiple unbatched batches (based on timestamp) and the oldest is not + * at least `batchNumber` in size (our L1 & L2 batches don't match). + */ + tryBuildL2BatchToMatchL1( + batchNumber: number, + batchSize: number + ): Promise } diff --git a/packages/rollup-core/src/types/data/types.ts b/packages/rollup-core/src/types/data/types.ts new file mode 100644 index 00000000000..b086e4ef35e --- /dev/null +++ b/packages/rollup-core/src/types/data/types.ts @@ -0,0 +1,5 @@ +export interface L1BatchRecord { + blockTimestamp: number + batchNumber: number + batchSize: number +} diff --git a/packages/rollup-core/src/types/index.ts b/packages/rollup-core/src/types/index.ts index 025dc20cb2c..1dfeec6d060 100644 --- a/packages/rollup-core/src/types/index.ts +++ b/packages/rollup-core/src/types/index.ts @@ -2,7 +2,7 @@ export * from './data' export * from './errors' export * from './fraud-prover' -export * from './listeners' +export * from './l2-node-service' export * from './node-context' export * from './opcodes' export * from './state-machine' diff --git a/packages/rollup-core/src/types/l2-node-service.ts b/packages/rollup-core/src/types/l2-node-service.ts new file mode 100644 index 00000000000..36846ff48e7 --- /dev/null +++ b/packages/rollup-core/src/types/l2-node-service.ts @@ -0,0 +1,10 @@ +import { BlockBatches } from './types' + +export interface L2NodeService { + /** + * Sends the provided BlockBatches to the configured L2 node. + * + * @param blockBatches The block batches to send to L2 + */ + sendBlockBatches(blockBatches: BlockBatches): Promise +} diff --git a/packages/rollup-core/src/types/listeners.ts b/packages/rollup-core/src/types/listeners.ts deleted file mode 100644 index 35ecafed227..00000000000 --- a/packages/rollup-core/src/types/listeners.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { BlockBatches } from './types' - -/** - * Defines the event handler interface for handling L1 Block Batches. - */ -export interface BlockBatchListener { - handleBlockBatches(transactionBatch: BlockBatches): Promise -} diff --git a/packages/rollup-core/src/types/types.ts b/packages/rollup-core/src/types/types.ts index ce1c84de54e..3b6267684bd 100644 --- a/packages/rollup-core/src/types/types.ts +++ b/packages/rollup-core/src/types/types.ts @@ -70,6 +70,7 @@ export interface LogHandlerContext { export type L1Batch = RollupTransaction[] export interface BlockBatches { + batchNumber: number timestamp: number blockNumber: number batches: L1Batch[] diff --git a/packages/rollup-core/test/app/block-batch-submitter.spec.ts b/packages/rollup-core/test/app/block-batch-submitter.spec.ts deleted file mode 100644 index be462ee78f3..00000000000 --- a/packages/rollup-core/test/app/block-batch-submitter.spec.ts +++ /dev/null @@ -1,210 +0,0 @@ -// TODO: FIX THIS WHEN BATCH SUBMITTER IS UPDATED - -// import '../setup' -// -// /* External Imports */ -// import { hexStrToNumber, keccak256, TestUtils } from '@eth-optimism/core-utils' -// -// import { Wallet } from 'ethers' -// import { JsonRpcProvider } from 'ethers/providers' -// -// /* Internal Imports */ -// import { BlockBatches, RollupTransaction } from '../../src/types' -// import { BlockBatchSubmitter } from '../../src/app' -// import { verifyMessage } from 'ethers/utils' -// -// interface Payload { -// method: string -// params: any -// } -// -// class MockedProvider extends JsonRpcProvider { -// public readonly sent: Payload[] -// -// constructor() { -// super() -// this.sent = [] -// } -// -// public async send(method: string, params: any): Promise { -// this.sent.push({ method, params }) -// return 'dope.' -// } -// } -// -// const timestamp: number = 123 -// const timestamp2: number = 1234 -// -// const blockNumber: number = 0 -// const blockNumber2: number = 1 -// -// const nonce: number = 0 -// const gasLimit: number = 10_000 -// const sender: string = Wallet.createRandom().address -// const target: string = Wallet.createRandom().address -// const calldata: string = keccak256(Buffer.from('calldata').toString('hex')) -// const rollupTx: RollupTransaction = { -// gasLimit, -// nonce, -// sender, -// target: target, -// calldata, -// } -// -// const nonce2: number = 1 -// const gasLimit2: number = 20_000 -// const sender2: string = Wallet.createRandom().address -// const target2: string = Wallet.createRandom().address -// const calldata2: string = keccak256(Buffer.from('calldata 2').toString('hex')) -// const rollupTx2: RollupTransaction = { -// gasLimit: gasLimit2, -// nonce: nonce2, -// sender: sender2, -// target: target2, -// calldata: calldata2, -// } -// -// const rollupTxsEqual = ( -// one: RollupTransaction, -// two: RollupTransaction -// ): boolean => { -// return JSON.stringify(one) === JSON.stringify(two) -// } -// -// const deserializeBlockBatches = (serialized: string): BlockBatches => { -// return JSON.parse(serialized, (k, v) => { -// switch (k) { -// case 'blockNumber': -// case 'timestamp': -// case 'gasLimit': -// case 'nonce': -// return hexStrToNumber(v) -// default: -// return v -// } -// }) -// } -// -// describe('L2 Transaction Batch Submitter', () => { -// let blockBatchSubmitter: BlockBatchSubmitter -// let mockedSendProvider: MockedProvider -// let wallet: Wallet -// -// beforeEach(async () => { -// mockedSendProvider = new MockedProvider() -// wallet = Wallet.createRandom().connect(mockedSendProvider) -// blockBatchSubmitter = new BlockBatchSubmitter(wallet) -// }) -// -// it('should handle undefined batch properly', async () => { -// await TestUtils.assertThrowsAsync(async () => { -// await blockBatchSubmitter.handleBlockBatches(undefined) -// }) -// }) -// -// it('should handle batch with undefined transactions properly', async () => { -// await blockBatchSubmitter.handleBlockBatches({ -// timestamp, -// blockNumber, -// batches: undefined, -// }) -// -// mockedSendProvider.sent.length.should.equal( -// 0, -// 'Should not have sent anything!' -// ) -// }) -// -// it('should handle batch with empty transactions properly', async () => { -// await blockBatchSubmitter.handleBlockBatches({ -// timestamp, -// blockNumber, -// batches: [], -// }) -// -// mockedSendProvider.sent.length.should.equal( -// 0, -// 'Should not have sent anything!' -// ) -// }) -// -// it('should send single-tx batch properly', async () => { -// await blockBatchSubmitter.handleBlockBatches({ -// timestamp, -// blockNumber, -// batches: [[rollupTx]], -// }) -// -// mockedSendProvider.sent.length.should.equal(1, 'Should have sent tx!') -// mockedSendProvider.sent[0].method.should.equal( -// BlockBatchSubmitter.sendBlockBatchesMethod, -// 'Sent to incorrect Web3 method!' -// ) -// Array.isArray(mockedSendProvider.sent[0].params).should.equal( -// true, -// 'Incorrect params type!' -// ) -// const paramsArray = mockedSendProvider.sent[0].params as string[] -// paramsArray.length.should.equal(2, 'Incorrect params length') -// const [payloadStr, signature] = paramsArray -// -// const blockBatches: BlockBatches = deserializeBlockBatches(payloadStr) -// -// blockBatches.timestamp.should.equal(timestamp, 'Incorrect timestamp!') -// blockBatches.batches.length.should.equal(1, 'Incorrect num batches!') -// blockBatches.batches[0].length.should.equal(1, 'Incorrect num txs!') -// rollupTxsEqual(blockBatches.batches[0][0], rollupTx).should.equal( -// true, -// 'Incorrect transaction received!' -// ) -// -// verifyMessage(payloadStr, signature).should.equal( -// wallet.address, -// 'IncorrectSignature!' -// ) -// }) -// -// it('should send multi-tx batch properly', async () => { -// await blockBatchSubmitter.handleBlockBatches({ -// timestamp, -// blockNumber, -// batches: [[rollupTx, rollupTx2]], -// }) -// -// mockedSendProvider.sent.length.should.equal(1, 'Should have sent tx!') -// mockedSendProvider.sent[0].method.should.equal( -// BlockBatchSubmitter.sendBlockBatchesMethod, -// 'Sent to incorrect Web3 method!' -// ) -// Array.isArray(mockedSendProvider.sent[0].params).should.equal( -// true, -// 'Incorrect params type!' -// ) -// const paramsArray = mockedSendProvider.sent[0].params as string[] -// paramsArray.length.should.equal(2, 'Incorrect params length') -// const [payloadStr, signature] = paramsArray -// -// const blockBatches: BlockBatches = deserializeBlockBatches(payloadStr) -// -// blockBatches.timestamp.should.equal(timestamp, 'Incorrect timestamp!') -// blockBatches.batches.length.should.equal(1, 'Incorrect num batches!') -// blockBatches.batches[0].length.should.equal( -// 2, -// 'Incorrect num transactions!' -// ) -// rollupTxsEqual(blockBatches.batches[0][0], rollupTx).should.equal( -// true, -// 'Incorrect transaction received!' -// ) -// -// rollupTxsEqual(blockBatches.batches[0][1], rollupTx2).should.equal( -// true, -// 'Incorrect transaction 2 received!' -// ) -// -// verifyMessage(payloadStr, signature).should.equal( -// wallet.address, -// 'IncorrectSignature!' -// ) -// }) -// }) diff --git a/packages/rollup-core/test/app/l1-chain-data-persister.spec.ts b/packages/rollup-core/test/app/l1-chain-data-persister.spec.ts index 5ef1d3e36ae..7de44167233 100644 --- a/packages/rollup-core/test/app/l1-chain-data-persister.spec.ts +++ b/packages/rollup-core/test/app/l1-chain-data-persister.spec.ts @@ -15,14 +15,19 @@ import { } from 'ethers/providers' /* Internal Imports */ -import { L1ChainDataPersister, CHAIN_ID } from '../../src/app' +import { + L1ChainDataPersister, + CHAIN_ID, + DefaultDataService, +} from '../../src/app' import { LogHandlerContext, RollupTransaction, L1DataService, + L1BatchRecord, } from '../../src/types' -class MockDataService implements L1DataService { +class MockDataService extends DefaultDataService { public readonly blocks: Block[] = [] public readonly processedBlocks: Set = new Set() public readonly blockTransactions: Map @@ -30,6 +35,7 @@ class MockDataService implements L1DataService { public readonly rollupTransactions: Map constructor() { + super(undefined) this.blocks = [] this.processedBlocks = new Set() this.blockTransactions = new Map() @@ -37,14 +43,14 @@ class MockDataService implements L1DataService { this.rollupTransactions = new Map() } - public async insertBlock(block: Block, processed: boolean): Promise { + public async insertL1Block(block: Block, processed: boolean): Promise { this.blocks.push(block) if (processed) { this.processedBlocks.add(block.hash) } } - public async insertBlockAndTransactions( + public async insertL1BlockAndTransactions( block: Block, txs: TransactionResponse[], processed: boolean @@ -56,7 +62,7 @@ class MockDataService implements L1DataService { } } - public async insertRollupStateRoots( + public async insertL1RollupStateRoots( l1TxHash: string, stateRoots: string[] ): Promise { @@ -64,7 +70,7 @@ class MockDataService implements L1DataService { return this.stateRoots.size } - public async insertRollupTransactions( + public async insertL1RollupTransactions( l1TxHash: string, rollupTransactions: RollupTransaction[] ): Promise { @@ -72,12 +78,6 @@ class MockDataService implements L1DataService { return this.rollupTransactions.size } - public async insertTransactions( - transactions: TransactionResponse[] - ): Promise { - throw Error(`this shouldn't be called`) - } - public async updateBlockToProcessed(blockHash: string): Promise { this.processedBlocks.add(blockHash) } @@ -336,7 +336,7 @@ describe('L1 Chain Data Persister', () => { it('should persist block, transaction, and rollup transactions with relevant logs', async () => { const rollupTxs = [getRollupTransaction()] configuredHandlerContext.handleLog = async (ds, l, t) => { - await ds.insertRollupTransactions(t.hash, rollupTxs) + await ds.insertL1RollupTransactions(t.hash, rollupTxs) } const tx: TransactionResponse = getTransactionResponse() @@ -391,7 +391,7 @@ describe('L1 Chain Data Persister', () => { it('should persist block, transaction, and state roots with relevant logs', async () => { const stateRoots = [keccak256FromUtf8('root')] configuredHandlerContext.handleLog = async (ds, l, t) => { - await ds.insertRollupStateRoots(t.hash, stateRoots) + await ds.insertL1RollupStateRoots(t.hash, stateRoots) } const tx: TransactionResponse = getTransactionResponse() @@ -445,7 +445,7 @@ describe('L1 Chain Data Persister', () => { const rollupTxs = [getRollupTransaction()] const stateRoots = [keccak256FromUtf8('root')] configuredHandlerContext.handleLog = async (ds, l, t) => { - await ds.insertRollupStateRoots(t.hash, stateRoots) + await ds.insertL1RollupStateRoots(t.hash, stateRoots) } const topic2 = 'derp_derp' chainDataPersister = await L1ChainDataPersister.create( @@ -458,7 +458,7 @@ describe('L1 Chain Data Persister', () => { topic: topic2, contractAddress, handleLog: async (ds, l, t) => { - await ds.insertRollupTransactions(t.hash, rollupTxs) + await ds.insertL1RollupTransactions(t.hash, rollupTxs) }, }, ] @@ -531,7 +531,7 @@ describe('L1 Chain Data Persister', () => { const rollupTxs = [getRollupTransaction()] const stateRoots = [keccak256FromUtf8('root')] configuredHandlerContext.handleLog = async (ds, l, t) => { - await ds.insertRollupStateRoots(tx.hash, stateRoots) + await ds.insertL1RollupStateRoots(tx.hash, stateRoots) } const topic2 = 'derp_derp' chainDataPersister = await L1ChainDataPersister.create( @@ -544,7 +544,7 @@ describe('L1 Chain Data Persister', () => { topic: topic2, contractAddress, handleLog: async (ds, l, t) => { - await ds.insertRollupTransactions(t.hash, rollupTxs) + await ds.insertL1RollupTransactions(t.hash, rollupTxs) }, }, ] @@ -625,7 +625,7 @@ describe('L1 Chain Data Persister', () => { it('should only persist relevant block, transaction, and rollup transactions with relevant logs', async () => { const rollupTxs = [getRollupTransaction()] configuredHandlerContext.handleLog = async (ds, l, t) => { - await ds.insertRollupTransactions(tx.hash, rollupTxs) + await ds.insertL1RollupTransactions(tx.hash, rollupTxs) } const tx: TransactionResponse = getTransactionResponse() diff --git a/packages/rollup-core/test/app/l2-batch-creator.spec.ts b/packages/rollup-core/test/app/l2-batch-creator.spec.ts new file mode 100644 index 00000000000..6daf9822640 --- /dev/null +++ b/packages/rollup-core/test/app/l2-batch-creator.spec.ts @@ -0,0 +1,73 @@ +import { DefaultDataService, L2BatchCreator } from '../../src/app/data' +import { L1BatchRecord } from '../../src/types/data' + +class MockDataService extends DefaultDataService { + public l2OnlyBatchesBuilt: number = 0 + public l1MatchingBatchesBuilt: number = 0 + public unverifiedL1Batches: L1BatchRecord[] = [] + + constructor() { + super(undefined) + } + + public async getOldestUnverifiedL1TransactionBatch(): Promise { + if (this.unverifiedL1Batches.length > 0) { + return this.unverifiedL1Batches[0] + } + return undefined + } + + public async tryBuildL2OnlyBatch(): Promise { + this.l2OnlyBatchesBuilt++ + return + } + + public async tryBuildL2BatchToMatchL1( + l1BatchSize: number, + l1BatchNumber: number + ): Promise { + this.l1MatchingBatchesBuilt++ + return this.l1MatchingBatchesBuilt + } +} + +describe('L2 Batch Creator', () => { + let batchCreator: L2BatchCreator + let dataService: MockDataService + + beforeEach(async () => { + dataService = new MockDataService() + batchCreator = new L2BatchCreator(dataService) + }) + + it('should try to build L2 only batch when no unverified L1 batches exist', async () => { + await batchCreator.runTask() + + dataService.l2OnlyBatchesBuilt.should.equal( + 1, + `No L2 only batches should have been attempted!` + ) + dataService.l1MatchingBatchesBuilt.should.equal( + 0, + `No L1 matching batches should have been attempted!` + ) + }) + + it('should try to build a matching batch when there is an unverified L1 batch', async () => { + dataService.unverifiedL1Batches.push({ + batchNumber: 1, + batchSize: 2, + blockTimestamp: 3, + }) + await batchCreator.runTask() + + dataService.l2OnlyBatchesBuilt.should.equal( + 0, + `No L2 only batches should have been attempted!` + ) + dataService.l1MatchingBatchesBuilt.should.equal( + 1, + `1 L1 matching batche should have been attempted!` + ) + }) +}) diff --git a/packages/rollup-core/test/app/l2-batch-submitter.spec.ts b/packages/rollup-core/test/app/l2-batch-submitter.spec.ts new file mode 100644 index 00000000000..3eacfa9cf42 --- /dev/null +++ b/packages/rollup-core/test/app/l2-batch-submitter.spec.ts @@ -0,0 +1,108 @@ +/* External Imports */ +import { Wallet } from 'ethers' + +/* Internal Imports */ +import { DefaultDataService, L2BatchSubmitter } from '../../src/app/data' +import { DefaultL2NodeService } from '../../src/app' +import { BlockBatches } from '../../src/types' +import { keccak256FromUtf8 } from '@eth-optimism/core-utils/build' + +class MockL2NodeService extends DefaultL2NodeService { + public readonly sentBlockBatches: BlockBatches[] = [] + + constructor() { + super(Wallet.createRandom()) + } + + public async sendBlockBatches(blockBatches: BlockBatches): Promise { + this.sentBlockBatches.push(blockBatches) + } +} + +class MockL1DataService extends DefaultDataService { + public readonly blockBatchesToReturn: BlockBatches[] = [] + public readonly batchesMarkedSubmitted: number[] = [] + constructor() { + super(undefined) + } + + public async getNextBatchForL2Submission(): Promise { + return this.blockBatchesToReturn.shift() + } + + public async markL1BatchSubmittedToL2(batchNumber: number): Promise { + this.batchesMarkedSubmitted.push(batchNumber) + } +} + +describe('L2 Batch Submitter', () => { + let batchSubmitter: L2BatchSubmitter + let l1DatService: MockL1DataService + let l2NodeService: MockL2NodeService + + beforeEach(async () => { + l1DatService = new MockL1DataService() + l2NodeService = new MockL2NodeService() + batchSubmitter = new L2BatchSubmitter(l1DatService, l2NodeService) + }) + + it('should not submit batch if no fitting L1 batch exists', async () => { + await batchSubmitter.runTask() + + l1DatService.batchesMarkedSubmitted.length.should.equal( + 0, + `No Batches should have been marked as sent!` + ) + + l2NodeService.sentBlockBatches.length.should.equal( + 0, + `No Batches should have been sent!` + ) + }) + + it('should send a batch if a fitting one exists', async () => { + const blockBatches: BlockBatches = { + batchNumber: 1, + timestamp: 1, + blockNumber: 1, + batches: [ + [ + { + batchIndex: 1, + gasLimit: 0, + nonce: 0, + sender: Wallet.createRandom().address, + target: Wallet.createRandom().address, + calldata: keccak256FromUtf8('calldata'), + l1Timestamp: 1, + l1BlockNumber: 1, + l1TxHash: keccak256FromUtf8('tx hash'), + queueOrigin: 1, + }, + ], + ], + } + + l1DatService.blockBatchesToReturn.push(blockBatches) + await batchSubmitter.runTask() + + l2NodeService.sentBlockBatches.length.should.equal( + 1, + `1 BlockBatches object should have been submitted!` + ) + l2NodeService.sentBlockBatches[0].should.deep.equal( + blockBatches, + `Sent BlockBatches object doesn't match!` + ) + + l1DatService.batchesMarkedSubmitted.length.should.equal( + 1, + `1 batch should have been marked submitted!` + ) + + l1DatService.batchesMarkedSubmitted[0].should.equal( + 1, + `1 batch should have been marked submitted!` + ) + }) +}) diff --git a/packages/rollup-core/test/app/l2-chain-data-persister.spec.ts b/packages/rollup-core/test/app/l2-chain-data-persister.spec.ts index 294ec9a194a..180134661e7 100644 --- a/packages/rollup-core/test/app/l2-chain-data-persister.spec.ts +++ b/packages/rollup-core/test/app/l2-chain-data-persister.spec.ts @@ -23,6 +23,17 @@ class MockDataService implements L2DataService { public async insertL2Transaction(transaction: TransactionAndRoot) { this.transactionAndRoots.push(transaction) } + + public async tryBuildL2BatchToMatchL1( + batchNumber: number, + batchSize: number + ): Promise { + return undefined + } + + public async tryBuildL2OnlyBatch(): Promise { + return undefined + } } class MockProvider extends JsonRpcProvider { diff --git a/packages/rollup-core/test/app/l2-node-service.spec.ts b/packages/rollup-core/test/app/l2-node-service.spec.ts new file mode 100644 index 00000000000..2bab87ceead --- /dev/null +++ b/packages/rollup-core/test/app/l2-node-service.spec.ts @@ -0,0 +1,226 @@ +import '../setup' + +/* External Imports */ +import { + hexStrToNumber, + keccak256FromUtf8, + TestUtils, +} from '@eth-optimism/core-utils' + +import { Wallet } from 'ethers' +import { JsonRpcProvider } from 'ethers/providers' + +/* Internal Imports */ +import { BlockBatches, RollupTransaction } from '../../src/types' +import { DefaultL2NodeService } from '../../src/app' +import { verifyMessage } from 'ethers/utils' + +interface Payload { + method: string + params: any +} + +class MockedProvider extends JsonRpcProvider { + public readonly sent: Payload[] + + constructor() { + super() + this.sent = [] + } + + public async send(method: string, params: any): Promise { + this.sent.push({ method, params }) + return 'dope.' + } +} + +const timestamp: number = 123 +const timestamp2: number = 1234 + +const blockNumber: number = 0 +const blockNumber2: number = 1 + +const l1TxHash: string = keccak256FromUtf8('tx 1') +const batchNumber: number = 1 + +const nonce: number = 0 +const gasLimit: number = 10_000 +const sender: string = Wallet.createRandom().address +const target: string = Wallet.createRandom().address +const calldata: string = keccak256FromUtf8('calldata') +const rollupTx: RollupTransaction = { + batchIndex: 1, + gasLimit, + nonce, + sender, + target, + calldata, + l1Timestamp: timestamp, + l1BlockNumber: blockNumber, + l1TxHash, + queueOrigin: 1, +} + +const nonce2: number = 1 +const gasLimit2: number = 20_000 +const sender2: string = Wallet.createRandom().address +const target2: string = Wallet.createRandom().address +const calldata2: string = keccak256FromUtf8('calldata 2') +const rollupTx2: RollupTransaction = { + batchIndex: 2, + gasLimit: gasLimit2, + nonce: nonce2, + sender: sender2, + target: target2, + calldata: calldata2, + l1Timestamp: timestamp, + l1BlockNumber: blockNumber, + l1TxHash, + queueOrigin: 1, +} + +const deserializeBlockBatches = (serialized: string): BlockBatches => { + return JSON.parse(serialized, (k, v) => { + switch (k) { + case 'blockNumber': + case 'timestamp': + case 'gasLimit': + case 'nonce': + case 'batchIndex': + case 'l1BlockNumber': + case 'l1Timestamp': + case 'queueOrigin': + return hexStrToNumber(v) + default: + return v + } + }) +} + +describe('L2 Node Service', () => { + let l2NodeService: DefaultL2NodeService + let mockedSendProvider: MockedProvider + let wallet: Wallet + + beforeEach(async () => { + mockedSendProvider = new MockedProvider() + wallet = Wallet.createRandom().connect(mockedSendProvider) + l2NodeService = new DefaultL2NodeService(wallet) + }) + + it('should handle undefined batch properly', async () => { + await TestUtils.assertThrowsAsync(async () => { + await l2NodeService.sendBlockBatches(undefined) + }) + }) + + it('should handle batch with undefined transactions properly', async () => { + await l2NodeService.sendBlockBatches({ + batchNumber, + timestamp, + blockNumber, + batches: undefined, + }) + + mockedSendProvider.sent.length.should.equal( + 0, + 'Should not have sent anything!' + ) + }) + + it('should handle batch with empty transactions properly', async () => { + await l2NodeService.sendBlockBatches({ + batchNumber, + timestamp, + blockNumber, + batches: [], + }) + + mockedSendProvider.sent.length.should.equal( + 0, + 'Should not have sent anything!' + ) + }) + + it('should send single-tx batch properly', async () => { + await l2NodeService.sendBlockBatches({ + batchNumber, + timestamp, + blockNumber, + batches: [[rollupTx]], + }) + + mockedSendProvider.sent.length.should.equal(1, 'Should have sent tx!') + mockedSendProvider.sent[0].method.should.equal( + DefaultL2NodeService.sendBlockBatchesMethod, + 'Sent to incorrect Web3 method!' + ) + Array.isArray(mockedSendProvider.sent[0].params).should.equal( + true, + 'Incorrect params type!' + ) + const paramsArray = mockedSendProvider.sent[0].params as string[] + paramsArray.length.should.equal(2, 'Incorrect params length') + const [payloadStr, signature] = paramsArray + + const blockBatches: BlockBatches = deserializeBlockBatches(payloadStr) + + blockBatches.timestamp.should.equal(timestamp, 'Incorrect timestamp!') + blockBatches.batches.length.should.equal(1, 'Incorrect num batches!') + blockBatches.batches[0].length.should.equal(1, 'Incorrect num txs!') + blockBatches.batches[0][0].should.deep.equal( + rollupTx, + 'Incorrect transaction received!' + ) + + verifyMessage(payloadStr, signature).should.equal( + wallet.address, + 'IncorrectSignature!' + ) + }) + + it('should send multi-tx batch properly', async () => { + await l2NodeService.sendBlockBatches({ + batchNumber, + timestamp, + blockNumber, + batches: [[rollupTx, rollupTx2]], + }) + + mockedSendProvider.sent.length.should.equal(1, 'Should have sent tx!') + mockedSendProvider.sent[0].method.should.equal( + DefaultL2NodeService.sendBlockBatchesMethod, + 'Sent to incorrect Web3 method!' + ) + Array.isArray(mockedSendProvider.sent[0].params).should.equal( + true, + 'Incorrect params type!' + ) + const paramsArray = mockedSendProvider.sent[0].params as string[] + paramsArray.length.should.equal(2, 'Incorrect params length') + const [payloadStr, signature] = paramsArray + + const blockBatches: BlockBatches = deserializeBlockBatches(payloadStr) + + blockBatches.timestamp.should.equal(timestamp, 'Incorrect timestamp!') + blockBatches.batches.length.should.equal(1, 'Incorrect num batches!') + blockBatches.batches[0].length.should.equal( + 2, + 'Incorrect num transactions!' + ) + blockBatches.batches[0][0].should.deep.equal( + rollupTx, + 'Incorrect transaction received!' + ) + + blockBatches.batches[0][1].should.deep.equal( + rollupTx2, + 'Incorrect transaction 2 received!' + ) + + verifyMessage(payloadStr, signature).should.equal( + wallet.address, + 'IncorrectSignature!' + ) + }) +}) diff --git a/packages/state-synchronizer/exec/index.ts b/packages/state-synchronizer/exec/index.ts index b36b50bf7f7..5dde99530ec 100644 --- a/packages/state-synchronizer/exec/index.ts +++ b/packages/state-synchronizer/exec/index.ts @@ -1 +1 @@ -export * from './rollup-transaction-synchronizer' +// export * from './rollup-transaction-synchronizer' diff --git a/packages/state-synchronizer/exec/rollup-transaction-synchronizer.ts b/packages/state-synchronizer/exec/rollup-transaction-synchronizer.ts index 3fd6652e6f4..b4c71d78527 100644 --- a/packages/state-synchronizer/exec/rollup-transaction-synchronizer.ts +++ b/packages/state-synchronizer/exec/rollup-transaction-synchronizer.ts @@ -1,195 +1,197 @@ -/* External Imports */ -import { - BaseDB, - DB, - EthereumBlockProcessor, - getLevelInstance, - newInMemoryDB, -} from '@eth-optimism/core-db' -import { add0x, getLogger, logError } from '@eth-optimism/core-utils' -import { - Environment, - initializeL1Node, - L1NodeContext, - CHAIN_ID, - BlockBatchProcessor, -} from '@eth-optimism/rollup-core' - -import { JsonRpcProvider, Provider } from 'ethers/providers' -import * as fs from 'fs' -import * as rimraf from 'rimraf' -import { Wallet } from 'ethers' -import { getWallets } from 'ethereum-waffle' - -const log = getLogger('l1-block-batch-processor') - -export const runTest = async ( - l1Provider?: Provider, - l2Provider?: JsonRpcProvider -): Promise => { - return run(true, l1Provider, l2Provider) -} - -export const run = async ( - testMode: boolean = false, - l1Provider?: Provider, - l2Provider?: JsonRpcProvider -): Promise => { - initializeDBPaths(testMode) - - let l1NodeContext: L1NodeContext - log.info(`Attempting to connect to L1 Node.`) - try { - l1NodeContext = await initializeL1Node(true, l1Provider) - } catch (e) { - logError(log, 'Error connecting to L1 Node', e) - throw e - } - - let provider: JsonRpcProvider = l2Provider - if (!provider && !!Environment.l2NodeWeb3Url()) { - log.info(`Connecting to L2 web3 URL: ${Environment.l2NodeWeb3Url()}`) - provider = new JsonRpcProvider(Environment.l2NodeWeb3Url(), CHAIN_ID) - } - - return getL1BlockBatchProcessor(testMode, l1NodeContext, provider) -} - -/** - * Gets an BlockBatchProcessor based on configuration and the provided arguments. - * - * @param testMode Whether or not this is running as a test - * @param l1NodeContext The L1 node context. - * @param l2Provider The L2 JSON RPC Provider to use to communicate with the L2 node. - * @returns The BlockBatchProcessor. - */ -const getL1BlockBatchProcessor = async ( - testMode: boolean, - l1NodeContext: L1NodeContext, - l2Provider: JsonRpcProvider -): Promise => { - const db: DB = getDB(testMode) - - const blockBatchProcessor = await BlockBatchProcessor.create( - db, - l1NodeContext.provider, - [], // TODO: fill this in - [] // TODO: Fill this in - ) - - const earliestBlock = Environment.l1EarliestBlock() - - const blockProcessor = new EthereumBlockProcessor( - db, - earliestBlock, - Environment.blockBatchProcessorNumConfirmsRequired() - ) - await blockProcessor.subscribe( - l1NodeContext.provider, - blockBatchProcessor, - true - ) - - return blockBatchProcessor -} - -/** - * Gets the appropriate db for this node to use based on whether or not this is run in test mode. - * - * @param isTestMode Whether or not it is test mode. - * @returns The constructed DB instance. - */ -const getDB = (isTestMode: boolean = false): DB => { - if (isTestMode) { - return newInMemoryDB() - } else { - if (!Environment.blockBatchProcessorPersistentDbPath()) { - log.error( - `No L1_BLOCK_BATCH_PROCESSOR_PERSISTENT_DB_PATH environment variable present. Please set one!` - ) - process.exit(1) - } - - return new BaseDB( - getLevelInstance(Environment.blockBatchProcessorPersistentDbPath()) - ) - } -} - -/** - * Gets the wallet to use to interact with the L2 node. This may be configured via - * private key file specified through environment variables. If not it is assumed - * that a local test provider is being used, from which the wallet may be fetched. - * - * @param provider The provider with which the wallet will be associated. - * @returns The wallet to use with the L2 node. - */ -const getL2Wallet = (provider: JsonRpcProvider): Wallet => { - let wallet: Wallet - if (!!Environment.blockBatchProcessorPrivateKey()) { - wallet = new Wallet( - add0x(Environment.blockBatchProcessorPrivateKey()), - provider - ) - log.info( - `Initialized Block Batch Processor wallet from private key. Address: ${wallet.address}` - ) - } else { - wallet = getWallets(provider)[0] - log.info( - `Getting wallet from provider. First wallet private key: [${wallet.privateKey}` - ) - } - - if (!wallet) { - const msg: string = `Wallet not created! Specify the L1_BLOCK_BATCH_PROCESSOR_PRIVATE_KEY environment variable to set one!` - log.error(msg) - throw Error(msg) - } else { - log.info(`Block Batch Processor wallet created. Address: ${wallet.address}`) - } - - return wallet -} - -/** - * Initializes filesystem DB paths. This will also purge all data if the `CLEAR_DATA_KEY` has changed. - */ -const initializeDBPaths = (isTestMode: boolean) => { - if (isTestMode) { - return - } - - if (!fs.existsSync(Environment.l2RpcServerPersistentDbPath())) { - makeDataDirectory() - } else { - if (Environment.clearDataKey() && !fs.existsSync(getClearDataFilePath())) { - log.info(`Detected change in CLEAR_DATA_KEY. Purging data...`) - rimraf.sync(`${Environment.blockBatchProcessorPersistentDbPath()}/{*,.*}`) - log.info( - `L2 RPC Server data purged from '${Environment.blockBatchProcessorPersistentDbPath()}/{*,.*}'` - ) - makeDataDirectory() - } - } -} - -/** - * Makes the data directory for this full node and adds a clear data key file if it is configured to use one. - */ -const makeDataDirectory = () => { - fs.mkdirSync(Environment.blockBatchProcessorPersistentDbPath(), { - recursive: true, - }) - if (Environment.clearDataKey()) { - fs.writeFileSync(getClearDataFilePath(), '') - } -} - -const getClearDataFilePath = () => { - return `${Environment.blockBatchProcessorPersistentDbPath()}/.clear_data_key_${Environment.clearDataKey()}` -} - -if (typeof require !== 'undefined' && require.main === module) { - run() -} +// TODO: Redo this when we're ready to create executables + +// /* External Imports */ +// import { +// BaseDB, +// DB, +// EthereumBlockProcessor, +// getLevelInstance, +// newInMemoryDB, +// } from '@eth-optimism/core-db' +// import { add0x, getLogger, logError } from '@eth-optimism/core-utils' +// import { +// Environment, +// initializeL1Node, +// L1NodeContext, +// CHAIN_ID, +// BlockBatchProcessor, +// } from '@eth-optimism/rollup-core' +// +// import { JsonRpcProvider, Provider } from 'ethers/providers' +// import * as fs from 'fs' +// import * as rimraf from 'rimraf' +// import { Wallet } from 'ethers' +// import { getWallets } from 'ethereum-waffle' +// +// const log = getLogger('l1-block-batch-processor') +// +// export const runTest = async ( +// l1Provider?: Provider, +// l2Provider?: JsonRpcProvider +// ): Promise => { +// return run(true, l1Provider, l2Provider) +// } +// +// export const run = async ( +// testMode: boolean = false, +// l1Provider?: Provider, +// l2Provider?: JsonRpcProvider +// ): Promise => { +// initializeDBPaths(testMode) +// +// let l1NodeContext: L1NodeContext +// log.info(`Attempting to connect to L1 Node.`) +// try { +// l1NodeContext = await initializeL1Node(true, l1Provider) +// } catch (e) { +// logError(log, 'Error connecting to L1 Node', e) +// throw e +// } +// +// let provider: JsonRpcProvider = l2Provider +// if (!provider && !!Environment.l2NodeWeb3Url()) { +// log.info(`Connecting to L2 web3 URL: ${Environment.l2NodeWeb3Url()}`) +// provider = new JsonRpcProvider(Environment.l2NodeWeb3Url(), CHAIN_ID) +// } +// +// return getL1BlockBatchProcessor(testMode, l1NodeContext, provider) +// } +// +// /** +// * Gets an BlockBatchProcessor based on configuration and the provided arguments. +// * +// * @param testMode Whether or not this is running as a test +// * @param l1NodeContext The L1 node context. +// * @param l2Provider The L2 JSON RPC Provider to use to communicate with the L2 node. +// * @returns The BlockBatchProcessor. +// */ +// const getL1BlockBatchProcessor = async ( +// testMode: boolean, +// l1NodeContext: L1NodeContext, +// l2Provider: JsonRpcProvider +// ): Promise => { +// const db: DB = getDB(testMode) +// +// const blockBatchProcessor = await BlockBatchProcessor.create( +// db, +// l1NodeContext.provider, +// [], // TODO: fill this in +// [] // TODO: Fill this in +// ) +// +// const earliestBlock = Environment.l1EarliestBlock() +// +// const blockProcessor = new EthereumBlockProcessor( +// db, +// earliestBlock, +// Environment.blockBatchProcessorNumConfirmsRequired() +// ) +// await blockProcessor.subscribe( +// l1NodeContext.provider, +// blockBatchProcessor, +// true +// ) +// +// return blockBatchProcessor +// } +// +// /** +// * Gets the appropriate db for this node to use based on whether or not this is run in test mode. +// * +// * @param isTestMode Whether or not it is test mode. +// * @returns The constructed DB instance. +// */ +// const getDB = (isTestMode: boolean = false): DB => { +// if (isTestMode) { +// return newInMemoryDB() +// } else { +// if (!Environment.blockBatchProcessorPersistentDbPath()) { +// log.error( +// `No L1_BLOCK_BATCH_PROCESSOR_PERSISTENT_DB_PATH environment variable present. Please set one!` +// ) +// process.exit(1) +// } +// +// return new BaseDB( +// getLevelInstance(Environment.blockBatchProcessorPersistentDbPath()) +// ) +// } +// } +// +// /** +// * Gets the wallet to use to interact with the L2 node. This may be configured via +// * private key file specified through environment variables. If not it is assumed +// * that a local test provider is being used, from which the wallet may be fetched. +// * +// * @param provider The provider with which the wallet will be associated. +// * @returns The wallet to use with the L2 node. +// */ +// const getL2Wallet = (provider: JsonRpcProvider): Wallet => { +// let wallet: Wallet +// if (!!Environment.blockBatchProcessorPrivateKey()) { +// wallet = new Wallet( +// add0x(Environment.blockBatchProcessorPrivateKey()), +// provider +// ) +// log.info( +// `Initialized Block Batch Processor wallet from private key. Address: ${wallet.address}` +// ) +// } else { +// wallet = getWallets(provider)[0] +// log.info( +// `Getting wallet from provider. First wallet private key: [${wallet.privateKey}` +// ) +// } +// +// if (!wallet) { +// const msg: string = `Wallet not created! Specify the L1_BLOCK_BATCH_PROCESSOR_PRIVATE_KEY environment variable to set one!` +// log.error(msg) +// throw Error(msg) +// } else { +// log.info(`Block Batch Processor wallet created. Address: ${wallet.address}`) +// } +// +// return wallet +// } +// +// /** +// * Initializes filesystem DB paths. This will also purge all data if the `CLEAR_DATA_KEY` has changed. +// */ +// const initializeDBPaths = (isTestMode: boolean) => { +// if (isTestMode) { +// return +// } +// +// if (!fs.existsSync(Environment.l2RpcServerPersistentDbPath())) { +// makeDataDirectory() +// } else { +// if (Environment.clearDataKey() && !fs.existsSync(getClearDataFilePath())) { +// log.info(`Detected change in CLEAR_DATA_KEY. Purging data...`) +// rimraf.sync(`${Environment.blockBatchProcessorPersistentDbPath()}/{*,.*}`) +// log.info( +// `L2 RPC Server data purged from '${Environment.blockBatchProcessorPersistentDbPath()}/{*,.*}'` +// ) +// makeDataDirectory() +// } +// } +// } +// +// /** +// * Makes the data directory for this full node and adds a clear data key file if it is configured to use one. +// */ +// const makeDataDirectory = () => { +// fs.mkdirSync(Environment.blockBatchProcessorPersistentDbPath(), { +// recursive: true, +// }) +// if (Environment.clearDataKey()) { +// fs.writeFileSync(getClearDataFilePath(), '') +// } +// } +// +// const getClearDataFilePath = () => { +// return `${Environment.blockBatchProcessorPersistentDbPath()}/.clear_data_key_${Environment.clearDataKey()}` +// } +// +// if (typeof require !== 'undefined' && require.main === module) { +// run() +// }