github.com/ethereum-optimism/optimism@v1.7.2/packages/chain-mon/src/fault-mon/service.ts (about)

     1  import {
     2    BaseServiceV2,
     3    StandardOptions,
     4    ExpressRouter,
     5    Gauge,
     6    validators,
     7    waitForProvider,
     8  } from '@eth-optimism/common-ts'
     9  import {
    10    BedrockOutputData,
    11    getChainId,
    12    sleep,
    13    toRpcHexString,
    14  } from '@eth-optimism/core-utils'
    15  import { config } from 'dotenv'
    16  import {
    17    CONTRACT_ADDRESSES,
    18    CrossChainMessenger,
    19    getOEContract,
    20    L2ChainID,
    21    OEL1ContractsLike,
    22  } from '@eth-optimism/sdk'
    23  import { Provider } from '@ethersproject/abstract-provider'
    24  import { Contract, ethers } from 'ethers'
    25  import dateformat from 'dateformat'
    26  
    27  import { version } from '../../package.json'
    28  import { findFirstUnfinalizedOutputIndex, findOutputForIndex } from './helpers'
    29  
    30  type Options = {
    31    l1RpcProvider: Provider
    32    l2RpcProvider: Provider
    33    startOutputIndex: number
    34    optimismPortalAddress?: string
    35  }
    36  
    37  type Metrics = {
    38    highestOutputIndex: Gauge
    39    isCurrentlyMismatched: Gauge
    40    nodeConnectionFailures: Gauge
    41  }
    42  
    43  type State = {
    44    faultProofWindow: number
    45    outputOracle: Contract
    46    messenger: CrossChainMessenger
    47    currentOutputIndex: number
    48    diverged: boolean
    49  }
    50  
    51  export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
    52    constructor(options?: Partial<Options & StandardOptions>) {
    53      super({
    54        version,
    55        name: 'fault-detector',
    56        loop: true,
    57        options: {
    58          loopIntervalMs: 1000,
    59          ...options,
    60        },
    61        optionsSpec: {
    62          l1RpcProvider: {
    63            validator: validators.provider,
    64            desc: 'Provider for interacting with L1',
    65          },
    66          l2RpcProvider: {
    67            validator: validators.provider,
    68            desc: 'Provider for interacting with L2',
    69          },
    70          startOutputIndex: {
    71            validator: validators.num,
    72            default: -1,
    73            desc: 'The L2 height to start from',
    74            public: true,
    75          },
    76          optimismPortalAddress: {
    77            validator: validators.str,
    78            default: ethers.constants.AddressZero,
    79            desc: '[Custom OP Chains] Deployed OptimismPortal contract address. Used to retrieve necessary info for output verification ',
    80            public: true,
    81          },
    82        },
    83        metricsSpec: {
    84          highestOutputIndex: {
    85            type: Gauge,
    86            desc: 'Highest output indices (checked and known)',
    87            labels: ['type'],
    88          },
    89          isCurrentlyMismatched: {
    90            type: Gauge,
    91            desc: '0 if state is ok, 1 if state is mismatched',
    92          },
    93          nodeConnectionFailures: {
    94            type: Gauge,
    95            desc: 'Number of times node connection has failed',
    96            labels: ['layer', 'section'],
    97          },
    98        },
    99      })
   100    }
   101  
   102    /**
   103     * Provides the required set of addresses used by the fault detector. For recognized op-chains, this
   104     * will fallback to the pre-defined set of addresses from options, otherwise aborting if unset.
   105     *
   106     * Required Contracts
   107     * - OptimismPortal (used to also fetch L2OutputOracle address variable). This is the preferred address
   108     * since in early versions of bedrock, OptimismPortal holds the FINALIZATION_WINDOW variable instead of L2OutputOracle.
   109     * The retrieved L2OutputOracle address from OptimismPortal is used to query for output roots.
   110     *
   111     * @param l2ChainId op chain id
   112     * @returns OEL1ContractsLike set of L1 contracts with only the required addresses set
   113     */
   114    async getOEL1Contracts(l2ChainId: number): Promise<OEL1ContractsLike> {
   115      // CrossChainMessenger requires all address to be defined. Default to `AddressZero` to ignore unused contracts
   116      let contracts: OEL1ContractsLike = {
   117        OptimismPortal: ethers.constants.AddressZero,
   118        L2OutputOracle: ethers.constants.AddressZero,
   119        // Unused contracts
   120        AddressManager: ethers.constants.AddressZero,
   121        BondManager: ethers.constants.AddressZero,
   122        CanonicalTransactionChain: ethers.constants.AddressZero,
   123        L1CrossDomainMessenger: ethers.constants.AddressZero,
   124        L1StandardBridge: ethers.constants.AddressZero,
   125        StateCommitmentChain: ethers.constants.AddressZero,
   126      }
   127  
   128      const knownChainId = L2ChainID[l2ChainId] !== undefined
   129      if (knownChainId) {
   130        this.logger.info(`Recognized L2 chain id ${L2ChainID[l2ChainId]}`)
   131  
   132        // fallback to the predefined defaults for this chain id
   133        contracts = CONTRACT_ADDRESSES[l2ChainId].l1
   134      }
   135  
   136      this.logger.info('checking contract address options...')
   137      const portalAddress = this.options.optimismPortalAddress
   138      if (!knownChainId && portalAddress === ethers.constants.AddressZero) {
   139        this.logger.error('OptimismPortal contract unspecified')
   140        throw new Error(
   141          '--optimismportalcontractaddress needs to set for custom op chains'
   142        )
   143      }
   144  
   145      if (portalAddress !== ethers.constants.AddressZero) {
   146        this.logger.info('set OptimismPortal contract override')
   147        contracts.OptimismPortal = portalAddress
   148  
   149        this.logger.info('fetching L2OutputOracle contract from OptimismPortal')
   150        const portalContract = getOEContract('OptimismPortal', l2ChainId, {
   151          address: portalAddress,
   152          signerOrProvider: this.options.l1RpcProvider,
   153        })
   154        contracts.L2OutputOracle = await portalContract.L2_ORACLE()
   155      }
   156  
   157      // ... for a known chain ids without an override, the L2OutputOracle will already
   158      // be set via the hardcoded default
   159      return contracts
   160    }
   161  
   162    async init(): Promise<void> {
   163      // Connect to L1.
   164      await waitForProvider(this.options.l1RpcProvider, {
   165        logger: this.logger,
   166        name: 'L1',
   167      })
   168  
   169      // Connect to L2.
   170      await waitForProvider(this.options.l2RpcProvider, {
   171        logger: this.logger,
   172        name: 'L2',
   173      })
   174  
   175      const l1ChainId = await getChainId(this.options.l1RpcProvider)
   176      const l2ChainId = await getChainId(this.options.l2RpcProvider)
   177      this.state.messenger = new CrossChainMessenger({
   178        l1SignerOrProvider: this.options.l1RpcProvider,
   179        l2SignerOrProvider: this.options.l2RpcProvider,
   180        l1ChainId,
   181        l2ChainId,
   182        bedrock: true,
   183        contracts: { l1: await this.getOEL1Contracts(l2ChainId) },
   184      })
   185  
   186      // Not diverged by default.
   187      this.state.diverged = false
   188  
   189      // We use this a lot, a bit cleaner to pull out to the top level of the state object.
   190      this.state.faultProofWindow =
   191        await this.state.messenger.getChallengePeriodSeconds()
   192      this.logger.info(
   193        `fault proof window is ${this.state.faultProofWindow} seconds`
   194      )
   195  
   196      this.state.outputOracle = this.state.messenger.contracts.l1.L2OutputOracle
   197  
   198      // Figure out where to start syncing from.
   199      if (this.options.startOutputIndex === -1) {
   200        this.logger.info('finding appropriate starting unfinalized output')
   201        const firstUnfinalized = await findFirstUnfinalizedOutputIndex(
   202          this.state.outputOracle,
   203          this.state.faultProofWindow,
   204          this.logger
   205        )
   206  
   207        // We may not have an unfinalized outputs in the case where no outputs have been submitted
   208        // for the entire duration of the FAULTPROOFWINDOW. We generally do not expect this to happen on mainnet,
   209        // but it happens often on testnets because the FAULTPROOFWINDOW is very short.
   210        if (firstUnfinalized === undefined) {
   211          this.logger.info(
   212            'no unfinalized outputes found. skipping all outputes.'
   213          )
   214          const totalOutputes = await this.state.outputOracle.nextOutputIndex()
   215          this.state.currentOutputIndex = totalOutputes.toNumber() - 1
   216        } else {
   217          this.state.currentOutputIndex = firstUnfinalized
   218        }
   219      } else {
   220        this.state.currentOutputIndex = this.options.startOutputIndex
   221      }
   222  
   223      this.logger.info('starting output', {
   224        outputIndex: this.state.currentOutputIndex,
   225      })
   226  
   227      // Set the initial metrics.
   228      this.metrics.isCurrentlyMismatched.set(0)
   229    }
   230  
   231    async routes(router: ExpressRouter): Promise<void> {
   232      router.get('/status', async (req, res) => {
   233        return res.status(200).json({
   234          ok: !this.state.diverged,
   235        })
   236      })
   237    }
   238  
   239    async main(): Promise<void> {
   240      const startMs = Date.now()
   241  
   242      let latestOutputIndex: number
   243      try {
   244        const totalOutputes = await this.state.outputOracle.nextOutputIndex()
   245        latestOutputIndex = totalOutputes.toNumber() - 1
   246      } catch (err) {
   247        this.logger.error('failed to query total # of outputes', {
   248          error: err,
   249          node: 'l1',
   250          section: 'nextOutputIndex',
   251        })
   252        this.metrics.nodeConnectionFailures.inc({
   253          layer: 'l1',
   254          section: 'nextOutputIndex',
   255        })
   256        await sleep(15000)
   257        return
   258      }
   259  
   260      if (this.state.currentOutputIndex > latestOutputIndex) {
   261        this.logger.info('output index is ahead of the oracle. waiting...', {
   262          outputIndex: this.state.currentOutputIndex,
   263          latestOutputIndex,
   264        })
   265        await sleep(15000)
   266        return
   267      }
   268  
   269      this.metrics.highestOutputIndex.set({ type: 'known' }, latestOutputIndex)
   270      this.logger.info('checking output', {
   271        outputIndex: this.state.currentOutputIndex,
   272        latestOutputIndex,
   273      })
   274  
   275      let outputData: BedrockOutputData
   276      try {
   277        outputData = await findOutputForIndex(
   278          this.state.outputOracle,
   279          this.state.currentOutputIndex,
   280          this.logger
   281        )
   282      } catch (err) {
   283        this.logger.error('failed to fetch output associated with output', {
   284          error: err,
   285          node: 'l1',
   286          section: 'findOutputForIndex',
   287          outputIndex: this.state.currentOutputIndex,
   288        })
   289        this.metrics.nodeConnectionFailures.inc({
   290          layer: 'l1',
   291          section: 'findOutputForIndex',
   292        })
   293        await sleep(15000)
   294        return
   295      }
   296  
   297      let latestBlock: number
   298      try {
   299        latestBlock = await this.options.l2RpcProvider.getBlockNumber()
   300      } catch (err) {
   301        this.logger.error('failed to query L2 block height', {
   302          error: err,
   303          node: 'l2',
   304          section: 'getBlockNumber',
   305        })
   306        this.metrics.nodeConnectionFailures.inc({
   307          layer: 'l2',
   308          section: 'getBlockNumber',
   309        })
   310        await sleep(15000)
   311        return
   312      }
   313  
   314      const outputBlockNumber = outputData.l2BlockNumber
   315      if (latestBlock < outputBlockNumber) {
   316        this.logger.info('L2 node is behind, waiting for sync...', {
   317          l2BlockHeight: latestBlock,
   318          outputBlock: outputBlockNumber,
   319        })
   320        return
   321      }
   322  
   323      let outputBlock: any
   324      try {
   325        outputBlock = await (
   326          this.options.l2RpcProvider as ethers.providers.JsonRpcProvider
   327        ).send('eth_getBlockByNumber', [toRpcHexString(outputBlockNumber), false])
   328      } catch (err) {
   329        this.logger.error('failed to fetch output block', {
   330          error: err,
   331          node: 'l2',
   332          section: 'getBlock',
   333          block: outputBlockNumber,
   334        })
   335        this.metrics.nodeConnectionFailures.inc({
   336          layer: 'l2',
   337          section: 'getBlock',
   338        })
   339        await sleep(15000)
   340        return
   341      }
   342  
   343      let messagePasserProofResponse: any
   344      try {
   345        messagePasserProofResponse = await (
   346          this.options.l2RpcProvider as ethers.providers.JsonRpcProvider
   347        ).send('eth_getProof', [
   348          this.state.messenger.contracts.l2.BedrockMessagePasser.address,
   349          [],
   350          toRpcHexString(outputBlockNumber),
   351        ])
   352      } catch (err) {
   353        this.logger.error('failed to fetch message passer proof', {
   354          error: err,
   355          node: 'l2',
   356          section: 'getProof',
   357          block: outputBlockNumber,
   358        })
   359        this.metrics.nodeConnectionFailures.inc({
   360          layer: 'l2',
   361          section: 'getProof',
   362        })
   363        await sleep(15000)
   364        return
   365      }
   366  
   367      const outputRoot = ethers.utils.solidityKeccak256(
   368        ['uint256', 'bytes32', 'bytes32', 'bytes32'],
   369        [
   370          0,
   371          outputBlock.stateRoot,
   372          messagePasserProofResponse.storageHash,
   373          outputBlock.hash,
   374        ]
   375      )
   376  
   377      if (outputRoot !== outputData.outputRoot) {
   378        this.state.diverged = true
   379        this.metrics.isCurrentlyMismatched.set(1)
   380        this.logger.error('state root mismatch', {
   381          blockNumber: outputBlock.number,
   382          expectedStateRoot: outputData.outputRoot,
   383          actualStateRoot: outputRoot,
   384          finalizationTime: dateformat(
   385            new Date(
   386              (ethers.BigNumber.from(outputBlock.timestamp).toNumber() +
   387                this.state.faultProofWindow) *
   388                1000
   389            ),
   390            'mmmm dS, yyyy, h:MM:ss TT'
   391          ),
   392        })
   393        return
   394      }
   395  
   396      const elapsedMs = Date.now() - startMs
   397  
   398      // Mark the current output index as checked
   399      this.logger.info('checked output ok', {
   400        outputIndex: this.state.currentOutputIndex,
   401        timeMs: elapsedMs,
   402      })
   403      this.metrics.highestOutputIndex.set(
   404        { type: 'checked' },
   405        this.state.currentOutputIndex
   406      )
   407  
   408      // If we got through the above without throwing an error, we should be
   409      // fine to reset and move onto the next output
   410      this.state.diverged = false
   411      this.state.currentOutputIndex++
   412      this.metrics.isCurrentlyMismatched.set(0)
   413    }
   414  }
   415  
   416  if (require.main === module) {
   417    config()
   418    const service = new FaultDetector()
   419    service.run()
   420  }