From 1a4a53288074d334d3805fef0753bad9363d6dd1 Mon Sep 17 00:00:00 2001 From: Annie Ke Date: Wed, 31 Mar 2021 09:35:19 -0700 Subject: [PATCH] [Feat] Concurrent block fetching in state batch submitter (#66) * concurrently fetch state batch blocks, remove concurrency restraint * bug fix and make async/await * add state batch submitter unit test Co-authored-by: Karl Floersch --- src/batch-submitter/state-batch-submitter.ts | 36 +- src/batch-submitter/tx-batch-submitter.ts | 2 +- test/batch-submitter/batch-submitter.spec.ts | 548 ++++++++++++++++++ test/batch-submitter/mockchain-provider.ts | 2 + .../transaction-batch-submitter.spec.ts | 435 -------------- yarn.lock | 2 +- 6 files changed, 571 insertions(+), 454 deletions(-) create mode 100644 test/batch-submitter/batch-submitter.spec.ts delete mode 100644 test/batch-submitter/transaction-batch-submitter.spec.ts diff --git a/src/batch-submitter/state-batch-submitter.ts b/src/batch-submitter/state-batch-submitter.ts index f7ca854..e3273c0 100644 --- a/src/batch-submitter/state-batch-submitter.ts +++ b/src/batch-submitter/state-batch-submitter.ts @@ -1,4 +1,5 @@ /* External Imports */ +import { Promise as bPromise } from 'bluebird' import { Contract, Signer } from 'ethers' import { TransactionReceipt } from '@ethersproject/abstract-provider' import { getContractFactory } from '@eth-optimism/contracts' @@ -108,14 +109,13 @@ export class StateBatchSubmitter extends BatchSubmitter { const startBlock: number = (await this.chainContract.getTotalElements()).toNumber() + BLOCK_OFFSET // We will submit state roots for txs which have been in the tx chain for a while. - const callBlockNumber: number = - (await this.signer.provider.getBlockNumber()) - this.finalityConfirmations const totalElements: number = (await this.ctcContract.getTotalElements()).toNumber() + BLOCK_OFFSET const endBlock: number = Math.min( startBlock + this.maxBatchSize, totalElements ) + if (startBlock >= endBlock) { if (startBlock > endBlock) { this.log.error( @@ -172,21 +172,22 @@ export class StateBatchSubmitter extends BatchSubmitter { startBlock: number, endBlock: number ): Promise { - const batch: Bytes32[] = [] - - for (let i = startBlock; i < endBlock; i++) { - const block = (await this.l2Provider.getBlockWithTransactions( - i - )) as L2Block - if (block.transactions[0].from === this.fraudSubmissionAddress) { - batch.push( - '0xbad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1' - ) - this.fraudSubmissionAddress = 'no fraud' - } else { - batch.push(block.stateRoot) - } - } + const blockRange = endBlock - startBlock + const batch: Bytes32[] = await bPromise.map( + [...Array(blockRange).keys()], + async (i: number) => { + this.log.debug('Fetching L2BatchElement', { blockNo: startBlock + i }) + const block = (await this.l2Provider.getBlockWithTransactions( + startBlock + i + )) as L2Block + if (block.transactions[0].from === this.fraudSubmissionAddress) { + this.fraudSubmissionAddress = 'no fraud' + return '0xbad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1' + } + return block.stateRoot + }, + { concurrency: 100 } + ) let tx = this.chainContract.interface.encodeFunctionData( 'appendStateBatch', @@ -199,6 +200,7 @@ export class StateBatchSubmitter extends BatchSubmitter { startBlock, ]) } + return batch } } diff --git a/src/batch-submitter/tx-batch-submitter.ts b/src/batch-submitter/tx-batch-submitter.ts index 5548ee2..8193cb8 100644 --- a/src/batch-submitter/tx-batch-submitter.ts +++ b/src/batch-submitter/tx-batch-submitter.ts @@ -283,7 +283,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter { this.log.debug('Fetching L2BatchElement', { blockNo: startBlock + i }) return this._getL2BatchElement(startBlock + i) }, - { concurrency: 50 } + { concurrency: 100 } ) // Fix our batches if we are configured to. TODO: Remove this. diff --git a/test/batch-submitter/batch-submitter.spec.ts b/test/batch-submitter/batch-submitter.spec.ts new file mode 100644 index 0000000..f6327b9 --- /dev/null +++ b/test/batch-submitter/batch-submitter.spec.ts @@ -0,0 +1,548 @@ +import { expect } from '../setup' + +/* External Imports */ +import { ethers } from 'hardhat' +import '@nomiclabs/hardhat-ethers' +import { Signer, ContractFactory, Contract, BigNumber } from 'ethers' +import ganache from 'ganache-core' +import sinon from 'sinon' +import { Web3Provider } from '@ethersproject/providers' +import { getContractInterface } from '@eth-optimism/contracts' +import * as scc from '@eth-optimism/contracts/build/artifacts/OVM_StateCommitmentChain.json' +import { smockit, MockContract } from '@eth-optimism/smock' + +/* Internal Imports */ +import { MockchainProvider } from './mockchain-provider' +import { + makeAddressManager, + setProxyTarget, + FORCE_INCLUSION_PERIOD_SECONDS, + getContractFactory, +} from '../helpers' +import { + CanonicalTransactionChainContract, + QueueOrigin, + TransactionBatchSubmitter as RealTransactionBatchSubmitter, + StateBatchSubmitter, + TX_BATCH_SUBMITTER_LOG_TAG, + STATE_BATCH_SUBMITTER_LOG_TAG, + Batch, + BatchSubmitter, +} from '../../src' + +import { + Signature, + TxType, + ctcCoder, + remove0x, + Logger, +} from '@eth-optimism/core-utils' + +const DECOMPRESSION_ADDRESS = '0x4200000000000000000000000000000000000008' +const DUMMY_ADDRESS = '0x' + '00'.repeat(20) +const EXAMPLE_STATE_ROOT = + '0x16b7f83f409c7195b1f4fde5652f1b54a4477eacb6db7927691becafba5f8801' +const MAX_GAS_LIMIT = 8_000_000 +const MAX_TX_SIZE = 100_000 +const MIN_TX_SIZE = 1_000 +const MIN_GAS_PRICE_IN_GWEI = 1 +const MAX_GAS_PRICE_IN_GWEI = 70 +const GAS_RETRY_INCREMENT = 5 +const GAS_THRESHOLD_IN_GWEI = 120 + +// Helper functions +interface QueueElement { + queueRoot: string + timestamp: number + blockNumber: number +} +const getQueueElement = async ( + ctcContract: Contract, + nextQueueIndex?: number +): Promise => { + if (!nextQueueIndex) { + nextQueueIndex = await ctcContract.getNextQueueIndex() + } + const nextQueueElement = await ctcContract.getQueueElement(nextQueueIndex) + return nextQueueElement +} +const DUMMY_SIG: Signature = { + r: '11'.repeat(32), + s: '22'.repeat(32), + v: 1, +} +// A transaction batch submitter which skips the validate batch check +class TransactionBatchSubmitter extends RealTransactionBatchSubmitter { + protected async _validateBatch(batch: Batch): Promise { + return true + } +} + +describe('BatchSubmitter', () => { + let signer: Signer + let sequencer: Signer + before(async () => { + ;[signer, sequencer] = await ethers.getSigners() + }) + + let AddressManager: Contract + let Mock__OVM_ExecutionManager: MockContract + let Mock__OVM_BondManager: MockContract + let Mock__OVM_StateCommitmentChain: MockContract + before(async () => { + AddressManager = await makeAddressManager() + await AddressManager.setAddress( + 'OVM_Sequencer', + await sequencer.getAddress() + ) + await AddressManager.setAddress( + 'OVM_DecompressionPrecompileAddress', + DECOMPRESSION_ADDRESS + ) + + Mock__OVM_ExecutionManager = await smockit( + await getContractFactory('OVM_ExecutionManager') + ) + + Mock__OVM_BondManager = await smockit( + await getContractFactory('OVM_BondManager') + ) + + Mock__OVM_StateCommitmentChain = await smockit( + await getContractFactory('OVM_StateCommitmentChain') + ) + + await setProxyTarget( + AddressManager, + 'OVM_ExecutionManager', + Mock__OVM_ExecutionManager + ) + + await setProxyTarget( + AddressManager, + 'OVM_BondManager', + Mock__OVM_BondManager + ) + + await setProxyTarget( + AddressManager, + 'OVM_StateCommitmentChain', + Mock__OVM_StateCommitmentChain + ) + + Mock__OVM_StateCommitmentChain.smocked.canOverwrite.will.return.with(false) + Mock__OVM_ExecutionManager.smocked.getMaxTransactionGasLimit.will.return.with( + MAX_GAS_LIMIT + ) + Mock__OVM_BondManager.smocked.isCollateralized.will.return.with(true) + }) + + let Factory__OVM_CanonicalTransactionChain: ContractFactory + let Factory__OVM_StateCommitmentChain: ContractFactory + before(async () => { + Factory__OVM_CanonicalTransactionChain = await getContractFactory( + 'OVM_CanonicalTransactionChain' + ) + + Factory__OVM_StateCommitmentChain = await getContractFactory( + 'OVM_StateCommitmentChain' + ) + }) + + let OVM_CanonicalTransactionChain: CanonicalTransactionChainContract + let OVM_StateCommitmentChain: Contract + let l2Provider: MockchainProvider + beforeEach(async () => { + const unwrapped_OVM_CanonicalTransactionChain = await Factory__OVM_CanonicalTransactionChain.deploy( + AddressManager.address, + FORCE_INCLUSION_PERIOD_SECONDS + ) + await unwrapped_OVM_CanonicalTransactionChain.init() + + await AddressManager.setAddress( + 'OVM_CanonicalTransactionChain', + unwrapped_OVM_CanonicalTransactionChain.address + ) + + OVM_CanonicalTransactionChain = new CanonicalTransactionChainContract( + unwrapped_OVM_CanonicalTransactionChain.address, + getContractInterface('OVM_CanonicalTransactionChain'), + sequencer + ) + + const unwrapped_OVM_StateCommitmentChain = await Factory__OVM_StateCommitmentChain.deploy( + AddressManager.address, + 0, // fraudProofWindowSeconds + 0 // sequencerPublishWindowSeconds + ) + + await unwrapped_OVM_StateCommitmentChain.init() + + await AddressManager.setAddress( + 'OVM_StateCommitmentChain', + unwrapped_OVM_StateCommitmentChain.address + ) + + OVM_StateCommitmentChain = new Contract( + unwrapped_OVM_StateCommitmentChain.address, + getContractInterface('OVM_StateCommitmentChain'), + sequencer + ) + + l2Provider = new MockchainProvider( + OVM_CanonicalTransactionChain.address, + OVM_StateCommitmentChain.address + ) + }) + + afterEach(() => { + sinon.restore() + }) + + const createBatchSubmitter = (timeout: number): TransactionBatchSubmitter => + new TransactionBatchSubmitter( + sequencer, + l2Provider as any, + MIN_TX_SIZE, + MAX_TX_SIZE, + 10, + timeout, + 1, + 100000, + AddressManager.address, + 1, + MIN_GAS_PRICE_IN_GWEI, + MAX_GAS_PRICE_IN_GWEI, + GAS_RETRY_INCREMENT, + GAS_THRESHOLD_IN_GWEI, + new Logger({ name: TX_BATCH_SUBMITTER_LOG_TAG }), + false + ) + + describe('TransactionBatchSubmitter', () => { + describe('submitNextBatch', () => { + const enqueuedElements: Array<{ + blockNumber: number + timestamp: number + }> = [] + + let batchSubmitter + beforeEach(async () => { + for (let i = 1; i < 15; i++) { + await OVM_CanonicalTransactionChain.enqueue( + '0x' + '01'.repeat(20), + 50_000, + '0x' + i.toString().repeat(64), + { + gasLimit: 1_000_000, + } + ) + } + batchSubmitter = createBatchSubmitter(0) + }) + + it('should submit a sequencer batch correctly', async () => { + l2Provider.setNumBlocksToReturn(5) + const nextQueueElement = await getQueueElement( + OVM_CanonicalTransactionChain + ) + const data = ctcCoder.eip155TxData.encode({ + sig: DUMMY_SIG, + gasLimit: 0, + gasPrice: 0, + nonce: 0, + target: DUMMY_ADDRESS, + data: '0x', + type: TxType.EIP155, + }) + l2Provider.setL2BlockData( + { + data, + l1BlockNumber: nextQueueElement.blockNumber - 1, + txType: TxType.EIP155, + queueOrigin: QueueOrigin.Sequencer, + l1TxOrigin: null, + } as any, + nextQueueElement.timestamp - 1 + ) + let receipt = await batchSubmitter.submitNextBatch() + let logData = remove0x(receipt.logs[1].data) + expect(parseInt(logData.slice(64 * 0, 64 * 1), 16)).to.equal(0) // _startingQueueIndex + expect(parseInt(logData.slice(64 * 1, 64 * 2), 16)).to.equal(0) // _numQueueElements + expect(parseInt(logData.slice(64 * 2, 64 * 3), 16)).to.equal(6) // _totalElements + receipt = await batchSubmitter.submitNextBatch() + logData = remove0x(receipt.logs[1].data) + expect(parseInt(logData.slice(64 * 0, 64 * 1), 16)).to.equal(0) // _startingQueueIndex + expect(parseInt(logData.slice(64 * 1, 64 * 2), 16)).to.equal(0) // _numQueueElements + expect(parseInt(logData.slice(64 * 2, 64 * 3), 16)).to.equal(11) // _totalElements + }) + + it('should submit a queue batch correctly', async () => { + l2Provider.setNumBlocksToReturn(5) + l2Provider.setL2BlockData({ + queueOrigin: QueueOrigin.L1ToL2, + } as any) + let receipt = await batchSubmitter.submitNextBatch() + let logData = remove0x(receipt.logs[1].data) + expect(parseInt(logData.slice(64 * 0, 64 * 1), 16)).to.equal(0) // _startingQueueIndex + expect(parseInt(logData.slice(64 * 1, 64 * 2), 16)).to.equal(6) // _numQueueElements + expect(parseInt(logData.slice(64 * 2, 64 * 3), 16)).to.equal(6) // _totalElements + receipt = await batchSubmitter.submitNextBatch() + logData = remove0x(receipt.logs[1].data) + expect(parseInt(logData.slice(64 * 0, 64 * 1), 16)).to.equal(6) // _startingQueueIndex + expect(parseInt(logData.slice(64 * 1, 64 * 2), 16)).to.equal(5) // _numQueueElements + expect(parseInt(logData.slice(64 * 2, 64 * 3), 16)).to.equal(11) // _totalElements + }) + + it('should submit a batch with both queue and sequencer chain elements', async () => { + l2Provider.setNumBlocksToReturn(10) // For this batch we'll return 10 elements! + l2Provider.setL2BlockData({ + queueOrigin: QueueOrigin.L1ToL2, + } as any) + // Turn blocks 3-5 into sequencer txs + const nextQueueElement = await getQueueElement( + OVM_CanonicalTransactionChain, + 2 + ) + const data = ctcCoder.ethSignTxData.encode({ + sig: DUMMY_SIG, + gasLimit: 0, + gasPrice: 0, + nonce: 0, + target: DUMMY_ADDRESS, + data: '0x', + type: TxType.EthSign, + }) + l2Provider.setL2BlockData( + { + data, + l1BlockNumber: nextQueueElement.blockNumber - 1, + txType: TxType.EthSign, + queueOrigin: QueueOrigin.Sequencer, + l1TxOrigin: null, + } as any, + nextQueueElement.timestamp - 1, + '', // blank stateRoot + 3, + 6 + ) + const receipt = await batchSubmitter.submitNextBatch() + const logData = remove0x(receipt.logs[1].data) + expect(parseInt(logData.slice(64 * 0, 64 * 1), 16)).to.equal(0) // _startingQueueIndex + expect(parseInt(logData.slice(64 * 1, 64 * 2), 16)).to.equal(8) // _numQueueElements + expect(parseInt(logData.slice(64 * 2, 64 * 3), 16)).to.equal(11) // _totalElements + }) + + it('should submit a small batch only after the timeout', async () => { + l2Provider.setNumBlocksToReturn(2) + l2Provider.setL2BlockData({ + queueOrigin: QueueOrigin.L1ToL2, + } as any) + + // Create a batch submitter with a long timeout & make sure it doesn't submit the batches one after another + const longTimeout = 10_000 + batchSubmitter = createBatchSubmitter(longTimeout) + let receipt = await batchSubmitter.submitNextBatch() + expect(receipt).to.not.be.undefined + receipt = await batchSubmitter.submitNextBatch() + // The receipt should be undefined because that means it didn't submit + expect(receipt).to.be.undefined + + // This time create a batch submitter with a short timeout & it should submit batches after the timeout is reached + const shortTimeout = 5 + batchSubmitter = createBatchSubmitter(shortTimeout) + receipt = await batchSubmitter.submitNextBatch() + expect(receipt).to.not.be.undefined + // Sleep for the short timeout + await new Promise((r) => setTimeout(r, shortTimeout)) + receipt = await batchSubmitter.submitNextBatch() + // The receipt should NOT be undefined because that means it successfully submitted! + expect(receipt).to.not.be.undefined + }) + + it('should not submit if gas price is over threshold', async () => { + l2Provider.setNumBlocksToReturn(2) + l2Provider.setL2BlockData({ + queueOrigin: QueueOrigin.L1ToL2, + } as any) + + const highGasPriceWei = BigNumber.from(200).mul(1_000_000_000) + + sinon + .stub(sequencer, 'getGasPrice') + .callsFake(async () => highGasPriceWei) + + const receipt = await batchSubmitter.submitNextBatch() + expect(sequencer.getGasPrice).to.have.been.calledOnce + expect(receipt).to.be.undefined + }) + + it('should submit if gas price is not over threshold', async () => { + l2Provider.setNumBlocksToReturn(2) + l2Provider.setL2BlockData({ + queueOrigin: QueueOrigin.L1ToL2, + } as any) + + const lowGasPriceWei = BigNumber.from(2).mul(1_000_000_000) + + sinon + .stub(sequencer, 'getGasPrice') + .callsFake(async () => lowGasPriceWei) + + const receipt = await batchSubmitter.submitNextBatch() + expect(sequencer.getGasPrice).to.have.been.calledOnce + expect(receipt).to.not.be.undefined + }) + }) + }) + + describe('StateBatchSubmitter', () => { + let txBatchSubmitter + let stateBatchSubmitter + beforeEach(async () => { + for (let i = 1; i < 15; i++) { + await OVM_CanonicalTransactionChain.enqueue( + '0x' + '01'.repeat(20), + 50_000, + '0x' + i.toString().repeat(64), + { + gasLimit: 1_000_000, + } + ) + } + + txBatchSubmitter = createBatchSubmitter(0) + + l2Provider.setNumBlocksToReturn(5) + const nextQueueElement = await getQueueElement( + OVM_CanonicalTransactionChain + ) + const data = ctcCoder.eip155TxData.encode({ + sig: DUMMY_SIG, + gasLimit: 0, + gasPrice: 0, + nonce: 0, + target: DUMMY_ADDRESS, + data: '0x', + type: TxType.EIP155, + }) + l2Provider.setL2BlockData( + { + data, + l1BlockNumber: nextQueueElement.blockNumber - 1, + txType: TxType.EIP155, + queueOrigin: QueueOrigin.Sequencer, + l1TxOrigin: null, + } as any, + nextQueueElement.timestamp - 1, + EXAMPLE_STATE_ROOT // example stateRoot + ) + + // submit a batch of transactions to enable state batch submission + await txBatchSubmitter.submitNextBatch() + + stateBatchSubmitter = new StateBatchSubmitter( + sequencer, + l2Provider as any, + MIN_TX_SIZE, + MAX_TX_SIZE, + 10, // maxBatchSize + 0, + 1, + 100000, + 0, // finalityConfirmations + AddressManager.address, + 1, + MIN_GAS_PRICE_IN_GWEI, + MAX_GAS_PRICE_IN_GWEI, + GAS_RETRY_INCREMENT, + GAS_THRESHOLD_IN_GWEI, + new Logger({ name: STATE_BATCH_SUBMITTER_LOG_TAG }), + '0x' + '01'.repeat(20) // placeholder for fraudSubmissionAddress + ) + }) + + describe('submitNextBatch', () => { + it('should submit a state batch after a transaction batch', async () => { + const receipt = await stateBatchSubmitter.submitNextBatch() + expect(receipt).to.not.be.undefined + + const iface = new ethers.utils.Interface(scc.abi) + const parsedLogs = iface.parseLog(receipt.logs[0]) + + expect(parsedLogs.eventFragment.name).to.eq('StateBatchAppended') + expect(parsedLogs.args._batchIndex.toNumber()).to.eq(0) + expect(parsedLogs.args._batchSize.toNumber()).to.eq(6) + expect(parsedLogs.args._prevTotalElements.toNumber()).to.eq(0) + }) + }) + }) +}) + +describe('Batch Submitter with Ganache', () => { + let signer + const server = ganache.server({ + default_balance_ether: 420, + blockTime: 2_000, + }) + const provider = new Web3Provider(ganache.provider()) + + before(async () => { + await server.listen(3001) + signer = await provider.getSigner() + }) + + after(async () => { + await server.close() + }) + + // Unit test for getReceiptWithResubmission function, + // tests for increasing gas price on resubmission + it('should resubmit a transaction if it is not confirmed', async () => { + const gasPrices = [] + const numConfirmations = 2 + const sendTxFunc = async (gasPrice) => { + // push the retried gasPrice + gasPrices.push(gasPrice) + + const tx = signer.sendTransaction({ + to: DECOMPRESSION_ADDRESS, + value: 88, + nonce: 0, + gasPrice, + }) + + const response = await tx + + return signer.provider.waitForTransaction(response.hash, numConfirmations) + } + + const resubmissionConfig = { + numConfirmations, + resubmissionTimeout: 1_000, // retry every second + minGasPriceInGwei: 0, + maxGasPriceInGwei: 100, + gasRetryIncrement: 5, + } + + BatchSubmitter.getReceiptWithResubmission( + sendTxFunc, + resubmissionConfig, + new Logger({ name: TX_BATCH_SUBMITTER_LOG_TAG }) + ) + + // Wait 1.5s for at least 1 retry + await new Promise((r) => setTimeout(r, 1500)) + + // Iterate through gasPrices to ensure each entry increases from + // the last + const isIncreasing = gasPrices.reduce( + (isInc, gasPrice, i, gP) => + (isInc && gasPrice > gP[i - 1]) || Number.NEGATIVE_INFINITY, + true + ) + + expect(gasPrices).to.have.lengthOf.above(1) // retried at least once + expect(isIncreasing).to.be.true + }) +}) diff --git a/test/batch-submitter/mockchain-provider.ts b/test/batch-submitter/mockchain-provider.ts index 9698d21..e9feca4 100644 --- a/test/batch-submitter/mockchain-provider.ts +++ b/test/batch-submitter/mockchain-provider.ts @@ -88,6 +88,7 @@ export class MockchainProvider extends OptimismProvider { public setL2BlockData( tx: L2Transaction, timestamp?: number, + stateRoot?: string, start: number = 1, end: number = this.mockBlocks.length ) { @@ -99,6 +100,7 @@ export class MockchainProvider extends OptimismProvider { ...this.mockBlocks[i].transactions[0], ...tx, } + this.mockBlocks[i].stateRoot = stateRoot } } diff --git a/test/batch-submitter/transaction-batch-submitter.spec.ts b/test/batch-submitter/transaction-batch-submitter.spec.ts deleted file mode 100644 index 3f5d50e..0000000 --- a/test/batch-submitter/transaction-batch-submitter.spec.ts +++ /dev/null @@ -1,435 +0,0 @@ -import { expect, chai } from '../setup' - -/* External Imports */ -import { ethers } from 'hardhat' -import '@nomiclabs/hardhat-ethers' -import { Signer, ContractFactory, Contract, BigNumber } from 'ethers' -import ganache from 'ganache-core' -import sinon from 'sinon' -import { Web3Provider, JsonRpcProvider } from '@ethersproject/providers' -import { getContractInterface } from '@eth-optimism/contracts' -import { smockit, MockContract } from '@eth-optimism/smock' - -/* Internal Imports */ -import { MockchainProvider } from './mockchain-provider' -import { - makeAddressManager, - setProxyTarget, - FORCE_INCLUSION_PERIOD_SECONDS, - getContractFactory, -} from '../helpers' -import { - CanonicalTransactionChainContract, - QueueOrigin, - TransactionBatchSubmitter as RealTransactionBatchSubmitter, - TX_BATCH_SUBMITTER_LOG_TAG, - Batch, - BatchSubmitter, -} from '../../src' - -import { - Signature, - TxType, - ctcCoder, - remove0x, - Logger, -} from '@eth-optimism/core-utils' - -const DECOMPRESSION_ADDRESS = '0x4200000000000000000000000000000000000008' -const MAX_GAS_LIMIT = 8_000_000 -const MAX_TX_SIZE = 100_000 -const MIN_TX_SIZE = 1_000 -const MIN_GAS_PRICE_IN_GWEI = 1 -const MAX_GAS_PRICE_IN_GWEI = 70 -const GAS_RETRY_INCREMENT = 5 -const GAS_THRESHOLD_IN_GWEI = 120 - -// Helper functions -interface QueueElement { - queueRoot: string - timestamp: number - blockNumber: number -} -const getQueueElement = async ( - ctcContract: Contract, - nextQueueIndex?: number -): Promise => { - if (!nextQueueIndex) { - nextQueueIndex = await ctcContract.getNextQueueIndex() - } - const nextQueueElement = await ctcContract.getQueueElement(nextQueueIndex) - return nextQueueElement -} -const DUMMY_SIG: Signature = { - r: '11'.repeat(32), - s: '22'.repeat(32), - v: 1, -} -// A transaction batch submitter which skips the validate batch check -class TransactionBatchSubmitter extends RealTransactionBatchSubmitter { - protected async _validateBatch(batch: Batch): Promise { - return true - } -} - -describe('TransactionBatchSubmitter', () => { - let signer: Signer - let sequencer: Signer - before(async () => { - ;[signer, sequencer] = await ethers.getSigners() - }) - - let AddressManager: Contract - let Mock__OVM_ExecutionManager: MockContract - let Mock__OVM_StateCommitmentChain: MockContract - before(async () => { - AddressManager = await makeAddressManager() - await AddressManager.setAddress( - 'OVM_Sequencer', - await sequencer.getAddress() - ) - await AddressManager.setAddress( - 'OVM_DecompressionPrecompileAddress', - DECOMPRESSION_ADDRESS - ) - - Mock__OVM_ExecutionManager = await smockit( - await getContractFactory('OVM_ExecutionManager') - ) - - Mock__OVM_StateCommitmentChain = await smockit( - await getContractFactory('OVM_StateCommitmentChain') - ) - - await setProxyTarget( - AddressManager, - 'OVM_ExecutionManager', - Mock__OVM_ExecutionManager - ) - - await setProxyTarget( - AddressManager, - 'OVM_StateCommitmentChain', - Mock__OVM_StateCommitmentChain - ) - - Mock__OVM_StateCommitmentChain.smocked.canOverwrite.will.return.with(false) - Mock__OVM_ExecutionManager.smocked.getMaxTransactionGasLimit.will.return.with( - MAX_GAS_LIMIT - ) - }) - - let Factory__OVM_CanonicalTransactionChain: ContractFactory - before(async () => { - Factory__OVM_CanonicalTransactionChain = await getContractFactory( - 'OVM_CanonicalTransactionChain' - ) - }) - - let OVM_CanonicalTransactionChain: CanonicalTransactionChainContract - let l2Provider: MockchainProvider - beforeEach(async () => { - const unwrapped_OVM_CanonicalTransactionChain = await Factory__OVM_CanonicalTransactionChain.deploy( - AddressManager.address, - FORCE_INCLUSION_PERIOD_SECONDS - ) - await unwrapped_OVM_CanonicalTransactionChain.init() - - await AddressManager.setAddress( - 'OVM_CanonicalTransactionChain', - unwrapped_OVM_CanonicalTransactionChain.address - ) - - OVM_CanonicalTransactionChain = new CanonicalTransactionChainContract( - unwrapped_OVM_CanonicalTransactionChain.address, - getContractInterface('OVM_CanonicalTransactionChain'), - sequencer - ) - l2Provider = new MockchainProvider( - OVM_CanonicalTransactionChain.address, - '0x' + '00'.repeat(20) - ) - }) - - afterEach(() => { - sinon.restore() - }) - - describe('Submit', () => { - const enqueuedElements: Array<{ - blockNumber: number - timestamp: number - }> = [] - - let batchSubmitter - beforeEach(async () => { - for (let i = 1; i < 15; i++) { - await OVM_CanonicalTransactionChain.enqueue( - '0x' + '01'.repeat(20), - 50_000, - '0x' + i.toString().repeat(64), - { - gasLimit: 1_000_000, - } - ) - } - batchSubmitter = new TransactionBatchSubmitter( - sequencer, - l2Provider as any, - MIN_TX_SIZE, - MAX_TX_SIZE, - 10, - 0, - 1, - 100000, - AddressManager.address, - 1, - MIN_GAS_PRICE_IN_GWEI, - MAX_GAS_PRICE_IN_GWEI, - GAS_RETRY_INCREMENT, - GAS_THRESHOLD_IN_GWEI, - new Logger({ name: TX_BATCH_SUBMITTER_LOG_TAG }), - false - ) - }) - - it('should submit a sequencer batch correctly', async () => { - l2Provider.setNumBlocksToReturn(5) - const nextQueueElement = await getQueueElement( - OVM_CanonicalTransactionChain - ) - const data = ctcCoder.eip155TxData.encode({ - sig: DUMMY_SIG, - gasLimit: 0, - gasPrice: 0, - nonce: 0, - target: '0x0000000000000000000000000000000000000000', - data: '0x', - type: TxType.EIP155, - }) - l2Provider.setL2BlockData( - { - data, - l1BlockNumber: nextQueueElement.blockNumber - 1, - txType: TxType.EIP155, - queueOrigin: QueueOrigin.Sequencer, - l1TxOrigin: null, - } as any, - nextQueueElement.timestamp - 1 - ) - let receipt = await batchSubmitter.submitNextBatch() - let logData = remove0x(receipt.logs[1].data) - expect(parseInt(logData.slice(64 * 0, 64 * 1), 16)).to.equal(0) // _startingQueueIndex - expect(parseInt(logData.slice(64 * 1, 64 * 2), 16)).to.equal(0) // _numQueueElements - expect(parseInt(logData.slice(64 * 2, 64 * 3), 16)).to.equal(6) // _totalElements - receipt = await batchSubmitter.submitNextBatch() - logData = remove0x(receipt.logs[1].data) - expect(parseInt(logData.slice(64 * 0, 64 * 1), 16)).to.equal(0) // _startingQueueIndex - expect(parseInt(logData.slice(64 * 1, 64 * 2), 16)).to.equal(0) // _numQueueElements - expect(parseInt(logData.slice(64 * 2, 64 * 3), 16)).to.equal(11) // _totalElements - }) - - it('should submit a queue batch correctly', async () => { - l2Provider.setNumBlocksToReturn(5) - l2Provider.setL2BlockData({ - queueOrigin: QueueOrigin.L1ToL2, - } as any) - let receipt = await batchSubmitter.submitNextBatch() - let logData = remove0x(receipt.logs[1].data) - expect(parseInt(logData.slice(64 * 0, 64 * 1), 16)).to.equal(0) // _startingQueueIndex - expect(parseInt(logData.slice(64 * 1, 64 * 2), 16)).to.equal(6) // _numQueueElements - expect(parseInt(logData.slice(64 * 2, 64 * 3), 16)).to.equal(6) // _totalElements - receipt = await batchSubmitter.submitNextBatch() - logData = remove0x(receipt.logs[1].data) - expect(parseInt(logData.slice(64 * 0, 64 * 1), 16)).to.equal(6) // _startingQueueIndex - expect(parseInt(logData.slice(64 * 1, 64 * 2), 16)).to.equal(5) // _numQueueElements - expect(parseInt(logData.slice(64 * 2, 64 * 3), 16)).to.equal(11) // _totalElements - }) - - it('should submit a batch with both queue and sequencer chain elements', async () => { - l2Provider.setNumBlocksToReturn(10) // For this batch we'll return 10 elements! - l2Provider.setL2BlockData({ - queueOrigin: QueueOrigin.L1ToL2, - } as any) - // Turn blocks 3-5 into sequencer txs - const nextQueueElement = await getQueueElement( - OVM_CanonicalTransactionChain, - 2 - ) - const data = ctcCoder.ethSignTxData.encode({ - sig: DUMMY_SIG, - gasLimit: 0, - gasPrice: 0, - nonce: 0, - target: '0x0000000000000000000000000000000000000000', - data: '0x', - type: TxType.EthSign, - }) - l2Provider.setL2BlockData( - { - data, - l1BlockNumber: nextQueueElement.blockNumber - 1, - txType: TxType.EthSign, - queueOrigin: QueueOrigin.Sequencer, - l1TxOrigin: null, - } as any, - nextQueueElement.timestamp - 1, - 3, - 6 - ) - const receipt = await batchSubmitter.submitNextBatch() - const logData = remove0x(receipt.logs[1].data) - expect(parseInt(logData.slice(64 * 0, 64 * 1), 16)).to.equal(0) // _startingQueueIndex - expect(parseInt(logData.slice(64 * 1, 64 * 2), 16)).to.equal(8) // _numQueueElements - expect(parseInt(logData.slice(64 * 2, 64 * 3), 16)).to.equal(11) // _totalElements - }) - - it('should submit a small batch only after the timeout', async () => { - l2Provider.setNumBlocksToReturn(2) - l2Provider.setL2BlockData({ - queueOrigin: QueueOrigin.L1ToL2, - } as any) - const createBatchSubmitter = ( - timeout: number - ): TransactionBatchSubmitter => - new TransactionBatchSubmitter( - sequencer, - l2Provider as any, - MIN_TX_SIZE, - MAX_TX_SIZE, - 10, - timeout, - 1, - 100000, - AddressManager.address, - 1, - MIN_GAS_PRICE_IN_GWEI, - MAX_GAS_PRICE_IN_GWEI, - GAS_RETRY_INCREMENT, - GAS_THRESHOLD_IN_GWEI, - new Logger({ name: TX_BATCH_SUBMITTER_LOG_TAG }), - false - ) - - // Create a batch submitter with a long timeout & make sure it doesn't submit the batches one after another - const longTimeout = 10_000 - batchSubmitter = createBatchSubmitter(longTimeout) - let receipt = await batchSubmitter.submitNextBatch() - expect(receipt).to.not.be.undefined - receipt = await batchSubmitter.submitNextBatch() - // The receipt should be undefined because that means it didn't submit - expect(receipt).to.be.undefined - - // This time create a batch submitter with a short timeout & it should submit batches after the timeout is reached - const shortTimeout = 5 - batchSubmitter = createBatchSubmitter(shortTimeout) - receipt = await batchSubmitter.submitNextBatch() - expect(receipt).to.not.be.undefined - // Sleep for the short timeout - await new Promise((r) => setTimeout(r, shortTimeout)) - receipt = await batchSubmitter.submitNextBatch() - // The receipt should NOT be undefined because that means it successfully submitted! - expect(receipt).to.not.be.undefined - }) - - it('should not submit if gas price is over threshold', async () => { - l2Provider.setNumBlocksToReturn(2) - l2Provider.setL2BlockData({ - queueOrigin: QueueOrigin.L1ToL2, - } as any) - - const highGasPriceWei = BigNumber.from(200).mul(1_000_000_000) - - sinon - .stub(sequencer, 'getGasPrice') - .callsFake(async () => highGasPriceWei) - - const receipt = await batchSubmitter.submitNextBatch() - expect(sequencer.getGasPrice).to.have.been.calledOnce - expect(receipt).to.be.undefined - }) - - it('should submit if gas price is not over threshold', async () => { - l2Provider.setNumBlocksToReturn(2) - l2Provider.setL2BlockData({ - queueOrigin: QueueOrigin.L1ToL2, - } as any) - - const lowGasPriceWei = BigNumber.from(2).mul(1_000_000_000) - - sinon.stub(sequencer, 'getGasPrice').callsFake(async () => lowGasPriceWei) - - const receipt = await batchSubmitter.submitNextBatch() - expect(sequencer.getGasPrice).to.have.been.calledOnce - expect(receipt).to.not.be.undefined - }) - }) -}) - -describe('TransactionBatchSubmitter to Ganache', () => { - let signer - const server = ganache.server({ - default_balance_ether: 420, - blockTime: 2_000, - }) - const provider = new Web3Provider(ganache.provider()) - - before(async () => { - await server.listen(3001) - signer = await provider.getSigner() - }) - - after(async () => { - await server.close() - }) - - // Unit test for getReceiptWithResubmission function, - // tests for increasing gas price on resubmission - it('should resubmit a transaction if it is not confirmed', async () => { - const gasPrices = [] - const numConfirmations = 2 - const sendTxFunc = async (gasPrice) => { - // push the retried gasPrice - gasPrices.push(gasPrice) - - const tx = signer.sendTransaction({ - to: DECOMPRESSION_ADDRESS, - value: 88, - nonce: 0, - gasPrice, - }) - - const response = await tx - - return signer.provider.waitForTransaction(response.hash, numConfirmations) - } - - const resubmissionConfig = { - numConfirmations, - resubmissionTimeout: 1_000, // retry every second - minGasPriceInGwei: 0, - maxGasPriceInGwei: 100, - gasRetryIncrement: 5, - } - - BatchSubmitter.getReceiptWithResubmission( - sendTxFunc, - resubmissionConfig, - new Logger({ name: TX_BATCH_SUBMITTER_LOG_TAG }) - ) - - // Wait 1.5s for at least 1 retry - await new Promise((r) => setTimeout(r, 1500)) - - // Iterate through gasPrices to ensure each entry increases from - // the last - const isIncreasing = gasPrices.reduce( - (isInc, gasPrice, i, gP) => - (isInc && gasPrice > gP[i - 1]) || Number.NEGATIVE_INFINITY, - true - ) - - expect(gasPrices).to.have.lengthOf.above(1) // retried at least once - expect(isIncreasing).to.be.true - }) -}) diff --git a/yarn.lock b/yarn.lock index 9ed731e..876d9ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4892,7 +4892,7 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-core-module@^2.1.0: +is-core-module@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==