Skip to content

Commit

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

1. Adds minimum stake validation to the supplier 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.

## Dependencies

- #809
- #843 
- #844 
- #845
- #847
- #848
- #849
- #850

## Dependents
  
- #852 
- #861 
- #851 

## Issue

- #612

## Type of change

Select one or more from the following:

- [x] New feature, functionality or library
- [ ] 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
- [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 <[email protected]>
Co-authored-by: Daniel Olshansky <[email protected]>
Co-authored-by: red-0ne <[email protected]>
  • Loading branch information
4 people authored Oct 14, 2024
1 parent c8f5230 commit e42f3e3
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 42 deletions.
16 changes: 14 additions & 2 deletions x/supplier/keeper/msg_server_stake_supplier.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,21 +96,33 @@ func (k msgServer) StakeSupplier(ctx context.Context, msg *types.MsgStakeSupplie
}
coinsToEscrow, err = (*msg.Stake).SafeSub(currSupplierStake)
if err != nil {
return nil, err
logger.Info(fmt.Sprintf("ERROR: %s", err))
return nil, status.Error(codes.Internal, err.Error())
}
logger.Info(fmt.Sprintf("Supplier is going to escrow an additional %+v coins", coinsToEscrow))

// If the supplier has initiated an unstake action, cancel it since they are staking again.
supplier.UnstakeSessionEndHeight = sharedtypes.SupplierNotUnstaking
}

// Must always stake or upstake (> 0 delta)
// MUST ALWAYS stake or upstake (> 0 delta)
if coinsToEscrow.IsZero() {
err = types.ErrSupplierInvalidStake.Wrapf("Signer %q must escrow more than 0 additional coins", msg.Signer)
logger.Info(fmt.Sprintf("WARN: %s", err))
return nil, status.Error(codes.InvalidArgument, err.Error())
}

// MUST ALWAYS have at least minimum stake.
minStake := k.GetParams(ctx).MinStake
if msg.Stake.Amount.LT(minStake.Amount) {
err = types.ErrSupplierInvalidStake.Wrapf(
"supplier with owner %q must stake at least %s",
msg.GetOwnerAddress(), minStake,
)
logger.Info(fmt.Sprintf("ERROR: %s", err))
return nil, status.Error(codes.InvalidArgument, err.Error())
}

// Retrieve the account address of the message signer
msgSignerAddress, err := sdk.AccAddressFromBech32(msg.Signer)
if err != nil {
Expand Down
118 changes: 92 additions & 26 deletions x/supplier/keeper/msg_server_stake_supplier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import (
"testing"

"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
cosmostypes "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/pokt-network/poktroll/app/volatile"
keepertest "github.com/pokt-network/poktroll/testutil/keeper"
"github.com/pokt-network/poktroll/testutil/sample"
sharedtypes "github.com/pokt-network/poktroll/x/shared/types"
"github.com/pokt-network/poktroll/x/supplier/keeper"
"github.com/pokt-network/poktroll/x/supplier/types"
suppliertypes "github.com/pokt-network/poktroll/x/supplier/types"
)

func TestMsgServer_StakeSupplier_SuccessfulCreateAndUpdate(t *testing.T) {
Expand All @@ -28,7 +30,7 @@ func TestMsgServer_StakeSupplier_SuccessfulCreateAndUpdate(t *testing.T) {
require.False(t, isSupplierFound)

// Prepare the stakeMsg
stakeMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 100, "svcId")
stakeMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 1000000, "svcId")

// Stake the supplier
_, err := srv.StakeSupplier(ctx, stakeMsg)
Expand All @@ -38,14 +40,14 @@ func TestMsgServer_StakeSupplier_SuccessfulCreateAndUpdate(t *testing.T) {
foundSupplier, isSupplierFound := supplierModuleKeepers.GetSupplier(ctx, operatorAddr)
require.True(t, isSupplierFound)
require.Equal(t, operatorAddr, foundSupplier.OperatorAddress)
require.Equal(t, int64(100), foundSupplier.Stake.Amount.Int64())
require.Equal(t, int64(1000000), foundSupplier.Stake.Amount.Int64())
require.Len(t, foundSupplier.Services, 1)
require.Equal(t, "svcId", foundSupplier.Services[0].ServiceId)
require.Len(t, foundSupplier.Services[0].Endpoints, 1)
require.Equal(t, "http://localhost:8080", foundSupplier.Services[0].Endpoints[0].Url)

// Prepare an updated supplier with a higher stake and a different URL for the service
updateMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 200, "svcId2")
updateMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 2000000, "svcId2")
updateMsg.Services[0].Endpoints[0].Url = "http://localhost:8082"

