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