Skip to content
Merged
13 changes: 13 additions & 0 deletions proto/poa/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ service Msg {
rpc AddValidator(MsgAddValidator) returns (MsgAddValidatorResponse);
// Removes an existing validator from the authority
rpc RemoveValidator(MsgRemoveValidator) returns (MsgRemoveValidatorResponse);
// Allows a validator to remove itself from the authority
rpc SelfRemoveValidator(MsgSelfRemoveValidator) returns (MsgSelfRemoveValidatorResponse);
}

// MsgAddValidator defines a message for adding a new validator
Expand Down Expand Up @@ -46,3 +48,14 @@ message MsgRemoveValidator {
// MsgRemoveValidatorResponse defines the response for removing an existing
// validator
message MsgRemoveValidatorResponse {}

// MsgSelfRemoveValidator defines a message for a validator to remove itself
message MsgSelfRemoveValidator {
option (cosmos.msg.v1.signer) = "address";

string address = 1
[ (cosmos_proto.scalar) = "cosmos.AddressString" ];
Comment thread
AdriaCarrera marked this conversation as resolved.
}
// MsgSelfRemoveValidatorResponse defines the response for a validator
// removing itself
message MsgSelfRemoveValidatorResponse {}
263 changes: 263 additions & 0 deletions tests/integration/poa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@ import (
"math/rand"
"time"

sdkmath "cosmossdk.io/math"
abcitypes "github.com/cometbft/cometbft/abci/types"
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
sdktypes "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/address"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/cosmos/evm/testutil/integration/base/factory"
"github.com/cosmos/evm/testutil/keyring"
"github.com/stretchr/testify/require"
"github.com/xrplevm/node/v10/testutil/integration/exrp/utils"
poatypes "github.com/xrplevm/node/v10/x/poa/types"
Expand Down Expand Up @@ -592,6 +597,264 @@ func (s *TestSuite) TestAddValidator_MaximumValidators() {
}
}

// SelfRemoveValidator tests

func (s *TestSuite) TestSelfRemoveValidator_ExistingValidator() {
tt := []struct {
name string
valIndex int
beforeRun func(valIndex int, valAddr sdktypes.ValAddress)
afterRun func(valIndex int)
}{
{
name: "self remove existing validator - status bonded",
valIndex: 0,
beforeRun: func(_ int, valAddr sdktypes.ValAddress) {
resVal, err := s.Network().GetStakingClient().Validator(
s.Network().GetContext(),
&stakingtypes.QueryValidatorRequest{
ValidatorAddr: valAddr.String(),
},
)
require.NoError(s.T(), err)
require.Equal(s.T(), stakingtypes.Bonded, resVal.Validator.Status)
require.Equal(s.T(), sdktypes.DefaultPowerReduction.ToLegacyDec(), resVal.Validator.DelegatorShares)
require.NotZero(s.T(), resVal.Validator.Tokens)
},
},
{
name: "self remove existing validator - status jailed",
valIndex: 1,
beforeRun: func(valIndex int, valAddr sdktypes.ValAddress) {
valSet := s.Network().GetValidatorSet()
require.NoError(
s.T(),
s.Network().NextNBlocksWithValidatorFlags(
slashingtypes.DefaultSignedBlocksWindow,
utils.NewValidatorFlags(
len(valSet.Validators),
utils.NewValidatorFlagOverride(valIndex, cmtproto.BlockIDFlagAbsent),
),
),
)

resVal, err := s.Network().GetStakingClient().Validator(
s.Network().GetContext(),
&stakingtypes.QueryValidatorRequest{
ValidatorAddr: valAddr.String(),
},
)
require.NoError(s.T(), err)
require.True(s.T(), resVal.Validator.Jailed)
require.Equal(s.T(), stakingtypes.Unbonding, resVal.Validator.Status)
},
},
{
name: "self remove existing validator - status tombstoned",
valIndex: 1,
beforeRun: func(valIndex int, _ sdktypes.ValAddress) {
valSet := s.Network().GetValidatorSet()
cmtValAddr := sdktypes.AccAddress(valSet.Validators[valIndex].Address.Bytes())

require.NoError(s.T(), s.Network().NextBlockWithMisBehaviors(
[]abcitypes.Misbehavior{
{
Type: abcitypes.MisbehaviorType_DUPLICATE_VOTE,
Validator: abcitypes.Validator{
Address: cmtValAddr,
},
Height: s.Network().GetContext().BlockHeight(),
TotalVotingPower: s.Network().GetValidatorSet().TotalVotingPower(),
},
},
))

cmtValConsAddr := sdktypes.ConsAddress(valSet.Validators[valIndex].Address.Bytes())
info, err := s.Network().GetSlashingClient().SigningInfo(
s.Network().GetContext(),
&slashingtypes.QuerySigningInfoRequest{
ConsAddress: cmtValConsAddr.String(),
},
)
require.NoError(s.T(), err)
require.True(s.T(), info.ValSigningInfo.Tombstoned)
},
afterRun: func(valIndex int) {
valSet := s.Network().GetValidatorSet()
cmtValConsAddr := sdktypes.ConsAddress(valSet.Validators[valIndex].Address.Bytes())
info, err := s.Network().GetSlashingClient().SigningInfo(
s.Network().GetContext(),
&slashingtypes.QuerySigningInfoRequest{
ConsAddress: cmtValConsAddr.String(),
},
)
require.NoError(s.T(), err)
require.True(s.T(), info.ValSigningInfo.Tombstoned)
},
},
}

for _, tc := range tt {
s.Run(tc.name, func() {
s.SetupTest()

validators := s.Network().GetValidators()
require.NotZero(s.T(), len(validators))

validator := validators[tc.valIndex]
valAddr, err := sdktypes.ValAddressFromBech32(validator.OperatorAddress)
Comment on lines +701 to +705

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate tc.valIndex bounds before indexing validators.

Line 702 only checks non-empty validators, but cases use index 1; this can panic on a single-validator setup.

Suggested fix
 			validators := s.Network().GetValidators()
 			require.NotZero(s.T(), len(validators))
+			require.Greater(s.T(), len(validators), tc.valIndex)

 			validator := validators[tc.valIndex]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/poa_test.go` around lines 701 - 705, The code checks that
the validators slice is non-empty but does not validate that tc.valIndex is
within the valid range of the validators slice before indexing into it on the
line where validator is assigned. Add a bounds check using require.Less or
require.True to ensure that tc.valIndex is less than len(validators) after
confirming validators is non-empty, preventing potential panics when test cases
use indices like 1 on single-validator setups.

require.NoError(s.T(), err)

valKey := s.keyring.GetKey(tc.valIndex)
require.Equal(s.T(), valAddr, sdktypes.ValAddress(valKey.AccAddr))

if tc.beforeRun != nil {
tc.beforeRun(tc.valIndex, valAddr)
}

msg := poatypes.NewMsgSelfRemoveValidator(valKey.AccAddr.String())
res, err := s.factory.CommitCosmosTx(valKey.Priv, factory.CosmosTxArgs{
Msgs: []sdktypes.Msg{msg},
})
require.NoError(s.T(), err)
require.Equal(s.T(), uint32(0), res.Code, res.Log)

resVal, err := s.Network().GetStakingClient().Validator(
s.Network().GetContext(),
&stakingtypes.QueryValidatorRequest{
ValidatorAddr: valAddr.String(),
},
)
require.NoError(s.T(), err)
require.True(s.T(), resVal.Validator.DelegatorShares.IsZero())
require.True(s.T(), resVal.Validator.Tokens.IsZero())
require.Equal(s.T(), stakingtypes.Unbonding, resVal.Validator.Status)

require.NoError(s.T(), s.Network().NextBlockAfter(stakingtypes.DefaultUnbondingTime))

_, err = s.Network().GetStakingClient().Validator(
s.Network().GetContext(),
&stakingtypes.QueryValidatorRequest{
ValidatorAddr: valAddr.String(),
},
)
require.Contains(s.T(), err.Error(), fmt.Sprintf("validator %s not found", valAddr.String()))

if tc.afterRun != nil {
tc.afterRun(tc.valIndex)
}
})
}
}

func (s *TestSuite) TestSelfRemoveValidator_UnexistingValidator() {
funder := s.keyring.GetKey(0)
nonValidator := keyring.NewKey()

fundAmount := sdktypes.NewCoins(sdktypes.NewCoin(s.Network().GetBaseDenom(), sdkmath.NewInt(1_000_000_000_000_000_000)))
sendRes, err := s.factory.CommitCosmosTx(funder.Priv, factory.CosmosTxArgs{
Msgs: []sdktypes.Msg{banktypes.NewMsgSend(funder.AccAddr, nonValidator.AccAddr, fundAmount)},
})
require.NoError(s.T(), err)
require.Equal(s.T(), uint32(0), sendRes.Code, sendRes.Log)

nonValAddr := sdktypes.ValAddress(nonValidator.AccAddr)

_, err = s.Network().GetStakingClient().Validator(
s.Network().GetContext(),
&stakingtypes.QueryValidatorRequest{
ValidatorAddr: nonValAddr.String(),
},
)
require.Contains(s.T(), err.Error(), fmt.Sprintf("validator %s not found", nonValAddr.String()))

Comment on lines +763 to +770

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard err before checking the not-found message in non-validator precheck.

Line 769 dereferences err without asserting it exists, which can panic and hide the real failure.

Suggested fix
 	_, err = s.Network().GetStakingClient().Validator(
 		s.Network().GetContext(),
 		&stakingtypes.QueryValidatorRequest{
 			ValidatorAddr: nonValAddr.String(),
 		},
 	)
+	require.Error(s.T(), err)
 	require.Contains(s.T(), err.Error(), fmt.Sprintf("validator %s not found", nonValAddr.String()))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_, err = s.Network().GetStakingClient().Validator(
s.Network().GetContext(),
&stakingtypes.QueryValidatorRequest{
ValidatorAddr: nonValAddr.String(),
},
)
require.Contains(s.T(), err.Error(), fmt.Sprintf("validator %s not found", nonValAddr.String()))
_, err = s.Network().GetStakingClient().Validator(
s.Network().GetContext(),
&stakingtypes.QueryValidatorRequest{
ValidatorAddr: nonValAddr.String(),
},
)
require.Error(s.T(), err)
require.Contains(s.T(), err.Error(), fmt.Sprintf("validator %s not found", nonValAddr.String()))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/poa_test.go` around lines 763 - 770, The test is
dereferencing err without first asserting it is not nil, which can cause a panic
and hide the actual test failure. Before calling err.Error() in the
require.Contains assertion on the line after the Validator call, add a guard
using require.Error(s.T(), err) to ensure the error is not nil and the Validator
call actually failed as expected.

gas := uint64(1_000_000)
msg := poatypes.NewMsgSelfRemoveValidator(nonValidator.AccAddr.String())
res, err := s.factory.CommitCosmosTx(nonValidator.Priv, factory.CosmosTxArgs{
Msgs: []sdktypes.Msg{msg},
Gas: &gas,
})
require.NoError(s.T(), err)
require.NotEqual(s.T(), uint32(0), res.Code)
require.Contains(s.T(), res.Log, poatypes.ErrAddressIsNotAValidator.Error())
}

func (s *TestSuite) TestSelfRemoveValidator_DifferentSigner() {
signer := s.keyring.GetKey(0)
target := s.keyring.GetKey(1)
targetValAddr := sdktypes.ValAddress(target.AccAddr)

gas := uint64(1_000_000)
msg := poatypes.NewMsgSelfRemoveValidator(target.AccAddr.String())
res, err := s.factory.CommitCosmosTx(signer.Priv, factory.CosmosTxArgs{
Msgs: []sdktypes.Msg{msg},
Gas: &gas,
})
require.NoError(s.T(), err)
require.NotEqual(s.T(), uint32(0), res.Code)
require.Contains(s.T(), res.Log, sdkerrors.ErrUnauthorized.Error())

resVal, err := s.Network().GetStakingClient().Validator(
s.Network().GetContext(),
&stakingtypes.QueryValidatorRequest{
ValidatorAddr: targetValAddr.String(),
},
)
require.NoError(s.T(), err)
require.Equal(s.T(), stakingtypes.Bonded, resVal.Validator.Status)
require.False(s.T(), resVal.Validator.Tokens.IsZero())
}

func (s *TestSuite) TestSelfRemoveValidator_AlreadyRemoved() {
validators := s.Network().GetValidators()
require.NotZero(s.T(), len(validators))

valIndex := 0
validator := validators[valIndex]
valAddr, err := sdktypes.ValAddressFromBech32(validator.OperatorAddress)
require.NoError(s.T(), err)

valKey := s.keyring.GetKey(valIndex)
require.Equal(s.T(), valAddr, sdktypes.ValAddress(valKey.AccAddr))

msg := poatypes.NewMsgSelfRemoveValidator(valKey.AccAddr.String())

res, err := s.factory.CommitCosmosTx(valKey.Priv, factory.CosmosTxArgs{
Msgs: []sdktypes.Msg{msg},
})
require.NoError(s.T(), err)
require.Equal(s.T(), uint32(0), res.Code, res.Log)

resVal, err := s.Network().GetStakingClient().Validator(
s.Network().GetContext(),
&stakingtypes.QueryValidatorRequest{
ValidatorAddr: valAddr.String(),
},
)
require.NoError(s.T(), err)
require.Equal(s.T(), stakingtypes.Unbonding, resVal.Validator.Status)
require.True(s.T(), resVal.Validator.Tokens.IsZero())

gas := uint64(1_000_000)
res, err = s.factory.CommitCosmosTx(valKey.Priv, factory.CosmosTxArgs{
Msgs: []sdktypes.Msg{msg},
Gas: &gas,
})
require.NoError(s.T(), err)
require.NotEqual(s.T(), uint32(0), res.Code)
require.Contains(s.T(), res.Log, stakingtypes.ErrNoDelegatorForAddress.Error())

resVal, err = s.Network().GetStakingClient().Validator(
s.Network().GetContext(),
&stakingtypes.QueryValidatorRequest{
ValidatorAddr: valAddr.String(),
},
)
require.NoError(s.T(), err)
require.Equal(s.T(), stakingtypes.Unbonding, resVal.Validator.Status)
require.True(s.T(), resVal.Validator.Tokens.IsZero())
}

// RemoveValidator tests

func (s *TestSuite) TestRemoveValidator_UnexistingValidator() {
Expand Down
30 changes: 30 additions & 0 deletions x/poa/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,33 @@ func (k Keeper) ExecuteRemoveValidator(ctx sdk.Context, validatorAddress string)

return nil
}

func (k Keeper) ExecuteSelfRemoveValidator(ctx sdk.Context, validatorAddress string) error {
accAddress, err := sdk.AccAddressFromBech32(validatorAddress)
if err != nil {
return err
}
valAddress := sdk.ValAddress(accAddress)
Comment thread
AdriaCarrera marked this conversation as resolved.

validator, err := k.sk.GetValidator(ctx, valAddress)
if err != nil {
ctx.Logger().Warn("Error getting validator", "error", err)
return types.ErrAddressIsNotAValidator
}
Comment on lines +300 to +304

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not collapse all GetValidator failures into “not a validator”.

Returning ErrAddressIsNotAValidator for every error masks real staking keeper failures (store/query issues) and can mislead callers. Only map the specific not-found case; propagate other errors unchanged.

Proposed fix
 	validator, err := k.sk.GetValidator(ctx, valAddress)
 	if err != nil {
-		return types.ErrAddressIsNotAValidator
+		if errors.IsOf(err, stakingtypes.ErrNoValidatorFound) {
+			return types.ErrAddressIsNotAValidator
+		}
+		return err
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@x/poa/keeper/keeper.go` around lines 299 - 302, In the GetValidator error
handling block around line 299-302, the current code returns
ErrAddressIsNotAValidator for all errors from k.sk.GetValidator, which masks
real staking keeper failures. Instead, check if the error is specifically a
not-found error (typically using sdk.ErrUnknownAddress or similar) and only then
return ErrAddressIsNotAValidator; for all other errors, return the original
error unchanged to preserve important debugging information about store or query
issues.


err = k.ExecuteRemoveValidator(ctx, accAddress.String())
if err != nil {
return err
}

ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeSelfRemoveValidator,
sdk.NewAttribute(types.AttributeValidator, valAddress.String()),
sdk.NewAttribute(types.AttributeHeight, fmt.Sprintf("%d", ctx.BlockHeight())),
sdk.NewAttribute(types.AttributeStakingTokens, fmt.Sprintf("%d", validator.Tokens)),
),
)

return nil
}
2 changes: 1 addition & 1 deletion x/poa/keeper/msg_server_add_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func TestMsgServer_AddValidator(t *testing.T) {
stakingKeeper.EXPECT().GetAllValidators(ctx).Return([]stakingtypes.Validator{}, nil)
stakingKeeper.EXPECT().GetValidator(ctx, gomock.Any()).Return(stakingtypes.Validator{Tokens: math.NewInt(0)}, nil)
stakingKeeper.EXPECT().GetAllDelegatorDelegations(ctx, gomock.Any()).Return([]stakingtypes.Delegation{}, nil)
stakingKeeper.EXPECT().GetUnbondingDelegationsFromValidator(ctx, gomock.Any()).Return([]stakingtypes.UnbondingDelegation{}, nil)
stakingKeeper.EXPECT().GetUnbondingDelegations(ctx, gomock.Any(), gomock.Any()).Return([]stakingtypes.UnbondingDelegation{}, nil)
},
bankMocks: func(ctx sdk.Context, bankKeeper *testutil.MockBankKeeper) {
bankKeeper.EXPECT().GetBalance(ctx, gomock.Any(), gomock.Any()).Return(sdk.Coin{
Expand Down
18 changes: 18 additions & 0 deletions x/poa/keeper/msg_server_self_remove_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package keeper

import (
"context"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/xrplevm/node/v10/x/poa/types"
)

func (k msgServer) SelfRemoveValidator(goCtx context.Context, msg *types.MsgSelfRemoveValidator) (*types.MsgSelfRemoveValidatorResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
err := k.ExecuteSelfRemoveValidator(ctx, msg.Address)
if err != nil {
return nil, err
}

return &types.MsgSelfRemoveValidatorResponse{}, nil
}
Loading