Skip to content

Commit

Permalink
[Gateway] Enforce minimum stake when staking (#843)
Browse files Browse the repository at this point in the history
## Summary

1. Adds minimum stake validation to the gateway stake message handler
(i.e. total stake must be >= minimum stake).
2. Updates error returns in the same handler to ensure all error paths
return appropriate gRPC status errors.
3. Replaces some warn and error level logs with info level, which I
believe is more appropriate (until we have a practical debug level, at
which point they should become debug logs).

## Dependencies

- #809 

## Dependents

- #844 
- #845 
- #847 
- #848 
- #849
- #850
- #857

## Issue

Gateway staking min stake validation.

- #612

## Type of change

Select one or more from the following:

- [x] New feature, functionality or library
- [x] Consensus breaking; add the `consensus-breaking` label if so. See
#791 for details
- [ ] Bug fix
- [ ] Code health or cleanup
- [ ] Documentation
- [ ] Other (specify)

## Testing

<!-- READ & DELETE:
- Documentation changes: only keep this if you're making documentation
changes
- Unit Testing: Remove this if you didn't make code changes
- E2E Testing: Remove this if you didn't make code changes
- See the quickstart guide for instructions:
https://dev.poktroll.com/developer_guide/quickstart
- DevNet E2E Testing: Remove this if you didn't make code changes
- THIS IS VERY EXPENSIVE: only do it after all the reviews are complete.
- Optionally run `make trigger_ci` if you want to re-trigger tests
without any code changes
- If tests fail, try re-running failed tests only using the GitHub UI as
shown
[here](https://github.com/pokt-network/poktroll/assets/1892194/607984e9-0615-4569-9452-4c730190c1d2)
-->

- [ ] **Documentation**: `make docusaurus_start`; only needed if you
make doc changes
- [x] **Unit Tests**: `make go_develop_and_test`
- [ ] **LocalNet E2E Tests**: `make test_e2e`
- [ ] **DevNet E2E Tests**: Add the `devnet-test-e2e` label to the PR.

## Sanity Checklist

- [x] I have tested my changes using the available tooling
- [x] I have commented my code
- [x] I have performed a self-review of my own code; both comments &
source code
- [ ] I create and reference any new tickets, if applicable
- [ ] I have left TODOs throughout the codebase, if applicable

---------

Co-authored-by: Redouane Lakrache <r3d0ne@gmail.com>
Co-authored-by: Daniel Olshansky <olshansky.daniel@gmail.com>
Co-authored-by: red-0ne <red-0ne@users.noreply.github.com>
  • Loading branch information
4 people authored Oct 4, 2024
1 parent 1fafa38 commit a752b66
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 39 deletions.
52 changes: 35 additions & 17 deletions x/gateway/keeper/msg_server_stake_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/pokt-network/poktroll/telemetry"
"github.com/pokt-network/poktroll/x/gateway/types"
Expand All @@ -24,46 +26,62 @@ func (k msgServer) StakeGateway(
ctx := sdk.UnwrapSDKContext(goCtx)

logger := k.Logger().With("method", "StakeGateway")
logger.Info(fmt.Sprintf("About to stake gateway with msg: %v", msg))
logger.Info(fmt.Sprintf("about to stake gateway with msg: %v", msg))

if err := msg.ValidateBasic(); err != nil {
return nil, err
return nil, status.Error(codes.InvalidArgument, err.Error())
}

// Retrieve the address of the gateway
gatewayAddress, err := sdk.AccAddressFromBech32(msg.Address)
// NB: This SHOULD NEVER happen because msg.ValidateBasic() validates the address as bech32.
if err != nil {
// TODO_TECHDEBT(#384): determine whether to continue using cosmos logger for debug level.
logger.Info(fmt.Sprintf("ERROR: could not parse address %q", msg.Address))
return nil, status.Error(codes.InvalidArgument, err.Error())
}

// Check if the gateway already exists or not
var err error
var coinsToEscrow sdk.Coin
gateway, isGatewayFound := k.GetGateway(ctx, msg.Address)
if !isGatewayFound {
logger.Info(fmt.Sprintf("Gateway not found. Creating new gateway for address %q", msg.Address))
logger.Info(fmt.Sprintf("gateway not found; creating new gateway for address %q", msg.Address))
gateway = k.createGateway(ctx, msg)
coinsToEscrow = *msg.Stake
} else {
logger.Info(fmt.Sprintf("Gateway found. About to try and update gateway for address %q", msg.Address))
logger.Info(fmt.Sprintf("gateway found; about to try and update gateway for address %q", msg.Address))
currGatewayStake := *gateway.Stake
if err = k.updateGateway(ctx, &gateway, msg); err != nil {
logger.Error(fmt.Sprintf("could not update gateway for address %q due to error %v", msg.Address, err))
return nil, err
return nil, status.Error(codes.InvalidArgument, err.Error())
}
coinsToEscrow, err = (*msg.Stake).SafeSub(currGatewayStake)
if err != nil {
return nil, err
return nil, status.Error(
codes.InvalidArgument,
types.ErrGatewayInvalidStake.Wrapf(
"stake (%s) must be higher than previous stake (%s)",
msg.Stake, currGatewayStake,
).Error(),
)
}
logger.Info(fmt.Sprintf("Gateway is going to escrow an additional %+v coins", coinsToEscrow))
logger.Info(fmt.Sprintf("gateway is going to escrow an additional %+v coins", coinsToEscrow))
}

// Must always stake or upstake (> 0 delta)
// MUST ALWAYS stake or upstake (> 0 delta).
// TODO_MAINNET(#853): Consider removing the requirement above.
if coinsToEscrow.IsZero() {
logger.Warn(fmt.Sprintf("Gateway %q must escrow more than 0 additional coins", msg.Address))
return nil, types.ErrGatewayInvalidStake.Wrapf("gateway %q must escrow more than 0 additional coins", msg.Address)
err = types.ErrGatewayInvalidStake.Wrapf("gateway %q must escrow more than 0 additional coins", msg.GetAddress())
logger.Info(fmt.Sprintf("ERROR: %s", err))
return nil, status.Error(codes.InvalidArgument, err.Error())
}

// Retrieve the address of the gateway
gatewayAddress, err := sdk.AccAddressFromBech32(msg.Address)
if err != nil {
// TODO_TECHDEBT(#384): determine whether to continue using cosmos logger for debug level.
logger.Error(fmt.Sprintf("could not parse address %q", msg.Address))
return nil, err
// MUST ALWAYS have at least minimum stake.
minStake := k.GetParams(ctx).MinStake
if msg.Stake.Amount.LT(minStake.Amount) {
err = types.ErrGatewayInvalidStake.Wrapf("gateway %q must stake at least %s", msg.Address, minStake)
logger.Info(fmt.Sprintf("ERROR: %s", err))
return nil, status.Error(codes.InvalidArgument, err.Error())
}

// Send the coins from the gateway to the staked gateway pool
Expand Down
73 changes: 51 additions & 22 deletions x/gateway/keeper/msg_server_stake_gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,52 @@ import (
"testing"

"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
cosmostypes "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"

"github.com/pokt-network/poktroll/app/volatile"
keepertest "github.com/pokt-network/poktroll/testutil/keeper"
"github.com/pokt-network/poktroll/testutil/sample"
"github.com/pokt-network/poktroll/x/gateway/keeper"
"github.com/pokt-network/poktroll/x/gateway/types"
gatewaytypes "github.com/pokt-network/poktroll/x/gateway/types"
)

func TestMsgServer_StakeGateway_SuccessfulCreateAndUpdate(t *testing.T) {
k, ctx := keepertest.GatewayKeeper(t)
srv := keeper.NewMsgServerImpl(k)

// Generate an address for the gateway
// Generate an address for the gateway.
addr := sample.AccAddress()

// Verify that the gateway does not exist yet
// Verify that the gateway does not exist yet.
_, isGatewayFound := k.GetGateway(ctx, addr)
require.False(t, isGatewayFound)

// Prepare the gateway
initialStake := sdk.NewCoin("upokt", math.NewInt(100))
stakeMsg := &types.MsgStakeGateway{
// Prepare the gateway.
initialStake := cosmostypes.NewCoin("upokt", math.NewInt(100))
stakeMsg := &gatewaytypes.MsgStakeGateway{
Address: addr,
Stake: &initialStake,
}

// Stake the gateway
// Stake the gateway.
_, err := srv.StakeGateway(ctx, stakeMsg)
require.NoError(t, err)

// Verify that the gateway exists
// Verify that the gateway exists.
foundGateway, isGatewayFound := k.GetGateway(ctx, addr)
require.True(t, isGatewayFound)
require.Equal(t, addr, foundGateway.Address)
require.Equal(t, initialStake.Amount, foundGateway.Stake.Amount)

// Prepare an updated gateway with a higher stake
updatedStake := sdk.NewCoin("upokt", math.NewInt(200))
updateMsg := &types.MsgStakeGateway{
// Prepare an updated gateway with a higher stake.
updatedStake := cosmostypes.NewCoin("upokt", math.NewInt(200))
updateMsg := &gatewaytypes.MsgStakeGateway{
Address: addr,
Stake: &updatedStake,
}

// Update the staked gateway
// Update the staked gateway.
_, err = srv.StakeGateway(ctx, updateMsg)
require.NoError(t, err)
foundGateway, isGatewayFound = k.GetGateway(ctx, addr)
Expand All @@ -60,33 +61,61 @@ func TestMsgServer_StakeGateway_FailLoweringStake(t *testing.T) {
k, ctx := keepertest.GatewayKeeper(t)
srv := keeper.NewMsgServerImpl(k)

// Prepare the gateway
// Prepare the gateway.
addr := sample.AccAddress()
initialStake := sdk.NewCoin("upokt", math.NewInt(100))
stakeMsg := &types.MsgStakeGateway{
initialStake := cosmostypes.NewCoin("upokt", math.NewInt(100))
stakeMsg := &gatewaytypes.MsgStakeGateway{
Address: addr,
Stake: &initialStake,
}

// Stake the gateway & verify that the gateway exists
// Stake the gateway & verify that the gateway exists.
_, err := srv.StakeGateway(ctx, stakeMsg)
require.NoError(t, err)
_, isGatewayFound := k.GetGateway(ctx, addr)
require.True(t, isGatewayFound)

// Prepare an updated gateway with a lower stake
updatedStake := sdk.NewCoin("upokt", math.NewInt(50))
updateMsg := &types.MsgStakeGateway{
// Prepare an updated gateway with a lower stake.
updatedStake := cosmostypes.NewCoin("upokt", math.NewInt(50))
updateMsg := &gatewaytypes.MsgStakeGateway{
Address: addr,
Stake: &updatedStake,
}

// Verify that it fails
// Verify that it fails.
_, err = srv.StakeGateway(ctx, updateMsg)
require.Error(t, err)

// Verify that the gateway stake is unchanged
// Verify that the gateway stake is unchanged.
gatewayFound, isGatewayFound := k.GetGateway(ctx, addr)
require.True(t, isGatewayFound)
require.Equal(t, initialStake.Amount, gatewayFound.Stake.Amount)
}

func TestMsgServer_StakeGateway_FailBelowMinStake(t *testing.T) {
k, ctx := keepertest.GatewayKeeper(t)
srv := keeper.NewMsgServerImpl(k)

addr := sample.AccAddress()
gatewayStake := cosmostypes.NewInt64Coin(volatile.DenomuPOKT, 100)
minStake := gatewayStake.AddAmount(math.NewInt(1))
expectedErr := gatewaytypes.ErrGatewayInvalidStake.Wrapf("gateway %q must stake at least %s", addr, minStake)

// Set the minimum stake to be greater than the gateway stake.
params := k.GetParams(ctx)
params.MinStake = &minStake
err := k.SetParams(ctx, params)
require.NoError(t, err)

// Prepare the gateway.
stakeMsg := &gatewaytypes.MsgStakeGateway{
Address: addr,
Stake: &gatewayStake,
}

// Attempt to stake the gateway & verify that the gateway does NOT exist.
_, err = srv.StakeGateway(ctx, stakeMsg)
require.ErrorContains(t, err, expectedErr.Error())
_, isGatewayFound := k.GetGateway(ctx, addr)
require.False(t, isGatewayFound)
}

0 comments on commit a752b66

Please sign in to comment.