Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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