Skip to content

Commit

Permalink
imp(voting): only vote with accounts that have delegations (#2)
Browse files Browse the repository at this point in the history
* suppress error in case voting fails for any keys

* add account type and convert to unmarshalling json instead of parsing text from response

* WIP parse delegation

* fix test

* nits

* add make install

* only vote with accounts that have a delegation

* use keyring type to unpack keys output

* fix output

* Update CHANGELOG.md

* fix test
  • Loading branch information
MalteHerrmann committed Aug 28, 2023
1 parent 0e3d55b commit 6e0470b
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 66 deletions.
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

- [#2](https://github.com/MalteHerrmann/upgrade-local-node-go/pull/2) Only vote if account has delegations
- [#3](https://github.com/MalteHerrmann/upgrade-local-node-go/pull/3) Use broadcast mode `sync` instead of `block`
- [#4](https://github.com/MalteHerrmann/upgrade-local-node-go/pull/4) Add GH actions and Makefile for testing

Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
# ----------------------------------
# Installation
install:
@go install ./...


# ----------------------------------
# Tests
test: test-unit

test-unit:
go test -mod=readonly ./...
@go test -mod=readonly ./...
94 changes: 77 additions & 17 deletions keys.go
Original file line number Diff line number Diff line change
@@ -1,36 +1,96 @@
package main

import (
"encoding/json"
"fmt"
"regexp"

cryptokeyring "github.com/cosmos/cosmos-sdk/crypto/keyring"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)

// getKeys returns the list of keys from the current running local node
func getKeys() ([]string, error) {
out, err := executeShellCommand([]string{"keys", "list"}, evmosdHome, "", false)
// Account is the type for a single account.
type Account struct {
Name string `json:"name"`
Type string `json:"type"`
Address string `json:"address"`
PubKey string `json:"pubkey"`
Delegations []stakingtypes.Delegation `json:"delegations"`
}

// getAccounts returns the list of keys from the current running local node
func getAccounts() ([]Account, error) {
out, err := executeShellCommand([]string{"keys", "list", "--output=json"}, evmosdHome, "", false, false)
if err != nil {
return nil, err
}

accounts, err := parseAccountsFromOut(out)
if err != nil {
return nil, err
}

return parseKeysFromOut(out)
return stakingAccounts(accounts)
}

func parseKeysFromOut(out string) ([]string, error) {
// Define the regular expression pattern
pattern := `\s+name:\s*(\w+)`
// stakingAccounts filters the given list of accounts for those, which are used for staking.
func stakingAccounts(accounts []Account) ([]Account, error) {
var stakingAccs []Account

for _, acc := range accounts {
out, err := executeShellCommand([]string{"query", "staking", "delegations", acc.Address, "--output=json"}, evmosdHome, "", false, false)
if err != nil {
return nil, err
}

delegations, err := parseDelegationsFromResponse(out)
if err != nil {
continue
}

acc.Delegations = delegations
if len(delegations) > 0 {
stakingAccs = append(stakingAccs, acc)
}
}

return stakingAccs, nil
}

// Compile the regular expression
re := regexp.MustCompile(pattern)
// parseDelegationsFromResponse parses the delegations from the given response.
func parseDelegationsFromResponse(out string) ([]stakingtypes.Delegation, error) {
var res stakingtypes.QueryDelegatorDelegationsResponse
err := cdc.UnmarshalJSON([]byte(out), &res)
if err != nil {
return nil, fmt.Errorf("error unmarshalling delegations: %w", err)
}

matches := re.FindAllStringSubmatch(out, -1)
if len(matches) == 0 {
return nil, fmt.Errorf("no keys found in output")
var delegations = make([]stakingtypes.Delegation, len(res.DelegationResponses))
for i, delegation := range res.DelegationResponses {
delegations[i] = delegation.Delegation
}

var keys []string
for _, match := range matches {
keys = append(keys, match[1])
return delegations, nil
}

// parseAccountsFromOut parses the keys from the given output from the keys list command.
func parseAccountsFromOut(out string) ([]Account, error) {
var (
accounts []Account
keys []cryptokeyring.KeyOutput
)

err := json.Unmarshal([]byte(out), &keys)
if err != nil {
return nil, fmt.Errorf("error unmarshalling keys: %w", err)
}

return keys, nil
for _, key := range keys {
accounts = append(accounts, Account{
Name: key.Name,
Type: key.Type,
Address: key.Address,
PubKey: key.PubKey,
})
}
return accounts, nil
}
99 changes: 62 additions & 37 deletions keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,54 +8,79 @@ import (

func TestParseKeysFromOut(t *testing.T) {
testcases := []struct {
name string
out string
expKeys []string
expError bool
name string
out string
expKeys []string
expError bool
errContains string
}{
{
name: "pass",
out: ` - address: evmos19mx9kcksequm4m4xume5h0k9fquwgmea3yvu89
name: dev0
pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"AmquZBW+CPcgHKx6D4YRDICzr0MNcRvl9Wm/jJn8wJxs"}'
type: local
- address: evmos18z7xfs864u49jcv6gkgajpteesjl5d7krpple6
name: dev1
pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"AtY/rqJrmhKbXrQ02xSxq/t9JGgbP2T7HPGTZJIbuT8I"}'
type: local
- address: evmos12rrt7vcnxvhxad6gzz0vt5psdlnurtldety57n
name: dev2
pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"A544btlGjv4zB/qpWT8dQqlAHrcmgZEvrFSgJnp7Yjt4"}'
type: local
- address: evmos1dln2gjtsfd2sny6gwdxzyxcsr0uu8sh5nwajun
name: testKey1
pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"Amja5pRiVw+5vPkozo6Eo20AEbYVVBqOKBi5yP7EbxyJ"}'
type: local
- address: evmos1qdxgxz9g2la8g9eyjdq4srlpxgrmuqd6ty88zm
name: testKey2
pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"A+ytKfWmkQiW0c6iOCXSL71e4b5njmJVUd1msONsPEnA"}'
type: local
- address: evmos1hduvvhjvu0pqu7m97pajymdsupqx3us3ntey9a
name: testKey3
pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"AsdAPndEVttzhUz5iSm0/FoFxkzB0oZE7DuKf3NjzXkS"}'
type: local`,
expKeys: []string{"dev0", "dev1", "dev2", "testKey1", "testKey2", "testKey3"},
name: "pass",
out: `[{"name":"dev0","type":"local","address":"evmos16qljjgus9zevcxdjscuf502zy6en427nty78c0","pubkey":"{\"@type\":\"/ethermint.crypto.v1.ethsecp256k1.PubKey\",\"key\":\"A7YjISvuApMJ/OGKVifuVqrUnJYryXPcVAR5zPzP5yz5\"}"},{"name":"dev1","type":"local","address":"evmos16cqwxv4hcqpzc7zd9fd4pw3jr4yf9jxrfr6tj0","pubkey":"{\"@type\":\"/ethermint.crypto.v1.ethsecp256k1.PubKey\",\"key\":\"A+VsC7GstX+ItZDKvWSmbQrjuvmZ0GenWB46Pi6F0fwL\"}"},{"name":"dev2","type":"local","address":"evmos1ecamqksjl7erx89lextmru88mpy669psjcehlz","pubkey":"{\"@type\":\"/ethermint.crypto.v1.ethsecp256k1.PubKey\",\"key\":\"Aha/x6t6Uaiw+md5F4XjaPleHTw6toUU9egkWCPm50wk\"}"},{"name":"testKey","type":"local","address":"evmos17slw9hdyxvxypzsdwj9vjg7uedhfw26ksqydye","pubkey":"{\"@type\":\"/ethermint.crypto.v1.ethsecp256k1.PubKey\",\"key\":\"ApDf/TgsVwangM3CciQuAoIgBvo5ZXxPHkA7K2XpeAae\"}"}]`,
expKeys: []string{"dev0", "dev1", "dev2", "testKey"},
},
{
name: "fail - no keys",
out: "invalid output",
expError: true,
name: "fail - no keys",
out: "invalid output",
expError: true,
errContains: "error unmarshalling keys",
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
keys, err := parseKeysFromOut(tc.out)
accounts, err := parseAccountsFromOut(tc.out)
if tc.expError {
require.Error(t, err, "expected error parsing keys")
require.Error(t, err, "expected error parsing accounts")
require.ErrorContains(t, err, tc.errContains, "expected different error")
} else {
require.NoError(t, err, "unexpected error parsing keys")
require.Equal(t, tc.expKeys, keys)
require.NoError(t, err, "unexpected error parsing accounts")

var keys []string
for _, account := range accounts {
keys = append(keys, account.Name)
}
require.Equal(t, tc.expKeys, keys, "expected different keys")
}
})
}
}

func TestParseDelegationsFromResponse(t *testing.T) {
testcases := []struct {
name string
out string
expVals []string
expError bool
errContains string
}{
{
name: "pass",
out: `{"delegation_responses":[{"delegation":{"delegator_address":"evmos1v6jyld5mcu37d3dfe7kjrw0htkc4wu2mxn9y25","validator_address":"evmosvaloper1v6jyld5mcu37d3dfe7kjrw0htkc4wu2mta25tf","shares":"1000000000000000000000.000000000000000000"},"balance":{"denom":"aevmos","amount":"1000000000000000000000"}}],"pagination":{"next_key":null,"total":"0"}}`,
expVals: []string{"evmosvaloper1v6jyld5mcu37d3dfe7kjrw0htkc4wu2mta25tf"},
},
{
name: "fail - no keys",
out: "invalid output",
expError: true,
errContains: "error unmarshalling delegations",
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
delegations, err := parseDelegationsFromResponse(tc.out)
if tc.expError {
require.Error(t, err, "expected error parsing delegations")
require.ErrorContains(t, err, tc.errContains, "expected different error")
} else {
require.NoError(t, err, "unexpected error parsing delegations")

var vals []string
for _, delegation := range delegations {
vals = append(vals, delegation.ValidatorAddress)
}
require.Equal(t, tc.expVals, vals, "expected different validators")
}
})
}
Expand Down
12 changes: 7 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,19 @@ func upgradeLocalNode(targetVersion string) {
}
fmt.Printf("Scheduled upgrade to %s at height %d.\n", targetVersion, upgradeHeight)

availableKeys, err := getKeys()
availableKeys, err := getAccounts()
if err != nil {
log.Fatalf("Error getting available keys: %v", err)
}
wait(1)
for _, key := range availableKeys {
if err = voteForProposal(proposalID, key); err != nil {
log.Fatalf("Error voting for upgrade: %v", err)
fmt.Println("Voting for upgrade...")
for _, acc := range availableKeys {
if err = voteForProposal(proposalID, acc.Name); err != nil {
fmt.Printf(" - could NOT vote using key: %s\n", acc.Name)
} else {
fmt.Printf(" - voted using key: %s\n", acc.Name)
}
}
fmt.Printf("Cast all %d 'yes' votes for proposal %d.\n", len(availableKeys), proposalID)
}

// wait waits for the specified amount of seconds.
Expand Down
9 changes: 7 additions & 2 deletions proposal.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ var (
// submitUpgradeProposal submits a software upgrade proposal with the given target version and upgrade height.
func submitUpgradeProposal(targetVersion string, upgradeHeight int) (int, error) {
upgradeProposal := buildUpgradeProposalCommand(targetVersion, upgradeHeight)
out, err := executeShellCommand(upgradeProposal, evmosdHome, "dev0", true)
out, err := executeShellCommand(upgradeProposal, evmosdHome, "dev0", true, false)
if err != nil {
return 0, err
}
Expand Down Expand Up @@ -73,6 +73,11 @@ func buildUpgradeProposalCommand(targetVersion string, upgradeHeight int) []stri

// voteForProposal votes for the proposal with the given ID using the given account.
func voteForProposal(proposalID int, sender string) error {
_, err := executeShellCommand([]string{"tx", "gov", "vote", fmt.Sprintf("%d", proposalID), "yes"}, evmosdHome, sender, true)
_, err := executeShellCommand(
[]string{"tx", "gov", "vote", fmt.Sprintf("%d", proposalID), "yes"},
evmosdHome,
sender,
true, true,
)
return err
}
8 changes: 4 additions & 4 deletions utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
)

// executeShellCommand executes a shell command and returns the output and error.
func executeShellCommand(command []string, home string, sender string, defaults bool) (string, error) {
func executeShellCommand(command []string, home string, sender string, defaults, quiet bool) (string, error) {
fullCommand := command
if home != "" {
fullCommand = append(fullCommand, "--home", home)
Expand All @@ -26,15 +26,15 @@ func executeShellCommand(command []string, home string, sender string, defaults

cmd := exec.Command("evmosd", fullCommand...)
output, err := cmd.CombinedOutput()
if err != nil {
if err != nil && !quiet {
fmt.Println(string(output))
}
return string(output), err
}

// getCurrentHeight returns the current block height of the node.
func getCurrentHeight() (int, error) {
output, err := executeShellCommand([]string{"q", "block", "--node", "http://localhost:26657"}, evmosdHome, "", false)
output, err := executeShellCommand([]string{"q", "block", "--node", "http://localhost:26657"}, evmosdHome, "", false, false)
if err != nil {
return 0, fmt.Errorf("error executing command: %w", err)
}
Expand Down Expand Up @@ -69,7 +69,7 @@ func getTxEvents(out string) (txEvents []abcitypes.Event, err error) {
var txOut string
nAttempts := 10
for i := 0; i < nAttempts; i++ {
txOut, err = executeShellCommand([]string{"q", "tx", txHash, "--output=json"}, evmosdHome, "", false)
txOut, err = executeShellCommand([]string{"q", "tx", txHash, "--output=json"}, evmosdHome, "", false, true)
if err == nil {
break
}
Expand Down

0 comments on commit 6e0470b

Please sign in to comment.