diff --git a/gno.land/cmd/genesis/validator.go b/gno.land/cmd/genesis/validator.go index b973114b21b..4dc74e43af1 100644 --- a/gno.land/cmd/genesis/validator.go +++ b/gno.land/cmd/genesis/validator.go @@ -27,6 +27,7 @@ func newValidatorCmd(io *commands.IO) *commands.Command { cmd.AddSubCommands( newValidatorAddCmd(cfg, io), + newValidatorRemoveCmd(cfg, io), ) return cmd diff --git a/gno.land/cmd/genesis/validator_add.go b/gno.land/cmd/genesis/validator_add.go index b4a73b0e814..905fc3b8f2b 100644 --- a/gno.land/cmd/genesis/validator_add.go +++ b/gno.land/cmd/genesis/validator_add.go @@ -17,7 +17,6 @@ var ( errInvalidName = errors.New("invalid validator name") errPublicKeyMismatch = errors.New("provided public key and address do not match") errAddressPresent = errors.New("validator with same address already present in genesis.json") - errPubKeyPresent = errors.New("validator with same public key already present in genesis.json") ) type validatorAddCfg struct { @@ -124,7 +123,7 @@ func execValidatorAdd(cfg *validatorAddCfg, io *commands.IO) error { // Add the validator genesis.Validators = append(genesis.Validators, validator) - // Save the updated + // Save the updated genesis if err := genesis.SaveAs(cfg.validatorCfg.genesisPath); err != nil { return fmt.Errorf("unable to save genesis.json, %w", err) } diff --git a/gno.land/cmd/genesis/validator_add_test.go b/gno.land/cmd/genesis/validator_add_test.go index 260e4a0015b..cca78ea9019 100644 --- a/gno.land/cmd/genesis/validator_add_test.go +++ b/gno.land/cmd/genesis/validator_add_test.go @@ -14,28 +14,32 @@ import ( "github.com/stretchr/testify/require" ) +// getDummyKey generates a random public key, +// and returns the key info +func getDummyKey(t *testing.T) keys.Info { + t.Helper() + + mnemonic, err := client.GenerateMnemonic(256) + require.NoError(t, err) + + kb := keys.NewInMemory() + + info, err := kb.CreateAccount( + "dummy", + mnemonic, + "", + "", + uint32(0), + uint32(0), + ) + require.NoError(t, err) + + return info +} + func TestGenesis_Validator_Add(t *testing.T) { t.Parallel() - getDummyKey := func() keys.Info { - mnemonic, err := client.GenerateMnemonic(256) - require.NoError(t, err) - - kb := keys.NewInMemory() - - info, err := kb.CreateAccount( - "dummy", - mnemonic, - "", - "", - uint32(0), - uint32(0), - ) - require.NoError(t, err) - - return info - } - t.Run("invalid genesis file", func(t *testing.T) { t.Parallel() @@ -87,7 +91,7 @@ func TestGenesis_Validator_Add(t *testing.T) { genesis := getDefaultGenesis() require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - key := getDummyKey() + key := getDummyKey(t) // Create the command cmd := newRootCmd(commands.NewTestIO()) @@ -116,7 +120,7 @@ func TestGenesis_Validator_Add(t *testing.T) { genesis := getDefaultGenesis() require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - key := getDummyKey() + key := getDummyKey(t) // Create the command cmd := newRootCmd(commands.NewTestIO()) @@ -145,7 +149,7 @@ func TestGenesis_Validator_Add(t *testing.T) { genesis := getDefaultGenesis() require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - key := getDummyKey() + key := getDummyKey(t) // Create the command cmd := newRootCmd(commands.NewTestIO()) @@ -177,8 +181,8 @@ func TestGenesis_Validator_Add(t *testing.T) { require.NoError(t, genesis.SaveAs(tempGenesis.Name())) dummyKeys := []keys.Info{ - getDummyKey(), - getDummyKey(), + getDummyKey(t), + getDummyKey(t), } // Create the command @@ -208,8 +212,8 @@ func TestGenesis_Validator_Add(t *testing.T) { t.Cleanup(cleanup) dummyKeys := []keys.Info{ - getDummyKey(), - getDummyKey(), + getDummyKey(t), + getDummyKey(t), } genesis := getDefaultGenesis() @@ -250,7 +254,7 @@ func TestGenesis_Validator_Add(t *testing.T) { tempGenesis, cleanup := testutils.NewTestFile(t) t.Cleanup(cleanup) - key := getDummyKey() + key := getDummyKey(t) genesis := getDefaultGenesis() require.NoError(t, genesis.SaveAs(tempGenesis.Name())) diff --git a/gno.land/cmd/genesis/validator_remove.go b/gno.land/cmd/genesis/validator_remove.go new file mode 100644 index 00000000000..1820c904aa5 --- /dev/null +++ b/gno.land/cmd/genesis/validator_remove.go @@ -0,0 +1,71 @@ +package main + +import ( + "context" + "errors" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" +) + +var errValidatorNotPresent = errors.New("validator not present in genesis.json") + +// newValidatorRemoveCmd creates the genesis validator remove subcommand +func newValidatorRemoveCmd(validatorCfg *validatorCfg, io *commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "remove", + ShortUsage: "validator remove [flags]", + LongHelp: "Removes a validator from the genesis.json", + }, + commands.NewEmptyConfig(), + func(_ context.Context, _ []string) error { + return execValidatorRemove(validatorCfg, io) + }, + ) +} + +func execValidatorRemove(cfg *validatorCfg, io *commands.IO) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Check the validator address + address, err := crypto.AddressFromString(cfg.address) + if err != nil { + return fmt.Errorf("invalid validator address, %w", err) + } + + index := -1 + + for indx, validator := range genesis.Validators { + if validator.Address == address { + index = indx + + break + } + } + + if index < 0 { + return errors.New("validator not present in genesis.json") + } + + // Drop the validator + genesis.Validators = append(genesis.Validators[:index], genesis.Validators[index+1:]...) + + // Save the updated genesis + if err := genesis.SaveAs(cfg.genesisPath); err != nil { + return fmt.Errorf("unable to save genesis.json, %w", err) + } + + io.Printfln( + "Validator with address %s removed from genesis file", + cfg.address, + ) + + return nil +} diff --git a/gno.land/cmd/genesis/validator_remove_test.go b/gno.land/cmd/genesis/validator_remove_test.go new file mode 100644 index 00000000000..dd4c070614a --- /dev/null +++ b/gno.land/cmd/genesis/validator_remove_test.go @@ -0,0 +1,134 @@ +package main + +import ( + "context" + "testing" + + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Validator_Remove(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis file", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "validator", + "remove", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "unable to load genesis") + }) + + t.Run("invalid validator address", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "validator", + "remove", + "--genesis-path", + tempGenesis.Name(), + "--address", + "dummyaddress", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "invalid validator address") + }) + + t.Run("validator not found", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + dummyKeys := []keys.Info{ + getDummyKey(t), + getDummyKey(t), + } + + genesis := getDefaultGenesis() + + // Set an existing validator + genesis.Validators = append(genesis.Validators, types.GenesisValidator{ + Address: dummyKeys[0].GetAddress(), + PubKey: dummyKeys[0].GetPubKey(), + Power: 1, + Name: "example", + }) + + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "validator", + "remove", + "--genesis-path", + tempGenesis.Name(), + "--address", + dummyKeys[1].GetPubKey().Address().String(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errValidatorNotPresent.Error()) + }) + + t.Run("validator removed", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + dummyKey := getDummyKey(t) + + genesis := getDefaultGenesis() + + // Set an existing validator + genesis.Validators = append(genesis.Validators, types.GenesisValidator{ + Address: dummyKey.GetAddress(), + PubKey: dummyKey.GetPubKey(), + Power: 1, + Name: "example", + }) + + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "validator", + "remove", + "--genesis-path", + tempGenesis.Name(), + "--address", + dummyKey.GetPubKey().Address().String(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.NoError(t, cmdErr) + }) +}