package debugapi import ( "context" "fmt" "math/big" "net/http" "sync" "time" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/hermeznetwork/hermez-node/common" "github.com/hermeznetwork/hermez-node/db/historydb" "github.com/hermeznetwork/hermez-node/db/statedb" "github.com/hermeznetwork/hermez-node/log" "github.com/hermeznetwork/hermez-node/synchronizer" "github.com/hermeznetwork/tracerr" ) func handleNoRoute(c *gin.Context) { c.JSON(http.StatusNotFound, gin.H{ "error": "404 page not found", }) } type errorMsg struct { Message string } func badReq(err error, c *gin.Context) { log.Errorw("Bad request", "err", err) c.JSON(http.StatusBadRequest, errorMsg{ Message: err.Error(), }) } const ( statusUpdating = "updating" statusOK = "ok" ) type tokenBalances struct { sync.RWMutex Value struct { Status string Block *common.Block Batch *common.Batch Balances map[common.TokenID]*big.Int } } func (t *tokenBalances) Update(historyDB *historydb.HistoryDB, sdb *statedb.StateDB) (err error) { var block *common.Block var batch *common.Batch var balances map[common.TokenID]*big.Int defer func() { t.Lock() if err == nil { t.Value.Status = statusOK t.Value.Block = block t.Value.Batch = batch t.Value.Balances = balances } else { t.Value.Status = fmt.Sprintf("tokenBalances.Update: %v", err) t.Value.Block = nil t.Value.Batch = nil t.Value.Balances = nil } t.Unlock() }() if block, err = historyDB.GetLastBlock(); err != nil { return tracerr.Wrap(err) } if batch, err = historyDB.GetLastBatch(); err != nil { return tracerr.Wrap(err) } balances = make(map[common.TokenID]*big.Int) sdb.LastRead(func(sdbLast *statedb.Last) error { return tracerr.Wrap( statedb.AccountsIter(sdbLast.DB(), func(a *common.Account) (bool, error) { if balance, ok := balances[a.TokenID]; !ok { balances[a.TokenID] = a.Balance } else { balance.Add(balance, a.Balance) } return true, nil }), ) }) return nil } // DebugAPI is an http API with debugging endpoints type DebugAPI struct { addr string historyDB *historydb.HistoryDB stateDB *statedb.StateDB // synchronizer statedb sync *synchronizer.Synchronizer tokenBalances tokenBalances } // NewDebugAPI creates a new DebugAPI func NewDebugAPI(addr string, historyDB *historydb.HistoryDB, stateDB *statedb.StateDB, sync *synchronizer.Synchronizer) *DebugAPI { return &DebugAPI{ addr: addr, historyDB: historyDB, stateDB: stateDB, sync: sync, } } // SyncBlockHook is a hook function that the node will call after every new synchronized block func (a *DebugAPI) SyncBlockHook() { a.tokenBalances.RLock() updateTokenBalances := a.tokenBalances.Value.Status == statusUpdating a.tokenBalances.RUnlock() if updateTokenBalances { if err := a.tokenBalances.Update(a.historyDB, a.stateDB); err != nil { log.Errorw("DebugAPI.tokenBalances.Upate", "err", err) } } } func (a *DebugAPI) handleTokenBalances(c *gin.Context) { a.tokenBalances.RLock() tokenBalances := a.tokenBalances.Value a.tokenBalances.RUnlock() c.JSON(http.StatusOK, tokenBalances) } func (a *DebugAPI) handlePostTokenBalances(c *gin.Context) { a.tokenBalances.Lock() a.tokenBalances.Value.Status = statusUpdating a.tokenBalances.Unlock() c.JSON(http.StatusOK, nil) } func (a *DebugAPI) handleAccount(c *gin.Context) { uri := struct { Idx uint32 }{} if err := c.ShouldBindUri(&uri); err != nil { badReq(err, c) return } account, err := a.stateDB.LastGetAccount(common.Idx(uri.Idx)) if err != nil { badReq(err, c) return } c.JSON(http.StatusOK, account) } func (a *DebugAPI) handleAccounts(c *gin.Context) { var accounts []common.Account if err := a.stateDB.LastRead(func(sdb *statedb.Last) error { var err error accounts, err = sdb.GetAccounts() return err }); err != nil { badReq(err, c) return } c.JSON(http.StatusOK, accounts) } func (a *DebugAPI) handleCurrentBatch(c *gin.Context) { batchNum, err := a.stateDB.LastGetCurrentBatch() if err != nil { badReq(err, c) return } c.JSON(http.StatusOK, batchNum) } func (a *DebugAPI) handleMTRoot(c *gin.Context) { root, err := a.stateDB.LastMTGetRoot() if err != nil { badReq(err, c) return } c.JSON(http.StatusOK, root) } func (a *DebugAPI) handleSyncStats(c *gin.Context) { stats := a.sync.Stats() c.JSON(http.StatusOK, stats) } // Run starts the http server of the DebugAPI. To stop it, pass a context with // cancelation (see `debugapi_test.go` for an example). func (a *DebugAPI) Run(ctx context.Context) error { api := gin.Default() api.NoRoute(handleNoRoute) api.Use(cors.Default()) debugAPI := api.Group("/debug") debugAPI.GET("sdb/batchnum", a.handleCurrentBatch) debugAPI.GET("sdb/mtroot", a.handleMTRoot) // Accounts returned by these endpoints will always have BatchNum = 0, // because the stateDB doesn't store the BatchNum in which an account // is created. debugAPI.GET("sdb/accounts", a.handleAccounts) debugAPI.GET("sdb/accounts/:Idx", a.handleAccount) debugAPI.POST("sdb/tokenbalances", a.handlePostTokenBalances) debugAPI.GET("sdb/tokenbalances", a.handleTokenBalances) debugAPI.GET("sync/stats", a.handleSyncStats) debugAPIServer := &http.Server{ Addr: a.addr, Handler: api, // Use some hardcoded numberes that are suitable for testing ReadTimeout: 30 * time.Second, //nolint:gomnd WriteTimeout: 30 * time.Second, //nolint:gomnd MaxHeaderBytes: 1 << 20, //nolint:gomnd } go func() { log.Infof("DebugAPI is ready at %v", a.addr) if err := debugAPIServer.ListenAndServe(); err != nil && tracerr.Unwrap(err) != http.ErrServerClosed { log.Fatalf("Listen: %s\n", err) } }() <-ctx.Done() log.Info("Stopping DebugAPI...") ctxTimeout, cancel := context.WithTimeout(context.Background(), 10*time.Second) //nolint:gomnd defer cancel() if err := debugAPIServer.Shutdown(ctxTimeout); err != nil { return tracerr.Wrap(err) } log.Info("DebugAPI done") return nil }