Commit e2985bf7 authored by Karl Floersch's avatar Karl Floersch

integrate new tx submission lib

parent 7b43840c
/* External Imports */
import { Contract, Signer, utils, providers } from 'ethers'
import { TransactionReceipt } from '@ethersproject/abstract-provider'
import {
Contract,
Signer,
utils,
providers,
PopulatedTransaction,
} from 'ethers'
import {
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/abstract-provider'
import { Gauge, Histogram, Counter } from 'prom-client'
import * as ynatm from '@eth-optimism/ynatm'
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
}
export interface ResubmissionConfig {
resubmissionTimeout: number
minGasPriceInGwei: number
maxGasPriceInGwei: number
gasRetryIncrement: number
}
interface BatchSubmitterMetrics {
batchSubmitterETHBalance: Gauge<string>
......@@ -49,10 +53,6 @@ export abstract class BatchSubmitter {
readonly finalityConfirmations: number,
readonly addressManagerAddress: string,
readonly minBalanceEther: number,
readonly minGasPriceInGwei: number,
readonly maxGasPriceInGwei: number,
readonly gasRetryIncrement: number,
readonly gasThresholdInGwei: number,
readonly blockOffset: number,
readonly logger: Logger,
readonly defaultMetrics: Metrics
......@@ -190,69 +190,37 @@ export abstract class BatchSubmitter {
return true
}
public static async getReceiptWithResubmission(
txFunc: (gasPrice) => Promise<TransactionReceipt>,
resubmissionConfig: ResubmissionConfig,
logger: Logger
): Promise<TransactionReceipt> {
const {
resubmissionTimeout,
minGasPriceInGwei,
maxGasPriceInGwei,
gasRetryIncrement,
} = resubmissionConfig
const receipt = await ynatm.send({
sendTransactionFunction: txFunc,
minGasPrice: ynatm.toGwei(minGasPriceInGwei),
maxGasPrice: ynatm.toGwei(maxGasPriceInGwei),
gasPriceScalingFunction: ynatm.LINEAR(gasRetryIncrement),
delay: resubmissionTimeout,
protected _makeHooks(txName: string): TxSubmissionHooks {
return {
beforeSendTransaction: (tx: PopulatedTransaction) => {
this.logger.info(`Submitting ${txName} transaction`, {
gasPrice: tx.gasPrice,
nonce: tx.nonce,
contractAddr: this.chainContract.address,
})
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
},
onTransactionResponse: (txResponse: TransactionResponse) => {
this.logger.info(`Submitted ${txName} transaction`, {
txHash: txResponse.hash,
from: txResponse.from,
})
this.logger.debug(`${txName} transaction data`, {
data: txResponse.data,
})
},
}
return minGasPriceInGwei
}
protected async _submitAndLogTx(
txFunc: (gasPrice) => Promise<TransactionReceipt>,
submitTransaction: () => Promise<TransactionReceipt>,
successMessage: string
): Promise<TransactionReceipt> {
this.lastBatchSubmissionTimestamp = Date.now()
this.logger.debug('Waiting for receipt...')
const resubmissionConfig: ResubmissionConfig = {
resubmissionTimeout: this.resubmissionTimeout,
minGasPriceInGwei: await this._getMinGasPriceInGwei(),
maxGasPriceInGwei: this.maxGasPriceInGwei,
gasRetryIncrement: this.gasRetryIncrement,
}
this.logger.debug('Submitting transaction & waiting for receipt...')
let receipt: TransactionReceipt
try {
receipt = await BatchSubmitter.getReceiptWithResubmission(
txFunc,
resubmissionConfig,
this.logger
)
receipt = await submitTransaction()
} catch (err) {
this.metrics.failedSubmissions.inc()
if (err.reason) {
......
......@@ -13,6 +13,7 @@ import { Logger, Metrics } from '@eth-optimism/common-ts'
/* Internal Imports */
import { BlockRange, BatchSubmitter } from '.'
import { TransactionSubmitter } from '../utils'
export class StateBatchSubmitter extends BatchSubmitter {
// TODO: Change this so that we calculate start = scc.totalElements() and end = ctc.totalElements()!
......@@ -23,6 +24,7 @@ export class StateBatchSubmitter extends BatchSubmitter {
protected syncing: boolean
protected ctcContract: Contract
private fraudSubmissionAddress: string
private transactionSubmitter: TransactionSubmitter
constructor(
signer: Signer,
......@@ -36,10 +38,7 @@ export class StateBatchSubmitter extends BatchSubmitter {
finalityConfirmations: number,
addressManagerAddress: string,
minBalanceEther: number,
minGasPriceInGwei: number,
maxGasPriceInGwei: number,
gasRetryIncrement: number,
gasThresholdInGwei: number,
transactionSubmitter: TransactionSubmitter,
blockOffset: number,
logger: Logger,
metrics: Metrics,
......@@ -57,15 +56,12 @@ export class StateBatchSubmitter extends BatchSubmitter {
finalityConfirmations,
addressManagerAddress,
minBalanceEther,
minGasPriceInGwei,
maxGasPriceInGwei,
gasRetryIncrement,
gasThresholdInGwei,
blockOffset,
logger,
metrics
)
this.fraudSubmissionAddress = fraudSubmissionAddress
this.transactionSubmitter = transactionSubmitter
}
/*****************************
......@@ -181,28 +177,16 @@ export class StateBatchSubmitter extends BatchSubmitter {
batch,
offsetStartsAtIndex
)
const contractFunction = async (gasPrice): Promise<TransactionReceipt> => {
this.logger.info('Submitting appendStateBatch transaction', {
gasPrice,
contractAddr: this.chainContract.address,
})
const txResponse = await this.signer.sendTransaction({
...tx,
gasPrice,
})
this.logger.info('Submitted appendStateBatch transaction', {
txHash: txResponse.hash,
from: txResponse.from,
})
this.logger.debug('appendStateBatch transaction data', {
data: txResponse.data,
})
return this.signer.provider.waitForTransaction(
txResponse.hash,
this.numConfirmations
const submitTransaction = (): Promise<TransactionReceipt> => {
return this.transactionSubmitter.submitTransaction(
tx,
this._makeHooks('appendStateBatch')
)
}
return this._submitAndLogTx(contractFunction, 'Submitted state root batch!')
return this._submitAndLogTx(
submitTransaction,
'Submitted state root batch!'
)
}
/*********************
......
......@@ -22,6 +22,7 @@ import {
} from '../transaction-chain-contract'
import { BlockRange, BatchSubmitter } from '.'
import { TransactionSubmitter } from '../utils'
export interface AutoFixBatchOptions {
fixDoublePlayedDeposits: boolean
......@@ -35,6 +36,8 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
protected syncing: boolean
private disableQueueBatchAppend: boolean
private autoFixBatchOptions: AutoFixBatchOptions
private transactionSubmitter: TransactionSubmitter
private gasThresholdInGwei: number
constructor(
signer: Signer,
......@@ -47,10 +50,8 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
resubmissionTimeout: number,
addressManagerAddress: string,
minBalanceEther: number,
minGasPriceInGwei: number,
maxGasPriceInGwei: number,
gasRetryIncrement: number,
gasThresholdInGwei: number,
transactionSubmitter: TransactionSubmitter,
blockOffset: number,
logger: Logger,
metrics: Metrics,
......@@ -73,16 +74,14 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
0, // Supply dummy value because it is not used.
addressManagerAddress,
minBalanceEther,
minGasPriceInGwei,
maxGasPriceInGwei,
gasRetryIncrement,
gasThresholdInGwei,
blockOffset,
logger,
metrics
)
this.disableQueueBatchAppend = disableQueueBatchAppend
this.autoFixBatchOptions = autoFixBatchOptions
this.gasThresholdInGwei = gasThresholdInGwei
this.transactionSubmitter = transactionSubmitter
}
/*****************************
......@@ -140,38 +139,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
)
if (!this.disableQueueBatchAppend) {
// Generate the transaction we will repeatedly submit
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 contractFunction = async (
gasPrice
): Promise<TransactionReceipt> => {
this.logger.info('Submitting appendQueueBatch transaction', {
gasPrice,
contractAddr: this.chainContract.address,
})
const txResponse = await this.chainContract.appendQueueBatch(
ethers.constants.MaxUint256, // Completely empty the queue by appending (up to) an enormous number of queue elements.
{
gasPrice,
}
)
this.logger.info('Submitted appendQueueBatch transaction', {
txHash: txResponse.hash,
from: txResponse.from,
})
this.logger.debug('appendQueueBatch transaction data', {
data: txResponse.data,
})
return this.signer.provider.waitForTransaction(
txResponse.hash,
this.numConfirmations
)
}
// Empty the queue with a huge `appendQueueBatch(..)` call
return this._submitAndLogTx(contractFunction, 'Cleared queue!')
return this.submitAppendQueueBatch()
}
}
this.logger.info('Syncing mode enabled but queue is empty. Skipping...')
......@@ -254,38 +222,43 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
l1tipHeight,
})
// Generate the transaction we will repeatedly submit
const tx = await this.chainContract.customPopulateTransaction.appendSequencerBatch(
batchParams
)
const contractFunction = async (gasPrice): Promise<TransactionReceipt> => {
this.logger.info('Submitting appendSequencerBatch transaction', {
gasPrice,
contractAddr: this.chainContract.address,
})
const txResponse = await this.signer.sendTransaction({
...tx,
gasPrice,
})
this.logger.info('Submitted appendSequencerBatch transaction', {
txHash: txResponse.hash,
from: txResponse.from,
})
this.logger.debug('appendSequencerBatch transaction data', {
data: txResponse.data,
})
return this.signer.provider.waitForTransaction(
txResponse.hash,
this.numConfirmations
)
}
return this._submitAndLogTx(contractFunction, 'Submitted batch!')
return this.submitAppendSequencerBatch(batchParams)
}
/*********************
* 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(
startBlock: number,
endBlock: number
......
......@@ -16,6 +16,11 @@ import {
STATE_BATCH_SUBMITTER_LOG_TAG,
TX_BATCH_SUBMITTER_LOG_TAG,
} from '..'
import {
TransactionSubmitter,
YnatmTransactionSubmitter,
ResubmissionConfig,
} from '../utils'
interface RequiredEnvVars {
// The HTTP provider URL for L1.
......@@ -349,6 +354,18 @@ export const run = async () => {
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(
sequencerSigner,
l2Provider,
......@@ -360,10 +377,8 @@ export const run = async () => {
requiredEnvVars.RESUBMISSION_TIMEOUT * 1_000,
requiredEnvVars.ADDRESS_MANAGER_ADDRESS,
requiredEnvVars.SAFE_MINIMUM_ETHER_BALANCE,
MIN_GAS_PRICE_IN_GWEI,
MAX_GAS_PRICE_IN_GWEI,
GAS_RETRY_INCREMENT,
GAS_THRESHOLD_IN_GWEI,
txBatchTxSubmitter,
BLOCK_OFFSET,
logger.child({ name: TX_BATCH_SUBMITTER_LOG_TAG }),
metrics,
......@@ -371,6 +386,12 @@ export const run = async () => {
autoFixBatchOptions
)
const stateBatchTxSubmitter: TransactionSubmitter =
new YnatmTransactionSubmitter(
proposerSigner,
resubmissionConfig,
requiredEnvVars.NUM_CONFIRMATIONS
)
const stateBatchSubmitter = new StateBatchSubmitter(
proposerSigner,
l2Provider,
......@@ -383,10 +404,7 @@ export const run = async () => {
requiredEnvVars.FINALITY_CONFIRMATIONS,
requiredEnvVars.ADDRESS_MANAGER_ADDRESS,
requiredEnvVars.SAFE_MINIMUM_ETHER_BALANCE,
MIN_GAS_PRICE_IN_GWEI,
MAX_GAS_PRICE_IN_GWEI,
GAS_RETRY_INCREMENT,
GAS_THRESHOLD_IN_GWEI,
stateBatchTxSubmitter,
BLOCK_OFFSET,
logger.child({ name: STATE_BATCH_SUBMITTER_LOG_TAG }),
metrics,
......
/* External Imports */
import { Contract, BigNumber, ethers } from 'ethers'
import { Contract, ethers } from 'ethers'
import {
TransactionResponse,
TransactionRequest,
} from '@ethersproject/abstract-provider'
import { JsonRpcProvider } from '@ethersproject/providers'
import { keccak256 } from 'ethers/lib/utils'
import {
AppendSequencerBatchParams,
BatchContext,
encodeAppendSequencerBatch,
encodeHex,
remove0x,
} from '@eth-optimism/core-utils'
......@@ -33,7 +31,6 @@ export class CanonicalTransactionChainContract extends Contract {
from: await this.signer.getAddress(),
data,
})
const value = 0
return {
nonce,
......@@ -55,7 +52,9 @@ export class CanonicalTransactionChainContract extends Contract {
* 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 (
OVM_CanonicalTransactionChain: Contract,
......@@ -70,18 +69,7 @@ const appendSequencerBatch = async (
}
const getEncodedCalldata = (batch: AppendSequencerBatchParams): string => {
const methodId = keccak256(
Buffer.from(APPEND_SEQUENCER_BATCH_METHOD_ID)
).slice(2, 10)
const methodId = APPEND_SEQUENCER_BATCH_METHOD_ID
const calldata = encodeAppendSequencerBatch(batch)
return '0x' + remove0x(methodId) + remove0x(calldata)
}
const encodeBatchContext = (context: BatchContext): string => {
return (
encodeHex(context.numSequencedTransactions, 6) +
encodeHex(context.numSubsequentQueueTransactions, 6) +
encodeHex(context.timestamp, 10) +
encodeHex(context.blockNumber, 10)
)
}
......@@ -5,7 +5,7 @@ import {
} from '@ethersproject/abstract-provider'
import * as ynatm from '@eth-optimism/ynatm'
interface ResubmissionConfig {
export interface ResubmissionConfig {
resubmissionTimeout: number
minGasPriceInGwei: number
maxGasPriceInGwei: number
......
......@@ -27,6 +27,8 @@ import {
TX_BATCH_SUBMITTER_LOG_TAG,
STATE_BATCH_SUBMITTER_LOG_TAG,
BatchSubmitter,
YnatmTransactionSubmitter,
ResubmissionConfig,
} from '../../src'
import {
......@@ -200,8 +202,19 @@ describe('BatchSubmitter', () => {
sinon.restore()
})
const createBatchSubmitter = (timeout: number): TransactionBatchSubmitter =>
new TransactionBatchSubmitter(
const createBatchSubmitter = (timeout: number): 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,
l2Provider as any,
MIN_TX_SIZE,
......@@ -212,15 +225,14 @@ describe('BatchSubmitter', () => {
100000,
AddressManager.address,
1,
MIN_GAS_PRICE_IN_GWEI,
MAX_GAS_PRICE_IN_GWEI,
GAS_RETRY_INCREMENT,
GAS_THRESHOLD_IN_GWEI,
txBatchTxSubmitter,
1,
new Logger({ name: TX_BATCH_SUBMITTER_LOG_TAG }),
testMetrics,
false
)
}
describe('TransactionBatchSubmitter', () => {
describe('submitNextBatch', () => {
......@@ -375,7 +387,7 @@ describe('BatchSubmitter', () => {
.callsFake(async () => lowGasPriceWei)
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
})
})
......@@ -417,6 +429,17 @@ describe('BatchSubmitter', () => {
// submit a batch of transactions to enable state batch submission
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(
sequencer,
l2Provider as any,
......@@ -429,10 +452,7 @@ describe('BatchSubmitter', () => {
0, // finalityConfirmations
AddressManager.address,
1,
MIN_GAS_PRICE_IN_GWEI,
MAX_GAS_PRICE_IN_GWEI,
GAS_RETRY_INCREMENT,
GAS_THRESHOLD_IN_GWEI,
stateBatchTxSubmitter,
1,
new Logger({ name: STATE_BATCH_SUBMITTER_LOG_TAG }),
testMetrics,
......@@ -473,54 +493,4 @@ describe('Batch Submitter with Ganache', () => {
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: 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
})
})
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