From 0c533ee1acbe1a93ea66c0beb3052351d474838c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Vincent?= <28714795+leovct@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:07:27 +0200 Subject: [PATCH] feat(monitor): add zkevm batch counters (#373) * feat(monitor): add trusted, virtual and verified batches counters * chore: update ui * chore: nit * chore: refactor * chore: refactor txpoolstatus * chore: refactor monitor top row * fix: lint * chore: nit * chore: better zkevm batch ux --- cmd/monitor/monitor.go | 73 ++++++++++++++++++++++------- cmd/monitor/ui/ui.go | 103 ++++++++++++++++++++++++++++++----------- util/util.go | 40 ++++++++++++++++ 3 files changed, 170 insertions(+), 46 deletions(-) diff --git a/cmd/monitor/monitor.go b/cmd/monitor/monitor.go index dd976771..2a30e6d9 100644 --- a/cmd/monitor/monitor.go +++ b/cmd/monitor/monitor.go @@ -64,8 +64,8 @@ type ( HeadBlock *big.Int PeerCount uint64 GasPrice *big.Int - PendingCount uint64 - QueuedCount uint64 + TxPoolStatus txPoolStatus + ZkEVMBatches zkEVMBatches SelectedBlock rpctypes.PolyBlock SelectedTransaction rpctypes.PolyTransaction BlockCache *lru.Cache `json:"-"` @@ -76,8 +76,17 @@ type ( ChainID *big.Int PeerCount uint64 GasPrice *big.Int - PendingCount uint64 - QueuedCount uint64 + TxPoolStatus txPoolStatus + ZkEVMBatches zkEVMBatches + } + txPoolStatus struct { + pending uint64 + queued uint64 + } + zkEVMBatches struct { + trusted uint64 + virtual uint64 + verified uint64 } historicalDataPoint struct { SampleTime time.Time @@ -111,6 +120,22 @@ func monitor(ctx context.Context) error { return errBatchRequestsNotSupported } + // Check if tx pool status is supported. + txPoolStatusSupported := false + if _, _, err = util.GetTxPoolStatus(rpc); err != nil { + log.Debug().Err(err).Msg("Unable to get tx pool status") + } else { + txPoolStatusSupported = true + } + + // Check if zkevm batches are supported. + zkEVMBatchesSupported := false + if _, _, _, err = util.GetZkEVMBatches(rpc); err != nil { + log.Debug().Err(err).Msg("Unable to get zkevm batches") + } else { + zkEVMBatchesSupported = true + } + ms := new(monitorStatus) ms.BlocksLock.Lock() ms.BlockCache, err = lru.New(blockCacheLimit) @@ -121,8 +146,8 @@ func monitor(ctx context.Context) error { ms.BlocksLock.Unlock() ms.ChainID = big.NewInt(0) - ms.PendingCount = 0 - ms.QueuedCount = 0 + ms.TxPoolStatus = txPoolStatus{} + ms.ZkEVMBatches = zkEVMBatches{} observedPendingTxs = make(historicalRange, 0) @@ -148,7 +173,7 @@ func monitor(ctx context.Context) error { } if !isUiRendered { go func() { - errChan <- renderMonitorUI(ctx, ec, ms, rpc) + errChan <- renderMonitorUI(ctx, ec, ms, rpc, txPoolStatusSupported, zkEVMBatchesSupported) }() isUiRendered = true } @@ -186,9 +211,14 @@ func getChainState(ctx context.Context, ec *ethclient.Client) (*chainState, erro return nil, fmt.Errorf("couldn't estimate gas: %s", err.Error()) } - cs.PendingCount, cs.QueuedCount, err = util.GetTxPoolStatus(ec.Client()) + cs.TxPoolStatus.pending, cs.TxPoolStatus.queued, err = util.GetTxPoolStatus(ec.Client()) if err != nil { - log.Debug().Err(err).Msg("Unable to get pending and queued transaction count") + log.Debug().Err(err).Msg("Unable to get tx pool status") + } + + cs.ZkEVMBatches.trusted, cs.ZkEVMBatches.virtual, cs.ZkEVMBatches.verified, err = util.GetZkEVMBatches(ec.Client()) + if err != nil { + log.Debug().Err(err).Msg("Unable to get zkevm batches") } return cs, nil @@ -214,7 +244,7 @@ func fetchCurrentBlockData(ctx context.Context, ec *ethclient.Client, ms *monito time.Sleep(interval) return err } - observedPendingTxs = append(observedPendingTxs, historicalDataPoint{SampleTime: time.Now(), SampleValue: float64(cs.PendingCount)}) + observedPendingTxs = append(observedPendingTxs, historicalDataPoint{SampleTime: time.Now(), SampleValue: float64(cs.TxPoolStatus.pending)}) if len(observedPendingTxs) > maxDataPoints { observedPendingTxs = observedPendingTxs[len(observedPendingTxs)-maxDataPoints:] } @@ -231,8 +261,8 @@ func fetchCurrentBlockData(ctx context.Context, ec *ethclient.Client, ms *monito ms.ChainID = cs.ChainID ms.PeerCount = cs.PeerCount ms.GasPrice = cs.GasPrice - ms.PendingCount = cs.PendingCount - ms.QueuedCount = cs.QueuedCount + ms.TxPoolStatus = cs.TxPoolStatus + ms.ZkEVMBatches = cs.ZkEVMBatches return } @@ -366,7 +396,7 @@ func (ms *monitorStatus) processBatchesConcurrently(ctx context.Context, rpc *et return errors.Join(errs...) } -func renderMonitorUI(ctx context.Context, ec *ethclient.Client, ms *monitorStatus, rpc *ethrpc.Client) error { +func renderMonitorUI(ctx context.Context, ec *ethclient.Client, ms *monitorStatus, rpc *ethrpc.Client, txPoolStatusSupported, zkEVMBatchesSupported bool) error { if err := termui.Init(); err != nil { log.Error().Err(err).Msg("Failed to initialize UI") return err @@ -375,7 +405,7 @@ func renderMonitorUI(ctx context.Context, ec *ethclient.Client, ms *monitorStatu currentMode := monitorModeExplorer - blockTable, blockInfo, transactionList, transactionInformationList, transactionInfo, grid, selectGrid, blockGrid, transactionGrid, skeleton := ui.SetUISkeleton() + blockTable, blockInfo, transactionList, transactionInformationList, transactionInfo, grid, selectGrid, blockGrid, transactionGrid, skeleton := ui.SetUISkeleton(txPoolStatusSupported, zkEVMBatchesSupported) termWidth, termHeight := termui.TerminalDimensions() windowSize = termHeight/2 - 4 @@ -476,8 +506,8 @@ func renderMonitorUI(ctx context.Context, ec *ethclient.Client, ms *monitorStatu Str("HeadBlock", ms.HeadBlock.String()). Uint64("PeerCount", ms.PeerCount). Str("GasPrice", ms.GasPrice.String()). - Uint64("PendingCount", ms.PendingCount). - Uint64("QueuedCount", ms.QueuedCount). + Interface("TxPoolStatus", ms.TxPoolStatus). + Interface("ZkEVMBatches", ms.ZkEVMBatches). Msg("Redrawing") if blockTable.SelectedRow == 0 { @@ -509,8 +539,15 @@ func renderMonitorUI(ctx context.Context, ec *ethclient.Client, ms *monitorStatu ms.BlocksLock.RUnlock() renderedBlocks = renderedBlocksTemp - log.Debug().Int("skeleton.Current.Inner.Dy()", skeleton.Current.Inner.Dy()).Int("skeleton.Current.Inner.Dx()", skeleton.Current.Inner.Dx()).Msg("the dimension of the current box") - skeleton.Current.Text = ui.GetCurrentBlockInfo(ms.HeadBlock, ms.GasPrice, ms.PeerCount, ms.PendingCount, ms.QueuedCount, ms.ChainID, rpcUrl, renderedBlocks, skeleton.Current.Inner.Dx(), skeleton.Current.Inner.Dy()) + skeleton.Current.Text = ui.GetCurrentText(skeleton.Current, ms.HeadBlock, ms.GasPrice, ms.PeerCount, ms.ChainID, rpcUrl) + if txPoolStatusSupported { + skeleton.TxPool.Text = ui.GetTxPoolText(skeleton.TxPool, ms.TxPoolStatus.pending, ms.TxPoolStatus.queued) + } + + if zkEVMBatchesSupported { + skeleton.ZkEVM.Text = ui.GetZkEVMText(skeleton.ZkEVM, ms.ZkEVMBatches.trusted, ms.ZkEVMBatches.virtual, ms.ZkEVMBatches.verified) + } + skeleton.TxPerBlockChart.Data = metrics.GetTxsPerBlock(renderedBlocks) skeleton.GasPriceChart.Data = metrics.GetMeanGasPricePerBlock(renderedBlocks) skeleton.BlockSizeChart.Data = metrics.GetSizePerBlock(renderedBlocks) diff --git a/cmd/monitor/ui/ui.go b/cmd/monitor/ui/ui.go index 4d61c87e..bc85cee3 100644 --- a/cmd/monitor/ui/ui.go +++ b/cmd/monitor/ui/ui.go @@ -20,35 +20,58 @@ import ( ) type UiSkeleton struct { - Current *widgets.Paragraph - TxPerBlockChart *widgets.Sparkline - GasPriceChart *widgets.Sparkline - BlockSizeChart *widgets.Sparkline - PendingTxChart *widgets.Sparkline - GasChart *widgets.Sparkline - BlockInfo *widgets.List - TxInfo *widgets.List - Receipts *widgets.List + Current, TxPool, ZkEVM *widgets.Paragraph + TxPerBlockChart *widgets.Sparkline + GasPriceChart *widgets.Sparkline + BlockSizeChart *widgets.Sparkline + PendingTxChart *widgets.Sparkline + GasChart *widgets.Sparkline + BlockInfo *widgets.List + TxInfo *widgets.List + Receipts *widgets.List } -func GetCurrentBlockInfo(headBlock *big.Int, gasPrice *big.Int, peerCount uint64, pendingCount uint64, queuedCount uint64, chainID *big.Int, rpcURL string, blocks []rpctypes.PolyBlock, dx int, dy int) string { - // Return an appropriate message if dy is 0 or less. - if dy <= 0 { - return "Invalid display configuration." - } - +func GetCurrentText(widget *widgets.Paragraph, headBlock, gasPrice *big.Int, peerCount uint64, chainID *big.Int, rpcURL string) string { + // First column height := fmt.Sprintf("Height: %s", headBlock.String()) timeInfo := fmt.Sprintf("Time: %s", time.Now().Format("02 Jan 06 15:04:05 MST")) gasPriceString := fmt.Sprintf("Gas Price: %s gwei", new(big.Int).Div(gasPrice, metrics.UnitShannon).String()) peers := fmt.Sprintf("Peers: %d", peerCount) - pendingTx := fmt.Sprintf("Pending Tx: %d", pendingCount) - queuedTx := fmt.Sprintf("Queued Tx: %d", queuedCount) - chainIdString := fmt.Sprintf("Chain ID: %s", chainID.String()) + + // Second column rpcURLString := fmt.Sprintf("RPC URL: %s", rpcURL) + chainIdString := fmt.Sprintf("Chain ID: %s", chainID.String()) + + return formatParagraph(widget, []string{height, timeInfo, gasPriceString, peers, chainIdString, rpcURLString}) +} + +func GetTxPoolText(widget *widgets.Paragraph, pendingTxCount, queuedTxCount uint64) string { + pendingTx := fmt.Sprintf("Pending Tx: %d", pendingTxCount) + queuedTx := fmt.Sprintf("Queued Tx: %d", queuedTxCount) + return formatParagraph(widget, []string{pendingTx, queuedTx}) +} + +func GetZkEVMText(widget *widgets.Paragraph, trustedBatchesCount, virtualBatchesCount, verifiedBatchesCount uint64) string { + trustedBatches := fmt.Sprintf("Trusted: %d", trustedBatchesCount) + trustedVirtualBatchesGap := trustedBatchesCount - virtualBatchesCount + virtualBatches := fmt.Sprintf("Virtual: %d (%d)", virtualBatchesCount, trustedVirtualBatchesGap) + + trustedVerifiedBatchesGap := trustedBatchesCount - verifiedBatchesCount + verifiedBatches := fmt.Sprintf("Verified: %d (%d)", verifiedBatchesCount, trustedVerifiedBatchesGap) + return formatParagraph(widget, []string{trustedBatches, virtualBatches, verifiedBatches}) +} - info := []string{height, timeInfo, gasPriceString, peers, pendingTx, queuedTx, chainIdString, rpcURLString} - columns := len(info) / dy - if len(info)%dy != 0 { +func formatParagraph(widget *widgets.Paragraph, content []string) string { + dx := widget.Inner.Dx() + dy := widget.Inner.Dy() + + // Return an appropriate message if dy is 0 or less. + if dy <= 0 { + return "Invalid display configuration." + } + + columns := len(content) / dy + if len(content)%dy != 0 { columns += 1 // Add an extra column for the remaining items } @@ -57,8 +80,8 @@ func GetCurrentBlockInfo(headBlock *big.Int, gasPrice *big.Int, peerCount uint64 for i := 0; i < columns; i++ { for j := 0; j < dy; j++ { index := i*dy + j - if index < len(info) && len(info[index]) > columnWidths[i] { - columnWidths[i] = len(info[index]) + if index < len(content) && len(content[index]) > columnWidths[i] { + columnWidths[i] = len(content[index]) } } // Add padding and ensure it doesn't exceed 'dx' @@ -72,9 +95,9 @@ func GetCurrentBlockInfo(headBlock *big.Int, gasPrice *big.Int, peerCount uint64 for i := 0; i < dy; i++ { for j := 0; j < columns; j++ { index := j*dy + i - if index < len(info) { + if index < len(content) { formatString := fmt.Sprintf("%%-%ds", columnWidths[j]) - formattedInfo.WriteString(fmt.Sprintf(formatString, info[index])) + formattedInfo.WriteString(fmt.Sprintf(formatString, content[index])) } } formattedInfo.WriteString("\n") @@ -449,7 +472,7 @@ func GetSimpleReceipt(ctx context.Context, rpc *ethrpc.Client, tx rpctypes.PolyT return fields } -func SetUISkeleton() (blockList *widgets.List, blockInfo *widgets.List, transactionList *widgets.List, transactionInformationList *widgets.List, transactionInfo *widgets.Table, grid *ui.Grid, selectGrid *ui.Grid, blockGrid *ui.Grid, transactionGrid *ui.Grid, termUi UiSkeleton) { +func SetUISkeleton(txPoolStatusSupported, zkEVMBatchesSupported bool) (blockList *widgets.List, blockInfo *widgets.List, transactionList *widgets.List, transactionInformationList *widgets.List, transactionInfo *widgets.Table, grid *ui.Grid, selectGrid *ui.Grid, blockGrid *ui.Grid, transactionGrid *ui.Grid, termUi UiSkeleton) { // help := widgets.NewParagraph() // help.Title = "Block Headers" // help.Text = "Use the arrow keys to scroll through the transactions. Press to go back to the explorer view" @@ -470,8 +493,32 @@ func SetUISkeleton() (blockList *widgets.List, blockInfo *widgets.List, transact termUi = UiSkeleton{} + // Top row termUi.Current = widgets.NewParagraph() termUi.Current.Title = "Current" + totalWidgets := 1 + + if txPoolStatusSupported { + termUi.TxPool = widgets.NewParagraph() + termUi.TxPool.Title = "TxPool" + totalWidgets++ + } + + if zkEVMBatchesSupported { + termUi.ZkEVM = widgets.NewParagraph() + termUi.ZkEVM.Title = "ZkEVM Batch No." + totalWidgets++ + } + + topRowBlocks := []interface{}{ + ui.NewCol((5.0-float64(totalWidgets-1))/5.0, termUi.Current), + } + if txPoolStatusSupported { + topRowBlocks = append(topRowBlocks, ui.NewCol(1.0/5.0, termUi.TxPool)) + } + if zkEVMBatchesSupported { + topRowBlocks = append(topRowBlocks, ui.NewCol(1.0/5.0, termUi.ZkEVM)) + } termUi.TxPerBlockChart = widgets.NewSparkline() termUi.TxPerBlockChart.LineColor = ui.ColorRed @@ -538,7 +585,7 @@ func SetUISkeleton() (blockList *widgets.List, blockInfo *widgets.List, transact termUi.Receipts.WrapText = true grid.Set( - ui.NewRow(1.0/10, termUi.Current), + ui.NewRow(1.0/10, topRowBlocks...), ui.NewRow(2.0/10, ui.NewCol(1.0/5, slg0), @@ -558,7 +605,7 @@ func SetUISkeleton() (blockList *widgets.List, blockInfo *widgets.List, transact ) selectGrid.Set( - ui.NewRow(1.0/10, termUi.Current), + ui.NewRow(1.0/10, topRowBlocks...), ui.NewRow(2.0/10, ui.NewCol(1.0/5, slg0), diff --git a/util/util.go b/util/util.go index caa71bd4..2f3de79c 100644 --- a/util/util.go +++ b/util/util.go @@ -9,6 +9,7 @@ import ( "time" "github.com/cenkalti/backoff" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus/clique" ethcrypto "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" @@ -203,6 +204,45 @@ func GetTxPoolStatus(rpc *ethrpc.Client) (uint64, uint64, error) { return pendingCount, queuedCount, nil } +func GetZkEVMBatches(rpc *ethrpc.Client) (uint64, uint64, uint64, error) { + trustedBatches, err := getZkEVMBatch(rpc, trusted) + if err != nil { + return 0, 0, 0, err + } + + virtualBatches, err := getZkEVMBatch(rpc, virtual) + if err != nil { + return trustedBatches, 0, 0, err + } + + verifiedBatches, err := getZkEVMBatch(rpc, verified) + if err != nil { + return trustedBatches, virtualBatches, 0, err + } + + return trustedBatches, virtualBatches, verifiedBatches, nil +} + +type batch string + +const ( + trusted batch = "zkevm_batchNumber" + virtual batch = "zkevm_virtualBatchNumber" + verified batch = "zkevm_verifiedBatchNumber" +) + +func getZkEVMBatch(rpc *ethrpc.Client, batchType batch) (uint64, error) { + var raw interface{} + if err := rpc.Call(&raw, string(batchType)); err != nil { + return 0, err + } + batch, err := hexutil.DecodeUint64(fmt.Sprintf("%v", raw)) + if err != nil { + return 0, err + } + return batch, nil +} + func tryCastToUint64(val any) (uint64, error) { switch t := val.(type) { case float64: