Commit fc64f875 authored by Kevin Chen's avatar Kevin Chen

fault-detector: Remove pre-bedrock support.

parent 52eb1a26
import { Contract, BigNumber } from 'ethers'
import { Logger } from '@eth-optimism/common-ts'
export interface OutputOracle<TSubmissionEventArgs> {
export interface OutputOracle {
contract: Contract
filter: any
getTotalElements: () => Promise<BigNumber>
getEventIndex: (args: TSubmissionEventArgs) => BigNumber
getEventIndex: (args: any) => BigNumber
}
/**
......@@ -54,8 +54,8 @@ const getCache = (
* @param contract Contract to update cache for.
* @param filter Event filter to use.
*/
export const updateOracleCache = async <TSubmissionEventArgs>(
oracle: OutputOracle<TSubmissionEventArgs>,
export const updateOracleCache = async (
oracle: OutputOracle,
logger?: Logger
): Promise<void> => {
const cache = getCache(oracle.contract.address)
......@@ -86,7 +86,7 @@ export const updateOracleCache = async <TSubmissionEventArgs>(
// Throw the events into the cache.
for (const event of events) {
cache.eventCache[
oracle.getEventIndex(event.args as TSubmissionEventArgs).toNumber()
oracle.getEventIndex(event.args).toNumber()
] = {
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
......@@ -135,8 +135,8 @@ export const updateOracleCache = async <TSubmissionEventArgs>(
* @param index State batch index to search for.
* @returns Event corresponding to the batch.
*/
export const findEventForStateBatch = async <TSubmissionEventArgs>(
oracle: OutputOracle<TSubmissionEventArgs>,
export const findEventForStateBatch = async (
oracle: OutputOracle,
index: number,
logger?: Logger
): Promise<PartialEvent> => {
......@@ -166,8 +166,8 @@ export const findEventForStateBatch = async <TSubmissionEventArgs>(
* @param oracle Output oracle contract.
* @returns Starting state root batch index.
*/
export const findFirstUnfinalizedStateBatchIndex = async <TSubmissionEventArgs>(
oracle: OutputOracle<TSubmissionEventArgs>,
export const findFirstUnfinalizedStateBatchIndex = async (
oracle: OutputOracle,
fpw: number,
logger?: Logger
): Promise<number> => {
......
......@@ -32,8 +32,9 @@ type Options = {
l1RpcProvider: Provider
l2RpcProvider: Provider
startBatchIndex: number
bedrock: boolean
optimismPortalAddress?: string
// Deprecated. Bedrock is the only version we support.
bedrock: boolean
stateCommitmentChainAddress?: string
}
......@@ -44,8 +45,8 @@ type Metrics = {
}
type State = {
fpw: number
oo: OutputOracle<any>
faultProofWindow: number
outputOracle: OutputOracle
messenger: CrossChainMessenger
currentBatchIndex: number
diverged: boolean
......@@ -76,22 +77,23 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
desc: 'Batch index to start checking from. For bedrock chains, this is the L2 height to start from',
public: true,
},
bedrock: {
validator: validators.bool,
default: true,
desc: 'Whether or not the service is running against a Bedrock chain',
public: true,
},
optimismPortalAddress: {
validator: validators.str,
default: ethers.constants.AddressZero,
desc: '[Custom Bedrock Chains] Deployed OptimismPortal contract address. Used to retrieve necessary info for ouput verification ',
desc: 'Deployed OptimismPortal contract address. Used to retrieve necessary info for ouput verification ',
public: true,
},
// Deprecated flags.
bedrock: {
validator: validators.bool,
default: true,
desc: '[Deprecated, must be set to true] Whether or not the service is running against a Bedrock chain',
public: true,
},
stateCommitmentChainAddress: {
validator: validators.str,
default: ethers.constants.AddressZero,
desc: '[Custom Legacy Chains] Deployed StateCommitmentChain contract address. Used to fetch necessary info for output verification.',
desc: '[Deprecated, must be set to 0x0] Deployed StateCommitmentChain contract address. Used to fetch necessary info for output verification.',
public: true,
},
},
......@@ -119,10 +121,9 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
* will fallback to the pre-defined set of addresses from options, otherwise aborting if unset.
*
* Required Contracts
* - Bedrock: OptimismPortal (used to also fetch L2OutputOracle address variable). This is the preferred address
* - OptimismPortal (used to also fetch L2OutputOracle address variable). This is the preferred address
* since in early versions of bedrock, OptimismPortal holds the FINALIZATION_WINDOW variable instead of L2OutputOracle.
* The retrieved L2OutputOracle address from OptimismPortal is used to query for output roots.
* - Legacy: StateCommitmentChain to query for output roots.
*
* @param l2ChainId op chain id
* @returns OEL1ContractsLike set of L1 contracts with only the required addresses set
......@@ -130,19 +131,17 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
async getOEL1Contracts(l2ChainId: number): Promise<OEL1ContractsLike> {
// CrossChainMessenger requires all address to be defined. Default to `AddressZero` to ignore unused contracts
let contracts: OEL1ContractsLike = {
OptimismPortal: ethers.constants.AddressZero,
L2OutputOracle: ethers.constants.AddressZero,
// Unused contracts
AddressManager: ethers.constants.AddressZero,
BondManager: ethers.constants.AddressZero,
CanonicalTransactionChain: ethers.constants.AddressZero,
L1CrossDomainMessenger: ethers.constants.AddressZero,
L1StandardBridge: ethers.constants.AddressZero,
StateCommitmentChain: ethers.constants.AddressZero,
CanonicalTransactionChain: ethers.constants.AddressZero,
BondManager: ethers.constants.AddressZero,
OptimismPortal: ethers.constants.AddressZero,
L2OutputOracle: ethers.constants.AddressZero,
}
const chainType = this.options.bedrock ? 'bedrock' : 'legacy'
this.logger.info(`Setting contracts for OP chain type: ${chainType}`)
const knownChainId = L2ChainID[l2ChainId] !== undefined
if (knownChainId) {
this.logger.info(`Recognized L2 chain id ${L2ChainID[l2ChainId]}`)
......@@ -152,42 +151,26 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
}
this.logger.info('checking contract address options...')
if (this.options.bedrock) {
const address = this.options.optimismPortalAddress
if (!knownChainId && address === ethers.constants.AddressZero) {
this.logger.error('OptimismPortal contract unspecified')
throw new Error(
'--optimismportalcontractaddress needs to set for custom bedrock op chains'
)
}
if (address !== ethers.constants.AddressZero) {
this.logger.info('set OptimismPortal contract override')
contracts.OptimismPortal = address
this.logger.info('fetching L2OutputOracle contract from OptimismPortal')
const opts = { address, signerOrProvider: this.options.l1RpcProvider }
const portalContract = getOEContract('OptimismPortal', l2ChainId, opts)
contracts.L2OutputOracle = await portalContract.L2_ORACLE()
}
const portalAddress = this.options.optimismPortalAddress
if (!knownChainId && portalAddress === ethers.constants.AddressZero) {
this.logger.error('OptimismPortal contract unspecified')
throw new Error(
'--optimismportalcontractaddress needs to set for custom bedrock op chains'
)
}
// ... for a known chain ids without an override, the L2OutputOracle will already
// be set via the hardcoded default
} else {
const address = this.options.stateCommitmentChainAddress
if (!knownChainId && address === ethers.constants.AddressZero) {
this.logger.error('StateCommitmentChain contract unspecified')
throw new Error(
'--statecommitmentchainaddress needs to set for custom legacy op chains'
)
}
if (portalAddress !== ethers.constants.AddressZero) {
this.logger.info('set OptimismPortal contract override')
contracts.OptimismPortal = portalAddress
if (address !== ethers.constants.AddressZero) {
this.logger.info('set StateCommitmentChain contract override')
contracts.StateCommitmentChain = address
}
this.logger.info('fetching L2OutputOracle contract from OptimismPortal')
const opts = { portalAddress, signerOrProvider: this.options.l1RpcProvider }
const portalContract = getOEContract('OptimismPortal', l2ChainId, opts)
contracts.L2OutputOracle = await portalContract.L2_ORACLE()
}
// ... for a known chain ids without an override, the L2OutputOracle will already
// be set via the hardcoded default
return contracts
}
......@@ -211,7 +194,7 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
l2SignerOrProvider: this.options.l2RpcProvider,
l1ChainId,
l2ChainId,
bedrock: this.options.bedrock,
bedrock: true,
contracts: { l1: await this.getOEL1Contracts(l2ChainId) },
})
......@@ -219,46 +202,36 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
this.state.diverged = false
// We use this a lot, a bit cleaner to pull out to the top level of the state object.
this.state.fpw = await this.state.messenger.getChallengePeriodSeconds()
this.logger.info(`fault proof window is ${this.state.fpw} seconds`)
if (this.options.bedrock) {
const oo = this.state.messenger.contracts.l1.L2OutputOracle
this.state.oo = {
contract: oo,
filter: oo.filters.OutputProposed(),
getTotalElements: async () => oo.nextOutputIndex(),
getEventIndex: (args) => args.l2OutputIndex,
}
} else {
const oo = this.state.messenger.contracts.l1.StateCommitmentChain
this.state.oo = {
contract: oo,
filter: oo.filters.StateBatchAppended(),
getTotalElements: async () => oo.getTotalBatches(),
getEventIndex: (args) => args._batchIndex,
}
this.state.faultProofWindow = await this.state.messenger.getChallengePeriodSeconds()
this.logger.info(`fault proof window is ${this.state.faultProofWindow} seconds`)
const outputOracle = this.state.messenger.contracts.l1.L2OutputOracle
this.state.outputOracle = {
contract: outputOracle,
filter: outputOracle.filters.OutputProposed(),
getTotalElements: async () => outputOracle.nextOutputIndex(),
getEventIndex: (args) => args.l2OutputIndex,
}
// Populate the event cache.
this.logger.info('warming event cache, this might take a while...')
await updateOracleCache(this.state.oo, this.logger)
await updateOracleCache(this.state.outputOracle, this.logger)
// Figure out where to start syncing from.
if (this.options.startBatchIndex === -1) {
this.logger.info('finding appropriate starting unfinalized batch')
const firstUnfinalized = await findFirstUnfinalizedStateBatchIndex(
this.state.oo,
this.state.fpw,
this.state.outputOracle,
this.state.faultProofWindow,
this.logger
)
// We may not have an unfinalized batches in the case where no batches have been submitted
// for the entire duration of the FPW. We generally do not expect this to happen on mainnet,
// but it happens often on testnets because the FPW is very short.
// for the entire duration of the FAULTPROOFWINDOW. We generally do not expect this to happen on mainnet,
// but it happens often on testnets because the FAULTPROOFWINDOW is very short.
if (firstUnfinalized === undefined) {
this.logger.info('no unfinalized batches found. skipping all batches.')
const totalBatches = await this.state.oo.getTotalElements()
const totalBatches = await this.state.outputOracle.getTotalElements()
this.state.currentBatchIndex = totalBatches.toNumber() - 1
} else {
this.state.currentBatchIndex = firstUnfinalized
......@@ -288,7 +261,7 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
let latestBatchIndex: number
try {
const totalBatches = await this.state.oo.getTotalElements()
const totalBatches = await this.state.outputOracle.getTotalElements()
latestBatchIndex = totalBatches.toNumber() - 1
} catch (err) {
this.logger.error('failed to query total # of batches', {
......@@ -322,7 +295,7 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
let event: PartialEvent
try {
event = await findEventForStateBatch(
this.state.oo,
this.state.outputOracle,
this.state.currentBatchIndex,
this.logger
)
......@@ -358,179 +331,89 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
return
}
if (this.options.bedrock) {
const outputBlockNumber = event.args.l2BlockNumber.toNumber()
if (latestBlock < outputBlockNumber) {
this.logger.info('L2 node is behind, waiting for sync...', {
l2BlockHeight: latestBlock,
outputBlock: outputBlockNumber,
})
return
}
const outputBlockNumber = event.args.l2BlockNumber.toNumber()
if (latestBlock < outputBlockNumber) {
this.logger.info('L2 node is behind, waiting for sync...', {
l2BlockHeight: latestBlock,
outputBlock: outputBlockNumber,
})
return
}
let outputBlock: any
try {
outputBlock = await (
this.options.l2RpcProvider as ethers.providers.JsonRpcProvider
).send('eth_getBlockByNumber', [
toRpcHexString(outputBlockNumber),
false,
])
} catch (err) {
this.logger.error('failed to fetch output block', {
error: err,
node: 'l2',
section: 'getBlock',
block: outputBlockNumber,
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l2',
section: 'getBlock',
})
await sleep(15000)
return
}
let outputBlock: any
try {
outputBlock = await (
this.options.l2RpcProvider as ethers.providers.JsonRpcProvider
).send('eth_getBlockByNumber', [
toRpcHexString(outputBlockNumber),
false,
])
} catch (err) {
this.logger.error('failed to fetch output block', {
error: err,
node: 'l2',
section: 'getBlock',
block: outputBlockNumber,
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l2',
section: 'getBlock',
})
await sleep(15000)
return
}
let messagePasserProofResponse: any
try {
messagePasserProofResponse = await (
this.options.l2RpcProvider as ethers.providers.JsonRpcProvider
).send('eth_getProof', [
this.state.messenger.contracts.l2.BedrockMessagePasser.address,
[],
toRpcHexString(outputBlockNumber),
])
} catch (err) {
this.logger.error('failed to fetch message passer proof', {
error: err,
node: 'l2',
section: 'getProof',
block: outputBlockNumber,
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l2',
section: 'getProof',
})
await sleep(15000)
return
}
let messagePasserProofResponse: any
try {
messagePasserProofResponse = await (
this.options.l2RpcProvider as ethers.providers.JsonRpcProvider
).send('eth_getProof', [
this.state.messenger.contracts.l2.BedrockMessagePasser.address,
[],
toRpcHexString(outputBlockNumber),
])
} catch (err) {
this.logger.error('failed to fetch message passer proof', {
error: err,
node: 'l2',
section: 'getProof',
block: outputBlockNumber,
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l2',
section: 'getProof',
})
await sleep(15000)
return
}
const outputRoot = ethers.utils.solidityKeccak256(
['uint256', 'bytes32', 'bytes32', 'bytes32'],
[
0,
outputBlock.stateRoot,
messagePasserProofResponse.storageHash,
outputBlock.hash,
]
)
const outputRoot = ethers.utils.solidityKeccak256(
['uint256', 'bytes32', 'bytes32', 'bytes32'],
[
0,
outputBlock.stateRoot,
messagePasserProofResponse.storageHash,
outputBlock.hash,
]
)
if (outputRoot !== event.args.outputRoot) {
this.state.diverged = true
this.metrics.isCurrentlyMismatched.set(1)
this.logger.error('state root mismatch', {
blockNumber: outputBlock.number,
expectedStateRoot: event.args.outputRoot,
actualStateRoot: outputRoot,
finalizationTime: dateformat(
new Date(
(ethers.BigNumber.from(outputBlock.timestamp).toNumber() +
this.state.fpw) *
1000
),
'mmmm dS, yyyy, h:MM:ss TT'
if (outputRoot !== event.args.outputRoot) {
this.state.diverged = true
this.metrics.isCurrentlyMismatched.set(1)
this.logger.error('state root mismatch', {
blockNumber: outputBlock.number,
expectedStateRoot: event.args.outputRoot,
actualStateRoot: outputRoot,
finalizationTime: dateformat(
new Date(
(ethers.BigNumber.from(outputBlock.timestamp).toNumber() +
this.state.faultProofWindow) *
1000
),
})
return
}
} else {
let batchTransaction: Transaction
try {
batchTransaction = await this.options.l1RpcProvider.getTransaction(
event.transactionHash
)
} catch (err) {
this.logger.error('failed to acquire batch transaction', {
error: err,
node: 'l1',
section: 'getTransaction',
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l1',
section: 'getTransaction',
})
await sleep(15000)
return
}
const [stateRoots] = this.state.oo.contract.interface.decodeFunctionData(
'appendStateBatch',
batchTransaction.data
)
const batchStart = event.args._prevTotalElements.toNumber() + 1
const batchSize = event.args._batchSize.toNumber()
const batchEnd = batchStart + batchSize
if (latestBlock < batchEnd) {
this.logger.info('L2 node is behind. waiting for sync...', {
batchBlockStart: batchStart,
batchBlockEnd: batchEnd,
l2BlockHeight: latestBlock,
})
return
}
// `getBlockRange` has a limit of 1000 blocks, so we have to break this request out into
// multiple requests of maximum 1000 blocks in the case that batchSize > 1000.
let blocks: any[] = []
for (let i = 0; i < batchSize; i += 1000) {
let newBlocks: any[]
try {
newBlocks = await (
this.options.l2RpcProvider as ethers.providers.JsonRpcProvider
).send('eth_getBlockRange', [
toRpcHexString(batchStart + i),
toRpcHexString(batchStart + i + Math.min(batchSize - i, 1000) - 1),
false,
])
} catch (err) {
this.logger.error('failed to query for blocks in batch', {
error: err,
node: 'l2',
section: 'getBlockRange',
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l2',
section: 'getBlockRange',
})
await sleep(15000)
return
}
blocks = blocks.concat(newBlocks)
}
for (const [i, stateRoot] of stateRoots.entries()) {
if (blocks[i].stateRoot !== stateRoot) {
this.state.diverged = true
this.metrics.isCurrentlyMismatched.set(1)
this.logger.error('state root mismatch', {
blockNumber: blocks[i].number,
expectedStateRoot: blocks[i].stateRoot,
actualStateRoot: stateRoot,
finalizationTime: dateformat(
new Date(
(ethers.BigNumber.from(blocks[i].timestamp).toNumber() +
this.state.fpw) *
1000
),
'mmmm dS, yyyy, h:MM:ss TT'
),
})
return
}
}
'mmmm dS, yyyy, h:MM:ss TT'
),
})
return
}
const elapsedMs = Date.now() - startMs
......
......@@ -29,7 +29,7 @@ describe('helpers', () => {
let AddressManager: Contract
let ChainStorageContainer: Contract
let StateCommitmentChain: Contract
let oracle: OutputOracle<any>
let oracle: OutputOracle
beforeEach(async () => {
// Set up fakes
FakeBondManager = await smock.fake(getContractInterface('BondManager'))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment