Add Sync stats, and report them in DebugAPI
@@ -21,6 +21,7 @@ URL = "http://localhost:8545"
[Synchronizer] SyncLoopInterval = "1s" +StatsRefreshPeriod = "1s" [Synchronizer.StartBlockNum] Rollup = 1
@@ -43,3 +43,12 @@ Outbidding uint16 `json:"outbidding" meddler:"outbidding" validate:"required"`
// Number of blocks at the end of a slot in which any coordinator can forge if the winner has not forged one before SlotDeadline uint8 `json:"slotDeadline" meddler:"slot_deadline" validate:"required"` } + +// Copy returns a deep copy of the Variables +func (v *AuctionVariables) Copy() *AuctionVariables { + vCpy := *v + for i := range v.DefaultSlotSetBid { + vCpy.DefaultSlotSetBid[i] = new(big.Int).SetBytes(v.DefaultSlotSetBid[i].Bytes()) + } + return &vCpy +}
@@ -163,3 +163,9 @@ ForgeL1L2BatchTimeout int64 `json:"forgeL1L2BatchTimeout" meddler:"forge_l1_timeout" validate:"required"`
WithdrawalDelay uint64 `json:"withdrawalDelay" meddler:"withdrawal_delay" validate:"required"` Buckets [RollupConstNumBuckets]Bucket `json:"buckets" meddler:"buckets,json"` } + +// Copy returns a deep copy of the Variables +func (v *RollupVariables) Copy() *RollupVariables { + vCpy := *v + return &vCpy +}
@@ -23,3 +23,9 @@ WithdrawalDelay uint64 `json:"withdrawalDelay" meddler:"withdrawal_delay" validate:"required"`
EmergencyModeStartingTime uint64 `json:"emergencyModeStartingTime" meddler:"emergency_start_time"` EmergencyMode bool `json:"emergencyMode" meddler:"emergency_mode"` } + +// Copy returns a deep copy of the Variables +func (v *WDelayerVariables) Copy() *WDelayerVariables { + vCpy := *v + return &vCpy +}
@@ -66,9 +66,10 @@ Web3 struct {
URL string `validate:"required"` } `validate:"required"` Synchronizer struct { - SyncLoopInterval Duration `validate:"required"` - StartBlockNum synchronizer.ConfigStartBlockNum `validate:"required"` - InitialVariables synchronizer.SCVariables `validate:"required"` + SyncLoopInterval Duration `validate:"required"` + StatsRefreshPeriod Duration `validate:"required"` + StartBlockNum synchronizer.ConfigStartBlockNum `validate:"required"` + InitialVariables synchronizer.SCVariables `validate:"required"` } `validate:"required"` SmartContracts struct { Rollup ethCommon.Address `validate:"required"`
@@ -172,6 +172,7 @@ RollupUpdateFeeAddToken(newFeeAddToken *big.Int) (*types.Transaction, error)
// Viewers RollupRegisterTokensCount() (*big.Int, error) + RollupLastForgedBatch() (int64, error) // // Smart Contract Status@@ -400,19 +401,26 @@ return tx, nil
} // RollupRegisterTokensCount is the interface to call the smart contract function -func (c *RollupClient) RollupRegisterTokensCount() (*big.Int, error) { - var registerTokensCount *big.Int +func (c *RollupClient) RollupRegisterTokensCount() (registerTokensCount *big.Int, err error) { if err := c.client.Call(func(ec *ethclient.Client) error { - hermez, err := Hermez.NewHermez(c.address, ec) - if err != nil { - return err - } - registerTokensCount, err = hermez.RegisterTokensCount(nil) + registerTokensCount, err = c.hermez.RegisterTokensCount(nil) return err }); err != nil { return nil, err } return registerTokensCount, nil +} + +// RollupLastForgedBatch is the interface to call the smart contract function +func (c *RollupClient) RollupLastForgedBatch() (lastForgedBatch int64, err error) { + if err := c.client.Call(func(ec *ethclient.Client) error { + _lastForgedBatch, err := c.hermez.LastForgedBatch(nil) + lastForgedBatch = int64(_lastForgedBatch) + return err + }); err != nil { + return 0, err + } + return lastForgedBatch, nil } // RollupUpdateForgeL1L2BatchTimeout is the interface to call the smart contract function
@@ -114,8 +114,9 @@ return nil, err
} sync, err := synchronizer.NewSynchronizer(client, historyDB, stateDB, synchronizer.Config{ - StartBlockNum: cfg.Synchronizer.StartBlockNum, - InitialVariables: cfg.Synchronizer.InitialVariables, + StartBlockNum: cfg.Synchronizer.StartBlockNum, + InitialVariables: cfg.Synchronizer.InitialVariables, + StatsRefreshPeriod: cfg.Synchronizer.StatsRefreshPeriod.Duration, }) if err != nil { return nil, err@@ -159,9 +160,8 @@ client,
) } var debugAPI *debugapi.DebugAPI - println("apiaddr", cfg.Debug.APIAddress) if cfg.Debug.APIAddress != "" { - debugAPI = debugapi.NewDebugAPI(cfg.Debug.APIAddress, stateDB) + debugAPI = debugapi.NewDebugAPI(cfg.Debug.APIAddress, stateDB, sync) } ctx, cancel := context.WithCancel(context.Background()) return &Node{
@@ -5,6 +5,8 @@ "context"
"database/sql" "fmt" "math/big" + "sync" + "time" "github.com/ethereum/go-ethereum" "github.com/hermeznetwork/hermez-node/common"@@ -39,6 +41,93 @@ // NextForgerAddr ethCommon.Address
// Synchronized bool // } +// Stats of the syncrhonizer +type Stats struct { + Eth struct { + RefreshPeriod time.Duration + Updated time.Time + FirstBlock int64 + LastBlock int64 + LastBatch int64 + } + Sync struct { + Updated time.Time + LastBlock int64 + LastBatch int64 + } +} + +// StatsHolder stores stats and that allows reading and writing them +// concurrently +type StatsHolder struct { + stats Stats + rw sync.RWMutex +} + +// NewStatsHolder creates a new StatsHolder +func NewStatsHolder(firstBlock int64, refreshPeriod time.Duration) *StatsHolder { + stats := Stats{} + stats.Eth.RefreshPeriod = refreshPeriod + stats.Eth.FirstBlock = firstBlock + return &StatsHolder{stats: stats} +} + +// UpdateSync updates the synchronizer stats +func (s *StatsHolder) UpdateSync(lastBlock int64, lastBatch *common.BatchNum) { + now := time.Now() + s.rw.Lock() + s.stats.Sync.LastBlock = lastBlock + if lastBatch != nil { + s.stats.Sync.LastBatch = int64(*lastBatch) + } + s.stats.Sync.Updated = now + s.rw.Unlock() +} + +// UpdateEth updates the ethereum stats, only if the previous stats expired +func (s *StatsHolder) UpdateEth(ethClient eth.ClientInterface) error { + now := time.Now() + s.rw.RLock() + elapsed := now.Sub(s.stats.Eth.Updated) + s.rw.RUnlock() + if elapsed < s.stats.Eth.RefreshPeriod { + return nil + } + + lastBlock, err := ethClient.EthCurrentBlock() + if err != nil { + return err + } + lastBatch, err := ethClient.RollupLastForgedBatch() + if err != nil { + return err + } + s.rw.Lock() + s.stats.Eth.Updated = now + s.stats.Eth.LastBlock = lastBlock + s.stats.Eth.LastBatch = lastBatch + s.rw.Unlock() + return nil +} + +// CopyStats returns a copy of the inner Stats +func (s *StatsHolder) CopyStats() *Stats { + s.rw.RLock() + sCopy := s.stats + s.rw.RUnlock() + return &sCopy +} + +func (s *StatsHolder) blocksPerc() float64 { + return float64(s.stats.Sync.LastBlock-s.stats.Eth.FirstBlock) * 100.0 / + float64(s.stats.Eth.LastBlock-s.stats.Eth.FirstBlock) +} + +func (s *StatsHolder) batchesPerc(batchNum int64) float64 { + return float64(batchNum) * 100.0 / + float64(s.stats.Eth.LastBatch) +} + // ConfigStartBlockNum sets the first block used to start tracking the smart // contracts type ConfigStartBlockNum struct {@@ -56,8 +145,9 @@ }
// Config is the Synchronizer configuration type Config struct { - StartBlockNum ConfigStartBlockNum - InitialVariables SCVariables + StartBlockNum ConfigStartBlockNum + InitialVariables SCVariables + StatsRefreshPeriod time.Duration } // Synchronizer implements the Synchronizer type@@ -71,6 +161,7 @@ stateDB *statedb.StateDB
cfg Config startBlockNum int64 vars SCVariables + stats *StatsHolder // firstSavedBlock *common.Block // mux sync.Mutex }@@ -103,6 +194,7 @@ }
if startBlockNum < cfg.StartBlockNum.WDelayer { startBlockNum = cfg.StartBlockNum.WDelayer } + stats := NewStatsHolder(startBlockNum, cfg.StatsRefreshPeriod) s := &Synchronizer{ ethClient: ethClient, auctionConstants: *auctionConstants,@@ -112,10 +204,16 @@ historyDB: historyDB,
stateDB: stateDB, cfg: cfg, startBlockNum: startBlockNum, + stats: stats, } return s, s.init() } +// Stats returns a copy of the Synchronizer Stats +func (s *Synchronizer) Stats() *Stats { + return s.stats.CopyStats() +} + // AuctionConstants returns the AuctionConstants read from the smart contract func (s *Synchronizer) AuctionConstants() *common.AuctionConstants { return &s.auctionConstants@@ -133,11 +231,13 @@ }
func (s *Synchronizer) init() error { rollup, auction, wDelayer, err := s.historyDB.GetSCVars() + // If SCVars are not in the HistoryDB, this is probably the first run + // of the Synchronizer: store the initial vars taken from config if err == sql.ErrNoRows { rollup = &s.cfg.InitialVariables.Rollup auction = &s.cfg.InitialVariables.Auction wDelayer = &s.cfg.InitialVariables.WDelayer - log.Debug("Setting initial SCVars in HistoryDB") + log.Info("Setting initial SCVars in HistoryDB") if err = s.historyDB.SetInitialSCVars(rollup, auction, wDelayer); err != nil { return err }@@ -145,6 +245,47 @@ }
s.vars.Rollup = *rollup s.vars.Auction = *auction s.vars.WDelayer = *wDelayer + + // Update stats parameters so that they have valid values before the + // first Sync call + if err := s.stats.UpdateEth(s.ethClient); err != nil { + return err + } + var lastBlockNum int64 + lastSavedBlock, err := s.historyDB.GetLastBlock() + if err != nil && err != sql.ErrNoRows { + return err + } + // If there's no block in the DB (or we only have the default block 0), + // make sure that the stateDB is clean + if err == sql.ErrNoRows || lastSavedBlock.EthBlockNum == 0 { + if err := s.stateDB.Reset(0); err != nil { + return err + } + } else { + lastBlockNum = lastSavedBlock.EthBlockNum + } + lastBatchNum, err := s.historyDB.GetLastBatchNum() + if err != nil && err != sql.ErrNoRows { + return err + } + if err == sql.ErrNoRows { + lastBatchNum = 0 + } + + s.stats.UpdateSync(lastBlockNum, &lastBatchNum) + + log.Infow("Sync init block", + "syncLastBlock", s.stats.stats.Sync.LastBlock, + "syncBlocksPerc", s.stats.blocksPerc(), + "ethFirstBlock", s.stats.stats.Eth.FirstBlock, + "ethLastBlock", s.stats.stats.Eth.LastBlock, + ) + log.Infow("Sync init batch", + "syncLastBatch", s.stats.stats.Sync.LastBatch, + "syncBatchesPerc", s.stats.batchesPerc(s.stats.stats.Sync.LastBatch), + "ethLastBatch", s.stats.stats.Eth.LastBatch, + ) return nil }@@ -182,7 +323,14 @@ return nil, nil, err
} log.Debugf("ethBlock: num: %v, parent: %v, hash: %v", ethBlock.EthBlockNum, ethBlock.ParentHash.String(), ethBlock.Hash.String()) - log.Debugw("Syncing...", "block", nextBlockNum) + if err := s.stats.UpdateEth(s.ethClient); err != nil { + return nil, nil, err + } + + log.Debugw("Syncing...", + "block", nextBlockNum, + "ethLastBlock", s.stats.stats.Eth.LastBlock, + ) // Check that the obtianed ethBlock.ParentHash == prevEthBlock.Hash; if not, reorg! if lastSavedBlock != nil {@@ -247,6 +395,26 @@ // }
err = s.historyDB.AddBlockSCData(&blockData) if err != nil { return nil, nil, err + } + + batchesLen := len(rollupData.Batches) + if batchesLen == 0 { + s.stats.UpdateSync(ethBlock.EthBlockNum, nil) + } else { + s.stats.UpdateSync(ethBlock.EthBlockNum, + &rollupData.Batches[batchesLen-1].Batch.BatchNum) + } + log.Debugw("Synced block", + "syncLastBlock", s.stats.stats.Sync.LastBlock, + "syncBlocksPerc", s.stats.blocksPerc(), + "ethLastBlock", s.stats.stats.Eth.LastBlock, + ) + for _, batchData := range rollupData.Batches { + log.Debugw("Synced batch", + "syncLastBatch", batchData.Batch.BatchNum, + "syncBatchesPerc", s.stats.batchesPerc(int64(batchData.Batch.BatchNum)), + "ethLastBatch", s.stats.stats.Eth.LastBatch, + ) } return &blockData, nil, nil@@ -566,7 +734,7 @@ }
if varsUpdate { s.vars.Rollup.EthBlockNum = blockNum - rollupData.Vars = &s.vars.Rollup + rollupData.Vars = s.vars.Rollup.Copy() } return &rollupData, nil@@ -665,7 +833,7 @@ // NOTE: We ignore HEZClaimed
if varsUpdate { s.vars.Auction.EthBlockNum = blockNum - auctionData.Vars = &s.vars.Auction + auctionData.Vars = s.vars.Auction.Copy() } return &auctionData, nil@@ -734,7 +902,7 @@ }
if varsUpdate { s.vars.WDelayer.EthBlockNum = blockNum - wDelayerData.Vars = &s.vars.WDelayer + wDelayerData.Vars = s.vars.WDelayer.Copy() } return &wDelayerData, nil
@@ -10,6 +10,7 @@ "github.com/gin-gonic/gin"
"github.com/hermeznetwork/hermez-node/common" "github.com/hermeznetwork/hermez-node/db/statedb" "github.com/hermeznetwork/hermez-node/log" + "github.com/hermeznetwork/hermez-node/synchronizer" ) func handleNoRoute(c *gin.Context) {@@ -33,13 +34,15 @@ // DebugAPI is an http API with debugging endpoints
type DebugAPI struct { addr string stateDB *statedb.StateDB // synchronizer statedb + sync *synchronizer.Synchronizer } // NewDebugAPI creates a new DebugAPI -func NewDebugAPI(addr string, stateDB *statedb.StateDB) *DebugAPI { +func NewDebugAPI(addr string, stateDB *statedb.StateDB, sync *synchronizer.Synchronizer) *DebugAPI { return &DebugAPI{ + addr: addr, stateDB: stateDB, - addr: addr, + sync: sync, } }@@ -82,6 +85,11 @@ root := a.stateDB.MTGetRoot()
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 {@@ -97,6 +105,8 @@ // 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.GET("sync/stats", a.handleSyncStats) debugAPIServer := &http.Server{ Addr: a.addr,
@@ -50,7 +50,8 @@ err = sdb.MakeCheckpoint() // Make a checkpoint to increment the batchNum
require.Nil(t, err) addr := "localhost:12345" - debugAPI := NewDebugAPI(addr, sdb) + // We won't test the sync/stats endpoint, so we can se the Syncrhonizer to nil + debugAPI := NewDebugAPI(addr, sdb, nil) ctx, cancel := context.WithCancel(context.Background()) go func() {
@@ -762,6 +762,16 @@ log.Error("TODO")
return nil, errTODO } +// RollupLastForgedBatch is the interface to call the smart contract function +func (c *Client) RollupLastForgedBatch() (int64, error) { + c.rw.RLock() + defer c.rw.RUnlock() + + currentBlock := c.currentBlock() + e := currentBlock.Rollup + return int64(len(e.State.ExitRoots)) - 1, nil +} + // RollupWithdrawCircuit is the interface to call the smart contract function func (c *Client) RollupWithdrawCircuit(proofA, proofC [2]*big.Int, proofB [2][2]*big.Int, tokenID uint32, numExitRoot, idx int64, amount *big.Int, instantWithdraw bool) (*types.Transaction, error) { log.Error("TODO")