// Update the staked supplier
Expand All @@ -54,7 +56,7 @@ func TestMsgServer_StakeSupplier_SuccessfulCreateAndUpdate(t *testing.T) {

foundSupplier, isSupplierFound = supplierModuleKeepers.GetSupplier(ctx, operatorAddr)
require.True(t, isSupplierFound)
require.Equal(t, int64(200), foundSupplier.Stake.Amount.Int64())
require.Equal(t, int64(2000000), foundSupplier.Stake.Amount.Int64())
require.Len(t, foundSupplier.Services, 1)
require.Equal(t, "svcId2", foundSupplier.Services[0].ServiceId)
require.Len(t, foundSupplier.Services[0].Endpoints, 1)
Expand All @@ -70,7 +72,7 @@ func TestMsgServer_StakeSupplier_FailRestakingDueToInvalidServices(t *testing.T)
operatorAddr := sample.AccAddress()

// Prepare the supplier stake message
stakeMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 100, "svcId")
stakeMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 1000000, "svcId")

// Stake the supplier
_, err := srv.StakeSupplier(ctx, stakeMsg)
Expand Down Expand Up @@ -120,7 +122,7 @@ func TestMsgServer_StakeSupplier_FailLoweringStake(t *testing.T) {
operatorAddr := sample.AccAddress()

// Prepare the supplier stake message
stakeMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 100, "svcId")
stakeMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 1000000, "svcId")

// Stake the supplier & verify that the supplier exists
_, err := srv.StakeSupplier(ctx, stakeMsg)
Expand All @@ -140,7 +142,7 @@ func TestMsgServer_StakeSupplier_FailLoweringStake(t *testing.T) {
// Verify that the supplier stake is unchanged
supplierFound, isSupplierFound := supplierModuleKeepers.GetSupplier(ctx, operatorAddr)
require.True(t, isSupplierFound)
require.Equal(t, int64(100), supplierFound.Stake.Amount.Int64())
require.Equal(t, int64(1000000), supplierFound.Stake.Amount.Int64())
require.Len(t, supplierFound.Services, 1)
}

Expand All @@ -153,11 +155,12 @@ func TestMsgServer_StakeSupplier_FailWithNonExistingService(t *testing.T) {
operatorAddr := sample.AccAddress()

// Prepare the supplier stake message with a non-existing service ID
stakeMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 100, "newService")
stakeMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 1000000, "newService")

