Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
N
nebula
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
exchain
nebula
Commits
1ed50c44
Unverified
Commit
1ed50c44
authored
Feb 04, 2024
by
smartcontracts
Committed by
GitHub
Feb 04, 2024
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: update wd-mon to work for OptimismPortal2 (#9334)
parent
31845fd2
Changes
6
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
179 additions
and
112 deletions
+179
-112
dry-bobcats-obey.md
.changeset/dry-bobcats-obey.md
+5
-0
ninety-pugs-clap.md
.changeset/ninety-pugs-clap.md
+5
-0
constants.ts
packages/chain-mon/src/wd-mon/constants.ts
+21
-0
helpers.ts
packages/chain-mon/src/wd-mon/helpers.ts
+0
-41
service.ts
packages/chain-mon/src/wd-mon/service.ts
+139
-71
validators.ts
packages/common-ts/src/base-service/validators.ts
+9
-0
No files found.
.changeset/dry-bobcats-obey.md
0 → 100644
View file @
1ed50c44
---
'
@eth-optimism/common-ts'
:
patch
---
Adds a new validator for address types.
.changeset/ninety-pugs-clap.md
0 → 100644
View file @
1ed50c44
---
'
@eth-optimism/chain-mon'
:
minor
---
Updates wd-mon inside chain-mon to support FPAC.
packages/chain-mon/src/wd-mon/constants.ts
0 → 100644
View file @
1ed50c44
import
{
L2ChainID
}
from
'
@eth-optimism/sdk
'
// TODO: Consider moving to `@eth-optimism/constants` and generating from superchain registry.
// @see https://github.com/ethereum-optimism/optimism/pull/9041
/**
* Mapping of L2ChainIDs to the L1 block numbers where the wd-mon service should start looking for
* withdrawals by default. L1 block numbers here are based on the block number in which the
* OptimismPortal proxy contract was deployed to L1.
*/
export
const
DEFAULT_STARTING_BLOCK_NUMBERS
:
{
[
ChainID
in
L2ChainID
]?:
number
}
=
{
[
L2ChainID
.
OPTIMISM
]:
17365802
as
const
,
[
L2ChainID
.
OPTIMISM_GOERLI
]:
8299684
as
const
,
[
L2ChainID
.
OPTIMISM_SEPOLIA
]:
4071248
as
const
,
[
L2ChainID
.
BASE_MAINNET
]:
17482143
as
const
,
[
L2ChainID
.
BASE_GOERLI
]:
8411116
as
const
,
[
L2ChainID
.
BASE_SEPOLIA
]:
4370901
as
const
,
[
L2ChainID
.
ZORA_MAINNET
]:
17473938
as
const
,
}
packages/chain-mon/src/wd-mon/helpers.ts
deleted
100644 → 0
View file @
31845fd2
import
{
Provider
}
from
'
@ethersproject/abstract-provider
'
import
{
Logger
}
from
'
@eth-optimism/common-ts
'
/**
* Finds
*
* @param
* @param
* @param
* @returns
*/
export
const
getLastFinalizedBlock
=
async
(
l1RpcProvider
:
Provider
,
faultProofWindow
:
number
,
logger
:
Logger
):
Promise
<
number
>
=>
{
let
guessWindowStartBlock
try
{
const
l1Block
=
await
l1RpcProvider
.
getBlock
(
'
latest
'
)
// The time corresponding to the start of the FPW, based on the current block.
const
windowStartTime
=
l1Block
.
timestamp
-
faultProofWindow
// Use the FPW to find the block number that is the start of the FPW.
guessWindowStartBlock
=
l1Block
.
number
-
faultProofWindow
/
12
let
block
=
await
l1RpcProvider
.
getBlock
(
guessWindowStartBlock
)
while
(
block
.
timestamp
>
windowStartTime
)
{
guessWindowStartBlock
--
block
=
await
l1RpcProvider
.
getBlock
(
guessWindowStartBlock
)
}
return
block
.
number
}
catch
(
err
)
{
logger
.
fatal
(
'
error when calling querying for block
'
,
{
errors
:
err
,
})
throw
new
Error
(
`unable to find block number
${
guessWindowStartBlock
||
'
latest
'
}
`
)
}
}
packages/chain-mon/src/wd-mon/service.ts
View file @
1ed50c44
...
@@ -6,30 +6,35 @@ import {
...
@@ -6,30 +6,35 @@ import {
validators
,
validators
,
waitForProvider
,
waitForProvider
,
}
from
'
@eth-optimism/common-ts
'
}
from
'
@eth-optimism/common-ts
'
import
{
CrossChainMessenger
}
from
'
@eth-optimism/sdk
'
import
{
getOEContract
,
DEFAULT_L2_CONTRACT_ADDRESSES
}
from
'
@eth-optimism/sdk
'
import
{
getChainId
,
sleep
}
from
'
@eth-optimism/core-utils
'
import
{
getChainId
,
sleep
}
from
'
@eth-optimism/core-utils
'
import
{
Provider
}
from
'
@ethersproject/abstract-provider
'
import
{
Provider
}
from
'
@ethersproject/abstract-provider
'
import
{
Event
}
from
'
ethers
'
import
{
ethers
}
from
'
ethers
'
import
dateformat
from
'
dateformat
'
import
dateformat
from
'
dateformat
'
import
{
version
}
from
'
../../package.json
'
import
{
version
}
from
'
../../package.json
'
import
{
getLastFinalizedBlock
as
getLastFinalizedBlock
}
from
'
./helper
s
'
import
{
DEFAULT_STARTING_BLOCK_NUMBERS
}
from
'
./constant
s
'
type
Options
=
{
type
Options
=
{
l1RpcProvider
:
Provider
l1RpcProvider
:
Provider
l2RpcProvider
:
Provider
l2RpcProvider
:
Provider
optimismPortalAddress
:
string
l2ToL1MessagePasserAddress
:
string
startBlockNumber
:
number
startBlockNumber
:
number
eventBlockRange
:
number
sleepTimeMs
:
number
sleepTimeMs
:
number
}
}
type
Metrics
=
{
type
Metrics
=
{
highestBlockNumber
:
Gauge
withdrawalsValidated
:
Gauge
withdrawalsValidated
:
Gauge
isDetectingForgeries
:
Gauge
isDetectingForgeries
:
Gauge
nodeConnectionFailures
:
Gauge
nodeConnectionFailures
:
Gauge
}
}
type
State
=
{
type
State
=
{
messenger
:
CrossChainMessenger
portal
:
ethers
.
Contract
messenger
:
ethers
.
Contract
highestUncheckedBlockNumber
:
number
highestUncheckedBlockNumber
:
number
faultProofWindow
:
number
faultProofWindow
:
number
forgeryDetected
:
boolean
forgeryDetected
:
boolean
...
@@ -54,12 +59,30 @@ export class WithdrawalMonitor extends BaseServiceV2<Options, Metrics, State> {
...
@@ -54,12 +59,30 @@ export class WithdrawalMonitor extends BaseServiceV2<Options, Metrics, State> {
validator
:
validators
.
provider
,
validator
:
validators
.
provider
,
desc
:
'
Provider for interacting with L2
'
,
desc
:
'
Provider for interacting with L2
'
,
},
},
optimismPortalAddress
:
{
validator
:
validators
.
address
,
default
:
null
,
desc
:
'
Address of the OptimismPortal proxy contract on L1
'
,
public
:
true
,
},
l2ToL1MessagePasserAddress
:
{
validator
:
validators
.
address
,
default
:
DEFAULT_L2_CONTRACT_ADDRESSES
.
BedrockMessagePasser
as
string
,
desc
:
'
Address of the L2ToL1MessagePasser contract on L2
'
,
public
:
true
,
},
startBlockNumber
:
{
startBlockNumber
:
{
validator
:
validators
.
num
,
validator
:
validators
.
num
,
default
:
-
1
,
default
:
-
1
,
desc
:
'
L1 block number to start checking from
'
,
desc
:
'
L1 block number to start checking from
'
,
public
:
true
,
public
:
true
,
},
},
eventBlockRange
:
{
validator
:
validators
.
num
,
default
:
2000
,
desc
:
'
Number of blocks to query for events over per loop
'
,
public
:
true
,
},
sleepTimeMs
:
{
sleepTimeMs
:
{
validator
:
validators
.
num
,
validator
:
validators
.
num
,
default
:
15000
,
default
:
15000
,
...
@@ -68,6 +91,11 @@ export class WithdrawalMonitor extends BaseServiceV2<Options, Metrics, State> {
...
@@ -68,6 +91,11 @@ export class WithdrawalMonitor extends BaseServiceV2<Options, Metrics, State> {
},
},
},
},
metricsSpec
:
{
metricsSpec
:
{
highestBlockNumber
:
{
type
:
Gauge
,
desc
:
'
Highest block number (checked and known)
'
,
labels
:
[
'
type
'
],
},
withdrawalsValidated
:
{
withdrawalsValidated
:
{
type
:
Gauge
,
type
:
Gauge
,
desc
:
'
Latest L1 Block (checked and known)
'
,
desc
:
'
Latest L1 Block (checked and known)
'
,
...
@@ -99,38 +127,41 @@ export class WithdrawalMonitor extends BaseServiceV2<Options, Metrics, State> {
...
@@ -99,38 +127,41 @@ export class WithdrawalMonitor extends BaseServiceV2<Options, Metrics, State> {
name
:
'
L2
'
,
name
:
'
L2
'
,
})
})
this
.
state
.
messenger
=
new
CrossChainMessenger
({
// Need L2 chain ID to resolve contract addresses.
l1SignerOrProvider
:
this
.
options
.
l1RpcProvider
,
const
l2ChainId
=
await
getChainId
(
this
.
options
.
l2RpcProvider
)
l2SignerOrProvider
:
this
.
options
.
l2RpcProvider
,
l1ChainId
:
await
getChainId
(
this
.
options
.
l1RpcProvider
),
l2ChainId
:
await
getChainId
(
this
.
options
.
l2RpcProvider
),
bedrock
:
true
,
})
// Not detected by default.
// Create the OptimismPortal contract instance. If the optimismPortal option is not provided
this
.
state
.
forgeryDetected
=
false
// then the SDK will attempt to resolve the address automatically based on the L2 chain ID. If
// the SDK isn't aware of the L2 chain ID then it will throw an error that makes it clear the
// user needs to provide this value explicitly.
this
.
state
.
portal
=
getOEContract
(
'
OptimismPortal
'
,
l2ChainId
,
{
signerOrProvider
:
this
.
options
.
l1RpcProvider
,
address
:
this
.
options
.
optimismPortalAddress
,
})
this
.
state
.
faultProofWindow
=
// Create the L2ToL1MessagePasser contract instance. If the l2ToL1MessagePasser option is not
await
this
.
state
.
messenger
.
getChallengePeriodSeconds
()
// provided then we'll use the default address which typically should be correct. It's very
this
.
logger
.
info
(
// unlikely that any user would change this address so this should work in 99% of cases. If we
`fault proof window is
${
this
.
state
.
faultProofWindow
}
seconds`
// really wanted to be extra safe we could do some sanity checks to make sure the contract has
)
// the interface we need but doesn't seem important for now.
this
.
state
.
messenger
=
getOEContract
(
'
L2ToL1MessagePasser
'
,
l2ChainId
,
{
signerOrProvider
:
this
.
options
.
l2RpcProvider
,
address
:
this
.
options
.
l2ToL1MessagePasserAddress
,
})
// Set the start block number.
// Previous versions of wd-mon would try to pick the starting block number automatically but
if
(
this
.
options
.
startBlockNumber
===
-
1
)
{
// this had the possibility of missing certain withdrawals if the service was restarted at the
// We default to starting from the last finalized block.
// wrong time. Given the added complexity of finding a starting point automatically after FPAC,
this
.
state
.
highestUncheckedBlockNumber
=
await
getLastFinalizedBlock
(
// it's much easier to simply start a fixed block number than trying to do something fancy. Use
this
.
options
.
l1RpcProvider
,
// the default configured in this service or use zero if no default is defined.
this
.
state
.
faultProofWindow
,
this
.
logger
)
}
else
{
this
.
state
.
highestUncheckedBlockNumber
=
this
.
options
.
startBlockNumber
this
.
state
.
highestUncheckedBlockNumber
=
this
.
options
.
startBlockNumber
if
(
this
.
options
.
startBlockNumber
===
-
1
)
{
this
.
state
.
highestUncheckedBlockNumber
=
DEFAULT_STARTING_BLOCK_NUMBERS
[
l2ChainId
]
||
0
}
}
this
.
logger
.
info
(
`starting L1 block height`
,
{
// Default state is that forgeries have not been detected.
startBlockNumber
:
this
.
state
.
highestUncheckedBlockNumber
,
this
.
state
.
forgeryDetected
=
false
})
}
}
// K8s healthcheck
// K8s healthcheck
...
@@ -143,94 +174,131 @@ export class WithdrawalMonitor extends BaseServiceV2<Options, Metrics, State> {
...
@@ -143,94 +174,131 @@ export class WithdrawalMonitor extends BaseServiceV2<Options, Metrics, State> {
}
}
async
main
():
Promise
<
void
>
{
async
main
():
Promise
<
void
>
{
// Get
current block number
// Get
the latest L1 block number.
let
latestL1BlockNumber
:
number
let
latestL1BlockNumber
:
number
try
{
try
{
latestL1BlockNumber
=
await
this
.
options
.
l1RpcProvider
.
getBlockNumber
()
latestL1BlockNumber
=
await
this
.
options
.
l1RpcProvider
.
getBlockNumber
()
}
catch
(
err
)
{
}
catch
(
err
)
{
// Log the issue so we can debug it.
this
.
logger
.
error
(
`got error when connecting to node`
,
{
this
.
logger
.
error
(
`got error when connecting to node`
,
{
error
:
err
,
error
:
err
,
node
:
'
l1
'
,
node
:
'
l1
'
,
section
:
'
getBlockNumber
'
,
section
:
'
getBlockNumber
'
,
})
})
// Increment the metric so we can detect the issue.
this
.
metrics
.
nodeConnectionFailures
.
inc
({
this
.
metrics
.
nodeConnectionFailures
.
inc
({
layer
:
'
l1
'
,
layer
:
'
l1
'
,
section
:
'
getBlockNumber
'
,
section
:
'
getBlockNumber
'
,
})
})
await
sleep
(
this
.
options
.
sleepTimeMs
)
return
// Sleep for a little to give intermittent errors a chance to recover.
return
sleep
(
this
.
options
.
sleepTimeMs
)
}
}
// See if we have a new unchecked block
// Update highest block number metrics so we can keep track of how the service is doing.
this
.
metrics
.
highestBlockNumber
.
set
({
type
:
'
known
'
},
latestL1BlockNumber
)
this
.
metrics
.
highestBlockNumber
.
set
(
{
type
:
'
checked
'
},
this
.
state
.
highestUncheckedBlockNumber
)
// Check if the RPC provider is behind us for some reason. Can happen occasionally,
// particularly if connected to an RPC provider that load balances over multiple nodes that
// might not be perfectly in sync.
if
(
latestL1BlockNumber
<=
this
.
state
.
highestUncheckedBlockNumber
)
{
if
(
latestL1BlockNumber
<=
this
.
state
.
highestUncheckedBlockNumber
)
{
// The RPC provider is behind us, wait a bit
// Sleep for a little to give the RPC a chance to catch up.
await
sleep
(
this
.
options
.
sleepTimeMs
)
return
sleep
(
this
.
options
.
sleepTimeMs
)
return
}
}
// Generally better to use a relatively small block range because it means this service can be
// used alongside many different types of L1 nodes. For instance, Geth will typically only
// support a block range of 2000 blocks out of the box.
const
toBlockNumber
=
Math
.
min
(
this
.
state
.
highestUncheckedBlockNumber
+
this
.
options
.
eventBlockRange
,
latestL1BlockNumber
)
// Useful to log this stuff just in case we get stuck or something.
this
.
logger
.
info
(
`checking recent blocks`
,
{
this
.
logger
.
info
(
`checking recent blocks`
,
{
fromBlockNumber
:
this
.
state
.
highestUncheckedBlockNumber
,
fromBlockNumber
:
this
.
state
.
highestUncheckedBlockNumber
,
toBlockNumber
:
latestL1BlockNumber
,
toBlockNumber
,
})
})
//
Perform the check
//
Query for WithdrawalProven events within the specified block range.
let
proofEvents
:
Event
[]
let
events
:
ethers
.
Event
[]
try
{
try
{
// The query includes events in the blockNumbers given as the last two arguments
events
=
await
this
.
state
.
portal
.
queryFilter
(
proofEvents
=
this
.
state
.
portal
.
filters
.
WithdrawalProven
(),
await
this
.
state
.
messenger
.
contracts
.
l1
.
OptimismPortal
.
queryFilter
(
this
.
state
.
messenger
.
contracts
.
l1
.
OptimismPortal
.
filters
.
WithdrawalProven
(),
this
.
state
.
highestUncheckedBlockNumber
,
this
.
state
.
highestUncheckedBlockNumber
,
latestL1
BlockNumber
to
BlockNumber
)
)
}
catch
(
err
)
{
}
catch
(
err
)
{
// Log the issue so we can debug it.
this
.
logger
.
error
(
`got error when connecting to node`
,
{
this
.
logger
.
error
(
`got error when connecting to node`
,
{
error
:
err
,
error
:
err
,
node
:
'
l1
'
,
node
:
'
l1
'
,
section
:
'
querying for WithdrawalProven events
'
,
section
:
'
querying for WithdrawalProven events
'
,
})
})
// Increment the metric so we can detect the issue.
this
.
metrics
.
nodeConnectionFailures
.
inc
({
this
.
metrics
.
nodeConnectionFailures
.
inc
({
layer
:
'
l1
'
,
layer
:
'
l1
'
,
section
:
'
querying for WithdrawalProven events
'
,
section
:
'
querying for WithdrawalProven events
'
,
})
})
// connection error, wait then restart
await
sleep
(
this
.
options
.
sleepTimeMs
)
// Sleep for a little to give intermittent errors a chance to recover.
return
return
sleep
(
this
.
options
.
sleepTimeMs
)
}
}
for
(
const
proofEvent
of
proofEvents
)
{
// Go over all the events and check if the withdrawal hash actually exists on L2.
const
exists
=
for
(
const
event
of
events
)
{
await
this
.
state
.
messenger
.
contracts
.
l2
.
BedrockMessagePasser
.
sentMessages
(
// Could consider using multicall here but this is efficient enough for now.
proofEvent
.
args
.
withdrawalHash
const
hash
=
event
.
args
.
withdrawalHash
)
const
exists
=
await
this
.
state
.
messenger
.
sentMessages
(
hash
)
const
block
=
await
proofEvent
.
getBlock
()
const
now
=
new
Date
(
block
.
timestamp
*
1000
)
// Hopefully the withdrawal exists!
const
dateString
=
dateformat
(
now
,
'
mmmm dS, yyyy, h:MM:ss TT
'
,
true
// use UTC time
)
const
provenAt
=
`
${
dateString
}
UTC`
if
(
exists
)
{
if
(
exists
)
{
this
.
metrics
.
withdrawalsValidated
.
inc
()
// Unlike below we don't grab the timestamp here because it adds an unnecessary request.
this
.
logger
.
info
(
`valid withdrawal`
,
{
this
.
logger
.
info
(
`valid withdrawal`
,
{
withdrawalHash
:
proofEvent
.
args
.
withdrawalHash
,
withdrawalHash
:
event
.
args
.
withdrawalHash
,
provenAt
,
})
})
// Bump the withdrawals metric so we can keep track.
this
.
metrics
.
withdrawalsValidated
.
inc
()
}
else
{
}
else
{
// Grab and format the timestamp so it's clear how much time is left.
const
block
=
await
event
.
getBlock
()
const
ts
=
`
${
dateformat
(
new
Date
(
block
.
timestamp
*
1000
),
'
mmmm dS, yyyy, h:MM:ss TT
'
,
true
)}
UTC`
// Uh oh!
this
.
logger
.
error
(
`withdrawalHash not seen on L2`
,
{
this
.
logger
.
error
(
`withdrawalHash not seen on L2`
,
{
withdrawalHash
:
proofE
vent
.
args
.
withdrawalHash
,
withdrawalHash
:
e
vent
.
args
.
withdrawalHash
,
provenAt
,
provenAt
:
ts
,
})
})
// Change to forgery state.
this
.
state
.
forgeryDetected
=
true
this
.
state
.
forgeryDetected
=
true
this
.
metrics
.
isDetectingForgeries
.
set
(
1
)
this
.
metrics
.
isDetectingForgeries
.
set
(
1
)
return
// Return early so that we never increment the highest unchecked block number and therefore
// will continue to loop on this forgery indefinitely. We probably want to change this
// behavior at some point so that we keep scanning for additional forgeries since the
// existence of one forgery likely implies the existence of many others.
return
sleep
(
this
.
options
.
sleepTimeMs
)
}
}
}
}
this
.
state
.
highestUncheckedBlockNumber
=
latestL1BlockNumber
+
1
// Increment the highest unchecked block number for the next loop.
this
.
state
.
highestUncheckedBlockNumber
=
toBlockNumber
// If we got through the above without throwing an error, we should be fine to reset.
// If we got through the above without throwing an error, we should be fine to reset. Only case
// where this is relevant is if something is detected as a forgery accidentally and the error
// doesn't happen again on the next loop.
this
.
state
.
forgeryDetected
=
false
this
.
state
.
forgeryDetected
=
false
this
.
metrics
.
isDetectingForgeries
.
set
(
0
)
this
.
metrics
.
isDetectingForgeries
.
set
(
0
)
}
}
...
...
packages/common-ts/src/base-service/validators.ts
View file @
1ed50c44
...
@@ -49,6 +49,14 @@ const logLevel = makeValidator<LogLevel>((input) => {
...
@@ -49,6 +49,14 @@ const logLevel = makeValidator<LogLevel>((input) => {
}
}
})
})
const
address
=
makeValidator
<
string
>
((
input
)
=>
{
if
(
!
ethers
.
utils
.
isHexString
(
input
,
20
))
{
throw
new
Error
(
`expected input to be an address:
${
input
}
`
)
}
else
{
return
input
as
`0x
${
string
}
`
}
})
export
const
validators
=
{
export
const
validators
=
{
str
,
str
,
bool
,
bool
,
...
@@ -63,4 +71,5 @@ export const validators = {
...
@@ -63,4 +71,5 @@ export const validators = {
jsonRpcProvider
,
jsonRpcProvider
,
staticJsonRpcProvider
,
staticJsonRpcProvider
,
logLevel
,
logLevel
,
address
,
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment