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: 111111111
 62	CreateAccountDeposit(1) C: 222222222
 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: 333333333
 72
 73	// close Block:0, Batch:2
 74	> batchL1
 75
 76	// close Block:0, Batch:3
 77	> batchL1
 78
 79	CreateAccountDepositTransfer(0) B-A: 444444444, 1234444444 // to_eth_addr is NULL
 80
 81	// close Block:0, Batch:4
 82	> batchL1
 83	CreateAccountDeposit(0) D: 555555555
 84
 85	// close Block:0, Batch:5
 86	> batchL1
 87
 88	CreateAccountCoordinator(1) B
 89
 90	Transfer(1) A-B: 111111 (2) // to_eth_addr is NULL
 91	Transfer(0) B-C: 222222 (3)
 92
 93	// close Block:0, Batch:6
 94	> batchL1 // forge L1User{1}, forge L1Coord{2}, forge L2{2}
 95
 96	Deposit(0) C: 666666666
 97	DepositTransfer(0) C-D: 777777777, 123777777 // to_eth_addr is NULL
 98
 99	Transfer(0) A-B: 333333 (111)
100	Transfer(0) C-A: 444444 (222)
101	Transfer(1) B-C: 555555 (123)
102	Exit(0) A: 666666 (44)
103
104	ForceTransfer(0) D-B: 777777 // to_eth_addr is NULL
105	ForceExit(0) B: 888888
106
107	// close Block:0, Batch:7
108	> batchL1
109	> block
110
111	Transfer(0) D-A: 999999 (77)
112	Transfer(0) B-D: 123123 (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
224	// API
225	apiGin := gin.Default()
226	api, err = NewAPI(
227		true,
228		true,
229		apiGin,
230		hdb,
231		sdb,
232		l2DB,
233		&config,
234	)
235	if err != nil {
236		panic(err)
237	}
238	// Start server
239	server := &http.Server{Addr: apiPort, Handler: apiGin}
240	go func() {
241		if err := server.ListenAndServe(); err != nil &&
242			err != http.ErrServerClosed {
243			panic(err)
244		}
245	}()
246
247	// Reset DB
248	test.WipeDB(api.h.DB())
249
250	// Genratre blockchain data with til
251	tcc := til.NewContext(common.RollupConstMaxL1UserTx)
252	tilCfgExtra := til.ConfigExtra{
253		BootCoordAddr: ethCommon.HexToAddress("0xE39fEc6224708f0772D2A74fd3f9055A90E0A9f2"),
254		CoordUser:     "Coord",
255	}
256	blocksData, err := tcc.GenerateBlocks(SetBlockchain)
257	if err != nil {
258		panic(err)
259	}
260	err = tcc.FillBlocksExtra(blocksData, &tilCfgExtra)
261	if err != nil {
262		panic(err)
263	}
264	AddAditionalInformation(blocksData)
265	// Generate L2 Txs with til
266	commonPoolTxs, err := tcc.GeneratePoolL2Txs(til.SetPoolL2MinimumFlow0)
267	if err != nil {
268		panic(err)
269	}
270
271	// Extract til generated data, and add it to HistoryDB
272	var commonBlocks []common.Block
273	var commonBatches []common.Batch
274	var commonAccounts []common.Account
275	var commonExitTree []common.ExitInfo
276	var commonL1Txs []common.L1Tx
277	var commonL2Txs []common.L2Tx
278	// Add ETH token at the beginning of the array
279	testTokens := []historydb.TokenWithUSD{}
280	ethUSD := float64(500)
281	ethNow := time.Now()
282	testTokens = append(testTokens, historydb.TokenWithUSD{
283		TokenID:     test.EthToken.TokenID,
284		EthBlockNum: test.EthToken.EthBlockNum,
285		EthAddr:     test.EthToken.EthAddr,
286		Name:        test.EthToken.Name,
287		Symbol:      test.EthToken.Symbol,
288		Decimals:    test.EthToken.Decimals,
289		USD:         &ethUSD,
290		USDUpdate:   &ethNow,
291	})
292	err = api.h.UpdateTokenValue(test.EthToken.Symbol, ethUSD)
293	if err != nil {
294		panic(err)
295	}
296	for _, block := range blocksData {
297		// Insert block into HistoryDB
298		if err := api.h.AddBlockSCData(&block); err != nil { //nolint:gosec block is used as read only in the function
299			panic(err)
300		}
301		// Extract data
302		commonBlocks = append(commonBlocks, block.Block)
303		for i, tkn := range block.Rollup.AddedTokens {
304			token := historydb.TokenWithUSD{
305				TokenID:     tkn.TokenID,
306				EthBlockNum: tkn.EthBlockNum,
307				EthAddr:     tkn.EthAddr,
308				Name:        tkn.Name,
309				Symbol:      tkn.Symbol,
310				Decimals:    tkn.Decimals,
311			}
312			value := float64(i + 423)
313			now := time.Now().UTC()
314			token.USD = &value
315			token.USDUpdate = &now
316			// Set value in DB
317			err = api.h.UpdateTokenValue(token.Symbol, value)
318			if err != nil {
319				panic(err)
320			}
321			testTokens = append(testTokens, token)
322		}
323		// Set USD value for tokens in DB
324		commonL1Txs = append(commonL1Txs, block.Rollup.L1UserTxs...)
325		for _, batch := range block.Rollup.Batches {
326			commonL2Txs = append(commonL2Txs, batch.L2Txs...)
327			commonAccounts = append(commonAccounts, batch.CreatedAccounts...)
328			commonBatches = append(commonBatches, batch.Batch)
329			commonExitTree = append(commonExitTree, batch.ExitTree...)
330			commonL1Txs = append(commonL1Txs, batch.L1CoordinatorTxs...)
331		}
332	}
333
334	// lastBlockNum2 := blocksData[len(blocksData)-1].Block.EthBlockNum
335
336	// Add accounts to StateDB
337	for i := 0; i < len(commonAccounts); i++ {
338		if _, err := api.s.CreateAccount(commonAccounts[i].Idx, &commonAccounts[i]); err != nil {
339			panic(err)
340		}
341	}
342
343	// Generate Coordinators and add them to HistoryDB
344	const nCoords = 10
345	commonCoords := test.GenCoordinators(nCoords, commonBlocks)
346	if err := api.h.AddCoordinators(commonCoords); err != nil {
347		panic(err)
348	}
349
350	// Generate Bids and add them to HistoryDB
351	const nBids = 20
352	commonBids := test.GenBids(nBids, commonBlocks, commonCoords)
353	if err = api.h.AddBids(commonBids); err != nil {
354		panic(err)
355	}
356
357	// Generate SC vars and add them to HistoryDB (if needed)
358	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)}
359	auctionVars := common.AuctionVariables{
360		EthBlockNum:        int64(2),
361		DonationAddress:    ethCommon.HexToAddress("0x1111111111111111111111111111111111111111"),
362		DefaultSlotSetBid:  defaultSlotSetBid,
363		Outbidding:         uint16(1),
364		SlotDeadline:       uint8(20),
365		BootCoordinator:    ethCommon.HexToAddress("0x1111111111111111111111111111111111111111"),
366		ClosedAuctionSlots: uint16(2),
367		OpenAuctionSlots:   uint16(5),
368	}
369
370	rollupVars := common.RollupVariables{
371		EthBlockNum:           int64(3),
372		FeeAddToken:           big.NewInt(100),
373		ForgeL1L2BatchTimeout: int64(44),
374		WithdrawalDelay:       uint64(3000),
375	}
376
377	wdelayerVars := common.WDelayerVariables{
378		WithdrawalDelay: uint64(3000),
379	}
380
381	err = api.h.AddAuctionVars(&auctionVars)
382	if err != nil {
383		panic(err)
384	}
385
386	// Generate test data, as expected to be received/sended from/to the API
387	testCoords := genTestCoordinators(commonCoords)
388	testBids := genTestBids(commonBlocks, testCoords, commonBids)
389	testExits := genTestExits(commonExitTree, testTokens, commonAccounts)
390	testTxs := genTestTxs(commonL1Txs, commonL2Txs, commonAccounts, testTokens, commonBlocks)
391	testBatches, testFullBatches := genTestBatches(commonBlocks, commonBatches, testTxs)
392	poolTxsToSend, poolTxsToReceive := genTestPoolTxs(commonPoolTxs, testTokens, commonAccounts)
393	tc = testCommon{
394		blocks:           commonBlocks,
395		tokens:           testTokens,
396		batches:          testBatches,
397		fullBatches:      testFullBatches,
398		coordinators:     testCoords,
399		accounts:         genTestAccounts(commonAccounts, testTokens),
400		txs:              testTxs,
401		exits:            testExits,
402		poolTxsToSend:    poolTxsToSend,
403		poolTxsToReceive: poolTxsToReceive,
404		auths:            genTestAuths(test.GenAuths(5)),
405		router:           router,
406		bids:             testBids,
407		slots: api.genTestSlots(
408			20,
409			commonBlocks[len(commonBlocks)-1].EthBlockNum,
410			testBids,
411			auctionVars,
412		),
413		auctionVars:  auctionVars,
414		rollupVars:   rollupVars,
415		wdelayerVars: wdelayerVars,
416	}
417
418	// Fake server
419	if os.Getenv("FAKE_SERVER") == "yes" {
420		for {
421			log.Info("Running fake server at " + apiURL + " until ^C is received")
422			time.Sleep(30 * time.Second)
423		}
424	}
425	// Run tests
426	result := m.Run()
427	// Stop server
428	if err := server.Shutdown(context.Background()); err != nil {
429		panic(err)
430	}
431	if err := database.Close(); err != nil {
432		panic(err)
433	}
434	if err := os.RemoveAll(dir); err != nil {
435		panic(err)
436	}
437	os.Exit(result)
438}
439
440func doGoodReqPaginated(
441	path, order string,
442	iterStruct Pendinger,
443	appendIter func(res interface{}),
444) error {
445	var next uint64
446	firstIte := true
447	expectedTotal := 0
448	totalReceived := 0
449	for {
450		// Calculate fromItem
451		iterPath := path
452		if firstIte {
453			if order == historydb.OrderDesc {
454				// Fetch first item in reverse order
455				iterPath += "99999" // Asumption that for testing there won't be any itemID > 99999
456			} else {
457				iterPath += "0"
458			}
459		} else {
460			iterPath += strconv.Itoa(int(next))
461		}
462		// Call API to get this iteration items
463		iterStruct = iterStruct.New()
464		if err := doGoodReq(
465			"GET", iterPath+"&order="+order, nil,
466			iterStruct,
467		); err != nil {
468			return err
469		}
470		appendIter(iterStruct)
471		// Keep iterating?
472		remaining, lastID := iterStruct.GetPending()
473		if remaining == 0 {
474			break
475		}
476		if order == historydb.OrderDesc {
477			next = lastID - 1
478		} else {
479			next = lastID + 1
480		}
481		// Check that the expected amount of items is consistent across iterations
482		totalReceived += iterStruct.Len()
483		if firstIte {
484			firstIte = false
485			expectedTotal = totalReceived + int(remaining)
486		}
487		if expectedTotal != totalReceived+int(remaining) {
488			panic(fmt.Sprintf(
489				"pagination error, totalReceived + remaining should be %d, but is %d",
490				expectedTotal, totalReceived+int(remaining),
491			))
492		}
493	}
494	return nil
495}
496
497func doGoodReq(method, path string, reqBody io.Reader, returnStruct interface{}) error {
498	ctx := context.Background()
499	client := &http.Client{}
500	httpReq, err := http.NewRequest(method, path, reqBody)
501	if err != nil {
502		return err
503	}
504	if reqBody != nil {
505		httpReq.Header.Add("Content-Type", "application/json")
506	}
507	route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
508	if err != nil {
509		return err
510	}
511	// Validate request against swagger spec
512	requestValidationInput := &swagger.RequestValidationInput{
513		Request:    httpReq,
514		PathParams: pathParams,
515		Route:      route,
516	}
517	if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
518		return err
519	}
520	// Do API call
521	resp, err := client.Do(httpReq)
522	if err != nil {
523		return err
524	}
525	if resp.Body == nil && returnStruct != nil {
526		return errors.New("Nil body")
527	}
528	//nolint
529	defer resp.Body.Close()
530	body, err := ioutil.ReadAll(resp.Body)
531	if err != nil {
532		return err
533	}
534	if resp.StatusCode != 200 {
535		return fmt.Errorf("%d response. Body: %s", resp.StatusCode, string(body))
536	}
537	if returnStruct == nil {
538		return nil
539	}
540	// Unmarshal body into return struct
541	if err := json.Unmarshal(body, returnStruct); err != nil {
542		log.Error("invalid json: " + string(body))
543		log.Error(err)
544		return err
545	}
546	// log.Info(string(body))
547	// Validate response against swagger spec
548	responseValidationInput := &swagger.ResponseValidationInput{
549		RequestValidationInput: requestValidationInput,
550		Status:                 resp.StatusCode,
551		Header:                 resp.Header,
552	}
553	responseValidationInput = responseValidationInput.SetBodyBytes(body)
554	return swagger.ValidateResponse(ctx, responseValidationInput)
555}
556
557func doBadReq(method, path string, reqBody io.Reader, expectedResponseCode int) error {
558	ctx := context.Background()
559	client := &http.Client{}
560	httpReq, _ := http.NewRequest(method, path, reqBody)
561	route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
562	if err != nil {
563		return err
564	}
565	// Validate request against swagger spec
566	requestValidationInput := &swagger.RequestValidationInput{
567		Request:    httpReq,
568		PathParams: pathParams,
569		Route:      route,
570	}
571	if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
572		if expectedResponseCode != 400 {
573			return err
574		}
575		log.Warn("The request does not match the API spec")
576	}
577	// Do API call
578	resp, err := client.Do(httpReq)
579	if err != nil {
580		return err
581	}
582	if resp.Body == nil {
583		return errors.New("Nil body")
584	}
585	//nolint
586	defer resp.Body.Close()
587	body, err := ioutil.ReadAll(resp.Body)
588	if err != nil {
589		return err
590	}
591	if resp.StatusCode != expectedResponseCode {
592		return fmt.Errorf("Unexpected response code: %d. Body: %s", resp.StatusCode, string(body))
593	}
594	// Validate response against swagger spec
595	responseValidationInput := &swagger.ResponseValidationInput{
596		RequestValidationInput: requestValidationInput,
597		Status:                 resp.StatusCode,
598		Header:                 resp.Header,
599	}
600	responseValidationInput = responseValidationInput.SetBodyBytes(body)
601	return swagger.ValidateResponse(ctx, responseValidationInput)
602}
603
604// test helpers
605
606func getTimestamp(blockNum int64, blocks []common.Block) time.Time {
607	for i := 0; i < len(blocks); i++ {
608		if blocks[i].EthBlockNum == blockNum {
609			return blocks[i].Timestamp
610		}
611	}
612	panic("timesamp not found")
613}
614
615func getTokenByID(id common.TokenID, tokens []historydb.TokenWithUSD) historydb.TokenWithUSD {
616	for i := 0; i < len(tokens); i++ {
617		if tokens[i].TokenID == id {
618			return tokens[i]
619		}
620	}
621	panic("token not found")
622}
623
624func getTokenByIdx(idx common.Idx, tokens []historydb.TokenWithUSD, accs []common.Account) historydb.TokenWithUSD {
625	for _, acc := range accs {
626		if idx == acc.Idx {
627			return getTokenByID(acc.TokenID, tokens)
628		}
629	}
630	panic("token not found")
631}
632
633func getAccountByIdx(idx common.Idx, accs []common.Account) *common.Account {
634	for _, acc := range accs {
635		if acc.Idx == idx {
636			return &acc
637		}
638	}
639	panic("account not found")
640}
641
642func getBlockByNum(ethBlockNum int64, blocks []common.Block) common.Block {
643	for _, b := range blocks {
644		if b.EthBlockNum == ethBlockNum {
645			return b
646		}
647	}
648	panic("block not found")
649}
650
651func getCoordinatorByBidder(bidder ethCommon.Address, coordinators []historydb.CoordinatorAPI) historydb.CoordinatorAPI {
652	for _, c := range coordinators {
653		if c.Bidder == bidder {
654			return c
655		}
656	}
657	panic("coordinator not found")
658}