• Kelvin Fichter's avatar
    feat(fd): support Bedrock networks · b004d1ad
    Kelvin Fichter authored
    Updates the fault detector to support Bedrock networks. Bedrock support
    is slightly hacky because we still also need to support the legacy
    system until mainnet has been upgraded to Bedrock. We will want to
    update the fault detector again once mainnet is upgraded to simplify the
    service and remove the legacy components.
    b004d1ad
helpers.ts 4.72 KB
import { Contract, BigNumber } from 'ethers'

export interface OutputOracle<TSubmissionEventArgs> {
  contract: Contract
  filter: any
  getTotalElements: () => Promise<BigNumber>
  getEventIndex: (args: TSubmissionEventArgs) => BigNumber
}

/**
 * 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 a contract and event.
 *
 * @param contract Contract to update cache for.
 * @param filter Event filter to use.
 */
export const updateOracleCache = async <TSubmissionEventArgs>(
  oracle: OutputOracle<TSubmissionEventArgs>
): Promise<void> => {
  const cache = getCache(oracle.contract.address)
  let currentBlock = cache.highestBlock
  const endingBlock = await oracle.contract.provider.getBlockNumber()
  let step = endingBlock - currentBlock
  let failures = 0
  while (currentBlock < endingBlock) {
    try {
      const events = await oracle.contract.queryFilter(
        oracle.filter,
        currentBlock,
        currentBlock + step
      )

      // Throw the events into the cache.
      for (const event of events) {
        cache.eventCache[
          oracle.getEventIndex(event.args as TSubmissionEventArgs).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.
 *
 * @param oracle Output oracle contract
 * @param index State batch index to search for.
 * @returns Event corresponding to the batch.
 */
export const findEventForStateBatch = async <TSubmissionEventArgs>(
  oracle: OutputOracle<TSubmissionEventArgs>,
  index: number
): Promise<PartialEvent> => {
  const cache = getCache(oracle.contract.address)

  // Try to find the event in cache first.
  if (cache.eventCache[index]) {
    return cache.eventCache[index]
  }

  // Update the event cache if we don't have the event.
  await updateOracleCache(oracle)

  // Event better be in cache now!
  if (cache.eventCache[index] === undefined) {
    throw new Error(`unable to find event for batch ${index}`)
  }

  return cache.eventCache[index]
}

/**
 * Finds the first state batch index that has not yet passed the fault proof window.
 *
 * @param oracle Output oracle contract.
 * @returns Starting state root batch index.
 */
export const findFirstUnfinalizedStateBatchIndex = async <TSubmissionEventArgs>(
  oracle: OutputOracle<TSubmissionEventArgs>,
  fpw: number
): Promise<number> => {
  const latestBlock = await oracle.contract.provider.getBlock('latest')
  const totalBatches = (await oracle.getTotalElements()).toNumber()

  // Perform a binary search to find the next batch that will pass the challenge period.
  let lo = 0
  let hi = totalBatches
  while (lo !== hi) {
    const mid = Math.floor((lo + hi) / 2)
    const event = await findEventForStateBatch(oracle, mid)
    const block = await oracle.contract.provider.getBlock(event.blockNumber)

    if (block.timestamp + fpw < latestBlock.timestamp) {
      lo = mid + 1
    } else {
      hi = mid
    }
  }

  // Result will be zero if the chain is less than FPW seconds old. Only returns undefined in the
  // case that no batches have been submitted for an entire challenge period.
  if (lo === totalBatches) {
    return undefined
  } else {
    return lo
  }
}