Commit 6b0d57aa authored by Conner Fromknecht's avatar Conner Fromknecht

feat: remove packages/batch-submitter

parent f3887481
# Environment
NODE_ENV=development
# Leave blank during local development
ETH_NETWORK_NAME=
# Logging & monitoring
DEBUG=info*,error*,warn*,debug*
RUN_METRICS_SERVER=
METRICS_PORT=
METRICS_HOSTNAME=
# Leave the SENTRY_DSN variable unset during local development
SENTRY_DSN=
SENTRY_TRACE_RATE=
USE_SENTRY=
L1_NODE_WEB3_URL=http://localhost:9545
L2_NODE_WEB3_URL=http://localhost:8545
MAX_L1_TX_SIZE=90000
MIN_L1_TX_SIZE=32
MAX_TX_BATCH_SIZE=50
MAX_STATE_BATCH_COUNT=2000
MAX_TX_BATCH_COUNT=250
MAX_BATCH_SUBMISSION_TIME=0
POLL_INTERVAL=15000
NUM_CONFIRMATIONS=0
RESUBMISSION_TIMEOUT=300 # in seconds
FINALITY_CONFIRMATIONS=0
RUN_TX_BATCH_SUBMITTER=true
RUN_STATE_BATCH_SUBMITTER=true
SAFE_MINIMUM_ETHER_BALANCE=0
CLEAR_PENDING_TXS=false
ADDRESS_MANAGER_ADDRESS=
USE_HARDHAT=
DEBUG_IMPERSONATE_SEQUENCER_ADDRESS=
DEBUG_IMPERSONATE_PROPOSER_ADDRESS=
# Optional gas settings
MAX_GAS_PRICE_IN_GWEI=200
GAS_RETRY_INCREMENT=5
GAS_THRESHOLD_IN_GWEI=100
SEQUENCER_PRIVATE_KEY=0xd2ab07f7c10ac88d5f86f1b4c1035d5195e81f27dbe62ad65e59cbf88205629b
module.exports = {
extends: '../../.eslintrc.js',
}
module.exports = {
...require('../../.prettierrc.js'),
};
\ No newline at end of file
This diff is collapsed.
(The MIT License)
Copyright 2020-2021 Optimism
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[![codecov](https://codecov.io/gh/ethereum-optimism/optimism/branch/master/graph/badge.svg?token=0VTG7PG7YR&flag=batch-submitter)](https://codecov.io/gh/ethereum-optimism/optimism)
# Batch Submitter
Contains an executable batch submitter service which watches L1 and a local L2 node and submits batches to the
`CanonicalTransactionChain` & `StateCommitmentChain` based on its local information.
## Configuration
All configuration is done via environment variables. See all variables at [.env.example](.env.example); copy into a `.env` file before running.
## Building & Running
1. Make sure dependencies are installed just run `yarn` in the base directory
2. Build `yarn build`
3. Run `yarn start`
## Controlling log output verbosity
Before running, set the `DEBUG` environment variable to specify the verbosity level. It must be made up of comma-separated values of patterns to match in debug logs. Here's a few common options:
* `debug*` - Will match all debug statements -- very verbose
* `info*` - Will match all info statements -- less verbose, useful in most cases
* `warn*` - Will match all warnings -- recommended at a minimum
* `error*` - Will match all errors -- would not omit this
Examples:
* Everything but debug: `export DEBUG=info*,error*,warn*`
* Most verbose: `export DEBUG=info*,error*,warn*,debug*`
## Testing & linting
### Local
- Run unit tests with `yarn test`
- See lint errors with `yarn lint`; auto-fix with `yarn lint --fix`
### Submission
You may test a submission locally against a local Hardhat fork.
1. Follow the instructions [here](https://github.com/ethereum-optimism/hardhat) to run a Hardhat node.
2. Change the Batch Submitter `.env` field `L1_NODE_WEB3_URL` to the local Hardhat url. Depending on which network you are using, update `ADDRESS_MANAGER_ADDRESS` according to the [Regenesis repo](https://github.com/ethereum-optimism/regenesis).
3. Also check `L2_NODE_WEB3_URL` is correctly set and has transactions to submit.
3. Run `yarn build` to build your changes.
4. Start Batch Submitter with `yarn start`. It will automatically start submitting pending transactions from L2.
## Observability in production
When deploying Batch Submitter to production / a live ETH network, populate the environment variables `NODE_ENV` (`development`, `production`, or `test`) and `ETH_NETWORK_NAME` (`mainnet`, `kovan`, `goerli`). This enables Batch Submitter to capture more context in logs and metrics, and initializes [Sentry](https://docs.sentry.io/platforms/node/) to track errors.
#!/usr/bin/env node
const batchSubmitter = require('../dist/src/exec/run-batch-submitter')
batchSubmitter.run()
import '@nomiclabs/hardhat-waffle'
import { HardhatUserConfig } from 'hardhat/config'
import {
DEFAULT_ACCOUNTS_HARDHAT,
RUN_OVM_TEST_GAS,
} from './test/helpers/constants'
import '@nomiclabs/hardhat-ethers'
const config: HardhatUserConfig = {
networks: {
hardhat: {
accounts: DEFAULT_ACCOUNTS_HARDHAT,
blockGasLimit: RUN_OVM_TEST_GAS * 2,
},
},
mocha: {
timeout: 50000,
},
solidity: {
version: '0.7.0',
settings: {
optimizer: {
enabled: true,
runs: 200,
},
metadata: {
bytecodeHash: 'none',
},
outputSelection: {
'*': {
'*': ['metadata', 'storageLayout'],
},
},
},
},
}
export default config
const rootPath = __dirname
export { rootPath }
{
"private": true,
"name": "@eth-optimism/batch-submitter",
"version": "0.4.20",
"description": "[Optimism] Service for submitting transactions and transaction results",
"main": "dist/index",
"types": "dist/index",
"files": [
"dist/*"
],
"scripts": {
"start": "node ./exec/run-batch-submitter.js",
"build": "tsc -p ./tsconfig.build.json",
"clean": "rimraf cache/ dist/ ./tsconfig.build.tsbuildinfo",
"lint": "yarn lint:fix && yarn lint:check",
"pre-commit": "lint-staged",
"lint:fix": "yarn lint:check --fix",
"lint:check": "eslint . --max-warnings=0",
"test": "hardhat test --show-stack-traces",
"test:coverage": "nyc hardhat test && nyc merge .nyc_output coverage.json"
},
"keywords": [
"optimism",
"ethereum",
"sequencer",
"aggregator"
],
"homepage": "https://github.com/ethereum-optimism/optimism/tree/develop/packages/batch-submitter#readme",
"license": "MIT",
"author": "Optimism PBC",
"repository": {
"type": "git",
"url": "https://github.com/ethereum-optimism/optimism.git"
},
"dependencies": {
"@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/contracts": "0.5.15",
"@eth-optimism/core-utils": "0.8.0",
"@eth-optimism/sdk": "^0.2.3",
"@eth-optimism/ynatm": "^0.2.2",
"@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/providers": "^5.5.3",
"@sentry/node": "^6.3.1",
"bcfg": "^0.1.6",
"bluebird": "^3.7.2",
"dotenv": "^10.0.0",
"ethers": "^5.5.4",
"old-contracts": "npm:@eth-optimism/contracts@^0.0.2-alpha.7",
"prom-client": "^13.1.0"
},
"devDependencies": {
"@eth-optimism/smock": "1.1.10",
"@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@types/bluebird": "^3.5.34",
"@types/chai": "^4.2.18",
"@types/mocha": "^8.2.2",
"@types/node": "^15.12.2",
"@types/sinon": "^9.0.10",
"@types/sinon-chai": "^3.2.5",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
"babel-eslint": "^10.1.0",
"chai": "^4.3.4",
"eslint": "^7.27.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-jsdoc": "^35.1.2",
"eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-unicorn": "^32.0.1",
"ethereum-waffle": "^3.3.0",
"hardhat": "^2.3.0",
"lint-staged": "11.0.0",
"mocha": "^8.4.0",
"prettier": "^2.3.1",
"rimraf": "^3.0.2",
"sinon": "^9.2.4",
"sinon-chai": "^3.5.0",
"typescript": "^4.3.5"
},
"resolutions": {
"ganache-core": "^2.13.2",
"**/@sentry/node": "^6.2.5"
},
"publishConfig": {
"access": "public"
}
}
/* External Imports */
import {
Contract,
Signer,
utils,
providers,
PopulatedTransaction,
} from 'ethers'
import {
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import { Gauge, Histogram, Counter } from 'prom-client'
import { RollupInfo, sleep } from '@eth-optimism/core-utils'
import { Logger, Metrics } from '@eth-optimism/common-ts'
import { getContractFactory } from 'old-contracts'
/* Internal Imports */
import { TxSubmissionHooks } from '..'
export interface BlockRange {
start: number
end: number
}
interface BatchSubmitterMetrics {
batchSubmitterETHBalance: Gauge<string>
batchSizeInBytes: Histogram<string>
numTxPerBatch: Histogram<string>
submissionTimestamp: Histogram<string>
submissionGasUsed: Histogram<string>
batchesSubmitted: Counter<string>
failedSubmissions: Counter<string>
malformedBatches: Counter<string>
batchTxBuildTime: Gauge<string>
}
export abstract class BatchSubmitter {
protected rollupInfo: RollupInfo
protected chainContract: Contract
protected l2ChainId: number
protected syncing: boolean
protected lastBatchSubmissionTimestamp: number = 0
protected metrics: BatchSubmitterMetrics
constructor(
readonly signer: Signer,
readonly l2Provider: providers.StaticJsonRpcProvider,
readonly minTxSize: number,
readonly maxTxSize: number,
readonly maxBatchSize: number,
readonly maxBatchSubmissionTime: number,
readonly numConfirmations: number,
readonly resubmissionTimeout: number,
readonly finalityConfirmations: number,
readonly addressManagerAddress: string,
readonly minBalanceEther: number,
readonly blockOffset: number,
readonly logger: Logger,
readonly defaultMetrics: Metrics
) {
this.metrics = this._registerMetrics(defaultMetrics)
}
public abstract _submitBatch(
startBlock: number,
endBlock: number
): Promise<TransactionReceipt>
public abstract _onSync(): Promise<TransactionReceipt>
public abstract _getBatchStartAndEnd(): Promise<BlockRange>
public abstract _updateChainInfo(): Promise<void>
public async submitNextBatch(): Promise<TransactionReceipt> {
if (typeof this.l2ChainId === 'undefined') {
this.l2ChainId = await this._getL2ChainId()
}
await this._updateChainInfo()
if (!(await this._hasEnoughETHToCoverGasCosts())) {
await sleep(this.resubmissionTimeout)
return
}
this.logger.info('Readying to submit next batch...', {
l2ChainId: this.l2ChainId,
batchSubmitterAddress: await this.signer.getAddress(),
})
if (this.syncing === true) {
this.logger.info(
'Syncing mode enabled! Skipping batch submission and clearing queue...'
)
return this._onSync()
}
const range = await this._getBatchStartAndEnd()
if (!range) {
return
}
return this._submitBatch(range.start, range.end)
}
protected async _hasEnoughETHToCoverGasCosts(): Promise<boolean> {
const address = await this.signer.getAddress()
const balance = await this.signer.getBalance()
const ether = utils.formatEther(balance)
const num = parseFloat(ether)
this.logger.info('Checked balance', {
address,
ether,
})
this.metrics.batchSubmitterETHBalance.set(num)
if (num < this.minBalanceEther) {
this.logger.fatal('Current balance lower than min safe balance', {
current: num,
safeBalance: this.minBalanceEther,
})
return false
}
return true
}
protected async _getRollupInfo(): Promise<RollupInfo> {
return this.l2Provider.send('rollup_getInfo', [])
}
protected async _getL2ChainId(): Promise<number> {
return this.l2Provider.send('eth_chainId', [])
}
protected async _getChainAddresses(): Promise<{
ctcAddress: string
sccAddress: string
}> {
const addressManager = (
await getContractFactory('Lib_AddressManager', this.signer)
).attach(this.addressManagerAddress)
const sccAddress = await addressManager.getAddress('StateCommitmentChain')
const ctcAddress = await addressManager.getAddress(
'CanonicalTransactionChain'
)
return {
ctcAddress,
sccAddress,
}
}
protected _shouldSubmitBatch(batchSizeInBytes: number): boolean {
const currentTimestamp = Date.now()
if (batchSizeInBytes < this.minTxSize) {
const timeSinceLastSubmission =
currentTimestamp - this.lastBatchSubmissionTimestamp
if (timeSinceLastSubmission < this.maxBatchSubmissionTime) {
this.logger.info(
'Skipping batch submission. Batch too small & max submission timeout not reached.',
{
batchSizeInBytes,
timeSinceLastSubmission,
maxBatchSubmissionTime: this.maxBatchSubmissionTime,
minTxSize: this.minTxSize,
lastBatchSubmissionTimestamp: this.lastBatchSubmissionTimestamp,
currentTimestamp,
}
)
return false
}
this.logger.info('Timeout reached, proceeding with batch submission.', {
batchSizeInBytes,
timeSinceLastSubmission,
maxBatchSubmissionTime: this.maxBatchSubmissionTime,
lastBatchSubmissionTimestamp: this.lastBatchSubmissionTimestamp,
currentTimestamp,
})
this.metrics.batchSizeInBytes.observe(batchSizeInBytes)
return true
}
this.logger.info(
'Sufficient batch size, proceeding with batch submission.',
{
batchSizeInBytes,
lastBatchSubmissionTimestamp: this.lastBatchSubmissionTimestamp,
currentTimestamp,
}
)
this.metrics.batchSizeInBytes.observe(batchSizeInBytes)
return true
}
protected _makeHooks(txName: string): TxSubmissionHooks {
return {
beforeSendTransaction: (tx: PopulatedTransaction) => {
this.logger.info(`Submitting ${txName} transaction`, {
gasPrice: tx.gasPrice,
maxFeePerGas: tx.maxFeePerGas,
maxPriorityFeePerGas: tx.maxPriorityFeePerGas,
gasLimit: tx.gasLimit,
nonce: tx.nonce,
contractAddr: this.chainContract.address,
})
},
onTransactionResponse: (txResponse: TransactionResponse) => {
this.logger.info(`Submitted ${txName} transaction`, {
txHash: txResponse.hash,
from: txResponse.from,
})
this.logger.debug(`${txName} transaction data`, {
data: txResponse.data,
})
},
}
}
protected async _submitAndLogTx(
submitTransaction: () => Promise<TransactionReceipt>,
successMessage: string
): Promise<TransactionReceipt> {
this.lastBatchSubmissionTimestamp = Date.now()
this.logger.debug('Submitting transaction & waiting for receipt...')
let receipt: TransactionReceipt
try {
receipt = await submitTransaction()
} catch (err) {
this.metrics.failedSubmissions.inc()
if (err.reason) {
this.logger.error(`Transaction invalid: ${err.reason}, aborting`, {
message: err.toString(),
stack: err.stack,
code: err.code,
})
return
}
this.logger.error('Encountered error at submission, aborting', {
message: err.toString(),
stack: err.stack,
code: err.code,
})
return
}
this.logger.info('Received transaction receipt', { receipt })
this.logger.info(successMessage)
this.metrics.batchesSubmitted.inc()
this.metrics.submissionGasUsed.observe(receipt.gasUsed.toNumber())
this.metrics.submissionTimestamp.observe(Date.now())
return receipt
}
private _registerMetrics(metrics: Metrics): BatchSubmitterMetrics {
metrics.registry.clear()
return {
batchSubmitterETHBalance: new metrics.client.Gauge({
name: 'batch_submitter_eth_balance',
help: 'ETH balance of the batch submitter',
registers: [metrics.registry],
}),
batchSizeInBytes: new metrics.client.Histogram({
name: 'batch_size_in_bytes',
help: 'Size of batches in bytes',
registers: [metrics.registry],
}),
numTxPerBatch: new metrics.client.Histogram({
name: 'num_txs_per_batch',
help: 'Number of transactions in each batch',
registers: [metrics.registry],
}),
submissionTimestamp: new metrics.client.Histogram({
name: 'submission_timestamp',
help: 'Timestamp of each batch submitter submission',
registers: [metrics.registry],
}),
submissionGasUsed: new metrics.client.Histogram({
name: 'submission_gash_used',
help: 'Gas used to submit each batch',
registers: [metrics.registry],
}),
batchesSubmitted: new metrics.client.Counter({
name: 'batches_submitted',
help: 'Count of batches submitted',
registers: [metrics.registry],
}),
failedSubmissions: new metrics.client.Counter({
name: 'failed_submissions',
help: 'Count of failed batch submissions',
registers: [metrics.registry],
}),
malformedBatches: new metrics.client.Counter({
name: 'malformed_batches',
help: 'Count of malformed batches',
registers: [metrics.registry],
}),
batchTxBuildTime: new metrics.client.Gauge({
name: 'batch_tx_build_time',
help: 'Time to construct batch transaction',
registers: [metrics.registry],
}),
}
}
}
export * from './batch-submitter'
export * from './tx-batch-submitter'
export * from './state-batch-submitter'
export const TX_BATCH_SUBMITTER_LOG_TAG = 'oe:batch_submitter:tx_chain'
export const STATE_BATCH_SUBMITTER_LOG_TAG = 'oe:batch_submitter:state_chain'
/* External Imports */
import { performance } from 'perf_hooks'
import { Promise as bPromise } from 'bluebird'
import { Contract, Signer, providers } from 'ethers'
import { TransactionReceipt } from '@ethersproject/abstract-provider'
import { getContractFactory } from 'old-contracts'
import {
L2Block,
RollupInfo,
Bytes32,
remove0x,
} from '@eth-optimism/core-utils'
import { Logger, Metrics } from '@eth-optimism/common-ts'
/* Internal Imports */
import { TransactionSubmitter } from '../utils'
import { BlockRange, BatchSubmitter } from '.'
export class StateBatchSubmitter extends BatchSubmitter {
// TODO: Change this so that we calculate start = scc.totalElements() and end = ctc.totalElements()!
// Not based on the length of the L2 chain -- that is only used in the batch submitter
// Note this means we've got to change the state / end calc logic
protected l2ChainId: number
protected syncing: boolean
protected ctcContract: Contract
private fraudSubmissionAddress: string
private transactionSubmitter: TransactionSubmitter
constructor(
signer: Signer,
l2Provider: providers.StaticJsonRpcProvider,
minTxSize: number,
maxTxSize: number,
maxBatchSize: number,
maxBatchSubmissionTime: number,
numConfirmations: number,
resubmissionTimeout: number,
finalityConfirmations: number,
addressManagerAddress: string,
minBalanceEther: number,
transactionSubmitter: TransactionSubmitter,
blockOffset: number,
logger: Logger,
metrics: Metrics,
fraudSubmissionAddress: string
) {
super(
signer,
l2Provider,
minTxSize,
maxTxSize,
maxBatchSize,
maxBatchSubmissionTime,
numConfirmations,
resubmissionTimeout,
finalityConfirmations,
addressManagerAddress,
minBalanceEther,
blockOffset,
logger,
metrics
)
this.fraudSubmissionAddress = fraudSubmissionAddress
this.transactionSubmitter = transactionSubmitter
}
/*****************************
* Batch Submitter Overrides *
****************************/
public async _updateChainInfo(): Promise<void> {
const info: RollupInfo = await this._getRollupInfo()
if (info.mode === 'verifier') {
this.logger.error(
'Verifier mode enabled! Batch submitter only compatible with sequencer mode'
)
process.exit(1)
}
this.syncing = info.syncing
const addrs = await this._getChainAddresses()
const sccAddress = addrs.sccAddress
const ctcAddress = addrs.ctcAddress
if (
typeof this.chainContract !== 'undefined' &&
sccAddress === this.chainContract.address &&
ctcAddress === this.ctcContract.address
) {
this.logger.debug('Chain contract already initialized', {
sccAddress,
ctcAddress,
})
return
}
this.chainContract = (
await getContractFactory('OVM_StateCommitmentChain', this.signer)
).attach(sccAddress)
this.ctcContract = (
await getContractFactory('OVM_CanonicalTransactionChain', this.signer)
).attach(ctcAddress)
this.logger.info('Connected Optimism contracts', {
stateCommitmentChain: this.chainContract.address,
canonicalTransactionChain: this.ctcContract.address,
})
return
}
public async _onSync(): Promise<TransactionReceipt> {
this.logger.info('Syncing mode enabled! Skipping state batch submission...')
return
}
public async _getBatchStartAndEnd(): Promise<BlockRange> {
this.logger.info('Getting batch start and end for state batch submitter...')
const startBlock: number =
(await this.chainContract.getTotalElements()).toNumber() +
this.blockOffset
this.logger.info('Retrieved start block number from SCC', {
startBlock,
})
// We will submit state roots for txs which have been in the tx chain for a while.
const totalElements: number =
(await this.ctcContract.getTotalElements()).toNumber() + this.blockOffset
this.logger.info('Retrieved total elements from CTC', {
totalElements,
})
const endBlock: number = Math.min(
startBlock + this.maxBatchSize,
totalElements
)
if (startBlock >= endBlock) {
if (startBlock > endBlock) {
this.logger.error(
'State commitment chain is larger than transaction chain. This should never happen!'
)
}
this.logger.info(
'No state commitments to submit. Skipping batch submission...'
)
return
}
return {
start: startBlock,
end: endBlock,
}
}
public async _submitBatch(
startBlock: number,
endBlock: number
): Promise<TransactionReceipt> {
const batchTxBuildStart = performance.now()
const batch = await this._generateStateCommitmentBatch(startBlock, endBlock)
const calldata = this.chainContract.interface.encodeFunctionData(
'appendStateBatch',
[batch, startBlock]
)
const batchSizeInBytes = remove0x(calldata).length / 2
this.logger.debug('State batch generated', {
batchSizeInBytes,
calldata,
})
if (!this._shouldSubmitBatch(batchSizeInBytes)) {
return
}
const batchTxBuildEnd = performance.now()
this.metrics.batchTxBuildTime.set(batchTxBuildEnd - batchTxBuildStart)
const offsetStartsAtIndex = startBlock - this.blockOffset
this.logger.debug('Submitting batch.', { calldata })
// Generate the transaction we will repeatedly submit
const nonce = await this.signer.getTransactionCount()
const tx = await this.chainContract.populateTransaction.appendStateBatch(
batch,
offsetStartsAtIndex,
{ nonce }
)
const submitTransaction = (): Promise<TransactionReceipt> => {
return this.transactionSubmitter.submitTransaction(
tx,
this._makeHooks('appendStateBatch')
)
}
return this._submitAndLogTx(
submitTransaction,
'Submitted state root batch!'
)
}
/*********************
* Private Functions *
********************/
private async _generateStateCommitmentBatch(
startBlock: number,
endBlock: number
): Promise<Bytes32[]> {
const blockRange = endBlock - startBlock
const batch: Bytes32[] = await bPromise.map(
[...Array(blockRange).keys()],
async (i: number) => {
this.logger.debug('Fetching L2BatchElement', {
blockNo: startBlock + i,
})
const block = (await this.l2Provider.getBlockWithTransactions(
startBlock + i
)) as L2Block
const blockTx = block.transactions[0]
if (blockTx.from === this.fraudSubmissionAddress) {
this.logger.warn('Found transaction from fraud submission address', {
txHash: blockTx.hash,
fraudSubmissionAddress: this.fraudSubmissionAddress,
})
this.fraudSubmissionAddress = 'no fraud'
return '0xbad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1bad1'
}
return block.stateRoot
},
{ concurrency: 100 }
)
let tx = this.chainContract.interface.encodeFunctionData(
'appendStateBatch',
[batch, startBlock]
)
while (remove0x(tx).length / 2 > this.maxTxSize) {
batch.splice(Math.ceil((batch.length * 2) / 3)) // Delete 1/3rd of all of the batch elements
this.logger.debug('Splicing batch...', {
batchSizeInBytes: tx.length / 2,
})
tx = this.chainContract.interface.encodeFunctionData('appendStateBatch', [
batch,
startBlock,
])
}
this.logger.info('Generated state commitment batch', {
batch, // list of stateRoots
})
return batch
}
}
export * from './batch-submitter'
export * from './utils'
export * from './transaction-chain-contract'
/* External Imports */
import { Contract, ethers } from 'ethers'
import {
TransactionResponse,
TransactionRequest,
} from '@ethersproject/abstract-provider'
import {
AppendSequencerBatchParams,
BatchContext,
encodeAppendSequencerBatch,
sequencerBatch,
} from '@eth-optimism/core-utils'
export { encodeAppendSequencerBatch, BatchContext, AppendSequencerBatchParams }
/*
* OVM_CanonicalTransactionChainContract is a wrapper around a normal Ethers contract
* where the `appendSequencerBatch(...)` function uses a specialized encoding for improved efficiency.
*/
export class CanonicalTransactionChainContract extends Contract {
public customPopulateTransaction = {
appendSequencerBatch: async (
batch: AppendSequencerBatchParams
): Promise<ethers.PopulatedTransaction> => {
const nonce = await this.signer.getTransactionCount()
const to = this.address
const data = getEncodedCalldata(batch)
const gasLimit = await this.signer.provider.estimateGas({
to,
from: await this.signer.getAddress(),
data,
})
return {
nonce,
to,
data,
gasLimit,
}
},
}
public async appendSequencerBatch(
batch: AppendSequencerBatchParams,
options?: TransactionRequest
): Promise<TransactionResponse> {
return appendSequencerBatch(this, batch, options)
}
}
/**********************
* Internal Functions *
*********************/
const appendSequencerBatch = async (
OVM_CanonicalTransactionChain: Contract,
batch: AppendSequencerBatchParams,
options?: TransactionRequest
): Promise<TransactionResponse> => {
return OVM_CanonicalTransactionChain.signer.sendTransaction({
to: OVM_CanonicalTransactionChain.address,
data: getEncodedCalldata(batch),
...options,
})
}
const getEncodedCalldata = (params: AppendSequencerBatchParams): string => {
return sequencerBatch.encode(params)
}
import { Signer, ethers, PopulatedTransaction } from 'ethers'
import {
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import * as ynatm from '@eth-optimism/ynatm'
export interface ResubmissionConfig {
resubmissionTimeout: number
minGasPriceInGwei: number
maxGasPriceInGwei: number
gasRetryIncrement: number
}
export type SubmitTransactionFn = (
tx: PopulatedTransaction
) => Promise<TransactionReceipt>
export interface TxSubmissionHooks {
beforeSendTransaction: (tx: PopulatedTransaction) => void
onTransactionResponse: (txResponse: TransactionResponse) => void
}
const getGasPriceInGwei = async (signer: Signer): Promise<number> => {
return parseInt(
ethers.utils.formatUnits(await signer.getGasPrice(), 'gwei'),
10
)
}
export const ynatmRejectOn = (e) => {
// taken almost verbatim from the readme,
// see https://github.com/ethereum-optimism/ynatm.
// immediately rejects on reverts and nonce errors
const errMsg = e.toString().toLowerCase()
const conditions = ['revert', 'nonce']
for (const cond of conditions) {
if (errMsg.includes(cond)) {
return true
}
}
return false
}
export const submitTransactionWithYNATM = async (
tx: PopulatedTransaction,
signer: Signer,
config: ResubmissionConfig,
numConfirmations: number,
hooks: TxSubmissionHooks
): Promise<TransactionReceipt> => {
const sendTxAndWaitForReceipt = async (
gasPrice
): Promise<TransactionReceipt> => {
const fullTx = {
...tx,
gasPrice,
}
hooks.beforeSendTransaction(fullTx)
const txResponse = await signer.sendTransaction(fullTx)
hooks.onTransactionResponse(txResponse)
return signer.provider.waitForTransaction(txResponse.hash, numConfirmations)
}
const minGasPrice = await getGasPriceInGwei(signer)
const receipt = await ynatm.send({
sendTransactionFunction: sendTxAndWaitForReceipt,
minGasPrice: ynatm.toGwei(minGasPrice),
maxGasPrice: ynatm.toGwei(config.maxGasPriceInGwei),
gasPriceScalingFunction: ynatm.LINEAR(config.gasRetryIncrement),
delay: config.resubmissionTimeout,
rejectImmediatelyOnCondition: ynatmRejectOn,
})
return receipt
}
export interface TransactionSubmitter {
submitTransaction(
tx: PopulatedTransaction,
hooks?: TxSubmissionHooks
): Promise<TransactionReceipt>
}
export class YnatmTransactionSubmitter implements TransactionSubmitter {
constructor(
readonly signer: Signer,
readonly ynatmConfig: ResubmissionConfig,
readonly numConfirmations: number
) {}
public async submitTransaction(
tx: PopulatedTransaction,
hooks?: TxSubmissionHooks
): Promise<TransactionReceipt> {
if (!hooks) {
hooks = {
beforeSendTransaction: () => undefined,
onTransactionResponse: () => undefined,
}
}
return submitTransactionWithYNATM(
tx,
this.signer,
this.ynatmConfig,
this.numConfirmations,
hooks
)
}
}
/* External Imports */
import { defaultAccounts } from 'ethereum-waffle'
export const FORCE_INCLUSION_PERIOD_SECONDS = 600
export const DEFAULT_ACCOUNTS = defaultAccounts
export const DEFAULT_ACCOUNTS_HARDHAT = defaultAccounts.map((account) => {
return {
balance: account.balance,
privateKey: account.secretKey,
}
})
export const OVM_TX_GAS_LIMIT = 10_000_000
export const RUN_OVM_TEST_GAS = 20_000_000
/* External Imports */
import { keccak256 } from 'ethers/lib/utils'
export const DUMMY_BYTECODE = '0x123412341234'
export const DUMMY_BYTECODE_BYTELEN = 6
export const DUMMY_BYTECODE_HASH = keccak256(DUMMY_BYTECODE)
/* External Imports */
import { ethers } from 'ethers'
export const DUMMY_BYTES32: string[] = Array.from(
{
length: 10,
},
(_, i) => {
return ethers.utils.keccak256(`0x0${i}`)
}
)
export * from './bytes32'
export * from './bytecode'
export * from './dummy'
export * from './constants'
export * from './resolver'
/* External Imports */
import { ethers } from 'hardhat'
import { Contract } from 'ethers'
import { getContractFactory as ctFactory } from 'old-contracts'
export const getContractFactory = async (contract: string) =>
ctFactory(contract, (await ethers.getSigners())[0])
export const setProxyTarget = async (
AddressManager: Contract,
name: string,
target: Contract
): Promise<void> => {
const SimpleProxy: Contract = await (
await getContractFactory('Helper_SimpleProxy')
).deploy()
await SimpleProxy.setTarget(target.address)
await AddressManager.setAddress(name, SimpleProxy.address)
}
export const makeAddressManager = async (): Promise<Contract> => {
return (await getContractFactory('Lib_AddressManager')).deploy()
}
/* External Imports */
import chai = require('chai')
import sinonChai from 'sinon-chai'
import Mocha from 'mocha'
const should = chai.should()
const expect = chai.expect
chai.use(sinonChai)
export { should, expect, chai, Mocha }
import { ethers, BigNumber, Signer } from 'ethers'
import {
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import { expect } from '../setup'
import { submitTransactionWithYNATM } from '../../src/utils/tx-submission'
import { ResubmissionConfig } from '../../src'
const nullFunction = () => undefined
const nullHooks = {
beforeSendTransaction: nullFunction,
onTransactionResponse: nullFunction,
}
describe('submitTransactionWithYNATM', async () => {
it('calls sendTransaction, waitForTransaction, and hooks with correct inputs', async () => {
const called = {
sendTransaction: false,
waitForTransaction: false,
beforeSendTransaction: false,
onTransactionResponse: false,
}
const dummyHash = 'dummy hash'
const numConfirmations = 3
const tx = {
data: 'we here though',
} as ethers.PopulatedTransaction
const sendTransaction = async (
_tx: ethers.PopulatedTransaction
): Promise<TransactionResponse> => {
called.sendTransaction = true
expect(_tx.data).to.equal(tx.data)
return {
hash: dummyHash,
} as TransactionResponse
}
const waitForTransaction = async (
hash: string,
_numConfirmations: number
): Promise<TransactionReceipt> => {
called.waitForTransaction = true
expect(hash).to.equal(dummyHash)
expect(_numConfirmations).to.equal(numConfirmations)
return {
to: '',
from: '',
status: 1,
} as TransactionReceipt
}
const signer = {
getGasPrice: async () => ethers.BigNumber.from(0),
sendTransaction,
provider: {
waitForTransaction,
},
} as Signer
const hooks = {
beforeSendTransaction: (submittingTx: ethers.PopulatedTransaction) => {
called.beforeSendTransaction = true
expect(submittingTx.data).to.equal(tx.data)
},
onTransactionResponse: (txResponse: TransactionResponse) => {
called.onTransactionResponse = true
expect(txResponse.hash).to.equal(dummyHash)
},
}
const config: ResubmissionConfig = {
resubmissionTimeout: 1000,
minGasPriceInGwei: 0,
maxGasPriceInGwei: 0,
gasRetryIncrement: 1,
}
await submitTransactionWithYNATM(
tx,
signer,
config,
numConfirmations,
hooks
)
expect(called.sendTransaction).to.be.true
expect(called.waitForTransaction).to.be.true
expect(called.beforeSendTransaction).to.be.true
expect(called.onTransactionResponse).to.be.true
})
it('repeatedly increases the gas limit of the transaction when wait takes too long', async () => {
// Make transactions take longer to be included
// than our resubmission timeout
const resubmissionTimeout = 100
const txReceiptDelay = resubmissionTimeout * 3
let lastGasPrice = BigNumber.from(0)
// Create a transaction which has a gas price that we will watch increment
const tx = {
gasPrice: lastGasPrice.add(1),
data: 'hello world!',
} as ethers.PopulatedTransaction
const sendTransaction = async (
_tx: ethers.PopulatedTransaction
): Promise<TransactionResponse> => {
// Ensure the gas price is always increasing
expect(_tx.gasPrice > lastGasPrice).to.be.true
lastGasPrice = _tx.gasPrice
return {
hash: 'dummy hash',
} as TransactionResponse
}
const waitForTransaction = async (): Promise<TransactionReceipt> => {
await new Promise((r) => setTimeout(r, txReceiptDelay))
return {} as TransactionReceipt
}
const signer = {
getGasPrice: async () => ethers.BigNumber.from(0),
sendTransaction,
provider: {
waitForTransaction: waitForTransaction as any,
},
} as Signer
const config: ResubmissionConfig = {
resubmissionTimeout,
minGasPriceInGwei: 0,
maxGasPriceInGwei: 1000,
gasRetryIncrement: 1,
}
await submitTransactionWithYNATM(tx, signer, config, 0, nullHooks)
})
it('should immediately reject if a nonce error is encountered', async () => {
const tx = {
gasPrice: BigNumber.from(1),
data: 'hello world!',
} as ethers.PopulatedTransaction
let txCount = 0
const waitForTransaction = async (): Promise<TransactionReceipt> => {
return {} as TransactionReceipt
}
const sendTransaction = async () => {
txCount++
throw new Error('Transaction nonce is too low.')
}
const signer = {
getGasPrice: async () => BigNumber.from(1),
sendTransaction: sendTransaction as any,
provider: {
waitForTransaction: waitForTransaction as any,
},
} as Signer
const config: ResubmissionConfig = {
resubmissionTimeout: 100,
minGasPriceInGwei: 0,
maxGasPriceInGwei: 1000,
gasRetryIncrement: 1,
}
try {
await submitTransactionWithYNATM(tx, signer, config, 0, nullHooks)
} catch (e) {
expect(txCount).to.equal(1)
return
}
expect.fail('Expected an error.')
})
})
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": [
"src/**/*"
]
}
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"resolveJsonModule": true
}
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment