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