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: ðUSD,
290 USDUpdate: ðNow,
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}