hermez-node/coordinator/coordinator_test.go
coordinator/coordinator_test.goRaw
1package coordinator
2
3import (
4 "context"
5 "fmt"
6 "io/ioutil"
7 "math/big"
8 "os"
9 "sync"
10 "testing"
11 "time"
12
13 ethCommon "github.com/ethereum/go-ethereum/common"
14 "github.com/hermeznetwork/hermez-node/batchbuilder"
15 "github.com/hermeznetwork/hermez-node/common"
16 dbUtils "github.com/hermeznetwork/hermez-node/db"
17 "github.com/hermeznetwork/hermez-node/db/historydb"
18 "github.com/hermeznetwork/hermez-node/db/l2db"
19 "github.com/hermeznetwork/hermez-node/db/statedb"
20 "github.com/hermeznetwork/hermez-node/eth"
21 "github.com/hermeznetwork/hermez-node/log"
22 "github.com/hermeznetwork/hermez-node/prover"
23 "github.com/hermeznetwork/hermez-node/synchronizer"
24 "github.com/hermeznetwork/hermez-node/test"
25 "github.com/hermeznetwork/hermez-node/txprocessor"
26 "github.com/hermeznetwork/hermez-node/txselector"
27 "github.com/hermeznetwork/tracerr"
28 "github.com/iden3/go-iden3-crypto/babyjub"
29 "github.com/iden3/go-merkletree/db/pebble"
30 "github.com/stretchr/testify/assert"
31 "github.com/stretchr/testify/require"
32)
33
34var deleteme = []string{}
35
36func pebbleMakeCheckpoint(source, dest string) error {
37 // Remove dest folder (if it exists) before doing the checkpoint
38 if _, err := os.Stat(dest); !os.IsNotExist(err) {
39 err := os.RemoveAll(dest)
40 if err != nil {
41 return tracerr.Wrap(err)
42 }
43 } else if err != nil && !os.IsNotExist(err) {
44 return tracerr.Wrap(err)
45 }
46
47 sto, err := pebble.NewPebbleStorage(source, false)
48 if err != nil {
49 return tracerr.Wrap(err)
50 }
51 defer func() {
52 errClose := sto.Pebble().Close()
53 if errClose != nil {
54 log.Errorw("Pebble.Close", "err", errClose)
55 }
56 }()
57
58 // execute Checkpoint
59 err = sto.Pebble().Checkpoint(dest)
60 if err != nil {
61 return tracerr.Wrap(err)
62 }
63
64 return nil
65}
66
67func TestMain(m *testing.M) {
68 exitVal := m.Run()
69 for _, dir := range deleteme {
70 if err := os.RemoveAll(dir); err != nil {
71 panic(err)
72 }
73 }
74 os.Exit(exitVal)
75}
76
77var syncDBPath string
78var txSelDBPath string
79var batchBuilderDBPath string
80
81type modules struct {
82 historyDB *historydb.HistoryDB
83 l2DB *l2db.L2DB
84 txSelector *txselector.TxSelector
85 batchBuilder *batchbuilder.BatchBuilder
86 stateDB *statedb.StateDB
87}
88
89var maxL1Txs uint64 = 256
90var maxTxs uint64 = 376
91var nLevels uint32 = 32 //nolint:deadcode,unused
92var maxFeeTxs uint32 = 64 //nolint:deadcode,varcheck
93var chainID uint16 = 0
94
95func newTestModules(t *testing.T) modules {
96 var err error
97 syncDBPath, err = ioutil.TempDir("", "tmpSyncDB")
98 require.NoError(t, err)
99 deleteme = append(deleteme, syncDBPath)
100 syncStateDB, err := statedb.NewStateDB(syncDBPath, 128, statedb.TypeSynchronizer, 48)
101 assert.NoError(t, err)
102
103 pass := os.Getenv("POSTGRES_PASS")
104 db, err := dbUtils.InitSQLDB(5432, "localhost", "hermez", pass, "hermez")
105 require.NoError(t, err)
106 test.WipeDB(db)
107 l2DB := l2db.NewL2DB(db, 10, 100, 24*time.Hour)
108 historyDB := historydb.NewHistoryDB(db)
109
110 txSelDBPath, err = ioutil.TempDir("", "tmpTxSelDB")
111 require.NoError(t, err)
112 deleteme = append(deleteme, txSelDBPath)
113
114 var bjj babyjub.PublicKeyComp
115 err = bjj.UnmarshalText([]byte("c433f7a696b7aa3a5224efb3993baf0ccd9e92eecee0c29a3f6c8208a9e81d9e"))
116 require.NoError(t, err)
117 coordAccount := &txselector.CoordAccount{ // TODO TMP
118 Addr: ethCommon.HexToAddress("0xc58d29fA6e86E4FAe04DDcEd660d45BCf3Cb2370"),
119 BJJ: bjj,
120 AccountCreationAuth: nil,
121 }
122 txSelector, err := txselector.NewTxSelector(coordAccount, txSelDBPath, syncStateDB, l2DB)
123 assert.NoError(t, err)
124
125 batchBuilderDBPath, err = ioutil.TempDir("", "tmpBatchBuilderDB")
126 require.NoError(t, err)
127 deleteme = append(deleteme, batchBuilderDBPath)
128 batchBuilder, err := batchbuilder.NewBatchBuilder(batchBuilderDBPath, syncStateDB, 0, uint64(nLevels))
129 assert.NoError(t, err)
130
131 return modules{
132 historyDB: historyDB,
133 l2DB: l2DB,
134 txSelector: txSelector,
135 batchBuilder: batchBuilder,
136 stateDB: syncStateDB,
137 }
138}
139
140type timer struct {
141 time int64
142}
143
144func (t *timer) Time() int64 {
145 currentTime := t.time
146 t.time++
147 return currentTime
148}
149
150var bidder = ethCommon.HexToAddress("0x6b175474e89094c44da98b954eedeac495271d0f")
151var forger = ethCommon.HexToAddress("0xc344E203a046Da13b0B4467EB7B3629D0C99F6E6")
152
153func newTestCoordinator(t *testing.T, forgerAddr ethCommon.Address, ethClient *test.Client,
154 ethClientSetup *test.ClientSetup, modules modules) *Coordinator {
155 debugBatchPath, err := ioutil.TempDir("", "tmpDebugBatch")
156 require.NoError(t, err)
157 deleteme = append(deleteme, debugBatchPath)
158
159 conf := Config{
160 ForgerAddress: forgerAddr,
161 ConfirmBlocks: 5,
162 L1BatchTimeoutPerc: 0.5,
163 EthClientAttempts: 5,
164 SyncRetryInterval: 400 * time.Microsecond,
165 EthClientAttemptsDelay: 100 * time.Millisecond,
166 TxManagerCheckInterval: 300 * time.Millisecond,
167 DebugBatchPath: debugBatchPath,
168 Purger: PurgerCfg{
169 PurgeBatchDelay: 10,
170 PurgeBlockDelay: 10,
171 InvalidateBatchDelay: 4,
172 InvalidateBlockDelay: 4,
173 },
174 TxProcessorConfig: txprocessor.Config{
175 NLevels: nLevels,
176 MaxFeeTx: maxFeeTxs,
177 MaxTx: uint32(maxTxs),
178 MaxL1Tx: uint32(maxL1Txs),
179 ChainID: chainID,
180 },
181 VerifierIdx: 0,
182 }
183
184 serverProofs := []prover.Client{
185 &prover.MockClient{Delay: 300 * time.Millisecond},
186 &prover.MockClient{Delay: 400 * time.Millisecond},
187 }
188
189 scConsts := &synchronizer.SCConsts{
190 Rollup: *ethClientSetup.RollupConstants,
191 Auction: *ethClientSetup.AuctionConstants,
192 WDelayer: *ethClientSetup.WDelayerConstants,
193 }
194 initSCVars := &synchronizer.SCVariables{
195 Rollup: *ethClientSetup.RollupVariables,
196 Auction: *ethClientSetup.AuctionVariables,
197 WDelayer: *ethClientSetup.WDelayerVariables,
198 }
199 coord, err := NewCoordinator(conf, modules.historyDB, modules.l2DB, modules.txSelector,
200 modules.batchBuilder, serverProofs, ethClient, scConsts, initSCVars)
201 require.NoError(t, err)
202 return coord
203}
204
205func newTestSynchronizer(t *testing.T, ethClient *test.Client, ethClientSetup *test.ClientSetup,
206 modules modules) *synchronizer.Synchronizer {
207 sync, err := synchronizer.NewSynchronizer(ethClient, modules.historyDB, modules.stateDB,
208 synchronizer.Config{
209 StatsRefreshPeriod: 0 * time.Second,
210 })
211 require.NoError(t, err)
212 return sync
213}
214
215// TestCoordinatorFlow is a test where the coordinator is stared (which means
216// that goroutines are spawned), and ethereum blocks are mined via the
217// test.Client to simulate starting and stopping forging times. This test
218// works without a synchronizer, and no l2txs are inserted in the pool, so all
219// the batches are forged empty. The purpose of this test is to manually
220// observe via the logs that nothing crashes and that the coordinator starts
221// and stops forging at the right blocks.
222func TestCoordinatorFlow(t *testing.T) {
223 if os.Getenv("TEST_COORD_FLOW") == "" {
224 return
225 }
226 ethClientSetup := test.NewClientSetupExample()
227 ethClientSetup.ChainID = big.NewInt(int64(chainID))
228 var timer timer
229 ethClient := test.NewClient(true, &timer, &bidder, ethClientSetup)
230 modules := newTestModules(t)
231 coord := newTestCoordinator(t, forger, ethClient, ethClientSetup, modules)
232
233 // Bid for slot 2 and 4
234 _, err := ethClient.AuctionSetCoordinator(forger, "https://foo.bar")
235 require.NoError(t, err)
236 bid, ok := new(big.Int).SetString("11000000000000000000", 10)
237 if !ok {
238 panic("bad bid")
239 }
240 _, err = ethClient.AuctionBidSimple(3, bid)
241 require.NoError(t, err)
242 _, err = ethClient.AuctionBidSimple(5, bid)
243 require.NoError(t, err)
244
245 coord.Start()
246 ctx := context.Background()
247 time.Sleep(1 * time.Second)
248
249 waitForSlot := func(slot int64) {
250 for {
251 blockNum, err := ethClient.EthLastBlock()
252 require.NoError(t, err)
253 nextBlockSlot, err := ethClient.AuctionGetSlotNumber(blockNum + 1)
254 require.NoError(t, err)
255 if nextBlockSlot == slot {
256 break
257 }
258 ethClient.CtlMineBlock()
259 time.Sleep(100 * time.Millisecond)
260 var stats synchronizer.Stats
261 stats.Eth.LastBlock = *ethClient.CtlLastBlock()
262 stats.Sync.LastBlock = stats.Eth.LastBlock
263 stats.Eth.LastBatch = ethClient.CtlLastForgedBatch()
264 stats.Sync.LastBatch = stats.Eth.LastBatch
265 canForge, err := ethClient.AuctionCanForge(forger, blockNum+1)
266 require.NoError(t, err)
267 var slot common.Slot
268 slotNum := ethClientSetup.AuctionConstants.SlotNum(blockNum + 1)
269 slot.StartBlock = ethClientSetup.AuctionConstants.GenesisBlockNum +
270 (slotNum)*int64(ethClientSetup.AuctionConstants.BlocksPerSlot)
271 slot.EndBlock = ethClientSetup.AuctionConstants.GenesisBlockNum +
272 (slotNum+1)*int64(ethClientSetup.AuctionConstants.BlocksPerSlot)
273 if canForge {
274 slot.Forger = forger
275 }
276 stats.Sync.Auction.CurrentSlot = slot
277
278 // Copy stateDB to synchronizer if there was a new batch
279 source := fmt.Sprintf("%v/BatchNum%v", batchBuilderDBPath, stats.Sync.LastBatch)
280 dest := fmt.Sprintf("%v/BatchNum%v", syncDBPath, stats.Sync.LastBatch)
281 if stats.Sync.LastBatch != 0 {
282 if _, err := os.Stat(dest); os.IsNotExist(err) {
283 log.Infow("Making pebble checkpoint for sync",
284 "source", source, "dest", dest)
285 err = pebbleMakeCheckpoint(source, dest)
286 require.NoError(t, err)
287 }
288 }
289 coord.SendMsg(ctx, MsgSyncBlock{
290 Stats: stats,
291 })
292 }
293 }
294
295 // NOTE: With the current test, the coordinator will enter in forge
296 // time before the bidded slot because no one else is forging in the
297 // other slots before the slot deadline.
298 // simulate forgeSequence time
299 waitForSlot(2)
300 log.Info("~~~ simulate entering in forge time")
301 time.Sleep(1 * time.Second)
302
303 // simulate going out from forgeSequence
304 waitForSlot(3)
305 log.Info("~~~ simulate going out from forge time")
306 time.Sleep(1 * time.Second)
307
308 // simulate entering forgeSequence time again
309 waitForSlot(4)
310 log.Info("~~~ simulate entering in forge time again")
311 time.Sleep(2 * time.Second)
312
313 // simulate stopping forgerLoop by channel
314 log.Info("~~~ simulate stopping forgerLoop by closing coordinator stopch")
315 coord.Stop()
316 time.Sleep(1 * time.Second)
317}
318
319func TestCoordinatorStartStop(t *testing.T) {
320 ethClientSetup := test.NewClientSetupExample()
321 ethClientSetup.ChainID = big.NewInt(int64(chainID))
322 var timer timer
323 ethClient := test.NewClient(true, &timer, &bidder, ethClientSetup)
324 modules := newTestModules(t)
325 coord := newTestCoordinator(t, forger, ethClient, ethClientSetup, modules)
326 coord.Start()
327 coord.Stop()
328}
329
330func TestCoordCanForge(t *testing.T) {
331 ethClientSetup := test.NewClientSetupExample()
332 ethClientSetup.ChainID = big.NewInt(int64(chainID))
333 bootForger := ethClientSetup.AuctionVariables.BootCoordinator
334
335 var timer timer
336 ethClient := test.NewClient(true, &timer, &bidder, ethClientSetup)
337 modules := newTestModules(t)
338 coord := newTestCoordinator(t, forger, ethClient, ethClientSetup, modules)
339 _, err := ethClient.AuctionSetCoordinator(forger, "https://foo.bar")
340 require.NoError(t, err)
341 bid, ok := new(big.Int).SetString("12000000000000000000", 10)
342 if !ok {
343 panic("bad bid")
344 }
345 _, err = ethClient.AuctionBidSimple(3, bid)
346 require.NoError(t, err)
347
348 modules2 := newTestModules(t)
349 bootCoord := newTestCoordinator(t, bootForger, ethClient, ethClientSetup, modules2)
350
351 assert.Equal(t, forger, coord.cfg.ForgerAddress)
352 assert.Equal(t, bootForger, bootCoord.cfg.ForgerAddress)
353 ethBootCoord, err := ethClient.AuctionGetBootCoordinator()
354 require.NoError(t, err)
355 assert.Equal(t, &bootForger, ethBootCoord)
356
357 var stats synchronizer.Stats
358
359 slots := [4]common.Slot{}
360 for i := 0; i < 4; i++ {
361 slots[i].StartBlock = ethClientSetup.AuctionConstants.GenesisBlockNum +
362 int64(i)*int64(ethClientSetup.AuctionConstants.BlocksPerSlot)
363 slots[i].EndBlock = ethClientSetup.AuctionConstants.GenesisBlockNum +
364 int64(i+1)*int64(ethClientSetup.AuctionConstants.BlocksPerSlot)
365 // Only slot 3 has Coordinator winner, the rest are BootCoordinator
366 if i == 3 {
367 slots[i].Forger = forger
368 } else {
369 slots[i].Forger = bootForger
370 }
371 }
372
373 // Slot 0. No bid, so the winner is the boot coordinator
374 stats.Eth.LastBlock.Num = ethClientSetup.AuctionConstants.GenesisBlockNum
375 stats.Sync.LastBlock = stats.Eth.LastBlock
376 stats.Sync.Auction.CurrentSlot = slots[0]
377 coord.stats = stats
378 bootCoord.stats = stats
379 assert.Equal(t, false, coord.canForge())
380 assert.Equal(t, true, bootCoord.canForge())
381
382 // Slot 0. No bid, and we reach the deadline, so anyone can forge
383 stats.Eth.LastBlock.Num = ethClientSetup.AuctionConstants.GenesisBlockNum +
384 int64(ethClientSetup.AuctionVariables.SlotDeadline)
385 stats.Sync.LastBlock = stats.Eth.LastBlock
386 stats.Sync.Auction.CurrentSlot = slots[0]
387 coord.stats = stats
388 bootCoord.stats = stats
389 assert.Equal(t, true, coord.canForge())
390 assert.Equal(t, true, bootCoord.canForge())
391
392 // Slot 3. coordinator bid, so the winner is the coordinator
393 stats.Eth.LastBlock.Num = ethClientSetup.AuctionConstants.GenesisBlockNum +
394 3*int64(ethClientSetup.AuctionConstants.BlocksPerSlot)
395 stats.Sync.LastBlock = stats.Eth.LastBlock
396 stats.Sync.Auction.CurrentSlot = slots[3]
397 coord.stats = stats
398 bootCoord.stats = stats
399 assert.Equal(t, true, coord.canForge())
400 assert.Equal(t, false, bootCoord.canForge())
401}
402
403func TestCoordHandleMsgSyncBlock(t *testing.T) {
404 ethClientSetup := test.NewClientSetupExample()
405 ethClientSetup.ChainID = big.NewInt(int64(chainID))
406 bootForger := ethClientSetup.AuctionVariables.BootCoordinator
407
408 var timer timer
409 ethClient := test.NewClient(true, &timer, &bidder, ethClientSetup)
410 modules := newTestModules(t)
411 coord := newTestCoordinator(t, forger, ethClient, ethClientSetup, modules)
412 _, err := ethClient.AuctionSetCoordinator(forger, "https://foo.bar")
413 require.NoError(t, err)
414 bid, ok := new(big.Int).SetString("11000000000000000000", 10)
415 if !ok {
416 panic("bad bid")
417 }
418 _, err = ethClient.AuctionBidSimple(3, bid)
419 require.NoError(t, err)
420
421 slots := [4]common.Slot{}
422 for i := 0; i < 4; i++ {
423 slots[i].StartBlock = ethClientSetup.AuctionConstants.GenesisBlockNum +
424 int64(i)*int64(ethClientSetup.AuctionConstants.BlocksPerSlot)
425 slots[i].EndBlock = ethClientSetup.AuctionConstants.GenesisBlockNum +
426 int64(i+1)*int64(ethClientSetup.AuctionConstants.BlocksPerSlot)
427 // Only slot 3 has Coordinator winner, the rest are BootCoordinator
428 if i == 3 {
429 slots[i].Forger = forger
430 } else {
431 slots[i].Forger = bootForger
432 }
433 }
434
435 var msg MsgSyncBlock
436 coord.stats = msg.Stats
437 ctx := context.Background()
438
439 // Slot 0. No bid, so the winner is the boot coordinator
440 // pipelineStarted: false -> false
441 coord.stats.Eth.LastBlock.Num = ethClientSetup.AuctionConstants.GenesisBlockNum
442 coord.stats.Sync.LastBlock = coord.stats.Eth.LastBlock
443 coord.stats.Sync.Auction.CurrentSlot = slots[0]
444 assert.Equal(t, false, coord.canForge())
445 msg.Stats = coord.stats
446 require.NoError(t, coord.handleMsgSyncBlock(ctx, &msg))
447 assert.Nil(t, coord.pipeline)
448
449 // Slot 0. No bid, and we reach the deadline, so anyone can forge
450 // pipelineStarted: false -> true
451 coord.stats.Eth.LastBlock.Num = ethClientSetup.AuctionConstants.GenesisBlockNum +
452 int64(ethClientSetup.AuctionVariables.SlotDeadline)
453 coord.stats.Sync.LastBlock = coord.stats.Eth.LastBlock
454 coord.stats.Sync.Auction.CurrentSlot = slots[0]
455 assert.Equal(t, true, coord.canForge())
456 msg.Stats = coord.stats
457 require.NoError(t, coord.handleMsgSyncBlock(ctx, &msg))
458 assert.NotNil(t, coord.pipeline)
459
460 // Slot 0. No bid, and we reach the deadline, so anyone can forge
461 // pipelineStarted: true -> true
462 coord.stats.Eth.LastBlock.Num = ethClientSetup.AuctionConstants.GenesisBlockNum +
463 int64(ethClientSetup.AuctionVariables.SlotDeadline) + 1
464 coord.stats.Sync.LastBlock = coord.stats.Eth.LastBlock
465 coord.stats.Sync.Auction.CurrentSlot = slots[0]
466 assert.Equal(t, true, coord.canForge())
467 msg.Stats = coord.stats
468 require.NoError(t, coord.handleMsgSyncBlock(ctx, &msg))
469 assert.NotNil(t, coord.pipeline)
470
471 // Slot 1. No bid, so the winner is the boot coordinator
472 // pipelineStarted: true -> false
473 coord.stats.Eth.LastBlock.Num = ethClientSetup.AuctionConstants.GenesisBlockNum +
474 1*int64(ethClientSetup.AuctionConstants.BlocksPerSlot)
475 coord.stats.Sync.LastBlock = coord.stats.Eth.LastBlock
476 coord.stats.Sync.Auction.CurrentSlot = slots[1]
477 assert.Equal(t, false, coord.canForge())
478 msg.Stats = coord.stats
479 require.NoError(t, coord.handleMsgSyncBlock(ctx, &msg))
480 assert.Nil(t, coord.pipeline)
481}
482
483// ethAddTokens adds the tokens from the blocks to the blockchain
484func ethAddTokens(blocks []common.BlockData, client *test.Client) {
485 for _, block := range blocks {
486 for _, token := range block.Rollup.AddedTokens {
487 consts := eth.ERC20Consts{
488 Name: fmt.Sprintf("Token %d", token.TokenID),
489 Symbol: fmt.Sprintf("TK%d", token.TokenID),
490 Decimals: 18,
491 }
492 // tokenConsts[token.TokenID] = consts
493 client.CtlAddERC20(token.EthAddr, consts)
494 }
495 }
496}
497
498func TestCoordinatorStress(t *testing.T) {
499 if os.Getenv("TEST_COORD_STRESS") == "" {
500 return
501 }
502 log.Info("Begin Test Coord Stress")
503 ethClientSetup := test.NewClientSetupExample()
504 ethClientSetup.ChainID = big.NewInt(int64(chainID))
505 var timer timer
506 ethClient := test.NewClient(true, &timer, &bidder, ethClientSetup)
507 modules := newTestModules(t)
508 coord := newTestCoordinator(t, forger, ethClient, ethClientSetup, modules)
509 syn := newTestSynchronizer(t, ethClient, ethClientSetup, modules)
510
511 coord.Start()
512 ctx, cancel := context.WithCancel(context.Background())
513 var wg sync.WaitGroup
514
515 // Synchronizer loop
516 wg.Add(1)
517 go func() {
518 for {
519 blockData, _, err := syn.Sync2(ctx, nil)
520 if ctx.Err() != nil {
521 wg.Done()
522 return
523 }
524 require.NoError(t, err)
525 if blockData != nil {
526 stats := syn.Stats()
527 coord.SendMsg(ctx, MsgSyncBlock{
528 Stats: *stats,
529 Batches: blockData.Rollup.Batches,
530 Vars: synchronizer.SCVariablesPtr{
531 Rollup: blockData.Rollup.Vars,
532 Auction: blockData.Auction.Vars,
533 WDelayer: blockData.WDelayer.Vars,
534 },
535 })
536 } else {
537 time.Sleep(100 * time.Millisecond)
538 }
539 }
540 }()
541
542 // Blockchain mining loop
543 wg.Add(1)
544 go func() {
545 for {
546 select {
547 case <-ctx.Done():
548 wg.Done()
549 return
550 case <-time.After(1 * time.Second):
551 ethClient.CtlMineBlock()
552 }
553 }
554 }()
555
556 time.Sleep(600 * time.Second)
557
558 cancel()
559 wg.Wait()
560 coord.Stop()
561}
562
563// TODO: Test Reorg
564// TODO: Test TxMonitor
565// TODO: Test forgeBatch
566// TODO: Test waitServerProof
567// TODO: Test handleReorg