From 64fedad2c3f1b0b8175bc18ce8e649f82a5e43c2 Mon Sep 17 00:00:00 2001 From: Matt Ketmo Date: Thu, 14 Sep 2023 18:23:10 +0200 Subject: [PATCH] feat: expose metrics related to validators rank (#9) added new metrics: - `rank` - `active_set` - `seat_price` renamed metric `bonded_tokens` to `token` --- README.md | 7 ++- pkg/exporter/exporter.go | 58 +++++++++++++++++++++--- pkg/exporter/exporter_test.go | 84 ++++++++++++++++++++++++++--------- pkg/metrics/metrics.go | 36 +++++++++++++-- 4 files changed, 151 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 1276668..e11fe25 100644 --- a/README.md +++ b/README.md @@ -81,13 +81,16 @@ GLOBAL OPTIONS: All metrics are by default prefixed by `cosmos_validator_watcher` but this can be changed through options. Metrics (without prefix) | Description -----------------------------------------------|------------------------------------------------ +-------------------------|------------------------------------------------------------------------- `block_height` | Latest known block height (all nodes mixed up) +`active_set` | Number of validators in the active set +`seat_price` | Min seat price to be in the active set (ie. bonded tokens of the latest validator) +`rank` | Rank of the validator `validated_blocks` | Number of validated blocks per validator (for a bonded validator) `missed_blocks` | Number of missed blocks per validator (for a bonded validator) `tracked_blocks` | Number of blocks tracked since start `skipped_blocks` | Number of blocks skipped (ie. not tracked) since start -`bonded_tokens` | Number of bonded tokens per validator +`tokens` | Number of staked tokens per validator `is_bonded` | Set to 1 if the validator is bonded `is_jail` | Set to 1 if the validator is jailed `node_block_height` | Latest fetched block height for each node diff --git a/pkg/exporter/exporter.go b/pkg/exporter/exporter.go index 68a83ae..b2a50a6 100644 --- a/pkg/exporter/exporter.go +++ b/pkg/exporter/exporter.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "sort" "strings" "github.com/cometbft/cometbft/types" @@ -75,6 +76,7 @@ type ValidatorStatus struct { Label string Bonded bool Signed bool + Rank int } func (e *Exporter) handleBlock(block *types.Block) { @@ -91,6 +93,7 @@ func (e *Exporter) handleBlock(block *types.Block) { e.latestBlockHeight = block.Header.Height e.cfg.Metrics.BlockHeight.Set(float64(block.Header.Height)) + e.cfg.Metrics.ActiveSet.Set(float64(len(block.LastCommit.Signatures))) e.cfg.Metrics.TrackedBlocks.Inc() result := BlockResult{ @@ -109,6 +112,7 @@ func (e *Exporter) handleBlock(block *types.Block) { for _, val := range e.cfg.TrackedValidators { bonded := false signed := false + rank := 0 for i, sig := range block.LastCommit.Signatures { if sig.ValidatorAddress.String() == "" { log.Warn().Msgf("empty validator address at pos %d", i) @@ -116,6 +120,7 @@ func (e *Exporter) handleBlock(block *types.Block) { if val.Address == sig.ValidatorAddress.String() { bonded = true signed = !sig.Absent() + rank = i + 1 } if signed { break @@ -126,6 +131,7 @@ func (e *Exporter) handleBlock(block *types.Block) { Label: val.Name, Bonded: bonded, Signed: signed, + Rank: rank, }) } @@ -152,21 +158,61 @@ func (e *Exporter) handleBlock(block *types.Block) { } func (e *Exporter) handleValidators(validators []stakingtypes.Validator) { + // Sort validators by tokens & status (bonded, unbonded, jailed) + sort.Sort(RankedValidators(validators)) + + seatPrice := decimal.Zero + for _, val := range validators { + tokens := decimal.NewFromBigInt(val.Tokens.BigInt(), -6) + if val.Status == stakingtypes.Bonded && (seatPrice.IsZero() || seatPrice.GreaterThan(tokens)) { + seatPrice = tokens + } + e.cfg.Metrics.SeatPrice.Set(seatPrice.InexactFloat64()) + } + for _, tracked := range e.cfg.TrackedValidators { name := tracked.Name - for _, val := range validators { + for i, val := range validators { pubkey := ed25519.PubKey{Key: val.ConsensusPubkey.Value[2:]} address := pubkey.Address().String() if tracked.Address == address { - bondedTokens, _ := decimal.NewFromBigInt(val.BondedTokens().BigInt(), -6).Float64() - - e.cfg.Metrics.BondedTokens.WithLabelValues(address, name).Set(bondedTokens) - e.cfg.Metrics.IsBonded.WithLabelValues(address, name).Set(metrics.BoolToFloat64(val.Status == stakingtypes.Bonded)) - e.cfg.Metrics.IsJailed.WithLabelValues(address, name).Set(metrics.BoolToFloat64(val.Jailed)) + var ( + rank = i + 1 + isBonded = val.Status == stakingtypes.Bonded + isJailed = val.Jailed + tokens = decimal.NewFromBigInt(val.Tokens.BigInt(), -6) + ) + + e.cfg.Metrics.Rank.WithLabelValues(address, name).Set(float64(rank)) + e.cfg.Metrics.Tokens.WithLabelValues(address, name).Set(tokens.InexactFloat64()) + e.cfg.Metrics.IsBonded.WithLabelValues(address, name).Set(metrics.BoolToFloat64(isBonded)) + e.cfg.Metrics.IsJailed.WithLabelValues(address, name).Set(metrics.BoolToFloat64(isJailed)) break } } } } + +type RankedValidators []stakingtypes.Validator + +func (p RankedValidators) Len() int { return len(p) } +func (p RankedValidators) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +func (s RankedValidators) Less(i, j int) bool { + // Jailed validators are always last + if s[i].Jailed && !s[j].Jailed { + return false + } else if !s[i].Jailed && s[j].Jailed { + return true + } + + // Not bonded validators are after bonded validators + if s[i].Status == stakingtypes.Bonded && s[j].Status != stakingtypes.Bonded { + return true + } else if s[i].Status != stakingtypes.Bonded && s[j].Status == stakingtypes.Bonded { + return false + } + + return s[i].Tokens.BigInt().Cmp(s[j].Tokens.BigInt()) > 0 +} diff --git a/pkg/exporter/exporter_test.go b/pkg/exporter/exporter_test.go index d168a77..3cd38d7 100644 --- a/pkg/exporter/exporter_test.go +++ b/pkg/exporter/exporter_test.go @@ -19,8 +19,10 @@ import ( func TestExporter(t *testing.T) { var ( - address = "3DC4DD610817606AD4A8F9D762A068A81E8741E2" - name = "Kiln" + kilnAddress = "3DC4DD610817606AD4A8F9D762A068A81E8741E2" + kilnName = "Kiln" + + miscAddress = "1234567890ABCDEF10817606AD4A8FD7620A81E4" ) exporter := New(&Config{ @@ -30,8 +32,8 @@ func TestExporter(t *testing.T) { ValidatorsChan: make(chan []stakingtypes.Validator), TrackedValidators: []TrackedValidator{ { - Address: address, - Name: name, + Address: kilnAddress, + Name: kilnName, }, }, }) @@ -68,7 +70,7 @@ func TestExporter(t *testing.T) { Signatures: []types.CommitSig{ { BlockIDFlag: types.BlockIDFlagAbsent, - ValidatorAddress: MustParseAddress(address), + ValidatorAddress: MustParseAddress(kilnAddress), }, }, }, @@ -77,9 +79,13 @@ func TestExporter(t *testing.T) { Header: types.Header{Height: 41}, LastCommit: &types.Commit{ Signatures: []types.CommitSig{ + { + BlockIDFlag: types.BlockIDFlagAbsent, + ValidatorAddress: MustParseAddress(miscAddress), + }, { BlockIDFlag: types.BlockIDFlagCommit, - ValidatorAddress: MustParseAddress(address), + ValidatorAddress: MustParseAddress(kilnAddress), }, }, }, @@ -90,7 +96,11 @@ func TestExporter(t *testing.T) { Signatures: []types.CommitSig{ { BlockIDFlag: types.BlockIDFlagCommit, - ValidatorAddress: MustParseAddress(address), + ValidatorAddress: MustParseAddress(miscAddress), + }, + { + BlockIDFlag: types.BlockIDFlagCommit, + ValidatorAddress: MustParseAddress(kilnAddress), }, }, }, @@ -105,8 +115,8 @@ func TestExporter(t *testing.T) { strings.Join([]string{ `#35 0/1 validators ⚪️ Kiln`, `#40 0/1 validators ❌ Kiln`, - `#41 1/1 validators ✅ Kiln`, - `#42 1/1 validators ✅ Kiln`, + `#41 1/2 validators ✅ Kiln`, + `#42 2/2 validators ✅ Kiln`, }, "\n")+"\n", exporter.cfg.Writer.(*bytes.Buffer).String(), ) @@ -115,6 +125,10 @@ func TestExporter(t *testing.T) { `gauge: `, ReadMetric(exporter.cfg.Metrics.BlockHeight), ) + assert.Equal(t, + `gauge: `, + ReadMetric(exporter.cfg.Metrics.ActiveSet), + ) assert.Equal(t, `counter: `, ReadMetric(exporter.cfg.Metrics.TrackedBlocks), @@ -125,48 +139,74 @@ func TestExporter(t *testing.T) { ) assert.Equal(t, `label: label: counter: `, - ReadMetric(exporter.cfg.Metrics.ValidatedBlocks.WithLabelValues(address, name)), + ReadMetric(exporter.cfg.Metrics.ValidatedBlocks.WithLabelValues(kilnAddress, kilnName)), ) assert.Equal(t, `label: label: counter: `, - ReadMetric(exporter.cfg.Metrics.MissedBlocks.WithLabelValues(address, name)), + ReadMetric(exporter.cfg.Metrics.MissedBlocks.WithLabelValues(kilnAddress, kilnName)), ) }) t.Run("Handle Validators", func(t *testing.T) { - prefix := "0000" - pubkey := "915dea44121fbceb01452f98ca005b457fe8360c5e191b6601ee01b8a8d407a0" - ba, err := hex.DecodeString(prefix + pubkey) - require.NoError(t, err) + createAddress := func(pubkey string) *codectypes.Any { + prefix := "0000" + ba, err := hex.DecodeString(prefix + pubkey) + require.NoError(t, err) - addr := &codectypes.Any{ - TypeUrl: "/cosmos.crypto.ed25519.PubKey", - Value: ba, + return &codectypes.Any{ + TypeUrl: "/cosmos.crypto.ed25519.PubKey", + Value: ba, + } } validators := []stakingtypes.Validator{ { OperatorAddress: "", - ConsensusPubkey: addr, + ConsensusPubkey: createAddress("915dea44121fbceb01452f98ca005b457fe8360c5e191b6601ee01b8a8d407a0"), // 3DC4DD610817606AD4A8F9D762A068A81E8741E2 Jailed: false, Status: stakingtypes.Bonded, Tokens: math.NewInt(42000000), }, + { + OperatorAddress: "", + ConsensusPubkey: createAddress("0000000000000000000000000000000000000000000000000000000000000001"), + Jailed: false, + Status: stakingtypes.Bonded, + Tokens: math.NewInt(43000000), + }, + { + OperatorAddress: "", + ConsensusPubkey: createAddress("0000000000000000000000000000000000000000000000000000000000000002"), + Jailed: false, + Status: stakingtypes.Unbonded, + Tokens: math.NewInt(1000000), + }, + { + OperatorAddress: "", + ConsensusPubkey: createAddress("0000000000000000000000000000000000000000000000000000000000000003"), + Jailed: true, + Status: stakingtypes.Bonded, + Tokens: math.NewInt(99000000), + }, } exporter.handleValidators(validators) assert.Equal(t, `label: label: gauge: `, - ReadMetric(exporter.cfg.Metrics.BondedTokens.WithLabelValues(address, name)), + ReadMetric(exporter.cfg.Metrics.Tokens.WithLabelValues(kilnAddress, kilnName)), + ) + assert.Equal(t, + `label: label: gauge: `, + ReadMetric(exporter.cfg.Metrics.Rank.WithLabelValues(kilnAddress, kilnName)), ) assert.Equal(t, `label: label: gauge: `, - ReadMetric(exporter.cfg.Metrics.IsBonded.WithLabelValues(address, name)), + ReadMetric(exporter.cfg.Metrics.IsBonded.WithLabelValues(kilnAddress, kilnName)), ) assert.Equal(t, `label: label: gauge: `, - ReadMetric(exporter.cfg.Metrics.IsJailed.WithLabelValues(address, name)), + ReadMetric(exporter.cfg.Metrics.IsJailed.WithLabelValues(kilnAddress, kilnName)), ) }) } diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index edbf579..5ce4169 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -5,11 +5,14 @@ import "github.com/prometheus/client_golang/prometheus" type Metrics struct { // Exporter metrics BlockHeight prometheus.Gauge + Rank *prometheus.GaugeVec + ActiveSet prometheus.Gauge + SeatPrice prometheus.Gauge ValidatedBlocks *prometheus.CounterVec MissedBlocks *prometheus.CounterVec TrackedBlocks prometheus.Counter SkippedBlocks prometheus.Counter - BondedTokens *prometheus.GaugeVec + Tokens *prometheus.GaugeVec IsBonded *prometheus.GaugeVec IsJailed *prometheus.GaugeVec @@ -27,6 +30,28 @@ func New(namespace string) *Metrics { Help: "Latest known block height (all nodes mixed up)", }, ), + ActiveSet: prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "active_set", + Help: "Number of validators in the active set", + }, + ), + SeatPrice: prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "seat_price", + Help: "Min seat price to be in the active set (ie. bonded tokens of the latest validator)", + }, + ), + Rank: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "rank", + Help: "Rank of the validator", + }, + []string{"address", "name"}, + ), ValidatedBlocks: prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: namespace, @@ -57,11 +82,11 @@ func New(namespace string) *Metrics { Help: "Number of blocks skipped (ie. not tracked) since start", }, ), - BondedTokens: prometheus.NewGaugeVec( + Tokens: prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: namespace, Name: "bonded_tokens", - Help: "Number of bonded tokens per validator", + Help: "Number of staked tokens per validator", }, []string{"address", "name"}, ), @@ -100,11 +125,14 @@ func New(namespace string) *Metrics { } prometheus.MustRegister(metrics.BlockHeight) + prometheus.MustRegister(metrics.ActiveSet) + prometheus.MustRegister(metrics.SeatPrice) + prometheus.MustRegister(metrics.Rank) prometheus.MustRegister(metrics.ValidatedBlocks) prometheus.MustRegister(metrics.MissedBlocks) prometheus.MustRegister(metrics.TrackedBlocks) prometheus.MustRegister(metrics.SkippedBlocks) - prometheus.MustRegister(metrics.BondedTokens) + prometheus.MustRegister(metrics.Tokens) prometheus.MustRegister(metrics.IsBonded) prometheus.MustRegister(metrics.IsJailed) prometheus.MustRegister(metrics.NodeBlockHeight)