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	"sort"
 14	"strconv"
 15	"testing"
 16	"time"
 17
 18	ethCommon "github.com/ethereum/go-ethereum/common"
 19	swagger "github.com/getkin/kin-openapi/openapi3filter"
 20	"github.com/gin-gonic/gin"
 21	"github.com/hermeznetwork/hermez-node/common"
 22	"github.com/hermeznetwork/hermez-node/db"
 23	"github.com/hermeznetwork/hermez-node/db/historydb"
 24	"github.com/hermeznetwork/hermez-node/db/l2db"
 25	"github.com/hermeznetwork/hermez-node/db/statedb"
 26	"github.com/hermeznetwork/hermez-node/log"
 27	"github.com/hermeznetwork/hermez-node/test"
 28	"github.com/iden3/go-iden3-crypto/babyjub"
 29)
 30
 31// Pendinger is an interface that allows getting last returned item ID and PendingItems to be used for building fromItem
 32// when testing paginated endpoints.
 33type Pendinger interface {
 34	GetPending() (pendingItems, lastItemID uint64)
 35	Len() int
 36}
 37
 38const apiPort = ":4010"
 39const apiURL = "http://localhost" + apiPort + "/"
 40
 41type testCommon struct {
 42	blocks           []common.Block
 43	tokens           []historydb.TokenWithUSD
 44	batches          []testBatch
 45	fullBatches      []testFullBatch
 46	coordinators     []historydb.CoordinatorAPI
 47	accounts         []testAccount
 48	usrAddr          string
 49	usrBjj           string
 50	accs             []common.Account
 51	usrTxs           []testTx
 52	allTxs           []testTx
 53	exits            []testExit
 54	usrExits         []testExit
 55	poolTxsToSend    []testPoolTxSend
 56	poolTxsToReceive []testPoolTxReceive
 57	auths            []testAuth
 58	router           *swagger.Router
 59	bids             []testBid
 60	slots            []testSlot
 61	auctionVars      common.AuctionVariables
 62	rollupVars       common.RollupVariables
 63	wdelayerVars     common.WDelayerVariables
 64}
 65
 66var tc testCommon
 67var config configAPI
 68var api *API
 69
 70// TestMain initializes the API server, and fill HistoryDB and StateDB with fake data,
 71// emulating the task of the synchronizer in order to have data to be returned
 72// by the API endpoints that will be tested
 73func TestMain(m *testing.M) {
 74	/*
 75		til update considerations:
 76		1. Two instructions sets should be enough (one for L2 another for historydb)
 77		2. FillBlocksExtra function must be used, there is a coment on top of the function that explains which data is setted
 78		3. Some data will not be generated by til nor FillBlocksExtra, test.GenXXX will still be required to cover this cases
 79		4. Most of the historydb inserts should be replaced with nBlocks calls to AddBlockSCData
 80		5. When defining til instructions, there is no need to have 100s of entries for each table, but it's interesting to
 81		   cover all different cases (for instance all tx types)
 82	*/
 83
 84	// Initializations
 85	// Swagger
 86	router := swagger.NewRouter().WithSwaggerFromFile("./swagger.yml")
 87	// HistoryDB
 88	pass := os.Getenv("POSTGRES_PASS")
 89
 90	database, err := db.InitSQLDB(5432, "localhost", "hermez", pass, "hermez")
 91	if err != nil {
 92		panic(err)
 93	}
 94	hdb := historydb.NewHistoryDB(database)
 95	if err != nil {
 96		panic(err)
 97	}
 98	// StateDB
 99	dir, err := ioutil.TempDir("", "tmpdb")
100	if err != nil {
101		panic(err)
102	}
103	defer func() {
104		if err := os.RemoveAll(dir); err != nil {
105			panic(err)
106		}
107	}()
108	sdb, err := statedb.NewStateDB(dir, statedb.TypeTxSelector, 0)
109	if err != nil {
110		panic(err)
111	}
112	// L2DB
113	l2DB := l2db.NewL2DB(database, 10, 100, 24*time.Hour)
114	test.WipeDB(l2DB.DB()) // this will clean HistoryDB and L2DB
115
116	// Config (smart contract constants)
117	config = getConfigTest()
118
119	// API
120	apiGin := gin.Default()
121	api, err = NewAPI(
122		true,
123		true,
124		apiGin,
125		hdb,
126		sdb,
127		l2DB,
128		&config,
129	)
130	if err != nil {
131		panic(err)
132	}
133	// Start server
134	server := &http.Server{Addr: apiPort, Handler: apiGin}
135	go func() {
136		if err := server.ListenAndServe(); err != nil &&
137			err != http.ErrServerClosed {
138			panic(err)
139		}
140	}()
141
142	// Fill HistoryDB and StateDB with fake data
143	// Gen blocks and add them to DB
144	const nBlocks = 5
145	// TODO: UPDATE with til
146	blocks := test.GenBlocks(1, nBlocks+1)
147	err = api.h.AddBlocks(blocks)
148	if err != nil {
149		panic(err)
150	}
151	lastBlockNum := blocks[nBlocks-1].EthBlockNum
152
153	// Gen tokens and add them to DB
154	const nTokens = 10
155	// TODO: UPDATE with til
156	tokens, ethToken := test.GenTokens(nTokens, blocks)
157	err = api.h.AddTokens(tokens)
158	if err != nil {
159		panic(err)
160	}
161	tokens = append([]common.Token{ethToken}, tokens...)
162	// Set token value
163	tokensUSD := []historydb.TokenWithUSD{}
164	for i, tkn := range tokens {
165		token := historydb.TokenWithUSD{
166			TokenID:     tkn.TokenID,
167			EthBlockNum: tkn.EthBlockNum,
168			EthAddr:     tkn.EthAddr,
169			Name:        tkn.Name,
170			Symbol:      tkn.Symbol,
171			Decimals:    tkn.Decimals,
172		}
173		// Set value of 50% of the tokens
174		if i%2 != 0 {
175			value := float64(i) * 1.234567
176			now := time.Now().UTC()
177			token.USD = &value
178			token.USDUpdate = &now
179			err = api.h.UpdateTokenValue(token.Symbol, value)
180			if err != nil {
181				panic(err)
182			}
183		}
184		tokensUSD = append(tokensUSD, token)
185	}
186	// Gen batches and add them to DB
187	const nBatches = 10
188	// TODO: UPDATE with til
189	batches := test.GenBatches(nBatches, blocks)
190	err = api.h.AddBatches(batches)
191	if err != nil {
192		panic(err)
193	}
194	// Gen accounts and add them to HistoryDB and StateDB
195	const totalAccounts = 40
196	const userAccounts = 4
197	usrAddr := ethCommon.BigToAddress(big.NewInt(4896847))
198	privK := babyjub.NewRandPrivKey()
199	usrBjj := privK.Public()
200	// TODO: UPDATE with til
201	accs := test.GenAccounts(totalAccounts, userAccounts, tokens, &usrAddr, usrBjj, batches)
202	err = api.h.AddAccounts(accs)
203	if err != nil {
204		panic(err)
205	}
206	for i := 0; i < len(accs); i++ {
207		if _, err := api.s.CreateAccount(accs[i].Idx, &accs[i]); err != nil {
208			panic(err)
209		}
210	}
211	// helper to vinculate user related resources
212	usrIdxs := []string{}
213	for _, acc := range accs {
214		if acc.EthAddr == usrAddr || acc.PublicKey == usrBjj {
215			for _, token := range tokens {
216				if token.TokenID == acc.TokenID {
217					usrIdxs = append(usrIdxs, idxToHez(acc.Idx, token.Symbol))
218				}
219			}
220		}
221	}
222	// Gen exits and add them to DB
223	const totalExits = 40
224	// TODO: UPDATE with til
225	exits := test.GenExitTree(totalExits, batches, accs)
226	err = api.h.AddExitTree(exits)
227	if err != nil {
228		panic(err)
229	}
230
231	// L1 and L2 txs need to be sorted in a combined way
232	// Gen L1Txs
233	const totalL1Txs = 40
234	const userL1Txs = 4
235	// TODO: UPDATE with til
236	usrL1Txs, othrL1Txs := test.GenL1Txs(256, totalL1Txs, userL1Txs, &usrAddr, accs, tokens, blocks, batches)
237	// Gen L2Txs
238	const totalL2Txs = 20
239	const userL2Txs = 4
240	// TODO: UPDATE with til
241	usrL2Txs, othrL2Txs := test.GenL2Txs(256+totalL1Txs, totalL2Txs, userL2Txs, &usrAddr, accs, tokens, blocks, batches)
242	// Sort txs
243	sortedTxs := []txSortFielder{}
244	for i := 0; i < len(usrL1Txs); i++ {
245		wL1 := wrappedL1(usrL1Txs[i])
246		sortedTxs = append(sortedTxs, &wL1)
247	}
248	for i := 0; i < len(othrL1Txs); i++ {
249		wL1 := wrappedL1(othrL1Txs[i])
250		sortedTxs = append(sortedTxs, &wL1)
251	}
252	for i := 0; i < len(usrL2Txs); i++ {
253		wL2 := wrappedL2(usrL2Txs[i])
254		sortedTxs = append(sortedTxs, &wL2)
255	}
256	for i := 0; i < len(othrL2Txs); i++ {
257		wL2 := wrappedL2(othrL2Txs[i])
258		sortedTxs = append(sortedTxs, &wL2)
259	}
260	sort.Sort(txsSort(sortedTxs))
261	// Store txs to DB
262	for _, genericTx := range sortedTxs {
263		l1 := genericTx.L1()
264		l2 := genericTx.L2()
265		if l1 != nil {
266			err = api.h.AddL1Txs([]common.L1Tx{*l1})
267			if err != nil {
268				panic(err)
269			}
270		} else if l2 != nil {
271			err = api.h.AddL2Txs([]common.L2Tx{*l2})
272			if err != nil {
273				panic(err)
274			}
275		} else {
276			panic("should be l1 or l2")
277		}
278	}
279
280	// Coordinators
281	const nCoords = 10
282	coords := test.GenCoordinators(nCoords, blocks)
283	err = api.h.AddCoordinators(coords)
284	if err != nil {
285		panic(err)
286	}
287	fromItem := uint(0)
288	limit := uint(99999)
289	coordinators, _, err := api.h.GetCoordinatorsAPI(&fromItem, &limit, historydb.OrderAsc)
290	if err != nil {
291		panic(err)
292	}
293
294	// Bids
295	const nBids = 20
296	bids := test.GenBids(nBids, blocks, coords)
297	err = api.h.AddBids(bids)
298	if err != nil {
299		panic(err)
300	}
301	testBids := genTestBids(blocks, coordinators, bids)
302
303	// Vars
304	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)}
305	auctionVars := common.AuctionVariables{
306		EthBlockNum:        int64(2),
307		DonationAddress:    ethCommon.HexToAddress("0x1111111111111111111111111111111111111111"),
308		DefaultSlotSetBid:  defaultSlotSetBid,
309		Outbidding:         uint16(1),
310		SlotDeadline:       uint8(20),
311		BootCoordinator:    ethCommon.HexToAddress("0x1111111111111111111111111111111111111111"),
312		ClosedAuctionSlots: uint16(2),
313		OpenAuctionSlots:   uint16(5),
314	}
315
316	rollupVars := common.RollupVariables{
317		EthBlockNum:           int64(3),
318		FeeAddToken:           big.NewInt(100),
319		ForgeL1L2BatchTimeout: int64(44),
320		WithdrawalDelay:       uint64(3000),
321	}
322
323	wdelayerVars := common.WDelayerVariables{
324		WithdrawalDelay: uint64(3000),
325	}
326
327	err = api.h.AddAuctionVars(&auctionVars)
328	if err != nil {
329		panic(err)
330	}
331
332	const nSlots = 20
333
334	// Set testCommon
335	usrTxs, allTxs := genTestTxs(sortedTxs, usrIdxs, accs, tokensUSD, blocks)
336	poolTxsToSend, poolTxsToReceive := genTestPoolTx(accs, []babyjub.PrivateKey{privK}, tokensUSD) // NOTE: pool txs are not inserted to the DB here. In the test they will be posted and getted.
337	testBatches, fullBatches := genTestBatches(blocks, batches, allTxs)
338	usrExits, allExits := genTestExits(exits, tokensUSD, accs, usrIdxs)
339	tc = testCommon{
340		blocks:           blocks,
341		tokens:           tokensUSD,
342		batches:          testBatches,
343		fullBatches:      fullBatches,
344		coordinators:     coordinators,
345		accounts:         genTestAccounts(accs, tokensUSD),
346		usrAddr:          ethAddrToHez(usrAddr),
347		usrBjj:           bjjToString(usrBjj),
348		accs:             accs,
349		usrTxs:           usrTxs,
350		allTxs:           allTxs,
351		exits:            allExits,
352		usrExits:         usrExits,
353		poolTxsToSend:    poolTxsToSend,
354		poolTxsToReceive: poolTxsToReceive,
355		auths:            genTestAuths(test.GenAuths(5)),
356		router:           router,
357		bids:             testBids,
358		slots:            api.genTestSlots(nSlots, lastBlockNum, testBids, auctionVars),
359		auctionVars:      auctionVars,
360		rollupVars:       rollupVars,
361		wdelayerVars:     wdelayerVars,
362	}
363
364	// Fake server
365	if os.Getenv("FAKE_SERVER") == "yes" {
366		for {
367			log.Info("Running fake server at " + apiURL + " until ^C is received")
368			time.Sleep(30 * time.Second)
369		}
370	}
371	// Run tests
372	result := m.Run()
373	// Stop server
374	if err := server.Shutdown(context.Background()); err != nil {
375		panic(err)
376	}
377	if err := database.Close(); err != nil {
378		panic(err)
379	}
380	if err := os.RemoveAll(dir); err != nil {
381		panic(err)
382	}
383	os.Exit(result)
384}
385
386func doGoodReqPaginated(
387	path, order string,
388	iterStruct Pendinger,
389	appendIter func(res interface{}),
390) error {
391	var next uint64
392	firstIte := true
393	expectedTotal := 0
394	totalReceived := 0
395	for {
396		// Calculate fromItem
397		iterPath := path
398		if firstIte {
399			if order == historydb.OrderDesc {
400				// Fetch first item in reverse order
401				iterPath += "99999" // Asumption that for testing there won't be any itemID > 99999
402			} else {
403				iterPath += "0"
404			}
405		} else {
406			iterPath += strconv.Itoa(int(next))
407		}
408		// Call API to get this iteration items
409		if err := doGoodReq("GET", iterPath+"&order="+order, nil, iterStruct); err != nil {
410			return err
411		}
412		appendIter(iterStruct)
413		// Keep iterating?
414		remaining, lastID := iterStruct.GetPending()
415		if remaining == 0 {
416			break
417		}
418		if order == historydb.OrderDesc {
419			next = lastID - 1
420		} else {
421			next = lastID + 1
422		}
423		// Check that the expected amount of items is consistent across iterations
424		totalReceived += iterStruct.Len()
425		if firstIte {
426			firstIte = false
427			expectedTotal = totalReceived + int(remaining)
428		}
429		if expectedTotal != totalReceived+int(remaining) {
430			panic(fmt.Sprintf(
431				"pagination error, totalReceived + remaining should be %d, but is %d",
432				expectedTotal, totalReceived+int(remaining),
433			))
434		}
435	}
436	return nil
437}
438
439func doGoodReq(method, path string, reqBody io.Reader, returnStruct interface{}) error {
440	ctx := context.Background()
441	client := &http.Client{}
442	httpReq, err := http.NewRequest(method, path, reqBody)
443	if err != nil {
444		return err
445	}
446	if reqBody != nil {
447		httpReq.Header.Add("Content-Type", "application/json")
448	}
449	route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
450	if err != nil {
451		return err
452	}
453	// Validate request against swagger spec
454	requestValidationInput := &swagger.RequestValidationInput{
455		Request:    httpReq,
456		PathParams: pathParams,
457		Route:      route,
458	}
459	if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
460		return err
461	}
462	// Do API call
463	resp, err := client.Do(httpReq)
464	if err != nil {
465		return err
466	}
467	if resp.Body == nil && returnStruct != nil {
468		return errors.New("Nil body")
469	}
470	//nolint
471	defer resp.Body.Close()
472	body, err := ioutil.ReadAll(resp.Body)
473	if err != nil {
474		return err
475	}
476	if resp.StatusCode != 200 {
477		return fmt.Errorf("%d response. Body: %s", resp.StatusCode, string(body))
478	}
479	if returnStruct == nil {
480		return nil
481	}
482	// Unmarshal body into return struct
483	if err := json.Unmarshal(body, returnStruct); err != nil {
484		log.Error("invalid json: " + string(body))
485		return err
486	}
487	// Validate response against swagger spec
488	responseValidationInput := &swagger.ResponseValidationInput{
489		RequestValidationInput: requestValidationInput,
490		Status:                 resp.StatusCode,
491		Header:                 resp.Header,
492	}
493	responseValidationInput = responseValidationInput.SetBodyBytes(body)
494	return swagger.ValidateResponse(ctx, responseValidationInput)
495}
496
497func doBadReq(method, path string, reqBody io.Reader, expectedResponseCode int) error {
498	ctx := context.Background()
499	client := &http.Client{}
500	httpReq, _ := http.NewRequest(method, path, reqBody)
501	route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
502	if err != nil {
503		return err
504	}
505	// Validate request against swagger spec
506	requestValidationInput := &swagger.RequestValidationInput{
507		Request:    httpReq,
508		PathParams: pathParams,
509		Route:      route,
510	}
511	if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
512		if expectedResponseCode != 400 {
513			return err
514		}
515		log.Warn("The request does not match the API spec")
516	}
517	// Do API call
518	resp, err := client.Do(httpReq)
519	if err != nil {
520		return err
521	}
522	if resp.Body == nil {
523		return errors.New("Nil body")
524	}
525	//nolint
526	defer resp.Body.Close()
527	body, err := ioutil.ReadAll(resp.Body)
528	if err != nil {
529		return err
530	}
531	if resp.StatusCode != expectedResponseCode {
532		return fmt.Errorf("Unexpected response code: %d. Body: %s", resp.StatusCode, string(body))
533	}
534	// Validate response against swagger spec
535	responseValidationInput := &swagger.ResponseValidationInput{
536		RequestValidationInput: requestValidationInput,
537		Status:                 resp.StatusCode,
538		Header:                 resp.Header,
539	}
540	responseValidationInput = responseValidationInput.SetBodyBytes(body)
541	return swagger.ValidateResponse(ctx, responseValidationInput)
542}
543
544// test helpers
545
546func getTimestamp(blockNum int64, blocks []common.Block) time.Time {
547	for i := 0; i < len(blocks); i++ {
548		if blocks[i].EthBlockNum == blockNum {
549			return blocks[i].Timestamp
550		}
551	}
552	panic("timesamp not found")
553}
554
555func getTokenByID(id common.TokenID, tokens []historydb.TokenWithUSD) historydb.TokenWithUSD {
556	for i := 0; i < len(tokens); i++ {
557		if tokens[i].TokenID == id {
558			return tokens[i]
559		}
560	}
561	panic("token not found")
562}
563
564func getTokenByIdx(idx common.Idx, tokens []historydb.TokenWithUSD, accs []common.Account) historydb.TokenWithUSD {
565	for _, acc := range accs {
566		if idx == acc.Idx {
567			return getTokenByID(acc.TokenID, tokens)
568		}
569	}
570	panic("token not found")
571}
572
573func getAccountByIdx(idx common.Idx, accs []common.Account) *common.Account {
574	for _, acc := range accs {
575		if acc.Idx == idx {
576			return &acc
577		}
578	}
579	panic("account not found")
580}
581
582func getBlockByNum(ethBlockNum int64, blocks []common.Block) common.Block {
583	for _, b := range blocks {
584		if b.EthBlockNum == ethBlockNum {
585			return b
586		}
587	}
588	panic("block not found")
589}
590
591func getCoordinatorByBidder(bidder ethCommon.Address, coordinators []historydb.CoordinatorAPI) historydb.CoordinatorAPI {
592	for _, c := range coordinators {
593		if c.Bidder == bidder {
594			return c
595		}
596	}
597	panic("coordinator not found")
598}