// Stake the supplier & verify that it fails because the service does not exist.
_, err := srv.StakeSupplier(ctx, stakeMsg)
require.ErrorContains(t, err, types.ErrSupplierServiceNotFound.Wrapf(
require.Equal(t, codes.InvalidArgument, status.Code(err))
require.ErrorContains(t, err, suppliertypes.ErrSupplierServiceNotFound.Wrapf(
"service %q does not exist", "newService",
).Error())
}
Expand All @@ -171,7 +174,7 @@ func TestMsgServer_StakeSupplier_OperatorAuthorizations(t *testing.T) {
operatorAddr := sample.AccAddress()

// Stake using the operator address as the signer and verify that it succeeds.
stakeMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 100, "svcId")
stakeMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 1000000, "svcId")
setStakeMsgSigner(stakeMsg, operatorAddr)
_, err := srv.StakeSupplier(ctx, stakeMsg)
require.NoError(t, err)
Expand All @@ -181,7 +184,7 @@ func TestMsgServer_StakeSupplier_OperatorAuthorizations(t *testing.T) {
require.Equal(t, ownerAddr, supplier.OwnerAddress)

// Update the supplier using the operator address as the signer and verify that it succeeds.
stakeMsgUpdateUrl := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 200, "svcId")
stakeMsgUpdateUrl := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 2000000, "svcId")
operatorUpdatedServiceUrl := "http://localhost:8081"
stakeMsgUpdateUrl.Services[0].Endpoints[0].Url = operatorUpdatedServiceUrl
setStakeMsgSigner(stakeMsgUpdateUrl, operatorAddr)
Expand All @@ -196,7 +199,7 @@ func TestMsgServer_StakeSupplier_OperatorAuthorizations(t *testing.T) {
// Update the supplier URL by using the owner address as the singer and verify that it succeeds.
ownerUpdaterServiceUrl := "http://localhost:8082"
stakeMsgUpdateUrl.Services[0].Endpoints[0].Url = ownerUpdaterServiceUrl
stakeMsgUpdateUrl.Stake.Amount = math.NewInt(300)
stakeMsgUpdateUrl.Stake.Amount = math.NewInt(3000000)
setStakeMsgSigner(stakeMsgUpdateUrl, ownerAddr)
_, err = srv.StakeSupplier(ctx, stakeMsgUpdateUrl)
require.NoError(t, err)
Expand All @@ -209,7 +212,7 @@ func TestMsgServer_StakeSupplier_OperatorAuthorizations(t *testing.T) {

// Try updating the supplier's operator address using the old operator as a signer
// will create a new supplier.
stakeMsgUpdateOperator := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 300, "svcId")
stakeMsgUpdateOperator := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 3000000, "svcId")
newOperatorAddress := sample.AccAddress()
stakeMsgUpdateOperator.OperatorAddress = newOperatorAddress
setStakeMsgSigner(stakeMsgUpdateOperator, operatorAddr)
Expand All @@ -229,7 +232,7 @@ func TestMsgServer_StakeSupplier_OperatorAuthorizations(t *testing.T) {
// will create a new supplier.
newOperatorAddress = sample.AccAddress()
stakeMsgUpdateOperator.OperatorAddress = newOperatorAddress
stakeMsgUpdateOperator.Stake.Amount = math.NewInt(400)
stakeMsgUpdateOperator.Stake.Amount = math.NewInt(4000000)
setStakeMsgSigner(stakeMsgUpdateOperator, ownerAddr)
_, err = srv.StakeSupplier(ctx, stakeMsgUpdateOperator)
require.NoError(t, err)
Expand All @@ -246,10 +249,11 @@ func TestMsgServer_StakeSupplier_OperatorAuthorizations(t *testing.T) {
// Try updating the supplier's owner address using the operator as a signer
// and verify that it fails.
newOwnerAddress := sample.AccAddress()
stakeMsgUpdateOwner := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 500, "svcId")
stakeMsgUpdateOwner := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 5000000, "svcId")
stakeMsgUpdateOwner.OwnerAddress = newOwnerAddress
setStakeMsgSigner(stakeMsgUpdateOwner, operatorAddr)
_, err = srv.StakeSupplier(ctx, stakeMsgUpdateOwner)
require.Equal(t, codes.InvalidArgument, status.Code(err))
require.ErrorContains(t, err, sharedtypes.ErrSharedUnauthorizedSupplierUpdate.Wrapf(
"signer %q is not allowed to update the owner address %q",
operatorAddr, ownerAddr,
Expand All @@ -275,13 +279,13 @@ func TestMsgServer_StakeSupplier_ActiveSupplier(t *testing.T) {
operatorAddr := sample.AccAddress()

// Prepare the supplier
stakeMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 100, "svcId")
stakeMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 1000000, "svcId")

// Stake the supplier & verify that the supplier exists.
_, err := srv.StakeSupplier(ctx, stakeMsg)
require.NoError(t, err)

sdkCtx := sdk.UnwrapSDKContext(ctx)
sdkCtx := cosmostypes.UnwrapSDKContext(ctx)
currentHeight := sdkCtx.BlockHeight()
sessionEndHeight := supplierModuleKeepers.SharedKeeper.GetSessionEndHeight(ctx, currentHeight)

Expand All @@ -304,7 +308,7 @@ func TestMsgServer_StakeSupplier_ActiveSupplier(t *testing.T) {
ctx = keepertest.SetBlockHeight(ctx, sessionEndHeight+1)

// Prepare the supplier stake message with a different service
updateMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 200, "svcId", "svcId2")
updateMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 2000000, "svcId", "svcId2")
updateMsg.Signer = operatorAddr

