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

imp(voting): only vote with accounts that have delegations #2

Merged
merged 12 commits into from
Aug 28, 2023
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
Loading