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 }