Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x/incentives v2: Validator incentives #320

Merged
merged 8 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ jobs:
"Auction",
"CellarFees",
"Incentives",
"ValidatorIncentives",
"Pubsub",
"Addresses",
]
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,9 @@ e2e_cellarfees_test: e2e_clean_slate
e2e_incentives_test: e2e_clean_slate
@E2E_SKIP_CLEANUP=true integration_tests/integration_tests.test -test.failfast -test.v -test.run IntegrationTestSuite -testify.m TestIncentives || make -s fail

e2e_validator_incentives_test: e2e_clean_slate
@E2E_SKIP_CLEANUP=true integration_tests/integration_tests.test -test.failfast -test.v -test.run IntegrationTestSuite -testify.m TestValidatorIncentives || make -s fail

e2e_pubsub_test: e2e_clean_slate
@E2E_SKIP_CLEANUP=true integration_tests/integration_tests.test -test.failfast -test.v -test.run IntegrationTestSuite -testify.m TestPubsub || make -s fail

Expand Down
6 changes: 1 addition & 5 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,11 +520,7 @@ func NewSommelierApp(
)

app.IncentivesKeeper = incentiveskeeper.NewKeeper(
appCodec, keys[incentivestypes.StoreKey], app.GetSubspace(incentivestypes.ModuleName), app.DistrKeeper, app.BankKeeper, app.MintKeeper,
)

app.IncentivesKeeper = incentiveskeeper.NewKeeper(
appCodec, keys[incentivestypes.StoreKey], app.GetSubspace(incentivestypes.ModuleName), app.DistrKeeper, app.BankKeeper, app.MintKeeper,
appCodec, keys[incentivestypes.StoreKey], app.GetSubspace(incentivestypes.ModuleName), app.DistrKeeper, app.BankKeeper, app.MintKeeper, app.StakingKeeper,
)

app.GravityKeeper = *app.GravityKeeper.SetHooks(
Expand Down
16 changes: 16 additions & 0 deletions integration_tests/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/cosmos/cosmos-sdk/x/genutil"
genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
"github.com/peggyjv/sommelier/v7/app/params"
incentivestypes "github.com/peggyjv/sommelier/v7/x/incentives/types"
)

func getGenDoc(path string) (*tmtypes.GenesisDoc, error) {
Expand Down Expand Up @@ -108,3 +110,17 @@ func addGenesisAccount(path, moniker, amountStr string, accAddr sdk.AccAddress)
genDoc.AppState = appStateJSON
return genutil.ExportGenesisFile(genDoc, genFile)
}

func (s *IntegrationTestSuite) setIncentivesGenState(appGenState map[string]json.RawMessage) error {
incentivesGenState := incentivestypes.DefaultGenesisState()
err := cdc.UnmarshalJSON(appGenState[incentivestypes.ModuleName], &incentivesGenState)
if err != nil {
return fmt.Errorf("failed to unmarshal incentives genesis state: %w", err)
}

incentivesGenState.Params.ValidatorIncentivesCutoffHeight = 0
incentivesGenState.Params.ValidatorMaxDistributionPerBlock = sdk.NewCoin(params.BaseCoinUnit, sdk.NewInt(0))

appGenState[incentivestypes.ModuleName] = cdc.MustMarshalJSON(&incentivesGenState)
return nil
}
246 changes: 246 additions & 0 deletions integration_tests/incentives_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,249 @@ func (s *IntegrationTestSuite) getRewardAmountAndHeight(ctx context.Context, dis

return amount, height
}

