Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for slashing parameters and missed blocks per signing window #85

Merged
merged 4 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkg/app/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ var Flags = []cli.Flag{
Name: "no-staking",
Usage: "disable calls to staking module (useful for consumer chains)",
},
&cli.BoolFlag{
Name: "no-slashing",
Usage: "disable calls to slashing module",
},
&cli.BoolFlag{
Name: "no-commission",
Usage: "disable calls to get validator commission (useful for chains without distribution module)",
Expand Down
14 changes: 14 additions & 0 deletions pkg/app/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func RunFunc(cCtx *cli.Context) error {
noStaking = cCtx.Bool("no-staking")
noUpgrade = cCtx.Bool("no-upgrade")
noCommission = cCtx.Bool("no-commission")
noSlashing = cCtx.Bool("no-slashing")
denom = cCtx.String("denom")
denomExpon = cCtx.Uint("denom-exponent")
startTimeout = cCtx.Duration("start-timeout")
Expand Down Expand Up @@ -128,6 +129,16 @@ func RunFunc(cCtx *cli.Context) error {
})
}

//
// Slashing watchers
//
if !noSlashing {
slashingWatcher := watcher.NewSlashingWatcher(metrics, pool)
errg.Go(func() error {
return slashingWatcher.Start(ctx)
})
}

//
// Pool watchers
//
Expand Down Expand Up @@ -320,8 +331,10 @@ func createTrackedValidators(ctx context.Context, pool *rpc.Pool, validators []s
for _, stakingVal := range stakingValidators {
address := crypto.PubKeyAddress(stakingVal.ConsensusPubkey)
if address == val.Address {
hrp := crypto.GetHrpPrefix(stakingVal.OperatorAddress) + "valcons"
val.Moniker = stakingVal.Description.Moniker
val.OperatorAddress = stakingVal.OperatorAddress
val.ConsensusAddress = crypto.PubKeyBech32Address(stakingVal.ConsensusPubkey, hrp)
}
}

Expand All @@ -336,6 +349,7 @@ func createTrackedValidators(ctx context.Context, pool *rpc.Pool, validators []s
Str("alias", val.Name).
Str("moniker", val.Moniker).
Str("operator", val.OperatorAddress).
Str("consensus", val.ConsensusAddress).
Msgf("validator info")

return val
Expand Down
35 changes: 35 additions & 0 deletions pkg/crypto/utils.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package crypto

import (
"strings"

types1 "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/cosmos/cosmos-sdk/types/bech32"
)

func PubKeyAddress(consensusPubkey *types1.Any) string {
Expand All @@ -19,3 +22,35 @@ func PubKeyAddress(consensusPubkey *types1.Any) string {

panic("unknown pubkey type: " + consensusPubkey.TypeUrl)
}

func PubKeyBech32Address(consensusPubkey *types1.Any, prefix string) string {
switch consensusPubkey.TypeUrl {
case "/cosmos.crypto.ed25519.PubKey":
key := ed25519.PubKey{Key: consensusPubkey.Value[2:]}
address, _ := bech32.ConvertAndEncode(prefix, key.Address())
return address

case "/cosmos.crypto.secp256k1.PubKey":
key := secp256k1.PubKey{Key: consensusPubkey.Value[2:]}
address, _ := bech32.ConvertAndEncode(prefix, key.Address())
return address
}

panic("unknown pubkey type: " + consensusPubkey.TypeUrl)
MattKetmo marked this conversation as resolved.
Show resolved Hide resolved
}

// GetHrpPrefix returns the human-readable prefix for a given address.
// Examples of valid address HRPs are "cosmosvalcons", "cosmosvaloper".
// So this will return "cosmos" as the prefix
func GetHrpPrefix(a string) string {

hrp, _, err := bech32.DecodeAndConvert(a)
if err != nil {
return err.Error()
}

for _, v := range []string{"valoper", "cncl", "valcons"} {
hrp = strings.TrimSuffix(hrp, v)
}
return hrp
}
96 changes: 78 additions & 18 deletions pkg/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,33 @@ type Metrics struct {
Registry *prometheus.Registry

// Global metrics
ActiveSet *prometheus.GaugeVec
BlockHeight *prometheus.GaugeVec
ProposalEndTime *prometheus.GaugeVec
SeatPrice *prometheus.GaugeVec
SkippedBlocks *prometheus.CounterVec
TrackedBlocks *prometheus.CounterVec
Transactions *prometheus.CounterVec
UpgradePlan *prometheus.GaugeVec
ActiveSet *prometheus.GaugeVec
BlockHeight *prometheus.GaugeVec
ProposalEndTime *prometheus.GaugeVec
SeatPrice *prometheus.GaugeVec
SkippedBlocks *prometheus.CounterVec
TrackedBlocks *prometheus.CounterVec
Transactions *prometheus.CounterVec
UpgradePlan *prometheus.GaugeVec
SignedBlocksWindow *prometheus.GaugeVec
MinSignedBlocksPerWindow *prometheus.GaugeVec
DowntimeJailDuration *prometheus.GaugeVec
SlashFractionDoubleSign *prometheus.GaugeVec
SlashFractionDowntime *prometheus.GaugeVec

// Validator metrics
Rank *prometheus.GaugeVec
ProposedBlocks *prometheus.CounterVec
ValidatedBlocks *prometheus.CounterVec
MissedBlocks *prometheus.CounterVec
SoloMissedBlocks *prometheus.CounterVec
Rank *prometheus.GaugeVec
ProposedBlocks *prometheus.CounterVec
ValidatedBlocks *prometheus.CounterVec
MissedBlocks *prometheus.CounterVec
SoloMissedBlocks *prometheus.CounterVec
ConsecutiveMissedBlocks *prometheus.GaugeVec
Tokens *prometheus.GaugeVec
IsBonded *prometheus.GaugeVec
IsJailed *prometheus.GaugeVec
Commission *prometheus.GaugeVec
Vote *prometheus.GaugeVec
MissedBlocksWindow *prometheus.GaugeVec
Tokens *prometheus.GaugeVec
IsBonded *prometheus.GaugeVec
IsJailed *prometheus.GaugeVec
Commission *prometheus.GaugeVec
Vote *prometheus.GaugeVec

// Node metrics
NodeBlockHeight *prometheus.GaugeVec
Expand Down Expand Up @@ -111,6 +117,14 @@ func New(namespace string) *Metrics {
},
[]string{"chain_id", "address", "name"},
),
MissedBlocksWindow: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: namespace,
Name: "missed_blocks_window",
Help: "Number of missed blocks per validator for the current signing window (for a bonded validator)",
},
[]string{"chain_id", "address", "name"},
),
TrackedBlocks: prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Expand Down Expand Up @@ -207,6 +221,46 @@ func New(namespace string) *Metrics {
},
[]string{"chain_id", "proposal_id"},
),
SignedBlocksWindow: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: namespace,
Name: "signed_blocks_window",
Help: "Number of blocks per signing window",
},
[]string{"chain_id"},
),
MinSignedBlocksPerWindow: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: namespace,
Name: "min_signed_blocks_per_window",
Help: "Minimum number of blocks required to be signed per signing window",
},
[]string{"chain_id"},
),
DowntimeJailDuration: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: namespace,
Name: "downtime_jail_duration",
Help: "Duration of the jail period for a validator in seconds",
},
[]string{"chain_id"},
),
SlashFractionDoubleSign: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: namespace,
Name: "slash_fraction_double_sign",
Help: "Slash penaltiy for double-signing",
},
[]string{"chain_id"},
),
SlashFractionDowntime: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: namespace,
Name: "slash_fraction_downtime",
Help: "Slash penaltiy for downtime",
},
[]string{"chain_id"},
),
}

