helpers.ts 4.72 KB
Newer Older
1 2 3 4 5 6 7 8
import { Contract, BigNumber } from 'ethers'

export interface OutputOracle<TSubmissionEventArgs> {
  contract: Contract
  filter: any
  getTotalElements: () => Promise<BigNumber>
  getEventIndex: (args: TSubmissionEventArgs) => BigNumber
}
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50

/**
 * 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]
}

/**
51
 * Updates the event cache for a contract and event.
52
 *
53 54
 * @param contract Contract to update cache for.
 * @param filter Event filter to use.
55
 */
56 57
export const updateOracleCache = async <TSubmissionEventArgs>(
  oracle: OutputOracle<TSubmissionEventArgs>
58
): Promise<void> => {
59
  const cache = getCache(oracle.contract.address)
60
  let currentBlock = cache.highestBlock
61
  const endingBlock = await oracle.contract.provider.getBlockNumber()
62 63 64 65
  let step = endingBlock - currentBlock
  let failures = 0
  while (currentBlock < endingBlock) {
    try {
66 67
      const events = await oracle.contract.queryFilter(
        oracle.filter,
68 69 70
        currentBlock,
        currentBlock + step
      )
71 72

      // Throw the events into the cache.
73
      for (const event of events) {
74 75 76
        cache.eventCache[
          oracle.getEventIndex(event.args as TSubmissionEventArgs).toNumber()
        ] = {
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
          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
}
108 109 110 111

/**
 * Finds the Event that corresponds to a given state batch by index.
 *
112
 * @param oracle Output oracle contract
113 114 115
 * @param index State batch index to search for.
 * @returns Event corresponding to the batch.
 */
116 117
export const findEventForStateBatch = async <TSubmissionEventArgs>(
  oracle: OutputOracle<TSubmissionEventArgs>,
118
  index: number
119
): Promise<PartialEvent> => {
120
  const cache = getCache(oracle.contract.address)
121

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

127
  // Update the event cache if we don't have the event.
128
  await updateOracleCache(oracle)
129 130 131 132

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

135
  return cache.eventCache[index]
136 137 138 139 140
}

/**
 * Finds the first state batch index that has not yet passed the fault proof window.
 *
141
 * @param oracle Output oracle contract.
142 143
 * @returns Starting state root batch index.
 */
144 145 146
export const findFirstUnfinalizedStateBatchIndex = async <TSubmissionEventArgs>(
  oracle: OutputOracle<TSubmissionEventArgs>,
  fpw: number
147
): Promise<number> => {
148 149
  const latestBlock = await oracle.contract.provider.getBlock('latest')
  const totalBatches = (await oracle.getTotalElements()).toNumber()
150 151 152 153 154 155

  // 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)
156 157
    const event = await findEventForStateBatch(oracle, mid)
    const block = await oracle.contract.provider.getBlock(event.blockNumber)
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173

    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
  }
}