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