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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
package driver
import (
"context"
crand "crypto/rand"
"encoding/binary"
"errors"
"fmt"
"math/big"
"math/rand"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-node/metrics"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/testutils"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/testlog"
)
var mockResetErr = fmt.Errorf("mock reset err: %w", derive.ErrReset)
type FakeEngineControl struct {
finalized eth.L2BlockRef
safe eth.L2BlockRef
unsafe eth.L2BlockRef
buildingOnto eth.L2BlockRef
buildingID eth.PayloadID
buildingSafe bool
buildingAttrs *eth.PayloadAttributes
buildingStart time.Time
cfg *rollup.Config
timeNow func() time.Time
makePayload func(onto eth.L2BlockRef, attrs *eth.PayloadAttributes) *eth.ExecutionPayload
errTyp derive.BlockInsertionErrType
err error
totalBuildingTime time.Duration
totalBuiltBlocks int
totalTxs int
}
func (m *FakeEngineControl) avgBuildingTime() time.Duration {
return m.totalBuildingTime / time.Duration(m.totalBuiltBlocks)
}
func (m *FakeEngineControl) avgTxsPerBlock() float64 {
return float64(m.totalTxs) / float64(m.totalBuiltBlocks)
}
func (m *FakeEngineControl) StartPayload(ctx context.Context, parent eth.L2BlockRef, attrs *eth.PayloadAttributes, updateSafe bool) (errType derive.BlockInsertionErrType, err error) {
if m.err != nil {
return m.errTyp, m.err
}
m.buildingID = eth.PayloadID{}
_, _ = crand.Read(m.buildingID[:])
m.buildingOnto = parent
m.buildingSafe = updateSafe
m.buildingAttrs = attrs
m.buildingStart = m.timeNow()
return derive.BlockInsertOK, nil
}
func (m *FakeEngineControl) ConfirmPayload(ctx context.Context) (out *eth.ExecutionPayload, errTyp derive.BlockInsertionErrType, err error) {
if m.err != nil {
return nil, m.errTyp, m.err
}
buildTime := m.timeNow().Sub(m.buildingStart)
m.totalBuildingTime += buildTime
m.totalBuiltBlocks += 1
payload := m.makePayload(m.buildingOnto, m.buildingAttrs)
ref, err := derive.PayloadToBlockRef(payload, &m.cfg.Genesis)
if err != nil {
panic(err)
}
m.unsafe = ref
if m.buildingSafe {
m.safe = ref
}
m.resetBuildingState()
m.totalTxs += len(payload.Transactions)
return payload, derive.BlockInsertOK, nil
}
func (m *FakeEngineControl) CancelPayload(ctx context.Context, force bool) error {
if force {
m.resetBuildingState()
}
return m.err
}
func (m *FakeEngineControl) BuildingPayload() (onto eth.L2BlockRef, id eth.PayloadID, safe bool) {
return m.buildingOnto, m.buildingID, m.buildingSafe
}
func (m *FakeEngineControl) Finalized() eth.L2BlockRef {
return m.finalized
}
func (m *FakeEngineControl) UnsafeL2Head() eth.L2BlockRef {
return m.unsafe
}
func (m *FakeEngineControl) SafeL2Head() eth.L2BlockRef {
return m.safe
}
func (m *FakeEngineControl) resetBuildingState() {
m.buildingID = eth.PayloadID{}
m.buildingOnto = eth.L2BlockRef{}
m.buildingSafe = false
m.buildingAttrs = nil
}
func (m *FakeEngineControl) Reset() {
m.err = nil
}
var _ derive.ResettableEngineControl = (*FakeEngineControl)(nil)
type testAttrBuilderFn func(ctx context.Context, l2Parent eth.L2BlockRef, epoch eth.BlockID) (attrs *eth.PayloadAttributes, err error)
func (fn testAttrBuilderFn) PreparePayloadAttributes(ctx context.Context, l2Parent eth.L2BlockRef, epoch eth.BlockID) (attrs *eth.PayloadAttributes, err error) {
return fn(ctx, l2Parent, epoch)
}
var _ derive.AttributesBuilder = (testAttrBuilderFn)(nil)
type testOriginSelectorFn func(ctx context.Context, l2Head eth.L2BlockRef) (eth.L1BlockRef, error)
func (fn testOriginSelectorFn) FindL1Origin(ctx context.Context, l2Head eth.L2BlockRef) (eth.L1BlockRef, error) {
return fn(ctx, l2Head)
}
var _ L1OriginSelectorIface = (testOriginSelectorFn)(nil)
// TestSequencerChaosMonkey runs the sequencer in a mocked adversarial environment with
// repeated random errors in dependencies and poor clock timing.
// At the end the health of the chain is checked to show that the sequencer kept the chain in shape.
func TestSequencerChaosMonkey(t *testing.T) {
mockL1Hash := func(num uint64) (out common.Hash) {
out[31] = 1
binary.BigEndian.PutUint64(out[:], num)
return
}
mockL2Hash := func(num uint64) (out common.Hash) {
out[31] = 2
binary.BigEndian.PutUint64(out[:], num)
return
}
mockL1ID := func(num uint64) eth.BlockID {
return eth.BlockID{Hash: mockL1Hash(num), Number: num}
}
mockL2ID := func(num uint64) eth.BlockID {
return eth.BlockID{Hash: mockL2Hash(num), Number: num}
}
rng := rand.New(rand.NewSource(12345))
l1Time := uint64(100000)
// mute errors. We expect a lot of the mocked errors to cause error-logs. We check chain health at the end of the test.
log := testlog.Logger(t, log.LvlCrit)
cfg := &rollup.Config{
Genesis: rollup.Genesis{
L1: mockL1ID(100000),
L2: mockL2ID(200000),
L2Time: l1Time + 300, // L2 may start with a relative old L1 origin and will have to catch it up
SystemConfig: eth.SystemConfig{},
},
BlockTime: 2,
MaxSequencerDrift: 30,
}
// keep track of the L1 timestamps we mock because sometimes we only have the L1 hash/num handy
l1Times := map[eth.BlockID]uint64{cfg.Genesis.L1: l1Time}
genesisL2 := eth.L2BlockRef{
Hash: cfg.Genesis.L2.Hash,
Number: cfg.Genesis.L2.Number,
ParentHash: mockL2Hash(cfg.Genesis.L2.Number - 1),
Time: cfg.Genesis.L2Time,
L1Origin: cfg.Genesis.L1,
SequenceNumber: 0,
}
// initialize our engine state
engControl := &FakeEngineControl{
finalized: genesisL2,
safe: genesisL2,
unsafe: genesisL2,
cfg: cfg,
}
// start wallclock at 5 minutes after the current L2 head. The sequencer has some catching up to do!
clockTime := time.Unix(int64(engControl.unsafe.Time)+5*60, 0)
clockFn := func() time.Time {
return clockTime
}
engControl.timeNow = clockFn
// mock payload building, we don't need to process any real txs.
engControl.makePayload = func(onto eth.L2BlockRef, attrs *eth.PayloadAttributes) *eth.ExecutionPayload {
txs := make([]eth.Data, 0)
txs = append(txs, attrs.Transactions...) // include deposits
if !attrs.NoTxPool { // if we are allowed to sequence from tx pool, mock some txs
n := rng.Intn(20)
for i := 0; i < n; i++ {
txs = append(txs, []byte(fmt.Sprintf("mock sequenced tx %d", i)))
}
}
return ð.ExecutionPayload{
ParentHash: onto.Hash,
BlockNumber: eth.Uint64Quantity(onto.Number) + 1,
Timestamp: attrs.Timestamp,
BlockHash: mockL2Hash(onto.Number),
Transactions: txs,
}
}
// We keep attribute building simple, we don't talk to a real execution engine in this test.
// Sometimes we fake an error in the attributes preparation.
var attrsErr error
attrBuilder := testAttrBuilderFn(func(ctx context.Context, l2Parent eth.L2BlockRef, epoch eth.BlockID) (attrs *eth.PayloadAttributes, err error) {
if attrsErr != nil {
return nil, attrsErr
}
seqNr := l2Parent.SequenceNumber + 1
if epoch != l2Parent.L1Origin {
seqNr = 0
}
l1Info := &testutils.MockBlockInfo{
InfoHash: epoch.Hash,
InfoParentHash: mockL1Hash(epoch.Number - 1),
InfoCoinbase: common.Address{},
InfoRoot: common.Hash{},
InfoNum: epoch.Number,
InfoTime: l1Times[epoch],
InfoMixDigest: [32]byte{},
InfoBaseFee: big.NewInt(1234),
InfoReceiptRoot: common.Hash{},
}
infoDep, err := derive.L1InfoDepositBytes(seqNr, l1Info, cfg.Genesis.SystemConfig, false)
require.NoError(t, err)
testGasLimit := eth.Uint64Quantity(10_000_000)
return ð.PayloadAttributes{
Timestamp: eth.Uint64Quantity(l2Parent.Time + cfg.BlockTime),
PrevRandao: eth.Bytes32{},
SuggestedFeeRecipient: common.Address{},
Transactions: []eth.Data{infoDep},
NoTxPool: false,
GasLimit: &testGasLimit,
}, nil
})
maxL1BlockTimeGap := uint64(100)
// The origin selector just generates random L1 blocks based on RNG
var originErr error
originSelector := testOriginSelectorFn(func(ctx context.Context, l2Head eth.L2BlockRef) (eth.L1BlockRef, error) {
if originErr != nil {
return eth.L1BlockRef{}, originErr
}
origin := eth.L1BlockRef{
Hash: mockL1Hash(l2Head.L1Origin.Number),
Number: l2Head.L1Origin.Number,
ParentHash: mockL1Hash(l2Head.L1Origin.Number),
Time: l1Times[l2Head.L1Origin],
}
// randomly make a L1 origin appear, if we can even select it
nextL2Time := l2Head.Time + cfg.BlockTime
if nextL2Time <= origin.Time {
return origin, nil
}
maxTimeIncrement := nextL2Time - origin.Time
if maxTimeIncrement > maxL1BlockTimeGap {
maxTimeIncrement = maxL1BlockTimeGap
}
if rng.Intn(10) == 0 {
nextOrigin := eth.L1BlockRef{
Hash: mockL1Hash(origin.Number + 1),
Number: origin.Number + 1,
ParentHash: origin.Hash,
Time: origin.Time + 1 + uint64(rng.Int63n(int64(maxTimeIncrement))),
}
l1Times[nextOrigin.ID()] = nextOrigin.Time
return nextOrigin, nil
} else {
return origin, nil
}
})
seq := NewSequencer(log, cfg, engControl, attrBuilder, originSelector, metrics.NoopMetrics)
seq.timeNow = clockFn
// try to build 1000 blocks, with 5x as many planning attempts, to handle errors and clock problems
desiredBlocks := 1000
for i := 0; i < 5*desiredBlocks && engControl.totalBuiltBlocks < desiredBlocks; i++ {
delta := seq.PlanNextSequencerAction()
x := rng.Float32()
if x < 0.01 { // 1%: mess a lot with the clock: simulate a hang of up to 30 seconds
if i < desiredBlocks/2 { // only in first 50% of blocks to let it heal, hangs take time
delta = time.Duration(rng.Float64() * float64(time.Second*30))
}
} else if x < 0.1 { // 9%: mess with the timing, -50% to 50% off
delta = time.Duration((0.5 + rng.Float64()) * float64(delta))
} else if x < 0.5 {
// 40%: mess slightly with the timing, -10% to 10% off
delta = time.Duration((0.9 + rng.Float64()*0.2) * float64(delta))
}
clockTime = clockTime.Add(delta)
// reset errors
originErr = nil
attrsErr = nil
if engControl.err != mockResetErr { // the mockResetErr requires the sequencer to Reset() to recover.
engControl.err = nil
}
engControl.errTyp = derive.BlockInsertOK
// maybe make something maybe fail, or try a new L1 origin
switch rng.Intn(20) { // 9/20 = 45% chance to fail sequencer action (!!!)
case 0, 1:
originErr = errors.New("mock origin error")
case 2, 3:
attrsErr = errors.New("mock attributes error")
case 4, 5:
engControl.err = errors.New("mock temporary engine error")
engControl.errTyp = derive.BlockInsertTemporaryErr
case 6, 7:
engControl.err = errors.New("mock prestate engine error")
engControl.errTyp = derive.BlockInsertPrestateErr
case 8:
engControl.err = mockResetErr
default:
// no error
}
payload, err := seq.RunNextSequencerAction(context.Background())
require.NoError(t, err)
if payload != nil {
require.Equal(t, engControl.UnsafeL2Head().ID(), payload.ID(), "head must stay in sync with emitted payloads")
var tx types.Transaction
require.NoError(t, tx.UnmarshalBinary(payload.Transactions[0]))
info, err := derive.L1InfoDepositTxData(tx.Data())
require.NoError(t, err)
require.GreaterOrEqual(t, uint64(payload.Timestamp), info.Time, "ensure L2 time >= L1 time")
}
}
// Now, even though:
// - the start state was behind the wallclock
// - the L1 origin was far behind the L2
// - we made all components fail at random
// - messed with the clock
// the L2 chain was still built and stats are healthy on average!
l2Head := engControl.UnsafeL2Head()
t.Logf("avg build time: %s, clock timestamp: %d, L2 head time: %d, L1 origin time: %d, avg txs per block: %f", engControl.avgBuildingTime(), clockFn().Unix(), l2Head.Time, l1Times[l2Head.L1Origin], engControl.avgTxsPerBlock())
require.Equal(t, engControl.totalBuiltBlocks, desiredBlocks, "persist through random errors and build the desired blocks")
require.Equal(t, l2Head.Time, cfg.Genesis.L2Time+uint64(desiredBlocks)*cfg.BlockTime, "reached desired L2 block timestamp")
require.GreaterOrEqual(t, l2Head.Time, l1Times[l2Head.L1Origin], "the L2 time >= the L1 time")
require.Less(t, l2Head.Time-l1Times[l2Head.L1Origin], uint64(100), "The L1 origin time is close to the L2 time")
require.Less(t, clockTime.Sub(time.Unix(int64(l2Head.Time), 0)).Abs(), 2*time.Second, "L2 time is accurate, within 2 seconds of wallclock")
require.Greater(t, engControl.avgBuildingTime(), time.Second, "With 2 second block time and 1 second error backoff and healthy-on-average errors, building time should at least be a second")
require.Greater(t, engControl.avgTxsPerBlock(), 3.0, "We expect at least 1 system tx per block, but with a mocked 0-10 txs we expect an higher avg")
}