Commit a8c9cd67 authored by Karl Floersch's avatar Karl Floersch Committed by GitHub

Merge pull request #1024 from ethereum-optimism/fix/broken-estimate-gas-when-pending-tx-in-mempool

fix: estimate gas when pending tx in mempool
parents 6702ce41 e2985bf7
---
'@eth-optimism/batch-submitter': patch
---
Fix tx resubmission estimateGas bug in batch submitter
/* External Imports */ /* External Imports */
import { Contract, Signer, utils, providers } from 'ethers' import {
import { TransactionReceipt } from '@ethersproject/abstract-provider' Contract,
Signer,
utils,
providers,
PopulatedTransaction,
} from 'ethers'
import {
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import { Gauge, Histogram, Counter } from 'prom-client' import { Gauge, Histogram, Counter } from 'prom-client'
import * as ynatm from '@eth-optimism/ynatm'
import { RollupInfo, sleep } from '@eth-optimism/core-utils' import { RollupInfo, sleep } from '@eth-optimism/core-utils'
import { Logger, Metrics } from '@eth-optimism/common-ts' import { Logger, Metrics } from '@eth-optimism/common-ts'
import { getContractFactory } from 'old-contracts' import { getContractFactory } from 'old-contracts'
/* Internal Imports */
import { TxSubmissionHooks } from '..'
export interface BlockRange { export interface BlockRange {
start: number start: number
end: number end: number
} }
export interface ResubmissionConfig {
resubmissionTimeout: number
minGasPriceInGwei: number
maxGasPriceInGwei: number
gasRetryIncrement: number
}
interface BatchSubmitterMetrics { interface BatchSubmitterMetrics {
batchSubmitterETHBalance: Gauge<string> batchSubmitterETHBalance: Gauge<string>
...@@ -49,10 +53,6 @@ export abstract class BatchSubmitter { ...@@ -49,10 +53,6 @@ export abstract class BatchSubmitter {
readonly finalityConfirmations: number, readonly finalityConfirmations: number,
readonly addressManagerAddress: string, readonly addressManagerAddress: string,
readonly minBalanceEther: number, readonly minBalanceEther: number,
readonly minGasPriceInGwei: number,
readonly maxGasPriceInGwei: number,
readonly gasRetryIncrement: number,
readonly gasThresholdInGwei: number,
readonly blockOffset: number, readonly blockOffset: number,
readonly logger: Logger, readonly logger: Logger,
readonly defaultMetrics: Metrics readonly defaultMetrics: Metrics
...@@ -190,69 +190,37 @@ export abstract class BatchSubmitter { ...@@ -190,69 +190,37 @@ export abstract class BatchSubmitter {
return true return true
} }
public static async getReceiptWithResubmission( protected _makeHooks(txName: string): TxSubmissionHooks {
txFunc: (gasPrice) => Promise<TransactionReceipt>, return {
resubmissionConfig: ResubmissionConfig, beforeSendTransaction: (tx: PopulatedTransaction) => {
logger: Logger this.logger.info(`Submitting ${txName} transaction`, {
): Promise<TransactionReceipt> { gasPrice: tx.gasPrice,
const { nonce: tx.nonce,
resubmissionTimeout, contractAddr: this.chainContract.address,
minGasPriceInGwei, })
maxGasPriceInGwei, },
gasRetryIncrement, onTransactionResponse: (txResponse: TransactionResponse) => {
} = resubmissionConfig this.logger.info(`Submitted ${txName} transaction`, {
txHash: txResponse.hash,
const receipt = await ynatm.send({ from: txResponse.from,
sendTransactionFunction: txFunc, })
minGasPrice: ynatm.toGwei(minGasPriceInGwei), this.logger.debug(`${txName} transaction data`, {
maxGasPrice: ynatm.toGwei(maxGasPriceInGwei), data: txResponse.data,
gasPriceScalingFunction: ynatm.LINEAR(gasRetryIncrement), })
delay: resubmissionTimeout, },
})
logger.debug('Resubmission tx receipt', { receipt })
return receipt
}
private async _getMinGasPriceInGwei(): Promise<number> {
if (this.minGasPriceInGwei !== 0) {
return this.minGasPriceInGwei
}
let minGasPriceInGwei = parseInt(
utils.formatUnits(await this.signer.getGasPrice(), 'gwei'),
10
)
if (minGasPriceInGwei > this.maxGasPriceInGwei) {
this.logger.warn(
'Minimum gas price is higher than max! Ethereum must be congested...'
)
minGasPriceInGwei = this.maxGasPriceInGwei
} }
return minGasPriceInGwei
} }
protected async _submitAndLogTx( protected async _submitAndLogTx(
txFunc: (gasPrice) => Promise<TransactionReceipt>, submitTransaction: () => Promise<TransactionReceipt>,
successMessage: string successMessage: string
): Promise<TransactionReceipt> { ): Promise<TransactionReceipt> {
this.lastBatchSubmissionTimestamp = Date.now() this.lastBatchSubmissionTimestamp = Date.now()
this.logger.debug('Waiting for receipt...') this.logger.debug('Submitting transaction & waiting for receipt...')
const resubmissionConfig: ResubmissionConfig = {
resubmissionTimeout: this.resubmissionTimeout,
minGasPriceInGwei: await this._getMinGasPriceInGwei(),
maxGasPriceInGwei: this.maxGasPriceInGwei,
gasRetryIncrement: this.gasRetryIncrement,
}
let receipt: TransactionReceipt let receipt: TransactionReceipt
try { try {
receipt = await BatchSubmitter.getReceiptWithResubmission( receipt = await submitTransaction()
txFunc,
resubmissionConfig,
this.logger
)
} catch (err) { } catch (err) {
this.metrics.failedSubmissions.inc() this.metrics.failedSubmissions.inc()
if (err.reason) { if (err.reason) {
......
...@@ -13,6 +13,7 @@ import { Logger, Metrics } from '@eth-optimism/common-ts' ...@@ -13,6 +13,7 @@ import { Logger, Metrics } from '@eth-optimism/common-ts'
/* Internal Imports */ /* Internal Imports */
import { BlockRange, BatchSubmitter } from '.' import { BlockRange, BatchSubmitter } from '.'
import { TransactionSubmitter } from '../utils'
export class StateBatchSubmitter extends BatchSubmitter { export class StateBatchSubmitter extends BatchSubmitter {
// TODO: Change this so that we calculate start = scc.totalElements() and end = ctc.totalElements()! // TODO: Change this so that we calculate start = scc.totalElements() and end = ctc.totalElements()!
...@@ -23,6 +24,7 @@ export class StateBatchSubmitter extends BatchSubmitter { ...@@ -23,6 +24,7 @@ export class StateBatchSubmitter extends BatchSubmitter {
protected syncing: boolean protected syncing: boolean
protected ctcContract: Contract protected ctcContract: Contract
private fraudSubmissionAddress: string private fraudSubmissionAddress: string
private transactionSubmitter: TransactionSubmitter
constructor( constructor(
signer: Signer, signer: Signer,
...@@ -36,10 +38,7 @@ export class StateBatchSubmitter extends BatchSubmitter { ...@@ -36,10 +38,7 @@ export class StateBatchSubmitter extends BatchSubmitter {
finalityConfirmations: number, finalityConfirmations: number,
addressManagerAddress: string, addressManagerAddress: string,
minBalanceEther: number, minBalanceEther: number,
minGasPriceInGwei: number, transactionSubmitter: TransactionSubmitter,
maxGasPriceInGwei: number,
gasRetryIncrement: number,
gasThresholdInGwei: number,
blockOffset: number, blockOffset: number,
logger: Logger, logger: Logger,
metrics: Metrics, metrics: Metrics,
...@@ -57,15 +56,12 @@ export class StateBatchSubmitter extends BatchSubmitter { ...@@ -57,15 +56,12 @@ export class StateBatchSubmitter extends BatchSubmitter {
finalityConfirmations, finalityConfirmations,
addressManagerAddress, addressManagerAddress,
minBalanceEther, minBalanceEther,
minGasPriceInGwei,
maxGasPriceInGwei,
gasRetryIncrement,
gasThresholdInGwei,
blockOffset, blockOffset,
logger, logger,
metrics metrics
) )
this.fraudSubmissionAddress = fraudSubmissionAddress this.fraudSubmissionAddress = fraudSubmissionAddress
this.transactionSubmitter = transactionSubmitter
} }
/***************************** /*****************************
...@@ -159,14 +155,14 @@ export class StateBatchSubmitter extends BatchSubmitter { ...@@ -159,14 +155,14 @@ export class StateBatchSubmitter extends BatchSubmitter {
endBlock: number endBlock: number
): Promise<TransactionReceipt> { ): Promise<TransactionReceipt> {
const batch = await this._generateStateCommitmentBatch(startBlock, endBlock) const batch = await this._generateStateCommitmentBatch(startBlock, endBlock)
const tx = this.chainContract.interface.encodeFunctionData( const calldata = this.chainContract.interface.encodeFunctionData(
'appendStateBatch', 'appendStateBatch',
[batch, startBlock] [batch, startBlock]
) )
const batchSizeInBytes = remove0x(tx).length / 2 const batchSizeInBytes = remove0x(calldata).length / 2
this.logger.debug('State batch generated', { this.logger.debug('State batch generated', {
batchSizeInBytes, batchSizeInBytes,
tx, calldata,
}) })
if (!this._shouldSubmitBatch(batchSizeInBytes)) { if (!this._shouldSubmitBatch(batchSizeInBytes)) {
...@@ -174,33 +170,23 @@ export class StateBatchSubmitter extends BatchSubmitter { ...@@ -174,33 +170,23 @@ export class StateBatchSubmitter extends BatchSubmitter {
} }
const offsetStartsAtIndex = startBlock - this.blockOffset const offsetStartsAtIndex = startBlock - this.blockOffset
this.logger.debug('Submitting batch.', { tx }) this.logger.debug('Submitting batch.', { calldata })
const nonce = await this.signer.getTransactionCount() // Generate the transaction we will repeatedly submit
const contractFunction = async (gasPrice): Promise<TransactionReceipt> => { const tx = await this.chainContract.populateTransaction.appendStateBatch(
this.logger.info('Submitting appendStateBatch transaction', { batch,
gasPrice, offsetStartsAtIndex
nonce, )
contractAddr: this.chainContract.address, const submitTransaction = (): Promise<TransactionReceipt> => {
}) return this.transactionSubmitter.submitTransaction(
const contractTx = await this.chainContract.appendStateBatch( tx,
batch, this._makeHooks('appendStateBatch')
offsetStartsAtIndex,
{ nonce, gasPrice }
)
this.logger.info('Submitted appendStateBatch transaction', {
txHash: contractTx.hash,
from: contractTx.from,
})
this.logger.debug('appendStateBatch transaction data', {
data: contractTx.data,
})
return this.signer.provider.waitForTransaction(
contractTx.hash,
this.numConfirmations
) )
} }
return this._submitAndLogTx(contractFunction, 'Submitted state root batch!') return this._submitAndLogTx(
submitTransaction,
'Submitted state root batch!'
)
} }
/********************* /*********************
......
...@@ -22,6 +22,7 @@ import { ...@@ -22,6 +22,7 @@ import {
} from '../transaction-chain-contract' } from '../transaction-chain-contract'
import { BlockRange, BatchSubmitter } from '.' import { BlockRange, BatchSubmitter } from '.'
import { TransactionSubmitter } from '../utils'
export interface AutoFixBatchOptions { export interface AutoFixBatchOptions {
fixDoublePlayedDeposits: boolean fixDoublePlayedDeposits: boolean
...@@ -35,6 +36,8 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -35,6 +36,8 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
protected syncing: boolean protected syncing: boolean
private disableQueueBatchAppend: boolean private disableQueueBatchAppend: boolean
private autoFixBatchOptions: AutoFixBatchOptions private autoFixBatchOptions: AutoFixBatchOptions
private transactionSubmitter: TransactionSubmitter
private gasThresholdInGwei: number
constructor( constructor(
signer: Signer, signer: Signer,
...@@ -47,10 +50,8 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -47,10 +50,8 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
resubmissionTimeout: number, resubmissionTimeout: number,
addressManagerAddress: string, addressManagerAddress: string,
minBalanceEther: number, minBalanceEther: number,
minGasPriceInGwei: number,
maxGasPriceInGwei: number,
gasRetryIncrement: number,
gasThresholdInGwei: number, gasThresholdInGwei: number,
transactionSubmitter: TransactionSubmitter,
blockOffset: number, blockOffset: number,
logger: Logger, logger: Logger,
metrics: Metrics, metrics: Metrics,
...@@ -73,16 +74,14 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -73,16 +74,14 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
0, // Supply dummy value because it is not used. 0, // Supply dummy value because it is not used.
addressManagerAddress, addressManagerAddress,
minBalanceEther, minBalanceEther,
minGasPriceInGwei,
maxGasPriceInGwei,
gasRetryIncrement,
gasThresholdInGwei,
blockOffset, blockOffset,
logger, logger,
metrics metrics
) )
this.disableQueueBatchAppend = disableQueueBatchAppend this.disableQueueBatchAppend = disableQueueBatchAppend
this.autoFixBatchOptions = autoFixBatchOptions this.autoFixBatchOptions = autoFixBatchOptions
this.gasThresholdInGwei = gasThresholdInGwei
this.transactionSubmitter = transactionSubmitter
} }
/***************************** /*****************************
...@@ -140,34 +139,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -140,34 +139,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
) )
if (!this.disableQueueBatchAppend) { if (!this.disableQueueBatchAppend) {
const nonce = await this.signer.getTransactionCount() return this.submitAppendQueueBatch()
const contractFunction = async (
gasPrice
): Promise<TransactionReceipt> => {
this.logger.info('Submitting appendQueueBatch transaction', {
gasPrice,
nonce,
contractAddr: this.chainContract.address,
})
const tx = await this.chainContract.appendQueueBatch(99999999, {
nonce,
gasPrice,
})
this.logger.info('Submitted appendQueueBatch transaction', {
txHash: tx.hash,
from: tx.from,
})
this.logger.debug('appendQueueBatch transaction data', {
data: tx.data,
})
return this.signer.provider.waitForTransaction(
tx.hash,
this.numConfirmations
)
}
// Empty the queue with a huge `appendQueueBatch(..)` call
return this._submitAndLogTx(contractFunction, 'Cleared queue!')
} }
} }
this.logger.info('Syncing mode enabled but queue is empty. Skipping...') this.logger.info('Syncing mode enabled but queue is empty. Skipping...')
...@@ -250,36 +222,43 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ...@@ -250,36 +222,43 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
l1tipHeight, l1tipHeight,
}) })
const nonce = await this.signer.getTransactionCount() return this.submitAppendSequencerBatch(batchParams)
const contractFunction = async (gasPrice): Promise<TransactionReceipt> => {
this.logger.info('Submitting appendSequencerBatch transaction', {
gasPrice,
nonce,
contractAddr: this.chainContract.address,
})
const tx = await this.chainContract.appendSequencerBatch(batchParams, {
nonce,
gasPrice,
})
this.logger.info('Submitted appendSequencerBatch transaction', {
txHash: tx.hash,
from: tx.from,
})
this.logger.debug('appendSequencerBatch transaction data', {
data: tx.data,
})
return this.signer.provider.waitForTransaction(
tx.hash,
this.numConfirmations
)
}
return this._submitAndLogTx(contractFunction, 'Submitted batch!')
} }
/********************* /*********************
* Private Functions * * Private Functions *
********************/ ********************/
private async submitAppendQueueBatch(): Promise<TransactionReceipt> {
const tx = await this.chainContract.populateTransaction.appendQueueBatch(
ethers.constants.MaxUint256 // Completely empty the queue by appending (up to) an enormous number of queue elements.
)
const submitTransaction = (): Promise<TransactionReceipt> => {
return this.transactionSubmitter.submitTransaction(
tx,
this._makeHooks('appendQueueBatch')
)
}
// Empty the queue with a huge `appendQueueBatch(..)` call
return this._submitAndLogTx(submitTransaction, 'Cleared queue!')
}
private async submitAppendSequencerBatch(
batchParams: AppendSequencerBatchParams
): Promise<TransactionReceipt> {
const tx =
await this.chainContract.customPopulateTransaction.appendSequencerBatch(
batchParams
)
const submitTransaction = (): Promise<TransactionReceipt> => {
return this.transactionSubmitter.submitTransaction(
tx,
this._makeHooks('appendSequencerBatch')
)
}
return this._submitAndLogTx(submitTransaction, 'Submitted batch!')
}
private async _generateSequencerBatchParams( private async _generateSequencerBatchParams(
startBlock: number, startBlock: number,
endBlock: number endBlock: number
......
...@@ -16,6 +16,11 @@ import { ...@@ -16,6 +16,11 @@ import {
STATE_BATCH_SUBMITTER_LOG_TAG, STATE_BATCH_SUBMITTER_LOG_TAG,
TX_BATCH_SUBMITTER_LOG_TAG, TX_BATCH_SUBMITTER_LOG_TAG,
} from '..' } from '..'
import {
TransactionSubmitter,
YnatmTransactionSubmitter,
ResubmissionConfig,
} from '../utils'
interface RequiredEnvVars { interface RequiredEnvVars {
// The HTTP provider URL for L1. // The HTTP provider URL for L1.
...@@ -349,6 +354,18 @@ export const run = async () => { ...@@ -349,6 +354,18 @@ export const run = async () => {
addressManagerAddress: requiredEnvVars.ADDRESS_MANAGER_ADDRESS, addressManagerAddress: requiredEnvVars.ADDRESS_MANAGER_ADDRESS,
}) })
const resubmissionConfig: ResubmissionConfig = {
resubmissionTimeout: requiredEnvVars.RESUBMISSION_TIMEOUT * 1_000,
minGasPriceInGwei: MIN_GAS_PRICE_IN_GWEI,
maxGasPriceInGwei: GAS_THRESHOLD_IN_GWEI,
gasRetryIncrement: GAS_RETRY_INCREMENT,
}
const txBatchTxSubmitter: TransactionSubmitter =
new YnatmTransactionSubmitter(
sequencerSigner,
resubmissionConfig,
requiredEnvVars.NUM_CONFIRMATIONS
)
const txBatchSubmitter = new TransactionBatchSubmitter( const txBatchSubmitter = new TransactionBatchSubmitter(
sequencerSigner, sequencerSigner,
l2Provider, l2Provider,
...@@ -360,10 +377,8 @@ export const run = async () => { ...@@ -360,10 +377,8 @@ export const run = async () => {
requiredEnvVars.RESUBMISSION_TIMEOUT * 1_000, requiredEnvVars.RESUBMISSION_TIMEOUT * 1_000,
requiredEnvVars.ADDRESS_MANAGER_ADDRESS, requiredEnvVars.ADDRESS_MANAGER_ADDRESS,
requiredEnvVars.SAFE_MINIMUM_ETHER_BALANCE, requiredEnvVars.SAFE_MINIMUM_ETHER_BALANCE,
MIN_GAS_PRICE_IN_GWEI,
MAX_GAS_PRICE_IN_GWEI,
GAS_RETRY_INCREMENT,
GAS_THRESHOLD_IN_GWEI, GAS_THRESHOLD_IN_GWEI,
txBatchTxSubmitter,
BLOCK_OFFSET, BLOCK_OFFSET,
logger.child({ name: TX_BATCH_SUBMITTER_LOG_TAG }), logger.child({ name: TX_BATCH_SUBMITTER_LOG_TAG }),
metrics, metrics,
...@@ -371,6 +386,12 @@ export const run = async () => { ...@@ -371,6 +386,12 @@ export const run = async () => {
autoFixBatchOptions autoFixBatchOptions
) )
const stateBatchTxSubmitter: TransactionSubmitter =
new YnatmTransactionSubmitter(
proposerSigner,
resubmissionConfig,
requiredEnvVars.NUM_CONFIRMATIONS
)
const stateBatchSubmitter = new StateBatchSubmitter( const stateBatchSubmitter = new StateBatchSubmitter(
proposerSigner, proposerSigner,
l2Provider, l2Provider,
...@@ -383,10 +404,7 @@ export const run = async () => { ...@@ -383,10 +404,7 @@ export const run = async () => {
requiredEnvVars.FINALITY_CONFIRMATIONS, requiredEnvVars.FINALITY_CONFIRMATIONS,
requiredEnvVars.ADDRESS_MANAGER_ADDRESS, requiredEnvVars.ADDRESS_MANAGER_ADDRESS,
requiredEnvVars.SAFE_MINIMUM_ETHER_BALANCE, requiredEnvVars.SAFE_MINIMUM_ETHER_BALANCE,
MIN_GAS_PRICE_IN_GWEI, stateBatchTxSubmitter,
MAX_GAS_PRICE_IN_GWEI,
GAS_RETRY_INCREMENT,
GAS_THRESHOLD_IN_GWEI,
BLOCK_OFFSET, BLOCK_OFFSET,
logger.child({ name: STATE_BATCH_SUBMITTER_LOG_TAG }), logger.child({ name: STATE_BATCH_SUBMITTER_LOG_TAG }),
metrics, metrics,
......
export * from './batch-submitter' export * from './batch-submitter'
export * from './utils'
export * from './transaction-chain-contract' export * from './transaction-chain-contract'
/* External Imports */ /* External Imports */
import { Contract, BigNumber } from 'ethers' import { Contract, ethers } from 'ethers'
import { import {
TransactionResponse, TransactionResponse,
TransactionRequest, TransactionRequest,
...@@ -9,7 +9,7 @@ import { ...@@ -9,7 +9,7 @@ import {
AppendSequencerBatchParams, AppendSequencerBatchParams,
BatchContext, BatchContext,
encodeAppendSequencerBatch, encodeAppendSequencerBatch,
encodeHex, remove0x,
} from '@eth-optimism/core-utils' } from '@eth-optimism/core-utils'
export { encodeAppendSequencerBatch, BatchContext, AppendSequencerBatchParams } export { encodeAppendSequencerBatch, BatchContext, AppendSequencerBatchParams }
...@@ -19,6 +19,27 @@ export { encodeAppendSequencerBatch, BatchContext, AppendSequencerBatchParams } ...@@ -19,6 +19,27 @@ export { encodeAppendSequencerBatch, BatchContext, AppendSequencerBatchParams }
* where the `appendSequencerBatch(...)` function uses a specialized encoding for improved efficiency. * where the `appendSequencerBatch(...)` function uses a specialized encoding for improved efficiency.
*/ */
export class CanonicalTransactionChainContract extends Contract { 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( public async appendSequencerBatch(
batch: AppendSequencerBatchParams, batch: AppendSequencerBatchParams,
options?: TransactionRequest options?: TransactionRequest
...@@ -31,29 +52,24 @@ export class CanonicalTransactionChainContract extends Contract { ...@@ -31,29 +52,24 @@ export class CanonicalTransactionChainContract extends Contract {
* Internal Functions * * Internal Functions *
*********************/ *********************/
const APPEND_SEQUENCER_BATCH_METHOD_ID = 'appendSequencerBatch()' const APPEND_SEQUENCER_BATCH_METHOD_ID = keccak256(
Buffer.from('appendSequencerBatch()')
).slice(2, 10)
const appendSequencerBatch = async ( const appendSequencerBatch = async (
OVM_CanonicalTransactionChain: Contract, OVM_CanonicalTransactionChain: Contract,
batch: AppendSequencerBatchParams, batch: AppendSequencerBatchParams,
options?: TransactionRequest options?: TransactionRequest
): Promise<TransactionResponse> => { ): Promise<TransactionResponse> => {
const methodId = keccak256(
Buffer.from(APPEND_SEQUENCER_BATCH_METHOD_ID)
).slice(2, 10)
const calldata = encodeAppendSequencerBatch(batch)
return OVM_CanonicalTransactionChain.signer.sendTransaction({ return OVM_CanonicalTransactionChain.signer.sendTransaction({
to: OVM_CanonicalTransactionChain.address, to: OVM_CanonicalTransactionChain.address,
data: '0x' + methodId + calldata, data: getEncodedCalldata(batch),
...options, ...options,
}) })
} }
const encodeBatchContext = (context: BatchContext): string => { const getEncodedCalldata = (batch: AppendSequencerBatchParams): string => {
return ( const methodId = APPEND_SEQUENCER_BATCH_METHOD_ID
encodeHex(context.numSequencedTransactions, 6) + const calldata = encodeAppendSequencerBatch(batch)
encodeHex(context.numSubsequentQueueTransactions, 6) + return '0x' + remove0x(methodId) + remove0x(calldata)
encodeHex(context.timestamp, 10) +
encodeHex(context.blockNumber, 10)
)
} }
export * from './tx-submission'
import { Signer, utils, 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 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,
})
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
)
}
}
...@@ -27,6 +27,8 @@ import { ...@@ -27,6 +27,8 @@ import {
TX_BATCH_SUBMITTER_LOG_TAG, TX_BATCH_SUBMITTER_LOG_TAG,
STATE_BATCH_SUBMITTER_LOG_TAG, STATE_BATCH_SUBMITTER_LOG_TAG,
BatchSubmitter, BatchSubmitter,
YnatmTransactionSubmitter,
ResubmissionConfig,
} from '../../src' } from '../../src'
import { import {
...@@ -200,8 +202,19 @@ describe('BatchSubmitter', () => { ...@@ -200,8 +202,19 @@ describe('BatchSubmitter', () => {
sinon.restore() sinon.restore()
}) })
const createBatchSubmitter = (timeout: number): TransactionBatchSubmitter => const createBatchSubmitter = (timeout: number): TransactionBatchSubmitter => {
new TransactionBatchSubmitter( const resubmissionConfig: ResubmissionConfig = {
resubmissionTimeout: 100000,
minGasPriceInGwei: MIN_GAS_PRICE_IN_GWEI,
maxGasPriceInGwei: GAS_THRESHOLD_IN_GWEI,
gasRetryIncrement: GAS_RETRY_INCREMENT,
}
const txBatchTxSubmitter = new YnatmTransactionSubmitter(
sequencer,
resubmissionConfig,
1
)
return new TransactionBatchSubmitter(
sequencer, sequencer,
l2Provider as any, l2Provider as any,
MIN_TX_SIZE, MIN_TX_SIZE,
...@@ -212,15 +225,14 @@ describe('BatchSubmitter', () => { ...@@ -212,15 +225,14 @@ describe('BatchSubmitter', () => {
100000, 100000,
AddressManager.address, AddressManager.address,
1, 1,
MIN_GAS_PRICE_IN_GWEI,
MAX_GAS_PRICE_IN_GWEI,
GAS_RETRY_INCREMENT,
GAS_THRESHOLD_IN_GWEI, GAS_THRESHOLD_IN_GWEI,
txBatchTxSubmitter,
1, 1,
new Logger({ name: TX_BATCH_SUBMITTER_LOG_TAG }), new Logger({ name: TX_BATCH_SUBMITTER_LOG_TAG }),
testMetrics, testMetrics,
false false
) )
}
describe('TransactionBatchSubmitter', () => { describe('TransactionBatchSubmitter', () => {
describe('submitNextBatch', () => { describe('submitNextBatch', () => {
...@@ -375,7 +387,7 @@ describe('BatchSubmitter', () => { ...@@ -375,7 +387,7 @@ describe('BatchSubmitter', () => {
.callsFake(async () => lowGasPriceWei) .callsFake(async () => lowGasPriceWei)
const receipt = await batchSubmitter.submitNextBatch() const receipt = await batchSubmitter.submitNextBatch()
expect(sequencer.getGasPrice).to.have.been.calledOnce expect(sequencer.getGasPrice).to.have.been.calledTwice
expect(receipt).to.not.be.undefined expect(receipt).to.not.be.undefined
}) })
}) })
...@@ -417,6 +429,17 @@ describe('BatchSubmitter', () => { ...@@ -417,6 +429,17 @@ describe('BatchSubmitter', () => {
// submit a batch of transactions to enable state batch submission // submit a batch of transactions to enable state batch submission
await txBatchSubmitter.submitNextBatch() await txBatchSubmitter.submitNextBatch()
const resubmissionConfig: ResubmissionConfig = {
resubmissionTimeout: 100000,
minGasPriceInGwei: MIN_GAS_PRICE_IN_GWEI,
maxGasPriceInGwei: GAS_THRESHOLD_IN_GWEI,
gasRetryIncrement: GAS_RETRY_INCREMENT,
}
const stateBatchTxSubmitter = new YnatmTransactionSubmitter(
sequencer,
resubmissionConfig,
1
)
stateBatchSubmitter = new StateBatchSubmitter( stateBatchSubmitter = new StateBatchSubmitter(
sequencer, sequencer,
l2Provider as any, l2Provider as any,
...@@ -429,10 +452,7 @@ describe('BatchSubmitter', () => { ...@@ -429,10 +452,7 @@ describe('BatchSubmitter', () => {
0, // finalityConfirmations 0, // finalityConfirmations
AddressManager.address, AddressManager.address,
1, 1,
MIN_GAS_PRICE_IN_GWEI, stateBatchTxSubmitter,
MAX_GAS_PRICE_IN_GWEI,
GAS_RETRY_INCREMENT,
GAS_THRESHOLD_IN_GWEI,
1, 1,
new Logger({ name: STATE_BATCH_SUBMITTER_LOG_TAG }), new Logger({ name: STATE_BATCH_SUBMITTER_LOG_TAG }),
testMetrics, testMetrics,
...@@ -473,54 +493,4 @@ describe('Batch Submitter with Ganache', () => { ...@@ -473,54 +493,4 @@ describe('Batch Submitter with Ganache', () => {
after(async () => { after(async () => {
await server.close() 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: predeploys.OVM_SequencerEntrypoint,
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
})
}) })
import { expect } from '../setup'
import { ethers, BigNumber, Signer } from 'ethers'
import { submitTransactionWithYNATM } from '../../src/utils/tx-submission'
import { ResubmissionConfig } from '../../src'
import {
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/abstract-provider'
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
const numConfirmations = 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 (
hash: string,
_numConfirmations: number
): Promise<TransactionReceipt> => {
await new Promise((r) => setTimeout(r, txReceiptDelay))
return {} as TransactionReceipt
}
const signer = {
getGasPrice: async () => ethers.BigNumber.from(0),
sendTransaction,
provider: {
waitForTransaction,
},
} as Signer
const config: ResubmissionConfig = {
resubmissionTimeout,
minGasPriceInGwei: 0,
maxGasPriceInGwei: 1000,
gasRetryIncrement: 1,
}
await submitTransactionWithYNATM(tx, signer, config, 0, nullHooks)
})
})
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