Commit ab5c1b89 authored by smartcontracts's avatar smartcontracts Committed by GitHub

feat(fd): event cache for Geth L1s (#3950)

Introduces a new event cache to the fault detector. Requires for running
the fault detector against Geth as the L1 node and with certain L1 node
providers.
parent 8b628a78
---
'@eth-optimism/fault-detector': minor
---
Includes a new event caching mechanism for running the fault detector against Geth.
import { Contract, ethers } from 'ethers'
import { Contract } from 'ethers'
/**
* Partial event interface, meant to reduce the size of the event cache to avoid
* 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: 0,
eventCache: new Map(),
}
}
return caches[address]
}
/**
* Updates the event cache for the SCC.
*
* @param scc The State Commitment Chain contract.
*/
export const updateStateBatchEventCache = async (
scc: Contract
): Promise<void> => {
const cache = getCache(scc.address)
let currentBlock = cache.highestBlock
const endingBlock = await scc.provider.getBlockNumber()
let step = endingBlock - currentBlock
let failures = 0
while (currentBlock < endingBlock) {
try {
const events = await scc.queryFilter(
scc.filters.StateBatchAppended(),
currentBlock,
currentBlock + step
)
for (const event of events) {
cache.eventCache[event.args._batchIndex.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 {
// 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++
} else {
failures = 0
}
// We've failed 3 times in a row, we're probably stuck.
if (failures >= 3) {
throw new Error('failed to update event cache')
}
}
}
// Update the highest block.
cache.highestBlock = endingBlock
}
/**
* Finds the Event that corresponds to a given state batch by index.
......@@ -10,20 +104,23 @@ import { Contract, ethers } from 'ethers'
export const findEventForStateBatch = async (
scc: Contract,
index: number
): Promise<ethers.Event> => {
const events = await scc.queryFilter(scc.filters.StateBatchAppended(index))
): Promise<PartialEvent> => {
const cache = getCache(scc.address)
// Only happens if the batch with the given index does not exist yet.
if (events.length === 0) {
throw new Error(`unable to find event for batch`)
// Try to find the event in cache first.
if (cache.eventCache[index]) {
return cache.eventCache[index]
}
// Should never happen.
if (events.length > 1) {
throw new Error(`found too many events for batch`)
// Update the event cache if we don't have the event.
await updateStateBatchEventCache(scc)
// Event better be in cache now!
if (cache.eventCache[index] === undefined) {
throw new Error(`unable to find event for batch ${index}`)
}
return events[0]
return cache.eventCache[index]
}
/**
......@@ -45,7 +142,7 @@ export const findFirstUnfinalizedStateBatchIndex = async (
while (lo !== hi) {
const mid = Math.floor((lo + hi) / 2)
const event = await findEventForStateBatch(scc, mid)
const block = await event.getBlock()
const block = await scc.provider.getBlock(event.blockNumber)
if (block.timestamp + fpw < latestBlock.timestamp) {
lo = mid + 1
......
......@@ -14,6 +14,8 @@ import { version } from '../package.json'
import {
findFirstUnfinalizedStateBatchIndex,
findEventForStateBatch,
updateStateBatchEventCache,
PartialEvent,
} from './helpers'
type Options = {
......@@ -95,6 +97,10 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
this.state.scc = this.state.messenger.contracts.l1.StateCommitmentChain
this.state.fpw = (await this.state.scc.FRAUD_PROOF_WINDOW()).toNumber()
// Populate the event cache.
this.logger.info(`warming event cache, this might take a while...`)
await updateStateBatchEventCache(this.state.scc)
// Figure out where to start syncing from.
if (this.options.startBatchIndex === -1) {
this.logger.info(`finding appropriate starting height`)
......@@ -165,7 +171,7 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
latestIndex: latestBatchIndex,
})
let event: ethers.Event
let event: PartialEvent
try {
event = await findEventForStateBatch(
this.state.scc,
......@@ -187,7 +193,9 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
let batchTransaction: Transaction
try {
batchTransaction = await event.getTransaction()
batchTransaction = await this.options.l1RpcProvider.getTransaction(
event.transactionHash
)
} catch (err) {
this.logger.error(`got error when connecting to node`, {
error: err,
......
......@@ -92,30 +92,6 @@ describe('helpers', () => {
).to.eventually.be.rejectedWith('unable to find event for batch')
})
})
describe('when more than one event exists', () => {
beforeEach(async () => {
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
0
)
await hre.ethers.provider.send('hardhat_setStorageAt', [
ChainStorageContainer.address,
'0x2',
hre.ethers.constants.HashZero,
])
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
0
)
})
it('should throw an error', async () => {
await expect(
findEventForStateBatch(StateCommitmentChain, 0)
).to.eventually.be.rejectedWith('found too many events for batch')
})
})
})
describe('findFirstUnfinalizedIndex', () => {
......
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