Skip to content

Commit 6e0470b

Browse files
imp(voting): only vote with accounts that have delegations (#2)
* 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
1 parent 0e3d55b commit 6e0470b

File tree

7 files changed

+167
-66
lines changed

7 files changed

+167
-66
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
3939

4040
### Improvements
4141

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

Makefile

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1+
# ----------------------------------
2+
# Installation
3+
install:
4+
@go install ./...
5+
6+
7+
# ----------------------------------
8+
# Tests
19
test: test-unit
210

311
test-unit:
4-
go test -mod=readonly ./...
12+
@go test -mod=readonly ./...

keys.go

+77-17
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,96 @@
11
package main
22

33
import (
4+
"encoding/json"
45
"fmt"
5-
"regexp"
6+
7+
cryptokeyring "github.com/cosmos/cosmos-sdk/crypto/keyring"
8+
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
69
)
710

8-
// getKeys returns the list of keys from the current running local node
9-
func getKeys() ([]string, error) {
10-
out, err := executeShellCommand([]string{"keys", "list"}, evmosdHome, "", false)
11+
// Account is the type for a single account.
12+
type Account struct {
13+
Name string `json:"name"`
14+
Type string `json:"type"`
15+
Address string `json:"address"`
16+
PubKey string `json:"pubkey"`
17+
Delegations []stakingtypes.Delegation `json:"delegations"`
18+
}
19+
20+
// getAccounts returns the list of keys from the current running local node
21+
func getAccounts() ([]Account, error) {
22+
out, err := executeShellCommand([]string{"keys", "list", "--output=json"}, evmosdHome, "", false, false)
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
accounts, err := parseAccountsFromOut(out)
1128
if err != nil {
1229
return nil, err
1330
}
1431

15-
return parseKeysFromOut(out)
32+
return stakingAccounts(accounts)
1633
}
1734

18-
func parseKeysFromOut(out string) ([]string, error) {
19-
// Define the regular expression pattern
20-
pattern := `\s+name:\s*(\w+)`
35+
// stakingAccounts filters the given list of accounts for those, which are used for staking.
36+
func stakingAccounts(accounts []Account) ([]Account, error) {
37+
var stakingAccs []Account
38+
39+
for _, acc := range accounts {
40+
out, err := executeShellCommand([]string{"query", "staking", "delegations", acc.Address, "--output=json"}, evmosdHome, "", false, false)
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
delegations, err := parseDelegationsFromResponse(out)
46+
if err != nil {
47+
continue
48+
}
49+
50+
acc.Delegations = delegations
51+
if len(delegations) > 0 {
52+
stakingAccs = append(stakingAccs, acc)
53+
}
54+
}
55+
56+
return stakingAccs, nil
57+
}
2158

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

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

30-
var keys []string
31-
for _, match := range matches {
32-
keys = append(keys, match[1])
72+
return delegations, nil
73+
}
74+
75+
// parseAccountsFromOut parses the keys from the given output from the keys list command.
76+
func parseAccountsFromOut(out string) ([]Account, error) {
77+
var (
78+
accounts []Account
79+
keys []cryptokeyring.KeyOutput
80+
)
81+
82+
err := json.Unmarshal([]byte(out), &keys)
83+
if err != nil {
84+
return nil, fmt.Errorf("error unmarshalling keys: %w", err)
3385
}
3486

35-
return keys, nil
87+
for _, key := range keys {
88+
accounts = append(accounts, Account{
89+
Name: key.Name,
90+
Type: key.Type,
91+
Address: key.Address,
92+
PubKey: key.PubKey,
93+
})
94+
}
95+
return accounts, nil
3696
}

keys_test.go

+62-37
Original file line numberDiff line numberDiff line change
@@ -8,54 +8,79 @@ import (
88

99
func TestParseKeysFromOut(t *testing.T) {
1010
testcases := []struct {
11-
name string
12-
out string
13-
expKeys []string
14-
expError bool
11+
name string
12+
out string
13+
expKeys []string
14+
expError bool
15+
errContains string
1516
}{
1617
{
17-
name: "pass",
18-
out: ` - address: evmos19mx9kcksequm4m4xume5h0k9fquwgmea3yvu89
19-
name: dev0
20-
pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"AmquZBW+CPcgHKx6D4YRDICzr0MNcRvl9Wm/jJn8wJxs"}'
21-
type: local
22-
- address: evmos18z7xfs864u49jcv6gkgajpteesjl5d7krpple6
23-
name: dev1
24-
pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"AtY/rqJrmhKbXrQ02xSxq/t9JGgbP2T7HPGTZJIbuT8I"}'
25-
type: local
26-
- address: evmos12rrt7vcnxvhxad6gzz0vt5psdlnurtldety57n
27-
name: dev2
28-
pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"A544btlGjv4zB/qpWT8dQqlAHrcmgZEvrFSgJnp7Yjt4"}'
29-
type: local
30-
- address: evmos1dln2gjtsfd2sny6gwdxzyxcsr0uu8sh5nwajun
31-
name: testKey1
32-
pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"Amja5pRiVw+5vPkozo6Eo20AEbYVVBqOKBi5yP7EbxyJ"}'
33-
type: local
34-
- address: evmos1qdxgxz9g2la8g9eyjdq4srlpxgrmuqd6ty88zm
35-
name: testKey2
36-
pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"A+ytKfWmkQiW0c6iOCXSL71e4b5njmJVUd1msONsPEnA"}'
37-
type: local
38-
- address: evmos1hduvvhjvu0pqu7m97pajymdsupqx3us3ntey9a
39-
name: testKey3
40-
pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"AsdAPndEVttzhUz5iSm0/FoFxkzB0oZE7DuKf3NjzXkS"}'
41-
type: local`,
42-
expKeys: []string{"dev0", "dev1", "dev2", "testKey1", "testKey2", "testKey3"},
18+
name: "pass",
19+
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\"}"}]`,
20+
expKeys: []string{"dev0", "dev1", "dev2", "testKey"},
4321
},
4422
{
45-
name: "fail - no keys",
46-
out: "invalid output",
47-
expError: true,
23+
name: "fail - no keys",
24+
out: "invalid output",
25+
expError: true,
26+
errContains: "error unmarshalling keys",
4827
},
4928
}
5029

5130
for _, tc := range testcases {
5231
t.Run(tc.name, func(t *testing.T) {
53-
keys, err := parseKeysFromOut(tc.out)
32+
accounts, err := parseAccountsFromOut(tc.out)
5433
if tc.expError {
55-
require.Error(t, err, "expected error parsing keys")
34+
require.Error(t, err, "expected error parsing accounts")
35+
require.ErrorContains(t, err, tc.errContains, "expected different error")
5636
} else {
57-
require.NoError(t, err, "unexpected error parsing keys")
58-
require.Equal(t, tc.expKeys, keys)
37+
require.NoError(t, err, "unexpected error parsing accounts")
38+
39+
var keys []string
40+
for _, account := range accounts {
41+
keys = append(keys, account.Name)
42+
}
43+
require.Equal(t, tc.expKeys, keys, "expected different keys")
44+
}
45+
})
46+
}
47+
}
48+
49+
func TestParseDelegationsFromResponse(t *testing.T) {
50+
testcases := []struct {
51+
name string
52+
out string
53+
expVals []string
54+
expError bool
55+
errContains string
56+
}{
57+
{
58+
name: "pass",
59+
out: `{"delegation_responses":[{"delegation":{"delegator_address":"evmos1v6jyld5mcu37d3dfe7kjrw0htkc4wu2mxn9y25","validator_address":"evmosvaloper1v6jyld5mcu37d3dfe7kjrw0htkc4wu2mta25tf","shares":"1000000000000000000000.000000000000000000"},"balance":{"denom":"aevmos","amount":"1000000000000000000000"}}],"pagination":{"next_key":null,"total":"0"}}`,
60+
expVals: []string{"evmosvaloper1v6jyld5mcu37d3dfe7kjrw0htkc4wu2mta25tf"},
61+
},
62+
{
63+
name: "fail - no keys",
64+
out: "invalid output",
65+
expError: true,
66+
errContains: "error unmarshalling delegations",
67+
},
68+
}
69+
70+
for _, tc := range testcases {
71+
t.Run(tc.name, func(t *testing.T) {
72+
delegations, err := parseDelegationsFromResponse(tc.out)
73+
if tc.expError {
74+
require.Error(t, err, "expected error parsing delegations")
75+
require.ErrorContains(t, err, tc.errContains, "expected different error")
76+
} else {
77+
require.NoError(t, err, "unexpected error parsing delegations")
78+
79+
var vals []string
80+
for _, delegation := range delegations {
81+
vals = append(vals, delegation.ValidatorAddress)
82+
}
83+
require.Equal(t, tc.expVals, vals, "expected different validators")
5984
}
6085
})
6186
}

main.go

+7-5
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,19 @@ func upgradeLocalNode(targetVersion string) {
7272
}
7373
fmt.Printf("Scheduled upgrade to %s at height %d.\n", targetVersion, upgradeHeight)
7474

75-
availableKeys, err := getKeys()
75+
availableKeys, err := getAccounts()
7676
if err != nil {
7777
log.Fatalf("Error getting available keys: %v", err)
7878
}
7979
wait(1)
80-
for _, key := range availableKeys {
81-
if err = voteForProposal(proposalID, key); err != nil {
82-
log.Fatalf("Error voting for upgrade: %v", err)
80+
fmt.Println("Voting for upgrade...")
81+
for _, acc := range availableKeys {
82+
if err = voteForProposal(proposalID, acc.Name); err != nil {
83+
fmt.Printf(" - could NOT vote using key: %s\n", acc.Name)
84+
} else {
85+
fmt.Printf(" - voted using key: %s\n", acc.Name)
8386
}
8487
}
85-
fmt.Printf("Cast all %d 'yes' votes for proposal %d.\n", len(availableKeys), proposalID)
8688
}
8789

8890
// wait waits for the specified amount of seconds.

proposal.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ var (
1919
// submitUpgradeProposal submits a software upgrade proposal with the given target version and upgrade height.
2020
func submitUpgradeProposal(targetVersion string, upgradeHeight int) (int, error) {
2121
upgradeProposal := buildUpgradeProposalCommand(targetVersion, upgradeHeight)
22-
out, err := executeShellCommand(upgradeProposal, evmosdHome, "dev0", true)
22+
out, err := executeShellCommand(upgradeProposal, evmosdHome, "dev0", true, false)
2323
if err != nil {
2424
return 0, err
2525
}
@@ -73,6 +73,11 @@ func buildUpgradeProposalCommand(targetVersion string, upgradeHeight int) []stri
7373

7474
// voteForProposal votes for the proposal with the given ID using the given account.
7575
func voteForProposal(proposalID int, sender string) error {
76-
_, err := executeShellCommand([]string{"tx", "gov", "vote", fmt.Sprintf("%d", proposalID), "yes"}, evmosdHome, sender, true)
76+
_, err := executeShellCommand(
77+
[]string{"tx", "gov", "vote", fmt.Sprintf("%d", proposalID), "yes"},
78+
evmosdHome,
79+
sender,
80+
true, true,
81+
)
7782
return err
7883
}

utils.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
)
1313

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

2727
cmd := exec.Command("evmosd", fullCommand...)
2828
output, err := cmd.CombinedOutput()
29-
if err != nil {
29+
if err != nil && !quiet {
3030
fmt.Println(string(output))
3131
}
3232
return string(output), err
3333
}
3434

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

0 commit comments

Comments
 (0)