diff --git a/packages/core-utils/src/app/crypto.ts b/packages/core-utils/src/app/crypto.ts index e19e147fe6ca8..2121c56c5fc0c 100644 --- a/packages/core-utils/src/app/crypto.ts +++ b/packages/core-utils/src/app/crypto.ts @@ -29,9 +29,13 @@ export const Md5Hash = (preimage: string): string => { * @param value Value to hash * @returns the hash of the value. */ -export const keccak256 = (value: string): string => { +export const keccak256 = ( + value: string, + returnPrefixed: boolean = false +): string => { const preimage = add0x(value) - return remove0x(ethers.utils.keccak256(preimage)) + const hash = ethers.utils.keccak256(preimage) + return returnPrefixed ? hash : remove0x(hash) } /** diff --git a/packages/core-utils/src/app/ethereum.ts b/packages/core-utils/src/app/ethereum.ts new file mode 100644 index 0000000000000..dd6122c76e5d0 --- /dev/null +++ b/packages/core-utils/src/app/ethereum.ts @@ -0,0 +1,104 @@ +import { Contract, Wallet } from 'ethers' +import { + Provider, + TransactionRequest, + TransactionResponse, +} from 'ethers/providers' +import { getLogger } from './log' +import { Logger } from '../types' + +const log: Logger = getLogger('core-utils/ethereum') + +/** + * Populates and signs a transaction for the function call specified by the provided contract, function name, and args. + * + * @param contract The contract with a connected provider. + * @param functionName The function name to be invoked by the returned TransactionRequest + * @param functionArgs The arguments for the contract function to invoke + * @param wallet The wallet with which the transaction will be signed. + * @returns the constructed TransactionRequest. + */ +export const getSignedTransaction = async ( + contract: Contract, + functionName: string, + functionArgs: any[], + wallet: Wallet +): Promise => { + let tx: TransactionRequest + let nonce + ;[tx, nonce] = await Promise.all([ + populateFunctionCallTx( + contract, + functionName, + functionArgs, + wallet.address + ), + wallet.getTransactionCount('pending'), + ]) + + tx.nonce = nonce + return wallet.sign(tx) +} + +/** + * Populates and returns a TransactionRequest for the function call specified by the provided contract, function name, and args. + * + * @param contract The contract with a connected provider. + * @param functionName The function name to be invoked by the returned TransactionRequest + * @param functionArgs The arguments for the contract function to invoke + * @param fromAddress The caller address for the tx + * @returns the constructed TransactionRequest. + */ +export const populateFunctionCallTx = async ( + contract: Contract, + functionName: string, + functionArgs: any[], + fromAddress?: string +): Promise => { + const data: string = contract.interface.functions[functionName].encode( + functionArgs + ) + const tx: TransactionRequest = { + to: contract.address, + data, + } + + const estimateTx = { ...tx } + if (!!fromAddress) { + estimateTx.from = fromAddress + } + + log.debug( + `Getting gas limit and gas price for tx to ${ + contract.address + } function ${functionName}, with args ${JSON.stringify(functionArgs)}` + ) + + let gasLimit + let gasPrice + ;[gasLimit, gasPrice] = await Promise.all([ + contract.provider.estimateGas(estimateTx), + contract.provider.getGasPrice(), + ]) + + tx.gasLimit = gasLimit + tx.gasPrice = gasPrice + + return tx +} + +/** + * Determines whether or not the tx with the provided hash has been submitted to the chain. + * Note: This will return true if it has been submitted whether or not is has been mined. + * + * @param provider A provider to use for the fetch. + * @param txHash The transaction hash. + * @returns True if the tx with the provided hash has been submitted, false otherwise. + */ +export const isTxSubmitted = async ( + provider: Provider, + txHash: string +): Promise => { + const tx: TransactionResponse = await provider.getTransaction(txHash) + return !!tx +} diff --git a/packages/core-utils/src/app/index.ts b/packages/core-utils/src/app/index.ts index 1463f25854db2..9f2c66d3ee27b 100644 --- a/packages/core-utils/src/app/index.ts +++ b/packages/core-utils/src/app/index.ts @@ -12,6 +12,7 @@ export * from './constants' export * from './contract-deployment' export * from './crypto' export * from './equals' +export * from './ethereum' export * from './log' export * from './misc' export * from './number' diff --git a/packages/rollup-core/src/app/data/consumers/canonical-chain-batch-submitter.ts b/packages/rollup-core/src/app/data/consumers/canonical-chain-batch-submitter.ts index 90537a44c4660..020350148c220 100644 --- a/packages/rollup-core/src/app/data/consumers/canonical-chain-batch-submitter.ts +++ b/packages/rollup-core/src/app/data/consumers/canonical-chain-batch-submitter.ts @@ -1,34 +1,34 @@ /* External Imports */ import { getLogger, + getSignedTransaction, + isTxSubmitted, + keccak256, logError, numberToHexString, remove0x, ScheduledTask, } from '@eth-optimism/core-utils' -import { Contract } from 'ethers' - +import { Contract, Wallet } from 'ethers' +import { TransactionReceipt, TransactionResponse } from 'ethers/providers' /* Internal Imports */ import { - TransactionBatchSubmission, BatchSubmissionStatus, L2DataService, - StateCommitmentBatchSubmission, - BatchSubmission, + TransactionBatchSubmission, } from '../../../types/data' -import { TransactionReceipt, TransactionResponse } from 'ethers/providers' import { - UnexpectedBatchStatus, FutureRollupBatchNumberError, FutureRollupBatchTimestampError, RollupBatchBlockNumberTooOldError, - RollupBatchTimestampTooOldError, - RollupBatchSafetyQueueBlockNumberError, - RollupBatchSafetyQueueBlockTimestampError, RollupBatchL1ToL2QueueBlockNumberError, RollupBatchL1ToL2QueueBlockTimestampError, RollupBatchOvmBlockNumberError, RollupBatchOvmTimestampError, + RollupBatchSafetyQueueBlockNumberError, + RollupBatchSafetyQueueBlockTimestampError, + RollupBatchTimestampTooOldError, + UnexpectedBatchStatus, } from '../../../types' const log = getLogger('canonical-chain-batch-submitter') @@ -42,6 +42,7 @@ export class CanonicalChainBatchSubmitter extends ScheduledTask { private readonly canonicalTransactionChain: Contract, private readonly l1ToL2QueueContract: Contract, private readonly safetyQueueContract: Contract, + private readonly submitterWallet: Wallet, periodMilliseconds = 10_000 ) { super(periodMilliseconds) @@ -78,39 +79,88 @@ export class CanonicalChainBatchSubmitter extends ScheduledTask { return false } - if (batchSubmission.status !== BatchSubmissionStatus.QUEUED) { + if ( + batchSubmission.status !== BatchSubmissionStatus.QUEUED && + batchSubmission.status !== BatchSubmissionStatus.SUBMITTING + ) { const msg = `Received tx batch to send in ${ batchSubmission.status - } instead of ${ - BatchSubmissionStatus.QUEUED + } instead of ${BatchSubmissionStatus.QUEUED} or ${ + BatchSubmissionStatus.SUBMITTING }. Batch Submission: ${JSON.stringify(batchSubmission)}.` log.error(msg) throw new UnexpectedBatchStatus(msg) } - try { - const batchBlockNumber = await this.getBatchSubmissionBlockNumber() + if (await this.shouldSubmitBatch(batchSubmission)) { + try { + const batchBlockNumber = await this.getBatchSubmissionBlockNumber() - await this.validateBatchSubmission(batchSubmission, batchBlockNumber) + await this.validateBatchSubmission(batchSubmission, batchBlockNumber) - const txHash: string = await this.buildAndSendRollupBatchTransaction( - batchSubmission, - batchBlockNumber - ) - if (!txHash) { + const txHash: string = await this.buildAndSendRollupBatchTransaction( + batchSubmission, + batchBlockNumber + ) + if (!txHash) { + return false + } + batchSubmission.submissionTxHash = txHash + } catch (e) { + logError(log, `Error validating or submitting rollup tx batch`, e) + if (throwOnError) { + // this is only used by testing + throw e + } return false } - return this.waitForProofThatTransactionSucceeded(txHash, batchSubmission) + } + + try { + return this.waitForProofThatTransactionSucceeded( + batchSubmission.submissionTxHash, + batchSubmission + ) } catch (e) { - logError(log, `Error validating or submitting rollup tx batch`, e) - if (throwOnError) { - // this is only used by testing - throw e - } + logError( + log, + `Error waiting for canonical tx chain batch ${batchSubmission.batchNumber} with hash ${batchSubmission.submissionTxHash} to succeed!`, + e + ) return false } } + protected async shouldSubmitBatch(batchSubmission): Promise { + return ( + batchSubmission.status === BatchSubmissionStatus.QUEUED || + !(await isTxSubmitted( + this.canonicalTransactionChain.provider, + batchSubmission.submissionTxHash + )) + ) + } + + protected async getSignedRollupBatchTx( + txsCalldata: string[], + timestamp: number, + batchBlockNumber: number, + startIndex: number + ): Promise { + return getSignedTransaction( + this.canonicalTransactionChain, + 'appendSequencerBatch', + [txsCalldata, timestamp, batchBlockNumber, startIndex], + this.submitterWallet + ) + } + + protected async getBatchSubmissionBlockNumber(): Promise { + // TODO: This will eventually be part of the output metadata from L2 tx outputs + // Need to update geth to have this functionality so this is a mock for now + return (await this.getL1BlockNumber()) - 10 + } + /** * Builds and sends a Rollup Batch transaction to L1, returning its tx hash. * @@ -131,16 +181,38 @@ export class CanonicalChainBatchSubmitter extends ScheduledTask { log.debug( `Submitting tx batch ${l2Batch.batchNumber} at start index ${l2Batch.startIndex} with ${l2Batch.transactions.length} transactions to canonical chain. Timestamp: ${timestamp}` ) - const txRes: TransactionResponse = await this.canonicalTransactionChain.appendSequencerBatch( + + const signedTx: string = await this.getSignedRollupBatchTx( txsCalldata, timestamp, batchBlockNumber, l2Batch.startIndex ) + + txHash = keccak256(signedTx, true) + await this.dataService.markTransactionBatchSubmittingToL1( + l2Batch.batchNumber, + txHash + ) + + log.debug( + `Marked tx ${txHash} for canonical tx batch ${l2Batch.batchNumber} as submitting.` + ) + + const txRes: TransactionResponse = await this.canonicalTransactionChain.provider.sendTransaction( + signedTx + ) + log.debug( - `Tx batch ${l2Batch.batchNumber} appended with at least one confirmation! Tx Hash: ${txRes.hash}` + `Tx batch ${l2Batch.batchNumber} was sent to the canonical chain! Tx Hash: ${txRes.hash}` ) - txHash = txRes.hash + + if (txHash !== txRes.hash) { + log.warn( + `Received tx hash not the same as calculated hash! Received: ${txRes.hash}, calculated: ${txHash}` + ) + txHash = txRes.hash + } } catch (e) { logError( log, @@ -231,12 +303,6 @@ export class CanonicalChainBatchSubmitter extends ScheduledTask { return true } - protected async getBatchSubmissionBlockNumber(): Promise { - // TODO: This will eventually be part of the output metadata from L2 tx outputs - // Need to update geth to have this functionality so this is a mock for now - return (await this.getL1BlockNumber()) - 10 - } - private async validateBatchSubmission( batchSubmission: TransactionBatchSubmission, batchBlockNumber: number diff --git a/packages/rollup-core/src/app/data/consumers/state-commitment-chain-batch-submitter.ts b/packages/rollup-core/src/app/data/consumers/state-commitment-chain-batch-submitter.ts index 625ea24e9c1af..10ec2646ceff0 100644 --- a/packages/rollup-core/src/app/data/consumers/state-commitment-chain-batch-submitter.ts +++ b/packages/rollup-core/src/app/data/consumers/state-commitment-chain-batch-submitter.ts @@ -1,6 +1,14 @@ /* External Imports */ -import { getLogger, logError, ScheduledTask } from '@eth-optimism/core-utils' -import { Contract } from 'ethers' +import { + add0x, + getLogger, + getSignedTransaction, + isTxSubmitted, + keccak256, + logError, + ScheduledTask, +} from '@eth-optimism/core-utils' +import { Contract, Wallet } from 'ethers' /* Internal Imports */ import { @@ -20,6 +28,7 @@ export class StateCommitmentChainBatchSubmitter extends ScheduledTask { constructor( private readonly dataService: L2DataService, private readonly stateCommitmentChain: Contract, + private readonly submitterWallet: Wallet, periodMilliseconds = 10_000 ) { super(periodMilliseconds) @@ -48,23 +57,73 @@ export class StateCommitmentChainBatchSubmitter extends ScheduledTask { return false } - if (stateBatch.status !== BatchSubmissionStatus.QUEUED) { + if ( + stateBatch.status !== BatchSubmissionStatus.QUEUED && + stateBatch.status !== BatchSubmissionStatus.SUBMITTING + ) { const msg = `Received state commitment batch to finalize in ${ stateBatch.status - } instead of ${ - BatchSubmissionStatus.SENT + } instead of ${BatchSubmissionStatus.QUEUED} or ${ + BatchSubmissionStatus.SUBMITTING }. Batch Submission: ${JSON.stringify(stateBatch)}.` log.error(msg) throw new UnexpectedBatchStatus(msg) } - const txHash: string = await this.buildAndSendRollupBatchTransaction( - stateBatch - ) - if (!txHash) { + if (await this.shouldSubmitBatch(stateBatch)) { + try { + const txHash: string = await this.buildAndSendRollupBatchTransaction( + stateBatch + ) + if (!txHash) { + return false + } + stateBatch.submissionTxHash = txHash + } catch (e) { + logError( + log, + `Error submitting state root batch number ${stateBatch.batchNumber}.`, + e + ) + return false + } + } + + try { + return this.waitForProofThatTransactionSucceeded( + stateBatch.submissionTxHash, + stateBatch + ) + } catch (e) { + logError( + log, + `Error waiting for state batch ${stateBatch.batchNumber} with hash ${stateBatch.submissionTxHash} to succeed!`, + e + ) return false } - return this.waitForProofThatTransactionSucceeded(txHash, stateBatch) + } + + protected async shouldSubmitBatch(batchSubmission): Promise { + return ( + batchSubmission.status === BatchSubmissionStatus.QUEUED || + !(await isTxSubmitted( + this.stateCommitmentChain.provider, + batchSubmission.submissionTxHash + )) + ) + } + + protected async getSignedRollupBatchTx( + stateRoots: string[], + startIndex: number + ): Promise { + return getSignedTransaction( + this.stateCommitmentChain, + 'appendStateBatch', + [stateRoots, startIndex], + this.submitterWallet + ) } /** @@ -81,17 +140,38 @@ export class StateCommitmentChainBatchSubmitter extends ScheduledTask { const stateRoots: string[] = stateRootBatch.stateRoots log.debug( - `Appending state root batch number: ${stateRootBatch.batchNumber} with ${stateRoots.length} state roots.` + `Appending state root batch number: ${stateRootBatch.batchNumber} with ${stateRoots.length} state roots at index ${stateRootBatch.startIndex}.` ) - const txRes: TransactionResponse = await this.stateCommitmentChain.appendStateBatch( + const signedTx: string = await this.getSignedRollupBatchTx( stateRoots, stateRootBatch.startIndex ) + + txHash = keccak256(signedTx, true) + await this.dataService.markStateRootBatchSubmittingToL1( + stateRootBatch.batchNumber, + txHash + ) + log.debug( - `State Root batch ${stateRootBatch.batchNumber} appended with at least one confirmation! Tx Hash: ${txRes.hash}` + `Marked tx ${txHash} for state batch ${stateRootBatch.batchNumber} as submitting.` ) - txHash = txRes.hash + + const txRes: TransactionResponse = await this.stateCommitmentChain.provider.sendTransaction( + signedTx + ) + + log.debug( + `Tx batch ${stateRootBatch.batchNumber} was sent to the state commitment chain! Tx Hash: ${txRes.hash}` + ) + + if (txHash !== txRes.hash) { + log.warn( + `Received tx hash not the same as calculated hash! Received: ${txRes.hash}, calculated: ${txHash}` + ) + txHash = txRes.hash + } } catch (e) { logError( log, diff --git a/packages/rollup-core/src/app/data/data-service.ts b/packages/rollup-core/src/app/data/data-service.ts index b3f321bf1ba4b..882ae5416c0f2 100644 --- a/packages/rollup-core/src/app/data/data-service.ts +++ b/packages/rollup-core/src/app/data/data-service.ts @@ -641,7 +641,7 @@ export class DefaultDataService implements DataService { WHERE batch_number = ( SELECT MIN(batch_number) FROM canonical_chain_batch - WHERE status = '${BatchSubmissionStatus.QUEUED}' + WHERE status IN ('${BatchSubmissionStatus.QUEUED}', '${BatchSubmissionStatus.SUBMITTING}') ) ORDER BY block_number ASC, tx_index ASC` ) @@ -720,6 +720,20 @@ export class DefaultDataService implements DataService { } } + /** + * @inheritDoc + */ + public async markTransactionBatchSubmittingToL1( + batchNumber: number, + l1TxHash: string + ): Promise { + return this.rdb.execute( + `UPDATE canonical_chain_batch + SET status = '${BatchSubmissionStatus.SUBMITTING}', submission_tx_hash = '${l1TxHash}' + WHERE batch_number = ${batchNumber}` + ) + } + /** * @inheritDoc */ @@ -762,7 +776,7 @@ export class DefaultDataService implements DataService { WHERE batch_number = ( SELECT MIN(batch_number) FROM state_commitment_chain_batch - WHERE status = '${BatchSubmissionStatus.QUEUED}' + WHERE status IN ('${BatchSubmissionStatus.QUEUED}', '${BatchSubmissionStatus.SUBMITTING}') ) ORDER BY block_number ASC, tx_index ASC` ) @@ -821,6 +835,20 @@ export class DefaultDataService implements DataService { } } + /** + * @inheritDoc + */ + public async markStateRootBatchSubmittingToL1( + batchNumber: number, + l1TxHash: string + ): Promise { + return this.rdb.execute( + `UPDATE state_commitment_chain_batch + SET status = '${BatchSubmissionStatus.SUBMITTING}', submission_tx_hash = '${l1TxHash}' + WHERE batch_number = ${batchNumber}` + ) + } + /** * @inheritDoc */ 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 453236461bc3c..8b13352942cec 100644 --- a/packages/rollup-core/src/types/data/l2-data-service.ts +++ b/packages/rollup-core/src/types/data/l2-data-service.ts @@ -78,6 +78,18 @@ export interface L2DataService { */ getNextCanonicalChainTransactionBatchToFinalize(): Promise + /** + * Marks the Canonical Chain Tx batch with the provided batch number as in the process of being submitted to the L1 chain. + * + * @param batchNumber The batch number to mark as submitting. + * @param l1SubmissionTxHash The tx hash of this rollup batch submission tx on L1. + * @throws An error if there is a DB error. + */ + markTransactionBatchSubmittingToL1( + batchNumber: number, + l1SubmissionTxHash: string + ): Promise + /** * Marks the Canonical Chain Tx batch with the provided batch number as submitted to the L1 chain. * @@ -118,6 +130,18 @@ export interface L2DataService { */ getNextStateCommitmentBatchToFinalize(): Promise + /** + * Marks the StateCommitment batch with the provided batch number as in the process of being submitted to the L1 chain. + * + * @param batchNumber The batch number to mark as submitting. + * @param l1SubmissionTxHash The tx hash of this batch submission tx on L1. + * @throws An error if there is a DB error. + */ + markStateRootBatchSubmittingToL1( + batchNumber: number, + l1SubmissionTxHash: string + ): Promise + /** * Marks the StateCommitment batch with the provided batch number as submitted to the L1 chain. * diff --git a/packages/rollup-core/src/types/data/types.ts b/packages/rollup-core/src/types/data/types.ts index 3409295c55811..c047cee96d67d 100644 --- a/packages/rollup-core/src/types/data/types.ts +++ b/packages/rollup-core/src/types/data/types.ts @@ -8,6 +8,7 @@ export enum QueueOrigin { export enum BatchSubmissionStatus { QUEUED = 'QUEUED', + SUBMITTING = 'SUBMITTING', SENT = 'SENT', FINALIZED = 'FINALIZED', } diff --git a/packages/rollup-core/test/app/canonical-chain-batch-submitter.spec.ts b/packages/rollup-core/test/app/canonical-chain-batch-submitter.spec.ts index 2e311a1f4de52..7b503cbcfb5f2 100644 --- a/packages/rollup-core/test/app/canonical-chain-batch-submitter.spec.ts +++ b/packages/rollup-core/test/app/canonical-chain-batch-submitter.spec.ts @@ -1,5 +1,10 @@ /* External Imports */ -import { keccak256FromUtf8, sleep, TestUtils } from '@eth-optimism/core-utils' +import { + keccak256, + keccak256FromUtf8, + sleep, + TestUtils, +} from '@eth-optimism/core-utils' import { TransactionReceipt, TransactionResponse } from 'ethers/providers' import { Contract, Wallet } from 'ethers' @@ -26,6 +31,7 @@ import { RollupBatchTimestampTooOldError, UnexpectedBatchStatus, } from '../../src/types' +import has = Reflect.has interface BatchNumberHash { batchNumber: number @@ -34,6 +40,10 @@ interface BatchNumberHash { class TestCanonicalChainBatchSubmitter extends CanonicalChainBatchSubmitter { public batchSubmissionBlockNumberOverride: number + public signedRollupBatchTxOverride: string = Buffer.from( + `signed tx`, + 'utf-8' + ).toString('hex') constructor( dataService: L2DataService, @@ -47,6 +57,7 @@ class TestCanonicalChainBatchSubmitter extends CanonicalChainBatchSubmitter { canonicalTransactionChain, l1ToL2QueueContract, safetyQueueContract, + Wallet.createRandom(), periodMilliseconds ) } @@ -57,10 +68,20 @@ class TestCanonicalChainBatchSubmitter extends CanonicalChainBatchSubmitter { } return super.getBatchSubmissionBlockNumber() } + + protected async getSignedRollupBatchTx( + txsCalldata: string[], + timestamp: number, + batchBlockNumber: number, + startIndex: number + ): Promise { + return this.signedRollupBatchTxOverride + } } class MockDataService extends DefaultDataService { public readonly nextBatch: TransactionBatchSubmission[] = [] + public readonly txBatchesSubmitting: BatchNumberHash[] = [] public readonly txBatchesSubmitted: BatchNumberHash[] = [] public readonly txBatchesFinalized: BatchNumberHash[] = [] @@ -74,6 +95,13 @@ class MockDataService extends DefaultDataService { return this.nextBatch.shift() } + public async markTransactionBatchSubmittingToL1( + batchNumber: number, + l1TxHash: string + ): Promise { + this.txBatchesSubmitting.push({ batchNumber, txHash: l1TxHash }) + } + public async markTransactionBatchSubmittedToL1( batchNumber: number, l1TxHash: string @@ -90,12 +118,24 @@ class MockDataService extends DefaultDataService { } class MockProvider { + public readonly submittedTxs: string[] = [] public txReceipts: Map = new Map< string, TransactionReceipt >() + + public txResponses: Map = new Map< + string, + TransactionResponse + >() + + public txExists: boolean = true public blockNumberOverride: number + public async getTransaction(hash: string): Promise { + return this.txExists ? this.txResponses.get(hash) : false + } + public async waitForTransaction( hash: string, numConfirms: number @@ -109,6 +149,15 @@ class MockProvider { public async getBlockNumber(): Promise { return this.blockNumberOverride || this.txReceipts.size } + + public async sendTransaction(signedTx: string): Promise { + const hash: string = keccak256(signedTx) + this.submittedTxs.push(hash) + if (!this.txResponses.has(hash)) { + throw Error(`tx threw`) + } + return this.txResponses.get(hash) + } } class MockCanonicalTransactionChain { @@ -121,19 +170,6 @@ class MockCanonicalTransactionChain { constructor(public readonly provider: MockProvider) {} - public async appendSequencerBatch( - calldata: string, - timestamp: number, - blockNumber: number, - startIndex: number - ): Promise { - const response: TransactionResponse = this.responses.shift() - if (!response) { - throw Error('no response') - } - return response - } - public async lastOVMTimestamp(): Promise { return this.lastOvmTimestampSeconds } @@ -204,6 +240,14 @@ describe('Canonical Chain Batch Submitter', () => { res.should.equal(false, 'Incorrect result when there are no batches') + canonicalProvider.submittedTxs.length.should.equal( + 0, + 'No batches should have been appended!' + ) + dataService.txBatchesSubmitting.length.should.equal( + 0, + 'No tx batches should have been submitting!' + ) dataService.txBatchesSubmitted.length.should.equal( 0, 'No tx batches should have been submitted!' @@ -239,6 +283,14 @@ describe('Canonical Chain Batch Submitter', () => { await batchSubmitter.runTask(true) }, UnexpectedBatchStatus) + canonicalProvider.submittedTxs.length.should.equal( + 0, + 'No batches should have been appended!' + ) + dataService.txBatchesSubmitting.length.should.equal( + 0, + 'No tx batches should have been submitting!' + ) dataService.txBatchesSubmitted.length.should.equal( 0, 'No tx batches should have been submitted!' @@ -249,8 +301,8 @@ describe('Canonical Chain Batch Submitter', () => { ) }) - it('should send txs if there is a batch', async () => { - const hash: string = keccak256FromUtf8('l1 tx hash') + it('should send txs if there is a QUEUED batch', async () => { + const hash: string = keccak256(batchSubmitter.signedRollupBatchTxOverride) const batchNumber: number = 1 dataService.nextBatch.push({ submissionTxHash: undefined, @@ -274,10 +326,141 @@ describe('Canonical Chain Batch Submitter', () => { canonicalTransactionChain.responses.push({ hash } as any) canonicalProvider.txReceipts.set(hash, { status: 1 } as any) + canonicalProvider.txResponses.set(hash, { hash } as any) const res: boolean = await batchSubmitter.runTask(true) res.should.equal(true, `Batch should have been submitted successfully.`) + canonicalProvider.submittedTxs.length.should.equal( + 1, + 'batch should have been appended!' + ) + canonicalProvider.submittedTxs[0].should.equal( + hash, + 'batch number mismatch!' + ) + + dataService.txBatchesSubmitting.length.should.equal( + 1, + 'No tx batches marked submitting!' + ) + 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.txBatchesFinalized.length.should.equal( + 0, + 'No tx batches should be confirmed!' + ) + }) + + it('should submit tx if there is a batch in SUBMITTING that has not been submitted', async () => { + const hash: string = keccak256(batchSubmitter.signedRollupBatchTxOverride) + const batchNumber: number = 1 + dataService.nextBatch.push({ + submissionTxHash: hash, + status: BatchSubmissionStatus.SUBMITTING, + 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) + canonicalProvider.txReceipts.set(hash, { status: 1 } as any) + canonicalProvider.txResponses.set(hash, { hash } as any) + canonicalProvider.txExists = false + + const res: boolean = await batchSubmitter.runTask(true) + res.should.equal(true, `Batch should have been submitted successfully.`) + + canonicalProvider.submittedTxs.length.should.equal( + 1, + 'No batches should have been appended!' + ) + + dataService.txBatchesSubmitting.length.should.equal( + 1, + 'No tx batches should be marked submitting!' + ) + 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.txBatchesFinalized.length.should.equal( + 0, + 'No tx batches should be confirmed!' + ) + }) + + it('should wait for tx confirmation if there is a batch in SUBMITTING that has been submitted', async () => { + const hash: string = keccak256(batchSubmitter.signedRollupBatchTxOverride) + const batchNumber: number = 1 + dataService.nextBatch.push({ + submissionTxHash: hash, + status: BatchSubmissionStatus.SUBMITTING, + 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) + canonicalProvider.txReceipts.set(hash, { status: 1 } as any) + canonicalProvider.txResponses.set(hash, { hash } as any) + + const res: boolean = await batchSubmitter.runTask(true) + res.should.equal(true, `Batch should have been submitted successfully.`) + + canonicalProvider.submittedTxs.length.should.equal( + 0, + 'No batches should have been appended!' + ) + + dataService.txBatchesSubmitting.length.should.equal( + 0, + 'No tx batches should be marked submitting!' + ) dataService.txBatchesSubmitted.length.should.equal( 1, 'No tx batches submitted!' @@ -326,14 +509,19 @@ describe('Canonical Chain Batch Submitter', () => { const res: boolean = await batchSubmitter.runTask(true) res.should.equal(false, `Batch tx should have errored out.`) + dataService.txBatchesSubmitting.length.should.equal( + 1, + 'Tx batch marked submitting!' + ) + dataService.txBatchesSubmitted.length.should.equal( 0, - 'No tx batches submitted!' + 'Tx batches submitted!' ) dataService.txBatchesFinalized.length.should.equal( 0, - 'No tx batches should be confirmed!' + 'Tx batches should be confirmed!' ) }) diff --git a/packages/rollup-core/test/app/state-commitment-chain-batch-submitter.spec.ts b/packages/rollup-core/test/app/state-commitment-chain-batch-submitter.spec.ts index 1e5ddfe71f32f..0aab0887bb687 100644 --- a/packages/rollup-core/test/app/state-commitment-chain-batch-submitter.spec.ts +++ b/packages/rollup-core/test/app/state-commitment-chain-batch-submitter.spec.ts @@ -1,7 +1,12 @@ /* External Imports */ -import { keccak256FromUtf8, sleep, TestUtils } from '@eth-optimism/core-utils' +import { + keccak256, + keccak256FromUtf8, + sleep, + TestUtils, +} from '@eth-optimism/core-utils' import { TransactionReceipt, TransactionResponse } from 'ethers/providers' -import { Wallet } from 'ethers' +import { Contract, Wallet } from 'ethers' /* Internal Imports */ import { @@ -10,6 +15,7 @@ import { } from '../../src/app/data' import { BatchSubmissionStatus, + L2DataService, StateCommitmentBatchSubmission, } from '../../src/types/data' import { StateCommitmentChainBatchSubmitter } from '../../src/app/data/consumers/state-commitment-chain-batch-submitter' @@ -20,8 +26,36 @@ interface BatchNumberHash { txHash: string } +class TestStateCommitmentChainBatchSubmitter extends StateCommitmentChainBatchSubmitter { + public signedRollupStateRootBatchTxOverride: string = Buffer.from( + `signed tx`, + 'utf-8' + ).toString('hex') + + constructor( + dataService: L2DataService, + stateCommitmentChainContract: Contract, + periodMilliseconds = 10_000 + ) { + super( + dataService, + stateCommitmentChainContract, + Wallet.createRandom(), + periodMilliseconds + ) + } + + protected async getSignedRollupBatchTx( + stateRoots: string[], + startIndex: number + ): Promise { + return this.signedRollupStateRootBatchTxOverride + } +} + class MockDataService extends DefaultDataService { public readonly nextBatch: StateCommitmentBatchSubmission[] = [] + public readonly stateRootBatchesSubmitting: BatchNumberHash[] = [] public readonly stateRootBatchesSubmitted: BatchNumberHash[] = [] public readonly stateRootBatchesFinalized: BatchNumberHash[] = [] @@ -35,6 +69,13 @@ class MockDataService extends DefaultDataService { return this.nextBatch.shift() } + public async markStateRootBatchSubmittingToL1( + batchNumber: number, + l1TxHash: string + ): Promise { + this.stateRootBatchesSubmitting.push({ batchNumber, txHash: l1TxHash }) + } + public async markStateRootBatchSubmittedToL1( batchNumber: number, l1TxHash: string @@ -51,11 +92,24 @@ class MockDataService extends DefaultDataService { } class MockProvider { + public readonly submittedTxs: string[] = [] public txReceipts: Map = new Map< string, TransactionReceipt >() + public txResponses: Map = new Map< + string, + TransactionResponse + >() + + public txExists: boolean = true + public blockNumberOverride: number + + public async getTransaction(hash: string): Promise { + return this.txExists ? this.txResponses.get(hash) : false + } + public async waitForTransaction( hash: string, numConfirms: number @@ -65,26 +119,29 @@ class MockProvider { } return this.txReceipts.get(hash) } + + public async getBlockNumber(): Promise { + return this.blockNumberOverride || this.txReceipts.size + } + + public async sendTransaction(signedTx: string): Promise { + const hash: string = keccak256(signedTx) + this.submittedTxs.push(hash) + if (!this.txResponses.has(hash)) { + throw Error(`tx threw`) + } + return this.txResponses.get(hash) + } } class MockStateCommitmentChain { public responses: TransactionResponse[] = [] constructor(public readonly provider: MockProvider) {} - - public async appendStateBatch( - stateRoots: string[] - ): Promise { - const response: TransactionResponse = this.responses.shift() - if (!response) { - throw Error('no response') - } - return response - } } describe('State Commitment Chain Batch Submitter', () => { - let batchSubmitter: StateCommitmentChainBatchSubmitter + let batchSubmitter: TestStateCommitmentChainBatchSubmitter let dataService: MockDataService let provider: MockProvider let stateCommitmentChain: MockStateCommitmentChain @@ -93,7 +150,7 @@ describe('State Commitment Chain Batch Submitter', () => { dataService = new MockDataService() provider = new MockProvider() stateCommitmentChain = new MockStateCommitmentChain(provider) - batchSubmitter = new StateCommitmentChainBatchSubmitter( + batchSubmitter = new TestStateCommitmentChainBatchSubmitter( dataService, stateCommitmentChain as any ) @@ -104,6 +161,14 @@ describe('State Commitment Chain Batch Submitter', () => { res.should.equal(false, 'Incorrect result when there are no batches') + provider.submittedTxs.length.should.equal( + 0, + `No state batches should have been appended!` + ) + dataService.stateRootBatchesSubmitting.length.should.equal( + 0, + 'No state root batches should have been marked as submitting!' + ) dataService.stateRootBatchesSubmitted.length.should.equal( 0, 'No state root batches should have been submitted!' @@ -126,6 +191,14 @@ describe('State Commitment Chain Batch Submitter', () => { await batchSubmitter.runTask() }, UnexpectedBatchStatus) + provider.submittedTxs.length.should.equal( + 0, + `No state batches should have been appended!` + ) + dataService.stateRootBatchesSubmitting.length.should.equal( + 0, + 'No state root batches should have been marked as submitting!' + ) dataService.stateRootBatchesSubmitted.length.should.equal( 0, 'No state root batches should have been submitted!' @@ -136,15 +209,17 @@ describe('State Commitment Chain Batch Submitter', () => { ) }) - it('should send roots if there is a batch', async () => { - const hash: string = keccak256FromUtf8('tx hash') + it('should send roots if there is a batch in QUEUED state', async () => { + const hash: string = keccak256( + batchSubmitter.signedRollupStateRootBatchTxOverride + ) const stateRoots: string[] = [ keccak256FromUtf8('root 1'), keccak256FromUtf8('root 2'), ] const batchNumber: number = 1 dataService.nextBatch.push({ - submissionTxHash: undefined, + submissionTxHash: hash, status: BatchSubmissionStatus.QUEUED, batchNumber, stateRoots, @@ -152,10 +227,120 @@ describe('State Commitment Chain Batch Submitter', () => { stateCommitmentChain.responses.push({ hash } as any) provider.txReceipts.set(hash, { status: 1 } as any) + provider.txResponses.set(hash, { hash } as any) const res: boolean = await batchSubmitter.runTask() res.should.equal(true, `Batch should have been submitted successfully.`) + provider.submittedTxs.length.should.equal( + 1, + `1 State batch should have been appended!` + ) + dataService.stateRootBatchesSubmitting.length.should.equal( + 1, + 'No state root batches marked as submitting!' + ) + dataService.stateRootBatchesSubmitted.length.should.equal( + 1, + 'No state root batches submitted!' + ) + dataService.stateRootBatchesSubmitted[0].txHash.should.equal( + hash, + 'Incorrect tx hash submitted!' + ) + dataService.stateRootBatchesSubmitted[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number submitted!' + ) + + dataService.stateRootBatchesFinalized.length.should.equal( + 0, + 'No state root batches should be confirmed!' + ) + }) + + it('should wait for tx confirmation there is a batch in SUBMITTING status that has been submitted', async () => { + const hash: string = keccak256( + batchSubmitter.signedRollupStateRootBatchTxOverride + ) + const stateRoots: string[] = [ + keccak256FromUtf8('root 1'), + keccak256FromUtf8('root 2'), + ] + const batchNumber: number = 1 + dataService.nextBatch.push({ + submissionTxHash: hash, + status: BatchSubmissionStatus.SUBMITTING, + batchNumber, + stateRoots, + }) + + stateCommitmentChain.responses.push({ hash } as any) + provider.txReceipts.set(hash, { status: 1 } as any) + provider.txResponses.set(hash, { hash } as any) + + const res: boolean = await batchSubmitter.runTask() + res.should.equal(true, `Batch should have been submitted successfully.`) + + provider.submittedTxs.length.should.equal( + 0, + `Batch should not be re-submitted!` + ) + dataService.stateRootBatchesSubmitting.length.should.equal( + 0, + 'Batch should not be marked as submitting again!' + ) + dataService.stateRootBatchesSubmitted.length.should.equal( + 1, + 'No state root batches submitted!' + ) + dataService.stateRootBatchesSubmitted[0].txHash.should.equal( + hash, + 'Incorrect tx hash submitted!' + ) + dataService.stateRootBatchesSubmitted[0].batchNumber.should.equal( + batchNumber, + 'Incorrect tx batch number submitted!' + ) + + dataService.stateRootBatchesFinalized.length.should.equal( + 0, + 'No state root batches should be confirmed!' + ) + }) + + it('should wait for tx confirmation there is a batch in SUBMITTING status that has not been submitted', async () => { + const hash: string = keccak256( + batchSubmitter.signedRollupStateRootBatchTxOverride + ) + const stateRoots: string[] = [ + keccak256FromUtf8('root 1'), + keccak256FromUtf8('root 2'), + ] + const batchNumber: number = 1 + dataService.nextBatch.push({ + submissionTxHash: hash, + status: BatchSubmissionStatus.SUBMITTING, + batchNumber, + stateRoots, + }) + + stateCommitmentChain.responses.push({ hash } as any) + provider.txReceipts.set(hash, { status: 1 } as any) + provider.txResponses.set(hash, { hash } as any) + provider.txExists = false + + const res: boolean = await batchSubmitter.runTask() + res.should.equal(true, `Batch should have been submitted successfully.`) + + provider.submittedTxs.length.should.equal( + 1, + `Batch should not be re-submitted!` + ) + dataService.stateRootBatchesSubmitting.length.should.equal( + 1, + 'Batch should not be marked as submitting again!' + ) dataService.stateRootBatchesSubmitted.length.should.equal( 1, 'No state root batches submitted!' @@ -176,7 +361,9 @@ describe('State Commitment Chain Batch Submitter', () => { }) it('should not mark batch as submitted if batch submission tx fails', async () => { - const hash: string = keccak256FromUtf8('tx hash') + const hash: string = keccak256( + batchSubmitter.signedRollupStateRootBatchTxOverride + ) const stateRoots: string[] = [ keccak256FromUtf8('root 1'), keccak256FromUtf8('root 2'), @@ -191,10 +378,20 @@ describe('State Commitment Chain Batch Submitter', () => { stateCommitmentChain.responses.push({ hash } as any) provider.txReceipts.set(hash, { status: 0 } as any) + provider.txResponses.set(hash, { hash } as any) const res: boolean = await batchSubmitter.runTask() res.should.equal(false, `Batch tx should have errored out.`) + provider.submittedTxs.length.should.equal( + 1, + `1 State batch should have been appended!` + ) + provider.submittedTxs[0].should.equal(hash, `Incorrect tx submitted!`) + dataService.stateRootBatchesSubmitting.length.should.equal( + 1, + 'No state root batches marked as submitting!' + ) dataService.stateRootBatchesSubmitted.length.should.equal( 0, 'No state root batches should have been submitted!' diff --git a/packages/rollup-services/src/exec/services.ts b/packages/rollup-services/src/exec/services.ts index 958dae40290aa..a1f334496bf58 100644 --- a/packages/rollup-services/src/exec/services.ts +++ b/packages/rollup-services/src/exec/services.ts @@ -332,6 +332,7 @@ const createCanonicalChainBatchSubmitter = (): CanonicalChainBatchSubmitter => { canonicalTxChainContract, l1ToL2TransactionChainContract, safetyQueueContract, + getSequencerWallet(), period ) } @@ -406,6 +407,7 @@ const createStateCommitmentChainBatchSubmitter = (): StateCommitmentChainBatchSu getContractDefinition('StateCommitmentChain').abi, getStateRootSubmissionWallet() ), + getStateRootSubmissionWallet(), period ) }