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

Masa/key assignment integration tests amend #548

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions tests/difference/node_modules/.yarn-integrity

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 30 additions & 12 deletions tests/integration/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types"
"github.com/cosmos/interchain-security/x/ccv/provider/client"
"github.com/tidwall/gjson"
)

type SendTokensAction struct {
Expand Down Expand Up @@ -826,21 +827,21 @@ func (tr TestRun) delegateTokens(
}

type unbondTokensAction struct {
chain chainID
sender validatorID
unbondFrom validatorID
amount uint
useFromConsumerKey bool
chain chainID
sender validatorID
unbondFrom validatorID
amount uint
}

func (tr TestRun) unbondTokens(
action unbondTokensAction,
verbose bool,
) {
unbondFrom := tr.validatorConfigs[action.unbondFrom].valoperAddress
if action.chain != chainID("provi") && action.useFromConsumerKey {
if tr.validatorConfigs[action.unbondFrom].useConsumerKey {
unbondFrom = tr.validatorConfigs[action.unbondFrom].consumerValoperAddress
}

//#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments.
cmd := exec.Command("docker", "exec", tr.containerConfig.instanceName, tr.chainConfigs[action.chain].binaryName,

Expand Down Expand Up @@ -927,7 +928,7 @@ func (tr TestRun) invokeDowntimeSlash(action downtimeSlashAction, verbose bool)
// Bring validator down
tr.setValidatorDowntime(action.chain, action.validator, true, verbose)
// Wait appropriate amount of blocks for validator to be slashed
tr.waitBlocks(action.chain, 3, time.Minute)
tr.waitBlocks(action.chain, 15, time.Minute)
// Bring validator back up
tr.setValidatorDowntime(action.chain, action.validator, false, verbose)
}
Expand Down Expand Up @@ -1088,20 +1089,23 @@ func (tr TestRun) invokeDoublesignSlash(
}

type assignConsumerPubKeyAction struct {
chain chainID
validator validatorID
chain chainID
validator validatorID
consumerPubkey string
// reconfigureNode will change keys the node uses and restart
reconfigureNode bool
// executing the action should raise an error
expectError bool
}

func (tr TestRun) assignConsumerPubKey(action assignConsumerPubKeyAction, verbose bool) {
valCfg := tr.validatorConfigs[action.validator]

assignKey := fmt.Sprintf(
`%s tx provider assign-consensus-key %s '%s' --from validator%s --chain-id %s --home %s --node %s --gas 900000 --keyring-backend test -b block -y`,
`%s tx provider assign-consensus-key %s '%s' --from validator%s --chain-id %s --home %s --node %s --gas 900000 --keyring-backend test -b block -y -o json`,
tr.chainConfigs[chainID("provi")].binaryName,
string(tr.chainConfigs[action.chain].chainId),
valCfg.consumerValPubKey,
action.consumerPubkey,
action.validator,
tr.chainConfigs[chainID("provi")].chainId,
tr.getValidatorHome(chainID("provi"), action.validator),
Expand All @@ -1123,6 +1127,20 @@ func (tr TestRun) assignConsumerPubKey(action assignConsumerPubKeyAction, verbos
log.Fatal(err, "\n", string(bz))
}

jsonStr := string(bz)
code := gjson.Get(jsonStr, "code")
rawLog := gjson.Get(jsonStr, "raw_log")
if !action.expectError && code.Int() != 0 {
log.Fatalf("unexpected error during key assignment - code: %s, output: %s", code, jsonStr)
}

if action.expectError {
if code.Int() == 0 {
} else if verbose {
fmt.Printf("got expected error during key assignment | code: %v | log: %s\n", code, rawLog)
}
}

// node was started with provider key
// we swap the nodes's keys for consumer keys and restart it
if action.reconfigureNode {
Expand Down Expand Up @@ -1155,7 +1173,6 @@ func (tr TestRun) assignConsumerPubKey(action assignConsumerPubKeyAction, verbos
out := scanner.Text()
if verbose {
fmt.Println("assign key - reconfigure: " + out)

}
if out == "done!!!!!!!!" {
break
Expand All @@ -1165,6 +1182,7 @@ func (tr TestRun) assignConsumerPubKey(action assignConsumerPubKeyAction, verbos
log.Fatal(err)
}

// TODO: @MSalopek refactor this so test config is not changed at runtime
// make the validator use consumer key
valCfg.useConsumerKey = true
tr.validatorConfigs[action.validator] = valCfg
Expand Down
54 changes: 11 additions & 43 deletions tests/integration/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,48 +76,16 @@ func getDefaultValidators() map[validatorID]ValidatorConfig {
privValidatorKey: `{"address":"06C0F3E47CC5C748269088DC2F36411D3AAA27C6","pub_key":{"type":"tendermint/PubKeyEd25519","value":"RrclQz9bIhkIy/gfL485g3PYMeiIku4qeo495787X10="},"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"uX+ZpDMg89a6gtqs/+MQpCTSqlkZ0nJQJOhLlCJvwvdGtyVDP1siGQjL+B8vjzmDc9gx6IiS7ip6jj3nvztfXQ=="}}`,
nodeKey: `{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"fjw4/DAhyRPnwKgXns5SV7QfswRSXMWJpHS7TyULDmJ8ofUc5poQP8dgr8bZRbCV5RV8cPqDq3FPdqwpmUbmdA=="}}`,
ipSuffix: "4",
},
validatorID("bob"): {
mnemonic: "glass trip produce surprise diamond spin excess gaze wash drum human solve dress minor artefact canoe hard ivory orange dinner hybrid moral potato jewel",
delAddress: "cosmos1dkas8mu4kyhl5jrh4nzvm65qz588hy9qcz08la",
valoperAddress: "cosmosvaloper1dkas8mu4kyhl5jrh4nzvm65qz588hy9qakmjnw",
valconsAddress: "cosmosvalcons1nx7n5uh0ztxsynn4sje6eyq2ud6rc6klc96w39",
privValidatorKey: `{"address":"99BD3A72EF12CD024E7584B3AC900AE3743C6ADF","pub_key":{"type":"tendermint/PubKeyEd25519","value":"mAN6RXYxSM4MNGSIriYiS7pHuwAcOHDQAy9/wnlSzOI="},"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"QePcwfWtOavNK7pBJrtoLMzarHKn6iBWfWPFeyV+IdmYA3pFdjFIzgw0ZIiuJiJLuke7ABw4cNADL3/CeVLM4g=="}}`,
nodeKey: `{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"TQ4vHcO/vKdzGtWpelkX53WdMQd4kTsWGFrdcatdXFvWyO215Rewn5IRP0FszPLWr2DqPzmuH8WvxYGk5aeOXw=="}}`,
ipSuffix: "5",
},
validatorID("carol"): {
mnemonic: "sight similar better jar bitter laptop solve fashion father jelly scissors chest uniform play unhappy convince silly clump another conduct behave reunion marble animal",
delAddress: "cosmos19hz4m226ztankqramvt4a7t0shejv4dc79gp9u",
valoperAddress: "cosmosvaloper19hz4m226ztankqramvt4a7t0shejv4dcm3u5f0",
valconsAddress: "cosmosvalcons1ezyrq65s3gshhx5585w6mpusq3xsj3ayzf4uv6",
privValidatorKey: `{"address":"C888306A908A217B9A943D1DAD8790044D0947A4","pub_key":{"type":"tendermint/PubKeyEd25519","value":"IHo4QEikWZfIKmM0X+N+BjKttz8HOzGs2npyjiba3Xk="},"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"z08bmSB91uFVpVmR3t2ewd/bDjZ/AzwQpe5rKjWiPG0gejhASKRZl8gqYzRf434GMq23Pwc7MazaenKOJtrdeQ=="}}`,
nodeKey: `{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"WLTcHEjbwB24Wp3z5oBSYTvtGQonz/7IQabOFw85BN0UkkyY5HDf38o8oHlFxVI26f+DFVeICuLbe9aXKGnUeg=="}}`,
ipSuffix: "6",

// consumer chain assigned key
consumerMnemonic: "clip choose cake west range gun slam cry village receive juice galaxy lend ritual range provide ritual can since verify breeze vacant play dragon",
consumerDelAddress: "cosmos1sx6j9g2rh324a342a5f0rnx7me34r9nwgf0mc7",
consumerValoperAddress: "cosmosvaloper1sx6j9g2rh324a342a5f0rnx7me34r9nwdamw5d",
consumerValconsAddress: "cosmosvalcons1kswr5sq599365kcjmhgufevfps9njf43e4lwdk",
consumerValPubKey: `{"@type":"/cosmos.crypto.ed25519.PubKey","key":"Ui5Gf1+mtWUdH8u3xlmzdKID+F3PK0sfXZ73GZ6q6is="}`,
consumerPrivValidatorKey: `{"address":"B41C3A40142963AA5B12DDD1C4E5890C0B3926B1","pub_key":{"type":"tendermint/PubKeyEd25519","value":"Ui5Gf1+mtWUdH8u3xlmzdKID+F3PK0sfXZ73GZ6q6is="},"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"3YaBAZLA+sl/E73lLfbFbG0u6DYm33ayr/0UpCt/vFBSLkZ/X6a1ZR0fy7fGWbN0ogP4Xc8rSx9dnvcZnqrqKw=="}}`,
consumerNodeKey: `{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"rxBzFedtD3pqgfJQblbxGusKOr47oBfr8ba0Iz14gobtDRZQZlSZ/UGP4pSHkVf+4vtkrkO1vRHBYJobuiP+7A=="}}`,
useConsumerKey: true,
},
}
}

func getKeyAssignValidators() map[validatorID]ValidatorConfig {
return map[validatorID]ValidatorConfig{
validatorID("alice"): {
mnemonic: "pave immune ethics wrap gain ceiling always holiday employ earth tumble real ice engage false unable carbon equal fresh sick tattoo nature pupil nuclear",
delAddress: "cosmos19pe9pg5dv9k5fzgzmsrgnw9rl9asf7ddwhu7lm",
valoperAddress: "cosmosvaloper19pe9pg5dv9k5fzgzmsrgnw9rl9asf7ddtrgtng",
valconsAddress: "cosmosvalcons1qmq08eruchr5sf5s3rwz7djpr5a25f7xw4mceq",
privValidatorKey: `{"address":"06C0F3E47CC5C748269088DC2F36411D3AAA27C6","pub_key":{"type":"tendermint/PubKeyEd25519","value":"RrclQz9bIhkIy/gfL485g3PYMeiIku4qeo495787X10="},"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"uX+ZpDMg89a6gtqs/+MQpCTSqlkZ0nJQJOhLlCJvwvdGtyVDP1siGQjL+B8vjzmDc9gx6IiS7ip6jj3nvztfXQ=="}}`,
nodeKey: `{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"fjw4/DAhyRPnwKgXns5SV7QfswRSXMWJpHS7TyULDmJ8ofUc5poQP8dgr8bZRbCV5RV8cPqDq3FPdqwpmUbmdA=="}}`,
ipSuffix: "4",
consumerMnemonic: "exile install vapor thing little toss immune notable lounge december final easy strike title end program interest quote cloth forget forward job october twenty",
consumerDelAddress: "cosmos1eeeggku6dzk3mv7wph3zq035rhtd890sjswszd",
consumerValoperAddress: "cosmosvaloper1eeeggku6dzk3mv7wph3zq035rhtd890shy69w7",
consumerValconsAddress: "cosmosvalcons1muys5jyqk4xd27e208nym85kn0t4zjcfeu63fe",
consumerValPubKey: `{"@type":"/cosmos.crypto.ed25519.PubKey","key":"ujY14AgopV907IYgPAk/5x8c9267S4fQf89nyeCPTes="}`,
consumerPrivValidatorKey: `{"address":"DF090A4880B54CD57B2A79E64D9E969BD7514B09","pub_key":{"type":"tendermint/PubKeyEd25519","value":"ujY14AgopV907IYgPAk/5x8c9267S4fQf89nyeCPTes="},"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"TRJgf7lkTjs/sj43pyweEOanyV7H7fhnVivOi0A4yjW6NjXgCCilX3TshiA8CT/nHxz3brtLh9B/z2fJ4I9N6w=="}}`,
consumerNodeKey: `{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"F966RL9pi20aXRzEBe4D0xRQJtZt696Xxz44XUON52cFc83FMn1WXJbP6arvA2JPyn2LA3DLKCFHSgALrCGXGA=="}}`,
useConsumerKey: false,
},
validatorID("bob"): {
mnemonic: "glass trip produce surprise diamond spin excess gaze wash drum human solve dress minor artefact canoe hard ivory orange dinner hybrid moral potato jewel",
Expand All @@ -135,7 +103,7 @@ func getKeyAssignValidators() map[validatorID]ValidatorConfig {
consumerValconsAddress: "cosmosvalcons1uuec3cjxajv5te08p220usrjhkfhg9wyvqn0tm",
consumerValPubKey: `{"@type":"/cosmos.crypto.ed25519.PubKey","key":"QlG+iYe6AyYpvY1z9RNJKCVlH14Q/qSz4EjGdGCru3o="}`,
consumerPrivValidatorKey: `{"address":"E73388E246EC9945E5E70A94FE4072BD937415C4","pub_key":{"type":"tendermint/PubKeyEd25519","value":"QlG+iYe6AyYpvY1z9RNJKCVlH14Q/qSz4EjGdGCru3o="},"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"OFR4w+FC6EMw5fAGTrHVexyPrjzQ7QfqgZOMgVf0izlCUb6Jh7oDJim9jXP1E0koJWUfXhD+pLPgSMZ0YKu7eg=="}}`,
consumerNodeKey: `{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"rxBzFedtD3pqgfJQblbxGusKOr47oBfr8ba0Iz14gobtDRZQZlSZ/UGP4pSHkVf+4vtkrkO1vRHBYJobuiP+7A=="}}`,
consumerNodeKey: `{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"uhPCqnL2KE8m/8OFNLQ5bN3CJr6mds+xfBi0E4umT/s2uWiJhet+vbYx88DHSdof3gGFNTIzAIxSppscBKX96w=="}}`,
useConsumerKey: false,
},
validatorID("carol"): {
Expand Down Expand Up @@ -169,7 +137,7 @@ func KeyAssignmentTestRun() TestRun {
ccvVersion: "1",
now: time.Now(),
},
validatorConfigs: getKeyAssignValidators(),
validatorConfigs: getDefaultValidators(),
chainConfigs: map[chainID]ChainConfig{
chainID("provi"): {
chainId: chainID("provi"),
Expand Down Expand Up @@ -229,7 +197,7 @@ func DefaultTestRun() TestRun {
ipPrefix: "7.7.8",
votingWaitTime: 20,
genesisChanges: ".app_state.gov.voting_params.voting_period = \"20s\" | " +
".app_state.slashing.params.signed_blocks_window = \"2\" | " +
".app_state.slashing.params.signed_blocks_window = \"20\" | " +
".app_state.slashing.params.min_signed_per_window = \"0.500000000000000000\" | " +
".app_state.slashing.params.downtime_jail_duration = \"2s\" | " +
".app_state.slashing.params.slash_fraction_downtime = \"0.010000000000000000\"",
Expand Down
16 changes: 0 additions & 16 deletions tests/integration/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +37,12 @@ func main() {
dmc.ValidateStringLiterals()
dmc.startDocker()

mul := MultiConsumerTestRun()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of just deleting these could we have the option to run them manually, just not on CI

Multiple consumer tests seem useful and I wouldn't want that previous work to goto waste

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a flag to turn multiconsumer tests on/off.

mul.SetLocalSDKPath(*localSdkPath)
mul.ValidateStringLiterals()
mul.startDocker()

keys := KeyAssignmentTestRun()
keys.SetLocalSDKPath(*localSdkPath)
keys.ValidateStringLiterals()
keys.startDocker()

wg.Add(1)
go keys.ExecuteSteps(&wg, keyAssignmentSteps)

wg.Add(1)
go tr.ExecuteSteps(&wg, happyPathSteps)

wg.Add(1)
go dmc.ExecuteSteps(&wg, democracySteps)

wg.Add(1)
go mul.ExecuteSteps(&wg, multipleConsumers)

wg.Wait()
fmt.Printf("TOTAL TIME ELAPSED: %v\n", time.Since(start))
}
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@ func (tr TestRun) getBalance(chain chainID, validator validatorID) uint {
if chain != chainID("provi") && tr.validatorConfigs[validator].useConsumerKey {
valDelAddress = tr.validatorConfigs[validator].consumerDelAddress
}

//#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments.
bz, err := exec.Command("docker", "exec", tr.containerConfig.instanceName, tr.chainConfigs[chain].binaryName,

"query", "bank", "balances",
Expand Down
10 changes: 2 additions & 8 deletions tests/integration/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,10 @@ func concatSteps(steps ...[]Step) []Step {
return concat
}

var keyAssignmentSteps = concatSteps(
stepsStartChains([]string{"consu"}, false),
stepsDelegate("consu"),
stepsAssignConsumerKeyOnStartedChain("consu", "bob"),
stepsUnbond("consu"),
stepsRedelegate("consu"),
)

var happyPathSteps = concatSteps(
stepsStartChains([]string{"consu"}, false),
stepsDelegate("consu"),
stepsAssignConsumerKeyOnStartedChain("consu", "bob"),
stepsUnbond("consu"),
stepsRedelegate("consu"),
stepsDowntime("consu"),
Expand All @@ -38,6 +31,7 @@ var democracySteps = concatSteps(
stepsDemocracy("democ"),
)

//nolint
var multipleConsumers = concatSteps(
stepsStartChains([]string{"consu", "densu"}, false),
stepsMultiConsumerDelegate("consu", "densu"),
Expand Down
59 changes: 56 additions & 3 deletions tests/integration/steps_start_chains.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ func stepsStartConsumerChain(consumerName string, proposalIndex, chainIndex uint
// the key will be present in consumer genesis initial_val_set
{
action: assignConsumerPubKeyAction{
chain: chainID(consumerName),
validator: validatorID("carol"),
chain: chainID(consumerName),
validator: validatorID("carol"),
consumerPubkey: `{"@type":"/cosmos.crypto.ed25519.PubKey","key":"Ui5Gf1+mtWUdH8u3xlmzdKID+F3PK0sfXZ73GZ6q6is="}`,
// consumer chain has not started
// we don't need to reconfigure the node
// since it will start with consumer key
Expand All @@ -79,6 +80,39 @@ func stepsStartConsumerChain(consumerName string, proposalIndex, chainIndex uint
},
},
},
{
// op should fail - key already assigned by the same validator
action: assignConsumerPubKeyAction{
chain: chainID(consumerName),
validator: validatorID("carol"),
consumerPubkey: `{"@type":"/cosmos.crypto.ed25519.PubKey","key":"Ui5Gf1+mtWUdH8u3xlmzdKID+F3PK0sfXZ73GZ6q6is="}`,
reconfigureNode: false,
expectError: true,
},
state: State{},
},
{
// op should fail - key allready assigned by another validator
action: assignConsumerPubKeyAction{
chain: chainID(consumerName),
validator: validatorID("bob"),
// same pub key as carol
consumerPubkey: `{"@type":"/cosmos.crypto.ed25519.PubKey","key":"Ui5Gf1+mtWUdH8u3xlmzdKID+F3PK0sfXZ73GZ6q6is="}`,
reconfigureNode: false,
expectError: true,
},
state: State{
chainID(consumerName): ChainState{
AssignedKeys: &map[validatorID]string{
validatorID("carol"): "cosmosvalcons1kswr5sq599365kcjmhgufevfps9njf43e4lwdk",
validatorID("bob"): "",
},
ProviderKeys: &map[validatorID]string{
validatorID("carol"): "cosmosvalcons1ezyrq65s3gshhx5585w6mpusq3xsj3ayzf4uv6",
},
},
},
},
{
action: voteGovProposalAction{
chain: chainID("provi"),
Expand Down Expand Up @@ -190,10 +224,20 @@ func stepsAssignConsumerKeyOnStartedChain(consumerName, validator string) []Step
chain: chainID(consumerName),
validator: validatorID("bob"),
// reconfigure the node -> validator was using provider key
// until this point
// until this point -> key matches config.consumerValPubKey for "bob"
consumerPubkey: `{"@type":"/cosmos.crypto.ed25519.PubKey","key":"QlG+iYe6AyYpvY1z9RNJKCVlH14Q/qSz4EjGdGCru3o="}`,
reconfigureNode: true,
},
state: State{
chainID("provi"): ChainState{
ValPowers: &map[validatorID]uint{
// this happens after some delegations
// so that the chain does not halt if 1/3 of power is offline
validatorID("alice"): 511,
validatorID("bob"): 500,
validatorID("carol"): 500,
},
},
chainID(consumerName): ChainState{
ValPowers: &map[validatorID]uint{
// this happens after some delegations
Expand All @@ -220,6 +264,15 @@ func stepsAssignConsumerKeyOnStartedChain(consumerName, validator string) []Step
channel: 0,
},
state: State{
chainID("provi"): ChainState{
ValPowers: &map[validatorID]uint{
// this happens after some delegations
// so that the chain does not halt if 1/3 of power is offline
validatorID("alice"): 511,
validatorID("bob"): 500,
validatorID("carol"): 500,
},
},
chainID(consumerName): ChainState{
ValPowers: &map[validatorID]uint{
// this happens after some delegations
Expand Down
5 changes: 4 additions & 1 deletion tests/integration/testnet-scripts/reconfigure-node.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ echo "$CONSUMER_PRIVATE_KEY" > /$CHAIN_ID/validator$VAL_ID/config/priv_validator

echo "$CONSUMER_NODE_KEY" > /$CHAIN_ID/validator$VAL_ID/config/node_key.json

# remove old key
$BIN keys delete validator$VAL_ID --keyring-backend test --home /$CHAIN_ID/validator$VAL_ID --yes

# add new key from mnemonic
echo "$CONSUMER_MNEMONIC" | $BIN keys add $CHAIN_ID-validator$VAL_ID --keyring-backend test --home /$CHAIN_ID/validator$VAL_ID --recover --output json
echo "$CONSUMER_MNEMONIC" | $BIN keys add validator$VAL_ID --keyring-backend test --home /$CHAIN_ID/validator$VAL_ID --recover --output json


# restart node with new key
Expand Down