Skip to content

Commit

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

- Adds minimum stake validation to the application stake message handler
(i.e. total stake must be >= minimum stake).
- Updates error returns in the same handler to ensure all error paths
return appropriate gRPC status errors.
- 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
- #843 
- #844 
- #845

## Dependents
 
- #848 
- #849
- #850
- #857
- #852 
- #861 
- #851 
- #863 

## Issue

- #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

- [ ] **Documentation**: `make docusaurus_start`; only needed if you
make doc changes
- [ ] **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
- [ ] I have performed a self-review of my own code; both comments &
source code
- [ ] I create and reference any new tickets, if applicable
- [x] I have left TODOs throughout the codebase, if applicable

---------

Co-authored-by: Redouane Lakrache <[email protected]>
Co-authored-by: Daniel Olshansky <[email protected]>
Co-authored-by: red-0ne <[email protected]>
  • Loading branch information
4 people authored Oct 9, 2024
1 parent 31797ee commit fd71973
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 72 deletions.
39 changes: 19 additions & 20 deletions x/application/keeper/msg_server_delegate_to_gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import (
"fmt"
"testing"

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

keepertest "github.com/pokt-network/poktroll/testutil/keeper"
"github.com/pokt-network/poktroll/testutil/sample"
"github.com/pokt-network/poktroll/x/application/keeper"
"github.com/pokt-network/poktroll/x/application/types"
apptypes "github.com/pokt-network/poktroll/x/application/types"
sharedtypes "github.com/pokt-network/poktroll/x/shared/types"
)

