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 "github.com/stretchr/testify/assert"
30)
31
32const apiPort = ":4010"
33const apiURL = "http://localhost" + apiPort + "/"
34
35type testCommon struct {
36 blocks []common.Block
37 tokens []historydb.TokenWithUSD
38 batches []testBatch
39 fullBatches []testFullBatch
40 coordinators []historydb.CoordinatorAPI
41 usrAddr string
42 usrBjj string
43 accs []common.Account
44 usrTxs []testTx
45 allTxs []testTx
46 exits []testExit
47 usrExits []testExit
48 poolTxsToSend []testPoolTxSend
49 poolTxsToReceive []testPoolTxReceive
50 auths []testAuth
51 router *swagger.Router
52 bids []testBid
53}
54
55var tc testCommon
56var config configAPI
57
58// TestMain initializes the API server, and fill HistoryDB and StateDB with fake data,
59// emulating the task of the synchronizer in order to have data to be returned
60// by the API endpoints that will be tested
61func TestMain(m *testing.M) {
62 // Initializations
63 // Swagger
64 router := swagger.NewRouter().WithSwaggerFromFile("./swagger.yml")
65 // HistoryDB
66 pass := os.Getenv("POSTGRES_PASS")
67 database, err := db.InitSQLDB(5432, "localhost", "hermez", pass, "hermez")
68 if err != nil {
69 panic(err)
70 }
71 hdb := historydb.NewHistoryDB(database)
72 if err != nil {
73 panic(err)
74 }
75 // StateDB
76 dir, err := ioutil.TempDir("", "tmpdb")
77 if err != nil {
78 panic(err)
79 }
80 defer func() {
81 if err := os.RemoveAll(dir); err != nil {
82 panic(err)
83 }
84 }()
85 sdb, err := statedb.NewStateDB(dir, statedb.TypeTxSelector, 0)
86 if err != nil {
87 panic(err)
88 }
89 // L2DB
90 l2DB := l2db.NewL2DB(database, 10, 100, 24*time.Hour)
91 test.WipeDB(l2DB.DB()) // this will clean HistoryDB and L2DB
92 // Config (smart contract constants)
93 config.RollupConstants.ExchangeMultiplier = common.RollupConstExchangeMultiplier
94 config.RollupConstants.ExitIdx = common.RollupConstExitIDx
95 config.RollupConstants.ReservedIdx = common.RollupConstReservedIDx
96 config.RollupConstants.LimitLoadAmount, _ = new(big.Int).SetString("340282366920938463463374607431768211456", 10)
97 config.RollupConstants.LimitL2TransferAmount, _ = new(big.Int).SetString("6277101735386680763835789423207666416102355444464034512896", 10)
98 config.RollupConstants.LimitTokens = common.RollupConstLimitTokens
99 config.RollupConstants.L1CoordinatorTotalBytes = common.RollupConstL1CoordinatorTotalBytes
100 config.RollupConstants.L1UserTotalBytes = common.RollupConstL1UserTotalBytes
101 config.RollupConstants.MaxL1UserTx = common.RollupConstMaxL1UserTx
102 config.RollupConstants.MaxL1Tx = common.RollupConstMaxL1Tx
103 config.RollupConstants.InputSHAConstantBytes = common.RollupConstInputSHAConstantBytes
104 config.RollupConstants.NumBuckets = common.RollupConstNumBuckets
105 config.RollupConstants.MaxWithdrawalDelay = common.RollupConstMaxWithdrawalDelay
106 var rollupPublicConstants common.RollupConstants
107 rollupPublicConstants.AbsoluteMaxL1L2BatchTimeout = 240
108 rollupPublicConstants.HermezAuctionContract = ethCommon.HexToAddress("0x500D1d6A4c7D8Ae28240b47c8FCde034D827fD5e")
109 rollupPublicConstants.HermezGovernanceDAOAddress = ethCommon.HexToAddress("0xeAD9C93b79Ae7C1591b1FB5323BD777E86e150d4")
110 rollupPublicConstants.SafetyAddress = ethCommon.HexToAddress("0xE5904695748fe4A84b40b3fc79De2277660BD1D3")
111 rollupPublicConstants.TokenHEZ = ethCommon.HexToAddress("0xf784709d2317D872237C4bC22f867d1BAe2913AB")
112 rollupPublicConstants.WithdrawDelayerContract = ethCommon.HexToAddress("0xD6C850aeBFDC46D7F4c207e445cC0d6B0919BDBe")
113 var verifier common.RollupVerifierStruct
114 verifier.MaxTx = 512
115 verifier.NLevels = 32
116 rollupPublicConstants.Verifiers = append(rollupPublicConstants.Verifiers, verifier)
117
118 var auctionConstants common.AuctionConstants
119 auctionConstants.BlocksPerSlot = 40
120 auctionConstants.GenesisBlockNum = 100
121 auctionConstants.GovernanceAddress = ethCommon.HexToAddress("0xeAD9C93b79Ae7C1591b1FB5323BD777E86e150d4")
122 auctionConstants.InitialMinimalBidding, _ = new(big.Int).SetString("10000000000000000000", 10)
123 auctionConstants.HermezRollup = ethCommon.HexToAddress("0xEa960515F8b4C237730F028cBAcF0a28E7F45dE0")
124 auctionConstants.TokenHEZ = ethCommon.HexToAddress("0xf784709d2317D872237C4bC22f867d1BAe2913AB")
125
126 var wdelayerConstants common.WDelayerConstants
127 wdelayerConstants.HermezRollup = ethCommon.HexToAddress("0xEa960515F8b4C237730F028cBAcF0a28E7F45dE0")
128 wdelayerConstants.MaxEmergencyModeTime = uint64(1000000)
129 wdelayerConstants.MaxWithdrawalDelay = uint64(10000000)
130
131 config.RollupConstants.PublicConstants = rollupPublicConstants
132 config.AuctionConstants = auctionConstants
133 config.WDelayerConstants = wdelayerConstants
134
135 // API
136 api := gin.Default()
137 if err := SetAPIEndpoints(
138 true,
139 true,
140 api,
141 hdb,
142 sdb,
143 l2DB,
144 &config,
145 ); err != nil {
146 panic(err)
147 }
148 // Start server
149 server := &http.Server{Addr: apiPort, Handler: api}
150 go func() {
151 if err := server.ListenAndServe(); err != nil &&
152 err != http.ErrServerClosed {
153 panic(err)
154 }
155 }()
156
157 // Fill HistoryDB and StateDB with fake data
158 // Gen blocks and add them to DB
159 const nBlocks = 5
160 blocks := test.GenBlocks(1, nBlocks+1)
161 err = h.AddBlocks(blocks)
162 if err != nil {
163 panic(err)
164 }
165 // Gen tokens and add them to DB
166 const nTokens = 10
167 tokens, ethToken := test.GenTokens(nTokens, blocks)
168 err = h.AddTokens(tokens)
169 if err != nil {
170 panic(err)
171 }
172 tokens = append([]common.Token{ethToken}, tokens...)
173 // Set token value
174 tokensUSD := []historydb.TokenWithUSD{}
175 for i, tkn := range tokens {
176 token := historydb.TokenWithUSD{
177 TokenID: tkn.TokenID,
178 EthBlockNum: tkn.EthBlockNum,
179 EthAddr: tkn.EthAddr,
180 Name: tkn.Name,
181 Symbol: tkn.Symbol,
182 Decimals: tkn.Decimals,
183 }
184 // Set value of 50% of the tokens
185 if i%2 != 0 {
186 value := float64(i) * 1.234567
187 now := time.Now().UTC()
188 token.USD = &value
189 token.USDUpdate = &now
190 err = h.UpdateTokenValue(token.Symbol, value)
191 if err != nil {
192 panic(err)
193 }
194 }
195 tokensUSD = append(tokensUSD, token)
196 }
197 // Gen batches and add them to DB
198 const nBatches = 10
199 batches := test.GenBatches(nBatches, blocks)
200 err = h.AddBatches(batches)
201 if err != nil {
202 panic(err)
203 }
204 // Gen accounts and add them to HistoryDB and StateDB
205 const totalAccounts = 40
206 const userAccounts = 4
207 usrAddr := ethCommon.BigToAddress(big.NewInt(4896847))
208 privK := babyjub.NewRandPrivKey()
209 usrBjj := privK.Public()
210 accs := test.GenAccounts(totalAccounts, userAccounts, tokens, &usrAddr, usrBjj, batches)
211 err = h.AddAccounts(accs)
212 if err != nil {
213 panic(err)
214 }
215 for i := 0; i < len(accs); i++ {
216 if _, err := s.CreateAccount(accs[i].Idx, &accs[i]); err != nil {
217 panic(err)
218 }
219 }
220 // helper to vinculate user related resources
221 usrIdxs := []string{}
222 for _, acc := range accs {
223 if acc.EthAddr == usrAddr || acc.PublicKey == usrBjj {
224 for _, token := range tokens {
225 if token.TokenID == acc.TokenID {
226 usrIdxs = append(usrIdxs, idxToHez(acc.Idx, token.Symbol))
227 }
228 }
229 }
230 }
231 // Gen exits and add them to DB
232 const totalExits = 40
233 exits := test.GenExitTree(totalExits, batches, accs)
234 err = h.AddExitTree(exits)
235 if err != nil {
236 panic(err)
237 }
238
239 // L1 and L2 txs need to be sorted in a combined way
240 // Gen L1Txs
241 const totalL1Txs = 40
242 const userL1Txs = 4
243 usrL1Txs, othrL1Txs := test.GenL1Txs(256, totalL1Txs, userL1Txs, &usrAddr, accs, tokens, blocks, batches)
244 // Gen L2Txs
245 const totalL2Txs = 20
246 const userL2Txs = 4
247 usrL2Txs, othrL2Txs := test.GenL2Txs(256+totalL1Txs, totalL2Txs, userL2Txs, &usrAddr, accs, tokens, blocks, batches)
248 // Sort txs
249 sortedTxs := []txSortFielder{}
250 for i := 0; i < len(usrL1Txs); i++ {
251 wL1 := wrappedL1(usrL1Txs[i])
252 sortedTxs = append(sortedTxs, &wL1)
253 }
254 for i := 0; i < len(othrL1Txs); i++ {
255 wL1 := wrappedL1(othrL1Txs[i])
256 sortedTxs = append(sortedTxs, &wL1)
257 }
258 for i := 0; i < len(usrL2Txs); i++ {
259 wL2 := wrappedL2(usrL2Txs[i])
260 sortedTxs = append(sortedTxs, &wL2)
261 }
262 for i := 0; i < len(othrL2Txs); i++ {
263 wL2 := wrappedL2(othrL2Txs[i])
264 sortedTxs = append(sortedTxs, &wL2)
265 }
266 sort.Sort(txsSort(sortedTxs))
267 // Store txs to DB
268 for _, genericTx := range sortedTxs {
269 l1 := genericTx.L1()
270 l2 := genericTx.L2()
271 if l1 != nil {
272 err = h.AddL1Txs([]common.L1Tx{*l1})
273 if err != nil {
274 panic(err)
275 }
276 } else if l2 != nil {
277 err = h.AddL2Txs([]common.L2Tx{*l2})
278 if err != nil {
279 panic(err)
280 }
281 } else {
282 panic("should be l1 or l2")
283 }
284 }
285
286 // Coordinators
287 const nCoords = 10
288 coords := test.GenCoordinators(nCoords, blocks)
289 err = hdb.AddCoordinators(coords)
290 if err != nil {
291 panic(err)
292 }
293 fromItem := uint(0)
294 limit := uint(99999)
295 coordinators, _, err := hdb.GetCoordinatorsAPI(&fromItem, &limit, historydb.OrderAsc)
296 if err != nil {
297 panic(err)
298 }
299
300 // Bids
301 const nBids = 10
302 bids := test.GenBids(nBids, blocks, coords)
303 err = hdb.AddBids(bids)
304 if err != nil {
305 panic(err)
306 }
307
308 // Set testCommon
309 usrTxs, allTxs := genTestTxs(sortedTxs, usrIdxs, accs, tokensUSD, blocks)
310 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.
311 testBatches, fullBatches := genTestBatches(blocks, batches, allTxs)
312 usrExits, allExits := genTestExits(exits, tokensUSD, accs, usrIdxs)
313 tc = testCommon{
314 blocks: blocks,
315 tokens: tokensUSD,
316 batches: testBatches,
317 fullBatches: fullBatches,
318 coordinators: coordinators,
319 usrAddr: ethAddrToHez(usrAddr),
320 usrBjj: bjjToString(usrBjj),
321 accs: accs,
322 usrTxs: usrTxs,
323 allTxs: allTxs,
324 exits: allExits,
325 usrExits: usrExits,
326 poolTxsToSend: poolTxsToSend,
327 poolTxsToReceive: poolTxsToReceive,
328 auths: genTestAuths(test.GenAuths(5)),
329 router: router,
330 bids: genTestBids(blocks, coordinators, bids),
331 }
332
333 // Fake server
334 if os.Getenv("FAKE_SERVER") == "yes" {
335 for {
336 log.Info("Running fake server at " + apiURL + " until ^C is received")
337 time.Sleep(30 * time.Second)
338 }
339 }
340 // Run tests
341 result := m.Run()
342 // Stop server
343 if err := server.Shutdown(context.Background()); err != nil {
344 panic(err)
345 }
346 if err := database.Close(); err != nil {
347 panic(err)
348 }
349 if err := os.RemoveAll(dir); err != nil {
350 panic(err)
351 }
352 os.Exit(result)
353}
354
355func TestGetConfig(t *testing.T) {
356 endpoint := apiURL + "config"
357 var configTest configAPI
358 assert.NoError(t, doGoodReq("GET", endpoint, nil, &configTest))
359 assert.Equal(t, config, configTest)
360 assert.Equal(t, cg, &configTest)
361}
362
363func doGoodReqPaginated(
364 path, order string,
365 iterStruct db.Paginationer,
366 appendIter func(res interface{}),
367) error {
368 next := 0
369 for {
370 // Call API to get this iteration items
371 iterPath := path
372 if next == 0 && order == historydb.OrderDesc {
373 // Fetch first item in reverse order
374 iterPath += "99999"
375 } else {
376 // Fetch from next item or 0 if it's ascending order
377 iterPath += strconv.Itoa(next)
378 }
379 if err := doGoodReq("GET", iterPath+"&order="+order, nil, iterStruct); err != nil {
380 return err
381 }
382 appendIter(iterStruct)
383 // Keep iterating?
384 pag := iterStruct.GetPagination()
385 if order == historydb.OrderAsc {
386 if pag.LastReturnedItem == pag.LastItem { // No
387 break
388 } else { // Yes
389 next = pag.LastReturnedItem + 1
390 }
391 } else {
392 if pag.FirstReturnedItem == pag.FirstItem { // No
393 break
394 } else { // Yes
395 next = pag.FirstReturnedItem - 1
396 }
397 }
398 }
399 return nil
400}
401
402func doGoodReq(method, path string, reqBody io.Reader, returnStruct interface{}) error {
403 ctx := context.Background()
404 client := &http.Client{}
405 httpReq, err := http.NewRequest(method, path, reqBody)
406 if err != nil {
407 return err
408 }
409 if reqBody != nil {
410 httpReq.Header.Add("Content-Type", "application/json")
411 }
412 route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
413 if err != nil {
414 return err
415 }
416 // Validate request against swagger spec
417 requestValidationInput := &swagger.RequestValidationInput{
418 Request: httpReq,
419 PathParams: pathParams,
420 Route: route,
421 }
422 if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
423 return err
424 }
425 // Do API call
426 resp, err := client.Do(httpReq)
427 if err != nil {
428 return err
429 }
430 if resp.Body == nil && returnStruct != nil {
431 return errors.New("Nil body")
432 }
433 //nolint
434 defer resp.Body.Close()
435 body, err := ioutil.ReadAll(resp.Body)
436 if err != nil {
437 return err
438 }
439 if resp.StatusCode != 200 {
440 return fmt.Errorf("%d response. Body: %s", resp.StatusCode, string(body))
441 }
442 if returnStruct == nil {
443 return nil
444 }
445 // Unmarshal body into return struct
446 if err := json.Unmarshal(body, returnStruct); err != nil {
447 log.Error("invalid json: " + string(body))
448 return err
449 }
450 // Validate response against swagger spec
451 responseValidationInput := &swagger.ResponseValidationInput{
452 RequestValidationInput: requestValidationInput,
453 Status: resp.StatusCode,
454 Header: resp.Header,
455 }
456 responseValidationInput = responseValidationInput.SetBodyBytes(body)
457 return swagger.ValidateResponse(ctx, responseValidationInput)
458}
459
460func doBadReq(method, path string, reqBody io.Reader, expectedResponseCode int) error {
461 ctx := context.Background()
462 client := &http.Client{}
463 httpReq, _ := http.NewRequest(method, path, reqBody)
464 route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
465 if err != nil {
466 return err
467 }
468 // Validate request against swagger spec
469 requestValidationInput := &swagger.RequestValidationInput{
470 Request: httpReq,
471 PathParams: pathParams,
472 Route: route,
473 }
474 if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
475 if expectedResponseCode != 400 {
476 return err
477 }
478 log.Warn("The request does not match the API spec")
479 }
480 // Do API call
481 resp, err := client.Do(httpReq)
482 if err != nil {
483 return err
484 }
485 if resp.Body == nil {
486 return errors.New("Nil body")
487 }
488 //nolint
489 defer resp.Body.Close()
490 body, err := ioutil.ReadAll(resp.Body)
491 if err != nil {
492 return err
493 }
494 if resp.StatusCode != expectedResponseCode {
495 return fmt.Errorf("Unexpected response code: %d. Body: %s", resp.StatusCode, string(body))
496 }
497 // Validate response against swagger spec
498 responseValidationInput := &swagger.ResponseValidationInput{
499 RequestValidationInput: requestValidationInput,
500 Status: resp.StatusCode,
501 Header: resp.Header,
502 }
503 responseValidationInput = responseValidationInput.SetBodyBytes(body)
504 return swagger.ValidateResponse(ctx, responseValidationInput)
505}
506
507// test helpers
508
509func getTimestamp(blockNum int64, blocks []common.Block) time.Time {
510 for i := 0; i < len(blocks); i++ {
511 if blocks[i].EthBlockNum == blockNum {
512 return blocks[i].Timestamp
513 }
514 }
515 panic("timesamp not found")
516}
517
518func getTokenByID(id common.TokenID, tokens []historydb.TokenWithUSD) historydb.TokenWithUSD {
519 for i := 0; i < len(tokens); i++ {
520 if tokens[i].TokenID == id {
521 return tokens[i]
522 }
523 }
524 panic("token not found")
525}
526
527func getTokenByIdx(idx common.Idx, tokens []historydb.TokenWithUSD, accs []common.Account) historydb.TokenWithUSD {
528 for _, acc := range accs {
529 if idx == acc.Idx {
530 return getTokenByID(acc.TokenID, tokens)
531 }
532 }
533 panic("token not found")
534}
535
536func getAccountByIdx(idx common.Idx, accs []common.Account) *common.Account {
537 for _, acc := range accs {
538 if acc.Idx == idx {
539 return &acc
540 }
541 }
542 panic("account not found")
543}
544
545func getBlockByNum(ethBlockNum int64, blocks []common.Block) common.Block {
546 for _, b := range blocks {
547 if b.EthBlockNum == ethBlockNum {
548 return b
549 }
550 }
551 panic("block not found")
552}
553
554func getCoordinatorByBidder(bidder ethCommon.Address, coordinators []historydb.CoordinatorAPI) historydb.CoordinatorAPI {
555 for _, c := range coordinators {
556 if c.Bidder == bidder {
557 return c
558 }
559 }
560 panic("coordinator not found")
561}