arnaucube
arnaucube/hermez-nodePublic Self-hosted repository
hermez-node/api/api_test.go
api/api_test.goRaw
  1package api
  2
  3import (
  4	"context"
  5	"encoding/json"
  6	"errors"
  7	"fmt"
  8	"io"
  9	"io/ioutil"
 10	"math/big"
 11	"net/http"
 12	"os"
 13	"strconv"
 14	"testing"
 15	"time"
 16
 17	ethCommon "github.com/ethereum/go-ethereum/common"
 18	swagger "github.com/getkin/kin-openapi/openapi3filter"
 19	"github.com/gin-gonic/gin"
 20	"github.com/hermeznetwork/hermez-node/common"
 21	"github.com/hermeznetwork/hermez-node/db"
 22	"github.com/hermeznetwork/hermez-node/db/historydb"
 23	"github.com/hermeznetwork/hermez-node/db/l2db"
 24	"github.com/hermeznetwork/hermez-node/db/statedb"
 25	"github.com/hermeznetwork/hermez-node/log"
 26	"github.com/hermeznetwork/hermez-node/test"
 27	"github.com/hermeznetwork/hermez-node/test/til"
 28)
 29
 30// Pendinger is an interface that allows getting last returned item ID and PendingItems to be used for building fromItem
 31// when testing paginated endpoints.
 32type Pendinger interface {
 33	GetPending() (pendingItems, lastItemID uint64)
 34	Len() int
 35	New() Pendinger
 36}
 37
 38const apiPort = ":4010"
 39const apiURL = "http://localhost" + apiPort + "/"
 40
 41var SetBlockchain = `
 42	Type: Blockchain
 43
 44	AddToken(1)
 45	AddToken(2)
 46	AddToken(3)
 47	AddToken(4)
 48	AddToken(5)
 49	AddToken(6)
 50	AddToken(7)
 51	AddToken(8)
 52	> block
 53
 54	// Coordinator accounts, Idxs: 256, 257
 55	CreateAccountCoordinator(0) Coord
 56	CreateAccountCoordinator(1) Coord
 57
 58	// close Block:0, Batch:0
 59	> batch
 60
 61	CreateAccountDeposit(0) A: 11111111100000000000
 62	CreateAccountDeposit(1) C: 22222222200000000000
 63	CreateAccountCoordinator(0) C
 64
 65	// close Block:0, Batch:1
 66	> batchL1
 67	// Expected balances:
 68	//     Coord(0): 0, Coord(1): 0
 69	//     C(0): 0
 70
 71	CreateAccountDeposit(1) A: 33333333300000000000
 72
 73	// close Block:0, Batch:2
 74	> batchL1
 75
 76	// close Block:0, Batch:3
 77	> batchL1
 78
 79	CreateAccountDepositTransfer(0) B-A: 44444444400000000000, 123444444400000000000
 80
 81	// close Block:0, Batch:4
 82	> batchL1
 83	CreateAccountDeposit(0) D: 55555555500000000000
 84
 85	// close Block:0, Batch:5
 86	> batchL1
 87
 88	CreateAccountCoordinator(1) B
 89
 90	Transfer(1) A-B: 11111100000000000 (2)
 91	Transfer(0) B-C: 22222200000000000 (3)
 92
 93	// close Block:0, Batch:6
 94	> batchL1 // forge L1User{1}, forge L1Coord{2}, forge L2{2}
 95
 96	Deposit(0) C: 66666666600000000000
 97	DepositTransfer(0) C-D: 77777777700000000000, 12377777700000000000
 98
 99	Transfer(0) A-B: 33333300000000000 (111)
100	Transfer(0) C-A: 44444400000000000 (222)
101	Transfer(1) B-C: 55555500000000000 (123)
102	Exit(0) A: 66666600000000000 (44)
103
104	ForceTransfer(0) D-B: 77777700000000000
105	ForceExit(0) B: 88888800000000000
106
107	// close Block:0, Batch:7
108	> batchL1
109	> block
110
111	Transfer(0) D-A: 99999900000000000 (77)
112	Transfer(0) B-D: 12312300000000000 (55)
113
114	// close Block:1, Batch:0
115	> batchL1
116
117	CreateAccountCoordinator(0) F
118
119	CreateAccountCoordinator(0) G
120	CreateAccountCoordinator(0) H
121	CreateAccountCoordinator(0) I
122	CreateAccountCoordinator(0) J
123	CreateAccountCoordinator(0) K
124	CreateAccountCoordinator(0) L
125	CreateAccountCoordinator(0) M
126	CreateAccountCoordinator(0) N
127	CreateAccountCoordinator(0) O
128	CreateAccountCoordinator(0) P
129
130	CreateAccountCoordinator(5) G
131	CreateAccountCoordinator(5) H
132	CreateAccountCoordinator(5) I
133	CreateAccountCoordinator(5) J
134	CreateAccountCoordinator(5) K
135	CreateAccountCoordinator(5) L
136	CreateAccountCoordinator(5) M
137	CreateAccountCoordinator(5) N
138	CreateAccountCoordinator(5) O
139	CreateAccountCoordinator(5) P
140
141	CreateAccountCoordinator(2) G
142	CreateAccountCoordinator(2) H
143	CreateAccountCoordinator(2) I
144	CreateAccountCoordinator(2) J
145	CreateAccountCoordinator(2) K
146	CreateAccountCoordinator(2) L
147	CreateAccountCoordinator(2) M
148	CreateAccountCoordinator(2) N
149	CreateAccountCoordinator(2) O
150	CreateAccountCoordinator(2) P
151
152
153	> batch
154	> block
155	> batch
156	> block
157	> batch
158	> block
159`
160
161type testCommon struct {
162	blocks           []common.Block
163	tokens           []historydb.TokenWithUSD
164	batches          []testBatch
165	fullBatches      []testFullBatch
166	coordinators     []historydb.CoordinatorAPI
167	accounts         []testAccount
168	txs              []testTx
169	exits            []testExit
170	poolTxsToSend    []testPoolTxSend
171	poolTxsToReceive []testPoolTxReceive
172	auths            []testAuth
173	router           *swagger.Router
174	bids             []testBid
175	slots            []testSlot
176	auctionVars      common.AuctionVariables
177	rollupVars       common.RollupVariables
178	wdelayerVars     common.WDelayerVariables
179}
180
181var tc testCommon
182var config configAPI
183var api *API
184
185// TestMain initializes the API server, and fill HistoryDB and StateDB with fake data,
186// emulating the task of the synchronizer in order to have data to be returned
187// by the API endpoints that will be tested
188func TestMain(m *testing.M) {
189	// Initializations
190	// Swagger
191	router := swagger.NewRouter().WithSwaggerFromFile("./swagger.yml")
192	// HistoryDB
193	pass := os.Getenv("POSTGRES_PASS")
194
195	database, err := db.InitSQLDB(5432, "localhost", "hermez", pass, "hermez")
196	if err != nil {
197		panic(err)
198	}
199	hdb := historydb.NewHistoryDB(database)
200	if err != nil {
201		panic(err)
202	}
203	// StateDB
204	dir, err := ioutil.TempDir("", "tmpdb")
205	if err != nil {
206		panic(err)
207	}
208	defer func() {
209		if err := os.RemoveAll(dir); err != nil {
210			panic(err)
211		}
212	}()
213	sdb, err := statedb.NewStateDB(dir, statedb.TypeTxSelector, 0)
214	if err != nil {
215		panic(err)
216	}
217	// L2DB
218	l2DB := l2db.NewL2DB(database, 10, 100, 24*time.Hour)
219	test.WipeDB(l2DB.DB()) // this will clean HistoryDB and L2DB
220
221	// Config (smart contract constants)
222	_config := getConfigTest()
223	config = configAPI{
224		RollupConstants:   *newRollupConstants(_config.RollupConstants),
225		AuctionConstants:  _config.AuctionConstants,
226		WDelayerConstants: _config.WDelayerConstants,
227	}
228
229	// API
230	apiGin := gin.Default()
231	api, err = NewAPI(
232		true,
233		true,
234		apiGin,
235		hdb,
236		sdb,
237		l2DB,
238		&_config,
239	)
240	if err != nil {
241		panic(err)
242	}
243	// Start server
244	server := &http.Server{Addr: apiPort, Handler: apiGin}
245	go func() {
246		if err := server.ListenAndServe(); err != nil &&
247			err != http.ErrServerClosed {
248			panic(err)
249		}
250	}()
251
252	// Reset DB
253	test.WipeDB(api.h.DB())
254
255	// Genratre blockchain data with til
256	tcc := til.NewContext(common.RollupConstMaxL1UserTx)
257	tilCfgExtra := til.ConfigExtra{
258		BootCoordAddr: ethCommon.HexToAddress("0xE39fEc6224708f0772D2A74fd3f9055A90E0A9f2"),
259		CoordUser:     "Coord",
260	}
261	blocksData, err := tcc.GenerateBlocks(SetBlockchain)
262	if err != nil {
263		panic(err)
264	}
265	err = tcc.FillBlocksExtra(blocksData, &tilCfgExtra)
266	if err != nil {
267		panic(err)
268	}
269	AddAditionalInformation(blocksData)
270	// Generate L2 Txs with til
271	commonPoolTxs, err := tcc.GeneratePoolL2Txs(til.SetPoolL2MinimumFlow0)
272	if err != nil {
273		panic(err)
274	}
275
276	// Extract til generated data, and add it to HistoryDB
277	var commonBlocks []common.Block
278	var commonBatches []common.Batch
279	var commonAccounts []common.Account
280	var commonExitTree []common.ExitInfo
281	var commonL1Txs []common.L1Tx
282	var commonL2Txs []common.L2Tx
283	// Add ETH token at the beginning of the array
284	testTokens := []historydb.TokenWithUSD{}
285	ethUSD := float64(500)
286	ethNow := time.Now()
287	testTokens = append(testTokens, historydb.TokenWithUSD{
288		TokenID:     test.EthToken.TokenID,
289		EthBlockNum: test.EthToken.EthBlockNum,
290		EthAddr:     test.EthToken.EthAddr,
291		Name:        test.EthToken.Name,
292		Symbol:      test.EthToken.Symbol,
293		Decimals:    test.EthToken.Decimals,
294		USD:         &ethUSD,
295		USDUpdate:   &ethNow,
296	})
297	err = api.h.UpdateTokenValue(test.EthToken.Symbol, ethUSD)
298	if err != nil {
299		panic(err)
300	}
301	for _, block := range blocksData {
302		// Insert block into HistoryDB
303		// nolint reason: block is used as read only in the function
304		if err := api.h.AddBlockSCData(&block); err != nil { //nolint:gosec
305			panic(err)
306		}
307		// Extract data
308		commonBlocks = append(commonBlocks, block.Block)
309		for i, tkn := range block.Rollup.AddedTokens {
310			token := historydb.TokenWithUSD{
311				TokenID:     tkn.TokenID,
312				EthBlockNum: tkn.EthBlockNum,
313				EthAddr:     tkn.EthAddr,
314				Name:        tkn.Name,
315				Symbol:      tkn.Symbol,
316				Decimals:    tkn.Decimals,
317			}
318			value := float64(i + 423)
319			now := time.Now().UTC()
320			token.USD = &value
321			token.USDUpdate = &now
322			// Set value in DB
323			err = api.h.UpdateTokenValue(token.Symbol, value)
324			if err != nil {
325				panic(err)
326			}
327			testTokens = append(testTokens, token)
328		}
329		// Set USD value for tokens in DB
330		commonL1Txs = append(commonL1Txs, block.Rollup.L1UserTxs...)
331		for _, batch := range block.Rollup.Batches {
332			commonL2Txs = append(commonL2Txs, batch.L2Txs...)
333			commonAccounts = append(commonAccounts, batch.CreatedAccounts...)
334			commonBatches = append(commonBatches, batch.Batch)
335			commonExitTree = append(commonExitTree, batch.ExitTree...)
336			commonL1Txs = append(commonL1Txs, batch.L1CoordinatorTxs...)
337		}
338	}
339
340	// lastBlockNum2 := blocksData[len(blocksData)-1].Block.EthBlockNum
341
342	// Add accounts to StateDB
343	for i := 0; i < len(commonAccounts); i++ {
344		if _, err := api.s.CreateAccount(commonAccounts[i].Idx, &commonAccounts[i]); err != nil {
345			panic(err)
346		}
347	}
348
349	// Generate Coordinators and add them to HistoryDB
350	const nCoords = 10
351	commonCoords := test.GenCoordinators(nCoords, commonBlocks)
352	if err := api.h.AddCoordinators(commonCoords); err != nil {
353		panic(err)
354	}
355
356	// Generate Bids and add them to HistoryDB
357	const nBids = 20
358	commonBids := test.GenBids(nBids, commonBlocks, commonCoords)
359	if err = api.h.AddBids(commonBids); err != nil {
360		panic(err)
361	}
362
363	// Generate SC vars and add them to HistoryDB (if needed)
364	var defaultSlotSetBid [6]*big.Int = [6]*big.Int{big.NewInt(10), big.NewInt(10), big.NewInt(10), big.NewInt(10), big.NewInt(10), big.NewInt(10)}
365	auctionVars := common.AuctionVariables{
366		EthBlockNum:        int64(2),
367		DonationAddress:    ethCommon.HexToAddress("0x1111111111111111111111111111111111111111"),
368		DefaultSlotSetBid:  defaultSlotSetBid,
369		Outbidding:         uint16(1),
370		SlotDeadline:       uint8(20),
371		BootCoordinator:    ethCommon.HexToAddress("0x1111111111111111111111111111111111111111"),
372		ClosedAuctionSlots: uint16(2),
373		OpenAuctionSlots:   uint16(5),
374	}
375
376	rollupVars := common.RollupVariables{
377		EthBlockNum:           int64(3),
378		FeeAddToken:           big.NewInt(100),
379		ForgeL1L2BatchTimeout: int64(44),
380		WithdrawalDelay:       uint64(3000),
381	}
382
383	wdelayerVars := common.WDelayerVariables{
384		WithdrawalDelay: uint64(3000),
385	}
386
387	err = api.h.AddAuctionVars(&auctionVars)
388	if err != nil {
389		panic(err)
390	}
391
392	// Generate test data, as expected to be received/sended from/to the API
393	testCoords := genTestCoordinators(commonCoords)
394	testBids := genTestBids(commonBlocks, testCoords, commonBids)
395	testExits := genTestExits(commonExitTree, testTokens, commonAccounts)
396	testTxs := genTestTxs(commonL1Txs, commonL2Txs, commonAccounts, testTokens, commonBlocks)
397	testBatches, testFullBatches := genTestBatches(commonBlocks, commonBatches, testTxs)
398	poolTxsToSend, poolTxsToReceive := genTestPoolTxs(commonPoolTxs, testTokens, commonAccounts)
399	tc = testCommon{
400		blocks:           commonBlocks,
401		tokens:           testTokens,
402		batches:          testBatches,
403		fullBatches:      testFullBatches,
404		coordinators:     testCoords,
405		accounts:         genTestAccounts(commonAccounts, testTokens),
406		txs:              testTxs,
407		exits:            testExits,
408		poolTxsToSend:    poolTxsToSend,
409		poolTxsToReceive: poolTxsToReceive,
410		auths:            genTestAuths(test.GenAuths(5)),
411		router:           router,
412		bids:             testBids,
413		slots: api.genTestSlots(
414			20,
415			commonBlocks[len(commonBlocks)-1].Num,
416			testBids,
417			auctionVars,
418		),
419		auctionVars:  auctionVars,
420		rollupVars:   rollupVars,
421		wdelayerVars: wdelayerVars,
422	}
423
424	// Fake server
425	if os.Getenv("FAKE_SERVER") == "yes" {
426		for {
427			log.Info("Running fake server at " + apiURL + " until ^C is received")
428			time.Sleep(30 * time.Second)
429		}
430	}
431	// Run tests
432	result := m.Run()
433	// Stop server
434	if err := server.Shutdown(context.Background()); err != nil {
435		panic(err)
436	}
437	if err := database.Close(); err != nil {
438		panic(err)
439	}
440	if err := os.RemoveAll(dir); err != nil {
441		panic(err)
442	}
443	os.Exit(result)
444}
445
446func doGoodReqPaginated(
447	path, order string,
448	iterStruct Pendinger,
449	appendIter func(res interface{}),
450) error {
451	var next uint64
452	firstIte := true
453	expectedTotal := 0
454	totalReceived := 0
455	for {
456		// Calculate fromItem
457		iterPath := path
458		if firstIte {
459			if order == historydb.OrderDesc {
460				// Fetch first item in reverse order
461				iterPath += "99999" // Asumption that for testing there won't be any itemID > 99999
462			} else {
463				iterPath += "0"
464			}
465		} else {
466			iterPath += strconv.Itoa(int(next))
467		}
468		// Call API to get this iteration items
469		iterStruct = iterStruct.New()
470		if err := doGoodReq(
471			"GET", iterPath+"&order="+order, nil,
472			iterStruct,
473		); err != nil {
474			return err
475		}
476		appendIter(iterStruct)
477		// Keep iterating?
478		remaining, lastID := iterStruct.GetPending()
479		if remaining == 0 {
480			break
481		}
482		if order == historydb.OrderDesc {
483			next = lastID - 1
484		} else {
485			next = lastID + 1
486		}
487		// Check that the expected amount of items is consistent across iterations
488		totalReceived += iterStruct.Len()
489		if firstIte {
490			firstIte = false
491			expectedTotal = totalReceived + int(remaining)
492		}
493		if expectedTotal != totalReceived+int(remaining) {
494			panic(fmt.Sprintf(
495				"pagination error, totalReceived + remaining should be %d, but is %d",
496				expectedTotal, totalReceived+int(remaining),
497			))
498		}
499	}
500	return nil
501}
502
503func doGoodReq(method, path string, reqBody io.Reader, returnStruct interface{}) error {
504	ctx := context.Background()
505	client := &http.Client{}
506	httpReq, err := http.NewRequest(method, path, reqBody)
507	if err != nil {
508		return err
509	}
510	if reqBody != nil {
511		httpReq.Header.Add("Content-Type", "application/json")
512	}
513	route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
514	if err != nil {
515		return err
516	}
517	// Validate request against swagger spec
518	requestValidationInput := &swagger.RequestValidationInput{
519		Request:    httpReq,
520		PathParams: pathParams,
521		Route:      route,
522	}
523	if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
524		return err
525	}
526	// Do API call
527	resp, err := client.Do(httpReq)
528	if err != nil {
529		return err
530	}
531	if resp.Body == nil && returnStruct != nil {
532		return errors.New("Nil body")
533	}
534	//nolint
535	defer resp.Body.Close()
536	body, err := ioutil.ReadAll(resp.Body)
537	if err != nil {
538		return err
539	}
540	if resp.StatusCode != 200 {
541		return fmt.Errorf("%d response. Body: %s", resp.StatusCode, string(body))
542	}
543	if returnStruct == nil {
544		return nil
545	}
546	// Unmarshal body into return struct
547	if err := json.Unmarshal(body, returnStruct); err != nil {
548		log.Error("invalid json: " + string(body))
549		log.Error(err)
550		return err
551	}
552	// log.Info(string(body))
553	// Validate response against swagger spec
554	responseValidationInput := &swagger.ResponseValidationInput{
555		RequestValidationInput: requestValidationInput,
556		Status:                 resp.StatusCode,
557		Header:                 resp.Header,
558	}
559	responseValidationInput = responseValidationInput.SetBodyBytes(body)
560	return swagger.ValidateResponse(ctx, responseValidationInput)
561}
562
563func doBadReq(method, path string, reqBody io.Reader, expectedResponseCode int) error {
564	ctx := context.Background()
565	client := &http.Client{}
566	httpReq, _ := http.NewRequest(method, path, reqBody)
567	route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
568	if err != nil {
569		return err
570	}
571	// Validate request against swagger spec
572	requestValidationInput := &swagger.RequestValidationInput{
573		Request:    httpReq,
574		PathParams: pathParams,
575		Route:      route,
576	}
577	if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
578		if expectedResponseCode != 400 {
579			return err
580		}
581		log.Warn("The request does not match the API spec")
582	}
583	// Do API call
584	resp, err := client.Do(httpReq)
585	if err != nil {
586		return err
587	}
588	if resp.Body == nil {
589		return errors.New("Nil body")
590	}
591	//nolint
592	defer resp.Body.Close()
593	body, err := ioutil.ReadAll(resp.Body)
594	if err != nil {
595		return err
596	}
597	if resp.StatusCode != expectedResponseCode {
598		return fmt.Errorf("Unexpected response code: %d. Body: %s", resp.StatusCode, string(body))
599	}
600	// Validate response against swagger spec
601	responseValidationInput := &swagger.ResponseValidationInput{
602		RequestValidationInput: requestValidationInput,
603		Status:                 resp.StatusCode,
604		Header:                 resp.Header,
605	}
606	responseValidationInput = responseValidationInput.SetBodyBytes(body)
607	return swagger.ValidateResponse(ctx, responseValidationInput)
608}
609
610// test helpers
611
612func getTimestamp(blockNum int64, blocks []common.Block) time.Time {
613	for i := 0; i < len(blocks); i++ {
614		if blocks[i].Num == blockNum {
615			return blocks[i].Timestamp
616		}
617	}
618	panic("timesamp not found")
619}
620
621func getTokenByID(id common.TokenID, tokens []historydb.TokenWithUSD) historydb.TokenWithUSD {
622	for i := 0; i < len(tokens); i++ {
623		if tokens[i].TokenID == id {
624			return tokens[i]
625		}
626	}
627	panic("token not found")
628}
629
630func getTokenByIdx(idx common.Idx, tokens []historydb.TokenWithUSD, accs []common.Account) historydb.TokenWithUSD {
631	for _, acc := range accs {
632		if idx == acc.Idx {
633			return getTokenByID(acc.TokenID, tokens)
634		}
635	}
636	panic("token not found")
637}
638
639func getAccountByIdx(idx common.Idx, accs []common.Account) *common.Account {
640	for _, acc := range accs {
641		if acc.Idx == idx {
642			return &acc
643		}
644	}
645	panic("account not found")
646}
647
648func getBlockByNum(ethBlockNum int64, blocks []common.Block) common.Block {
649	for _, b := range blocks {
650		if b.Num == ethBlockNum {
651			return b
652		}
653	}
654	panic("block not found")
655}
656
657func getCoordinatorByBidder(bidder ethCommon.Address, coordinators []historydb.CoordinatorAPI) historydb.CoordinatorAPI {
658	for _, c := range coordinators {
659		if c.Bidder == bidder {
660			return c
661		}
662	}
663	panic("coordinator not found")
664}