func (s *IntegrationTestSuite) TestValidatorIncentives() {
validator := s.chain.validators[0]
proposer := s.chain.proposer
proposerCtx, err := s.chain.clientContext("tcp://localhost:26657", proposer.keyring, "proposer", proposer.address())
s.Require().NoError(err)
orch := s.chain.orchestrators[0]
orchClientCtx, err := s.chain.clientContext("tcp://localhost:26657", orch.keyring, "orch", orch.address())
s.Require().NoError(err)
ctx := context.Background()

s.T().Log("Getting the initial community pool balance")
queryClient := disttypes.NewQueryClient(proposerCtx)
queryRes, err := queryClient.CommunityPool(ctx, &disttypes.QueryCommunityPoolRequest{})
s.Require().NoError(err)
s.T().Logf("Initial community pool balance: %s", queryRes.Pool.String())
initialCommunityPool := queryRes.Pool

// Wait for outstanding rewards to equal 4000000usomm. Current theory is these initial rewards come from
// the genesis delegation tx fees.
s.T().Log("Waiting for outstanding rewards to equal 4000000usomm")
initialRewards := disttypes.ValidatorOutstandingRewards{
Rewards: sdk.DecCoins{
sdk.DecCoin{
Denom: params.BaseCoinUnit,
Amount: sdk.NewDec(4000000),
},
},
}
s.Require().Eventually(func() bool {
rewards, err := s.getValidatorOutstandingRewards(validator)
s.Require().NoError(err)
return rewards.Rewards.AmountOf(params.BaseCoinUnit).Equal(initialRewards.Rewards.AmountOf(params.BaseCoinUnit))
}, time.Second*30, time.Second*1, "outstanding rewards did not reach 4000000usomm")

// Submit proposal to enable validator incentives
s.T().Log("Submitting proposal to enable validator incentives")
cutoffHeight := 100
proposal := paramsproposal.ParameterChangeProposal{
Title: "Enable validator incentives",
Description: "Enable validator incentives",
Changes: []paramsproposal.ParamChange{
{
Subspace: "incentives",
Key: "ValidatorIncentivesCutoffHeight",
Value: fmt.Sprintf("\"%d\"", cutoffHeight),
},
{
Subspace: "incentives",
Key: "ValidatorMaxDistributionPerBlock",
Value: fmt.Sprintf("{\"denom\":\"%s\",\"amount\":\"%d\"}", params.BaseCoinUnit, 1000000),
},
},
}

proposalMsg, err := govtypesv1beta1.NewMsgSubmitProposal(
&proposal,
sdk.Coins{
{
Denom: testDenom,
Amount: stakeAmount.Quo(sdk.NewInt(2)),
},
},
proposer.address(),
)
s.Require().NoError(err)
submitProposalResponse, err := s.chain.sendMsgs(*proposerCtx, proposalMsg)
s.Require().NoError(err)
s.Require().Zero(submitProposalResponse.Code, "raw log: %s", submitProposalResponse.RawLog)

s.T().Log("Checking proposal was submitted correctly")
govQueryClient := govtypesv1beta1.NewQueryClient(orchClientCtx)
s.Require().Eventually(func() bool {
proposalsQueryResponse, err := govQueryClient.Proposals(context.Background(), &govtypesv1beta1.QueryProposalsRequest{})
if err != nil {
s.T().Logf("error querying proposals: %e", err)
return false
}

s.Require().NotEmpty(proposalsQueryResponse.Proposals)
s.Require().Equal(uint64(1), proposalsQueryResponse.Proposals[0].ProposalId, "not proposal id 1")
s.Require().Equal(govtypesv1beta1.StatusVotingPeriod, proposalsQueryResponse.Proposals[0].Status, "proposal not in voting period")

return true
}, time.Second*30, time.Second*5, "proposal submission was never found")

s.T().Log("Vote for proposal")
for _, val := range s.chain.validators {
kr, err := val.keyring()
s.Require().NoError(err)
localClientCtx, err := s.chain.clientContext("tcp://localhost:26657", &kr, "val", val.address())
s.Require().NoError(err)

voteMsg := govtypesv1beta1.NewMsgVote(val.address(), 1, govtypesv1beta1.OptionYes)
voteResponse, err := s.chain.sendMsgs(*localClientCtx, voteMsg)
s.Require().NoError(err)
s.Require().Zero(voteResponse.Code, "Vote error: %s", voteResponse.RawLog)
}

// Wait for proposal to be approved
s.T().Log("Waiting for proposal to be approved")
s.Require().Eventually(func() bool {
proposalQueryResponse, _ := govQueryClient.Proposal(context.Background(), &govtypesv1beta1.QueryProposalRequest{ProposalId: 1})
return govtypesv1beta1.StatusPassed == proposalQueryResponse.Proposal.Status
}, time.Second*30, time.Second*5, "proposal was never accepted")
s.T().Log("proposal approved!")

// Wait for a few blocks to pass to allow the validator rewards to increase
s.T().Log("Waiting for a few blocks to pass")
s.waitForBlocks(10)

// Get the updated outstanding rewards for the validator
s.T().Log("Getting updated validator rewards")
currentRewards2, err := s.getValidatorOutstandingRewards(validator)
s.Require().NoError(err)

// Check if the validator's outstanding rewards have increased
s.T().Logf("Initial rewards: %s, updated rewards: %s", initialRewards.Rewards, currentRewards2.Rewards)
s.Require().True(currentRewards2.Rewards.AmountOf(params.BaseCoinUnit).GT(initialRewards.Rewards.AmountOf(params.BaseCoinUnit)),
"Expected validator rewards to increase, got initial: %s, updated: %s", initialRewards.Rewards, currentRewards2.Rewards)

s.T().Logf("Validator rewards increased from %s to %s", initialRewards.Rewards, currentRewards2.Rewards)

s.T().Logf("Waiting to see validator rewards cut off at height %d", cutoffHeight)
s.waitUntilHeight(int64(cutoffHeight))

s.T().Log("Getting current validator rewards")
currentRewards, err := s.getValidatorOutstandingRewards(validator)
s.Require().NoError(err)

s.T().Logf("Current rewards: %s", currentRewards.Rewards)

s.T().Log("Waiting for a few blocks to pass")
s.waitForBlocks(10)

s.T().Log("Getting updated validator rewards")
currentRewards2, err = s.getValidatorOutstandingRewards(validator)
s.Require().NoError(err)

s.T().Logf("Current rewards: %s", currentRewards2.Rewards)
s.Require().Equal(currentRewards.Rewards, currentRewards2.Rewards, "Expected validator rewards to remain constant after cutoff height")
s.T().Log("Validator rewards ended!")

s.T().Log("Getting sum of all validator rewards")
totalRewards := sdk.DecCoins{}
for _, val := range s.chain.validators {
rewards, err := s.getValidatorOutstandingRewards(val)
s.Require().NoError(err)
totalRewards = totalRewards.Add(rewards.Rewards...)
}
s.T().Logf("Total rewards: %s", totalRewards)

s.T().Log("Getting community pool balance")
queryRes, err = queryClient.CommunityPool(ctx, &disttypes.QueryCommunityPoolRequest{})
s.Require().NoError(err)
s.T().Logf("Community pool balance: %s", queryRes.Pool.String())

// Subtract the initial rewards and the tx fees from the total rewards to get the incentive rewards
s.T().Log("Total incentive rewards is current rewards minus initial rewards minus tx fees from the proposal submission and votes")
totalIncentiveRewards := totalRewards.Sub(initialRewards.Rewards.MulDec(sdk.NewDec(4))).Sub(sdk.DecCoins{
{
Denom: testDenom,
Amount: sdk.NewDec(246913560),
},
}.MulDec(sdk.NewDec(5)))
s.T().Logf("Total incentive rewards: %s", totalIncentiveRewards)

s.T().Log("Checking that the total incentive rewards are equal to the community pool balance")
s.T().Logf("Initial community pool: %s, updated community pool: %s", initialCommunityPool, queryRes.Pool)
s.Require().Equal(totalIncentiveRewards, initialCommunityPool.Sub(queryRes.Pool), "Expected sum of all validator rewards to be equal to the change in community pool balance")
}

