Commit 61ec2ca5 authored by smartcontracts's avatar smartcontracts Committed by GitHub

Merge pull request #2210 from ethereum-optimism/sc/base-service-v2

feat: introduce the new BaseServiceV2 class 
parents 64d886fe 860fef46
---
'@eth-optimism/message-relayer': minor
---
Rewrites the message-relayer to use the BaseServiceV2.
---
'@eth-optimism/common-ts': patch
---
Introduces the new BaseServiceV2 class.
---
'@eth-optimism/sdk': patch
---
Tighten type restriction on ProviderLike
...@@ -123,7 +123,6 @@ services: ...@@ -123,7 +123,6 @@ services:
relayer: relayer:
depends_on: depends_on:
- l1_chain - l1_chain
- deployer
- l2geth - l2geth
deploy: deploy:
replicas: 0 replicas: 0
...@@ -133,14 +132,10 @@ services: ...@@ -133,14 +132,10 @@ services:
target: relayer target: relayer
entrypoint: ./relayer.sh entrypoint: ./relayer.sh
environment: environment:
L1_NODE_WEB3_URL: http://l1_chain:8545 MESSAGE_RELAYER__L1RPCPROVIDER: http://l1_chain:8545
L2_NODE_WEB3_URL: http://l2geth:8545 MESSAGE_RELAYER__L2RPCPROVIDER: http://l2geth:8545
URL: http://deployer:8081/addresses.json MESSAGE_RELAYER__L1WALLET: '0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97'
# a funded hardhat account
L1_WALLET_KEY: '0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97'
RETRIES: 60 RETRIES: 60
POLLING_INTERVAL: 500
GET_LOGS_INTERVAL: 500
verifier: verifier:
depends_on: depends_on:
......
...@@ -4,13 +4,6 @@ set -e ...@@ -4,13 +4,6 @@ set -e
RETRIES=${RETRIES:-60} RETRIES=${RETRIES:-60}
if [[ ! -z "$URL" ]]; then
# get the addrs from the URL provided
ADDRESSES=$(curl --fail --show-error --silent --retry-connrefused --retry $RETRIES --retry-delay 5 $URL)
# set the env
export ADDRESS_MANAGER_ADDRESS=$(echo $ADDRESSES | jq -r '.AddressManager')
fi
# waits for l2geth to be up # waits for l2geth to be up
curl \ curl \
--fail \ --fail \
...@@ -20,7 +13,7 @@ curl \ ...@@ -20,7 +13,7 @@ curl \
--retry-connrefused \ --retry-connrefused \
--retry $RETRIES \ --retry $RETRIES \
--retry-delay 1 \ --retry-delay 1 \
$L2_NODE_WEB3_URL $MESSAGE_RELAYER__L2RPCPROVIDER
# go # go
exec yarn start exec yarn start
...@@ -31,14 +31,23 @@ ...@@ -31,14 +31,23 @@
"url": "https://github.com/ethereum-optimism/optimism.git" "url": "https://github.com/ethereum-optimism/optimism.git"
}, },
"dependencies": { "dependencies": {
"@eth-optimism/core-utils": "0.8.1",
"@sentry/node": "^6.3.1", "@sentry/node": "^6.3.1",
"bcfg": "^0.1.7",
"commander": "^9.0.0",
"dotenv": "^16.0.0",
"envalid": "^7.2.2",
"ethers": "^5.5.4",
"express": "^4.17.1", "express": "^4.17.1",
"lodash": "^4.17.21",
"pino": "^6.11.3", "pino": "^6.11.3",
"pino-multi-stream": "^5.3.0", "pino-multi-stream": "^5.3.0",
"pino-sentry": "^0.7.0", "pino-sentry": "^0.7.0",
"prom-client": "^13.1.0" "prom-client": "^13.1.0"
}, },
"devDependencies": { "devDependencies": {
"@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/abstract-signer": "^5.5.0",
"@types/chai": "^4.2.18", "@types/chai": "^4.2.18",
"@types/express": "^4.17.12", "@types/express": "^4.17.12",
"@types/mocha": "^8.2.2", "@types/mocha": "^8.2.2",
......
/* Imports: External */
import Config from 'bcfg'
import * as dotenv from 'dotenv'
import { Command, Option } from 'commander'
import { ValidatorSpec, Spec, cleanEnv } from 'envalid'
import { sleep } from '@eth-optimism/core-utils'
import snakeCase from 'lodash/snakeCase'
/* Imports: Internal */
import { Logger } from '../common/logger'
import { Metric } from './metrics'
export type Options = {
[key: string]: any
}
export type OptionsSpec<TOptions extends Options> = {
[P in keyof Required<TOptions>]: {
validator: (spec?: Spec<TOptions[P]>) => ValidatorSpec<TOptions[P]>
desc: string
default?: TOptions[P]
}
}
export type MetricsV2 = {
[key: string]: Metric
}
export type MetricsSpec<TMetrics extends MetricsV2> = {
[P in keyof Required<TMetrics>]: {
type: new (configuration: any) => TMetrics[P]
desc: string
labels?: string[]
}
}
/**
* BaseServiceV2 is an advanced but simple base class for long-running TypeScript services.
*/
export abstract class BaseServiceV2<
TOptions extends Options,
TMetrics extends MetricsV2,
TServiceState
> {
/**
* Whether or not the service will loop.
*/
protected loop: boolean
/**
* Waiting period in ms between loops, if the service will loop.
*/
protected loopIntervalMs: number
/**
* Logger class for this service.
*/
protected logger: Logger
/**
* Service state, persisted between loops.
*/
protected state: TServiceState
/**
* Service options.
*/
protected readonly options: TOptions
/**
* Metrics.
*/
protected readonly metrics: TMetrics
/**
* @param params Options for the construction of the service.
* @param params.name Name for the service. This name will determine the prefix used for logging,
* metrics, and loading environment variables.
* @param params.optionsSpec Settings for input options. You must specify at least a
* description for each option.
* @param params.metricsSpec Settings that define which metrics are collected. All metrics that
* you plan to collect must be defined within this object.
* @param params.options Options to pass to the service.
* @param params.loops Whether or not the service should loop. Defaults to true.
* @param params.loopIntervalMs Loop interval in milliseconds. Defaults to zero.
*/
constructor(params: {
name: string
optionsSpec: OptionsSpec<TOptions>
metricsSpec: MetricsSpec<TMetrics>
options?: Partial<TOptions>
loop?: boolean
loopIntervalMs?: number
}) {
this.loop = params.loop !== undefined ? params.loop : true
this.loopIntervalMs =
params.loopIntervalMs !== undefined ? params.loopIntervalMs : 0
this.state = {} as TServiceState
// Use commander as a way to communicate info about the service. We don't actually *use*
// commander for anything besides the ability to run `ts-node ./service.ts --help`.
const program = new Command()
for (const [optionName, optionSpec] of Object.entries(params.optionsSpec)) {
program.addOption(
new Option(`--${optionName.toLowerCase()}`, `${optionSpec.desc}`).env(
`${params.name
.replace(/-/g, '_')
.toUpperCase()}__${optionName.toUpperCase()}`
)
)
}
const longestMetricNameLength = Object.keys(params.metricsSpec).reduce(
(acc, key) => {
const nameLength = snakeCase(key).length
if (nameLength > acc) {
return nameLength
} else {
return acc
}
},
0
)
program.addHelpText(
'after',
`\nMetrics:\n${Object.entries(params.metricsSpec)
.map(([metricName, metricSpec]) => {
const parsedName = snakeCase(metricName)
return ` ${parsedName}${' '.repeat(
longestMetricNameLength - parsedName.length + 2
)}${metricSpec.desc} (type: ${metricSpec.type.name})`
})
.join('\n')}
`
)
// Load all configuration values from the environment and argv.
program.parse()
dotenv.config()
const config = new Config(params.name)
config.load({
env: true,
argv: true,
})
// Clean configuration values using the options spec.
// Since BCFG turns everything into lower case, we're required to turn all of the input option
// names into lower case for the validation step. We'll turn the names back into their original
// names when we're done.
const cleaned = cleanEnv<TOptions>(
{ ...config.env, ...config.args },
Object.entries(params.optionsSpec || {}).reduce((acc, [key, val]) => {
acc[key.toLowerCase()] = val.validator({
desc: val.desc,
default: val.default,
})
return acc
}, {}) as any,
Object.entries(params.options || {}).reduce((acc, [key, val]) => {
acc[key.toLowerCase()] = val
return acc
}, {}) as any
)
// Turn the lowercased option names back into camelCase.
this.options = Object.keys(params.optionsSpec || {}).reduce((acc, key) => {
acc[key] = cleaned[key.toLowerCase()]
return acc
}, {}) as TOptions
// Create the metrics objects.
this.metrics = Object.keys(params.metricsSpec || {}).reduce((acc, key) => {
const spec = params.metricsSpec[key]
acc[key] = new spec.type({
name: `${snakeCase(params.name)}_${snakeCase(key)}`,
help: spec.desc,
labelNames: spec.labels || [],
})
return acc
}, {}) as TMetrics
this.logger = new Logger({ name: params.name })
}
/**
* Runs the main function. If this service is set up to loop, will repeatedly loop around the
* main function. Will also catch unhandled errors.
*/
public run(): void {
const _run = async () => {
if (this.init) {
this.logger.info('initializing service')
await this.init()
this.logger.info('service initialized')
}
if (this.loop) {
this.logger.info('starting main loop')
while (true) {
try {
await this.main()
} catch (err) {
this.logger.error('caught an unhandled exception', {
message: err.message,
stack: err.stack,
code: err.code,
})
}
// Always sleep between loops
await sleep(this.loopIntervalMs)
}
} else {
this.logger.info('running main function')
await this.main()
}
}
_run()
}
/**
* Initialization function. Runs once before the main function.
*/
protected init?(): Promise<void>
/**
* Main function. Runs repeatedly when run() is called.
*/
protected abstract main(): Promise<void>
}
/* Imports: Internal */ /* Imports: Internal */
import { Logger } from './common/logger' import { Logger } from '../common/logger'
import { Metrics } from './common/metrics' import { Metrics } from '../common/metrics'
type OptionSettings<TOptions> = { type OptionSettings<TOptions> = {
[P in keyof TOptions]?: { [P in keyof TOptions]?: {
......
export * from './base-service'
export * from './base-service-v2'
export * from './validators'
export * from './metrics'
import {
Gauge as PGauge,
Counter as PCounter,
Histogram as PHistogram,
Summary as PSummary,
} from 'prom-client'
export class Gauge extends PGauge<string> {}
export class Counter extends PCounter<string> {}
export class Histogram extends PHistogram<string> {}
export class Summary extends PSummary<string> {}
export type Metric = Gauge | Counter | Histogram | Summary
import {
str,
bool,
num,
email,
host,
port,
url,
json,
makeValidator,
} from 'envalid'
import { Provider } from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
import { ethers } from 'ethers'
const provider = makeValidator<Provider>((input) => {
const parsed = url()._parse(input)
return new ethers.providers.JsonRpcProvider(parsed)
})
const wallet = makeValidator<Signer>((input) => {
if (!ethers.utils.isHexString(input)) {
throw new Error(`expected wallet to be a hex string`)
} else {
return new ethers.Wallet(input)
}
})
export const validators = {
str,
bool,
num,
email,
host,
port,
url,
json,
wallet,
provider,
}
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
*This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed behind a proxy. Since a proxied contract can&#39;t have a constructor, it&#39;s common to move constructor logic to an external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. [CAUTION] ==== Avoid leaving a contract uninitialized. An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation contract, which may impact the proxy. To initialize the implementation contract, you can either invoke the initializer manually, or you can include a constructor to automatically mark it as initialized when it is deployed: [.hljs-theme-light.nopadding] ```* *This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed behind a proxy. Since a proxied contract can&#39;t have a constructor, it&#39;s common to move constructor logic to an external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure that all initializers are idempotent. This is not verified automatically as constructors are by Solidity.*
import { Wallet, providers } from 'ethers'
import { Bcfg } from '@eth-optimism/core-utils'
import { Logger, LoggerOptions } from '@eth-optimism/common-ts'
import * as Sentry from '@sentry/node'
import * as dotenv from 'dotenv'
import Config from 'bcfg'
import { MessageRelayerService } from '../src'
dotenv.config()
const main = async () => {
const config: Bcfg = new Config('message-relayer')
config.load({
env: true,
argv: true,
})
const env = process.env
const SENTRY_DSN = config.str('sentry-dsn', env.SENTRY_DSN)
const USE_SENTRY = config.bool('use-sentry', env.USE_SENTRY === 'true')
const ETH_NETWORK_NAME = config.str('eth-network-name', env.ETH_NETWORK_NAME)
const loggerOptions: LoggerOptions = {
name: 'Message_Relayer',
}
if (USE_SENTRY) {
const sentryOptions = {
release: `message-relayer@${process.env.npm_package_version}`,
dsn: SENTRY_DSN,
environment: ETH_NETWORK_NAME,
}
loggerOptions.sentryOptions = sentryOptions
Sentry.init(sentryOptions)
}
const logger = new Logger(loggerOptions)
const L2_NODE_WEB3_URL = config.str('l2-node-web3-url', env.L2_NODE_WEB3_URL)
const L1_NODE_WEB3_URL = config.str('l1-node-web3-url', env.L1_NODE_WEB3_URL)
const ADDRESS_MANAGER_ADDRESS = config.str(
'address-manager-address',
env.ADDRESS_MANAGER_ADDRESS
)
const L1_WALLET_KEY = config.str('l1-wallet-key', env.L1_WALLET_KEY)
const MNEMONIC = config.str('mnemonic', env.MNEMONIC)
const HD_PATH = config.str('hd-path', env.HD_PATH)
const RELAY_GAS_LIMIT = config.uint(
'relay-gas-limit',
parseInt(env.RELAY_GAS_LIMIT, 10) || 4000000
)
const POLLING_INTERVAL = config.uint(
'polling-interval',
parseInt(env.POLLING_INTERVAL, 10) || 5000
)
const GET_LOGS_INTERVAL = config.uint(
'get-logs-interval',
parseInt(env.GET_LOGS_INTERVAL, 10) || 2000
)
const FROM_L2_TRANSACTION_INDEX = config.uint(
'from-l2-transaction-index',
parseInt(env.FROM_L2_TRANSACTION_INDEX, 10) || 0
)
if (!ADDRESS_MANAGER_ADDRESS) {
throw new Error('Must pass ADDRESS_MANAGER_ADDRESS')
}
if (!L1_NODE_WEB3_URL) {
throw new Error('Must pass L1_NODE_WEB3_URL')
}
if (!L2_NODE_WEB3_URL) {
throw new Error('Must pass L2_NODE_WEB3_URL')
}
const l2Provider = new providers.StaticJsonRpcProvider({
url: L2_NODE_WEB3_URL,
headers: { 'User-Agent': 'message-relayer' },
})
const l1Provider = new providers.StaticJsonRpcProvider({
url: L1_NODE_WEB3_URL,
headers: { 'User-Agent': 'message-relayer' },
})
let wallet: Wallet
if (L1_WALLET_KEY) {
wallet = new Wallet(L1_WALLET_KEY, l1Provider)
} else if (MNEMONIC) {
wallet = Wallet.fromMnemonic(MNEMONIC, HD_PATH)
wallet = wallet.connect(l1Provider)
} else {
throw new Error('Must pass one of L1_WALLET_KEY or MNEMONIC')
}
const service = new MessageRelayerService({
l2RpcProvider: l2Provider,
l1Wallet: wallet,
relayGasLimit: RELAY_GAS_LIMIT,
fromL2TransactionIndex: FROM_L2_TRANSACTION_INDEX,
pollingInterval: POLLING_INTERVAL,
getLogsInterval: GET_LOGS_INTERVAL,
logger,
})
await service.start()
}
main()
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
"dist/*" "dist/*"
], ],
"scripts": { "scripts": {
"start": "ts-node ./bin/run.ts", "start": "ts-node ./src/service.ts",
"build": "tsc -p ./tsconfig.build.json", "build": "tsc -p ./tsconfig.build.json",
"clean": "rimraf dist/ ./tsconfig.build.tsbuildinfo", "clean": "rimraf dist/ ./tsconfig.build.tsbuildinfo",
"lint": "yarn lint:fix && yarn lint:check", "lint": "yarn lint:fix && yarn lint:check",
...@@ -31,13 +31,11 @@ ...@@ -31,13 +31,11 @@
"dependencies": { "dependencies": {
"@eth-optimism/common-ts": "0.2.1", "@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/core-utils": "0.8.1", "@eth-optimism/core-utils": "0.8.1",
"@eth-optimism/sdk": "^1.0.0", "@eth-optimism/sdk": "1.0.0",
"@sentry/node": "^6.3.1",
"bcfg": "^0.1.6",
"dotenv": "^10.0.0",
"ethers": "^5.5.4" "ethers": "^5.5.4"
}, },
"devDependencies": { "devDependencies": {
"@ethersproject/abstract-provider": "^5.5.1",
"@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-waffle": "^2.0.1", "@nomiclabs/hardhat-waffle": "^2.0.1",
"@typescript-eslint/eslint-plugin": "^4.26.0", "@typescript-eslint/eslint-plugin": "^4.26.0",
......
/* Imports: External */ /* Imports: External */
import { Wallet } from 'ethers' import { Signer } from 'ethers'
import { sleep } from '@eth-optimism/core-utils' import { sleep } from '@eth-optimism/core-utils'
import { Logger, BaseService, Metrics } from '@eth-optimism/common-ts'
import { import {
CrossChainMessenger, BaseServiceV2,
MessageStatus, validators,
ProviderLike, Gauge,
} from '@eth-optimism/sdk' Counter,
} from '@eth-optimism/common-ts'
interface MessageRelayerOptions { import { CrossChainMessenger, MessageStatus } from '@eth-optimism/sdk'
/** import { Provider } from '@ethersproject/abstract-provider'
* Provider for interacting with L2.
*/ type MessageRelayerOptions = {
l2RpcProvider: ProviderLike l1RpcProvider: Provider
l2RpcProvider: Provider
/** l1Wallet: Signer
* Wallet used to interact with L1.
*/
l1Wallet: Wallet
/**
* Gas to relay transactions with. If not provided, will use the estimated gas for the relay
* transaction.
*/
relayGasLimit?: number
/**
* Index of the first L2 transaction to start processing from.
*/
fromL2TransactionIndex?: number fromL2TransactionIndex?: number
}
/** type MessageRelayerMetrics = {
* Waiting interval between loops when the service is at the tip. highestCheckedL2Tx: Gauge
*/ highestKnownL2Tx: Gauge
pollingInterval?: number numRelayedMessages: Counter
/**
* Size of the block range to query when looking for new SentMessage events.
*/
getLogsInterval?: number
/**
* Logger to transport logs. Defaults to STDOUT.
*/
logger?: Logger
/**
* Metrics object to use. Defaults to no metrics.
*/
metrics?: Metrics
} }
export class MessageRelayerService extends BaseService<MessageRelayerOptions> { type MessageRelayerState = {
constructor(options: MessageRelayerOptions) { wallet: Signer
super('Message_Relayer', options, { messenger: CrossChainMessenger
relayGasLimit: { highestCheckedL2Tx: number
default: 4_000_000, highestKnownL2Tx: number
}, }
fromL2TransactionIndex: {
default: 0, export class MessageRelayerService extends BaseServiceV2<
}, MessageRelayerOptions,
pollingInterval: { MessageRelayerMetrics,
default: 5000, MessageRelayerState
> {
constructor(options?: Partial<MessageRelayerOptions>) {
super({
name: 'Message_Relayer',
options,
optionsSpec: {
l1RpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L1.',
},
l2RpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L2.',
},
l1Wallet: {
validator: validators.wallet,
desc: 'Wallet used to interact with L1.',
},
fromL2TransactionIndex: {
validator: validators.num,
desc: 'Index of the first L2 transaction to start processing from.',
default: 0,
},
}, },
getLogsInterval: { metricsSpec: {
default: 2000, highestCheckedL2Tx: {
type: Gauge,
desc: 'Highest L2 tx that has been scanned for messages',
},
highestKnownL2Tx: {
type: Gauge,
desc: 'Highest known L2 transaction',
},
numRelayedMessages: {
type: Counter,
desc: 'Number of messages relayed by the service',
},
}, },
}) })
} }
private state: { protected async init(): Promise<void> {
messenger: CrossChainMessenger this.state.wallet = this.options.l1Wallet.connect(
highestCheckedL2Tx: number this.options.l1RpcProvider
} = {} as any )
protected async _init(): Promise<void> {
this.logger.info('Initializing message relayer', {
relayGasLimit: this.options.relayGasLimit,
fromL2TransactionIndex: this.options.fromL2TransactionIndex,
pollingInterval: this.options.pollingInterval,
getLogsInterval: this.options.getLogsInterval,
})
const l1Network = await this.options.l1Wallet.provider.getNetwork() const l1Network = await this.state.wallet.provider.getNetwork()
const l1ChainId = l1Network.chainId const l1ChainId = l1Network.chainId
this.state.messenger = new CrossChainMessenger({ this.state.messenger = new CrossChainMessenger({
l1SignerOrProvider: this.options.l1Wallet, l1SignerOrProvider: this.state.wallet,
l2SignerOrProvider: this.options.l2RpcProvider, l2SignerOrProvider: this.options.l2RpcProvider,
l1ChainId, l1ChainId,
}) })
this.state.highestCheckedL2Tx = this.options.fromL2TransactionIndex || 1 this.state.highestCheckedL2Tx = this.options.fromL2TransactionIndex || 1
this.state.highestKnownL2Tx =
await this.state.messenger.l2Provider.getBlockNumber()
} }
protected async _start(): Promise<void> { protected async main(): Promise<void> {
while (this.running) { // Update metrics
await sleep(this.options.pollingInterval) this.metrics.highestCheckedL2Tx.set(this.state.highestCheckedL2Tx)
this.metrics.highestKnownL2Tx.set(this.state.highestKnownL2Tx)
// If we're already at the tip, then update the latest tip and loop again.
if (this.state.highestCheckedL2Tx > this.state.highestKnownL2Tx) {
this.state.highestKnownL2Tx =
await this.state.messenger.l2Provider.getBlockNumber()
// Sleeping for 1000ms is good enough since this is meant for development and not for live
// networks where we might want to restrict the number of requests per second.
await sleep(1000)
return
}
this.logger.info(`checking L2 block ${this.state.highestCheckedL2Tx}`)
const block =
await this.state.messenger.l2Provider.getBlockWithTransactions(
this.state.highestCheckedL2Tx
)
// Should never happen.
if (block.transactions.length !== 1) {
throw new Error(
`got an unexpected number of transactions in block: ${block.number}`
)
}
const messages = await this.state.messenger.getMessagesByTransaction(
block.transactions[0].hash
)
// No messages in this transaction so we can move on to the next one.
if (messages.length === 0) {
this.state.highestCheckedL2Tx++
return
}
// Make sure that all messages sent within the transaction are finalized. If any messages
// are not finalized, then we're going to break the loop which will trigger the sleep and
// wait for a few seconds before we check again to see if this transaction is finalized.
let isFinalized = true
for (const message of messages) {
const status = await this.state.messenger.getMessageStatus(message)
if (
status === MessageStatus.IN_CHALLENGE_PERIOD ||
status === MessageStatus.STATE_ROOT_NOT_PUBLISHED
) {
isFinalized = false
}
}
if (!isFinalized) {
this.logger.info(
`tx not yet finalized, waiting: ${this.state.highestCheckedL2Tx}`
)
return
} else {
this.logger.info(
`tx is finalized, relaying: ${this.state.highestCheckedL2Tx}`
)
}
// If we got here then all messages in the transaction are finalized. Now we can relay
// each message to L1.
for (const message of messages) {
try { try {
// Loop strategy is as follows: const tx = await this.state.messenger.finalizeMessage(message)
// 1. Get the current L2 tip this.logger.info(`relayer sent tx: ${tx.hash}`)
// 2. While we're not at the tip: this.metrics.numRelayedMessages.inc()
// 2.1. Get the transaction for the next L2 block to parse.
// 2.2. Find any messages sent in the L2 block.
// 2.3. Make sure all messages are ready to be relayed.
// 3.4. Relay the messages.
const l2BlockNumber =
await this.state.messenger.l2Provider.getBlockNumber()
while (this.state.highestCheckedL2Tx <= l2BlockNumber) {
this.logger.info(`checking L2 block ${this.state.highestCheckedL2Tx}`)
const block =
await this.state.messenger.l2Provider.getBlockWithTransactions(
this.state.highestCheckedL2Tx
)
// Should never happen.
if (block.transactions.length !== 1) {
throw new Error(
`got an unexpected number of transactions in block: ${block.number}`
)
}
const messages = await this.state.messenger.getMessagesByTransaction(
block.transactions[0].hash
)
// No messages in this transaction so we can move on to the next one.
if (messages.length === 0) {
this.state.highestCheckedL2Tx++
continue
}
// Make sure that all messages sent within the transaction are finalized. If any messages
// are not finalized, then we're going to break the loop which will trigger the sleep and
// wait for a few seconds before we check again to see if this transaction is finalized.
let isFinalized = true
for (const message of messages) {
const status = await this.state.messenger.getMessageStatus(message)
if (
status === MessageStatus.IN_CHALLENGE_PERIOD ||
status === MessageStatus.STATE_ROOT_NOT_PUBLISHED
) {
isFinalized = false
}
}
if (!isFinalized) {
this.logger.info(
`tx not yet finalized, waiting: ${this.state.highestCheckedL2Tx}`
)
break
} else {
this.logger.info(
`tx is finalized, relaying: ${this.state.highestCheckedL2Tx}`
)
}
// If we got here then all messages in the transaction are finalized. Now we can relay
// each message to L1.
for (const message of messages) {
try {
const tx = await this.state.messenger.finalizeMessage(message)
this.logger.info(`relayer sent tx: ${tx.hash}`)
} catch (err) {
if (err.message.includes('message has already been received')) {
// It's fine, the message was relayed by someone else
} else {
throw err
}
}
await this.state.messenger.waitForMessageReceipt(message)
}
// All messages have been relayed so we can move on to the next block.
this.state.highestCheckedL2Tx++
}
} catch (err) { } catch (err) {
this.logger.error('Caught an unhandled error', { if (err.message.includes('message has already been received')) {
message: err.toString(), // It's fine, the message was relayed by someone else
stack: err.stack, } else {
code: err.code, throw err
}) }
} }
await this.state.messenger.waitForMessageReceipt(message)
} }
// All messages have been relayed so we can move on to the next block.
this.state.highestCheckedL2Tx++
} }
} }
if (require.main === module) {
const service = new MessageRelayerService()
service.run()
}
...@@ -269,7 +269,7 @@ export type MessageRequestLike = ...@@ -269,7 +269,7 @@ export type MessageRequestLike =
/** /**
* Stuff that can be coerced into a provider. * Stuff that can be coerced into a provider.
*/ */
export type ProviderLike = string | Provider | any export type ProviderLike = string | Provider
/** /**
* Stuff that can be coerced into a signer. * Stuff that can be coerced into a signer.
......
...@@ -4407,6 +4407,13 @@ bcfg@^0.1.6: ...@@ -4407,6 +4407,13 @@ bcfg@^0.1.6:
dependencies: dependencies:
bsert "~0.0.10" bsert "~0.0.10"
bcfg@^0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/bcfg/-/bcfg-0.1.7.tgz#610198a67a56160305fdc1f54b5b5c90b52530d7"
integrity sha512-+4beq5bXwfmxdcEoHYQsaXawh1qFzjLcRvPe5k5ww/NEWzZTm56Jk8LuPmfeGB7X584jZ8xGq6UgMaZnNDa5Ww==
dependencies:
bsert "~0.0.10"
bcrypt-pbkdf@^1.0.0: bcrypt-pbkdf@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
...@@ -5383,6 +5390,11 @@ commander@^8.3.0: ...@@ -5383,6 +5390,11 @@ commander@^8.3.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
commander@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-9.0.0.tgz#86d58f24ee98126568936bd1d3574e0308a99a40"
integrity sha512-JJfP2saEKbQqvW+FI93OYUB4ByV5cizMpFMiiJI8xDbBvQvSkIk0VvQdn1CZ8mqAO8Loq2h0gYTYtDFUZUeERw==
comment-parser@1.1.6-beta.0: comment-parser@1.1.6-beta.0:
version "1.1.6-beta.0" version "1.1.6-beta.0"
resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.1.6-beta.0.tgz#57e503b18d0a5bd008632dcc54b1f95c2fffe8f6" resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.1.6-beta.0.tgz#57e503b18d0a5bd008632dcc54b1f95c2fffe8f6"
...@@ -6142,6 +6154,11 @@ dotenv@^10.0.0: ...@@ -6142,6 +6154,11 @@ dotenv@^10.0.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
dotenv@^16.0.0:
version "16.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411"
integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==
dotignore@~0.1.2: dotignore@~0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905" resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905"
...@@ -6288,6 +6305,13 @@ envalid@^7.1.0: ...@@ -6288,6 +6305,13 @@ envalid@^7.1.0:
dependencies: dependencies:
tslib "2.3.1" tslib "2.3.1"
envalid@^7.2.2:
version "7.2.2"
resolved "https://registry.yarnpkg.com/envalid/-/envalid-7.2.2.tgz#f3219f85e692002dca0f28076740227d30c817e3"
integrity sha512-bl/3VF5PhoF26HlDWiE0NRRHUbKT/+UDP/+0JtOFmhUwK3cUPS7JgWYGbE8ArvA61T+SyNquxscLCS6y4Wnpdw==
dependencies:
tslib "2.3.1"
envinfo@^7.7.4: envinfo@^7.7.4:
version "7.8.1" version "7.8.1"
resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475"
......
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