Expand All @@ -28,9 +27,9 @@ func TestMsgServer_DelegateToGateway_SuccessfullyDelegate(t *testing.T) {
keepertest.AddGatewayToStakedGatewayMap(t, gatewayAddr2)

// Prepare the application
stakeMsg := &types.MsgStakeApplication{
stakeMsg := &apptypes.MsgStakeApplication{
Address: appAddr,
Stake: &sdk.Coin{Denom: "upokt", Amount: math.NewInt(100)},
Stake: &apptypes.DefaultMinStake,
Services: []*sharedtypes.ApplicationServiceConfig{
{
ServiceId: "svc1",
Expand All @@ -45,7 +44,7 @@ func TestMsgServer_DelegateToGateway_SuccessfullyDelegate(t *testing.T) {
require.True(t, isAppFound)

// Prepare the delegation message
delegateMsg := &types.MsgDelegateToGateway{
delegateMsg := &apptypes.MsgDelegateToGateway{
AppAddress: appAddr,
GatewayAddress: gatewayAddr1,
}
Expand All @@ -72,7 +71,7 @@ func TestMsgServer_DelegateToGateway_SuccessfullyDelegate(t *testing.T) {
require.Equal(t, gatewayAddr1, foundApp.DelegateeGatewayAddresses[0])

// Prepare a second delegation message
delegateMsg2 := &types.MsgDelegateToGateway{
delegateMsg2 := &apptypes.MsgDelegateToGateway{
AppAddress: appAddr,
GatewayAddress: gatewayAddr2,
}
Expand Down Expand Up @@ -108,9 +107,9 @@ func TestMsgServer_DelegateToGateway_FailDuplicate(t *testing.T) {
keepertest.AddGatewayToStakedGatewayMap(t, gatewayAddr)

// Prepare the application
stakeMsg := &types.MsgStakeApplication{
stakeMsg := &apptypes.MsgStakeApplication{
Address: appAddr,
Stake: &sdk.Coin{Denom: "upokt", Amount: math.NewInt(100)},
Stake: &apptypes.DefaultMinStake,
Services: []*sharedtypes.ApplicationServiceConfig{
{
ServiceId: "svc1",
Expand All @@ -125,7 +124,7 @@ func TestMsgServer_DelegateToGateway_FailDuplicate(t *testing.T) {
require.True(t, isAppFound)

// Prepare the delegation message
delegateMsg := &types.MsgDelegateToGateway{
delegateMsg := &apptypes.MsgDelegateToGateway{
AppAddress: appAddr,
GatewayAddress: gatewayAddr,
}
Expand All @@ -152,14 +151,14 @@ func TestMsgServer_DelegateToGateway_FailDuplicate(t *testing.T) {
require.Equal(t, gatewayAddr, foundApp.DelegateeGatewayAddresses[0])

// Prepare a second delegation message
delegateMsg2 := &types.MsgDelegateToGateway{
delegateMsg2 := &apptypes.MsgDelegateToGateway{
AppAddress: appAddr,
GatewayAddress: gatewayAddr,
}

// Attempt to delegate the application to the gateway again
_, err = srv.DelegateToGateway(ctx, delegateMsg2)
require.ErrorIs(t, err, types.ErrAppAlreadyDelegated)
require.ErrorIs(t, err, apptypes.ErrAppAlreadyDelegated)
events = sdkCtx.EventManager().Events()
require.Equal(t, 1, len(events))
foundApp, isAppFound = k.GetApplication(ctx, appAddr)
Expand All @@ -177,9 +176,9 @@ func TestMsgServer_DelegateToGateway_FailGatewayNotStaked(t *testing.T) {
gatewayAddr := sample.AccAddress()

// Prepare the application
stakeMsg := &types.MsgStakeApplication{
stakeMsg := &apptypes.MsgStakeApplication{
Address: appAddr,
Stake: &sdk.Coin{Denom: "upokt", Amount: math.NewInt(100)},
Stake: &apptypes.DefaultMinStake,
Services: []*sharedtypes.ApplicationServiceConfig{
{
ServiceId: "svc1",
Expand All @@ -194,14 +193,14 @@ func TestMsgServer_DelegateToGateway_FailGatewayNotStaked(t *testing.T) {
require.True(t, isAppFound)

// Prepare the delegation message
delegateMsg := &types.MsgDelegateToGateway{
delegateMsg := &apptypes.MsgDelegateToGateway{
AppAddress: appAddr,
GatewayAddress: gatewayAddr,
}

// Attempt to delegate the application to the unstaked gateway
_, err = srv.DelegateToGateway(ctx, delegateMsg)
require.ErrorIs(t, err, types.ErrAppGatewayNotFound)
require.ErrorIs(t, err, apptypes.ErrAppGatewayNotFound)
foundApp, isAppFound := k.GetApplication(ctx, appAddr)
require.True(t, isAppFound)
require.Equal(t, 0, len(foundApp.DelegateeGatewayAddresses))
Expand All @@ -215,9 +214,9 @@ func TestMsgServer_DelegateToGateway_FailMaxReached(t *testing.T) {
appAddr := sample.AccAddress()

// Prepare the application
stakeMsg := &types.MsgStakeApplication{
stakeMsg := &apptypes.MsgStakeApplication{
Address: appAddr,
Stake: &sdk.Coin{Denom: "upokt", Amount: math.NewInt(100)},
Stake: &apptypes.DefaultMinStake,
Services: []*sharedtypes.ApplicationServiceConfig{
{
ServiceId: "svc1",
Expand All @@ -240,7 +239,7 @@ func TestMsgServer_DelegateToGateway_FailMaxReached(t *testing.T) {
gatewayAddresses[i] = gatewayAddr
// Mock the gateway being staked via the staked gateway map
keepertest.AddGatewayToStakedGatewayMap(t, gatewayAddr)
delegateMsg := &types.MsgDelegateToGateway{
delegateMsg := &apptypes.MsgDelegateToGateway{
AppAddress: appAddr,
GatewayAddress: gatewayAddr,
}
Expand Down Expand Up @@ -270,14 +269,14 @@ func TestMsgServer_DelegateToGateway_FailMaxReached(t *testing.T) {
keepertest.AddGatewayToStakedGatewayMap(t, gatewayAddr)

// Prepare the delegation message
delegateMsg := &types.MsgDelegateToGateway{
delegateMsg := &apptypes.MsgDelegateToGateway{
AppAddress: appAddr,
GatewayAddress: gatewayAddr,
}

// Attempt to delegate the application when the max is already reached
_, err = srv.DelegateToGateway(ctx, delegateMsg)
require.ErrorIs(t, err, types.ErrAppMaxDelegatedGateways)
require.ErrorIs(t, err, apptypes.ErrAppMaxDelegatedGateways)
events = sdkCtx.EventManager().Events()
require.Equal(t, int(maxDelegatedParam), len(events))
foundApp, isStakedAppFound := k.GetApplication(ctx, appAddr)
Expand Down
38 changes: 28 additions & 10 deletions x/application/keeper/msg_server_stake_application.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/application/types"
Expand All @@ -23,7 +25,7 @@ func (k msgServer) StakeApplication(ctx context.Context, msg *types.MsgStakeAppl

if err := msg.ValidateBasic(); err != nil {
logger.Error(fmt.Sprintf("invalid MsgStakeApplication: %v", err))
return nil, err
return nil, status.Error(codes.InvalidArgument, err.Error())
}

// Check if the application already exists or not
Expand All @@ -38,38 +40,54 @@ func (k msgServer) StakeApplication(ctx context.Context, msg *types.MsgStakeAppl
logger.Info(fmt.Sprintf("Application found. About to try and update application for address %q", msg.Address))
currAppStake := *foundApp.Stake
if err = k.updateApplication(ctx, &foundApp, msg); err != nil {
logger.Error(fmt.Sprintf("could not update application for address %q due to error %v", msg.Address, err))
return nil, err
logger.Info(fmt.Sprintf("could not update application for address %q due to error %v", msg.Address, err))
return nil, status.Error(codes.InvalidArgument, err.Error())
}
coinsToEscrow, err = (*msg.Stake).SafeSub(currAppStake)
if err != nil {
logger.Error(fmt.Sprintf("could not calculate coins to escrow due to error %v", err))
return nil, err
logger.Info(fmt.Sprintf("could not calculate coins to escrow due to error %v", err))
return nil, status.Error(codes.InvalidArgument, err.Error())
}
logger.Info(fmt.Sprintf("Application is going to escrow an additional %+v coins", coinsToEscrow))

// If the application has initiated an unstake action, cancel it since it is staking again.
foundApp.UnstakeSessionEndHeight = types.ApplicationNotUnstaking
}

// Must always stake or upstake (> 0 delta)
// MUST ALWAYS stake or upstake (> 0 delta)
if coinsToEscrow.IsZero() {
logger.Warn(fmt.Sprintf("Application %q must escrow more than 0 additional coins", msg.Address))
return nil, types.ErrAppInvalidStake.Wrapf("application %q must escrow more than 0 additional coins", msg.Address)
return nil, status.Error(
codes.InvalidArgument,
types.ErrAppInvalidStake.Wrapf(
"application %q must escrow more than 0 additional coins",
msg.Address,
).Error())
}

// MUST ALWAYS have at least minimum stake.
minStake := k.GetParams(ctx).MinStake
if msg.Stake.Amount.LT(minStake.Amount) {
errFmt := "application %q must stake at least %s"
logger.Info(fmt.Sprintf(errFmt, msg.Address, minStake))
return nil, status.Error(
codes.InvalidArgument,
types.ErrAppInvalidStake.Wrapf(errFmt, msg.Address, minStake).Error(),
)
}

// Retrieve the address of the application
appAddress, err := sdk.AccAddressFromBech32(msg.Address)
if err != nil {
logger.Error(fmt.Sprintf("could not parse address %q", msg.Address))
return nil, err
logger.Info(fmt.Sprintf("could not parse address %q", msg.Address))
return nil, status.Error(codes.InvalidArgument, err.Error())
}

// Send the coins from the application to the staked application pool
err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, appAddress, types.ModuleName, []sdk.Coin{coinsToEscrow})
if err != nil {
logger.Error(fmt.Sprintf("could not send %v coins from %q to %q module account due to %v", coinsToEscrow, appAddress, types.ModuleName, err))
return nil, err
return nil, status.Error(codes.Internal, err.Error())
}
logger.Info(fmt.Sprintf("Successfully escrowed %v coins from %q to %q module account", coinsToEscrow, appAddress, types.ModuleName))

Expand Down
76 changes: 57 additions & 19 deletions x/application/keeper/msg_server_stake_application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ 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/application/keeper"
"github.com/pokt-network/poktroll/x/application/types"
apptypes "github.com/pokt-network/poktroll/x/application/types"
sharedtypes "github.com/pokt-network/poktroll/x/shared/types"
)

Expand All @@ -26,9 +27,10 @@ func TestMsgServer_StakeApplication_SuccessfulCreateAndUpdate(t *testing.T) {
require.False(t, isAppFound)

// Prepare the application
stakeMsg := &types.MsgStakeApplication{
initialStake := &apptypes.DefaultMinStake
stakeMsg := &apptypes.MsgStakeApplication{
Address: appAddr,
Stake: &sdk.Coin{Denom: "upokt", Amount: math.NewInt(100)},
Stake: initialStake,
Services: []*sharedtypes.ApplicationServiceConfig{
{ServiceId: "svc1"},
},
Expand All @@ -42,14 +44,15 @@ func TestMsgServer_StakeApplication_SuccessfulCreateAndUpdate(t *testing.T) {
foundApp, isAppFound := k.GetApplication(ctx, appAddr)
require.True(t, isAppFound)
require.Equal(t, appAddr, foundApp.Address)
require.Equal(t, int64(100), foundApp.Stake.Amount.Int64())
require.Equal(t, initialStake, foundApp.Stake)
require.Len(t, foundApp.ServiceConfigs, 1)
require.Equal(t, "svc1", foundApp.ServiceConfigs[0].ServiceId)

// Prepare an updated application with a higher stake and another service
updateStakeMsg := &types.MsgStakeApplication{
upStake := initialStake.AddAmount(math.NewInt(100))
updateStakeMsg := &apptypes.MsgStakeApplication{
Address: appAddr,
Stake: &sdk.Coin{Denom: "upokt", Amount: math.NewInt(200)},
Stake: &upStake,
Services: []*sharedtypes.ApplicationServiceConfig{
{ServiceId: "svc1"},
{ServiceId: "svc2"},
Expand All @@ -61,7 +64,7 @@ func TestMsgServer_StakeApplication_SuccessfulCreateAndUpdate(t *testing.T) {
require.NoError(t, err)
foundApp, isAppFound = k.GetApplication(ctx, appAddr)
require.True(t, isAppFound)
require.Equal(t, int64(200), foundApp.Stake.Amount.Int64())
require.Equal(t, &upStake, foundApp.Stake)
require.Len(t, foundApp.ServiceConfigs, 2)
require.Equal(t, "svc1", foundApp.ServiceConfigs[0].ServiceId)
require.Equal(t, "svc2", foundApp.ServiceConfigs[1].ServiceId)
Expand All @@ -74,9 +77,10 @@ func TestMsgServer_StakeApplication_FailRestakingDueToInvalidServices(t *testing
appAddr := sample.AccAddress()

// Prepare the application stake message
stakeMsg := &types.MsgStakeApplication{
initialStake := &apptypes.DefaultMinStake
stakeMsg := &apptypes.MsgStakeApplication{
Address: appAddr,
Stake: &sdk.Coin{Denom: "upokt", Amount: math.NewInt(100)},
Stake: initialStake,
Services: []*sharedtypes.ApplicationServiceConfig{
{ServiceId: "svc1"},
},
Expand All @@ -87,9 +91,10 @@ func TestMsgServer_StakeApplication_FailRestakingDueToInvalidServices(t *testing
require.NoError(t, err)

// Prepare the application stake message without any services
updateStakeMsg := &types.MsgStakeApplication{
upStake := initialStake.AddAmount(math.NewInt(100))
updateStakeMsg := &apptypes.MsgStakeApplication{
Address: appAddr,
Stake: &sdk.Coin{Denom: "upokt", Amount: math.NewInt(100)},
Stake: &upStake,
Services: []*sharedtypes.ApplicationServiceConfig{},
}

Expand All @@ -105,9 +110,9 @@ func TestMsgServer_StakeApplication_FailRestakingDueToInvalidServices(t *testing
require.Equal(t, "svc1", foundApp.ServiceConfigs[0].ServiceId)

// Prepare the application stake message with an invalid service ID
updateStakeMsg = &types.MsgStakeApplication{
updateStakeMsg = &apptypes.MsgStakeApplication{
Address: appAddr,
Stake: &sdk.Coin{Denom: "upokt", Amount: math.NewInt(100)},
Stake: &upStake,
Services: []*sharedtypes.ApplicationServiceConfig{
{ServiceId: "svc1 INVALID ! & *"},
},
Expand All @@ -130,10 +135,11 @@ func TestMsgServer_StakeApplication_FailLoweringStake(t *testing.T) {
srv := keeper.NewMsgServerImpl(k)

// Prepare the application
initialStake := &apptypes.DefaultMinStake
appAddr := sample.AccAddress()
stakeMsg := &types.MsgStakeApplication{
stakeMsg := &apptypes.MsgStakeApplication{
Address: appAddr,
Stake: &sdk.Coin{Denom: "upokt", Amount: math.NewInt(100)},
Stake: initialStake,
Services: []*sharedtypes.ApplicationServiceConfig{
{ServiceId: "svc1"},
},
Expand All @@ -146,9 +152,10 @@ func TestMsgServer_StakeApplication_FailLoweringStake(t *testing.T) {
require.True(t, isAppFound)

// Prepare an updated application with a lower stake
updateMsg := &types.MsgStakeApplication{
downStake := initialStake.SubAmount(math.NewInt(1000))
updateMsg := &apptypes.MsgStakeApplication{
Address: appAddr,
Stake: &sdk.Coin{Denom: "upokt", Amount: math.NewInt(50)},
Stake: &downStake,
Services: []*sharedtypes.ApplicationServiceConfig{
{ServiceId: "svc1"},
},
Expand All @@ -161,5 +168,36 @@ func TestMsgServer_StakeApplication_FailLoweringStake(t *testing.T) {
// Verify that the application stake is unchanged
foundApp, isAppFound := k.GetApplication(ctx, appAddr)
require.True(t, isAppFound)
require.Equal(t, int64(100), foundApp.Stake.Amount.Int64())
require.Equal(t, initialStake, foundApp.Stake)
}

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

addr := sample.AccAddress()
appStake := cosmostypes.NewInt64Coin(volatile.DenomuPOKT, 100)
minStake := appStake.AddAmount(math.NewInt(1))
expectedErr := apptypes.ErrAppInvalidStake.Wrapf("application %q must stake at least %s", addr, minStake)

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

// Prepare the application.
stakeMsg := &apptypes.MsgStakeApplication{
Address: addr,
Stake: &appStake,
Services: []*sharedtypes.ApplicationServiceConfig{
{ServiceId: "svc1"},
},
}

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

0 comments on commit fd71973

Please sign in to comment.