// Update the staked supplier
Expand Down Expand Up @@ -339,7 +343,7 @@ func stakeSupplierForServicesMsg(
ownerAddr, operatorAddr string,
amount int64,
serviceIds ...string,
) *types.MsgStakeSupplier {
) *suppliertypes.MsgStakeSupplier {
services := make([]*sharedtypes.SupplierServiceConfig, 0, len(serviceIds))
for _, serviceId := range serviceIds {
services = append(services, &sharedtypes.SupplierServiceConfig{
Expand All @@ -348,7 +352,7 @@ func stakeSupplierForServicesMsg(
{
Url: "http://localhost:8080",
RpcType: sharedtypes.RPCType_JSON_RPC,
Configs: make([]*sharedtypes.ConfigOption, 0),
Configs: nil,
},
},
RevShare: []*sharedtypes.ServiceRevenueShare{
Expand All @@ -360,20 +364,82 @@ func stakeSupplierForServicesMsg(
})
}

return &types.MsgStakeSupplier{
return &suppliertypes.MsgStakeSupplier{
Signer: ownerAddr,
OwnerAddress: ownerAddr,
OperatorAddress: operatorAddr,
Stake: &sdk.Coin{Denom: volatile.DenomuPOKT, Amount: math.NewInt(amount)},
Stake: &cosmostypes.Coin{Denom: volatile.DenomuPOKT, Amount: math.NewInt(amount)},
Services: services,
}
}

// setStakeMsgSigner sets the signer of the given MsgStakeSupplier to the given address
func setStakeMsgSigner(
msg *types.MsgStakeSupplier,
msg *suppliertypes.MsgStakeSupplier,
signer string,
) *types.MsgStakeSupplier {
) *suppliertypes.MsgStakeSupplier {
msg.Signer = signer
return msg
}

func TestMsgServer_StakeSupplier_FailBelowMinStake(t *testing.T) {
k, ctx := keepertest.SupplierKeeper(t)
srv := keeper.NewMsgServerImpl(*k.Keeper)

addr := sample.AccAddress()
supplierStake := cosmostypes.NewInt64Coin(volatile.DenomuPOKT, 100)
minStake := supplierStake.AddAmount(math.NewInt(1))
expectedErr := suppliertypes.ErrSupplierInvalidStake.Wrapf("supplier with owner %q must stake at least %s", addr, minStake)

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

// Prepare the supplier stake message.
stakeMsg := stakeSupplierForServicesMsg(addr, addr, 100, "svcId")

// Attempt to stake the supplier & verify that the supplier does NOT exist.
_, err = srv.StakeSupplier(ctx, stakeMsg)
require.ErrorContains(t, err, expectedErr.Error())
_, isSupplierFound := k.GetSupplier(ctx, addr)
require.False(t, isSupplierFound)
}

func TestMsgServer_StakeSupplier_UpStakeFromBelowMinStake(t *testing.T) {
k, ctx := keepertest.SupplierKeeper(t)
srv := keeper.NewMsgServerImpl(*k.Keeper)

addr := sample.AccAddress()
supplierParams := k.Keeper.GetParams(ctx)
minStake := supplierParams.GetMinStake()
belowMinStake := minStake.AddAmount(math.NewInt(-1))
aboveMinStake := minStake.AddAmount(math.NewInt(1))

stakeMsg := stakeSupplierForServicesMsg(addr, addr, aboveMinStake.Amount.Int64(), "svcId")

// Stake (via keeper methods) a supplier with stake below min. stake.
initialSupplier := sharedtypes.Supplier{
OwnerAddress: addr,
OperatorAddress: addr,
Stake: &belowMinStake,
Services: stakeMsg.GetServices(),
ServicesActivationHeightsMap: map[string]uint64{
"svcId": 0,
},
}

k.SetSupplier(ctx, initialSupplier)

// Attempt to upstake the supplier with stake above min. stake.
_, err := srv.StakeSupplier(ctx, stakeMsg)
require.NoError(t, err)

// Assert supplier is staked for above min. stake.
expectedSupplier := initialSupplier
expectedSupplier.Stake = &aboveMinStake
supplier, isSupplierFound := k.GetSupplier(ctx, addr)
require.True(t, isSupplierFound)
require.EqualValues(t, expectedSupplier, supplier)
}
Loading

0 comments on commit e42f3e3

Please sign in to comment.