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