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 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: ðUSD,
295 USDUpdate: ðNow,
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].EthBlockNum,
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].EthBlockNum == 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.EthBlockNum == 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}