diff --git a/.github/workflows/dev-ecr-deploy.yml b/.github/workflows/dev-ecr-deploy.yml index 3fa04c21cc94a..416ca7aa9c348 100644 --- a/.github/workflows/dev-ecr-deploy.yml +++ b/.github/workflows/dev-ecr-deploy.yml @@ -1,54 +1,55 @@ -name: Build & Tag Container, Push to ECR, Deploy to Dev - -on: - push: - branches: - - master - -jobs: - build: - name: Build, Tag & push to ECR, Deploy task - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Setup node - uses: actions/setup-node@v1 - - - name: Install Dependencies - run: yarn install - - - name: Build - run: | - yarn clean - yarn build - - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_CI_USER_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_CI_USER_SECRET_ACCESS_KEY }} - aws-region: us-east-2 - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - - - name: Build, tag, and push image to Amazon ECR - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - ECR_REPOSITORY: optimism/rollup-full-node - IMAGE_TAG: latest - run: | - docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - - - name: Stop existing dev-rollup-full-node ECS task to auto-start task with new image - run: | - ./.github/scripts/stop-ecs-task.sh dev-full-node full-node - ./.github/scripts/stop-ecs-task.sh synthetix-dev-full-node full-node - - - - name: Logout of Amazon ECR - if: always() - run: docker logout ${{ steps.login-ecr.outputs.registry }} +# TODO: Uncomment when Dev works +#name: Build & Tag Container, Push to ECR, Deploy to Dev +# +#on: +# push: +# branches: +# - master +# +#jobs: +# build: +# name: Build, Tag & push to ECR, Deploy task +# runs-on: ubuntu-latest +# +# steps: +# - uses: actions/checkout@v2 +# - name: Setup node +# uses: actions/setup-node@v1 +# +# - name: Install Dependencies +# run: yarn install +# +# - name: Build +# run: | +# yarn clean +# yarn build +# +# - name: Configure AWS Credentials +# uses: aws-actions/configure-aws-credentials@v1 +# with: +# aws-access-key-id: ${{ secrets.AWS_CI_USER_ACCESS_KEY_ID }} +# aws-secret-access-key: ${{ secrets.AWS_CI_USER_SECRET_ACCESS_KEY }} +# aws-region: us-east-2 +# +# - name: Login to Amazon ECR +# id: login-ecr +# uses: aws-actions/amazon-ecr-login@v1 +# +# - name: Build, tag, and push image to Amazon ECR +# env: +# ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} +# ECR_REPOSITORY: optimism/rollup-full-node +# IMAGE_TAG: latest +# run: | +# docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . +# docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG +# +# - name: Stop existing dev-rollup-full-node ECS task to auto-start task with new image +# run: | +# ./.github/scripts/stop-ecs-task.sh dev-full-node full-node +# ./.github/scripts/stop-ecs-task.sh synthetix-dev-full-node full-node +# +# +# - name: Logout of Amazon ECR +# if: always() +# run: docker logout ${{ steps.login-ecr.outputs.registry }} diff --git a/packages/core-utils/src/app/constants.ts b/packages/core-utils/src/app/constants.ts index aab266df5fd2c..f56a2749956e8 100644 --- a/packages/core-utils/src/app/constants.ts +++ b/packages/core-utils/src/app/constants.ts @@ -1 +1,2 @@ export const ZERO_ADDRESS = '0x' + '00'.repeat(20) +export const INVALID_ADDRESS = '0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD' diff --git a/packages/core-utils/src/app/crypto.ts b/packages/core-utils/src/app/crypto.ts index 0b0d4f15886f3..1ee42ce1fa965 100644 --- a/packages/core-utils/src/app/crypto.ts +++ b/packages/core-utils/src/app/crypto.ts @@ -1,6 +1,12 @@ /* External Imports */ import { Md5 } from 'ts-md5' import { ethers } from 'ethers' +import { TransactionRequest } from 'ethers/providers/abstract-provider' +import { + joinSignature, + resolveProperties, + serializeTransaction, +} from 'ethers/utils' /* Internal Imports */ import { HashAlgorithm, HashFunction } from '../types' @@ -54,3 +60,36 @@ export const hashFunctionFor = (algo: HashAlgorithm): HashFunction => { throw Error(`HashAlgorithm ${algo} not supported.`) } } + +/** + * Gets the tx signer address from the Tx Request and r, s, v. + * + * @param tx The Transaction Request. + * @param r The r parameter of the signature. + * @param s The s parameter of the signature. + * @param v The v parameter of the signature. + * @returns The signer's address. + */ +export const getTxSigner = async ( + tx: TransactionRequest, + r: string, + s: string, + v: number +): Promise => { + const txHash: string = ethers.utils.keccak256( + serializeTransaction(await resolveProperties(tx)) + ) + + try { + return ethers.utils.recoverAddress( + ethers.utils.arrayify(txHash), + joinSignature({ + s: add0x(s), + r: add0x(r), + v, + }) + ) + } catch (e) { + return undefined + } +} diff --git a/packages/core-utils/src/app/log.ts b/packages/core-utils/src/app/log.ts index bf316467a9464..2b562d315c096 100644 --- a/packages/core-utils/src/app/log.ts +++ b/packages/core-utils/src/app/log.ts @@ -1,7 +1,7 @@ import debug from 'debug' import { Logger } from '../types' -export const LOG_NEWLINE_STRING = ' <\\n> ' +export const LOG_NEWLINE_STRING = process.env.LOG_NEW_LINES ? '\n' : ' <\\n> ' /** * Gets a logger specific to the provided identifier. diff --git a/packages/core-utils/src/app/misc.ts b/packages/core-utils/src/app/misc.ts index 26597ce5a9703..fd80c275d5509 100644 --- a/packages/core-utils/src/app/misc.ts +++ b/packages/core-utils/src/app/misc.ts @@ -80,6 +80,9 @@ export const sleep = (ms: number): Promise => { * @returns the string without "0x". */ export const remove0x = (str: string): string => { + if (str === undefined) { + return str + } return str.startsWith('0x') ? str.slice(2) : str } @@ -89,6 +92,9 @@ export const remove0x = (str: string): string => { * @returns the string with "0x". */ export const add0x = (str: string): string => { + if (str === undefined) { + return str + } return str.startsWith('0x') ? str : '0x' + str } @@ -162,12 +168,20 @@ export const bnToHexString = (bn: BigNumber): string => { } /** - * Converts a JavaScript number to a hex string. + * Converts a JavaScript number to a big-endian hex string. * @param number the JavaScript number to be converted. + * @param padToBytes the number of numeric bytes the resulting string should be, -1 if no padding should be done. * @returns the JavaScript number as a string. */ -export const numberToHexString = (number: number): string => { - return add0x(number.toString(16)) +export const numberToHexString = ( + number: number, + padToBytes: number = -1 +): string => { + let str = number.toString(16) + if (padToBytes > 0 || str.length < padToBytes * 2) { + str = `${'0'.repeat(padToBytes * 2 - str.length)}${str}` + } + return add0x(str) } /** diff --git a/packages/rollup-core/src/app/data/consumers/index.ts b/packages/rollup-core/src/app/data/consumers/index.ts index ff39044bb38e3..7e3a4489a5315 100644 --- a/packages/rollup-core/src/app/data/consumers/index.ts +++ b/packages/rollup-core/src/app/data/consumers/index.ts @@ -1,3 +1,4 @@ +export * from './l1-batch-submitter' export * from './l2-batch-creator' export * from './l2-batch-submitter' export * from './verifier' diff --git a/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts b/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts new file mode 100644 index 0000000000000..1f5cb0755ee8e --- /dev/null +++ b/packages/rollup-core/src/app/data/consumers/l1-batch-submitter.ts @@ -0,0 +1,309 @@ +/* External Imports */ +import { + getLogger, + logError, + numberToHexString, + remove0x, + ScheduledTask, +} from '@eth-optimism/core-utils' +import { Contract, Wallet } from 'ethers' + +/* Internal Imports */ +import { + L1BatchSubmission, + L2BatchStatus, + L2DataService, +} from '../../../types/data' +import { TransactionReceipt, TransactionResponse } from 'ethers/providers' + +const log = getLogger('l2-batch-creator') + +/** + * Polls the DB for L2 batches ready to send to L1 and submits them. + */ +export class L1BatchSubmitter extends ScheduledTask { + constructor( + private readonly dataService: L2DataService, + private readonly canonicalTransactionChain: Contract, + private readonly stateCommitmentChain: Contract, + private readonly confirmationsUntilFinal: number = 1, + periodMilliseconds = 10_000 + ) { + super(periodMilliseconds) + } + + /** + * @inheritDoc + * + * Submits L2 batches from L2 Transactions in the DB whenever there is a batch that is ready. + * + */ + public async runTask(): Promise { + let l2Batch: L1BatchSubmission + try { + l2Batch = await this.dataService.getNextBatchForL1Submission() + } catch (e) { + logError(log, `Error fetching batch for L1 submission! Continuing...`, e) + return + } + + if (!l2Batch || !l2Batch.transactions || !l2Batch.transactions.length) { + log.debug(`No batches found for L1 submission.`) + return + } + + let txBatchTxHash: string = l2Batch.l1TxBatchTxHash + let rootBatchTxHash: string = l2Batch.l1StateRootBatchTxHash + switch (l2Batch.status) { + case L2BatchStatus.BATCHED: + txBatchTxHash = await this.buildAndSendRollupBatchTransaction(l2Batch) + if (!txBatchTxHash) { + return + } + // Fallthrough on purpose -- this is a workflow + case L2BatchStatus.TXS_SUBMITTED: + await this.waitForTxBatchConfirms(txBatchTxHash, l2Batch.l2BatchNumber) + // Fallthrough on purpose -- this is a workflow + case L2BatchStatus.TXS_CONFIRMED: + rootBatchTxHash = await this.buildAndSendStateRootBatchTransaction( + l2Batch + ) + if (!rootBatchTxHash) { + return + } + // Fallthrough on purpose -- this is a workflow + case L2BatchStatus.ROOTS_SUBMITTED: + await this.waitForStateRootBatchConfirms( + rootBatchTxHash, + l2Batch.l2BatchNumber + ) + break + default: + log.error( + `Received L1 Batch submission in unexpected state: ${l2Batch.status}!` + ) + break + } + } + + /** + * Builds and sends a Rollup Batch transaction to L1, returning its tx hash. + * + * @param l2Batch The L2 batch to send to L1. + * @returns The L1 tx hash. + */ + private async buildAndSendRollupBatchTransaction( + l2Batch: L1BatchSubmission + ): Promise { + let txHash: string + try { + const txsCalldata: string[] = this.getTransactionBatchCalldata(l2Batch) + + const timestamp = l2Batch.transactions[0].timestamp + log.debug( + `Submitting tx batch ${ + l2Batch.l2BatchNumber + } to canonical chain. Batch: ${JSON.stringify( + l2Batch + )}, txs bytes: ${JSON.stringify(txsCalldata)}, timestamp: ${timestamp}` + ) + const txRes: TransactionResponse = await this.canonicalTransactionChain.appendSequencerBatch( + txsCalldata, + timestamp + ) + log.debug( + `Tx batch ${l2Batch.l2BatchNumber} appended with at least one confirmation! Tx Hash: ${txRes.hash}` + ) + txHash = txRes.hash + } catch (e) { + logError( + log, + `Error submitting tx batch ${l2Batch.l2BatchNumber} to canonical chain!`, + e + ) + return undefined + } + + try { + log.debug(`Marking tx batch ${l2Batch.l2BatchNumber} submitted`) + await this.dataService.markTransactionBatchSubmittedToL1( + l2Batch.l2BatchNumber, + txHash + ) + } catch (e) { + logError( + log, + `Error marking tx batch ${l2Batch.l2BatchNumber} as submitted!`, + e + ) + // TODO: Should we return here? Don't want to resubmit, so I think we should update the DB + } + return txHash + } + + /** + * Waits for the configured number of confirms for the provided rollup tx transaction hash and + * marks the tx as + * + * @param txHash The tx hash to wait for. + * @param batchNumber The rollup batch number in question. + */ + private async waitForTxBatchConfirms( + txHash: string, + batchNumber: number + ): Promise { + if (this.confirmationsUntilFinal > 1) { + try { + log.debug( + `Waiting for ${this.confirmationsUntilFinal} confirmations before treating tx batch ${batchNumber} submission as final.` + ) + const receipt: TransactionReceipt = await this.canonicalTransactionChain.provider.waitForTransaction( + txHash, + this.confirmationsUntilFinal + ) + log.debug(`Batch submission finalized for tx batch ${batchNumber}!`) + } catch (e) { + logError( + log, + `Error waiting for necessary block confirmations until final!`, + e + ) + // TODO: Should we return here? Don't want to resubmit, so I think we should update the DB + } + } + + try { + log.debug(`Marking tx batch ${batchNumber} confirmed!`) + await this.dataService.markTransactionBatchConfirmedOnL1( + batchNumber, + txHash + ) + log.debug(`Tx batch ${batchNumber} marked confirmed!`) + } catch (e) { + logError(log, `Error marking tx batch ${batchNumber} as confirmed!`, e) + } + } + + /** + * Builds and sends a State Root Batch transaction to L1, returning its tx hash. + * + * @param l2Batch The l2 batch from which state roots may be retrieved. + * @returns The L1 tx hash. + */ + private async buildAndSendStateRootBatchTransaction( + l2Batch: L1BatchSubmission + ): Promise { + let txHash: string + try { + const stateRoots: string[] = l2Batch.transactions.map((x) => x.stateRoot) + + log.debug( + `Submitting state root batch ${ + l2Batch.l2BatchNumber + } to state commitment chain. State roots: ${JSON.stringify(stateRoots)}` + ) + const stateRootRes: TransactionResponse = await this.stateCommitmentChain.appendStateBatch( + stateRoots + ) + log.debug( + `State batch ${l2Batch.l2BatchNumber} appended with at least one confirmation! Tx Hash: ${stateRootRes.hash}` + ) + txHash = stateRootRes.hash + } catch (e) { + logError( + log, + `Error submitting state root batch ${l2Batch.l2BatchNumber} to state commitment chain!`, + e + ) + return undefined + } + + try { + log.debug(`Marking state root batch ${l2Batch.l2BatchNumber} submitted`) + await this.dataService.markStateRootBatchSubmittedToL1( + l2Batch.l2BatchNumber, + txHash + ) + } catch (e) { + logError( + log, + `Error marking state root batch ${l2Batch.l2BatchNumber} as submitted!`, + e + ) + // TODO: Should we return here? Don't want to resubmit, so I think we should update the DB + } + return txHash + } + + /** + * Waits for the configured number of confirms for the provided rollup tx transaction hash and + * marks the tx as + * + * @param txHash The tx hash to wait for. + * @param batchNumber The rollup batch number in question. + */ + private async waitForStateRootBatchConfirms( + txHash: string, + batchNumber: number + ): Promise { + if (this.confirmationsUntilFinal > 1) { + try { + log.debug( + `Waiting for ${this.confirmationsUntilFinal} confirmations before treating state root batch ${batchNumber} submission as final.` + ) + const receipt: TransactionReceipt = await this.stateCommitmentChain.provider.waitForTransaction( + txHash, + this.confirmationsUntilFinal + ) + log.debug( + `State root batch submission finalized for batch ${batchNumber}!` + ) + } catch (e) { + logError( + log, + `Error waiting for necessary block confirmations until final!`, + e + ) + // TODO: Should we return here? Don't want to resubmit, so I think we should update the DB + } + } + + try { + log.debug(`Marking state root batch ${batchNumber} confirmed!`) + await this.dataService.markStateRootBatchConfirmedOnL1( + batchNumber, + txHash + ) + log.debug(`State root batch ${batchNumber} marked confirmed!`) + } catch (e) { + logError(log, `Error marking batch ${batchNumber} as confirmed!`, e) + } + } + + /** + * Gets the calldata bytes for a transaction batch to be submitted by the sequencer. + * Rollup Transaction Format: + * target: 20-byte address 0-20 + * nonce: 32-byte uint 20-52 + * gasLimit: 32-byte uint 52-84 + * signature: 65-byte bytes 84-149 + * calldata: bytes 149-end + * + * @param batch The batch to turn into ABI-encoded calldata bytes. + * @returns The ABI-encoded bytes[] of the Rollup Transactions in the format listed above. + */ + private getTransactionBatchCalldata(batch: L1BatchSubmission): string[] { + const txs: string[] = [] + for (const tx of batch.transactions) { + const nonce: string = remove0x(numberToHexString(tx.nonce, 32)) + const gasLimit: string = tx.gasLimit + ? tx.gasLimit.toString('hex', 64) + : '00'.repeat(32) + const signature: string = remove0x(tx.signature) + const calldata: string = remove0x(tx.calldata) + txs.push(`${tx.to}${nonce}${gasLimit}${signature}${calldata}`) + } + + return txs + } +} diff --git a/packages/rollup-core/src/app/data/data-service.ts b/packages/rollup-core/src/app/data/data-service.ts index b708ea065f7c9..773c0f85189e0 100644 --- a/packages/rollup-core/src/app/data/data-service.ts +++ b/packages/rollup-core/src/app/data/data-service.ts @@ -1,6 +1,6 @@ /* External Imports */ import { RDB, Row } from '@eth-optimism/core-db' -import { getLogger, logError } from '@eth-optimism/core-utils' +import { add0x, getLogger, logError } from '@eth-optimism/core-utils' import { Block, TransactionResponse } from 'ethers/providers' @@ -9,6 +9,9 @@ import { BlockBatches, DataService, L1BatchRecord, + L1BatchSubmission, + L2BatchStatus, + QueueOrigin, RollupTransaction, TransactionAndRoot, VerificationCandidate, @@ -88,22 +91,26 @@ export class DefaultDataService implements DataService { */ public async insertL1RollupTransactions( l1TxHash: string, - rollupTransactions: RollupTransaction[] + rollupTransactions: RollupTransaction[], + createBatch: boolean = false ): Promise { if (!rollupTransactions || !rollupTransactions.length) { return } - let batchNumber + let batchNumber: number await this.rdb.startTransaction() try { - batchNumber = await this.insertNewL1TransactionBatch( - rollupTransactions[0].l1TxHash - ) + if (createBatch) { + batchNumber = await this.insertNewL1TransactionBatch( + rollupTransactions[0].l1TxHash + ) + } const values: string[] = rollupTransactions.map( (x) => `(${getL1RollupTransactionInsertValue(x, batchNumber)})` ) + await this.rdb.execute( `${l1RollupTxInsertStatement} VALUES ${values.join(',')}` ) @@ -122,6 +129,86 @@ export class DefaultDataService implements DataService { } } + /** + * @inheritDoc + */ + public async createNextL1ToL2Batch(): Promise { + const txHashRes = await this.rdb.select(` + SELECT l1_tx_hash, l1_tx_log_index + FROM rollup_tx + WHERE batch_number IS NULL AND queue_origin = ${QueueOrigin.L1_TO_L2_QUEUE} + ORDER BY l1_block_number ASC, l1_tx_index ASC, l1_tx_log_index ASC + LIMIT 1 + `) + if (!txHashRes || !txHashRes.length || !txHashRes[0].columns['tx_hash']) { + return undefined + } + + const txHash = txHashRes[0].columns['l1_tx_hash'] + const txLogIndex = txHashRes[0].columns['l1_tx_log_index'] + + await this.rdb.startTransaction() + try { + const batchNumber: number = await this.insertNewL1TransactionBatch(txHash) + + await this.rdb.execute(` + UPDATE rollup_tx + SET batch_number = ${batchNumber}, batch_index = 0 + WHERE l1_tx_hash = '${txHash}' AND l1_tx_log_index = ${txLogIndex} + `) + + return batchNumber + } catch (e) { + logError( + log, + `Error executing createNextL1ToL2Batch for tx hash ${txHash}... rolling back`, + e + ) + await this.rdb.rollback() + throw e + } + } + + /** + * @inheritDoc + */ + public async createNextSafetyQueueBatch(): Promise { + const txHashRes = await this.rdb.select(` + SELECT l1_tx_hash, l1_tx_log_index + FROM rollup_tx + WHERE batch_number IS NULL AND queue_origin = ${QueueOrigin.SAFETY_QUEUE} + ORDER BY l1_block_number ASC, l1_tx_index ASC, l1_tx_log_index ASC + LIMIT 1 + `) + if (!txHashRes || !txHashRes.length || !txHashRes[0].columns['tx_hash']) { + return undefined + } + + const txHash = txHashRes[0].columns['l1_tx_hash'] + const txLogIndex = txHashRes[0].columns['l1_tx_log_index'] + + await this.rdb.startTransaction() + try { + const batchNumber: number = await this.insertNewL1TransactionBatch(txHash) + + await this.rdb.execute(` + UPDATE rollup_tx + SET batch_number = ${batchNumber}, batch_index = 0 + WHERE l1_tx_hash = '${txHash}' AND l1_tx_log_index = ${txLogIndex} + `) + + return batchNumber + } catch (e) { + logError( + log, + `Error executing createNextSafetyQueueBatch for tx hash ${txHash}... rolling back`, + e + ) + await this.rdb.rollback() + throw e + } + } + /** * @inheritDoc */ @@ -184,7 +271,7 @@ export class DefaultDataService implements DataService { */ 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 + SELECT batch_number, target, calldata, block_timestamp, block_number, l1_tx_hash, l1_tx_index, l1_tx_log_index, queue_origin, sender, l1_message_sender, gas_limit, nonce, signature FROM next_l2_submission_batch `) @@ -204,11 +291,13 @@ export class DefaultDataService implements DataService { res.map((row: Row, batchIndex: number) => { const tx: RollupTransaction = { batchIndex, + l1TxHash: row.columns['l1_tx_hash'], + l1TxIndex: row.columns['l1_tx_index'], + l1TxLogIndex: row.columns['l1_tx_log_index'], 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'], } @@ -276,7 +365,7 @@ export class DefaultDataService implements DataService { const timestampRes = await this.rdb.select( `SELECT DISTINCT block_timestamp FROM l2_tx - WHERE status = 'UNBATCHED' + WHERE status = '${L2BatchStatus.UNBATCHED}' ORDER BY block_timestamp ASC ` ) @@ -292,8 +381,8 @@ export class DefaultDataService implements DataService { 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} + SET status = '${L2BatchStatus.BATCHED}', batch_number = ${batchNumber} + WHERE status = '${L2BatchStatus.UNBATCHED}' AND block_timestamp = ${batchTimestamp} `) await this.rdb.commit() @@ -320,7 +409,7 @@ export class DefaultDataService implements DataService { const transactionsToBatchRes = await this.rdb.select(` SELECT COUNT(*) as batchable_tx_count, block_timestamp FROM l2_tx - WHERE status = 'UNBATCHED' + WHERE status = '${L2BatchStatus.UNBATCHED}' GROUP BY block_timestamp ORDER BY block_timestamp ASC `) @@ -357,11 +446,11 @@ export class DefaultDataService implements DataService { } await this.rdb.execute(` UPDATE l2_tx l - SET l.status = 'BATCHED', l.batch_number = ${batchNumber} + SET l.status = '${L2BatchStatus.BATCHED}', l.batch_number = ${batchNumber} FROM ( SELECT * FROM l2_tx - WHERE status = 'UNBATCHED' + WHERE status = '${L2BatchStatus.UNBATCHED}' LIMIT ${l1BatchSize} ) t WHERE l.id = t.id @@ -379,6 +468,106 @@ export class DefaultDataService implements DataService { } } + /** + * @inheritDoc + */ + public async getNextBatchForL1Submission(): Promise { + const res = await this.rdb.select(` + SELECT b.batch_number, b.status, b.tx_batch_tx_hash, b.state_batch_tx_hash, tx.block_number, tx.block_timestamp, tx.tx_index, tx.tx_hash, tx.sender, tx.l1_message_sender, tx.target, tx.calldata, tx.nonce, tx.signature, tx.state_root + FROM l2_tx tx + INNER JOIN l2_tx_batch b + WHERE b.status = '${L2BatchStatus.BATCHED}' + ORDER BY block_number ASC, tx_index ASC + `) + + if (!res || !res.length || !res[0].data.length) { + return undefined + } + + const batch: L1BatchSubmission = { + l1TxBatchTxHash: res[0].columns['tx_batch_tx_hash'], + l1StateRootBatchTxHash: res[0].columns['state_batch_tx_hash'], + status: res[0].columns['status'], + l2BatchNumber: res[0].columns['batch_number'], + transactions: [], + } + for (const row of res) { + batch.transactions.push({ + timestamp: row.columns['block_timestamp'], + blockNumber: row.columns['block_number'], + transactionIndex: row.columns['tx_index'], + transactionHash: row.columns['tx_hash'], + to: row.columns['target'], + from: row.columns['sender'], + nonce: row.columns['nonce'], + calldata: row.columns['calldata'], + stateRoot: row.columns['state_root'], + gasPrice: row.columns['gas_price'], + gasLimit: row.columns['gas_limit'], + l1MessageSender: row.columns['l1_message_sender'], // should never be present in this case + signature: row.columns['signature'], + }) + } + + return batch + } + + /** + * @inheritDoc + */ + public async markTransactionBatchSubmittedToL1( + batchNumber: number, + l1TxHash: string + ): Promise { + return this.rdb.execute(` + UPDATE l2_tx_batch + SET status = '${L2BatchStatus.TXS_SUBMITTED}', tx_batch_tx_hash = '${l1TxHash}' + WHERE batch_number = ${batchNumber} + `) + } + + /** + * @inheritDoc + */ + public async markTransactionBatchConfirmedOnL1( + batchNumber: number, + l1TxHash: string + ): Promise { + return this.rdb.execute(` + UPDATE l2_tx_batch + SET status = '${L2BatchStatus.TXS_CONFIRMED}', tx_batch_tx_hash = '${l1TxHash}' + WHERE batch_number = ${batchNumber} + `) + } + + /** + * @inheritDoc + */ + public async markStateRootBatchSubmittedToL1( + batchNumber: number, + l1TxHash: string + ): Promise { + return this.rdb.execute(` + UPDATE l2_tx_batch + SET status = '${L2BatchStatus.ROOTS_SUBMITTED}', state_batch_tx_hash = '${l1TxHash}' + WHERE batch_number = ${batchNumber} + `) + } + + /** + * @inheritDoc + */ + public async markStateRootBatchConfirmedOnL1( + batchNumber: number, + l1TxHash: string + ): Promise { + return this.rdb.execute(` + UPDATE l2_tx_batch + SET status = '${L2BatchStatus.ROOTS_CONFIRMED}', state_batch_tx_hash = '${l1TxHash}' + WHERE batch_number = ${batchNumber} + `) + } + /************ * VERIFIER * ************/ diff --git a/packages/rollup-core/src/app/data/index.ts b/packages/rollup-core/src/app/data/index.ts index 54c30899e583e..e14cc384bacf3 100644 --- a/packages/rollup-core/src/app/data/index.ts +++ b/packages/rollup-core/src/app/data/index.ts @@ -1,4 +1,4 @@ -export * from './consumers' -export * from './producers' +export * from './consumers/' +export * from './producers/' export * from './data-service' diff --git a/packages/rollup-core/src/app/data/producers/index.ts b/packages/rollup-core/src/app/data/producers/index.ts index 1eef20ee6cb78..801fc99dd8fd7 100644 --- a/packages/rollup-core/src/app/data/producers/index.ts +++ b/packages/rollup-core/src/app/data/producers/index.ts @@ -1,3 +1,4 @@ export * from './chain-data-processor' export * from './l1-chain-data-persister' export * from './l2-chain-data-persister' +export * from './log-handlers' diff --git a/packages/rollup-core/src/app/data/producers/log-handlers.ts b/packages/rollup-core/src/app/data/producers/log-handlers.ts new file mode 100644 index 0000000000000..c6d8a7df04fcd --- /dev/null +++ b/packages/rollup-core/src/app/data/producers/log-handlers.ts @@ -0,0 +1,370 @@ +/* External Imports */ +import { + add0x, + BigNumber, + getLogger, + getTxSigner, + logError, + remove0x, +} from '@eth-optimism/core-utils' +import { + Log, + TransactionRequest, + TransactionResponse, +} from 'ethers/providers/abstract-provider' +import { ethers } from 'ethers' + +/* Internal Imports */ +import { + Address, + L1DataService, + QueueOrigin, + RollupTransaction, +} from '../../../types' +import { CHAIN_ID } from '../../constants' + +const abi = new ethers.utils.AbiCoder() +const log = getLogger('log-handler') + +/** + * Handles the L1ToL2TxEnqueued event by parsing a RollupTransaction + * from the event data and storing it in the DB. + * + * Assumed Log Data Format: + * - sender: 20-byte address 0-20 + * - target: 20-byte address 20-40 + * - gasLimit: 32-byte uint 40-72 + * - calldata: bytes 72-end + * + * @param ds The L1DataService to use for persistence. + * @param l The log event that was emitted. + * @param tx The transaction that emitted the event. + * @throws Error if there's an error with persistence. + */ +export const L1ToL2TxEnqueuedLogHandler = async ( + ds: L1DataService, + l: Log, + tx: TransactionResponse +): Promise => { + log.debug( + `L1ToL2TxEnqueued event received at block ${tx.blockNumber}, tx ${l.transactionIndex}, log: ${l.transactionLogIndex}. TxHash: ${tx.hash}. Log Data: ${l.data}` + ) + + const data: string = remove0x(l.data) + + let rollupTransaction: RollupTransaction + try { + rollupTransaction = { + l1BlockNumber: tx.blockNumber, + l1Timestamp: tx.timestamp, + l1TxHash: l.transactionHash, + l1TxIndex: l.transactionIndex, + l1TxLogIndex: l.transactionLogIndex, + queueOrigin: QueueOrigin.L1_TO_L2_QUEUE, + batchIndex: 0, + sender: l.address, + l1MessageSender: add0x(data.substr(0, 40)), + target: add0x(data.substr(40, 40)), + // TODO: Change gasLimit to a BigNumber so it can support 256 bits + gasLimit: new BigNumber(data.substr(80, 64), 'hex').toNumber(), + calldata: add0x(data.substr(144)), + } + } catch (e) { + // This is, by definition, just an ill-formatted, and therefore invalid, tx. + log.debug( + `Error parsing calldata tx from CalldataTxEnqueued event. Calldata: ${tx.data}. Error: ${e.message}. Stack: ${e.stack}.` + ) + return + } + + await ds.insertL1RollupTransactions(l.transactionHash, [rollupTransaction]) +} + +/** + * Handles the CalldataTxEnqueued event by parsing a RollupTransaction + * from the transaction calldata and storing it in the DB. + * + * Assumed calldata format: + * - target: 20-byte address 0-20 + * - nonce: 32-byte uint 20-52 + * - gasLimit: 32-byte uint 52-84 + * - signature: 65-byte bytes 84-149 + * - calldata: bytes 149-end + * + * @param ds The L1DataService to use for persistence. + * @param l The log event that was emitted. + * @param tx The transaction that emitted the event. + * @throws Error if there's an error with persistence. + */ +export const CalldataTxEnqueuedLogHandler = async ( + ds: L1DataService, + l: Log, + tx: TransactionResponse +): Promise => { + log.debug( + `CalldataTxEnqueued event received at block ${tx.blockNumber}, tx ${l.transactionIndex}, log: ${l.transactionLogIndex}. TxHash: ${tx.hash}. Calldata: ${tx.data}` + ) + + let rollupTransaction: RollupTransaction + try { + // Skip the 4 bytes of MethodID + const l1TxCalldata = remove0x(ethers.utils.hexDataSlice(tx.data, 4)) + + const target = add0x(l1TxCalldata.substr(0, 40)) + const nonce = new BigNumber(l1TxCalldata.substr(40, 64), 'hex') + const gasLimit = new BigNumber(l1TxCalldata.substr(104, 64), 'hex') + const signature = add0x(l1TxCalldata.substr(168, 130)) + const calldata = add0x(l1TxCalldata.substr(298)) + + const unsigned: TransactionRequest = { + to: target, + nonce: add0x(nonce.toString('hex')), + gasPrice: 0, + gasLimit: add0x(gasLimit.toString('hex')), + value: 0, + data: calldata, + chainId: CHAIN_ID, + } + + const r = add0x(signature.substr(2, 64)) + const s = add0x(signature.substr(66, 64)) + const v = parseInt(signature.substr(130, 2), 16) + const sender: string = await getTxSigner(unsigned, r, s, v) + + rollupTransaction = { + l1BlockNumber: tx.blockNumber, + l1Timestamp: tx.timestamp, + l1TxHash: l.transactionHash, + l1TxIndex: l.transactionIndex, + l1TxLogIndex: l.transactionLogIndex, + queueOrigin: QueueOrigin.SAFETY_QUEUE, + batchIndex: 0, + sender, + target, + // TODO Change nonce to a BigNumber so it can support 256 bits + nonce: nonce.toNumber(), + // TODO: Change gasLimit to a BigNumber so it can support 256 bits + gasLimit: gasLimit.toNumber(), + signature, + calldata, + } + } catch (e) { + // This is, by definition, just an ill-formatted, and therefore invalid, tx. + log.debug( + `Error parsing calldata tx from CalldataTxEnqueued event. Calldata: ${tx.data}. Error: ${e.message}. Stack: ${e.stack}.` + ) + return + } + + await ds.insertL1RollupTransactions(l.transactionHash, [rollupTransaction]) +} + +/** + * Handles the L1ToL2BatchAppended event by parsing a RollupTransaction + * from the log event and storing it in the DB. + * + * @param ds The L1DataService to use for persistence. + * @param l The log event that was emitted. + * @param tx The transaction that emitted the event. + * @throws Error if there's an error with persistence. + */ +export const L1ToL2BatchAppendedLogHandler = async ( + ds: L1DataService, + l: Log, + tx: TransactionResponse +): Promise => { + log.debug( + `L1ToL2BatchAppended event received at block ${tx.blockNumber}, tx ${l.transactionIndex}, log: ${l.transactionLogIndex}. TxHash: ${tx.hash}` + ) + let batchNumber: number + try { + batchNumber = await ds.createNextL1ToL2Batch() + } catch (e) { + logError( + log, + `Error creating next L1ToL2Batch after receiving an event to do so!`, + e + ) + throw e + } + + if (!batchNumber) { + const msg = `Attempted to create L1 to L2 Batch upon receiving L1ToL2BatchAppended log, but no tx was available for batching!` + log.error(msg) + throw Error(msg) + } else { + log.debug( + `Successfully created L1 to L2 Batch! Batch number: ${batchNumber}` + ) + } +} + +/** + * Handles the SafetyQueueBatchAppended event by parsing a RollupTransaction + * from the transaction calldata and storing it in the DB. + * + * @param ds The L1DataService to use for persistence. + * @param l The log event that was emitted. + * @param tx The transaction that emitted the event. + * @throws Error if there's an error with persistence. + */ +export const SafetyQueueBatchAppendedLogHandler = async ( + ds: L1DataService, + l: Log, + tx: TransactionResponse +): Promise => { + log.debug( + `SafetyQueueBatchAppended event received at block ${tx.blockNumber}, tx ${l.transactionIndex}, log: ${l.transactionLogIndex}. TxHash: ${tx.hash}` + ) + let batchNumber: number + + try { + batchNumber = await ds.createNextSafetyQueueBatch() + } catch (e) { + logError( + log, + `Error creating next SafetyQueueBatch after receiving an event to do so!`, + e + ) + throw e + } + + if (!batchNumber) { + const msg = `Attempted to create Safety Queue Batch upon receiving SafetyQueueBatchAppended log, but no tx was available for batching!` + log.error(msg) + throw Error(msg) + } else { + log.debug( + `Successfully created Safety Queue Batch! Batch number: ${batchNumber}` + ) + } +} + +/** + * Handles the SequencerBatchAppended event by parsing: + * - a list of RollupTransactions + * - L1 Block Timestamp as monotonically assigned by the sequencer + * from the transaction calldata and storing it in the DB. + * + * Assumed calldata format: + * - target: 20-byte address 0-20 + * - nonce: 32-byte uint 20-52 + * - gasLimit: 32-byte uint 52-84 + * - signature: 65-byte bytes 84-149 + * - calldata: bytes 149-end + * + * @param ds The L1DataService to use for persistence. + * @param l The log event that was emitted. + * @param tx The transaction that emitted the event. + * @throws Error if there's an error with persistence. + */ +export const SequencerBatchAppendedLogHandler = async ( + ds: L1DataService, + l: Log, + tx: TransactionResponse +): Promise => { + log.debug( + `SequencerBatchAppended event received at block ${tx.blockNumber}, tx ${l.transactionIndex}, log: ${l.transactionLogIndex}. TxHash: ${tx.hash}. Calldata: ${tx.data}` + ) + + const rollupTransactions: RollupTransaction[] = [] + let timestamp: any + try { + let transactionsBytes: string[] + ;[transactionsBytes, timestamp] = abi.decode( + ['bytes[]', 'uint256'], + ethers.utils.hexDataSlice(tx.data, 4) + ) + + for (let i = 0; i < transactionsBytes.length; i++) { + const txBytes = remove0x(transactionsBytes[i]) + + const target = add0x(txBytes.substr(0, 40)) + const nonce = new BigNumber(txBytes.substr(40, 64), 'hex') + const gasLimit = new BigNumber(txBytes.substr(104, 64), 'hex') + const signature = add0x(txBytes.substr(168, 130)) + const calldata = add0x(txBytes.substr(298)) + + const unsigned: TransactionRequest = { + to: target, + nonce: nonce.toNumber(), + gasPrice: 0, + gasLimit: add0x(gasLimit.toString('hex')), + value: 0, + data: calldata, + chainId: CHAIN_ID, + } + + const r = add0x(signature.substr(2, 64)) + const s = add0x(signature.substr(66, 64)) + const v = parseInt(signature.substr(130, 2), 16) + const sender: string = await getTxSigner(unsigned, r, s, v) + + rollupTransactions.push({ + l1BlockNumber: tx.blockNumber, + l1Timestamp: timestamp.toNumber(), + l1TxHash: l.transactionHash, + l1TxIndex: l.transactionIndex, + l1TxLogIndex: l.transactionLogIndex, + queueOrigin: QueueOrigin.SEQUENCER, + batchIndex: i, + sender, + target, + // TODO Change nonce to a BigNumber so it can support 256 bits + nonce: nonce.toNumber(), + // TODO: Change gasLimit to a BigNumber so it can support 256 bits + gasLimit: gasLimit.toNumber(), + signature, + calldata, + }) + } + } catch (e) { + // This is, by definition, just an ill-formatted, and therefore invalid, tx. + log.debug( + `Error parsing calldata tx from CalldataTxEnqueued event. Calldata: ${tx.data}. Error: ${e.message}. Stack: ${e.stack}.` + ) + return + } + + const batchNumber = await ds.insertL1RollupTransactions( + l.transactionHash, + rollupTransactions, + true + ) + log.debug(`Sequencer batch number ${batchNumber} successfully created!`) +} + +/** + * Handles the StateBatchAppended event by parsing a batch of state roots + * from the provided transaction calldata and storing it in the DB. + * + * @param ds The L1DataService to use for persistence. + * @param l The log event that was emitted. + * @param tx The transaction that emitted the event. + * @throws Error if there's an error with persistence. + */ +export const StateBatchAppendedLogHandler = async ( + ds: L1DataService, + l: Log, + tx: TransactionResponse +): Promise => { + log.debug( + `StateBatchAppended event received at block ${tx.blockNumber}, tx ${l.transactionIndex}, log: ${l.transactionLogIndex}. TxHash: ${tx.hash}. Calldata: ${tx.data}` + ) + + let stateRoots: string[] + try { + ;[stateRoots] = abi.decode( + ['bytes32[]'], + ethers.utils.hexDataSlice(tx.data, 4) + ) + } catch (e) { + // This is, by definition, just an ill-formatted, and therefore invalid, tx. + log.debug( + `Error parsing calldata tx from CalldataTxEnqueued event. Calldata: ${tx.data}. Error: ${e.message}. Stack: ${e.stack}.` + ) + return + } + + await ds.insertL1RollupStateRoots(l.transactionHash, stateRoots) +} diff --git a/packages/rollup-core/src/app/data/query-utils.ts b/packages/rollup-core/src/app/data/query-utils.ts index 0fa50995dcc83..68c2ea0c087b8 100644 --- a/packages/rollup-core/src/app/data/query-utils.ts +++ b/packages/rollup-core/src/app/data/query-utils.ts @@ -27,16 +27,19 @@ export const getL1BlockInsertValue = ( )}` } -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 l1RollupTxInsertStatement = `INSERT INTO rollup_tx(sender, l1_message_sender, target, calldata, queue_origin, nonce, gas_limit, signature, batch_number, batch_index, l1_block_number, l1_tx_hash, l1_tx_index, l1_tx_log_index) ` export const getL1RollupTransactionInsertValue = ( tx: RollupTransaction, - batchNumber: number + batchNumber?: number ): string => { + const batchNum = batchNumber || 'NULL' return `${stringOrNull(tx.sender)}, ${stringOrNull(tx.l1MessageSender)}, '${ tx.target }', '${tx.calldata}', ${tx.queueOrigin}, ${numOrNull(tx.nonce)}, ${numOrNull( tx.gasLimit - )}, ${stringOrNull(tx.signature)}, ${batchNumber}, ${tx.batchIndex}` + )}, ${stringOrNull(tx.signature)}, ${batchNum}, ${tx.batchIndex}, ${ + tx.l1BlockNumber + }, '${tx.l1TxHash}', ${tx.l1TxIndex}, ${numOrNull(tx.l1TxLogIndex)}` } export const l1RollupStateRootInsertStatement = `INSERT into l1_state_root(state_root, batch_number, batch_index) ` @@ -48,13 +51,15 @@ export const getL1RollupStateRootInsertValue = ( return `'${stateRoot}', ${batchNumber}, ${batchIndex}` } -export const l2TransactionInsertStatement = `INSERT INTO l2_tx(block_number, block_timestamp, tx_index, tx_hash, sender, l1_message_sender, target, calldata, nonce, signature) ` +export const l2TransactionInsertStatement = `INSERT INTO l2_tx(block_number, block_timestamp, tx_index, tx_hash, sender, l1_message_sender, target, calldata, nonce, signature, state_root) ` export const getL2TransactionInsertValue = (tx: TransactionAndRoot): string => { return `'${tx.blockNumber}', ${tx.timestamp}, ${tx.transactionIndex}, '${ tx.transactionHash }' ${stringOrNull(tx.from)}, ${stringOrNull(tx.l1MessageSender)}, '${ tx.to - }', '${tx.calldata}', ${tx.nonce}, ${stringOrNull(tx.signature)}` + }', '${tx.calldata}', ${tx.nonce}, ${stringOrNull(tx.signature)}, '${ + tx.stateRoot + }'` } export const bigNumOrNull = (bn: any): string => { 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 5b198616f48fa..1f949e6ee975a 100644 --- a/packages/rollup-core/src/types/data/l1-data-service.ts +++ b/packages/rollup-core/src/types/data/l1-data-service.ts @@ -57,14 +57,32 @@ export interface L1DataService { * * @param l1TxHash The L1 Transaction hash. * @param rollupTransactions The RollupTransactions to insert. - * @returns The inserted transaction batch number. + * @param createBatch Whether or not to create a batch from the provided RollupTransactions (whether or not they're part of the canonical chain). + * @returns The inserted transaction batch number if a batch was created. * @throws An error if there is a DB error. */ insertL1RollupTransactions( l1TxHash: string, - rollupTransactions: RollupTransaction[] + rollupTransactions: RollupTransaction[], + createBatch?: boolean ): Promise + /** + * Creates a batch from the oldest un-batched transaction that is from the L1 To L2 queue. + * + * @returns The created batch number or undefined if no fitting L1ToL2 transaction exists. + * @throws Error if there is a DB error + */ + createNextL1ToL2Batch(): Promise + + /** + * Creates a batch from the oldest un-batched transaction that is from the Safety queue. + * + * @returns The created batch number or undefined if no fitting Safety Queue transaction exists. + * @throws Error if there is a DB error or no such transaction exists. + */ + createNextSafetyQueueBatch(): Promise + /** * Atomically inserts the provided State Roots, creating a batch for them. * 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 77f9716b0d882..eda2ad4de9dd0 100644 --- a/packages/rollup-core/src/types/data/l2-data-service.ts +++ b/packages/rollup-core/src/types/data/l2-data-service.ts @@ -1,5 +1,6 @@ /* Internal Imports */ import { TransactionAndRoot } from '../types' +import { L1BatchSubmission, L2BatchStatus } from './types' export interface L2DataService { /** @@ -29,4 +30,60 @@ export interface L2DataService { batchNumber: number, batchSize: number ): Promise + + /** + * Gets the next L2 Batch for submission to L1, if one exists. + * + * @returns The L1BatchSubmission object, or undefined + * @throws An error if there is a DB error. + */ + getNextBatchForL1Submission(): Promise + + /** + * Marks the tx batch with the provided batch number as submitted to the L1 chain. + * + * @param batchNumber The batch number to mark as submitted. + * @param l1TxHash The L1 transaction hash for the batch submission. + * @throws An error if there is a DB error. + */ + markTransactionBatchSubmittedToL1( + batchNumber: number, + l1TxHash: string + ): Promise + + /** + * Marks the tx batch with the provided batch number as confirmed on the L1 chain. + * + * @param batchNumber The batch number to mark as confirmed. + * @param l1TxHash The L1 transaction hash for the batch submission. + * @throws An error if there is a DB error. + */ + markTransactionBatchConfirmedOnL1( + batchNumber: number, + l1TxHash: string + ): Promise + + /** + * Marks the state root batch with the provided batch number as submitted to the L1 chain. + * + * @param batchNumber The batch number to mark as submitted. + * @param l1TxHash The L1 transaction hash for the batch submission. + * @throws An error if there is a DB error. + */ + markStateRootBatchSubmittedToL1( + batchNumber: number, + l1TxHash: string + ): Promise + + /** + * Marks the state root batch with the provided batch number as confirmed on the L1 chain. + * + * @param batchNumber The batch number to mark as confirmed. + * @param l1TxHash The L1 transaction hash for the batch submission. + * @throws An error if there is a DB error. + */ + markStateRootBatchConfirmedOnL1( + batchNumber: number, + l1TxHash: string + ): Promise } diff --git a/packages/rollup-core/src/types/data/types.ts b/packages/rollup-core/src/types/data/types.ts index b086e4ef35e29..7c6e16bd72989 100644 --- a/packages/rollup-core/src/types/data/types.ts +++ b/packages/rollup-core/src/types/data/types.ts @@ -1,5 +1,30 @@ +import { TransactionAndRoot } from '../types' + +export enum QueueOrigin { + L1_TO_L2_QUEUE = 0, + SAFETY_QUEUE = 1, + SEQUENCER = 2, +} + +export enum L2BatchStatus { + UNBATCHED = 'UNBATCHED', + BATCHED = 'BATCHED', + TXS_SUBMITTED = 'TXS_SUBMITTED', + TXS_CONFIRMED = 'TXS_CONFIRMED', + ROOTS_SUBMITTED = 'ROOTS_SUBMITTED', + ROOTS_CONFIRMED = 'ROOTS_CONFIRMED', +} + export interface L1BatchRecord { blockTimestamp: number batchNumber: number batchSize: number } + +export interface L1BatchSubmission { + l1TxBatchTxHash: string + l1StateRootBatchTxHash: string + status: string + l2BatchNumber: number + transactions: TransactionAndRoot[] +} diff --git a/packages/rollup-core/src/types/types.ts b/packages/rollup-core/src/types/types.ts index 3b6267684bdb0..e85d3cf660da3 100644 --- a/packages/rollup-core/src/types/types.ts +++ b/packages/rollup-core/src/types/types.ts @@ -26,7 +26,9 @@ export interface RollupTransaction { gasLimit?: number l1Timestamp: number l1BlockNumber: number + l1TxIndex: number l1TxHash: string + l1TxLogIndex?: number nonce?: number queueOrigin: number signature?: string @@ -62,6 +64,7 @@ export type LogHandler = ( l: Log, tx: TransactionResponse ) => Promise + export interface LogHandlerContext { topic: string contractAddress: Address diff --git a/packages/rollup-core/test/app/l1-batch-submitter.spec.ts b/packages/rollup-core/test/app/l1-batch-submitter.spec.ts new file mode 100644 index 0000000000000..ab682647cbcda --- /dev/null +++ b/packages/rollup-core/test/app/l1-batch-submitter.spec.ts @@ -0,0 +1,601 @@ +/* External Imports */ +import { + keccak256FromUtf8, + sleep, + ZERO_ADDRESS, +} from '@eth-optimism/core-utils' +import { TransactionReceipt, TransactionResponse } from 'ethers/providers' +import { Wallet } from 'ethers' + +/* Internal Imports */ +import { DefaultDataService } from '../../src/app/data' +import { L1BatchSubmission, L2BatchStatus } from '../../src/types/data' +import { L1BatchSubmitter } from '../../src/app/data/consumers/l1-batch-submitter' + +interface BatchNumberHash { + batchNumber: number + txHash: string +} + +class MockDataService extends DefaultDataService { + public readonly nextBatch: L1BatchSubmission[] = [] + public readonly txBatchesSubmitted: BatchNumberHash[] = [] + public readonly txBatchesConfirmed: BatchNumberHash[] = [] + public readonly stateBatchesSubmitted: BatchNumberHash[] = [] + public readonly stateBatchesConfirmed: BatchNumberHash[] = [] + + constructor() { + super(undefined) + } + + public async getNextBatchForL1Submission(): Promise { + return this.nextBatch.shift() + } + + public async markTransactionBatchSubmittedToL1( + batchNumber: number, + l1TxHash: string + ): Promise { + this.txBatchesSubmitted.push({ batchNumber, txHash: l1TxHash }) + } + + public async markTransactionBatchConfirmedOnL1( + batchNumber: number, + l1TxHash: string + ): Promise { + this.txBatchesConfirmed.push({ batchNumber, txHash: l1TxHash }) + } + + public async markStateRootBatchSubmittedToL1( + batchNumber: number, + l1TxHash: string + ): Promise { + this.stateBatchesSubmitted.push({ batchNumber, txHash: l1TxHash }) + } + + public async markStateRootBatchConfirmedOnL1( + batchNumber: number, + l1TxHash: string + ): Promise { + this.stateBatchesConfirmed.push({ batchNumber, txHash: l1TxHash }) + } +} + +class MockProvider { + public confirmedTxs: Map = new Map< + string, + TransactionReceipt + >() + + public async waitForTransaction( + hash: string, + numConfirms: number + ): Promise { + while (!this.confirmedTxs.get(hash)) { + await sleep(100) + } + return this.confirmedTxs.get(hash) + } +} + +class MockCanonicalTransactionChain { + public responses: TransactionResponse[] = [] + + constructor(public readonly provider: MockProvider) {} + + public async appendSequencerBatch( + calldata: string, + timestamp: number + ): Promise { + const response: TransactionResponse = this.responses.shift() + if (!response) { + throw Error('no response') + } + return response + } +} + +class MockStateCommitmentChain { + public responses: TransactionResponse[] = [] + + constructor(public readonly provider: MockProvider) {} + + public async appendStateBatch( + batches: string[] + ): Promise { + const response: TransactionResponse = this.responses.shift() + if (!response) { + throw Error('no response') + } + return response + } +} + +describe('L1 Batch Submitter', () => { + let batchSubmitter: L1BatchSubmitter + let dataService: MockDataService + let canonicalProvider: MockProvider + let canonicalTransactionChain: MockCanonicalTransactionChain + let stateCommitmentProvider: MockProvider + let stateCommitmentChain: MockStateCommitmentChain + + beforeEach(async () => { + dataService = new MockDataService() + canonicalProvider = new MockProvider() + canonicalTransactionChain = new MockCanonicalTransactionChain( + canonicalProvider + ) + stateCommitmentProvider = new MockProvider() + stateCommitmentChain = new MockStateCommitmentChain(stateCommitmentProvider) + batchSubmitter = new L1BatchSubmitter( + dataService, + canonicalTransactionChain as any, + stateCommitmentChain as any + ) + }) + + it('should not do anything if there are no batches', async () => { + await batchSubmitter.runTask() + + dataService.txBatchesSubmitted.length.should.equal( + 0, + 'No tx batches should have been submitted!' + ) + dataService.txBatchesConfirmed.length.should.equal( + 0, + 'No tx batches should have been confirmed!' + ) + + dataService.stateBatchesSubmitted.length.should.equal( + 0, + 'No state batches should have been submitted!' + ) + dataService.stateBatchesConfirmed.length.should.equal( + 0, + 'No state batches should have been confirmed!' + ) + }) + + it('should not do anything if the next batch has an invalid status', async () => { + dataService.nextBatch.push({ + l1TxBatchTxHash: undefined, + l1StateRootBatchTxHash: undefined, + status: L2BatchStatus.UNBATCHED, + l2BatchNumber: 1, + transactions: [ + { + timestamp: 1, + blockNumber: 2, + transactionHash: keccak256FromUtf8('l2 tx hash'), + transactionIndex: 0, + to: Wallet.createRandom().address, + from: Wallet.createRandom().address, + nonce: 1, + calldata: keccak256FromUtf8('some calldata'), + stateRoot: keccak256FromUtf8('l2 state root'), + signature: 'ab'.repeat(65), + }, + ], + }) + + await batchSubmitter.runTask() + + dataService.txBatchesSubmitted.length.should.equal( + 0, + 'No tx batches should have been submitted!' + ) + dataService.txBatchesConfirmed.length.should.equal( + 0, + 'No tx batches should have been confirmed!' + ) + + dataService.stateBatchesSubmitted.length.should.equal( + 0, + 'No state batches should have been submitted!' + ) + dataService.stateBatchesConfirmed.length.should.equal( + 0, + 'No state batches should have been confirmed!' + ) + }) + + it('should send txs and roots if there is a batch', async () => { + const hash: string = keccak256FromUtf8('l1 tx hash') + const batchNumber: number = 1 + dataService.nextBatch.push({ + l1TxBatchTxHash: undefined, + l1StateRootBatchTxHash: undefined, + status: L2BatchStatus.BATCHED, + l2BatchNumber: batchNumber, + transactions: [ + { + timestamp: 1, + blockNumber: 2, + transactionHash: keccak256FromUtf8('l2 tx hash'), + transactionIndex: 0, + to: Wallet.createRandom().address, + from: Wallet.createRandom().address, + nonce: 1, + calldata: keccak256FromUtf8('some calldata'), + stateRoot: keccak256FromUtf8('l2 state root'), + signature: 'ab'.repeat(65), + }, + ], + }) + + canonicalTransactionChain.responses.push({ hash } as any) + stateCommitmentChain.responses.push({ hash } as any) + + await batchSubmitter.runTask() + + dataService.txBatchesSubmitted.length.should.equal( + 1, + 'No tx batches submitted!' + ) + dataService.txBatchesSubmitted[0].txHash.should.equal( + hash, + 'Incorrect tx hash submitted!' + ) + dataService.txBatchesSubmitted[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number submitted!' + ) + + dataService.txBatchesConfirmed.length.should.equal( + 1, + 'No tx batches confirmed!' + ) + dataService.txBatchesConfirmed[0].txHash.should.equal( + hash, + 'Incorrect tx hash confirmed!' + ) + dataService.txBatchesConfirmed[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number confirmed!' + ) + + dataService.stateBatchesSubmitted.length.should.equal( + 1, + 'No state batches submitted!' + ) + dataService.stateBatchesSubmitted[0].txHash.should.equal( + hash, + 'Incorrect tx hash state root confirmed!' + ) + dataService.stateBatchesSubmitted[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number state root confirmed!' + ) + + dataService.stateBatchesConfirmed.length.should.equal( + 1, + 'No state batches confirmed!' + ) + dataService.stateBatchesConfirmed[0].txHash.should.equal( + hash, + 'Incorrect tx hash state root confirmed!' + ) + dataService.stateBatchesConfirmed[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number state root confirmed!' + ) + }) + + it('should wait for tx confirmation and send roots if there is a batch in TXS_SUBMITTED status', async () => { + const hash: string = keccak256FromUtf8('l1 tx hash') + const batchNumber: number = 1 + dataService.nextBatch.push({ + l1TxBatchTxHash: hash, + l1StateRootBatchTxHash: undefined, + status: L2BatchStatus.TXS_SUBMITTED, + l2BatchNumber: batchNumber, + transactions: [ + { + timestamp: 1, + blockNumber: 2, + transactionHash: keccak256FromUtf8('l2 tx hash'), + transactionIndex: 0, + to: Wallet.createRandom().address, + from: Wallet.createRandom().address, + nonce: 1, + calldata: keccak256FromUtf8('some calldata'), + stateRoot: keccak256FromUtf8('l2 state root'), + signature: 'ab'.repeat(65), + }, + ], + }) + + canonicalTransactionChain.responses.push({ hash } as any) + stateCommitmentChain.responses.push({ hash } as any) + + await batchSubmitter.runTask() + + dataService.txBatchesSubmitted.length.should.equal( + 0, + 'No tx batches should have been submitted!' + ) + + dataService.txBatchesConfirmed.length.should.equal( + 1, + 'No tx batches confirmed!' + ) + dataService.txBatchesConfirmed[0].txHash.should.equal( + hash, + 'Incorrect tx hash confirmed!' + ) + dataService.txBatchesConfirmed[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number confirmed!' + ) + + dataService.stateBatchesSubmitted.length.should.equal( + 1, + 'No state batches submitted!' + ) + dataService.stateBatchesSubmitted[0].txHash.should.equal( + hash, + 'Incorrect tx hash state root confirmed!' + ) + dataService.stateBatchesSubmitted[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number state root confirmed!' + ) + + dataService.stateBatchesConfirmed.length.should.equal( + 1, + 'No state batches confirmed!' + ) + dataService.stateBatchesConfirmed[0].txHash.should.equal( + hash, + 'Incorrect tx hash state root confirmed!' + ) + dataService.stateBatchesConfirmed[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number state root confirmed!' + ) + }) + + it('should send roots if there is a batch in TXS_CONFIRMED status', async () => { + const hash: string = keccak256FromUtf8('l1 tx hash') + const batchNumber: number = 1 + dataService.nextBatch.push({ + l1TxBatchTxHash: hash, + l1StateRootBatchTxHash: undefined, + status: L2BatchStatus.TXS_CONFIRMED, + l2BatchNumber: batchNumber, + transactions: [ + { + timestamp: 1, + blockNumber: 2, + transactionHash: keccak256FromUtf8('l2 tx hash'), + transactionIndex: 0, + to: Wallet.createRandom().address, + from: Wallet.createRandom().address, + nonce: 1, + calldata: keccak256FromUtf8('some calldata'), + stateRoot: keccak256FromUtf8('l2 state root'), + signature: 'ab'.repeat(65), + }, + ], + }) + + canonicalTransactionChain.responses.push({ hash } as any) + stateCommitmentChain.responses.push({ hash } as any) + + await batchSubmitter.runTask() + + dataService.txBatchesSubmitted.length.should.equal( + 0, + 'No tx batches should have been submitted!' + ) + + dataService.txBatchesConfirmed.length.should.equal( + 0, + 'No tx batches should have been confirmed!' + ) + + dataService.stateBatchesSubmitted.length.should.equal( + 1, + 'No state batches submitted!' + ) + dataService.stateBatchesSubmitted[0].txHash.should.equal( + hash, + 'Incorrect tx hash state root confirmed!' + ) + dataService.stateBatchesSubmitted[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number state root confirmed!' + ) + + dataService.stateBatchesConfirmed.length.should.equal( + 1, + 'No state batches confirmed!' + ) + dataService.stateBatchesConfirmed[0].txHash.should.equal( + hash, + 'Incorrect tx hash state root confirmed!' + ) + dataService.stateBatchesConfirmed[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number state root confirmed!' + ) + }) + + it('should wait for roots tx if there is a batch in ROOTS_SUBMITTED status', async () => { + const hash: string = keccak256FromUtf8('l1 tx hash') + const batchNumber: number = 1 + dataService.nextBatch.push({ + l1TxBatchTxHash: hash, + l1StateRootBatchTxHash: hash, + status: L2BatchStatus.ROOTS_SUBMITTED, + l2BatchNumber: batchNumber, + transactions: [ + { + timestamp: 1, + blockNumber: 2, + transactionHash: keccak256FromUtf8('l2 tx hash'), + transactionIndex: 0, + to: Wallet.createRandom().address, + from: Wallet.createRandom().address, + nonce: 1, + calldata: keccak256FromUtf8('some calldata'), + stateRoot: keccak256FromUtf8('l2 state root'), + signature: 'ab'.repeat(65), + }, + ], + }) + + canonicalTransactionChain.responses.push({ hash } as any) + stateCommitmentChain.responses.push({ hash } as any) + + await batchSubmitter.runTask() + + dataService.txBatchesSubmitted.length.should.equal( + 0, + 'No tx batches should have been submitted!' + ) + + dataService.txBatchesConfirmed.length.should.equal( + 0, + 'No tx batches should have been confirmed!' + ) + + dataService.stateBatchesSubmitted.length.should.equal( + 0, + 'No state batches should have been submitted!' + ) + + dataService.stateBatchesConfirmed.length.should.equal( + 1, + 'No state batches confirmed!' + ) + dataService.stateBatchesConfirmed[0].txHash.should.equal( + hash, + 'Incorrect tx hash state root confirmed!' + ) + dataService.stateBatchesConfirmed[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number state root confirmed!' + ) + }) + + describe('waiting for confirmations', () => { + beforeEach(() => { + batchSubmitter = new L1BatchSubmitter( + dataService, + canonicalTransactionChain as any, + stateCommitmentChain as any, + 2 + ) + }) + + it('should wait for tx confirmations', async () => { + const hash: string = keccak256FromUtf8('l1 tx hash') + const batchNumber: number = 1 + dataService.nextBatch.push({ + l1TxBatchTxHash: hash, + l1StateRootBatchTxHash: undefined, + status: L2BatchStatus.TXS_SUBMITTED, + l2BatchNumber: batchNumber, + transactions: [ + { + timestamp: 1, + blockNumber: 2, + transactionHash: keccak256FromUtf8('l2 tx hash'), + transactionIndex: 0, + to: Wallet.createRandom().address, + from: Wallet.createRandom().address, + nonce: 1, + calldata: keccak256FromUtf8('some calldata'), + stateRoot: keccak256FromUtf8('l2 state root'), + signature: 'ab'.repeat(65), + }, + ], + }) + + canonicalTransactionChain.responses.push({ hash } as any) + stateCommitmentChain.responses.push({ hash } as any) + + batchSubmitter.runTask() + + await sleep(1000) + + dataService.txBatchesConfirmed.length.should.equal( + 0, + 'batch should not yet be confirmed' + ) + + canonicalProvider.confirmedTxs.set(hash, {} as any) + + await sleep(2_000) + + dataService.txBatchesConfirmed.length.should.equal( + 1, + 'No tx batches confirmed!' + ) + dataService.txBatchesConfirmed[0].txHash.should.equal( + hash, + 'Incorrect tx hash confirmed!' + ) + dataService.txBatchesConfirmed[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number confirmed!' + ) + + // the rest omitted because they're confirmed in tests above + }) + + it('should wait for state root confirmations', async () => { + const hash: string = keccak256FromUtf8('l1 tx hash') + const batchNumber: number = 1 + dataService.nextBatch.push({ + l1TxBatchTxHash: hash, + l1StateRootBatchTxHash: hash, + status: L2BatchStatus.ROOTS_SUBMITTED, + l2BatchNumber: batchNumber, + transactions: [ + { + timestamp: 1, + blockNumber: 2, + transactionHash: keccak256FromUtf8('l2 tx hash'), + transactionIndex: 0, + to: Wallet.createRandom().address, + from: Wallet.createRandom().address, + nonce: 1, + calldata: keccak256FromUtf8('some calldata'), + stateRoot: keccak256FromUtf8('l2 state root'), + signature: 'ab'.repeat(65), + }, + ], + }) + + stateCommitmentChain.responses.push({ hash } as any) + + batchSubmitter.runTask() + + await sleep(1000) + + dataService.stateBatchesConfirmed.length.should.equal( + 0, + 'batch should not yet be confirmed' + ) + + stateCommitmentProvider.confirmedTxs.set(hash, {} as any) + + await sleep(2_000) + + dataService.stateBatchesConfirmed.length.should.equal( + 1, + 'No state root batches confirmed!' + ) + dataService.stateBatchesConfirmed[0].txHash.should.equal( + hash, + 'Incorrect state root hash confirmed!' + ) + dataService.stateBatchesConfirmed[0].batchNumber.should.equal( + batchNumber, + 'Incorrect state root batch number confirmed!' + ) + }) + }) +}) 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 7de4416723378..6903912139f9f 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 @@ -72,7 +72,8 @@ class MockDataService extends DefaultDataService { public async insertL1RollupTransactions( l1TxHash: string, - rollupTransactions: RollupTransaction[] + rollupTransactions: RollupTransaction[], + createBatch: boolean = false ): Promise { this.rollupTransactions.set(l1TxHash, rollupTransactions) return this.rollupTransactions.size @@ -136,6 +137,8 @@ const getRollupTransaction = (): RollupTransaction => { l1Timestamp: 0, l1BlockNumber: 0, l1TxHash: keccak256FromUtf8('0xdeadbeef'), + l1TxIndex: 0, + l1TxLogIndex: 0, nonce: 0, queueOrigin: 0, } diff --git a/packages/rollup-core/test/app/l2-batch-submitter.spec.ts b/packages/rollup-core/test/app/l2-batch-submitter.spec.ts index 3eacfa9cf4210..04477e5924b23 100644 --- a/packages/rollup-core/test/app/l2-batch-submitter.spec.ts +++ b/packages/rollup-core/test/app/l2-batch-submitter.spec.ts @@ -77,6 +77,8 @@ describe('L2 Batch Submitter', () => { l1Timestamp: 1, l1BlockNumber: 1, l1TxHash: keccak256FromUtf8('tx hash'), + l1TxIndex: 0, + l1TxLogIndex: 0, queueOrigin: 1, }, ], 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 180134661e70b..95663f8601d5d 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 @@ -15,11 +15,19 @@ import { /* Internal Imports */ import { L2DataService, TransactionAndRoot } from '../../src/types' -import { CHAIN_ID, L2ChainDataPersister } from '../../src/app' +import { + CHAIN_ID, + DefaultDataService, + L2ChainDataPersister, +} from '../../src/app' -class MockDataService implements L2DataService { +class MockDataService extends DefaultDataService { public readonly transactionAndRoots: TransactionAndRoot[] = [] + constructor() { + super(undefined) + } + public async insertL2Transaction(transaction: TransactionAndRoot) { this.transactionAndRoots.push(transaction) } diff --git a/packages/rollup-core/test/app/l2-node-service.spec.ts b/packages/rollup-core/test/app/l2-node-service.spec.ts index 2bab87ceead60..bd3dc550dd204 100644 --- a/packages/rollup-core/test/app/l2-node-service.spec.ts +++ b/packages/rollup-core/test/app/l2-node-service.spec.ts @@ -11,7 +11,7 @@ import { Wallet } from 'ethers' import { JsonRpcProvider } from 'ethers/providers' /* Internal Imports */ -import { BlockBatches, RollupTransaction } from '../../src/types' +import { BlockBatches, QueueOrigin, RollupTransaction } from '../../src/types' import { DefaultL2NodeService } from '../../src/app' import { verifyMessage } from 'ethers/utils' @@ -58,7 +58,9 @@ const rollupTx: RollupTransaction = { l1Timestamp: timestamp, l1BlockNumber: blockNumber, l1TxHash, - queueOrigin: 1, + l1TxIndex: 0, + l1TxLogIndex: 0, + queueOrigin: QueueOrigin.SAFETY_QUEUE, } const nonce2: number = 1 @@ -76,7 +78,9 @@ const rollupTx2: RollupTransaction = { l1Timestamp: timestamp, l1BlockNumber: blockNumber, l1TxHash, - queueOrigin: 1, + l1TxIndex: 0, + l1TxLogIndex: 1, + queueOrigin: QueueOrigin.SAFETY_QUEUE, } const deserializeBlockBatches = (serialized: string): BlockBatches => { @@ -90,6 +94,8 @@ const deserializeBlockBatches = (serialized: string): BlockBatches => { case 'l1BlockNumber': case 'l1Timestamp': case 'queueOrigin': + case 'l1TxIndex': + case 'l1TxLogIndex': return hexStrToNumber(v) default: return v diff --git a/packages/rollup-core/test/app/log-handlers.spec.ts b/packages/rollup-core/test/app/log-handlers.spec.ts new file mode 100644 index 0000000000000..5aabd10f9e3df --- /dev/null +++ b/packages/rollup-core/test/app/log-handlers.spec.ts @@ -0,0 +1,384 @@ +/* External Imports */ +import { + add0x, + keccak256FromUtf8, + remove0x, + ZERO_ADDRESS, +} from '@eth-optimism/core-utils' + +import { ethers, Wallet } from 'ethers' +import { Log } from 'ethers/providers' +import { TransactionResponse } from 'ethers/providers/abstract-provider' + +/* Internal Imports */ +import { + L1ToL2TxEnqueuedLogHandler, + DefaultDataService, + RollupTransaction, + QueueOrigin, + CalldataTxEnqueuedLogHandler, + L1ToL2BatchAppendedLogHandler, + SafetyQueueBatchAppendedLogHandler, + SequencerBatchAppendedLogHandler, + StateBatchAppendedLogHandler, + CHAIN_ID, +} from '../../src' +import { + arrayify, + joinSignature, + keccak256, + parseTransaction, + recoverAddress, + resolveProperties, + serializeTransaction, + Transaction, + UnsignedTransaction, + verifyMessage, +} from 'ethers/utils' + +const abi = new ethers.utils.AbiCoder() + +const createLog = ( + data: string, + txHash: string = keccak256FromUtf8('tx hash'), + txIndex: number = 0, + txLogIndex: number = 0 +): Log => { + return { + blockNumber: 1, + blockHash: keccak256FromUtf8('block'), + transactionHash: txHash, + transactionIndex: txIndex, + transactionLogIndex: txLogIndex, + address: ZERO_ADDRESS, + data, + logIndex: txLogIndex, + topics: [], + } +} + +const createTx = ( + calldata: string, + txHash: string = keccak256FromUtf8('tx hash'), + blockNumber: number = 1, + timestamp: number = 1 +): TransactionResponse => { + return { + hash: txHash, + blockHash: keccak256FromUtf8('block'), + blockNumber, + timestamp, + from: Wallet.createRandom().address, + to: Wallet.createRandom().address, + nonce: 1, + data: calldata, + chainId: 108, + confirmations: 0, + gasLimit: undefined, + gasPrice: undefined, + value: undefined, + wait: undefined, + } +} + +class MockDataService extends DefaultDataService { + public createdL1ToL2Batches: number = 0 + public createdSafetyQueueBatches: number = 0 + public rollupTransactionsInserted: RollupTransaction[][] = [] + public txHashToRollupRootsInserted: Map = new Map< + string, + string[] + >() + + public txHashBatchesCreated: Set = new Set() + + constructor() { + super(undefined) + } + + public async createNextL1ToL2Batch(): Promise { + return ++this.createdL1ToL2Batches + } + + public async createNextSafetyQueueBatch(): Promise { + return ++this.createdSafetyQueueBatches + } + + public async insertL1RollupTransactions( + l1TxHash: string, + rollupTransactions: RollupTransaction[], + createBatch: boolean = false + ): Promise { + this.rollupTransactionsInserted.push(rollupTransactions) + if (createBatch) { + this.txHashBatchesCreated.add(l1TxHash) + return this.txHashBatchesCreated.size + } + return undefined + } + + public async insertL1RollupStateRoots( + l1TxHash: string, + stateRoots: string[] + ): Promise { + this.txHashToRollupRootsInserted.set(l1TxHash, stateRoots) + return undefined + } +} + +const wallet = Wallet.createRandom() + +const getTxSignature = async ( + to: string, + nonce: string, + gasLimit: string, + data: string, + w: Wallet = wallet +): Promise => { + const trans = { + to: add0x(to), + nonce: add0x(nonce), + gasLimit: add0x(gasLimit), + data: add0x(data), + value: 0, + chainId: CHAIN_ID, + } + + const sig = await w.sign(trans) + const t: Transaction = parseTransaction(sig) + + return add0x(`${t.r}${remove0x(t.s)}${t.v.toString(16)}`) +} + +describe('Log Handlers', () => { + let dataService: MockDataService + beforeEach(() => { + dataService = new MockDataService() + }) + + it('should parse and insert L1ToL2Tx', async () => { + const sender: string = 'aa'.repeat(20) + const target: string = 'bb'.repeat(20) + const gasLimit: string = '00'.repeat(32) + const calldata: string = 'abcd'.repeat(40) + + const data = `0x${sender}${target}${gasLimit}${calldata}` + + const l = createLog(data) + const tx = createTx('00'.repeat(64)) + + await L1ToL2TxEnqueuedLogHandler(dataService, l, tx) + + dataService.rollupTransactionsInserted.length.should.equal( + 1, + `No tx batch inserted!` + ) + dataService.rollupTransactionsInserted[0].length.should.equal( + 1, + `No tx inserted!` + ) + const received: RollupTransaction = + dataService.rollupTransactionsInserted[0][0] + + received.l1BlockNumber.should.equal(tx.blockNumber, 'Block number mismatch') + received.l1Timestamp.should.equal(tx.timestamp, 'Timestamp mismatch') + received.l1TxHash.should.equal(l.transactionHash, 'Tx hash mismatch') + received.l1TxIndex.should.equal(l.transactionIndex, 'Tx index mismatch') + received.l1TxLogIndex.should.equal(l.logIndex, 'Tx log index mismatch') + received.queueOrigin.should.equal( + QueueOrigin.L1_TO_L2_QUEUE, + 'Queue Origin mismatch' + ) + received.batchIndex.should.equal(0, 'Batch index mismatch') + received.sender.should.equal(l.address, 'Sender mismatch') + remove0x(received.l1MessageSender).should.equal( + sender, + 'L1 Message Sender mismatch' + ) + remove0x(received.target).should.equal(target, 'Target mismatch') + received.gasLimit.should.equal(0, 'Gas Limit mismatch') + remove0x(received.calldata).should.equal(calldata, 'Calldata mismatch') + + dataService.txHashBatchesCreated.size.should.equal( + 0, + 'Should not have created batch!' + ) + }) + + it('should parse and insert Slow Queue Tx', async () => { + const target: string = 'bb'.repeat(20) + const nonce: string = '00'.repeat(32) + const gasLimit: string = '00'.repeat(31) + '01' + const calldata: string = 'abcd'.repeat(40) + + const signature = await getTxSignature(target, nonce, gasLimit, calldata) + + const data = `0x22222222${target}${nonce}${gasLimit}${remove0x( + signature + )}${calldata}` + + const l = createLog('00'.repeat(64)) + const tx = createTx(data) + + await CalldataTxEnqueuedLogHandler(dataService, l, tx) + + dataService.rollupTransactionsInserted.length.should.equal( + 1, + `No tx batch inserted!` + ) + dataService.rollupTransactionsInserted[0].length.should.equal( + 1, + `No tx inserted!` + ) + const received: RollupTransaction = + dataService.rollupTransactionsInserted[0][0] + + received.l1BlockNumber.should.equal(tx.blockNumber, 'Block number mismatch') + received.l1Timestamp.should.equal(tx.timestamp, 'Timestamp mismatch') + received.l1TxHash.should.equal(l.transactionHash, 'Tx hash mismatch') + received.l1TxIndex.should.equal(l.transactionIndex, 'Tx index mismatch') + received.l1TxLogIndex.should.equal(l.logIndex, 'Tx log index mismatch') + received.queueOrigin.should.equal( + QueueOrigin.SAFETY_QUEUE, + 'Queue Origin mismatch' + ) + received.batchIndex.should.equal(0, 'Batch index mismatch') + remove0x(received.sender).should.equal( + remove0x(wallet.address), + 'Sender mismatch' + ) + remove0x(received.target).should.equal(target, 'Target mismatch') + received.nonce.should.equal(0, 'Nonce mismatch') + received.gasLimit.should.equal(1, 'Gas Limit mismatch') + remove0x(received.signature).should.equal( + remove0x(signature), + 'Signature mismatch' + ) + remove0x(received.calldata).should.equal(calldata, 'Calldata mismatch') + + dataService.txHashBatchesCreated.size.should.equal( + 0, + 'Should not have created batch!' + ) + }) + + it('should append L1ToL2Batch on L1ToL2BatchAppendedLogHandler call', async () => { + dataService.createdL1ToL2Batches.should.equal( + 0, + 'starting batch count should be 0!' + ) + await L1ToL2BatchAppendedLogHandler( + dataService, + createLog(''), + createTx('') + ) + dataService.createdL1ToL2Batches.should.equal(1, 'batch not created!') + }) + + it('should append L1ToL2Batch on L1ToL2BatchAppendedLogHandler call', async () => { + dataService.createdSafetyQueueBatches.should.equal( + 0, + 'starting batch count should be 0!' + ) + await SafetyQueueBatchAppendedLogHandler( + dataService, + createLog(''), + createTx('') + ) + dataService.createdSafetyQueueBatches.should.equal(1, 'batch not created!') + }) + + it('should parse and insert Sequencer Batch', async () => { + const timestamp = 1 + + const target: string = 'bb'.repeat(20) + const nonce: string = '00'.repeat(32) + const gasLimit: string = '00'.repeat(31) + '01' + const calldata: string = 'abcd'.repeat(40) + + const signature = await getTxSignature(target, nonce, gasLimit, calldata) + + let data = `0x${target}${nonce}${gasLimit}${remove0x(signature)}${calldata}` + data = abi.encode(['bytes[]', 'uint256'], [[data, data, data], timestamp]) + + const l = createLog('00'.repeat(64)) + const tx = createTx(`0x22222222${remove0x(data)}`) + + await SequencerBatchAppendedLogHandler(dataService, l, tx) + + dataService.rollupTransactionsInserted.length.should.equal( + 1, + `No tx batch inserted!` + ) + dataService.rollupTransactionsInserted[0].length.should.equal( + 3, + `Tx inserted count mismatch!` + ) + for (let i = 0; i < dataService.rollupTransactionsInserted[0].length; i++) { + const received = dataService.rollupTransactionsInserted[0][i] + + received.l1BlockNumber.should.equal( + tx.blockNumber, + 'Block number mismatch' + ) + received.l1Timestamp.should.equal(tx.timestamp, 'Timestamp mismatch') + received.l1TxHash.should.equal(l.transactionHash, 'Tx hash mismatch') + received.l1TxIndex.should.equal(l.transactionIndex, 'Tx index mismatch') + received.l1TxLogIndex.should.equal(l.logIndex, 'Tx log index mismatch') + received.queueOrigin.should.equal( + QueueOrigin.SEQUENCER, + 'Queue Origin mismatch' + ) + received.batchIndex.should.equal(i, 'Batch index mismatch') + remove0x(received.sender).should.equal( + remove0x(wallet.address), + 'Sender mismatch' + ) + remove0x(received.target).should.equal(target, 'Target mismatch') + received.nonce.should.equal(0, 'Nonce mismatch') + received.gasLimit.should.equal(1, 'Gas Limit mismatch') + remove0x(received.signature).should.equal( + remove0x(signature), + 'Signature mismatch' + ) + remove0x(received.calldata).should.equal(calldata, 'Calldata mismatch') + } + + dataService.txHashBatchesCreated.size.should.equal( + 1, + 'Should have created batch!' + ) + }) + + it('should parse and insert State Batch', async () => { + const timestamp = 1 + + const roots: string[] = [ + keccak256FromUtf8('1'), + keccak256FromUtf8('2'), + keccak256FromUtf8('3'), + ] + const data = abi.encode(['bytes32[]'], [roots]) + + const l = createLog('00'.repeat(64)) + const tx = createTx(`0x22222222${remove0x(data)}`) + + await StateBatchAppendedLogHandler(dataService, l, tx) + + dataService.txHashToRollupRootsInserted.size.should.equal( + 1, + `No root batch inserted!` + ) + dataService.txHashToRollupRootsInserted + .get(tx.hash) + .length.should.equal(3, `State root inserted count mismatch!`) + for ( + let i = 0; + i < dataService.txHashToRollupRootsInserted.get(tx.hash).length; + i++ + ) { + const received = dataService.txHashToRollupRootsInserted.get(tx.hash)[i] + received.should.equal(roots[i], `Root ${i} mismatch!`) + } + }) +})