return metrics
Expand All @@ -225,6 +279,7 @@ func (m *Metrics) Register() {
m.Registry.MustRegister(m.MissedBlocks)
m.Registry.MustRegister(m.SoloMissedBlocks)
m.Registry.MustRegister(m.ConsecutiveMissedBlocks)
m.Registry.MustRegister(m.MissedBlocksWindow)
m.Registry.MustRegister(m.TrackedBlocks)
m.Registry.MustRegister(m.Transactions)
m.Registry.MustRegister(m.SkippedBlocks)
Expand All @@ -237,4 +292,9 @@ func (m *Metrics) Register() {
m.Registry.MustRegister(m.NodeSynced)
m.Registry.MustRegister(m.UpgradePlan)
m.Registry.MustRegister(m.ProposalEndTime)
m.Registry.MustRegister(m.SignedBlocksWindow)
m.Registry.MustRegister(m.MinSignedBlocksPerWindow)
m.Registry.MustRegister(m.DowntimeJailDuration)
m.Registry.MustRegister(m.SlashFractionDoubleSign)
m.Registry.MustRegister(m.SlashFractionDowntime)
}
90 changes: 90 additions & 0 deletions pkg/watcher/slashing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package watcher

import (
"context"
"fmt"
"time"

"github.com/cosmos/cosmos-sdk/client"
slashing "github.com/cosmos/cosmos-sdk/x/slashing/types"
"github.com/kilnfi/cosmos-validator-watcher/pkg/metrics"
"github.com/kilnfi/cosmos-validator-watcher/pkg/rpc"
"github.com/rs/zerolog/log"
)

type SlashingWatcher struct {
metrics *metrics.Metrics
pool *rpc.Pool

signedBlocksWindow int64
min_signed_per_window float64
downtime_jail_duration float64
slash_fraction_double_sign float64
slash_fraction_downtime float64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use camelCase everywhere

}

func NewSlashingWatcher(metrics *metrics.Metrics, pool *rpc.Pool) *SlashingWatcher {
return &SlashingWatcher{
metrics: metrics,
pool: pool,
}
}

func (w *SlashingWatcher) Start(ctx context.Context) error {
// update metrics every 30 minutes
ticker := time.NewTicker(30 * time.Minute)

for {
node := w.pool.GetSyncedNode()
if node == nil {
log.Warn().Msg("no node available to fetch slashing parameters")
} else if err := w.fetchSlashingParameters(ctx, node); err != nil {
log.Error().Err(err).
Str("node", node.Redacted()).
Msg("failed to fetch slashing parameters")
}

select {
case <-ctx.Done():
return nil
case <-ticker.C:
}
}
}

func (w *SlashingWatcher) fetchSlashingParameters(ctx context.Context, node *rpc.Node) error {
clientCtx := (client.Context{}).WithClient(node.Client)
queryClient := slashing.NewQueryClient(clientCtx)
sigininParams, err := queryClient.Params(ctx, &slashing.QueryParamsRequest{})
if err != nil {
return fmt.Errorf("failed to get signing infos: %w", err)
MattKetmo marked this conversation as resolved.
Show resolved Hide resolved
}

w.handleSlashingParams(node.ChainID(), sigininParams.Params)

return nil

}

func (w *SlashingWatcher) handleSlashingParams(chainID string, params slashing.Params) {
log.Info().
Str("Slashing parameters for chain:", chainID).
Str("Signed blocks window:", fmt.Sprint(params.SignedBlocksWindow)).
Str("Min signed per window:", params.MinSignedPerWindow.String()).
Str("Downtime jail duration:", params.DowntimeJailDuration.String()).
Str("Slash fraction double sign:", params.SlashFractionDoubleSign.String()).
Str("Slash fraction downtime:", params.SlashFractionDowntime.String()).
Msgf("Updating slashing metrics for chain %s", chainID)
qwertzlbert marked this conversation as resolved.
Show resolved Hide resolved

w.signedBlocksWindow = params.SignedBlocksWindow
w.min_signed_per_window, _ = params.MinSignedPerWindow.Float64()
w.downtime_jail_duration = params.DowntimeJailDuration.Seconds()
w.slash_fraction_double_sign, _ = params.SlashFractionDoubleSign.Float64()
w.slash_fraction_downtime, _ = params.SlashFractionDowntime.Float64()

w.metrics.SignedBlocksWindow.WithLabelValues(chainID).Set(float64(w.signedBlocksWindow))
w.metrics.MinSignedBlocksPerWindow.WithLabelValues(chainID).Set(w.min_signed_per_window)
w.metrics.DowntimeJailDuration.WithLabelValues(chainID).Set(w.downtime_jail_duration)
w.metrics.SlashFractionDoubleSign.WithLabelValues(chainID).Set(w.slash_fraction_double_sign)
w.metrics.SlashFractionDowntime.WithLabelValues(chainID).Set(w.slash_fraction_downtime)
}
45 changes: 45 additions & 0 deletions pkg/watcher/slashing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package watcher

import (
"testing"
"time"

cosmossdk_io_math "cosmossdk.io/math"
slashing "github.com/cosmos/cosmos-sdk/x/slashing/types"
"github.com/kilnfi/cosmos-validator-watcher/pkg/metrics"
"github.com/prometheus/client_golang/prometheus/testutil"
"gotest.tools/assert"
)

func TestSlashingWatcher(t *testing.T) {
var chainID = "test-chain"

watcher := NewSlashingWatcher(
metrics.New("cosmos_validator_watcher"),
nil,
)

t.Run("Handle Slashing Parameters", func(t *testing.T) {

min_signed_per_window := cosmossdk_io_math.LegacyMustNewDecFromStr("0.1")
slash_fraction_double_sign := cosmossdk_io_math.LegacyMustNewDecFromStr("0.01")
slash_fraction_downtime := cosmossdk_io_math.LegacyMustNewDecFromStr("0.001")

params := slashing.Params{
SignedBlocksWindow: int64(1000),
MinSignedPerWindow: min_signed_per_window,
DowntimeJailDuration: time.Duration(10) * time.Second,
SlashFractionDoubleSign: slash_fraction_double_sign,
SlashFractionDowntime: slash_fraction_downtime,
}

watcher.handleSlashingParams(chainID, params)

assert.Equal(t, float64(1000), testutil.ToFloat64(watcher.metrics.SignedBlocksWindow.WithLabelValues(chainID)))
assert.Equal(t, float64(0.1), testutil.ToFloat64(watcher.metrics.MinSignedBlocksPerWindow.WithLabelValues(chainID)))
assert.Equal(t, float64(10), testutil.ToFloat64(watcher.metrics.DowntimeJailDuration.WithLabelValues(chainID)))
assert.Equal(t, float64(0.01), testutil.ToFloat64(watcher.metrics.SlashFractionDoubleSign.WithLabelValues(chainID)))
assert.Equal(t, float64(0.001), testutil.ToFloat64(watcher.metrics.SlashFractionDowntime.WithLabelValues(chainID)))
})

}
Loading