func (s *IntegrationTestSuite) getValidatorOutstandingRewards(val *validator) (disttypes.ValidatorOutstandingRewards, error) {
ctx := context.Background()
kb, err := val.keyring()
s.Require().NoError(err)
clientCtx, err := s.chain.clientContext("tcp://localhost:26657", &kb, "val", val.address())
s.Require().NoError(err)
queryClient := disttypes.NewQueryClient(clientCtx)
resp, err := queryClient.ValidatorOutstandingRewards(
ctx,
&disttypes.QueryValidatorOutstandingRewardsRequest{
ValidatorAddress: val.validatorAddress().String(),
},
)
if err != nil {
return disttypes.ValidatorOutstandingRewards{}, err
}
return resp.Rewards, nil
}

func (s *IntegrationTestSuite) waitForBlocks(numBlocks int64) error {
validator := s.chain.validators[0]
kb, err := validator.keyring()
s.Require().NoError(err)
clientCtx, err := s.chain.clientContext("tcp://localhost:26657", &kb, "val", validator.address())
s.Require().NoError(err)

initialHeight, err := s.getCurrentHeight(clientCtx)
s.Require().NoError(err)
targetHeight := initialHeight + numBlocks

for {
height, err := s.getCurrentHeight(clientCtx)
if err != nil {
return err
}

if height >= targetHeight {
break
}

time.Sleep(time.Second)
}

return nil
}

