Commit f5bdbf00 authored by OptimismBot's avatar OptimismBot Committed by GitHub

Merge pull request #6117 from ethereum-optimism/k0622/sec-90-fd-rpc-tests

fault-detector: replace OutputProposed log cache with direct call to getL2Output.
parents 0bb1b923 83872ec2
import { Contract } from 'ethers' import { Contract } from 'ethers'
import { Logger } from '@eth-optimism/common-ts' import { Logger } from '@eth-optimism/common-ts'
import { BedrockOutputData } from '@eth-optimism/core-utils'
/** /**
* Partial event interface, meant to reduce the size of the event cache to avoid * Finds the BedrockOutputData that corresponds to a given output index.
* running out of memory.
*/
export interface PartialEvent {
blockNumber: number
transactionHash: string
args: any
}
// Event caching is necessary for the fault detector to work properly with Geth.
const caches: {
[contractAddress: string]: {
highestBlock: number
eventCache: Map<string, PartialEvent>
}
} = {}
/**
* Retrieves the cache for a given address.
*
* @param address Address to get cache for.
* @returns Address cache.
*/
const getCache = (
address: string
): {
highestBlock: number
eventCache: Map<string, PartialEvent>
} => {
if (!caches[address]) {
caches[address] = {
highestBlock: -1,
eventCache: new Map(),
}
}
return caches[address]
}
/**
* Updates the event cache for a contract and event.
*
* @param contract Contract to update cache for.
* @param filter Event filter to use.
*/
export const updateOracleCache = async (
oracle: Contract,
logger?: Logger
): Promise<void> => {
const cache = getCache(oracle.address)
const endBlock = await oracle.provider.getBlockNumber()
logger?.info('visiting uncached oracle events for range', {
node: 'l1',
cachedUntilBlock: cache.highestBlock,
latestBlock: endBlock,
})
let failures = []
let currentBlock = cache.highestBlock + 1
let step = endBlock - currentBlock
while (currentBlock < endBlock) {
try {
logger?.info('polling events for range', {
node: 'l1',
startBlock: currentBlock,
blockRangeSize: step,
})
const events = await oracle.queryFilter(
oracle.filters.OutputProposed(),
currentBlock,
currentBlock + step
)
// Throw the events into the cache.
for (const event of events) {
cache.eventCache[event.args.l2OutputIndex.toNumber()] = {
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
args: event.args,
}
}
// Update the current block and increase the step size for the next iteration.
currentBlock += step
step = Math.ceil(step * 2)
} catch (err) {
logger?.warn('error fetching events', {
err,
node: 'l1',
section: 'getLogs',
})
// Might happen if we're querying too large an event range.
step = Math.floor(step / 2)
// When the step gets down to zero, we're pretty much guaranteed that range size isn't the
// problem. If we get three failures like this in a row then we should just give up.
if (step === 0) {
failures.push(err)
} else {
failures = []
}
// We've failed 5 times in a row, we're probably stuck.
if (failures.length >= 5) {
logger?.fatal('unable to fetch oracle events', { errors: failures })
throw new Error('failed to update event cache')
}
}
}
// Update the highest block.
cache.highestBlock = endBlock
logger?.info('done caching oracle events')
}
/**
* Finds the Event that corresponds to a given state batch by index.
* *
* @param oracle Output oracle contract * @param oracle Output oracle contract
* @param index State batch index to search for. * @param index Output index to search for.
* @returns Event corresponding to the batch. * @returns BedrockOutputData corresponding to the output index.
*/ */
export const findEventForStateBatch = async ( export const findOutputForIndex = async (
oracle: Contract, oracle: Contract,
index: number, index: number,
logger?: Logger logger?: Logger
): Promise<PartialEvent> => { ): Promise<BedrockOutputData> => {
const cache = getCache(oracle.address) try {
const proposal = await oracle.getL2Output(index)
// Try to find the event in cache first. return {
if (cache.eventCache[index]) { outputRoot: proposal.outputRoot,
return cache.eventCache[index] l1Timestamp: proposal.timestamp.toNumber(),
} l2BlockNumber: proposal.l2BlockNumber.toNumber(),
l2OutputIndex: index,
// Update the event cache if we don't have the event. }
logger?.info('event not cached for index. warming cache...', { index }) } catch (err) {
await updateOracleCache(oracle, logger) logger?.fatal('error when calling L2OuputOracle.getL2Output', { errors: err })
throw new Error(`unable to find output for index ${index}`)
// Event better be in cache now!
if (cache.eventCache[index] === undefined) {
logger?.fatal('expected event for index!', { index })
throw new Error(`unable to find event for batch ${index}`)
} }
return cache.eventCache[index]
} }
/** /**
...@@ -170,10 +47,9 @@ export const findFirstUnfinalizedStateBatchIndex = async ( ...@@ -170,10 +47,9 @@ export const findFirstUnfinalizedStateBatchIndex = async (
let hi = totalBatches let hi = totalBatches
while (lo !== hi) { while (lo !== hi) {
const mid = Math.floor((lo + hi) / 2) const mid = Math.floor((lo + hi) / 2)
const event = await findEventForStateBatch(oracle, mid, logger) const outputData = await findOutputForIndex(oracle, mid, logger)
const block = await oracle.provider.getBlock(event.blockNumber)
if (block.timestamp + fpw < latestBlock.timestamp) { if (outputData.l1Timestamp + fpw < latestBlock.timestamp) {
lo = mid + 1 lo = mid + 1
} else { } else {
hi = mid hi = mid
......
...@@ -6,7 +6,7 @@ import { ...@@ -6,7 +6,7 @@ import {
validators, validators,
waitForProvider, waitForProvider,
} from '@eth-optimism/common-ts' } from '@eth-optimism/common-ts'
import { getChainId, sleep, toRpcHexString } from '@eth-optimism/core-utils' import { BedrockOutputData, getChainId, sleep, toRpcHexString } from '@eth-optimism/core-utils'
import { config } from 'dotenv' import { config } from 'dotenv'
import { import {
CONTRACT_ADDRESSES, CONTRACT_ADDRESSES,
...@@ -22,9 +22,7 @@ import dateformat from 'dateformat' ...@@ -22,9 +22,7 @@ import dateformat from 'dateformat'
import { version } from '../package.json' import { version } from '../package.json'
import { import {
findFirstUnfinalizedStateBatchIndex, findFirstUnfinalizedStateBatchIndex,
findEventForStateBatch, findOutputForIndex,
PartialEvent,
updateOracleCache,
} from './helpers' } from './helpers'
type Options = { type Options = {
...@@ -196,10 +194,6 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> { ...@@ -196,10 +194,6 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
this.state.outputOracle = this.state.messenger.contracts.l1.L2OutputOracle this.state.outputOracle = this.state.messenger.contracts.l1.L2OutputOracle
// Populate the event cache.
this.logger.info('warming event cache, this might take a while...')
await updateOracleCache(this.state.outputOracle, this.logger)
// Figure out where to start syncing from. // Figure out where to start syncing from.
if (this.options.startBatchIndex === -1) { if (this.options.startBatchIndex === -1) {
this.logger.info('finding appropriate starting unfinalized batch') this.logger.info('finding appropriate starting unfinalized batch')
...@@ -275,23 +269,23 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> { ...@@ -275,23 +269,23 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
latestBatchIndex, latestBatchIndex,
}) })
let event: PartialEvent let outputData: BedrockOutputData
try { try {
event = await findEventForStateBatch( outputData = await findOutputForIndex(
this.state.outputOracle, this.state.outputOracle,
this.state.currentBatchIndex, this.state.currentBatchIndex,
this.logger this.logger
) )
} catch (err) { } catch (err) {
this.logger.error('failed to fetch event associated with batch', { this.logger.error('failed to fetch output associated with batch', {
error: err, error: err,
node: 'l1', node: 'l1',
section: 'findEventForStateBatch', section: 'findOutputForIndex',
batchIndex: this.state.currentBatchIndex, batchIndex: this.state.currentBatchIndex,
}) })
this.metrics.nodeConnectionFailures.inc({ this.metrics.nodeConnectionFailures.inc({
layer: 'l1', layer: 'l1',
section: 'findEventForStateBatch', section: 'findOutputForIndex',
}) })
await sleep(15000) await sleep(15000)
return return
...@@ -314,7 +308,7 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> { ...@@ -314,7 +308,7 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
return return
} }
const outputBlockNumber = event.args.l2BlockNumber.toNumber() const outputBlockNumber = outputData.l2BlockNumber
if (latestBlock < outputBlockNumber) { if (latestBlock < outputBlockNumber) {
this.logger.info('L2 node is behind, waiting for sync...', { this.logger.info('L2 node is behind, waiting for sync...', {
l2BlockHeight: latestBlock, l2BlockHeight: latestBlock,
...@@ -377,18 +371,18 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> { ...@@ -377,18 +371,18 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
] ]
) )
if (outputRoot !== event.args.outputRoot) { if (outputRoot !== outputData.outputRoot) {
this.state.diverged = true this.state.diverged = true
this.metrics.isCurrentlyMismatched.set(1) this.metrics.isCurrentlyMismatched.set(1)
this.logger.error('state root mismatch', { this.logger.error('state root mismatch', {
blockNumber: outputBlock.number, blockNumber: outputBlock.number,
expectedStateRoot: event.args.outputRoot, expectedStateRoot: outputData.outputRoot,
actualStateRoot: outputRoot, actualStateRoot: outputRoot,
finalizationTime: dateformat( finalizationTime: dateformat(
new Date( new Date(
(ethers.BigNumber.from(outputBlock.timestamp).toNumber() + (ethers.BigNumber.from(outputBlock.timestamp).toNumber() +
this.state.faultProofWindow) * this.state.faultProofWindow) *
1000 1000
), ),
'mmmm dS, yyyy, h:MM:ss TT' 'mmmm dS, yyyy, h:MM:ss TT'
), ),
......
...@@ -6,7 +6,7 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' ...@@ -6,7 +6,7 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { expect } from './setup' import { expect } from './setup'
import { import {
findEventForStateBatch, findOutputForIndex,
findFirstUnfinalizedStateBatchIndex, findFirstUnfinalizedStateBatchIndex,
} from '../src' } from '../src'
...@@ -40,8 +40,8 @@ describe('helpers', () => { ...@@ -40,8 +40,8 @@ describe('helpers', () => {
) )
}) })
describe('findEventForStateBatch', () => { describe('findOutputForIndex', () => {
describe('when the event exists once', () => { describe('when the output exists once', () => {
beforeEach(async () => { beforeEach(async () => {
const latestBlock = await hre.ethers.provider.getBlock('latest') const latestBlock = await hre.ethers.provider.getBlock('latest')
const params = { const params = {
...@@ -60,18 +60,18 @@ describe('helpers', () => { ...@@ -60,18 +60,18 @@ describe('helpers', () => {
) )
}) })
it('should return the event', async () => { it('should return the output', async () => {
const event = await findEventForStateBatch(L2OutputOracle, 0) const output = await findOutputForIndex(L2OutputOracle, 0)
expect(event.args.l2OutputIndex).to.equal(0) expect(output.l2OutputIndex).to.equal(0)
}) })
}) })
describe('when the event does not exist', () => { describe('when the output does not exist', () => {
it('should throw an error', async () => { it('should throw an error', async () => {
await expect( await expect(
findEventForStateBatch(L2OutputOracle, 0) findOutputForIndex(L2OutputOracle, 0)
).to.eventually.be.rejectedWith('unable to find event for batch') ).to.eventually.be.rejectedWith('unable to find output for index')
}) })
}) })
}) })
...@@ -81,7 +81,6 @@ describe('helpers', () => { ...@@ -81,7 +81,6 @@ describe('helpers', () => {
beforeEach(async () => { beforeEach(async () => {
const latestBlock = await hre.ethers.provider.getBlock('latest') const latestBlock = await hre.ethers.provider.getBlock('latest')
const params = { const params = {
_outputRoot: utils.formatBytes32String('testhash'),
_l2BlockNumber: _l2BlockNumber:
deployConfig.l2OutputOracleStartingBlockNumber + deployConfig.l2OutputOracleStartingBlockNumber +
deployConfig.l2OutputOracleSubmissionInterval, deployConfig.l2OutputOracleSubmissionInterval,
...@@ -89,7 +88,7 @@ describe('helpers', () => { ...@@ -89,7 +88,7 @@ describe('helpers', () => {
_l1BlockNumber: latestBlock.number, _l1BlockNumber: latestBlock.number,
} }
await L2OutputOracle.proposeL2Output( await L2OutputOracle.proposeL2Output(
params._outputRoot, utils.formatBytes32String('outputRoot1'),
params._l2BlockNumber, params._l2BlockNumber,
params._l1BlockHash, params._l1BlockHash,
params._l1BlockNumber params._l1BlockNumber
...@@ -101,15 +100,15 @@ describe('helpers', () => { ...@@ -101,15 +100,15 @@ describe('helpers', () => {
]) ])
await L2OutputOracle.proposeL2Output( await L2OutputOracle.proposeL2Output(
params._outputRoot, utils.formatBytes32String('outputRoot2'),
params._l2BlockNumber + deployConfig.l2OutputOracleSubmissionInterval, params._l2BlockNumber + deployConfig.l2OutputOracleSubmissionInterval,
params._l1BlockHash, params._l1BlockHash,
params._l1BlockNumber params._l1BlockNumber
) )
await L2OutputOracle.proposeL2Output( await L2OutputOracle.proposeL2Output(
params._outputRoot, utils.formatBytes32String('outputRoot3'),
params._l2BlockNumber + params._l2BlockNumber +
deployConfig.l2OutputOracleSubmissionInterval * 2, deployConfig.l2OutputOracleSubmissionInterval * 2,
params._l1BlockHash, params._l1BlockHash,
params._l1BlockNumber params._l1BlockNumber
) )
...@@ -151,7 +150,7 @@ describe('helpers', () => { ...@@ -151,7 +150,7 @@ describe('helpers', () => {
await L2OutputOracle.proposeL2Output( await L2OutputOracle.proposeL2Output(
params._outputRoot, params._outputRoot,
params._l2BlockNumber + params._l2BlockNumber +
deployConfig.l2OutputOracleSubmissionInterval * 2, deployConfig.l2OutputOracleSubmissionInterval * 2,
params._l1BlockHash, params._l1BlockHash,
params._l1BlockNumber params._l1BlockNumber
) )
...@@ -193,7 +192,7 @@ describe('helpers', () => { ...@@ -193,7 +192,7 @@ describe('helpers', () => {
await L2OutputOracle.proposeL2Output( await L2OutputOracle.proposeL2Output(
params._outputRoot, params._outputRoot,
params._l2BlockNumber + params._l2BlockNumber +
deployConfig.l2OutputOracleSubmissionInterval * 2, deployConfig.l2OutputOracleSubmissionInterval * 2,
params._l1BlockHash, params._l1BlockHash,
params._l1BlockNumber params._l1BlockNumber
) )
......
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