1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
/* Imports: External */
import { Signer } from 'ethers'
import { getChainId, sleep } from '@eth-optimism/core-utils'
import {
BaseServiceV2,
validators,
Gauge,
Counter,
} from '@eth-optimism/common-ts'
import { CrossChainMessenger, MessageStatus } from '@eth-optimism/sdk'
import { Provider } from '@ethersproject/abstract-provider'
import { version } from '../package.json'
type MessageRelayerOptions = {
l1RpcProvider: Provider
l2RpcProvider: Provider
l1Wallet: Signer
fromL2TransactionIndex?: number
}
type MessageRelayerMetrics = {
highestCheckedL2Tx: Gauge
highestKnownL2Tx: Gauge
numRelayedMessages: Counter
}
type MessageRelayerState = {
wallet: Signer
messenger: CrossChainMessenger
highestCheckedL2Tx: number
highestKnownL2Tx: number
}
export class MessageRelayerService extends BaseServiceV2<
MessageRelayerOptions,
MessageRelayerMetrics,
MessageRelayerState
> {
constructor(options?: Partial<MessageRelayerOptions>) {
super({
version,
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,
public: true,
},
},
metricsSpec: {
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',
},
},
})
}
protected async init(): Promise<void> {
this.state.wallet = this.options.l1Wallet.connect(
this.options.l1RpcProvider
)
this.state.messenger = new CrossChainMessenger({
l1SignerOrProvider: this.state.wallet,
l2SignerOrProvider: this.options.l2RpcProvider,
l1ChainId: await getChainId(this.state.wallet.provider),
l2ChainId: await getChainId(this.options.l2RpcProvider),
})
this.state.highestCheckedL2Tx = this.options.fromL2TransactionIndex || 1
this.state.highestKnownL2Tx =
await this.state.messenger.l2Provider.getBlockNumber()
}
protected async main(): Promise<void> {
// Update metrics
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 {
const tx = await this.state.messenger.finalizeMessage(message)
this.logger.info(`relayer sent tx: ${tx.hash}`)
this.metrics.numRelayedMessages.inc()
} 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++
}
}
if (require.main === module) {
const service = new MessageRelayerService()
service.run()
}