func (s *IntegrationTestSuite) waitUntilHeight(height int64) error {
validator := s.chain.validators[0]
kb, err := validator.keyring()
s.Require().NoError(err)
clientCtx, err := s.chain.clientContext("tcp://localhost:26657", &kb, "val", validator.address())
s.Require().NoError(err)

errorsTotal := 0
for {
if errorsTotal > 5 {
return fmt.Errorf("failed to get to height %d: too many errors", height)
}

currentHeight, err := s.getCurrentHeight(clientCtx)
if err != nil {
errorsTotal++
continue
}

if currentHeight >= height {
break
}

time.Sleep(time.Second * 3)
}

return nil
}
7 changes: 2 additions & 5 deletions integration_tests/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,11 +534,8 @@ func (s *IntegrationTestSuite) initGenesis() {
s.Require().NoError(err)
appGenState[gravitytypes.ModuleName] = bz

// incentivesGenState := incentivestypes.DefaultGenesisState()
// s.Require().NoError(cdc.UnmarshalJSON(appGenState[gravitytypes.ModuleName], &gravityGenState))
// bz, err = cdc.MarshalJSON(&incentivesGenState)
// s.Require().NoError(err)
// appGenState[incentivestypes.ModuleName] = bz
// set incentives gen state
s.Require().NoError(s.setIncentivesGenState(appGenState))

// serialize genesis state
bz, err = json.MarshalIndent(appGenState, "", " ")
Expand Down
8 changes: 8 additions & 0 deletions proto/incentives/v1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,12 @@ message Params {
// IncentivesCutoffHeight defines the block height after which the incentives module will stop sending coins to the distribution module from
// the community pool
uint64 incentives_cutoff_height = 2;
// ValidatorMaxDistributionPerBlock defines the maximum coins to be sent directly to voters in the last block from the community pool every block. Leftover coins remain in the community pool.
cosmos.base.v1beta1.Coin validator_max_distribution_per_block = 3 [(gogoproto.nullable) = false];
// ValidatorIncentivesCutoffHeight defines the block height after which the validator incentives will be stopped
uint64 validator_incentives_cutoff_height = 4;
// ValidatorIncentivesMaxFraction defines the maximum fraction of the validator distribution per block that can be sent to a single validator
string validator_incentives_max_fraction = 5 [(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", (gogoproto.nullable) = false];
// ValidatorIncentivesSetSizeLimit defines the max number of validators to apportion the validator distribution per block to
uint64 validator_incentives_set_size_limit = 6;
}
2 changes: 1 addition & 1 deletion x/axelarcork/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ func queryScheduledCorksByID() *cobra.Command {

func queryCorkResult() *cobra.Command {
cmd := &cobra.Command{
Use: "cork-result [chain-id] [cork-id]",
Use: "cork-result [cork-id] [chain-id]",
Aliases: []string{"cr"},
Args: cobra.ExactArgs(2),
Short: "query cork result from the chain",
Expand Down
Loading
Loading