From 45a86e08c0c17bc95939565fb3d6fa6b3adcc5cf Mon Sep 17 00:00:00 2001 From: Matt Ketmo Date: Fri, 29 Sep 2023 17:09:48 +0200 Subject: [PATCH] feat: add option to switch between gov/v1 & gov/v1beta1 (#36) --- pkg/app/flags.go | 5 + pkg/app/run.go | 19 +++- pkg/watcher/votes_v1.go | 102 ++++++++++++++++++ pkg/watcher/{votes.go => votes_v1beta1.go} | 12 +-- .../{votes_test.go => votes_v1beta1_test.go} | 2 +- 5 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 pkg/watcher/votes_v1.go rename pkg/watcher/{votes.go => votes_v1beta1.go} (84%) rename pkg/watcher/{votes_test.go => votes_v1beta1_test.go} (96%) diff --git a/pkg/app/flags.go b/pkg/app/flags.go index 51e1ee7..9f64856 100644 --- a/pkg/app/flags.go +++ b/pkg/app/flags.go @@ -43,4 +43,9 @@ var Flags = []cli.Flag{ Name: "validator", Usage: "validator address(es) to track (use :my-label to add a custom label in metrics & ouput)", }, + &cli.StringFlag{ + Name: "x-gov", + Usage: "version of the gov module to use (v1|v1beta1)", + Value: "v1beta1", + }, } diff --git a/pkg/app/run.go b/pkg/app/run.go index f4bbfb0..2b39556 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -38,6 +38,7 @@ func RunFunc(cCtx *cli.Context) error { noGov = cCtx.Bool("no-gov") noStaking = cCtx.Bool("no-staking") validators = cCtx.StringSlice("validator") + xGov = cCtx.String("x-gov") ) // @@ -100,10 +101,20 @@ func RunFunc(cCtx *cli.Context) error { }) } if !noGov { - votesWatcher := watcher.NewVotesWatcher(trackedValidators, metrics, pool) - errg.Go(func() error { - return votesWatcher.Start(ctx) - }) + switch xGov { + case "v1beta1": + votesWatcher := watcher.NewVotesV1Beta1Watcher(trackedValidators, metrics, pool) + errg.Go(func() error { + return votesWatcher.Start(ctx) + }) + case "v1": + votesWatcher := watcher.NewVotesV1Watcher(trackedValidators, metrics, pool) + errg.Go(func() error { + return votesWatcher.Start(ctx) + }) + default: + log.Warn().Msgf("unknown gov module version: %s", xGov) + } } upgradeWatcher := watcher.NewUpgradeWatcher(metrics, pool, watcher.UpgradeWatcherOptions{ CheckPendingProposals: !noGov, diff --git a/pkg/watcher/votes_v1.go b/pkg/watcher/votes_v1.go new file mode 100644 index 0000000..c42f62b --- /dev/null +++ b/pkg/watcher/votes_v1.go @@ -0,0 +1,102 @@ +package watcher + +import ( + "context" + "fmt" + "time" + + "github.com/cosmos/cosmos-sdk/client" + gov "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/kilnfi/cosmos-validator-watcher/pkg/metrics" + "github.com/kilnfi/cosmos-validator-watcher/pkg/rpc" + "github.com/rs/zerolog/log" +) + +type VotesV1Watcher struct { + metrics *metrics.Metrics + validators []TrackedValidator + pool *rpc.Pool +} + +func NewVotesV1Watcher(validators []TrackedValidator, metrics *metrics.Metrics, pool *rpc.Pool) *VotesV1Watcher { + return &VotesV1Watcher{ + metrics: metrics, + validators: validators, + pool: pool, + } +} + +func (w *VotesV1Watcher) Start(ctx context.Context) error { + ticker := time.NewTicker(1 * time.Minute) + + for { + node := w.pool.GetSyncedNode() + if node == nil { + log.Warn().Msg("no node available to fetch proposals") + } else if err := w.fetchProposals(ctx, node); err != nil { + log.Error().Err(err).Msg("failed to fetch pending proposals") + } + + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + } + } +} + +func (w *VotesV1Watcher) fetchProposals(ctx context.Context, node *rpc.Node) error { + clientCtx := (client.Context{}).WithClient(node.Client) + queryClient := gov.NewQueryClient(clientCtx) + + // Fetch all proposals in voting period + proposalsResp, err := queryClient.Proposals(ctx, &gov.QueryProposalsRequest{ + ProposalStatus: gov.StatusVotingPeriod, + }) + if err != nil { + return fmt.Errorf("failed to get proposals: %w", err) + } + + chainID := node.ChainID() + + // For each proposal, fetch validators vote + for _, proposal := range proposalsResp.GetProposals() { + w.metrics.ProposalEndTime.WithLabelValues(chainID, fmt.Sprintf("%d", proposal.Id)).Set(float64(proposal.VotingEndTime.Unix())) + + for _, validator := range w.validators { + voter := validator.AccountAddress() + if voter == "" { + log.Warn().Str("validator", validator.Name).Msg("no account address for validator") + continue + } + voteResp, err := queryClient.Vote(ctx, &gov.QueryVoteRequest{ + ProposalId: proposal.Id, + Voter: voter, + }) + if isInvalidArgumentError(err) { + w.handleVote(chainID, validator, proposal.Id, nil) + } else if err != nil { + return fmt.Errorf("failed to get validator vote for proposal %d: %w", proposal.Id, err) + } else { + vote := voteResp.GetVote() + w.handleVote(chainID, validator, proposal.Id, vote.Options) + } + } + } + + return nil +} + +func (w *VotesV1Watcher) handleVote(chainID string, validator TrackedValidator, proposalId uint64, votes []*gov.WeightedVoteOption) { + voted := false + for _, option := range votes { + if option.Option != gov.OptionEmpty { + voted = true + break + } + } + + w.metrics.Vote. + WithLabelValues(chainID, validator.Address, validator.Name, fmt.Sprintf("%d", proposalId)). + Set(metrics.BoolToFloat64(voted)) +} diff --git a/pkg/watcher/votes.go b/pkg/watcher/votes_v1beta1.go similarity index 84% rename from pkg/watcher/votes.go rename to pkg/watcher/votes_v1beta1.go index ed0d76b..a7deb7d 100644 --- a/pkg/watcher/votes.go +++ b/pkg/watcher/votes_v1beta1.go @@ -14,21 +14,21 @@ import ( "google.golang.org/grpc/status" ) -type VotesWatcher struct { +type VotesV1Beta1Watcher struct { metrics *metrics.Metrics validators []TrackedValidator pool *rpc.Pool } -func NewVotesWatcher(validators []TrackedValidator, metrics *metrics.Metrics, pool *rpc.Pool) *VotesWatcher { - return &VotesWatcher{ +func NewVotesV1Beta1Watcher(validators []TrackedValidator, metrics *metrics.Metrics, pool *rpc.Pool) *VotesV1Beta1Watcher { + return &VotesV1Beta1Watcher{ metrics: metrics, validators: validators, pool: pool, } } -func (w *VotesWatcher) Start(ctx context.Context) error { +func (w *VotesV1Beta1Watcher) Start(ctx context.Context) error { ticker := time.NewTicker(1 * time.Minute) for { @@ -47,7 +47,7 @@ func (w *VotesWatcher) Start(ctx context.Context) error { } } -func (w *VotesWatcher) fetchProposals(ctx context.Context, node *rpc.Node) error { +func (w *VotesV1Beta1Watcher) fetchProposals(ctx context.Context, node *rpc.Node) error { clientCtx := (client.Context{}).WithClient(node.Client) queryClient := gov.NewQueryClient(clientCtx) @@ -89,7 +89,7 @@ func (w *VotesWatcher) fetchProposals(ctx context.Context, node *rpc.Node) error return nil } -func (w *VotesWatcher) handleVote(chainID string, validator TrackedValidator, proposalId uint64, votes []gov.WeightedVoteOption) { +func (w *VotesV1Beta1Watcher) handleVote(chainID string, validator TrackedValidator, proposalId uint64, votes []gov.WeightedVoteOption) { voted := false for _, option := range votes { if option.Option != gov.OptionEmpty { diff --git a/pkg/watcher/votes_test.go b/pkg/watcher/votes_v1beta1_test.go similarity index 96% rename from pkg/watcher/votes_test.go rename to pkg/watcher/votes_v1beta1_test.go index 65bc64e..93dd198 100644 --- a/pkg/watcher/votes_test.go +++ b/pkg/watcher/votes_v1beta1_test.go @@ -22,7 +22,7 @@ func TestVotesWatcher(t *testing.T) { } ) - votesWatcher := NewVotesWatcher( + votesWatcher := NewVotesV1Beta1Watcher( validators, metrics.New("cosmos_validator_watcher"), nil,