Skip to content

Commit

Permalink
feat(fd): event cache for Geth L1s (#3950)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
smartcontracts committed Dec 2, 2022
1 parent 8b628a7 commit ab5c1b8
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 37 deletions.
5 changes: 5 additions & 0 deletions .changeset/shaggy-donkeys-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@eth-optimism/fault-detector': minor
---

Includes a new event caching mechanism for running the fault detector against Geth.
119 changes: 108 additions & 11 deletions packages/fault-detector/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,98 @@
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.
Expand All @@ -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]
}

/**
Expand All @@ -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
Expand Down
12 changes: 10 additions & 2 deletions packages/fault-detector/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { version } from '../package.json'
import {
findFirstUnfinalizedStateBatchIndex,
findEventForStateBatch,
updateStateBatchEventCache,
PartialEvent,
} from './helpers'

type Options = {
Expand Down Expand Up @@ -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`)
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
24 changes: 0 additions & 24 deletions packages/fault-detector/test/helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down

0 comments on commit ab5c1b8

Please sign in to comment.