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}