Commit 37ed84de authored by OptimismBot's avatar OptimismBot Committed by GitHub

Merge pull request #6008 from ethereum-optimism/jm/tfr-mon

feat(c-mon): Add wallet-mon service
parents a541c8a8 8ab436a5
---
'@eth-optimism/chain-mon': minor
'@eth-optimism/core-utils': patch
---
Added a new service wallet-mon to identify unexpected transfers from key accounts
......@@ -89,3 +89,7 @@ ENTRYPOINT ["npm", "run", "start:drippie-mon"]
FROM base as wd-mon
WORKDIR /opt/optimism/packages/chain-mon
ENTRYPOINT ["yarn", "run", "start:wd-mon"]
FROM base as wallet-mon
WORKDIR /opt/optimism/packages/chain-mon
ENTRYPOINT ["yarn", "run", "start:wallet-mon"]
......@@ -8,6 +8,17 @@ BALANCE_MON__RPC=
# JSON array in the format [{ "address": <address>, "nickname": <nickname> }, ... ]
BALANCE_MON__ACCOUNTS=
###############################################################################
# ↓ wallet-mon ↓ #
###############################################################################
# RPC pointing to network to monitor
WALLET_MON__RPC=
# The block number to start monitoring from
# Defaults to the first bedrock block if unset.
WALLET_MON__START_BLOCK_NUMBER=
###############################################################################
# ↓ drippie-mon ↓ #
###############################################################################
......
......@@ -10,6 +10,7 @@
],
"scripts": {
"start:balance-mon": "ts-node ./src/balance-mon/service.ts",
"start:wallet-mon": "ts-node ./src/wallet-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.'",
......@@ -35,6 +36,7 @@
"dependencies": {
"@eth-optimism/common-ts": "0.8.1",
"@eth-optimism/contracts-periphery": "1.0.8",
"@eth-optimism/contracts-bedrock": "0.14.0",
"@eth-optimism/core-utils": "0.12.0",
"@eth-optimism/sdk": "2.1.0",
"ethers": "^5.7.0",
......
export * from './balance-mon/service'
export * from './drippie-mon/service'
export * from './wd-mon/service'
export * from './wallet-mon/service'
import {
BaseServiceV2,
StandardOptions,
Gauge,
Counter,
validators,
waitForProvider,
} from '@eth-optimism/common-ts'
import { getChainId, compareAddrs } from '@eth-optimism/core-utils'
import { Provider } from '@ethersproject/abstract-provider'
import mainnetConfig from '@eth-optimism/contracts-bedrock/deploy-config/mainnet.json'
import goerliConfig from '@eth-optimism/contracts-bedrock/deploy-config/goerli.json'
import l2OutputOracleArtifactsMainnet from '@eth-optimism/contracts-bedrock/deployments/mainnet/L2OutputOracleProxy.json'
import l2OutputOracleArtifactsGoerli from '@eth-optimism/contracts-bedrock/deployments/goerli/L2OutputOracleProxy.json'
import { version } from '../../package.json'
const networks = {
1: {
name: 'mainnet',
l1StartingBlockTag: mainnetConfig.l1StartingBlockTag,
accounts: [
{
label: 'Proposer',
wallet: mainnetConfig.l2OutputOracleProposer,
target: l2OutputOracleArtifactsMainnet.address,
},
{
label: 'Batcher',
wallet: mainnetConfig.batchSenderAddress,
target: mainnetConfig.batchInboxAddress,
},
],
},
10: {
name: 'goerli',
l1StartingBlockTag: goerliConfig.l1StartingBlockTag,
accounts: [
{
label: 'Proposer',
wallet: goerliConfig.l2OutputOracleProposer,
target: l2OutputOracleArtifactsGoerli.address,
},
{
label: 'Batcher',
wallet: goerliConfig.batchSenderAddress,
target: goerliConfig.batchInboxAddress,
},
],
},
}
type WalletMonOptions = {
rpc: Provider
startBlockNumber: number
}
type WalletMonMetrics = {
validatedCalls: Counter
unexpectedCalls: Counter
unexpectedRpcErrors: Counter
}
type WalletMonState = {
chainId: number
highestUncheckedBlockNumber: number
}
export class WalletMonService extends BaseServiceV2<
WalletMonOptions,
WalletMonMetrics,
WalletMonState
> {
constructor(options?: Partial<WalletMonOptions & StandardOptions>) {
super({
version,
name: 'wallet-mon',
loop: true,
options: {
loopIntervalMs: 1000,
...options,
},
optionsSpec: {
rpc: {
validator: validators.provider,
desc: 'Provider for network to monitor balances on',
},
startBlockNumber: {
validator: validators.num,
default: -1,
desc: 'L1 block number to start checking from',
public: true,
},
},
metricsSpec: {
validatedCalls: {
type: Gauge,
desc: 'Transactions from the account checked',
labels: ['wallet', 'target', 'nickname'],
},
unexpectedCalls: {
type: Counter,
desc: 'Number of unexpected wallets',
labels: ['wallet', 'target', 'nickname'],
},
unexpectedRpcErrors: {
type: Counter,
desc: 'Number of unexpected RPC errors',
labels: ['section', 'name'],
},
},
})
}
protected async init(): Promise<void> {
// Connect to L1.
await waitForProvider(this.options.rpc, {
logger: this.logger,
name: 'L1',
})
this.state.chainId = await getChainId(this.options.rpc)
const l1StartingBlockTag = networks[this.state.chainId].l1StartingBlockTag
if (this.options.startBlockNumber === -1) {
const block = await this.options.rpc.getBlock(l1StartingBlockTag)
this.state.highestUncheckedBlockNumber = block.number
} else {
this.state.highestUncheckedBlockNumber = this.options.startBlockNumber
}
}
protected async main(): Promise<void> {
if (
(await this.options.rpc.getBlockNumber()) <
this.state.highestUncheckedBlockNumber
) {
this.logger.info('Waiting for new blocks')
return
}
const network = networks[this.state.chainId]
const accounts = network.accounts
const block = await this.options.rpc.getBlock(
this.state.highestUncheckedBlockNumber
)
this.logger.info('Checking block', {
number: block.number,
})
const transactions = []
for (const txHash of block.transactions) {
const t = await this.options.rpc.getTransaction(txHash)
transactions.push(t)
}
for (const transaction of transactions) {
for (const account of accounts) {
if (compareAddrs(account.wallet, transaction.from)) {
if (compareAddrs(account.target, transaction.to)) {
this.metrics.validatedCalls.inc({
nickname: account.label,
wallet: account.address,
target: account.target,
})
this.logger.info('validated call', {
nickname: account.label,
wallet: account.address,
target: account.target,
})
} else {
this.metrics.unexpectedCalls.inc({
nickname: account.label,
wallet: account.address,
target: transaction.to,
})
this.logger.error('Unexpected call detected', {
nickname: account.label,
address: account.address,
target: transaction.to,
})
}
}
}
}
this.logger.info('Checked block', {
number: this.state.highestUncheckedBlockNumber,
})
this.state.highestUncheckedBlockNumber++
}
}
if (require.main === module) {
const service = new WalletMonService()
service.run()
}
......@@ -48,3 +48,14 @@ export const reqenv = (name: string): string => {
export const getenv = (name: string, fallback?: string): string | undefined => {
return process.env[name] || fallback
}
/**
* Returns true if the given string is a valid address.
*
* @param a First address to check.
* @param b Second address to check.
* @returns True if the given addresses match.
*/
export const compareAddrs = (a: string, b: string): boolean => {
return a.toLowerCase() === b.toLowerCase()
}
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