Skip to content

Commit

Permalink
imp(gov): enable using the tool to just vote (#14)
Browse files Browse the repository at this point in the history
* enable just voting

* move getAccounts method

* add tests with coverage to makefile

* address linters, other changes

* more improvements to function calls etc

* use string events from logs and parse custom proposals struct

* use gov v1 proposals response

* address linters

* fix tests

* address linters

* update readme and changelog
  • Loading branch information
MalteHerrmann committed Aug 29, 2023
1 parent a3ac543 commit 8669a48
Show file tree
Hide file tree
Showing 12 changed files with 263 additions and 111 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
# DS Store
.DS_Store

# Coverage report
coverage.out
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Improvements

- [#14](https://github.com/MalteHerrmann/upgrade-local-node-go/pull/14) Enable just voting with the binary
- [#7](https://github.com/MalteHerrmann/upgrade-local-node-go/pull/7) Add linters plus corresponding refactors
- [#6](https://github.com/MalteHerrmann/upgrade-local-node-go/pull/6) Restructuring and refactoring
- [#4](https://github.com/MalteHerrmann/upgrade-local-node-go/pull/4) Add GH actions and Makefile for testing
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ test: test-unit

test-unit:
@go test -mod=readonly ./...

test-unit-cover:
@go test -mod=readonly -coverprofile=coverage.out ./...
30 changes: 22 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# Upgrade a local Evmos node
# Evmos Dev Utils

This utility helps executing the necessary commands to prepare a
software upgrade proposal, submit it to a running local Evmos node,
and vote on the proposal.
This tool contains several utility functionalities that are useful during
development of the Evmos blockchain.

Note, that this script is designed to work with a local node that was
At the core, all interactions go through the Evmos CLI interface, which is
called from within the Go code.

Note, that this script is designed to work with a local node that was
started by calling the `local_node.sh` script from the Evmos main repository.

## Installation
Expand All @@ -13,13 +15,25 @@ started by calling the `local_node.sh` script from the Evmos main repository.
go install github.com/MalteHerrmann/upgrade-local-node-go@latest
```

## Usage
## Features

### Upgrade Local Node

Start a local node by running `local_node.sh` from the Evmos repository.
In order to schedule an upgrade, run:
The tool creates and submits a software upgrade proposal to a running local Evmos node,
and votes on the proposal. To do so, run:

```bash
upgrade-local-node-go [TARGET_VERSION]
```

The target version must be specified in the format `vX.Y.Z(-rc*)`, e.g. `v13.0.0-rc2`.

### Vote on Proposal

The tool can vote with all keys from the configured keyring, that have delegations
to validators. This can either target the most recent proposal, or a specific one when
passing an ID to the command.

```bash
upgrade-local-node-go vote [PROPOSAL_ID]
```
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/cometbft/cometbft v0.37.2
github.com/cosmos/cosmos-sdk v0.47.4
github.com/evmos/evmos/v14 v14.0.0-rc3
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.8.4
)

Expand Down Expand Up @@ -139,7 +140,6 @@ require (
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/petermattis/goid v0.0.0-20230518223814-80aa455d8761 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
Expand Down
153 changes: 114 additions & 39 deletions gov/proposal.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,118 @@ package gov

import (
"fmt"
"log"
"strconv"
"strings"

"github.com/MalteHerrmann/upgrade-local-node-go/utils"
abcitypes "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
govv1types "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
"github.com/pkg/errors"
)

// buildUpgradeProposalCommand builds the command to submit a software upgrade proposal.
func buildUpgradeProposalCommand(targetVersion string, upgradeHeight int) []string {
return []string{
"tx", "gov", "submit-legacy-proposal", "software-upgrade", targetVersion,
"--title", fmt.Sprintf("'Upgrade to %s'", targetVersion),
"--description", fmt.Sprintf("'Upgrade to %s'", targetVersion),
"--upgrade-height", fmt.Sprintf("%d", upgradeHeight),
"--deposit", "100000000000000000000aevmos",
"--output", "json",
"--no-validate",
}
}

// GetProposalIDFromSubmitEvents looks for the proposal submission event in the given transaction events
// and returns the proposal id, if found.
func GetProposalIDFromSubmitEvents(events []sdk.StringEvent) (int, error) {
for _, event := range events {
if event.Type != "submit_proposal" {
continue
}

for _, attribute := range event.Attributes {
if attribute.Key == "proposal_id" {
proposalID, err := strconv.Atoi(attribute.Value)
if err != nil {
return 0, fmt.Errorf("error parsing proposal id: %w", err)
}

return proposalID, nil
}
}
}

return 0, fmt.Errorf("proposal submission event not found")
}

// QueryLatestProposalID queries the latest proposal ID.
func QueryLatestProposalID(bin *utils.Binary) (int, error) {
out, err := utils.ExecuteBinaryCmd(bin, utils.BinaryCmdArgs{
Subcommand: []string{"q", "gov", "proposals", "--output=json"},
Quiet: true,
})
if err != nil {
if strings.Contains(out, "no proposals found") {
return 0, errors.New("no proposals found")
}

return 0, errors.Wrap(err, "error querying proposals")
}

// NOTE: the SDK CLI command uses the x/gov v1 package
// see: https://github.com/cosmos/cosmos-sdk/blob/v0.47.4/x/gov/client/cli/query.go#L151-L159
var res govv1types.QueryProposalsResponse

err = bin.Cdc.UnmarshalJSON([]byte(out), &res)
if err != nil {
return 0, errors.Wrap(err, "error unmarshalling proposals")
}

if len(res.Proposals) == 0 {
return 0, errors.New("no proposals found")
}

return int(res.Proposals[len(res.Proposals)-1].Id), nil
}

// SubmitAllVotesForProposal submits a vote for the given proposal ID using all testing accounts.
func SubmitAllVotesForProposal(bin *utils.Binary, proposalID int) error {
accsWithDelegations, err := utils.FilterAccountsWithDelegations(bin)
if err != nil {
return errors.Wrap(err, "Error filtering accounts")
}

if len(accsWithDelegations) == 0 {
return errors.New("No accounts with delegations found")
}

utils.Wait(1)
log.Printf("Voting for proposal %d...\n", proposalID)

var out string

for _, acc := range accsWithDelegations {
out, err = VoteForProposal(bin, proposalID, acc.Name)
if err != nil {
if strings.Contains(out, fmt.Sprintf("%d: unknown proposal", proposalID)) {
return fmt.Errorf("no proposal with ID %d found", proposalID)
}

if strings.Contains(out, fmt.Sprintf("%d: inactive proposal", proposalID)) {
return fmt.Errorf("proposal with ID %d is inactive", proposalID)
}

log.Printf(" - could NOT vote using key: %s\n", acc.Name)
} else {
log.Printf(" - voted using key: %s\n", acc.Name)
}
}

return nil
}

// SubmitUpgradeProposal submits a software upgrade proposal with the given target version and upgrade height.
func SubmitUpgradeProposal(bin *utils.Binary, targetVersion string, upgradeHeight int) (int, error) {
upgradeProposal := buildUpgradeProposalCommand(targetVersion, upgradeHeight)
Expand All @@ -35,53 +139,24 @@ func SubmitUpgradeProposal(bin *utils.Binary, targetVersion string, upgradeHeigh
return 0, fmt.Errorf("error getting tx events: %w", err)
}

return GetProposalID(events)
}

// GetProposalID looks for the proposal submission event in the given transaction events
// and returns the proposal id, if found.
func GetProposalID(events []abcitypes.Event) (int, error) {
for _, event := range events {
if event.Type != "submit_proposal" {
continue
}

for _, attribute := range event.Attributes {
if attribute.Key == "proposal_id" {
proposalID, err := strconv.Atoi(attribute.Value)
if err != nil {
return 0, fmt.Errorf("error parsing proposal id: %w", err)
}

return proposalID, nil
}
}
if len(events) == 0 {
return 0, fmt.Errorf("no events found in transaction to submit proposal")
}

return 0, fmt.Errorf("proposal submission event not found")
}

// buildUpgradeProposalCommand builds the command to submit a software upgrade proposal.
func buildUpgradeProposalCommand(targetVersion string, upgradeHeight int) []string {
return []string{
"tx", "gov", "submit-legacy-proposal", "software-upgrade", targetVersion,
"--title", fmt.Sprintf("'Upgrade to %s'", targetVersion),
"--description", fmt.Sprintf("'Upgrade to %s'", targetVersion),
"--upgrade-height", fmt.Sprintf("%d", upgradeHeight),
"--deposit", "100000000000000000000aevmos",
"--output", "json",
"--no-validate",
}
return GetProposalIDFromSubmitEvents(events)
}

// VoteForProposal votes for the proposal with the given ID using the given account.
func VoteForProposal(bin *utils.Binary, proposalID int, sender string) error {
_, err := utils.ExecuteBinaryCmd(bin, utils.BinaryCmdArgs{
func VoteForProposal(bin *utils.Binary, proposalID int, sender string) (string, error) {
out, err := utils.ExecuteBinaryCmd(bin, utils.BinaryCmdArgs{
Subcommand: []string{"tx", "gov", "vote", fmt.Sprintf("%d", proposalID), "yes"},
From: sender,
UseDefaults: true,
Quiet: true,
})
if err != nil {
return out, errors.Wrap(err, fmt.Sprintf("failed to vote for proposal %d", proposalID))
}

return errors.Wrap(err, fmt.Sprintf("failed to vote for proposal %d", proposalID))
return out, nil
}
24 changes: 12 additions & 12 deletions gov/proposal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"testing"

"github.com/MalteHerrmann/upgrade-local-node-go/gov"
abcitypes "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)

Expand All @@ -14,16 +14,16 @@ func TestGetProposalID(t *testing.T) {

testcases := []struct {
name string
events []abcitypes.Event
events []sdk.StringEvent
expID int
expError bool
errContains string
}{
{
name: "pass",
events: []abcitypes.Event{{
events: []sdk.StringEvent{{
Type: "submit_proposal",
Attributes: []abcitypes.EventAttribute{
Attributes: []sdk.Attribute{
{Key: "proposal_id", Value: "5"},
{Key: "proposal_messages", Value: ",/cosmos.gov.v1.MsgExecLegacyContent"},
},
Expand All @@ -32,18 +32,18 @@ func TestGetProposalID(t *testing.T) {
},
{
name: "pass - multiple events",
events: []abcitypes.Event{
events: []sdk.StringEvent{
{
Type: "message",
Attributes: []abcitypes.EventAttribute{
Attributes: []sdk.Attribute{
{Key: "action", Value: "/cosmos.gov.v1beta1.MsgSubmitProposal"},
{Key: "sender", Value: "evmos1vv6hqcxp0w5we5rzdvf4ddhsas5gx0dep8vmv2"},
{Key: "module", Value: "gov"},
},
},
{
Type: "submit_proposal",
Attributes: []abcitypes.EventAttribute{
Attributes: []sdk.Attribute{
{Key: "proposal_id", Value: "5"},
{Key: "proposal_messages", Value: ",/cosmos.gov.v1.MsgExecLegacyContent"},
},
Expand All @@ -53,9 +53,9 @@ func TestGetProposalID(t *testing.T) {
},
{
name: "fail - no submit proposal event",
events: []abcitypes.Event{{
events: []sdk.StringEvent{{
Type: "other type",
Attributes: []abcitypes.EventAttribute{
Attributes: []sdk.Attribute{
{Key: "proposal_id", Value: "4"},
{Key: "proposal_messages", Value: ",/cosmos.gov.v1.MsgExecLegacyContent"},
},
Expand All @@ -65,9 +65,9 @@ func TestGetProposalID(t *testing.T) {
},
{
name: "fail - invalid proposal ID",
events: []abcitypes.Event{{
events: []sdk.StringEvent{{
Type: "submit_proposal",
Attributes: []abcitypes.EventAttribute{
Attributes: []sdk.Attribute{
{Key: "proposal_id", Value: "invalid"},
},
}},
Expand All @@ -80,7 +80,7 @@ func TestGetProposalID(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
propID, err := gov.GetProposalID(tc.events)
propID, err := gov.GetProposalIDFromSubmitEvents(tc.events)
if tc.expError {
require.Error(t, err, "expected error parsing proposal ID")
require.ErrorContains(t, err, tc.errContains, "expected different error")
Expand Down
Loading

0 comments on commit 8669a48

Please sign in to comment.