Commit 8d98d5d4 authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into seb/batcher-watch-timeouts

parents e03b5073 407f97b9
---
'@eth-optimism/sdk': patch
---
Remove assert node builtin from sdk
---
'@eth-optimism/chain-mon': patch
---
Added withdrawal monitoring to identify proven withdrawals not included in the L2ToL1MessagePasser's sentMessages mapping
...@@ -20,7 +20,7 @@ import ( ...@@ -20,7 +20,7 @@ import (
// It also buffers batches that have been output because multiple batches can // It also buffers batches that have been output because multiple batches can
// be created at once. // be created at once.
// //
// This stage can be reset by clearing it's batch buffer. // This stage can be reset by clearing its batch buffer.
// This stage does not need to retain any references to L1 blocks. // This stage does not need to retain any references to L1 blocks.
type AttributesBuilder interface { type AttributesBuilder interface {
......
...@@ -517,7 +517,7 @@ func (eq *EngineQueue) ConfirmPayload(ctx context.Context) (out *eth.ExecutionPa ...@@ -517,7 +517,7 @@ func (eq *EngineQueue) ConfirmPayload(ctx context.Context) (out *eth.ExecutionPa
return nil, BlockInsertPrestateErr, fmt.Errorf("cannot complete payload building: not currently building a payload") return nil, BlockInsertPrestateErr, fmt.Errorf("cannot complete payload building: not currently building a payload")
} }
if eq.buildingOnto.Hash != eq.unsafeHead.Hash { // E.g. when safe-attributes consolidation fails, it will drop the existing work. if eq.buildingOnto.Hash != eq.unsafeHead.Hash { // E.g. when safe-attributes consolidation fails, it will drop the existing work.
eq.log.Warn("engine is building block that reorgs previous usafe head", "onto", eq.buildingOnto, "unsafe", eq.unsafeHead) eq.log.Warn("engine is building block that reorgs previous unsafe head", "onto", eq.buildingOnto, "unsafe", eq.unsafeHead)
} }
fc := eth.ForkchoiceState{ fc := eth.ForkchoiceState{
HeadBlockHash: common.Hash{}, // gets overridden HeadBlockHash: common.Hash{}, // gets overridden
......
...@@ -7,3 +7,14 @@ DRIPPIE_MON__RPC= ...@@ -7,3 +7,14 @@ DRIPPIE_MON__RPC=
# Address of the Drippie contract # Address of the Drippie contract
DRIPPIE_MON__DRIPPIE_ADDRESS= DRIPPIE_MON__DRIPPIE_ADDRESS=
###############################################################################
# ↓ wd-mon ↓ #
###############################################################################
# RPCs pointing to a base chain and ptimism chain
TWO_STEP_MONITOR__L1_RPC_PROVIDER=
TWO_STEP_MONITOR__L2_RPC_PROVIDER=
# The block number to start monitoring from
TWO_STEP_MONITOR__START_BLOCK_NUMBER=
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
], ],
"scripts": { "scripts": {
"start:drippie-mon": "ts-node ./src/drippie-mon/service.ts", "start:drippie-mon": "ts-node ./src/drippie-mon/service.ts",
"start:wd-mon": "ts-node ./src/wd-mon/service.ts",
"test:coverage": "echo 'No tests defined.'", "test:coverage": "echo 'No tests defined.'",
"build": "tsc -p ./tsconfig.json", "build": "tsc -p ./tsconfig.json",
"clean": "rimraf dist/ ./tsconfig.tsbuildinfo", "clean": "rimraf dist/ ./tsconfig.tsbuildinfo",
...@@ -35,7 +36,10 @@ ...@@ -35,7 +36,10 @@
"@eth-optimism/contracts-periphery": "1.0.7", "@eth-optimism/contracts-periphery": "1.0.7",
"@eth-optimism/core-utils": "0.12.0", "@eth-optimism/core-utils": "0.12.0",
"@eth-optimism/sdk": "1.10.1", "@eth-optimism/sdk": "1.10.1",
"ethers": "^5.7.0" "ethers": "^5.7.0",
"@types/dateformat": "^5.0.0",
"chai-as-promised": "^7.1.1",
"dateformat": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@ethersproject/abstract-provider": "^5.7.0", "@ethersproject/abstract-provider": "^5.7.0",
......
export * from './drippie-mon/service' export * from './drippie-mon/service'
export * from './wd-mon/service'
import {
BaseServiceV2,
StandardOptions,
ExpressRouter,
Gauge,
validators,
waitForProvider,
} from '@eth-optimism/common-ts'
import { CrossChainMessenger } from '@eth-optimism/sdk'
import { getChainId, sleep } from '@eth-optimism/core-utils'
import { Provider } from '@ethersproject/abstract-provider'
import { Event } from 'ethers'
import dateformat from 'dateformat'
import { version } from '../../package.json'
type Options = {
l1RpcProvider: Provider
l2RpcProvider: Provider
startBlockNumber: number
sleepTimeMs: number
}
type Metrics = {
withdrawalsValidated: Gauge
isDetectingForgeries: Gauge
nodeConnectionFailures: Gauge
}
type State = {
messenger: CrossChainMessenger
highestUncheckedBlockNumber: number
finalizationWindow: number
forgeryDetected: boolean
}
export class WithdrawalMonitor extends BaseServiceV2<Options, Metrics, State> {
constructor(options?: Partial<Options & StandardOptions>) {
super({
version,
name: 'two-step-monitor',
loop: true,
options: {
loopIntervalMs: 1000,
...options,
},
optionsSpec: {
l1RpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L1',
},
l2RpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L2',
},
startBlockNumber: {
validator: validators.num,
default: -1,
desc: 'L1 block number to start checking from',
public: true,
},
sleepTimeMs: {
validator: validators.num,
default: 15000,
desc: 'Time in ms to sleep when waiting for a node',
public: true,
},
},
metricsSpec: {
withdrawalsValidated: {
type: Gauge,
desc: 'Latest L1 Block (checked and known)',
labels: ['type'],
},
isDetectingForgeries: {
type: Gauge,
desc: '0 if state is ok. 1 or more if forged withdrawals are detected.',
},
nodeConnectionFailures: {
type: Gauge,
desc: 'Number of times node connection has failed',
labels: ['layer', 'section'],
},
},
})
}
async init(): Promise<void> {
// Connect to L1.
await waitForProvider(this.options.l1RpcProvider, {
logger: this.logger,
name: 'L1',
})
// Connect to L2.
await waitForProvider(this.options.l2RpcProvider, {
logger: this.logger,
name: 'L2',
})
this.state.messenger = new CrossChainMessenger({
l1SignerOrProvider: this.options.l1RpcProvider,
l2SignerOrProvider: this.options.l2RpcProvider,
l1ChainId: await getChainId(this.options.l1RpcProvider),
l2ChainId: await getChainId(this.options.l2RpcProvider),
})
// Not detected by default.
this.state.forgeryDetected = false
// For now we'll just start take it from the env or the tip of the chain
if (this.options.startBlockNumber === -1) {
this.state.highestUncheckedBlockNumber =
await this.options.l1RpcProvider.getBlockNumber()
} else {
this.state.highestUncheckedBlockNumber = this.options.startBlockNumber
}
this.logger.info(`starting L1 block height`, {
startBlockNumber: this.state.highestUncheckedBlockNumber,
})
}
// K8s healthcheck
async routes(router: ExpressRouter): Promise<void> {
router.get('/healthz', async (req, res) => {
return res.status(200).json({
ok: !this.state.forgeryDetected,
})
})
}
async main(): Promise<void> {
// Get current block number
let latestL1BlockNumber: number
try {
latestL1BlockNumber = await this.options.l1RpcProvider.getBlockNumber()
} catch (err) {
this.logger.error(`got error when connecting to node`, {
error: err,
node: 'l1',
section: 'getBlockNumber',
})
this.metrics.nodeConnectionFailures.inc({
chainId: this.state.messenger.l1ChainId,
section: 'getBlockNumber',
})
await sleep(this.options.sleepTimeMs)
return
}
// See if we have a new unchecked block
if (latestL1BlockNumber <= this.state.highestUncheckedBlockNumber) {
// The RPC provider is behind us, wait a bit
await sleep(this.options.sleepTimeMs)
return
}
this.logger.info(`checking recent blocks`, {
fromBlockNumber: this.state.highestUncheckedBlockNumber,
toBlockNumber: latestL1BlockNumber,
})
// Perform the check
let proofEvents: Event[]
try {
// The query includes events in the blockNumbers given as the last two arguments
proofEvents =
await this.state.messenger.contracts.l1.OptimismPortal.queryFilter(
this.state.messenger.contracts.l1.OptimismPortal.filters.WithdrawalProven(),
this.state.highestUncheckedBlockNumber,
latestL1BlockNumber
)
} catch (err) {
this.logger.error(`got error when connecting to node`, {
error: err,
node: 'l1',
section: 'querying for WithdrawalProven events',
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l1',
section: 'querying for WithdrawalProven events',
})
// connection error, wait then restart
await sleep(this.options.sleepTimeMs)
return
}
for (const proofEvent of proofEvents) {
const exists =
await this.state.messenger.contracts.l2.BedrockMessagePasser.sentMessages(
proofEvent.args.withdrawalHash
)
const provenAt = `${
(dateformat(
new Date(
(await this.options.l1RpcProvider.getBlock(proofEvent.blockHash))
.timestamp * 1000
)
),
'mmmm dS, yyyy, h:MM:ss TT',
true)
} UTC`
if (exists) {
this.metrics.withdrawalsValidated.inc()
this.logger.info(`valid withdrawal`, {
withdrawalHash: proofEvent.args.withdrawalHash,
provenAt,
})
} else {
this.logger.error(`withdrawalHash not seen on L2`, {
withdrawalHash: proofEvent.args.withdrawalHash,
provenAt,
})
this.state.forgeryDetected = true
this.metrics.isDetectingForgeries.set(1)
return
}
}
this.state.highestUncheckedBlockNumber = latestL1BlockNumber + 1
// If we got through the above without throwing an error, we should be fine to reset.
this.state.forgeryDetected = false
this.metrics.isDetectingForgeries.set(0)
}
}
if (require.main === module) {
const service = new WithdrawalMonitor()
service.run()
}
module.exports = { module.exports = {
extends: '../../.eslintrc.js', extends: '../../.eslintrc.js',
overrides: [
{
files: ['src/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
'assert',
'buffer',
'child_process',
'cluster',
'crypto',
'dgram',
'dns',
'domain',
'events',
'freelist',
'fs',
'http',
'https',
'module',
'net',
'os',
'path',
'punycode',
'querystring',
'readline',
'repl',
'smalloc',
'stream',
'string_decoder',
'sys',
'timers',
'tls',
'tracing',
'tty',
'url',
'util',
'vm',
'zlib',
],
},
},
],
} }
import assert from 'assert'
import { Provider, TransactionRequest } from '@ethersproject/abstract-provider' import { Provider, TransactionRequest } from '@ethersproject/abstract-provider'
import { serialize } from '@ethersproject/transactions' import { serialize } from '@ethersproject/transactions'
import { Contract, BigNumber } from 'ethers' import { Contract, BigNumber } from 'ethers'
import { predeploys, getContractInterface } from '@eth-optimism/contracts' import { predeploys, getContractInterface } from '@eth-optimism/contracts'
import cloneDeep from 'lodash/cloneDeep' import cloneDeep from 'lodash/cloneDeep'
import { assert } from './utils/assert'
import { L2Provider, ProviderLike, NumberLike } from './interfaces' import { L2Provider, ProviderLike, NumberLike } from './interfaces'
import { toProvider, toNumber, toBigNumber } from './utils' import { toProvider, toNumber, toBigNumber } from './utils'
......
export const assert = (condition: boolean, message: string): void => {
if (!condition) {
throw new Error(message)
}
}
import assert from 'assert'
import { import {
Provider, Provider,
TransactionReceipt, TransactionReceipt,
...@@ -8,6 +6,7 @@ import { ...@@ -8,6 +6,7 @@ import {
import { Signer } from '@ethersproject/abstract-signer' import { Signer } from '@ethersproject/abstract-signer'
import { ethers, BigNumber } from 'ethers' import { ethers, BigNumber } from 'ethers'
import { assert } from './assert'
import { import {
SignerOrProviderLike, SignerOrProviderLike,
ProviderLike, ProviderLike,
......
...@@ -22,7 +22,6 @@ The `L2StandardBridge` is a predeploy contract located at ...@@ -22,7 +22,6 @@ The `L2StandardBridge` is a predeploy contract located at
```solidity ```solidity
interface StandardBridge { interface StandardBridge {
event ERC20BridgeFinalized(address indexed localToken, address indexed remoteToken, address indexed from, address to, uint256 amount, bytes extraData);
event ERC20BridgeFinalized(address indexed localToken, address indexed remoteToken, address indexed from, address to, uint256 amount, bytes extraData); event ERC20BridgeFinalized(address indexed localToken, address indexed remoteToken, address indexed from, address to, uint256 amount, bytes extraData);
event ERC20BridgeInitiated(address indexed localToken, address indexed remoteToken, address indexed from, address to, uint256 amount, bytes extraData); event ERC20BridgeInitiated(address indexed localToken, address indexed remoteToken, address indexed from, address to, uint256 amount, bytes extraData);
event ETHBridgeFinalized(address indexed from, address indexed to, uint256 amount, bytes extraData); event ETHBridgeFinalized(address indexed from, address indexed to, uint256 amount, bytes extraData);
......
...@@ -710,6 +710,8 @@ enact the change, as linear rewinds of the tip of the chain may not be supported ...@@ -710,6 +710,8 @@ enact the change, as linear rewinds of the tip of the chain may not be supported
#### L1-sync: payload attributes processing #### L1-sync: payload attributes processing
[exec-engine-comm]: exec-engine.md#engine-api
If the safe and unsafe L2 heads are identical (whether because of failed consolidation or not), we send the L2 payload If the safe and unsafe L2 heads are identical (whether because of failed consolidation or not), we send the L2 payload
attributes to the execution engine to be constructed into a proper L2 block. attributes to the execution engine to be constructed into a proper L2 block.
This L2 block will then become both the new L2 safe and unsafe head. This L2 block will then become both the new L2 safe and unsafe head.
...@@ -728,6 +730,7 @@ The payload attributes are then processed with a sequence of: ...@@ -728,6 +730,7 @@ The payload attributes are then processed with a sequence of:
- `engine_forkchoiceUpdatedV1` with current forkchoice state of the stage, and the attributes to start block building. - `engine_forkchoiceUpdatedV1` with current forkchoice state of the stage, and the attributes to start block building.
- Non-deterministic sources, like the tx-pool, must be disabled to reconstruct the expected block. - Non-deterministic sources, like the tx-pool, must be disabled to reconstruct the expected block.
- `engine_getPayload` to retrieve the payload, by the payload-ID in the result of the previous step. - `engine_getPayload` to retrieve the payload, by the payload-ID in the result of the previous step.
- `engine_newPayload` to import the new payload into the execution engine.
- `engine_forkchoiceUpdatedV1` to make the new payload canonical, - `engine_forkchoiceUpdatedV1` to make the new payload canonical,
now with a change of both `safe` and `unsafe` fields to refer to the payload, and no payload attributes. now with a change of both `safe` and `unsafe` fields to refer to the payload, and no payload attributes.
......
...@@ -133,8 +133,8 @@ The Optimism Portal serves as both the entry and exit point to the Optimism L2. ...@@ -133,8 +133,8 @@ The Optimism Portal serves as both the entry and exit point to the Optimism L2.
the [DepositFeed](./deposits.md#deposit-contract) contract, and in addition provides the following interface for the [DepositFeed](./deposits.md#deposit-contract) contract, and in addition provides the following interface for
withdrawals: withdrawals:
- [WithdrawalTransaction type](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/contracts/libraries/Types.sol#L46-L56) - [`WithdrawalTransaction` type](https://github.com/ethereum-optimism/optimism/blob/6c6d142d7bb95faa11066aab5d8aed7187abfe38/packages/contracts-bedrock/contracts/libraries/Types.sol#L76-L83)
- [OutputRootProof type](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/contracts/libraries/Types.sol#L20-L29) - [`OutputRootProof` type](https://github.com/ethereum-optimism/optimism/blob/6c6d142d7bb95faa11066aab5d8aed7187abfe38/packages/contracts-bedrock/contracts/libraries/Types.sol#L33-L38)
```js ```js
interface OptimismPortal { interface OptimismPortal {
...@@ -184,7 +184,7 @@ These inputs must satisfy the following conditions: ...@@ -184,7 +184,7 @@ These inputs must satisfy the following conditions:
### Key Properties of Withdrawal Verification ### Key Properties of Withdrawal Verification
1. It should not be possible 'double spend' a withdrawal, ie. to relay a withdrawal on L1 which does not 1. It should not be possible to 'double spend' a withdrawal, ie. to relay a withdrawal on L1 which does not
correspond to a message initiated on L2. For reference, see [this writeup][polygon-dbl-spend] of a vulnerability correspond to a message initiated on L2. For reference, see [this writeup][polygon-dbl-spend] of a vulnerability
of this type found on Polygon. of this type found on Polygon.
......
...@@ -10580,9 +10580,9 @@ http-basic@^8.1.1: ...@@ -10580,9 +10580,9 @@ http-basic@^8.1.1:
parse-cache-control "^1.0.1" parse-cache-control "^1.0.1"
http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0:
version "4.1.0" version "4.1.1"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
http-errors@1.7.2: http-errors@1.7.2:
version "1.7.2" version "1.7.2"
......
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