From 0d7041766fdaecda3b55e39391362a41a07abdb4 Mon Sep 17 00:00:00 2001 From: Tom <54514587+GAtom22@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:06:03 -0300 Subject: [PATCH] imp(costaking): track baby staked to active validators (#1776) # Description Currently `AfterDelegationModified` hook will update co-staking tracker no matter val status This PR introduces the changes to hooks when delegation modified: - ONLY update baby amt in co-staking tracker if validator was in active set - using `staking.IterateLastValidatorPowers` Later on, `AfterEpochEnds` hook: - get newly active and inactive validators from: - cached `staking.IterateLastValidatorPowers` - is the prev-epoch validator set - `staking.IterateLastValidatorPowers` - returns the new epoch validators (updated before calling the hook) - if validator becomes inactive: - In case was add before - OK, will reduce the corresponding total amount - In case was reduced before - OK, will reduce the remaining (validator delegations already updated) - if validator becomes active: - update all delegators co-staking trackers --------- Co-authored-by: RafilxTenfen (cherry picked from commit c8277ad31bee50eddcecb80a9d751104ea6e493c) --- .github/workflows/ci.yml | 22 + CHANGELOG.md | 1 + Makefile | 4 + app/keepers/keepers.go | 5 +- app/upgrades/v4/upgrades.go | 37 +- app/upgrades/v4/upgrades_costaking_test.go | 136 ++++- proto/babylon/costaking/v1/costaking.proto | 24 + proto/babylon/costaking/v1/genesis.proto | 3 + test/e2e/configurer/chain/commands.go | 25 +- test/e2e/configurer/chain/node.go | 22 +- test/e2e/configurer/chain/queries.go | 109 ++++ test/e2e/e2e_test.go | 5 + test/e2e/epoching_spam_prevention_e2e_test.go | 66 +-- test/e2e/validator_jailing_e2e_test.go | 419 +++++++++++++++ test/e2ev2/tmanager/genesis.go | 1 - test/replay/costaking_test.go | 363 +++++++++++++ test/replay/driver.go | 29 +- test/replay/slashing.go | 11 + test/replay/staking.go | 16 + types/sdk_math.go | 2 +- x/btccheckpoint/keeper/hooks.go | 2 + x/costaking/keeper/genesis.go | 14 + x/costaking/keeper/genesis_test.go | 37 ++ x/costaking/keeper/hooks_epoching.go | 224 ++++++++ x/costaking/keeper/hooks_epoching_test.go | 338 ++++++++++++ x/costaking/keeper/hooks_staking.go | 141 ++++- x/costaking/keeper/hooks_staking_test.go | 189 ++++++- x/costaking/keeper/keeper.go | 8 + x/costaking/keeper/keeper_test.go | 12 +- x/costaking/keeper/reward_tracker.go | 4 + x/costaking/keeper/validator.go | 33 ++ x/costaking/types/costaking.pb.go | 501 +++++++++++++++++- x/costaking/types/expected_keepers.go | 4 +- x/costaking/types/genesis.go | 3 + x/costaking/types/genesis.pb.go | 116 ++-- x/costaking/types/keys.go | 1 + x/costaking/types/mocked_keepers.go | 43 +- x/costaking/types/rewards.go | 8 + x/costaking/types/rewards_test.go | 100 ++++ x/costaking/types/staking_cache.go | 108 +++- x/costaking/types/staking_cache_test.go | 123 +++-- x/epoching/abci.go | 3 + x/epoching/keeper/hooks.go | 7 + x/epoching/types/expected_keepers.go | 1 + x/epoching/types/hooks.go | 6 + x/monitor/keeper/hooks.go | 2 + 46 files changed, 3086 insertions(+), 242 deletions(-) create mode 100644 test/e2e/validator_jailing_e2e_test.go create mode 100644 test/replay/slashing.go create mode 100644 x/costaking/keeper/hooks_epoching.go create mode 100644 x/costaking/keeper/hooks_epoching_test.go create mode 100644 x/costaking/keeper/validator.go create mode 100644 x/costaking/types/rewards_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ccb59a8c..8d52fe5bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -313,6 +313,28 @@ jobs: run: | make test-e2e-cache-btc-stake-expansion + e2e-run-validator-jailing: + needs: [e2e-docker-build-babylon] + runs-on: ubuntu-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Download babylon artifact + uses: actions/download-artifact@v4 + with: + name: babylond-${{ github.sha }} + path: /tmp + - name: Docker load babylond + run: | + docker load < /tmp/docker-babylond.tar.gz + - name: Cache Go + uses: actions/setup-go@v5 + with: + go-version: 1.23 + - name: Run e2e TestValidatorJailingTestSuite + run: | + make test-e2e-cache-validator-jailing + e2e-V2: needs: [e2e-docker-build-babylon] runs-on: ubuntu-22.04 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d0d7dd5f..fcb3d62f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [#1762](https://github.com/babylonlabs-io/babylon/pull/1762) Add new test case on stake expansion - [#1785](https://github.com/babylonlabs-io/babylon/pull/1785) CI reusable to v0.13.5 and golang lint version 2 +- [#1776](https://github.com/babylonlabs-io/babylon/pull/1776) Track baby staked to active validators only ### Bug fixes diff --git a/Makefile b/Makefile index e461213a4..ad02e3ded 100644 --- a/Makefile +++ b/Makefile @@ -248,6 +248,7 @@ test-e2e-cache: $(MAKE) test-e2e-cache-upgrade-v2 $(MAKE) test-e2e-cache-epoching-spam-prevention $(MAKE) test-e2e-cache-btc-stake-expansion + $(MAKE) test-e2e-cache-validator-jailing clean-e2e: docker container rm -f $(shell docker container ls -a -q) || true @@ -295,6 +296,9 @@ test-e2e-cache-upgrade-v4: test-e2e-cache-btc-stake-expansion: go test -run TestBTCStakeExpansionTestSuite -mod=readonly -timeout=60m -v $(PACKAGES_E2E) --tags=e2e +test-e2e-cache-validator-jailing: + go test -run TestValidatorJailingTestSuite -mod=readonly -timeout=60m -v $(PACKAGES_E2E) --tags=e2e + test-sim-nondeterminism: @echo "Running non-determinism test..." @go test -mod=readonly $(SIMAPP) -run TestAppStateDeterminism -Enabled=true \ diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index b96aaba6f..7cc695f2e 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -544,7 +544,10 @@ func (ak *AppKeepers) InitKeepers( // make ZoneConcierge and Monitor to subscribe to the epoching's hooks ak.EpochingKeeper = *epochingKeeper.SetHooks( - epochingtypes.NewMultiEpochingHooks(ak.MonitorKeeper.Hooks()), + epochingtypes.NewMultiEpochingHooks( + ak.MonitorKeeper.Hooks(), + ak.CostakingKeeper.HookEpoching(), + ), ) // set up Checkpointing, BTCCheckpoint, and BTCLightclient keepers diff --git a/app/upgrades/v4/upgrades.go b/app/upgrades/v4/upgrades.go index 2c09e6c50..1e21e6521 100644 --- a/app/upgrades/v4/upgrades.go +++ b/app/upgrades/v4/upgrades.go @@ -134,7 +134,7 @@ func InitializeCoStakerRwdsTracker( // Returns the total score of the co-staker rewards tracker func saveBABYStakersRwdTracker(ctx context.Context, cdc codec.BinaryCodec, costkStoreService corestoretypes.KVStoreService, stkKeeper *stkkeeper.Keeper, params costktypes.Params) (math.Int, error) { totalScore := math.ZeroInt() - // Get all BABY stakers + // Get all BABY stakers that are staking to an active validator babyStakers, err := getAllBABYStakers(ctx, stkKeeper) if err != nil { return totalScore, fmt.Errorf("failed to get all BABY stakers: %w", err) @@ -153,36 +153,23 @@ func saveBABYStakersRwdTracker(ctx context.Context, cdc codec.BinaryCodec, costk return totalScore, nil } -// getAllBABYStakers retrieves all BABY stakers with pagination +// getAllBABYStakers retrieves all BABY stakers by iterating only over active validators func getAllBABYStakers(ctx context.Context, stkKeeper *stkkeeper.Keeper) (map[string]math.Int, error) { stkQuerier := stkkeeper.NewQuerier(stkKeeper) babyStakers := make(map[string]math.Int) - // First get all validators - var nextKey []byte - for { - req := &stktypes.QueryValidatorsRequest{ - Pagination: &query.PageRequest{ - Key: nextKey, - }, - } - - res, err := stkQuerier.Validators(ctx, req) - if err != nil { - return nil, err + // Iterate directly over active validators (last validator powers) + err := stkKeeper.IterateLastValidatorPowers(ctx, func(valAddr sdk.ValAddress, power int64) bool { + // Get all delegations for this active validator + if err := getValidatorDelegations(ctx, stkQuerier, valAddr.String(), babyStakers); err != nil { + // Return true to stop iteration on error + return true } + return false // continue iteration + }) - // For each validator, get all delegations - for _, validator := range res.Validators { - if err := getValidatorDelegations(ctx, stkQuerier, validator.OperatorAddress, babyStakers); err != nil { - return nil, err - } - } - - if res.Pagination == nil || len(res.Pagination.NextKey) == 0 { - break - } - nextKey = res.Pagination.NextKey + if err != nil { + return nil, fmt.Errorf("failed to iterate active validators: %w", err) } return babyStakers, nil diff --git a/app/upgrades/v4/upgrades_costaking_test.go b/app/upgrades/v4/upgrades_costaking_test.go index 535ea5ab8..cfde1ec88 100644 --- a/app/upgrades/v4/upgrades_costaking_test.go +++ b/app/upgrades/v4/upgrades_costaking_test.go @@ -199,6 +199,54 @@ func TestInitializeCoStakerRwdsTracker_FpNotActive(t *testing.T) { verifyCoStakerCreated(t, ctx, cdc, storeService, stakerAddr, math.ZeroInt(), babyAmount) } +func TestInitializeCoStakerRwdsTracker_ValidatorNotActive(t *testing.T) { + ctx, cdc, storeService, stkKeeper, btcStkKeeper, _, costkKeeper, fKeeper, ctrl := setupTestKeepers(t, 10) + defer ctrl.Finish() + + require.NoError(t, btcStkKeeper.SetParams(ctx, btcstktypes.DefaultParams())) + require.NoError(t, stkKeeper.SetParams(ctx, stktypes.DefaultParams())) + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + // Create a test staker address + stakerAddr := datagen.GenRandomAccount().GetAddress() + + // Create BTC delegation + btcDel := createTestBTCDelegation(t, r, ctx, btcStkKeeper, stakerAddr, 50000) + + // Create baby staking delegation to an INACTIVE validator (not in LastValidatorPowers) + validatorAddr := datagen.GenRandomValidatorAddress() + babyAmount := math.NewInt(25000) + delegation := stktypes.Delegation{ + DelegatorAddress: stakerAddr.String(), + ValidatorAddress: validatorAddr.String(), + Shares: math.LegacyNewDecFromInt(babyAmount), + } + + // Create validator but DON'T add to LastValidatorPowers (making it inactive) + validator := stktypes.Validator{ + OperatorAddress: validatorAddr.String(), + Tokens: babyAmount, + DelegatorShares: math.LegacyNewDecFromInt(babyAmount), + Status: stktypes.Unbonded, // Inactive validator + } + require.NoError(t, stkKeeper.SetValidator(ctx, validator)) + require.NoError(t, stkKeeper.SetDelegation(ctx, delegation)) + // NOTE: Not calling SetLastValidatorPower - validator is NOT in active set + + // seed voting power dist cache with FP as active + setupVotingPowerDistCacheWithActiveFPs(t, r, ctx, fKeeper, btcDel.FpBtcPkList) + + // Execute upgrade function + err := v4.InitializeCoStakerRwdsTracker( + ctx, cdc, storeService, stkKeeper, btcStkKeeper, *costkKeeper, *fKeeper, + ) + require.NoError(t, err) + + // Verify co-staker was created with zero active baby (validator not active, BTC only) + verifyCoStakerCreated(t, ctx, cdc, storeService, stakerAddr, math.NewIntFromUint64(btcDel.TotalSat), math.ZeroInt()) +} + func TestInitializeCoStakerRwdsTracker_WithRealDelegations(t *testing.T) { ctx, cdc, storeService, stkKeeper, btcStkKeeper, _, costkKeeper, fKeeper, ctrl := setupTestKeepers(t, 10) defer ctrl.Finish() @@ -259,6 +307,73 @@ func TestInitializeCoStakerRwdsTracker_OnlyBTCStaking(t *testing.T) { verifyCoStakerCreated(t, ctx, cdc, storeService, stakerAddr, math.NewIntFromUint64(btcDel.TotalSat), math.ZeroInt()) } +func TestInitializeCoStakerRwdsTracker_MixedActiveInactiveValidators(t *testing.T) { + ctx, cdc, storeService, stkKeeper, btcStkKeeper, _, costkKeeper, fKeeper, ctrl := setupTestKeepers(t, 10) + defer ctrl.Finish() + + require.NoError(t, btcStkKeeper.SetParams(ctx, btcstktypes.DefaultParams())) + require.NoError(t, stkKeeper.SetParams(ctx, stktypes.DefaultParams())) + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + // Create a test staker address + stakerAddr := datagen.GenRandomAccount().GetAddress() + + // Create BTC delegation + btcDel := createTestBTCDelegation(t, r, ctx, btcStkKeeper, stakerAddr, 50000) + + // Create delegation to ACTIVE validator + activeValAddr := datagen.GenRandomValidatorAddress() + activeBabyAmount := math.NewInt(15000) + activeDelegation := stktypes.Delegation{ + DelegatorAddress: stakerAddr.String(), + ValidatorAddress: activeValAddr.String(), + Shares: math.LegacyNewDecFromInt(activeBabyAmount), + } + activeValidator := stktypes.Validator{ + OperatorAddress: activeValAddr.String(), + Tokens: activeBabyAmount, + DelegatorShares: math.LegacyNewDecFromInt(activeBabyAmount), + Status: stktypes.Bonded, + } + require.NoError(t, stkKeeper.SetValidator(ctx, activeValidator)) + require.NoError(t, stkKeeper.SetDelegation(ctx, activeDelegation)) + // Mark as active validator + power := stkKeeper.TokensToConsensusPower(ctx, activeBabyAmount) + require.NoError(t, stkKeeper.SetLastValidatorPower(ctx, activeValAddr, power)) + + // Create delegation to INACTIVE validator + inactiveValAddr := datagen.GenRandomValidatorAddress() + inactiveBabyAmount := math.NewInt(10000) + inactiveDelegation := stktypes.Delegation{ + DelegatorAddress: stakerAddr.String(), + ValidatorAddress: inactiveValAddr.String(), + Shares: math.LegacyNewDecFromInt(inactiveBabyAmount), + } + inactiveValidator := stktypes.Validator{ + OperatorAddress: inactiveValAddr.String(), + Tokens: inactiveBabyAmount, + DelegatorShares: math.LegacyNewDecFromInt(inactiveBabyAmount), + Status: stktypes.Unbonded, // Not bonded + } + require.NoError(t, stkKeeper.SetValidator(ctx, inactiveValidator)) + require.NoError(t, stkKeeper.SetDelegation(ctx, inactiveDelegation)) + // NOTE: Not calling SetLastValidatorPower - validator is NOT active + + // seed voting power dist cache with FP as active + setupVotingPowerDistCacheWithActiveFPs(t, r, ctx, fKeeper, btcDel.FpBtcPkList) + + // Execute upgrade function + err := v4.InitializeCoStakerRwdsTracker( + ctx, cdc, storeService, stkKeeper, btcStkKeeper, *costkKeeper, *fKeeper, + ) + require.NoError(t, err) + + // Verify co-staker was created with only the active validator's baby amount + // Total baby should be 15000 (from active validator), not 25000 (15000 + 10000) + verifyCoStakerCreated(t, ctx, cdc, storeService, stakerAddr, math.NewIntFromUint64(btcDel.TotalSat), activeBabyAmount) +} + func TestInitializeCoStakerRwdsTracker_MultipleCombinations(t *testing.T) { ctx, cdc, storeService, stkKeeper, btcStkKeeper, _, costkKeeper, fKeeper, ctrl := setupTestKeepers(t, 10) defer ctrl.Finish() @@ -646,12 +761,19 @@ func createBabyDelegation(t *testing.T, ctx context.Context, stkKeeper *stkkeepe Shares: math.LegacyNewDecFromInt(delAmount), } - require.NoError(t, stkKeeper.SetValidator(ctx, stktypes.Validator{ + // Create validator and mark it as active (bonded with power) + validator := stktypes.Validator{ OperatorAddress: validatorAddr.String(), Tokens: delAmount, DelegatorShares: math.LegacyNewDecFromInt(delAmount), - })) + Status: stktypes.Bonded, + } + require.NoError(t, stkKeeper.SetValidator(ctx, validator)) require.NoError(t, stkKeeper.SetDelegation(ctx, delegation)) + + // Add to LastValidatorPowers to mark as active validator + power := stkKeeper.TokensToConsensusPower(ctx, delAmount) + require.NoError(t, stkKeeper.SetLastValidatorPower(ctx, validatorAddr, power)) } func verifyCoStakerCreated(t *testing.T, ctx sdk.Context, cdc codec.BinaryCodec, storeService corestore.KVStoreService, stakerAddr sdk.AccAddress, expectedBTCAmount, expectedBabyAmount math.Int) { @@ -949,6 +1071,7 @@ func loadAndSeedCosmosDelegations(t *testing.T, ctx sdk.Context, env string, stk OperatorAddress: rawDel.Delegation.ValidatorAddress, Tokens: math.ZeroInt(), DelegatorShares: math.LegacyZeroDec(), + Status: stktypes.Bonded, // Mark as bonded so it's considered active } if err := stkKeeper.SetValidator(ctx, validator); err != nil { return 0, fmt.Errorf("failed to set validator %s: %w", rawDel.Delegation.ValidatorAddress, err) @@ -968,7 +1091,8 @@ func loadAndSeedCosmosDelegations(t *testing.T, ctx sdk.Context, env string, stk } // Update validator shares and tokens - validator, err := stkKeeper.GetValidator(ctx, sdk.MustValAddressFromBech32(rawDel.Delegation.ValidatorAddress)) + valAddr := sdk.MustValAddressFromBech32(rawDel.Delegation.ValidatorAddress) + validator, err := stkKeeper.GetValidator(ctx, valAddr) if err != nil { return 0, fmt.Errorf("validator %s not found after creation", rawDel.Delegation.ValidatorAddress) } @@ -978,6 +1102,12 @@ func loadAndSeedCosmosDelegations(t *testing.T, ctx sdk.Context, env string, stk return 0, fmt.Errorf("failed to update validator %s: %w", rawDel.Delegation.ValidatorAddress, err) } + // Mark validator as active by adding to LastValidatorPowers + power := stkKeeper.TokensToConsensusPower(ctx, validator.Tokens) + if err := stkKeeper.SetLastValidatorPower(ctx, valAddr, power); err != nil { + return 0, fmt.Errorf("failed to set last validator power for %s: %w", rawDel.Delegation.ValidatorAddress, err) + } + count++ if count%1000 == 0 { t.Logf("Processed %d cosmos delegations...", count) diff --git a/proto/babylon/costaking/v1/costaking.proto b/proto/babylon/costaking/v1/costaking.proto index 33884aec9..75f970b6e 100644 --- a/proto/babylon/costaking/v1/costaking.proto +++ b/proto/babylon/costaking/v1/costaking.proto @@ -35,3 +35,27 @@ message Params { (gogoproto.nullable) = false ]; } + +// Validator is a message that denotes a validator +message Validator { + // addr is the validator's address (in sdk.ValAddress) + bytes addr = 1; + // tokens define the delegated tokens (incl. self-delegation). + bytes tokens = 2 [ + (cosmos_proto.scalar) = "cosmos.Int", + (gogoproto.customtype) = "cosmossdk.io/math.Int", + (gogoproto.nullable) = false + ]; + // shares defines total shares issued to a validator's delegators. + string shares = 3 [ + (cosmos_proto.scalar) = "cosmos.Dec", + (gogoproto.customtype) = "cosmossdk.io/math.LegacyDec", + (gogoproto.nullable) = false + ]; +} + +// ValidatorSet is a message that denotes a set of validators +message ValidatorSet { + // validators is the list of all validators and their delegated tokens. + repeated Validator validators = 1; +} \ No newline at end of file diff --git a/proto/babylon/costaking/v1/genesis.proto b/proto/babylon/costaking/v1/genesis.proto index ae1895f2f..dfed81b0a 100644 --- a/proto/babylon/costaking/v1/genesis.proto +++ b/proto/babylon/costaking/v1/genesis.proto @@ -22,6 +22,9 @@ message GenesisState { // costaker addresses. repeated CostakerRewardsTrackerEntry costakers_rewards_tracker = 4 [ (gogoproto.nullable) = false ]; + // validator_set contains all validators and their delegated tokens. + ValidatorSet validator_set = 5 + [ (gogoproto.nullable) = false ]; } // CurrentRewardsEntry represents the chain current rewards. diff --git a/test/e2e/configurer/chain/commands.go b/test/e2e/configurer/chain/commands.go index 4e5758aa7..4b9324a05 100644 --- a/test/e2e/configurer/chain/commands.go +++ b/test/e2e/configurer/chain/commands.go @@ -22,6 +22,9 @@ import ( btccheckpointtypes "github.com/babylonlabs-io/babylon/v4/x/btccheckpoint/types" blc "github.com/babylonlabs-io/babylon/v4/x/btclightclient/types" cttypes "github.com/babylonlabs-io/babylon/v4/x/checkpointing/types" + cmtjson "github.com/cometbft/cometbft/libs/json" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/bech32" sdkquerytypes "github.com/cosmos/cosmos-sdk/types/query" @@ -573,11 +576,29 @@ func (n *NodeConfig) FailICASendTx(from, connectionID, packetMsgPath string) { n.LogActionF("Failed to perform ICA send (as expected)") } -func (n *NodeConfig) Delegate(fromWallet, validator string, amount string, overallFlags ...string) { +func (n *NodeConfig) Delegate(fromWallet, validator string, amount string, overallFlags ...string) (txHash string) { n.LogActionF("delegating from %s to validator %s", fromWallet, validator) cmd := []string{"babylond", "tx", "epoching", "delegate", validator, amount, fmt.Sprintf("--from=%s", fromWallet)} - _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainId, n.Name, append(cmd, overallFlags...)) + outBuf, _, err := n.containerManager.ExecTxCmd(n.t, n.chainId, n.Name, append(cmd, overallFlags...)) require.NoError(n.t, err) n.LogActionF("successfully delegated %s to validator %s", fromWallet, validator) + return GetTxHashFromOutput(outBuf.String()) +} + +// ValidatorConsPubKey gets the consensus pubkey base64 key from a validator node +func (n *NodeConfig) ValidatorConsPubKey() cryptotypes.PubKey { + cmd := []string{"babylond", "comet", "show-validator", "--home=/home/babylon/babylondata"} + outBuf, _, err := n.containerManager.ExecCmd(n.t, n.Name, cmd, "") + require.NoError(n.t, err) + + // Parse the JSON output to extract the base64 key + // Format: {"@type":"/cosmos.crypto.ed25519.PubKey","key":""} + var pubKey ed25519.PubKey + output := strings.TrimSpace(outBuf.String()) + + err = cmtjson.Unmarshal([]byte(output), &pubKey) + require.NoError(n.t, err) + + return &pubKey } diff --git a/test/e2e/configurer/chain/node.go b/test/e2e/configurer/chain/node.go index 74e0de71b..aca066d85 100644 --- a/test/e2e/configurer/chain/node.go +++ b/test/e2e/configurer/chain/node.go @@ -5,7 +5,6 @@ import ( "encoding/hex" "fmt" "regexp" - "strings" "testing" "time" @@ -175,13 +174,26 @@ func (n *NodeConfig) extractOperatorAddressIfValidator() error { cmd := []string{"babylond", "debug", "addr", hex.EncodeToString(n.PublicKey)} n.t.Logf("extracting validator operator addresses for validator: %s", n.Name) - _, errBuf, err := n.containerManager.ExecCmd(n.t, n.Name, cmd, "") + outBuf, errBuf, err := n.containerManager.ExecCmd(n.t, n.Name, cmd, "") if err != nil { return err } - re := regexp.MustCompile("bbnvaloper(.{39})") - operAddr := fmt.Sprintf("%s\n", re.FindString(errBuf.String())) - n.OperatorAddress = strings.TrimSuffix(operAddr, "\n") + + // Try to find the operator address in stdout first, then stderr + output := outBuf.String() + if output == "" { + output = errBuf.String() + } + + // Match the full bech32 validator operator address (bbnvaloper1... with ~59 chars total) + re := regexp.MustCompile(`bbnvaloper1[a-z0-9]{38,59}`) + operAddr := re.FindString(output) + n.OperatorAddress = operAddr + + if n.OperatorAddress == "" { + n.t.Logf("Warning: could not extract operator address for validator %s from output: %s", n.Name, output) + } + return nil } diff --git a/test/e2e/configurer/chain/queries.go b/test/e2e/configurer/chain/queries.go index 6da8d39c8..872d220c6 100644 --- a/test/e2e/configurer/chain/queries.go +++ b/test/e2e/configurer/chain/queries.go @@ -24,6 +24,7 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" icacontrollertypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/controller/types" "github.com/stretchr/testify/require" @@ -668,6 +669,22 @@ func (n *NodeConfig) QueryValidators() ([]stakingtypes.Validator, error) { return resp.Validators, nil } +// QueryValidators returns all validators +func (n *NodeConfig) QueryValidator(valAddr string) (*stakingtypes.Validator, error) { + path := fmt.Sprintf("cosmos/staking/v1beta1/validators/%s", valAddr) + bz, err := n.QueryGRPCGateway(path, url.Values{}) + if err != nil { + return nil, err + } + + var resp stakingtypes.QueryValidatorResponse + if err := util.Cdc.UnmarshalJSON(bz, &resp); err != nil { + return nil, err + } + + return &resp.Validator, nil +} + // QueryDelegatorDelegations returns delegator delegations for a given address func (n *NodeConfig) QueryDelegatorDelegations(delegatorAddr string) ([]stakingtypes.DelegationResponse, error) { path := fmt.Sprintf("cosmos/staking/v1beta1/delegations/%s", delegatorAddr) @@ -684,6 +701,22 @@ func (n *NodeConfig) QueryDelegatorDelegations(delegatorAddr string) ([]stakingt return resp.DelegationResponses, nil } +// QueryValidatorDelegations returns validator delegations for a given address +func (n *NodeConfig) QueryValidatorDelegations(validatorAddr string) ([]stakingtypes.DelegationResponse, error) { + path := fmt.Sprintf("/cosmos/staking/v1beta1/validators/%s/delegations", validatorAddr) + bz, err := n.QueryGRPCGateway(path, url.Values{}) + if err != nil { + return nil, err + } + + var resp stakingtypes.QueryValidatorDelegationsResponse + if err := util.Cdc.UnmarshalJSON(bz, &resp); err != nil { + return nil, err + } + + return resp.DelegationResponses, nil +} + // WaitForNextEpoch waits for the next epoch to start func (n *NodeConfig) WaitForNextEpoch() (uint64, error) { currentEpoch, err := n.QueryCurrentEpoch() @@ -704,3 +737,79 @@ func (n *NodeConfig) WaitForNextEpoch() (uint64, error) { return nextEpoch, nil } + +// QueryAllSigningInfos queries all validator signing infos +func (n *NodeConfig) QueryAllSigningInfos() ([]slashingtypes.ValidatorSigningInfo, error) { + path := "cosmos/slashing/v1beta1/signing_infos" + bz, err := n.QueryGRPCGateway(path, url.Values{}) + if err != nil { + return nil, err + } + + var resp slashingtypes.QuerySigningInfosResponse + if err := util.Cdc.UnmarshalJSON(bz, &resp); err != nil { + return nil, err + } + + return resp.Info, nil +} + +// QuerySigningInfo queries the signing info for a validator +func (n *NodeConfig) QuerySigningInfo(consAddr string) (slashingtypes.ValidatorSigningInfo, error) { + path := fmt.Sprintf("cosmos/slashing/v1beta1/signing_infos/%s", consAddr) + bz, err := n.QueryGRPCGateway(path, url.Values{}) + if err != nil { + return slashingtypes.ValidatorSigningInfo{}, err + } + + var resp slashingtypes.QuerySigningInfoResponse + if err := util.Cdc.UnmarshalJSON(bz, &resp); err != nil { + return slashingtypes.ValidatorSigningInfo{}, err + } + + return resp.ValSigningInfo, nil +} + +// QuerySlashingParams queries the slashing module parameters +func (n *NodeConfig) QuerySlashingParams() (slashingtypes.Params, error) { + path := "cosmos/slashing/v1beta1/params" + bz, err := n.QueryGRPCGateway(path, url.Values{}) + if err != nil { + return slashingtypes.Params{}, err + } + + var resp slashingtypes.QueryParamsResponse + if err := util.Cdc.UnmarshalJSON(bz, &resp); err != nil { + return slashingtypes.Params{}, err + } + + return resp.Params, nil +} + +// QueryEpochValSet queries the validator set of a given epoch +func (n *NodeConfig) QueryEpochValSet(epochNum uint64) (*etypes.QueryEpochValSetResponse, error) { + path := fmt.Sprintf("babylon/epoching/v1/epochs/%d/validator_set", epochNum) + bz, err := n.QueryGRPCGateway(path, url.Values{}) + if err != nil { + return nil, err + } + + var resp etypes.QueryEpochValSetResponse + if err := util.Cdc.UnmarshalJSON(bz, &resp); err != nil { + return nil, err + } + + return &resp, nil +} + +// QueryCurrentEpochValSet queries the validator set of the current epoch +func (n *NodeConfig) QueryCurrentEpochValSet() (*etypes.QueryEpochValSetResponse, error) { + // First get the current epoch number + currentEpoch, err := n.QueryCurrentEpoch() + if err != nil { + return nil, err + } + + // Then query the validator set for that epoch + return n.QueryEpochValSet(currentEpoch) +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index f6b30065a..89da86b44 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -58,3 +58,8 @@ func TestEpochingSpamPreventionTestSuite(t *testing.T) { func TestBTCStakeExpansionTestSuite(t *testing.T) { suite.Run(t, new(BTCStakeExpansionTestSuite)) } + +// TestValidatorJailingTestSuite tests validator jailing scenario end-to-end +func TestValidatorJailingTestSuite(t *testing.T) { + suite.Run(t, new(ValidatorJailingTestSuite)) +} diff --git a/test/e2e/epoching_spam_prevention_e2e_test.go b/test/e2e/epoching_spam_prevention_e2e_test.go index b97914683..c1004ab6c 100644 --- a/test/e2e/epoching_spam_prevention_e2e_test.go +++ b/test/e2e/epoching_spam_prevention_e2e_test.go @@ -1,6 +1,7 @@ package e2e import ( + "bytes" "encoding/base64" "encoding/json" "fmt" @@ -16,6 +17,7 @@ import ( "github.com/babylonlabs-io/babylon/v4/test/e2e/util" "github.com/babylonlabs-io/babylon/v4/testutil/datagen" etypes "github.com/babylonlabs-io/babylon/v4/x/epoching/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" sdked25519 "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" @@ -108,22 +110,31 @@ func (s *EpochingSpamPreventionTestSuite) TestNormalDelegationCase() { s.Require().NotNil(validatorNode, "Should have at least one validator node") - // Get validator operator address - if validatorNode.OperatorAddress != "" { - validatorAddr = validatorNode.OperatorAddress - } else { - s.T().Logf("OperatorAddress is empty, converting PublicAddress using SDK...") - // Convert account address to validator address using proper bech32 conversion - if validatorNode.PublicAddress != "" { - accAddr, err := sdk.AccAddressFromBech32(validatorNode.PublicAddress) - s.NoError(err) - // Convert account address to validator address - valAddr := sdk.ValAddress(accAddr) - validatorAddr = valAddr.String() - } else { - s.T().Fatalf("Cannot derive validator address from empty PublicAddress") + // Get consensus pubkeys from each validator node to map them to on-chain validators + s.T().Log("Mapping validator nodes to on-chain validators...") + nodeConsPubKey := validatorNode.ValidatorConsPubKey() + s.T().Logf("Node consensus pubkey: %s", nodeConsPubKey) + + // Query validators from the chain + validators, err := nonValidatorNode.QueryValidators() + s.NoError(err) + s.Require().True(len(validators) > 0, "Need at least 1 validator") + + // Map nodes to validators by matching consensus pubkey + var val *stakingtypes.Validator + for i := range validators { + // Unmarshal the consensus pubkey using the codec + var valConsPubKey cryptotypes.PubKey + err := util.Cdc.UnpackAny(validators[i].ConsensusPubkey, &valConsPubKey) + s.Require().NoError(err, "failed to unmarshal consensus pubkey") + + if bytes.Equal(valConsPubKey.Bytes(), nodeConsPubKey.Bytes()) { + val = &validators[i] + s.T().Logf("Node maps to validator: %s (tokens: %s)", validators[i].OperatorAddress, validators[i].Tokens.String()) } } + s.Require().NotNil(val, "Could not map node to validator") + validatorAddr = val.OperatorAddress s.T().Logf("Using validator address: '%s'", validatorAddr) s.Require().NotEmpty(validatorAddr, "Validator address should not be empty") @@ -158,27 +169,8 @@ func (s *EpochingSpamPreventionTestSuite) TestNormalDelegationCase() { validatorAddr, delegationAmountCoin, nonValidatorNode.WalletName) // Execute the epoching delegate transaction and capture txHash (similar to createValidator approach) - delegateCmd := []string{ - "babylond", "tx", "epoching", "delegate", validatorAddr, delegationAmountCoin, - fmt.Sprintf("--from=%s", nonValidatorNode.WalletName), - "--keyring-backend=test", - "--home=/home/babylon/babylondata", - "--chain-id", nonValidatorNode.GetChainID(), - "--yes", - "--gas=auto", - "--gas-adjustment=1.3", - "--gas-prices=1ubbn", - "-b=sync", - } - outBuf, errBuf, err := nonValidatorNode.ExecRawCmd(delegateCmd) - if err != nil { - s.T().Logf("delegate failed: %s", errBuf.String()) - } - s.NoError(err) + txHash := nonValidatorNode.Delegate(nonValidatorNode.WalletName, validatorAddr, delegationAmountCoin, "--gas=500000") - // Extract txHash from output - txOutput := outBuf.String() - txHash := chain.GetTxHashFromOutput(txOutput) s.Require().NotEmpty(txHash, "Failed to extract txHash from transaction output") s.T().Logf("WrappedMsgDelegate sent successfully, txHash: %s", txHash) @@ -186,8 +178,8 @@ func (s *EpochingSpamPreventionTestSuite) TestNormalDelegationCase() { chainA.WaitForNumHeights(2) // Query the transaction to get actual gas used (following reviewer's example) - txResponse, _, err := nonValidatorNode.QueryTxWithError(txHash) - s.NoError(err) + txResponse, _ := nonValidatorNode.QueryTx(txHash) + s.Equal(txResponse.Code, uint32(0), txResponse.RawLog) // Step 1 Verification: Check that funds are locked (delegator balance decreased, module balance increased) delegatorBalanceAfterLock, err := nonValidatorNode.QueryBalance(delegatorAddr, "ubbn") @@ -253,7 +245,7 @@ func (s *EpochingSpamPreventionTestSuite) TestNormalDelegationCase() { "babylond", "query", "staking", "delegation", delegatorAddr, validatorAddr, "--output=json", "--home=/home/babylon/babylondata", } - outBuf, errBuf, err = nonValidatorNode.ExecRawCmd(delegationCmd) + outBuf, errBuf, err := nonValidatorNode.ExecRawCmd(delegationCmd) if err != nil { s.T().Logf("delegation query failed: %s", errBuf.String()) } diff --git a/test/e2e/validator_jailing_e2e_test.go b/test/e2e/validator_jailing_e2e_test.go new file mode 100644 index 000000000..8ef5c1b89 --- /dev/null +++ b/test/e2e/validator_jailing_e2e_test.go @@ -0,0 +1,419 @@ +//go:build e2e +// +build e2e + +package e2e + +import ( + "bytes" + "math" + "time" + + sdkmath "cosmossdk.io/math" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/suite" + + "github.com/babylonlabs-io/babylon/v4/test/e2e/configurer" + "github.com/babylonlabs-io/babylon/v4/test/e2e/configurer/chain" + "github.com/babylonlabs-io/babylon/v4/test/e2e/util" +) + +type ValidatorJailingTestSuite struct { + suite.Suite + + configurer configurer.Configurer +} + +func (s *ValidatorJailingTestSuite) SetupSuite() { + s.T().Log("setting up validator jailing e2e integration test suite...") + var err error + + // Configure 1 chain with 2 validator nodes + s.configurer, err = configurer.NewBabylonConfigurer(s.T(), true) + s.NoError(err) + err = s.configurer.ConfigureChains() + s.NoError(err) + err = s.configurer.RunSetup() + s.NoError(err) +} + +func (s *ValidatorJailingTestSuite) TearDownSuite() { + err := s.configurer.ClearResources() + if err != nil { + s.T().Logf("error to clear resources %s", err.Error()) + } +} + +// TestValidatorJailingWithExtraDelegation tests the scenario where: +// 1. Two validators are running with delegations +// 2. We delegate to validator 1 to give it >66% of voting power +// 3. The second validator stops signing blocks +// 4. After missing enough blocks, the second validator gets jailed according to x/slashing logic +// 5. The chain continues operating because validator 1 has >66% voting power +func (s *ValidatorJailingTestSuite) TestValidatorJailingWithExtraDelegation() { + chainA := s.configurer.GetChainConfig(0) + chainA.WaitUntilHeight(1) + + // Get both validator nodes + validatorNode1, err := chainA.GetNodeAtIndex(0) + s.NoError(err) + validatorNode2, err := chainA.GetNodeAtIndex(1) + s.NoError(err) + + nonValidatorNode, err := chainA.GetNodeAtIndex(2) + s.NoError(err) + + // Get consensus pubkeys from each validator node to map them to on-chain validators + s.T().Log("Mapping validator nodes to on-chain validators...") + node0ConsPubKey := validatorNode1.ValidatorConsPubKey() + node1ConsPubKey := validatorNode2.ValidatorConsPubKey() + s.T().Logf("Node 0 consensus pubkey: %s", node0ConsPubKey) + s.T().Logf("Node 1 consensus pubkey: %s", node1ConsPubKey) + + // Query validators from the chain + validators, err := nonValidatorNode.QueryValidators() + s.NoError(err) + s.Require().Len(validators, 2, "Need exactly 2 validators") + s.T().Logf("Initial validator count: %d", len(validators)) + + // Map nodes to validators by matching consensus pubkey + var val1, val2 *stakingtypes.Validator + var val2ConsAddr string + for i := range validators { + // Unmarshal the consensus pubkey using the codec + var valConsPubKey cryptotypes.PubKey + err := util.Cdc.UnpackAny(validators[i].ConsensusPubkey, &valConsPubKey) + s.Require().NoError(err, "failed to unmarshal consensus pubkey") + + if bytes.Equal(valConsPubKey.Bytes(), node0ConsPubKey.Bytes()) { + val1 = &validators[i] + s.T().Logf("Node 0 maps to validator: %s (tokens: %s)", validators[i].OperatorAddress, validators[i].Tokens.String()) + } + if bytes.Equal(valConsPubKey.Bytes(), node1ConsPubKey.Bytes()) { + val2 = &validators[i] + val2ConsAddr = sdk.ConsAddress(valConsPubKey.Address()).String() + s.T().Logf("Node 1 maps to validator: %s (tokens: %s)", validators[i].OperatorAddress, validators[i].Tokens.String()) + } + } + + s.Require().NotNil(val1, "Could not map node 0 to validator") + s.Require().NotNil(val2, "Could not map node 1 to validator") + s.T().Logf("Validator 2 consensus address: %s", val2ConsAddr) + + s.T().Logf("Will delegate to validator 1: %s", val1.OperatorAddress) + s.T().Logf("Will stop validator 2 node: %s (validator: %s)", validatorNode2.Name, val2.OperatorAddress) + + // Calculate total voting power + totalVotingPower := val1.Tokens.Add(val2.Tokens) + val1Percentage := val1.Tokens.ToLegacyDec().Quo(totalVotingPower.ToLegacyDec()).MulInt64(100) + val2Percentage := val2.Tokens.ToLegacyDec().Quo(totalVotingPower.ToLegacyDec()).MulInt64(100) + + s.T().Logf("Total voting power: %s", totalVotingPower.String()) + s.T().Logf("Validator 1 percentage: %s%%", val1Percentage.String()) + s.T().Logf("Validator 2 percentage: %s%%", val2Percentage.String()) + + // Delegate to validator 1 to give it >66% voting power + s.T().Log("Delegating to validator 1 to achieve >66% voting power...") + + // Calculate how much to delegate to get validator 1 to 70% of total + targetPercentage := sdkmath.LegacyMustNewDecFromStr("0.70") + // If val1 needs 70% of total, then: (val1 + x) / (total + x) = 0.70 + // Solving for x: x = (0.70 * total - val1) / 0.30 + targetTotalTokens := totalVotingPower.ToLegacyDec().Mul(targetPercentage) + currentVal1Tokens := val1.Tokens.ToLegacyDec() + numerator := targetTotalTokens.Sub(currentVal1Tokens) + denominator := sdkmath.LegacyOneDec().Sub(targetPercentage) + additionalDelegation := numerator.Quo(denominator).TruncateInt() + + // Add 10% buffer to ensure we're well above 66% + additionalDelegation = additionalDelegation.MulRaw(11).QuoRaw(10) + + s.T().Logf("Need to delegate %s ubbn to validator 1", additionalDelegation.String()) + + // Delegate from non-validator node to validator 1 + delegationAmount := additionalDelegation.String() + "ubbn" + txHash := nonValidatorNode.Delegate(nonValidatorNode.WalletName, val1.OperatorAddress, delegationAmount, "--gas=500000") + + chainA.WaitForNumHeights(2) + res, _ := nonValidatorNode.QueryTx(txHash) + s.Equal(res.Code, uint32(0), res.RawLog) + + // delegate 1 ubbn to validator 2 as well + txHash = nonValidatorNode.Delegate(nonValidatorNode.WalletName, val2.OperatorAddress, "1ubbn", "--gas=500000") + + chainA.WaitForNumHeights(2) + res, _ = nonValidatorNode.QueryTx(txHash) + s.Equal(res.Code, uint32(0), res.RawLog) + + // Wait for delegation to take effect - need to wait for epoch end in Babylon + s.T().Log("Waiting for epoch to end so delegation takes effect...") + _, err = nonValidatorNode.WaitForNextEpoch() + s.NoError(err) + s.T().Log("Epoch ended, delegation should now be active") + + // Wait a few more blocks for validator set update + chainA.WaitForNumHeights(3) + + // Verify the delegation took effect + validators, err = nonValidatorNode.QueryValidators() + s.NoError(err) + for i := range validators { + if validators[i].OperatorAddress == val1.OperatorAddress { + val1 = &validators[i] + } + if validators[i].OperatorAddress == val2.OperatorAddress { + val2 = &validators[i] + } + } + totalVotingPower = val1.Tokens.Add(val2.Tokens) + val1Percentage = val1.Tokens.ToLegacyDec().Quo(totalVotingPower.ToLegacyDec()).MulInt64(100) + s.T().Logf("After delegation - Validator 1 percentage: %s%%", val1Percentage.String()) + s.T().Logf("After delegation - Validator 1 tokens: %s, operator: %s", val1.Tokens.String(), val1.OperatorAddress) + s.T().Logf("After delegation - Validator 2 tokens: %s, operator: %s", val2.Tokens.String(), val2.OperatorAddress) + s.Require().True(val1Percentage.GT(sdkmath.LegacyMustNewDecFromStr("66")), + "Validator 1 should have >66%% voting power after delegation") + + // Query initial signing info for validator 2 + val2SigningInfo, err := nonValidatorNode.QuerySigningInfo(val2ConsAddr) + s.NoError(err) + s.T().Logf("Initial signing info for validator 2:") + s.T().Logf(" - Missed blocks counter: %d", val2SigningInfo.MissedBlocksCounter) + s.T().Logf(" - Jailed until: %s", val2SigningInfo.JailedUntil) + + // Get both validators' delegators and their delegation amounts + s.T().Log("Checking co-staking trackers before jailing validator 2...") + + val1Delegators := s.getValidatorDelegators(nonValidatorNode, val1.OperatorAddress) + s.Require().NotEmpty(val1Delegators, "Validator 1 should have at least one delegator") + + val2Delegators := s.getValidatorDelegators(nonValidatorNode, val2.OperatorAddress) + s.Require().NotEmpty(val2Delegators, "Validator 2 should have at least one delegator") + + // Get all unique delegators from both validators + allDelegators := make(map[string]bool) + for d := range val1Delegators { + allDelegators[d] = true + } + for d := range val2Delegators { + allDelegators[d] = true + } + + var delegatorsList []string + for d := range allDelegators { + delegatorsList = append(delegatorsList, d) + } + + // Query co-staking trackers before jailing + s.T().Log("Co-staking trackers BEFORE jailing:") + trackersBefore := s.getCostakingTrackers(nonValidatorNode, delegatorsList) + + // Verify that all delegators have the expected active baby before jailing + for delegator, activeBaby := range trackersBefore { + // Calculate expected active baby (sum of delegations to both validators) + expectedBaby := sdkmath.ZeroInt() + if amt, ok := val1Delegators[delegator]; ok { + expectedBaby = expectedBaby.Add(amt) + } + if amt, ok := val2Delegators[delegator]; ok { + expectedBaby = expectedBaby.Add(amt) + } + + s.T().Logf("Delegator %s expected active baby: %s", delegator, expectedBaby.String()) + s.Require().Equal(expectedBaby, activeBaby, + "Delegator %s should have active baby equal to total delegations before jailing", delegator) + } + + // Query slashing parameters to know how many blocks need to be missed + slashingParams, err := nonValidatorNode.QuerySlashingParams() + s.NoError(err) + s.T().Logf("Slashing parameters:") + s.T().Logf(" - Signed blocks window: %d", slashingParams.SignedBlocksWindow) + s.T().Logf(" - Min signed per window: %s", slashingParams.MinSignedPerWindow.String()) + s.T().Logf(" - Downtime jail duration: %s", slashingParams.DowntimeJailDuration) + + // Calculate how many blocks need to be missed + minSignedPerWindow := slashingParams.MinSignedPerWindow + signedBlocksWindow := slashingParams.SignedBlocksWindow + maxMissedBlocks := signedBlocksWindow - minSignedPerWindow.MulInt64(signedBlocksWindow).TruncateInt64() + s.T().Logf("Max blocks that can be missed before jailing: %d", maxMissedBlocks) + s.T().Logf("Need to miss at least %d blocks to trigger jailing", maxMissedBlocks+1) + + // Get current height before stopping validator + currentHeight, err := nonValidatorNode.QueryCurrentHeight() + s.NoError(err) + s.T().Logf("Current height before stopping validator 2: %d", currentHeight) + + // Stop validator 2 node to make it miss blocks + // Note: We're stopping the node at index 1, which should correspond to one of the validators + s.T().Logf("Stopping validator 2 node (%s) to simulate downtime...", validatorNode2.Name) + err = validatorNode2.Stop() + s.NoError(err) + s.T().Logf("Validator 2 node stopped successfully") + + // Wait for enough blocks to be produced for validator 2 to be jailed + // We need to wait for signed_blocks_window + buffer blocks to ensure jailing + blocksToWait := int64(math.Ceil(float64(signedBlocksWindow)*1.2)) + 5 + s.T().Logf("Waiting for %d blocks to ensure validator 2 gets jailed...", blocksToWait) + + // Wait by polling from non-validator node only (since validator 2 is stopped) + targetHeight := currentHeight + blocksToWait + s.waitForHeight(nonValidatorNode, targetHeight) + + // Query signing info again to see missed blocks + afterStopSigningInfo, err := nonValidatorNode.QuerySigningInfo(val2ConsAddr) + s.NoError(err) + s.T().Logf("Signing info after stopping validator 2:") + s.T().Logf(" - Missed blocks counter: %d", afterStopSigningInfo.MissedBlocksCounter) + s.T().Logf(" - Index offset: %d", afterStopSigningInfo.IndexOffset) + + // Verify validator 2 is jailed + s.T().Log("Verifying validator 2 is jailed...") + validators, err = nonValidatorNode.QueryValidators() + s.NoError(err) + + var jailedVal *stakingtypes.Validator + for i := range validators { + if validators[i].OperatorAddress == val2.OperatorAddress { + jailedVal = &validators[i] + break + } + } + + s.Require().NotNil(jailedVal, "Validator 2 should still be in validator set") + s.Require().True(jailedVal.Jailed, "Validator 2 should be jailed after missing blocks") + s.T().Logf("✓ Validator 2 successfully jailed!") + s.T().Logf(" - Jailed status: %v", jailedVal.Jailed) + s.T().Logf(" - Validator status: %s", jailedVal.Status) + + // Verify the signing info shows jailing + finalSigningInfo, err := nonValidatorNode.QuerySigningInfo(val2ConsAddr) + s.NoError(err) + s.T().Logf("Final signing info:") + s.T().Logf(" - Missed blocks counter: %d", finalSigningInfo.MissedBlocksCounter) + s.T().Logf(" - Jailed until: %s", finalSigningInfo.JailedUntil) + s.Require().True(finalSigningInfo.JailedUntil.After(time.Now()), + "JailedUntil timestamp should be in the future") + + // Need to wait for epoch to end so that co-staking tracker updates + s.T().Log("Waiting for epoch to end so co-staking trackers update after jailing...") + _, err = nonValidatorNode.WaitForNextEpoch() + s.NoError(err) + s.T().Log("Epoch ended, co-staking trackers should now be updated") + + // Check co-staking trackers after jailing + s.T().Log("Checking co-staking trackers AFTER jailing validator 2...") + trackersAfter := s.getCostakingTrackers(nonValidatorNode, delegatorsList) + + // Verify the co-staking tracker changes based on delegation pattern + for delegator, activeBabyAfter := range trackersAfter { + activeBabyBefore := trackersBefore[delegator] + val1Amount, hasVal1Delegation := val1Delegators[delegator] + val2Amount, hasVal2Delegation := val2Delegators[delegator] + + // Calculate expected active baby after jailing (only val1 delegations should remain active) + expectedBabyAfter := sdkmath.ZeroInt() + if hasVal1Delegation { + expectedBabyAfter = expectedBabyAfter.Add(val1Amount) + } + + s.T().Logf("Delegator %s:", delegator) + s.T().Logf(" - Val1 delegation: %s (active: %v)", val1Amount.String(), hasVal1Delegation) + s.T().Logf(" - Val2 delegation: %s (active: %v, now jailed)", val2Amount.String(), hasVal2Delegation) + s.T().Logf(" - Active baby before jailing: %s", activeBabyBefore.String()) + s.T().Logf(" - Active baby after jailing: %s", activeBabyAfter.String()) + s.T().Logf(" - Expected active baby after: %s", expectedBabyAfter.String()) + + // Verify exact expected amount + s.Require().Equal(expectedBabyAfter, activeBabyAfter, + "Delegator %s active baby after jailing should equal only val1 delegation amount", delegator) + + // Additional specific checks based on delegation pattern + if hasVal2Delegation && !hasVal1Delegation { + s.T().Logf(" ✓ Delegator only to val2: active baby correctly decreased to zero") + } else if hasVal1Delegation && !hasVal2Delegation { + s.T().Logf(" ✓ Delegator only to val1: active baby correctly remained unchanged") + } else if hasVal1Delegation && hasVal2Delegation { + s.T().Logf(" ✓ Delegator to both: active baby correctly decreased by val2 amount") + } + } + + // Verify chain is still producing blocks (validator 1 has >66% so consensus continues) + finalHeight, err := nonValidatorNode.QueryCurrentHeight() + s.NoError(err) + s.T().Logf("Final height: %d", finalHeight) + s.Require().Greater(finalHeight, currentHeight+blocksToWait-5, + "Chain should continue producing blocks with validator 1's >66%% voting power") + + s.T().Log("✓ Test completed successfully!") + s.T().Log("Summary:") + s.T().Logf(" - Validator 1 had >66%% voting power and continued producing blocks") + s.T().Logf(" - Validator 2 stopped signing and missed %d blocks", afterStopSigningInfo.MissedBlocksCounter) + s.T().Logf(" - Validator 2 was automatically jailed by the slashing module") + s.T().Logf(" - Co-staking trackers verified for %d unique delegators", len(delegatorsList)) + s.T().Logf(" - Delegators only to val2 (jailed): active baby decreased to zero") + s.T().Logf(" - Delegators only to val1 (not jailed): active baby remained unchanged") + s.T().Logf(" - Delegators to both validators: active baby decreased proportionally") + s.T().Logf(" - Chain continued operating with single validator (>66%% threshold)") +} + +// waitForHeight waits for a specific node to reach a target height +func (s *ValidatorJailingTestSuite) waitForHeight(node *chain.NodeConfig, targetHeight int64) { + maxAttempts := 500 + for i := 0; i < maxAttempts; i++ { + currentHeight, err := node.QueryCurrentHeight() + if err == nil && currentHeight >= targetHeight { + s.T().Logf("Reached target height %d (current: %d)", targetHeight, currentHeight) + return + } + if i%10 == 0 { + s.T().Logf("Waiting for height %d, current: %d (attempt %d/%d)", targetHeight, currentHeight, i+1, maxAttempts) + } + time.Sleep(2 * time.Second) + } + s.FailNow("Timeout waiting for height %d", targetHeight) +} + +// getCostakingTrackers returns a map of delegator addresses to their co-staking tracker ActiveBaby amounts +// Skips delegators that don't have a co-staking tracker (e.g., validators self-delegating) +func (s *ValidatorJailingTestSuite) getCostakingTrackers( + node *chain.NodeConfig, + delegatorAddresses []string, +) map[string]sdkmath.Int { + trackers := make(map[string]sdkmath.Int) + + for _, delegator := range delegatorAddresses { + s.T().Logf("Querying co-staking tracker for delegator: %s", delegator) + tracker, err := node.QueryCostakerRewardsTracker(delegator) + if err != nil { + // Co-staking tracker might not exist for some delegators (e.g., validators self-delegating) + s.T().Logf(" - Co-staking tracker not found (skipping): %v", err) + continue + } + trackers[delegator] = tracker.ActiveBaby + s.T().Logf(" - Active Baby: %s", tracker.ActiveBaby.String()) + s.T().Logf(" - Active Satoshis: %s", tracker.ActiveSatoshis.String()) + s.T().Logf(" - Total Score: %s", tracker.TotalScore.String()) + } + + return trackers +} + +// getValidatorDelegators returns a map of delegator addresses to their delegation amounts for a validator +func (s *ValidatorJailingTestSuite) getValidatorDelegators( + node *chain.NodeConfig, + validatorAddr string, +) map[string]sdkmath.Int { + allDelegations, err := node.QueryValidatorDelegations(validatorAddr) + s.NoError(err) + + delegators := make(map[string]sdkmath.Int) + for _, delegation := range allDelegations { + if delegation.Delegation.ValidatorAddress == validatorAddr { + delegators[delegation.Delegation.DelegatorAddress] = delegation.Balance.Amount + } + } + + s.T().Logf("Validator %s has %d delegators", validatorAddr, len(delegators)) + return delegators +} diff --git a/test/e2ev2/tmanager/genesis.go b/test/e2ev2/tmanager/genesis.go index b559452ee..13e8262d4 100644 --- a/test/e2ev2/tmanager/genesis.go +++ b/test/e2ev2/tmanager/genesis.go @@ -309,4 +309,3 @@ func UpdateGenesisTokenFactory(tokenfactoryGenState *tokenfactorytypes.GenesisSt DenomCreationFee: sdk.NewCoins(sdk.NewCoin(appparams.DefaultBondDenom, sdkmath.NewInt(10000))), } } - diff --git a/test/replay/costaking_test.go b/test/replay/costaking_test.go index 8b5478f81..c56ca42d2 100644 --- a/test/replay/costaking_test.go +++ b/test/replay/costaking_test.go @@ -2,6 +2,7 @@ package replay import ( "bytes" + "context" "encoding/hex" "math/rand" "testing" @@ -9,14 +10,19 @@ import ( "cosmossdk.io/math" sdkmath "cosmossdk.io/math" + "github.com/babylonlabs-io/babylon/v4/test/e2e/util" bbn "github.com/babylonlabs-io/babylon/v4/types" + costkkeeper "github.com/babylonlabs-io/babylon/v4/x/costaking/keeper" costktypes "github.com/babylonlabs-io/babylon/v4/x/costaking/types" + epochingtypes "github.com/babylonlabs-io/babylon/v4/x/epoching/types" ictvtypes "github.com/babylonlabs-io/babylon/v4/x/incentive/types" "github.com/btcsuite/btcd/wire" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" distkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" disttypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + stktypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/stretchr/testify/require" ) @@ -1036,3 +1042,360 @@ func TestCostakingBabyBondUnbondAllBondAgain(t *testing.T) { d.CheckCostakerRewards(del1.Address(), del1BabyDelegatedAmtAgain, zero, zero, rwd.Period) // period doesn't change as the delegator has zero score } + +// TestBabyCoStaking creates 2 validators and jails one +// Performs delegations to the jailed validator and makes corresponding checks +func TestBabyCoStaking(t *testing.T) { + t.Parallel() + r := rand.New(rand.NewSource(time.Now().UnixNano())) + d := NewBabylonAppDriverTmpDir(r, t) + d.GenerateNewBlockAssertExecutionSuccess() + + stkK, costkK, slashK, epochK := d.App.StakingKeeper, d.App.CostakingKeeper, d.App.SlashingKeeper, d.App.EpochingKeeper + + stkParams, err := stkK.GetParams(d.Ctx()) + require.NoError(d.t, err) + maxVals := 3 + stkParams.MaxValidators = uint32(maxVals) + + err = stkK.SetParams(d.Ctx(), stkParams) + require.NoError(d.t, err) + + d.GenerateNewBlockAssertExecutionSuccess() + stkParams, err = stkK.GetParams(d.Ctx()) + require.NoError(d.t, err) + require.Equal(d.t, maxVals, int(stkParams.MaxValidators)) + + // Get all validators to check their commissions + validators, err := stkK.GetAllValidators(d.Ctx()) + require.NoError(d.t, err) + require.Len(d.t, validators, 1, "There should be exactly one validator in the test setup") + // val := validators[0] + + delegators := d.CreateNStakerAccounts(8) + val2Oper := delegators[0] + val3Oper := delegators[5] + val4Oper := delegators[6] + val5Oper := delegators[7] + + d.MintNativeTo(val2Oper.Address(), 1000_000000) + + // Create a new validator + newValSelfDelegatedAmt := sdkmath.NewInt(10_000000) + d.TxCreateValidator(val2Oper.SenderInfo, newValSelfDelegatedAmt) + + otherValSelfDelegatedAmt := sdkmath.NewInt(1_000000) + for i, del := range delegators[5:] { + d.MintNativeTo(del.Address(), 1000_000000) + d.TxCreateValidator(del.SenderInfo, otherValSelfDelegatedAmt.AddRaw(int64(i)*1_000000)) + } + + d.GenerateNewBlockAssertExecutionSuccess() + d.ProgressTillFirstBlockTheNextEpoch() + + // Check if new validator is in the list + validators, err = stkK.GetAllValidators(d.Ctx()) + require.NoError(d.t, err) + require.Len(d.t, validators, 5, "There should be exactly five validators in the test setup") + var val2, val4, val5 stktypes.Validator + var val2ConsPubKey cryptotypes.PubKey + for _, v := range validators { + valAddrBz := sdk.MustValAddressFromBech32(v.OperatorAddress) + if bytes.Equal(valAddrBz.Bytes(), val2Oper.Address().Bytes()) { + val2 = v + err := util.Cdc.UnpackAny(v.ConsensusPubkey, &val2ConsPubKey) + require.NoError(t, err) + } + if bytes.Equal(valAddrBz.Bytes(), val4Oper.Address().Bytes()) { + val4 = v + } + if bytes.Equal(valAddrBz.Bytes(), val5Oper.Address().Bytes()) { + val5 = v + } + } + require.Equal(t, val2.Status, stktypes.Bonded, "New validator should be in Bonded status") + + epoch := epochK.GetEpoch(d.Ctx()) + valset := epochK.GetValidatorSet(d.Ctx(), epoch.EpochNumber) + require.Len(t, valset, maxVals) + require.True(t, isValidatorIncluded(valset, sdk.MustValAddressFromBech32(val2.OperatorAddress)), "New validator should be in the validator set") + require.True(t, isValidatorIncluded(valset, sdk.MustValAddressFromBech32(val5.OperatorAddress)), "New validator should be in the validator set") + + // new validators should have a costaker tracker created with the self delegation + val2Tracker, err := costkK.GetCostakerRewards(d.Ctx(), val2Oper.Address()) + require.NoError(t, err) + require.NotNil(t, val2Tracker) + require.Equal(t, val2Tracker.ActiveBaby, newValSelfDelegatedAmt, "active baby should be self delegation amount", val2Tracker.ActiveBaby.String()) + require.True(t, val2Tracker.ActiveSatoshis.IsZero(), "Active sats should be zero") + require.True(t, val2Tracker.TotalScore.IsZero()) + + val5Tracker, err := costkK.GetCostakerRewards(d.Ctx(), val5Oper.Address()) + require.NoError(t, err) + require.NotNil(t, val5Tracker) + require.Equal(t, val5Tracker.ActiveBaby, otherValSelfDelegatedAmt.AddRaw(2*1_000000), "active baby should be self delegation amount", val5Tracker.ActiveBaby.String()) + require.True(t, val5Tracker.ActiveSatoshis.IsZero(), "Active sats should be zero") + require.True(t, val5Tracker.TotalScore.IsZero()) + + // Others validators outside the active set should not have a costaker tracker created + _, err = costkK.GetCostakerRewards(d.Ctx(), val3Oper.Address()) + require.ErrorContains(t, err, "not found") + _, err = costkK.GetCostakerRewards(d.Ctx(), val4Oper.Address()) + require.ErrorContains(t, err, "not found") + + // delegate to new validator (val2) + del3 := delegators[2] + del3BabyDelegatedAmtBeforeJailing := sdkmath.NewInt(1_000000) + d.TxWrappedDelegate(del3.SenderInfo, val2.OperatorAddress, del3BabyDelegatedAmtBeforeJailing) + + del4 := delegators[3] + del4BabyDelegatedAmt := sdkmath.NewInt(2_000000) + d.TxWrappedDelegate(del4.SenderInfo, val2.OperatorAddress, del4BabyDelegatedAmt) + + del5 := delegators[4] + del5BabyDelegatedAmt := sdkmath.NewInt(1_000000) + d.TxWrappedDelegate(del5.SenderInfo, val2.OperatorAddress, del5BabyDelegatedAmt) + + // partial undelegate from val5 (currently in active set) to make it inactive + // val4 should take its place in the active set + partialUnbondVal5 := sdkmath.NewInt(3 * 1_000000) + d.TxWrappedUndelegate(val5Oper.SenderInfo, val5.OperatorAddress, partialUnbondVal5) + + // make a delegation to val4 (currently not in active set but will become active after val5 partial unbonding) + val4DelAmt := sdkmath.NewInt(1_000000) + d.TxWrappedDelegate(val4Oper.SenderInfo, val4.OperatorAddress, val4DelAmt) + + d.GenerateNewBlockAssertExecutionSuccess() + d.ProgressTillFirstBlockTheNextEpoch() + + // check costaking trackers are created accordingly + del3Tracker, err := costkK.GetCostakerRewards(d.Ctx(), del3.Address()) + require.NoError(t, err) + require.NotNil(t, del3Tracker) + require.Equal(t, del3Tracker.ActiveBaby, del3BabyDelegatedAmtBeforeJailing, "active baby should be self delegation amount", del3Tracker.ActiveBaby.String()) + require.True(t, del3Tracker.ActiveSatoshis.IsZero()) + require.True(t, del3Tracker.TotalScore.IsZero()) + + del4Tracker, err := costkK.GetCostakerRewards(d.Ctx(), del4.Address()) + require.NoError(t, err) + require.NotNil(t, del4Tracker) + require.Equal(t, del4Tracker.ActiveBaby, del4BabyDelegatedAmt, "active baby should be self delegation amount", del4Tracker.ActiveBaby.String()) + require.True(t, del4Tracker.ActiveSatoshis.IsZero()) + require.True(t, del4Tracker.TotalScore.IsZero()) + + del5Tracker, err := costkK.GetCostakerRewards(d.Ctx(), del5.Address()) + require.NoError(t, err) + require.NotNil(t, del5Tracker) + require.Equal(t, del5Tracker.ActiveBaby, del5BabyDelegatedAmt, "active baby should be self delegation amount", del5Tracker.ActiveBaby.String()) + require.True(t, del5Tracker.ActiveSatoshis.IsZero()) + require.True(t, del5Tracker.TotalScore.IsZero()) + + // Check that val5 dropped from the active set and val4 entered + epoch = epochK.GetEpoch(d.Ctx()) + valset = epochK.GetValidatorSet(d.Ctx(), epoch.EpochNumber) + require.Len(t, valset, maxVals) + require.True(t, isValidatorIncluded(valset, sdk.MustValAddressFromBech32(val2.OperatorAddress)), "Validator 2 should be in the validator set") + require.True(t, isValidatorIncluded(valset, sdk.MustValAddressFromBech32(val4.OperatorAddress)), "Validator 4 should be in the validator set") + require.False(t, isValidatorIncluded(valset, sdk.MustValAddressFromBech32(val5.OperatorAddress)), "Validator 5 should not be in the validator set") + + // Check that val5 co-staker tracker is zeroed + assertZeroCostkTracker(d.t, d.Ctx(), costkK, val5Oper.Address()) + + // Check that val4 co-staker tracker is created with self delegation amount + val4Tracker, err := costkK.GetCostakerRewards(d.Ctx(), val4Oper.Address()) + require.NoError(t, err) + require.NotNil(t, val4Tracker) + require.Equal(t, val4Tracker.ActiveBaby, otherValSelfDelegatedAmt.AddRaw(1_000000).Add(val4DelAmt), "active baby should be self delegation amount", val4Tracker.ActiveBaby.String()) + require.True(t, val4Tracker.ActiveSatoshis.IsZero()) + require.True(t, val4Tracker.TotalScore.IsZero()) + + // Produce new blocks till new validator gets jailed for missing blocks + val2ValAddr := sdk.MustValAddressFromBech32(val2.OperatorAddress) + jailedHeight := int64(0) // validator is jailed at height 111 + for jailedHeight == 0 { + d.GenerateNewBlockAssertExecutionSuccess() + height := d.Ctx().BlockHeight() + val, err := stkK.GetValidator(d.Ctx(), val2ValAddr) + require.NoError(t, err) + if val.Jailed { + jailedHeight = height + epoch := epochK.GetEpoch(d.Ctx()) + valset := epochK.GetValidatorSet(d.Ctx(), epoch.EpochNumber) + // check that jailed validator is still on epoch validator set + require.Len(t, valset, maxVals, "Jailed validator should still be in the validator set") + require.True(t, isValidatorIncluded(valset, val2ValAddr), "Jailed validator should not be in the validator set") + } + } + + // ================================================= + // OPERATIONS ON SAME EPOCH THAT VALIDATOR IS JAILED + // ================================================= + val2, err = stkK.GetValidator(d.Ctx(), val2ValAddr) + require.NoError(d.t, err) + + // Make a NEW delegation to validator + del2 := delegators[1] + del2BabyDelegatedAmt := sdkmath.NewInt(1000000) + d.TxWrappedDelegate(del2.SenderInfo, val2.OperatorAddress, del2BabyDelegatedAmt) + + // Extend the existing del3 delegation + del3BabyDelegatedAmtAfterJailing := sdkmath.NewInt(500000) + d.TxWrappedDelegate(del3.SenderInfo, val2.OperatorAddress, del3BabyDelegatedAmtAfterJailing) + + // The first delegation of del3 was slashed. Get the new delegation amount + del3Delegation, err := stkK.GetDelegation(d.Ctx(), del3.Address(), val2ValAddr) + require.NoError(d.t, err) + del3FirstDelAmtAfterSlashing := val2.TokensFromShares(del3Delegation.Shares).TruncateInt() + + // Totally unbond a delegation + // Tokens are already slashed, so for total unbonding need to get the new tokens per shares + del4Delegation, err := stkK.GetDelegation(d.Ctx(), del4.Address(), val2ValAddr) + require.NoError(d.t, err) + del4TotalUnbondAmt := val2.TokensFromShares(del4Delegation.Shares).TruncateInt() + d.TxWrappedUndelegate(del4.SenderInfo, val2.OperatorAddress, del4TotalUnbondAmt) + + // get updated delegation amount for del5 after slashing + del5Delegation, err := stkK.GetDelegation(d.Ctx(), del5.Address(), val2ValAddr) + require.NoError(d.t, err) + del5TotalAmtAfterSlashing := val2.TokensFromShares(del5Delegation.Shares).TruncateInt() + + // Partially unbond a delegation with many msgs and re-delegate + del5BabyUnstakeAmt := sdkmath.NewInt(7) + d.TxWrappedUndelegate(del5.SenderInfo, val2.OperatorAddress, del5BabyUnstakeAmt) + d.TxWrappedUndelegate(del5.SenderInfo, val2.OperatorAddress, del5BabyUnstakeAmt) + d.TxWrappedDelegate(del5.SenderInfo, val2.OperatorAddress, del5BabyUnstakeAmt) + + d.GenerateNewBlockAssertExecutionSuccess() + // progress to next epoch to ensure delegation and jailing are processed + d.ProgressTillFirstBlockTheNextEpoch() + + // check active set stored in epoching module removed the jailed validator + epoch = epochK.GetEpoch(d.Ctx()) + valset = epochK.GetValidatorSet(d.Ctx(), epoch.EpochNumber) + // check that jailed validator is still on epoch validator set + require.Len(t, valset, maxVals, "Jailed validator should not be in the validator set") + require.False(t, isValidatorIncluded(valset, val2ValAddr), "Jailed validator should not be in the validator set") + + // check delegation was created + del, err := stkK.GetDelegation(d.Ctx(), del2.Address(), val2ValAddr) + require.NoError(t, err) + require.Equal(t, del.DelegatorAddress, del2.Address().String()) + + // Check costaker trackers are correct + // del2 created a delegation at same epoch that the validator got jailed, so the tracker was not even created (skipped due to jailing) + _, err = costkK.GetCostakerRewards(d.Ctx(), del2.Address()) + require.Error(t, err) + require.ErrorContains(t, err, "not found") + + // Trackers for val2, del3, del4 y del5 should be zeroed + for _, addr := range []sdk.AccAddress{val2Oper.Address(), del3.Address(), del4.Address(), del5.Address()} { + assertZeroCostkTracker(d.t, d.Ctx(), costkK, addr) + } + + // ================================================= + // OPERATIONS AFTER VALIDATOR IS JAILED + // ================================================= + + // New delegation to already jailed validator (should continue as zero active baby) + del3DelegatedAmtAfterJailing := sdkmath.NewInt(100000) + d.TxWrappedDelegate(del3.SenderInfo, val2.OperatorAddress, del3DelegatedAmtAfterJailing) + + d.GenerateNewBlockAssertExecutionSuccess() + d.ProgressTillFirstBlockTheNextEpoch() + + epoch = epochK.GetEpoch(d.Ctx()) + valset = epochK.GetValidatorSet(d.Ctx(), epoch.EpochNumber) + require.Len(t, valset, 2, "jailed validator should not be in the validator set") + require.False(t, isValidatorIncluded(valset, val2ValAddr), "jailed validator should not be in the validator set") + + // check costk tracker is still zero + assertZeroCostkTracker(d.t, d.Ctx(), costkK, del3.Address()) + + // Unjail the jail validator + // make sure block time is after the jail timeout + var valConsPubKey cryptotypes.PubKey + err = util.Cdc.UnpackAny(val2.ConsensusPubkey, &valConsPubKey) + require.NoError(d.t, err) + // check unjailing time + val2ConsAddr := sdk.ConsAddress(valConsPubKey.Address()) + info, err := slashK.GetValidatorSigningInfo(d.Ctx(), val2ConsAddr) + require.NoError(d.t, err) + currBlckTime := d.Ctx().BlockTime() + timeToUnjail := info.JailedUntil.Sub(currBlckTime) + require.True(d.t, timeToUnjail > 0) + + // produce blocks till after unjail time + for currBlckTime.Before(info.JailedUntil.Add(1 * time.Second)) { + currBlckTime = d.Ctx().BlockTime() + d.GenerateNewBlockAssertExecutionSuccess() + } + + d.TxUnjailValidator(val2Oper.SenderInfo, val2.OperatorAddress) + d.GenerateNewBlockAssertExecutionSuccess() + + // Wait for an epoch + d.ProgressTillFirstBlockTheNextEpoch() + // check unjailed validator is back in active set + epoch = epochK.GetEpoch(d.Ctx()) + valset = epochK.GetValidatorSet(d.Ctx(), epoch.EpochNumber) + require.Len(t, valset, 2, "Unjailed validator should be in the validator set") + require.True(t, isValidatorIncluded(valset, val2ValAddr), "Unjailed validator should be in the validator set") + + // Check the active baby is properly set back for delegations to this validator + // NOTE: Consider that the ones that were slashed will be less than the original staking amount + + // val2 self delegation was slashed + selfDel, err := stkK.GetDelegation(d.Ctx(), val2Oper.Address(), val2ValAddr) + require.NoError(t, err) + expSelfDelAmt := val2.TokensFromShares(selfDel.Shares).TruncateInt() + require.True(t, expSelfDelAmt.LT(newValSelfDelegatedAmt), "self delegation should be less than original amount due to slashing", expSelfDelAmt.String()) + // active baby should be less than self delegation amount due to slashing + val2Tracker, err = costkK.GetCostakerRewards(d.Ctx(), val2Oper.Address()) + require.NoError(t, err) + require.NotNil(t, val2Tracker) + require.Equal(t, val2Tracker.ActiveBaby, expSelfDelAmt, "active baby should be less than self delegation amount after slashing", val2Tracker.ActiveBaby.String()) + require.True(t, val2Tracker.ActiveSatoshis.IsZero(), "Active sats should be zero") + require.True(t, val2Tracker.TotalScore.IsZero(), "Active score should be zero as validator was jailed entire epoch") + + del3Tracker, err = costkK.GetCostakerRewards(d.Ctx(), del3.Address()) + require.NoError(t, err) + require.NotNil(t, del3Tracker) + + expectedDel3ActiveBaby := del3FirstDelAmtAfterSlashing.Add(del3BabyDelegatedAmtAfterJailing).Add(del3DelegatedAmtAfterJailing) + require.Equal(t, del3Tracker.ActiveBaby, expectedDel3ActiveBaby, "active baby should be less than total delegation amount after slashing", del3Tracker.ActiveBaby.String()) + require.True(t, del3Tracker.ActiveSatoshis.IsZero(), "Active sats should be zero") + require.True(t, del3Tracker.TotalScore.IsZero(), "Active score should be zero as validator was jailed entire epoch") + + // del4 fully unbonded so tracker should still be zero + assertZeroCostkTracker(d.t, d.Ctx(), costkK, del4.Address()) + + // del5 got slashed first and then partially unbonded with 2 msgs + del5Tracker, err = costkK.GetCostakerRewards(d.Ctx(), del5.Address()) + require.NoError(t, err) + require.NotNil(t, del5Tracker) + // expected active baby is total delegation after slashing minus the unstake amount + // There're 2 undelegate msgs of 7 ubbn each, but after the second one, there's a re-delegation for same amount + expectedDel5ActiveBaby := del5TotalAmtAfterSlashing.Sub(del5BabyUnstakeAmt) + require.Equal(t, del5Tracker.ActiveBaby, expectedDel5ActiveBaby, "active baby should be less than total delegation amount after slashing and unstaking", del5Tracker.ActiveBaby.String()) + require.True(t, del5Tracker.ActiveSatoshis.IsZero(), "Active sats should be zero") + require.True(t, del5Tracker.TotalScore.IsZero(), "Active score should be zero as validator was jailed entire epoch") +} + +func assertZeroCostkTracker(t *testing.T, ctx context.Context, costkK costkkeeper.Keeper, addr sdk.AccAddress) { + trk, err := costkK.GetCostakerRewards(ctx, addr) + require.NoError(t, err) + require.NotNil(t, trk) + require.True(t, trk.ActiveBaby.IsZero(), "active baby should be zero", trk.ActiveBaby.String()) + require.True(t, trk.ActiveSatoshis.IsZero(), "Active sats should be zero", trk.ActiveSatoshis.String()) + require.True(t, trk.TotalScore.IsZero(), "Active score should be zero", trk.TotalScore.String()) +} + +func isValidatorIncluded(valset []epochingtypes.Validator, valAddr sdk.ValAddress) bool { + found := false + for _, v := range valset { + if bytes.Equal(v.GetValAddress().Bytes(), valAddr.Bytes()) { + found = true + break + } + } + return found +} diff --git a/test/replay/driver.go b/test/replay/driver.go index e598e9920..c1c890818 100644 --- a/test/replay/driver.go +++ b/test/replay/driver.go @@ -545,13 +545,30 @@ func (d *BabylonAppDriver) GenerateNewBlock() *abci.ResponseFinalizeBlock { } d.CurrentTime = newTime + // Get current validator set to build proper commit + validators := lastState.Validators + extendedSignatures := make([]cmttypes.ExtendedCommitSig, len(validators.Validators)) + + // Build commit signatures for each validator + for i, val := range validators.Validators { + if bytes.Equal(val.Address.Bytes(), d.CometAddress) { + // This is our signing validator + extendedSignatures[i] = extCommitSig + } else { + // Other validators are absent (not voting) + extendedSignatures[i] = cmttypes.ExtendedCommitSig{ + CommitSig: cmttypes.CommitSig{ + BlockIDFlag: cmttypes.BlockIDFlagAbsent, + }, + } + } + } + oneValExtendedCommit := &cmttypes.ExtendedCommit{ - Height: block1.Height, - Round: 0, - BlockID: block1ID, - ExtendedSignatures: []cmttypes.ExtendedCommitSig{ - extCommitSig, - }, + Height: block1.Height, + Round: 0, + BlockID: block1ID, + ExtendedSignatures: extendedSignatures, } accepted, err := d.BlockExec.ProcessProposal(block1, lastState) diff --git a/test/replay/slashing.go b/test/replay/slashing.go new file mode 100644 index 000000000..8ee813f9e --- /dev/null +++ b/test/replay/slashing.go @@ -0,0 +1,11 @@ +package replay + +import ( + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" +) + +func (d *BabylonAppDriver) TxUnjailValidator(operator *SenderInfo, valAddr string) { + msgUnjail := slashingtypes.NewMsgUnjail(valAddr) + d.SendTxWithMessagesSuccess(d.t, operator, DefaultGasLimit, defaultFeeCoin, msgUnjail) + operator.IncSeq() +} diff --git a/test/replay/staking.go b/test/replay/staking.go index 3d8da458c..3b5b9ad15 100644 --- a/test/replay/staking.go +++ b/test/replay/staking.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" appparams "github.com/babylonlabs-io/babylon/v4/app/params" + "github.com/babylonlabs-io/babylon/v4/testutil/datagen" epochingtypes "github.com/babylonlabs-io/babylon/v4/x/epoching/types" stktypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) @@ -19,6 +20,21 @@ func (d *BabylonAppDriver) TxWrappedDelegate(delegator *SenderInfo, valAddr stri delegator.IncSeq() } +func (d *BabylonAppDriver) TxCreateValidator(operator *SenderInfo, amount sdkmath.Int) { + msgCreateValidator, err := datagen.BuildMsgWrappedCreateValidatorWithAmount(operator.Address(), amount) + if err != nil { + d.t.Fatal(err) + } + msgCreateValidator.MsgCreateValidator.Commission = stktypes.NewCommissionRates( + sdkmath.LegacyMustNewDecFromStr("0.1"), + sdkmath.LegacyMustNewDecFromStr("0.9"), + sdkmath.LegacyMustNewDecFromStr("0.05"), + ) + + d.SendTxWithMessagesSuccess(d.t, operator, DefaultGasLimit, defaultFeeCoin, msgCreateValidator) + operator.IncSeq() +} + func (d *BabylonAppDriver) TxWrappedUndelegate(delegator *SenderInfo, valAddr string, amount sdkmath.Int) { msgUndelegate := stktypes.NewMsgUndelegate( delegator.AddressString(), valAddr, sdk.NewCoin(appparams.DefaultBondDenom, amount), diff --git a/types/sdk_math.go b/types/sdk_math.go index 2f54aeab6..1314b7b3d 100644 --- a/types/sdk_math.go +++ b/types/sdk_math.go @@ -48,4 +48,4 @@ func SafeNewCoin(denom string, amount sdkmath.Int) (sdk.Coin, error) { } return coin, nil -} \ No newline at end of file +} diff --git a/x/btccheckpoint/keeper/hooks.go b/x/btccheckpoint/keeper/hooks.go index 50f587b4d..624979925 100644 --- a/x/btccheckpoint/keeper/hooks.go +++ b/x/btccheckpoint/keeper/hooks.go @@ -34,6 +34,8 @@ func (h Hooks) AfterBTCHeaderInserted(_ context.Context, _ *ltypes.BTCHeaderInfo func (h Hooks) AfterEpochBegins(_ context.Context, _ uint64) {} +func (h Hooks) BeforeEpochEnds(_ context.Context, _ uint64) {} + func (h Hooks) AfterEpochEnds(_ context.Context, _ uint64) {} func (h Hooks) BeforeSlashThreshold(_ context.Context, _ etypes.ValidatorSet) {} diff --git a/x/costaking/keeper/genesis.go b/x/costaking/keeper/genesis.go index 8ade7a4d8..6c0d631d2 100644 --- a/x/costaking/keeper/genesis.go +++ b/x/costaking/keeper/genesis.go @@ -35,6 +35,12 @@ func (k Keeper) InitGenesis(ctx context.Context, gs types.GenesisState) error { } } + if len(gs.ValidatorSet.Validators) > 0 { + if err := k.validatorSet.Set(ctx, gs.ValidatorSet); err != nil { + return err + } + } + return k.SetParams(ctx, gs.Params) } @@ -50,11 +56,19 @@ func (k Keeper) ExportGenesis(ctx context.Context) (*types.GenesisState, error) return nil, err } + valSet, err := k.validatorSet.Get(ctx) + if err != nil { + // If the key is empty, will return an error. Log the error and return empty validator set. + k.Logger(ctx).Error("failed to get validator set from store during export genesis", "error", err) + valSet = types.ValidatorSet{} // return empty validator set on error + } + return &types.GenesisState{ Params: k.GetParams(ctx), CurrentRewards: k.getCurrentRewardsEntry(ctx), HistoricalRewards: historicalRewards, CostakersRewardsTracker: costakersRewardsTracker, + ValidatorSet: valSet, }, nil } diff --git a/x/costaking/keeper/genesis_test.go b/x/costaking/keeper/genesis_test.go index 251eac3ab..677e2a3a8 100644 --- a/x/costaking/keeper/genesis_test.go +++ b/x/costaking/keeper/genesis_test.go @@ -65,6 +65,15 @@ func FuzzInitExportGenesis(f *testing.F) { }, HistoricalRewards: historicalRewards, CostakersRewardsTracker: costakerTrackers, + ValidatorSet: types.ValidatorSet{ + Validators: []*types.Validator{ + { + Addr: datagen.GenRandomAddress().Bytes(), + Tokens: math.NewInt(1000), + Shares: math.LegacyNewDec(1000), + }, + }, + }, } // Validate genesis state @@ -127,6 +136,15 @@ func TestInitGenesisWithCurrentRewards(t *testing.T) { }, HistoricalRewards: []types.HistoricalRewardsEntry{}, CostakersRewardsTracker: []types.CostakerRewardsTrackerEntry{}, + ValidatorSet: types.ValidatorSet{ + Validators: []*types.Validator{ + { + Addr: datagen.GenRandomAddress().Bytes(), + Tokens: math.NewInt(1000), + Shares: math.LegacyNewDec(1000), + }, + }, + }, } err := k.InitGenesis(ctx, genState) @@ -175,6 +193,15 @@ func TestInitGenesisWithHistoricalRewards(t *testing.T) { CurrentRewards: types.CurrentRewardsEntry{}, HistoricalRewards: historicalRewards, CostakersRewardsTracker: []types.CostakerRewardsTrackerEntry{}, + ValidatorSet: types.ValidatorSet{ + Validators: []*types.Validator{ + { + Addr: datagen.GenRandomAddress().Bytes(), + Tokens: math.NewInt(1000), + Shares: math.LegacyNewDec(1000), + }, + }, + }, } err := k.InitGenesis(ctx, genState) @@ -226,6 +253,15 @@ func TestInitGenesisWithCostakerTrackers(t *testing.T) { CurrentRewards: types.CurrentRewardsEntry{}, HistoricalRewards: []types.HistoricalRewardsEntry{}, CostakersRewardsTracker: costakerTrackers, + ValidatorSet: types.ValidatorSet{ + Validators: []*types.Validator{ + { + Addr: datagen.GenRandomAddress().Bytes(), + Tokens: math.NewInt(1000), + Shares: math.LegacyNewDec(1000), + }, + }, + }, } err := k.InitGenesis(ctx, genState) @@ -271,4 +307,5 @@ func TestExportGenesisEmpty(t *testing.T) { require.Equal(t, types.DefaultParams(), exportedGenState.Params) require.Empty(t, exportedGenState.HistoricalRewards) require.Empty(t, exportedGenState.CostakersRewardsTracker) + require.Empty(t, exportedGenState.ValidatorSet) } diff --git a/x/costaking/keeper/hooks_epoching.go b/x/costaking/keeper/hooks_epoching.go new file mode 100644 index 000000000..ec8b1e778 --- /dev/null +++ b/x/costaking/keeper/hooks_epoching.go @@ -0,0 +1,224 @@ +package keeper + +import ( + "context" + "fmt" + "sort" + + "cosmossdk.io/math" + epochingtypes "github.com/babylonlabs-io/babylon/v4/x/epoching/types" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/babylonlabs-io/babylon/v4/x/costaking/types" +) + +var _ epochingtypes.EpochingHooks = HookEpoching{} + +// Wrapper struct +type HookEpoching struct { + k Keeper +} + +// AfterEpochBegins is called after an epoch begins +func (h HookEpoching) AfterEpochBegins(ctx context.Context, epoch uint64) { + // Initialize the validator set for the first epoch if not already done + // For subsequent epochs, the validator set is updated in AfterEpochEnds + _, err := h.k.validatorSet.Get(ctx) + if err != nil { + h.k.Logger(ctx).Info("Initializing validator set for the first epoch. Got error:", err) + // First epoch, initialize validator set + _, valAddrs, err := h.buildNewActiveValSetMap(ctx) + if err != nil { + h.k.Logger(ctx).Error("failed to build initial validator set", "error", err) + return + } + if err := h.k.updateValidatorSet(ctx, valAddrs); err != nil { + h.k.Logger(ctx).Error("failed to store initial validator set", "error", err) + return + } + } +} + +// BeforeEpochEnds is called before an epoch ends, before ApplyAndReturnValidatorSetUpdates +// This populates the cache with the current validator set to ensure we have the correct +// validator tokens before processing any delegations/undelegations in AfterEpochEnds +func (h HookEpoching) BeforeEpochEnds(ctx context.Context, epoch uint64) { + // Populate the cache with the current active validator set + // This ensures that when AfterEpochEnds runs, we have the correct previous validator set + _, err := h.k.stkCache.GetActiveValidatorSet(ctx, h.k.buildCurrEpochValSetMap) + if err != nil { + h.k.Logger(ctx).Error("failed to populate validator set cache in BeforeEpochEnds", "error", err) + } +} + +// AfterEpochEnds is called after an epoch ends +// It handles the transition of validators between active and inactive states: +// - Newly active validators: add their delegators' baby tokens to ActiveBaby +// - Newly inactive validators: remove their delegators' baby tokens from ActiveBaby +func (h HookEpoching) AfterEpochEnds(ctx context.Context, epoch uint64) { + // Get the validator set from the ending epoch (cached in stkCache) + prevValMap, err := h.k.stkCache.GetActiveValidatorSet(ctx, h.k.buildCurrEpochValSetMap) + if err != nil { + h.k.Logger(ctx).Error("failed to get previous validator set", "error", err) + return + } + + // Build an array of previous validator addresses for deterministic iteration + // when checking for newly inactive validators + prevValAddrs := make([]string, 0, len(prevValMap)) + for valAddr := range prevValMap { + prevValAddrs = append(prevValAddrs, valAddr) + } + // Sort the previous validator addresses for deterministic iteration + sort.Strings(prevValAddrs) + + // Build the new validator set map from the staking module + // Note: This is called after ApplyAndReturnValidatorSetUpdates, so the staking + // module's last validator powers reflect the NEW epoch's validator set + newValMap, newValAddrs, err := h.buildNewActiveValSetMap(ctx) + if err != nil { + h.k.Logger(ctx).Error("failed to build new validator set", "error", err) + return + } + + // Identify newly active validators (in new set but not in prev set) + for _, valAddr := range newValAddrs { + valAddrStr := valAddr.String() + if _, found := prevValMap[valAddrStr]; !found { + // Newly active validator - add baby tokens for all delegators + if err := h.addBabyForDelegators(ctx, valAddrStr); err != nil { + h.k.Logger(ctx).Error("failed to add baby tokens for newly active validator", "validator", valAddrStr, "error", err) + return + } + } + } + + // Identify newly inactive validators (in prev set but not in new set) + for _, prevValAddr := range prevValAddrs { + if _, found := newValMap[prevValAddr]; !found { + // Newly inactive validator - remove baby tokens for all delegators + prevVal := prevValMap[prevValAddr] + if err := h.removeBabyForDelegators(ctx, prevVal); err != nil { + h.k.Logger(ctx).Error("failed to remove baby tokens for newly inactive validator", "validator", prevValAddr, "error", err) + return + } + } + } + + // Store the validator set for the NEXT epoch (epoch+1) + if err := h.k.updateValidatorSet(ctx, newValAddrs); err != nil { + h.k.Logger(ctx).Error("failed to store validator set for next epoch", "error", err) + } +} + +// updateCoStkTrackerForDelegators updates costaking tracker for all delegators of a validator +func (h HookEpoching) updateCoStkTrackerForDelegators( + ctx context.Context, + val stakingtypes.Validator, + updateFn func(*types.CostakerRewardsTracker, math.Int), +) error { + valAddr, err := sdk.ValAddressFromBech32(val.GetOperator()) + if err != nil { + return err + } + + delegations, err := h.k.stkK.GetValidatorDelegations(ctx, valAddr) + if err != nil { + return err + } + + for _, del := range delegations { + delAddr := sdk.MustAccAddressFromBech32(del.DelegatorAddress) + + // We should only update the costaker tracker based on the remaining shares + remainingShares := del.Shares + // In case the validator is jailed/slashed, + // check if there are any cached delta shares to consider + cachedDeltas := h.k.stkCache.GetDeltaShares(valAddr, delAddr) + for _, deltaShares := range cachedDeltas { + // Should remove the delta to update properly the costaker tracker + // with remaining shares only + remainingShares = remainingShares.Sub(deltaShares) + } + + if remainingShares.IsZero() { + // No shares left to process + continue + } + + // Get delegation tokens using truncated division to avoid precision loss + delTokens := val.TokensFromShares(remainingShares) + + // Update ActiveBaby using the provided update function + if err := h.k.costakerModified(ctx, delAddr, func(rwdTracker *types.CostakerRewardsTracker) { + updateFn(rwdTracker, delTokens.TruncateInt()) + }); err != nil { + h.k.Logger(ctx).Error("failed to update costaker tracker", + "delegator", delAddr.String(), + "error", err) + return err + } + } + + return nil +} + +// addBabyForDelegators adds baby tokens to all delegators of a newly active validator +func (h HookEpoching) addBabyForDelegators(ctx context.Context, valAddrStr string) error { + valAddr := sdk.MustValAddressFromBech32(valAddrStr) + val, err := h.k.stkK.GetValidator(ctx, valAddr) + if err != nil { + return fmt.Errorf("failed to get validator %s: %w", valAddrStr, err) + } + return h.updateCoStkTrackerForDelegators(ctx, val, func(rwdTracker *types.CostakerRewardsTracker, amount math.Int) { + rwdTracker.ActiveBaby = rwdTracker.ActiveBaby.Add(amount) + }) +} + +// removeBabyForDelegators removes baby tokens from all delegators of a newly inactive validator +func (h HookEpoching) removeBabyForDelegators(ctx context.Context, valInfo types.ValidatorInfo) error { + // Get validator from staking keeper to get updated shares + val, err := h.k.stkK.GetValidator(ctx, valInfo.ValAddress) + if err != nil { + return fmt.Errorf("failed to get validator %s: %w", valInfo.ValAddress.String(), err) + } + if valInfo.IsSlashed { + // If the validator has been slashed, we need to restore the original tokens + // before removing the baby tokens to avoid miscalculating the token amount + val.Tokens = valInfo.OriginalTokens + // restore original shares in case validator was slashed + val.DelegatorShares = valInfo.OriginalShares + } + return h.updateCoStkTrackerForDelegators(ctx, val, func(rwdTracker *types.CostakerRewardsTracker, amount math.Int) { + rwdTracker.ActiveBaby = rwdTracker.ActiveBaby.Sub(amount) + }) +} + +// BeforeSlashThreshold is called before a certain threshold of validators are slashed +func (h HookEpoching) BeforeSlashThreshold(ctx context.Context, valSet epochingtypes.ValidatorSet) { +} + +// buildNewActiveValSetMap builds the new active validator set map +// from the staking module's last validator powers (for next epoch) +func (h HookEpoching) buildNewActiveValSetMap(ctx context.Context) (map[string]struct{}, []sdk.ValAddress, error) { + valMap := make(map[string]struct{}) + valAddrs := make([]sdk.ValAddress, 0) + + err := h.k.stkK.IterateLastValidatorPowers(ctx, func(valAddr sdk.ValAddress, power int64) bool { + valMap[valAddr.String()] = struct{}{} + valAddrs = append(valAddrs, valAddr) + return false // continue iteration + }) + + if err != nil { + return nil, nil, err + } + + return valMap, valAddrs, nil +} + +// Create new epoching hooks +func (k Keeper) HookEpoching() HookEpoching { + return HookEpoching{k} +} diff --git a/x/costaking/keeper/hooks_epoching_test.go b/x/costaking/keeper/hooks_epoching_test.go new file mode 100644 index 000000000..af40341c5 --- /dev/null +++ b/x/costaking/keeper/hooks_epoching_test.go @@ -0,0 +1,338 @@ +package keeper + +import ( + "context" + "testing" + + "cosmossdk.io/math" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + "github.com/babylonlabs-io/babylon/v4/testutil/datagen" + tmocks "github.com/babylonlabs-io/babylon/v4/testutil/mocks" + "github.com/babylonlabs-io/babylon/v4/x/costaking/types" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +func TestHookEpochingAfterEpochEnds_ValidatorBecomesActive(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockIctvK := types.NewMockIncentiveKeeper(ctrl) + k, ctx := NewKeeperWithMockIncentiveKeeper(t, mockIctvK) + ctx = ctx.WithBlockHeight(100) + + // Setup: create a delegator and validator + delAddr := datagen.GenRandomAddress() + valAddr := datagen.GenRandomValidatorAddress() + shares := math.LegacyNewDec(1000) + + delegation := stakingtypes.Delegation{ + DelegatorAddress: delAddr.String(), + ValidatorAddress: valAddr.String(), + Shares: shares, + } + + val, err := tmocks.CreateValidator(valAddr, shares.RoundInt()) + require.NoError(t, err) + + mockStkK := k.stkK.(*types.MockStakingKeeper) + + hooks := k.HookEpoching() + + // Store an initial empty validator set to simulate previous epoch + err = k.updateValidatorSet(ctx, []sdk.ValAddress{}) + require.NoError(t, err) + + // Now validator becomes active (new epoch) - IterateLastValidatorPowers is called to build new validator set + mockStkK.EXPECT().IterateLastValidatorPowers(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, fn func(sdk.ValAddress, int64) bool) error { + fn(valAddr, 1000) // Validator is now active + return nil + }, + ).Times(1) + + // Mock getting validator for updateValidatorSet (to store validator set for next epoch) + mockStkK.EXPECT().GetValidator(gomock.Any(), valAddr).Return(val, nil).AnyTimes() + + // Mock getting delegations for the newly active validator + mockStkK.EXPECT().GetValidatorDelegations(gomock.Any(), valAddr).Return([]stakingtypes.Delegation{delegation}, nil).Times(1) + + // Call AfterEpochEnds - should add baby tokens for the newly active validator + hooks.AfterEpochEnds(ctx, 1) + + // Verify the costaker tracker was created/updated with the delegation amount + tracker, err := k.GetCostakerRewards(ctx, delAddr) + require.NoError(t, err) + require.Equal(t, shares.TruncateInt().String(), tracker.ActiveBaby.String()) +} + +func TestHookEpochingAfterEpochEnds_ValidatorBecomesInactive(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockIctvK := types.NewMockIncentiveKeeper(ctrl) + k, ctx := NewKeeperWithMockIncentiveKeeper(t, mockIctvK) + ctx = ctx.WithBlockHeight(100) + + delAddr := datagen.GenRandomAddress() + valAddr := datagen.GenRandomValidatorAddress() + shares := math.LegacyNewDec(1000) + + delegation := stakingtypes.Delegation{ + DelegatorAddress: delAddr.String(), + ValidatorAddress: valAddr.String(), + Shares: shares, + } + + val, err := tmocks.CreateValidator(valAddr, shares.RoundInt()) + require.NoError(t, err) + + // Setup: create a costaker tracker with existing ActiveBaby + err = k.setCostakerRewardsTracker(ctx, delAddr, types.NewCostakerRewardsTracker(0, math.ZeroInt(), shares.TruncateInt(), math.ZeroInt())) + require.NoError(t, err) + + mockStkK := k.stkK.(*types.MockStakingKeeper) + + hooks := k.HookEpoching() + + // Store validator set with validator as active (simulating previous epoch state) + mockStkK.EXPECT().GetValidator(gomock.Any(), valAddr).Return(val, nil).Times(1) + err = k.updateValidatorSet(ctx, []sdk.ValAddress{valAddr}) + require.NoError(t, err) + + // Now validator becomes inactive (new epoch) + mockStkK.EXPECT().IterateLastValidatorPowers(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, fn func(sdk.ValAddress, int64) bool) error { + // Empty - validator is no longer active + return nil + }, + ).Times(1) + + // Mock getting delegations for the newly inactive validator + mockStkK.EXPECT().GetValidatorDelegations(gomock.Any(), valAddr).Return([]stakingtypes.Delegation{delegation}, nil).Times(1) + + // Call AfterEpochEnds - should remove baby tokens for the newly inactive validator + mockStkK.EXPECT().GetValidator(gomock.Any(), valAddr).Return(val, nil).Times(2) + hooks.AfterEpochEnds(ctx, 1) + + // Verify the costaker tracker was updated (ActiveBaby should be zero) + tracker, err := k.GetCostakerRewards(ctx, delAddr) + require.NoError(t, err) + require.True(t, tracker.ActiveBaby.IsZero(), "ActiveBaby should be zero after validator becomes inactive", "active baby", tracker.ActiveBaby.String()) +} + +func TestHookEpochingAfterEpochEnds_MultipleValidatorsTransition(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockIctvK := types.NewMockIncentiveKeeper(ctrl) + k, ctx := NewKeeperWithMockIncentiveKeeper(t, mockIctvK) + ctx = ctx.WithBlockHeight(100) + + // Setup: 3 validators, 1 delegator + delAddr := datagen.GenRandomAddress() + val1Addr := datagen.GenRandomValidatorAddress() // Stays active + val2Addr := datagen.GenRandomValidatorAddress() // Becomes inactive + val3Addr := datagen.GenRandomValidatorAddress() // Becomes active + + shares1 := math.LegacyNewDec(1000) + shares2 := math.LegacyNewDec(500) + shares3 := math.LegacyNewDec(750) + + delegation2 := stakingtypes.Delegation{ + DelegatorAddress: delAddr.String(), + ValidatorAddress: val2Addr.String(), + Shares: shares2, + } + delegation3 := stakingtypes.Delegation{ + DelegatorAddress: delAddr.String(), + ValidatorAddress: val3Addr.String(), + Shares: shares3, + } + + val1, err := tmocks.CreateValidator(val1Addr, shares2.RoundInt()) + require.NoError(t, err) + val2, err := tmocks.CreateValidator(val2Addr, shares2.RoundInt()) + require.NoError(t, err) + val3, err := tmocks.CreateValidator(val3Addr, shares3.RoundInt()) + require.NoError(t, err) + + // Setup: create a costaker tracker with ActiveBaby from val1 and val2 + initialActiveBaby := shares1.Add(shares2).TruncateInt() + err = k.setCostakerRewardsTracker(ctx, delAddr, types.NewCostakerRewardsTracker(0, math.ZeroInt(), initialActiveBaby, math.ZeroInt())) + require.NoError(t, err) + + mockStkK := k.stkK.(*types.MockStakingKeeper) + + hooks := k.HookEpoching() + + // Store validator set with val1 and val2 active (simulating previous epoch state) + mockStkK.EXPECT().GetValidator(gomock.Any(), val1Addr).Return(val1, nil).Times(1) + mockStkK.EXPECT().GetValidator(gomock.Any(), val2Addr).Return(val2, nil).Times(1) + err = k.updateValidatorSet(ctx, []sdk.ValAddress{val1Addr, val2Addr}) + require.NoError(t, err) + + // New epoch: val1 and val3 are active (val2 became inactive, val3 became active) + mockStkK.EXPECT().IterateLastValidatorPowers(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, fn func(sdk.ValAddress, int64) bool) error { + fn(val1Addr, 1000) + fn(val3Addr, 750) + return nil + }, + ).Times(1) + + // Mock getting delegations for val1 (no change - active) + mockStkK.EXPECT().GetValidator(gomock.Any(), val1Addr).Return(val1, nil).Times(2) + + // Mock getting delegations for val3 (newly active) + mockStkK.EXPECT().GetValidatorDelegations(gomock.Any(), val3Addr).Return([]stakingtypes.Delegation{delegation3}, nil).Times(1) + mockStkK.EXPECT().GetValidator(gomock.Any(), val3Addr).Return(val3, nil).Times(2) + + // Mock getting delegations for val2 (newly inactive) + mockStkK.EXPECT().GetValidatorDelegations(gomock.Any(), val2Addr).Return([]stakingtypes.Delegation{delegation2}, nil).Times(1) + mockStkK.EXPECT().GetValidator(gomock.Any(), val2Addr).Return(val2, nil).Times(2) + + // Call AfterEpochEnds + hooks.AfterEpochEnds(ctx, 1) + + // Verify the costaker tracker: + // Should have: val1 (1000) + val3 (750) = 1750 + // Lost: val2 (500) + tracker, err := k.GetCostakerRewards(ctx, delAddr) + require.NoError(t, err) + expectedActiveBaby := shares1.Add(shares3).TruncateInt() // 1000 + 750 = 1750 + require.Equal(t, expectedActiveBaby.String(), tracker.ActiveBaby.String()) +} + +func TestHookEpochingAfterEpochEnds_NoValidatorChanges(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockIctvK := types.NewMockIncentiveKeeper(ctrl) + k, ctx := NewKeeperWithMockIncentiveKeeper(t, mockIctvK) + ctx = ctx.WithBlockHeight(100) + + delAddr := datagen.GenRandomAddress() + valAddr := datagen.GenRandomValidatorAddress() + shares := math.LegacyNewDec(1000) + + // Setup: create a costaker tracker with existing ActiveBaby + err := k.setCostakerRewardsTracker(ctx, delAddr, types.NewCostakerRewardsTracker(0, math.ZeroInt(), shares.TruncateInt(), math.ZeroInt())) + require.NoError(t, err) + + mockStkK := k.stkK.(*types.MockStakingKeeper) + + val, err := tmocks.CreateValidator(valAddr, shares.RoundInt()) + require.NoError(t, err) + + // Mock GetValidator for the setup phase (when storing initial validator set) + mockStkK.EXPECT().GetValidator(gomock.Any(), gomock.Any()).Return(val, nil).AnyTimes() + + hooks := k.HookEpoching() + + // Store validator set with validator as active (simulating previous epoch state) + err = k.updateValidatorSet(ctx, []sdk.ValAddress{valAddr}) + require.NoError(t, err) + + // Mock GetValidator for buildCurrEpochValSetMap (reads previous epoch's validator set) + mockStkK.EXPECT().GetValidator(gomock.Any(), gomock.Any()).Return(val, nil).AnyTimes() + + // New epoch: same validator is still active (no change) + mockStkK.EXPECT().IterateLastValidatorPowers(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, fn func(sdk.ValAddress, int64) bool) error { + fn(valAddr, 1000) + return nil + }, + ).Times(1) + + // Mock getting validator for updateValidatorSet + mockStkK.EXPECT().GetValidator(gomock.Any(), gomock.Any()).Return(val, nil).AnyTimes() + + // Call AfterEpochEnds - should not modify anything since no validator transitions + hooks.AfterEpochEnds(ctx, 1) + + // Verify the costaker tracker is unchanged + tracker, err := k.GetCostakerRewards(ctx, delAddr) + require.NoError(t, err) + require.Equal(t, shares.TruncateInt().String(), tracker.ActiveBaby.String()) +} + +func TestHookEpochingAfterEpochEnds_MultipleDelegators(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockIctvK := types.NewMockIncentiveKeeper(ctrl) + k, ctx := NewKeeperWithMockIncentiveKeeper(t, mockIctvK) + ctx = ctx.WithBlockHeight(100) + + // Setup: 1 validator, 3 delegators + del1Addr := datagen.GenRandomAddress() + del2Addr := datagen.GenRandomAddress() + del3Addr := datagen.GenRandomAddress() + valAddr := datagen.GenRandomValidatorAddress() + + shares1 := math.LegacyNewDec(1000) + shares2 := math.LegacyNewDec(500) + shares3 := math.LegacyNewDec(750) + + delegations := []stakingtypes.Delegation{ + { + DelegatorAddress: del1Addr.String(), + ValidatorAddress: valAddr.String(), + Shares: shares1, + }, + { + DelegatorAddress: del2Addr.String(), + ValidatorAddress: valAddr.String(), + Shares: shares2, + }, + { + DelegatorAddress: del3Addr.String(), + ValidatorAddress: valAddr.String(), + Shares: shares3, + }, + } + + val, err := tmocks.CreateValidator(valAddr, math.NewInt(2250)) + require.NoError(t, err) + + mockStkK := k.stkK.(*types.MockStakingKeeper) + + hooks := k.HookEpoching() + + // Store an initial empty validator set + err = k.updateValidatorSet(ctx, []sdk.ValAddress{}) + require.NoError(t, err) + + // New epoch: validator becomes active + mockStkK.EXPECT().IterateLastValidatorPowers(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, fn func(sdk.ValAddress, int64) bool) error { + fn(valAddr, 2250) + return nil + }, + ).Times(1) + + // Mock getting validator for every call + mockStkK.EXPECT().GetValidator(gomock.Any(), valAddr).Return(val, nil).AnyTimes() + + // Mock getting all delegations for the validator + mockStkK.EXPECT().GetValidatorDelegations(gomock.Any(), valAddr).Return(delegations, nil).Times(1) + + // Call AfterEpochEnds - should add baby tokens for all delegators + hooks.AfterEpochEnds(ctx, 1) + + // Verify all delegators have their trackers updated + tracker1, err := k.GetCostakerRewards(ctx, del1Addr) + require.NoError(t, err) + require.Equal(t, shares1.TruncateInt().String(), tracker1.ActiveBaby.String()) + + tracker2, err := k.GetCostakerRewards(ctx, del2Addr) + require.NoError(t, err) + require.Equal(t, shares2.TruncateInt().String(), tracker2.ActiveBaby.String()) + + tracker3, err := k.GetCostakerRewards(ctx, del3Addr) + require.NoError(t, err) + require.Equal(t, shares3.TruncateInt().String(), tracker3.ActiveBaby.String()) +} diff --git a/x/costaking/keeper/hooks_staking.go b/x/costaking/keeper/hooks_staking.go index 561745641..30fe81d62 100644 --- a/x/costaking/keeper/hooks_staking.go +++ b/x/costaking/keeper/hooks_staking.go @@ -29,6 +29,15 @@ type HookStaking struct { // in the same block as bond, unbond, bond again func (h HookStaking) AfterDelegationModified(ctx context.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) error { defer h.k.stkCache.Delete(delAddr, valAddr) + // Check if validator is in the active set + active, valInfo, err := h.isActiveValidator(ctx, valAddr) + if err != nil { + return err + } + if !active { + // Validator not in active set, skip processing + return nil + } del, err := h.k.stkK.GetDelegation(ctx, delAddr, valAddr) if err != nil { // we stop if the delegation is not found, because it must be found @@ -40,8 +49,20 @@ func (h HookStaking) AfterDelegationModified(ctx context.Context, delAddr sdk.Ac return err } - delTokensBefore := h.k.stkCache.GetStakedAmount(delAddr, valAddr) - delTokenChange := delTokens.Sub(delTokensBefore).TruncateInt() + infoBefore := h.k.stkCache.GetStakedInfo(delAddr, valAddr) + delTokenChange := delTokens.Sub(infoBefore.Amount).TruncateInt() + + // if validator is jailed/slashed, don't update the costaker tracker + // but keep track of the delta shares (due to unstaking/redelegating/restaking) to remove the remaining shares + // when updating the validator's delegators co-staking trackers + if valInfo.IsSlashed { + // cache the delta shares for future use + // NOTE: once the validator is slashed, the 1:1 ratio between tokens and shares is broken + deltaShares := del.Shares.Sub(infoBefore.Shares) + h.k.stkCache.AddDeltaShares(valAddr, delAddr, deltaShares) + return nil + } + return h.k.costakerModified(ctx, delAddr, func(rwdTracker *types.CostakerRewardsTracker) { rwdTracker.ActiveBaby = rwdTracker.ActiveBaby.Add(delTokenChange) }) @@ -57,11 +78,29 @@ func (h HookStaking) AfterDelegationModified(ctx context.Context, delAddr sdk.Ac func (h HookStaking) BeforeDelegationRemoved(ctx context.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) error { defer h.k.stkCache.Delete(delAddr, valAddr) - delTokensBefore := h.k.stkCache.GetStakedAmount(delAddr, valAddr) - delTokenChange := delTokensBefore.TruncateInt() + // Check if validator is in the active set + active, valInfo, err := h.isActiveValidator(ctx, valAddr) + if err != nil { + return err + } + if !active { + // Validator not in active set, skip processing + return nil + } + + info := h.k.stkCache.GetStakedInfo(delAddr, valAddr) + delTokenChange := info.Amount.TruncateInt() if delTokenChange.IsZero() { return nil } + + // if validator is jailed/slashed, update the costaker tracker + // but we need to correct here the tokens to be removed to from the co-staking tracker + // which is the amount of tokens before slashing + if valInfo.IsSlashed { + delTokenChange = info.Shares.MulInt(valInfo.OriginalTokens).Quo(valInfo.OriginalShares).TruncateInt() + } + return h.k.costakerModified(ctx, delAddr, func(rwdTracker *types.CostakerRewardsTracker) { rwdTracker.ActiveBaby = rwdTracker.ActiveBaby.Sub(delTokenChange) }) @@ -74,6 +113,16 @@ func (h HookStaking) BeforeDelegationRemoved(ctx context.Context, delAddr sdk.Ac // State Changes: // - Caches current delegation amount in temporary storage func (h HookStaking) BeforeDelegationSharesModified(ctx context.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) error { + // Check if validator is in the active set + active, _, err := h.isActiveValidator(ctx, valAddr) + if err != nil { + return err + } + if !active { + // Validator not in active set, skip processing + return nil + } + del, err := h.k.stkK.GetDelegation(ctx, delAddr, valAddr) if err != nil { // probably is not found, but we don't want to stop execution for this @@ -85,7 +134,7 @@ func (h HookStaking) BeforeDelegationSharesModified(ctx context.Context, delAddr if err != nil { return err } - h.k.stkCache.SetStakedAmount(delAddr, valAddr, delTokens) + h.k.stkCache.SetStakedInfo(delAddr, valAddr, delTokens, del.Shares) return nil } @@ -135,11 +184,89 @@ func (k Keeper) HookStaking() HookStaking { } // TokensFromShares gets the validator and returns the tokens based on the amount of shares +// This function uses the validator's original tokens stored in the module state +// to calculate the delegation tokens from shares. In this way, we avoid issues +// that may arise from changes in the validator's tokens due to slashing func (k Keeper) TokensFromShares(ctx context.Context, valAddr sdk.ValAddress, delShares math.LegacyDec) (math.LegacyDec, error) { - valI, err := k.stkK.Validator(ctx, valAddr) + val, err := k.stkK.GetValidator(ctx, valAddr) if err != nil { return math.LegacyDec{}, err } - delTokens := valI.TokensFromShares(delShares) + + delTokens := val.TokensFromShares(delShares) return delTokens, nil } + +// buildCurrEpochValSetMap builds the current epoch's validator set map +// with their original tokens stored in the module state +func (k Keeper) buildCurrEpochValSetMap(ctx context.Context) (map[string]types.ValidatorInfo, error) { + valMap := make(map[string]types.ValidatorInfo) + + // During genesis, the epoching store may not be initialized yet. + // In this case, we return an empty map and rely on assumeActiveValidatorIfGenesis + // to populate validators as needed. + sdkCtx := sdk.UnwrapSDKContext(ctx) + if sdkCtx.BlockHeader().Height == 0 { + return valMap, nil + } + + // Get the current epoch's validator set from the epoching keeper + valSet, err := k.validatorSet.Get(ctx) + if err != nil { + return nil, err + } + + // Convert epoching ValidatorSet to map + for _, val := range valSet.Validators { + valAddr := sdk.ValAddress(val.Addr) + // Get current state of validators from staking keeper + stkVal, err := k.stkK.GetValidator(ctx, valAddr) + if err != nil { + return nil, err + } + + currentTokens := stkVal.GetTokens() + valMap[valAddr.String()] = types.ValidatorInfo{ + ValAddress: valAddr, + OriginalTokens: val.Tokens, + OriginalShares: val.Shares, + CurrentTokens: currentTokens, + IsSlashed: currentTokens.LT(val.Tokens), // consider slashed if current tokens < original tokens + } + } + + return valMap, nil +} + +// assumeActiveValidatorIfGenesis adds the given validator to the active set if block height is genesis height (0) +// and the validator is not already in the set +func (k Keeper) assumeActiveValidatorIfGenesis(ctx context.Context, valSet map[string]types.ValidatorInfo, valAddr sdk.ValAddress) { + sdkCtx := sdk.UnwrapSDKContext(ctx) + if sdkCtx.BlockHeader().Height == 0 { + // Add validator to active set during genesis + valSet[valAddr.String()] = types.ValidatorInfo{ + ValAddress: valAddr, + IsSlashed: false, + } + } +} + +func (h HookStaking) isActiveValidator(ctx context.Context, valAddr sdk.ValAddress) (bool, types.ValidatorInfo, error) { + // Check if validator is in the active set + valSet, err := h.k.stkCache.GetActiveValidatorSet(ctx, h.k.buildCurrEpochValSetMap) + if err != nil { + return false, types.ValidatorInfo{}, err + } + + // NOTE: co-staking genesis is called before staking genesis. + // The active set will be populated during the staking genesis but after calling the hooks, so the active validators map will be empty. + // Thus, for testing purposes, we assume all validators are active if the set is empty and block height is 0. + h.k.assumeActiveValidatorIfGenesis(ctx, valSet, valAddr) + + valInfo, ok := valSet[valAddr.String()] + if !ok { + // Validator not in active set, skip processing + return false, types.ValidatorInfo{}, nil + } + return true, valInfo, nil +} diff --git a/x/costaking/keeper/hooks_staking_test.go b/x/costaking/keeper/hooks_staking_test.go index f6bd274bf..6d92dfa81 100644 --- a/x/costaking/keeper/hooks_staking_test.go +++ b/x/costaking/keeper/hooks_staking_test.go @@ -32,8 +32,9 @@ func TestHookStakingBeforeDelegationSharesModifiedUpdateCache(t *testing.T) { require.NoError(t, err) mockStkK := k.stkK.(*types.MockStakingKeeper) + mockStkK.EXPECT().GetDelegation(ctx, delAddr, valAddr).Return(delegation, nil).Times(1) - mockStkK.EXPECT().Validator(ctx, valAddr).Return(val, nil).Times(1) + mockStkK.EXPECT().GetValidator(ctx, valAddr).Return(val, nil).Times(1) hooks := k.HookStaking() @@ -41,19 +42,22 @@ func TestHookStakingBeforeDelegationSharesModifiedUpdateCache(t *testing.T) { require.NoError(t, err) // Verify the amount was cached by retrieving - cachedAmount := k.stkCache.GetStakedAmount(delAddr, valAddr) - require.True(t, shares.Equal(cachedAmount)) + info := k.stkCache.GetStakedInfo(delAddr, valAddr) + require.True(t, shares.Equal(info.Amount)) + require.True(t, shares.Equal(info.Shares)) // get again and make sure it is not deleted - cachedAmount = k.stkCache.GetStakedAmount(delAddr, valAddr) - require.True(t, shares.Equal(cachedAmount)) + info = k.stkCache.GetStakedInfo(delAddr, valAddr) + require.True(t, shares.Equal(info.Amount)) + require.True(t, shares.Equal(info.Shares)) mockStkK.EXPECT().GetDelegation(ctx, delAddr, valAddr).Return(stakingtypes.Delegation{}, stakingtypes.ErrNoDelegation).Times(1) // Call BeforeDelegationSharesModified - should not return error even though the get del returned err err = hooks.BeforeDelegationSharesModified(ctx, delAddr, valAddr) require.NoError(t, err) - cachedAmount = k.stkCache.GetStakedAmount(delAddr, valAddr) - require.True(t, shares.Equal(cachedAmount)) + info = k.stkCache.GetStakedInfo(delAddr, valAddr) + require.True(t, shares.Equal(info.Amount)) + require.True(t, shares.Equal(info.Shares)) } func TestHookStakingAfterDelegationModified(t *testing.T) { @@ -81,8 +85,12 @@ func TestHookStakingAfterDelegationModified(t *testing.T) { require.NoError(t, err) mockStkK := k.stkK.(*types.MockStakingKeeper) + mockStkK.EXPECT().GetDelegation(ctx, delAddr, valAddr).Return(delegation, nil).Times(1) - mockStkK.EXPECT().Validator(gomock.Any(), gomock.Eq(valAddr)).Return(&val, nil).Times(1) + mockStkK.EXPECT().GetValidator(gomock.Any(), valAddr).Return(val, nil).Times(2) + // Store an initial validator set + err = k.updateValidatorSet(ctx, []sdk.ValAddress{valAddr}) + require.NoError(t, err) hooks := k.HookStaking() @@ -107,7 +115,7 @@ func TestHookStakingAfterDelegationModified(t *testing.T) { require.NoError(t, err) mockStkK.EXPECT().GetDelegation(ctx, delAddr, valAddr).Return(delegation, nil).Times(1) - mockStkK.EXPECT().Validator(gomock.Any(), gomock.Eq(valAddr)).Return(&val, nil).Times(1) + mockStkK.EXPECT().GetValidator(gomock.Any(), gomock.Eq(valAddr)).Return(val, nil).Times(1) err = hooks.BeforeDelegationSharesModified(ctx, delAddr, valAddr) require.NoError(t, err) @@ -120,7 +128,7 @@ func TestHookStakingAfterDelegationModified(t *testing.T) { mockBankK.EXPECT().SendCoinsFromModuleToModule(ctx, types.ModuleName, ictvtypes.ModuleName, expRwd).Return(nil).Times(1) mockIctvK.EXPECT().AccumulateRewardGaugeForCostaker(gomock.Any(), gomock.Eq(delAddr), expRwd).Times(1) - mockStkK.EXPECT().Validator(gomock.Any(), gomock.Eq(valAddr)).Return(&val, nil).Times(1) + mockStkK.EXPECT().GetValidator(gomock.Any(), gomock.Eq(valAddr)).Return(val, nil).Times(1) err = hooks.AfterDelegationModified(ctx, delAddr, valAddr) require.NoError(t, err) } @@ -130,6 +138,7 @@ func TestHookStakingAfterDelegationModifiedErrorDelegationNotFound(t *testing.T) delAddr, valAddr := datagen.GenRandomAddress(), datagen.GenRandomValidatorAddress() mockStkK := k.stkK.(*types.MockStakingKeeper) + mockStkK.EXPECT().GetDelegation(ctx, delAddr, valAddr).Return(stakingtypes.Delegation{}, stakingtypes.ErrNoDelegation).Times(1) hooks := k.HookStaking() @@ -147,7 +156,7 @@ func TestHookStakingAfterDelegationModifiedReducingAmountStaked(t *testing.T) { err := k.setCostakerRewardsTracker(ctx, delAddr, types.NewCostakerRewardsTracker(0, math.ZeroInt(), initShares, math.ZeroInt())) require.NoError(t, err) - k.stkCache.SetStakedAmount(delAddr, valAddr, initShares.ToLegacyDec()) + k.stkCache.SetStakedInfo(delAddr, valAddr, initShares.ToLegacyDec(), initShares.ToLegacyDec()) // reduces it by 500 afterShares := math.LegacyNewDec(1500) @@ -158,11 +167,15 @@ func TestHookStakingAfterDelegationModifiedReducingAmountStaked(t *testing.T) { } mockStkK := k.stkK.(*types.MockStakingKeeper) + mockStkK.EXPECT().GetDelegation(ctx, delAddr, valAddr).Return(delegation, nil).Times(1) val, err := tmocks.CreateValidator(valAddr, math.NewInt(100)) require.NoError(t, err) - mockStkK.EXPECT().Validator(gomock.Any(), gomock.Any()).Return(&val, nil).Times(1) + mockStkK.EXPECT().GetValidator(gomock.Any(), gomock.Any()).Return(val, nil).Times(2) + // Store an initial validator set + err = k.updateValidatorSet(ctx, []sdk.ValAddress{valAddr}) + require.NoError(t, err) hooks := k.HookStaking() @@ -174,3 +187,155 @@ func TestHookStakingAfterDelegationModifiedReducingAmountStaked(t *testing.T) { require.NoError(t, err) require.Equal(t, afterShares.TruncateInt().String(), tracker.ActiveBaby.String()) } + +func TestHookStakingAfterDelegationModified_InactiveValidator(t *testing.T) { + k, ctx := NewKeeperWithMockIncentiveKeeper(t, nil) + // Set block height > 0 to avoid genesis special case + ctx = ctx.WithBlockHeight(100) + + delAddr, valAddr := datagen.GenRandomAddress(), datagen.GenRandomValidatorAddress() + + hooks := k.HookStaking() + // Store an initial validator set + err := k.updateValidatorSet(ctx, []sdk.ValAddress{}) + require.NoError(t, err) + + // Call AfterDelegationModified - should skip processing because validator is not active + err = hooks.AfterDelegationModified(ctx, delAddr, valAddr) + require.NoError(t, err) + + // Verify no costaker tracker was created (validator was not active) + _, err = k.GetCostakerRewards(ctx, delAddr) + require.Error(t, err) // Should return error because no tracker exists +} + +func TestHookStakingBeforeDelegationSharesModified_InactiveValidator(t *testing.T) { + k, ctx := NewKeeperWithMockIncentiveKeeper(t, nil) + // Set block height > 0 to avoid genesis special case + ctx = ctx.WithBlockHeight(100) + + delAddr, valAddr := datagen.GenRandomAddress(), datagen.GenRandomValidatorAddress() + + hooks := k.HookStaking() + // Store an initial validator set + err := k.updateValidatorSet(ctx, []sdk.ValAddress{}) + require.NoError(t, err) + + // Call BeforeDelegationSharesModified - should skip processing + err = hooks.BeforeDelegationSharesModified(ctx, delAddr, valAddr) + require.NoError(t, err) + + // Verify nothing was cached (validator was not active) + info := k.stkCache.GetStakedInfo(delAddr, valAddr) + require.True(t, info.Amount.IsZero()) +} + +func TestHookStakingMultipleValidators_MixedActiveInactive(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockIctvK := types.NewMockIncentiveKeeper(ctrl) + k, ctx := NewKeeperWithMockIncentiveKeeper(t, mockIctvK) + // Set block height > 0 to avoid genesis special case + ctx = ctx.WithBlockHeight(100) + + delAddr := datagen.GenRandomAddress() + activeValAddr := datagen.GenRandomValidatorAddress() + inactiveValAddr := datagen.GenRandomValidatorAddress() + + activeShares := math.LegacyNewDec(1000) + + activeDelegation := stakingtypes.Delegation{ + DelegatorAddress: delAddr.String(), + ValidatorAddress: activeValAddr.String(), + Shares: activeShares, + } + + activeVal, err := tmocks.CreateValidator(activeValAddr, activeShares.RoundInt()) + require.NoError(t, err) + + mockStkK := k.stkK.(*types.MockStakingKeeper) + + // Store an initial validator set + mockStkK.EXPECT().GetValidator(gomock.Any(), activeValAddr).Return(activeVal, nil).AnyTimes() + err = k.updateValidatorSet(ctx, []sdk.ValAddress{activeValAddr}) + require.NoError(t, err) + mockStkK.EXPECT().GetDelegation(ctx, delAddr, activeValAddr).Return(activeDelegation, nil).Times(1) + + hooks := k.HookStaking() + + // Delegate to active validator - should be tracked + err = hooks.AfterDelegationModified(ctx, delAddr, activeValAddr) + require.NoError(t, err) + + // Verify tracker was created with active validator's amount + tracker, err := k.GetCostakerRewards(ctx, delAddr) + require.NoError(t, err) + require.Equal(t, activeShares.TruncateInt().String(), tracker.ActiveBaby.String()) + + // Second call: try to delegate to inactive validator + // Note: cache is already populated, so no IterateLastValidatorPowers call + err = hooks.AfterDelegationModified(ctx, delAddr, inactiveValAddr) + require.NoError(t, err) + + // Verify tracker amount didn't change (inactive validator was skipped) + tracker, err = k.GetCostakerRewards(ctx, delAddr) + require.NoError(t, err) + require.Equal(t, activeShares.TruncateInt().String(), tracker.ActiveBaby.String()) +} + +func TestHookStakingValidatorBecomesInactive(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockIctvK := types.NewMockIncentiveKeeper(ctrl) + k, ctx := NewKeeperWithMockIncentiveKeeper(t, mockIctvK) + // Set block height > 0 to avoid genesis special case + ctx = ctx.WithBlockHeight(100) + + delAddr, valAddr := datagen.GenRandomAddress(), datagen.GenRandomValidatorAddress() + shares := math.LegacyNewDec(1000) + + delegation := stakingtypes.Delegation{ + DelegatorAddress: delAddr.String(), + ValidatorAddress: valAddr.String(), + Shares: shares, + } + + val, err := tmocks.CreateValidator(valAddr, shares.RoundInt()) + require.NoError(t, err) + + mockStkK := k.stkK.(*types.MockStakingKeeper) + + hooks := k.HookStaking() + + // First: validator is active + + mockStkK.EXPECT().GetDelegation(gomock.Any(), delAddr, valAddr).Return(delegation, nil).Times(1) + mockStkK.EXPECT().GetValidator(gomock.Any(), gomock.Eq(valAddr)).Return(val, nil).AnyTimes() + err = k.updateValidatorSet(ctx, []sdk.ValAddress{valAddr}) + require.NoError(t, err) + + // Delegate while validator is active + err = hooks.AfterDelegationModified(ctx, delAddr, valAddr) + require.NoError(t, err) + + tracker, err := k.GetCostakerRewards(ctx, delAddr) + require.NoError(t, err) + require.Equal(t, shares.TruncateInt().String(), tracker.ActiveBaby.String()) + + // Clear the cache to simulate new block/epoch + k.stkCache.Clear() + + err = k.updateValidatorSet(ctx, []sdk.ValAddress{}) + require.NoError(t, err) + + // Try to modify delegation while validator is inactive - should be skipped + err = hooks.AfterDelegationModified(ctx, delAddr, valAddr) + require.NoError(t, err) + + // Tracker should still have the old amount (no change because validator is inactive) + tracker, err = k.GetCostakerRewards(ctx, delAddr) + require.NoError(t, err) + require.Equal(t, shares.TruncateInt().String(), tracker.ActiveBaby.String()) +} diff --git a/x/costaking/keeper/keeper.go b/x/costaking/keeper/keeper.go index b0b1d06df..ced681a58 100644 --- a/x/costaking/keeper/keeper.go +++ b/x/costaking/keeper/keeper.go @@ -43,6 +43,8 @@ type ( historicalRewards collections.Map[uint64, types.HistoricalRewards] // costakerRewardsTracker maps (costakerAddr) => costakerRewardsTracker costakerRewardsTracker collections.Map[[]byte, types.CostakerRewardsTracker] + // validatorSet stores the active validator set for the current epoch + validatorSet collections.Item[types.ValidatorSet] } ) @@ -106,6 +108,12 @@ func NewKeeper( collections.BytesKey, codec.CollValue[types.CostakerRewardsTracker](cdc), ), + validatorSet: collections.NewItem( + sb, + types.ValidatorsKeyPrefix, + "validators", + codec.CollValue[types.ValidatorSet](cdc), + ), } } diff --git a/x/costaking/keeper/keeper_test.go b/x/costaking/keeper/keeper_test.go index 596d6d81d..882c76ae0 100644 --- a/x/costaking/keeper/keeper_test.go +++ b/x/costaking/keeper/keeper_test.go @@ -14,16 +14,16 @@ func TestKeeperEndBlock(t *testing.T) { delAddr, valAddr := datagen.GenRandomAddress(), datagen.GenRandomValidatorAddress() shares := math.LegacyNewDec(1500) - k.stkCache.SetStakedAmount(delAddr, valAddr, shares) + k.stkCache.SetStakedInfo(delAddr, valAddr, shares, shares) - cachedAmount := k.stkCache.GetStakedAmount(delAddr, valAddr) - require.True(t, shares.Equal(cachedAmount)) + cachedInfo := k.stkCache.GetStakedInfo(delAddr, valAddr) + require.True(t, shares.Equal(cachedInfo.Amount)) - k.stkCache.SetStakedAmount(delAddr, valAddr, shares) + k.stkCache.SetStakedInfo(delAddr, valAddr, shares, shares) err := k.EndBlock(ctx) require.NoError(t, err) - cachedAmount = k.stkCache.GetStakedAmount(delAddr, valAddr) - require.True(t, cachedAmount.IsZero()) + cachedInfo = k.stkCache.GetStakedInfo(delAddr, valAddr) + require.True(t, cachedInfo.Amount.IsZero()) } diff --git a/x/costaking/keeper/reward_tracker.go b/x/costaking/keeper/reward_tracker.go index d8c625dc2..597c83b1d 100644 --- a/x/costaking/keeper/reward_tracker.go +++ b/x/costaking/keeper/reward_tracker.go @@ -34,6 +34,10 @@ func (k Keeper) costakerModified(ctx context.Context, costaker sdk.AccAddress, m } modifyCostaker(rwdTracker) + // sanitize and validate the costaker tracker + // this is necessary because the ActiveBaby can be -1 due to rounding when the validator + // lost its 1:1 ratio between shares and tokens due to slashing + rwdTracker.Sanitize() if err := rwdTracker.Validate(); err != nil { return fmt.Errorf("failed to validate costaker: %s - %w", costaker.String(), err) } diff --git a/x/costaking/keeper/validator.go b/x/costaking/keeper/validator.go new file mode 100644 index 000000000..07209d652 --- /dev/null +++ b/x/costaking/keeper/validator.go @@ -0,0 +1,33 @@ +package keeper + +import ( + "context" + + "github.com/babylonlabs-io/babylon/v4/x/costaking/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// updateValidatorSet stores the current validator set with their original tokens and shares +// This is called upon AfterEpochBegins +func (k Keeper) updateValidatorSet(ctx context.Context, newValAddrs []sdk.ValAddress) error { + var validatorSet types.ValidatorSet + // Iterate over the new validator set addresses + for _, valAddr := range newValAddrs { + // store the original tokens delegated to the validator + // We can get the validator from staking keeper + val, err := k.stkK.GetValidator(ctx, valAddr) + if err != nil { + return err + } + validatorSet.Validators = append( + validatorSet.Validators, + &types.Validator{ + Addr: valAddr, + Tokens: val.Tokens, + Shares: val.DelegatorShares, + }, + ) + } + + return k.validatorSet.Set(ctx, validatorSet) +} diff --git a/x/costaking/types/costaking.pb.go b/x/costaking/types/costaking.pb.go index 147c8ec61..ff92beaea 100644 --- a/x/costaking/types/costaking.pb.go +++ b/x/costaking/types/costaking.pb.go @@ -76,8 +76,106 @@ func (m *Params) XXX_DiscardUnknown() { var xxx_messageInfo_Params proto.InternalMessageInfo +// Validator is a message that denotes a validator +type Validator struct { + // addr is the validator's address (in sdk.ValAddress) + Addr []byte `protobuf:"bytes,1,opt,name=addr,proto3" json:"addr,omitempty"` + // tokens define the delegated tokens (incl. self-delegation). + Tokens cosmossdk_io_math.Int `protobuf:"bytes,2,opt,name=tokens,proto3,customtype=cosmossdk.io/math.Int" json:"tokens"` + // shares defines total shares issued to a validator's delegators. + Shares cosmossdk_io_math.LegacyDec `protobuf:"bytes,3,opt,name=shares,proto3,customtype=cosmossdk.io/math.LegacyDec" json:"shares"` +} + +func (m *Validator) Reset() { *m = Validator{} } +func (m *Validator) String() string { return proto.CompactTextString(m) } +func (*Validator) ProtoMessage() {} +func (*Validator) Descriptor() ([]byte, []int) { + return fileDescriptor_e3ebc90d718f5cee, []int{1} +} +func (m *Validator) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Validator) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Validator.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Validator) XXX_Merge(src proto.Message) { + xxx_messageInfo_Validator.Merge(m, src) +} +func (m *Validator) XXX_Size() int { + return m.Size() +} +func (m *Validator) XXX_DiscardUnknown() { + xxx_messageInfo_Validator.DiscardUnknown(m) +} + +var xxx_messageInfo_Validator proto.InternalMessageInfo + +func (m *Validator) GetAddr() []byte { + if m != nil { + return m.Addr + } + return nil +} + +// ValidatorSet is a message that denotes a set of validators +type ValidatorSet struct { + // validators is the list of all validators and their delegated tokens. + Validators []*Validator `protobuf:"bytes,1,rep,name=validators,proto3" json:"validators,omitempty"` +} + +func (m *ValidatorSet) Reset() { *m = ValidatorSet{} } +func (m *ValidatorSet) String() string { return proto.CompactTextString(m) } +func (*ValidatorSet) ProtoMessage() {} +func (*ValidatorSet) Descriptor() ([]byte, []int) { + return fileDescriptor_e3ebc90d718f5cee, []int{2} +} +func (m *ValidatorSet) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ValidatorSet) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ValidatorSet.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *ValidatorSet) XXX_Merge(src proto.Message) { + xxx_messageInfo_ValidatorSet.Merge(m, src) +} +func (m *ValidatorSet) XXX_Size() int { + return m.Size() +} +func (m *ValidatorSet) XXX_DiscardUnknown() { + xxx_messageInfo_ValidatorSet.DiscardUnknown(m) +} + +var xxx_messageInfo_ValidatorSet proto.InternalMessageInfo + +func (m *ValidatorSet) GetValidators() []*Validator { + if m != nil { + return m.Validators + } + return nil +} + func init() { proto.RegisterType((*Params)(nil), "babylon.costaking.v1.Params") + proto.RegisterType((*Validator)(nil), "babylon.costaking.v1.Validator") + proto.RegisterType((*ValidatorSet)(nil), "babylon.costaking.v1.ValidatorSet") } func init() { @@ -85,28 +183,33 @@ func init() { } var fileDescriptor_e3ebc90d718f5cee = []byte{ - // 325 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x91, 0xc1, 0x4a, 0x02, 0x51, - 0x18, 0x85, 0x67, 0x0c, 0x84, 0x86, 0x16, 0x39, 0x19, 0x99, 0xc1, 0x28, 0xd1, 0x42, 0x08, 0xe7, - 0x22, 0x45, 0x0f, 0x30, 0xb8, 0x11, 0x5a, 0x88, 0xcb, 0x16, 0x4d, 0xff, 0xbd, 0x0e, 0xe3, 0x45, - 0x67, 0x7e, 0x99, 0xfb, 0x37, 0x74, 0xdf, 0xa2, 0x17, 0x69, 0xd7, 0x43, 0xb8, 0x94, 0x56, 0xd1, - 0x42, 0x42, 0x5f, 0x24, 0xf4, 0x4e, 0x2a, 0xb4, 0x6b, 0x77, 0x0f, 0xe7, 0xdc, 0xef, 0xe7, 0x70, - 0x9c, 0x2b, 0x0e, 0x5c, 0x4f, 0x30, 0x65, 0x02, 0x15, 0xc1, 0x58, 0xa6, 0x31, 0xcb, 0x3b, 0x3b, - 0xe1, 0x4f, 0x33, 0x24, 0x74, 0xab, 0x45, 0xca, 0xdf, 0x19, 0x79, 0xa7, 0x5e, 0x8d, 0x31, 0xc6, - 0x4d, 0x80, 0xad, 0x5f, 0x26, 0x5b, 0x3f, 0x17, 0xa8, 0x12, 0x54, 0xa1, 0x31, 0x8c, 0x30, 0xd6, - 0xe5, 0x5b, 0xc9, 0x29, 0xf7, 0x21, 0x83, 0x44, 0xb9, 0x8f, 0x4e, 0x65, 0xcb, 0x0a, 0xa7, 0x98, - 0x91, 0xc4, 0xb4, 0x66, 0x37, 0xed, 0xd6, 0x61, 0xd0, 0x99, 0x2d, 0x1a, 0xd6, 0xd7, 0xa2, 0x71, - 0x61, 0xfe, 0xaa, 0xe1, 0xd8, 0x97, 0xc8, 0x12, 0xa0, 0x91, 0x7f, 0x1f, 0xc5, 0x20, 0x74, 0x37, - 0x12, 0x1f, 0xef, 0x6d, 0xa7, 0x40, 0x77, 0x23, 0x31, 0x38, 0xde, 0xb2, 0xfa, 0x06, 0xe5, 0x82, - 0x73, 0xa6, 0x04, 0x66, 0x51, 0x98, 0x01, 0x49, 0x0c, 0x39, 0x89, 0x90, 0xeb, 0x70, 0x5d, 0xa3, - 0x56, 0x6a, 0xda, 0xad, 0xa3, 0xe0, 0xba, 0xb8, 0x72, 0xfa, 0xf7, 0x4a, 0x2f, 0xa5, 0x3d, 0x7e, - 0x2f, 0xa5, 0xc1, 0xc9, 0x86, 0x35, 0x58, 0xa3, 0x02, 0x12, 0x81, 0x0e, 0x80, 0x6b, 0xf7, 0xc9, - 0x71, 0x73, 0x98, 0xc8, 0x21, 0x10, 0x66, 0x6a, 0xdb, 0xe1, 0xe0, 0xbf, 0x1d, 0x2a, 0x3b, 0x58, - 0x51, 0x22, 0xe8, 0xcf, 0x96, 0x9e, 0x3d, 0x5f, 0x7a, 0xf6, 0xf7, 0xd2, 0xb3, 0x5f, 0x57, 0x9e, - 0x35, 0x5f, 0x79, 0xd6, 0xe7, 0xca, 0xb3, 0x1e, 0xee, 0x62, 0x49, 0xa3, 0x67, 0xee, 0x0b, 0x4c, - 0x58, 0xb1, 0xcd, 0x04, 0xb8, 0x6a, 0x4b, 0xfc, 0x95, 0x2c, 0xbf, 0x65, 0x2f, 0x7b, 0xab, 0x92, - 0x9e, 0x46, 0x8a, 0x97, 0x37, 0x43, 0xdc, 0xfc, 0x04, 0x00, 0x00, 0xff, 0xff, 0xc6, 0xb0, 0x76, - 0x45, 0xf7, 0x01, 0x00, 0x00, + // 403 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x92, 0xc1, 0xca, 0xd3, 0x40, + 0x10, 0xc7, 0xb3, 0xad, 0x04, 0xba, 0xf6, 0x60, 0xd7, 0x8a, 0xb5, 0x42, 0x5a, 0x8a, 0x87, 0x82, + 0x34, 0xa1, 0x2a, 0x5e, 0x85, 0xd8, 0x4b, 0x41, 0xb0, 0x44, 0xf0, 0xe0, 0xc1, 0xb8, 0xbb, 0x59, + 0xd2, 0xd0, 0x26, 0x53, 0x76, 0xd7, 0x60, 0xde, 0xc2, 0x17, 0x11, 0x2f, 0x3e, 0x44, 0x8f, 0xc5, + 0x93, 0x78, 0x28, 0x1f, 0xed, 0x8b, 0x7c, 0x34, 0x49, 0x93, 0xc2, 0xf7, 0x9d, 0x7a, 0x9b, 0xd9, + 0x99, 0xff, 0x6f, 0xe6, 0xbf, 0x0c, 0x7e, 0xc1, 0x28, 0xcb, 0xd6, 0x90, 0x38, 0x1c, 0x94, 0xa6, + 0xab, 0x28, 0x09, 0x9d, 0x74, 0x5a, 0x27, 0xf6, 0x46, 0x82, 0x06, 0xd2, 0x2d, 0xbb, 0xec, 0xba, + 0x90, 0x4e, 0xfb, 0xdd, 0x10, 0x42, 0xc8, 0x1b, 0x9c, 0x53, 0x54, 0xf4, 0xf6, 0x9f, 0x71, 0x50, + 0x31, 0x28, 0xbf, 0x28, 0x14, 0x49, 0x51, 0x1a, 0xfd, 0x6a, 0x60, 0x73, 0x41, 0x25, 0x8d, 0x15, + 0xf9, 0x8a, 0x3b, 0x15, 0xcb, 0xdf, 0x80, 0xd4, 0x11, 0x24, 0x3d, 0x34, 0x44, 0xe3, 0x96, 0x3b, + 0xdd, 0xee, 0x07, 0xc6, 0xff, 0xfd, 0xe0, 0x79, 0xa1, 0x55, 0xc1, 0xca, 0x8e, 0xc0, 0x89, 0xa9, + 0x5e, 0xda, 0x1f, 0x44, 0x48, 0x79, 0x36, 0x13, 0xfc, 0xef, 0x9f, 0x09, 0x2e, 0xd1, 0x33, 0xc1, + 0xbd, 0x47, 0x15, 0x6b, 0x51, 0xa0, 0x08, 0xc5, 0x4f, 0x15, 0x07, 0x29, 0x7c, 0x49, 0x75, 0x04, + 0x3e, 0xd3, 0xdc, 0x67, 0x99, 0x7f, 0xb2, 0xd1, 0x6b, 0x0c, 0xd1, 0xb8, 0xed, 0xbe, 0x2c, 0xa7, + 0x3c, 0xb9, 0x3b, 0x65, 0x9e, 0xe8, 0x0b, 0xfe, 0x3c, 0xd1, 0xde, 0xe3, 0x9c, 0xe5, 0x9d, 0x50, + 0xae, 0xe6, 0x6e, 0xe6, 0x52, 0x96, 0x91, 0x6f, 0x98, 0xa4, 0x74, 0x1d, 0x05, 0x54, 0x83, 0x54, + 0x95, 0x87, 0xe6, 0xb5, 0x1e, 0x3a, 0x35, 0xac, 0x34, 0x31, 0xfa, 0x8d, 0x70, 0xeb, 0xf3, 0xf9, + 0x95, 0x10, 0xfc, 0x80, 0x06, 0x81, 0xcc, 0x7f, 0xa9, 0xed, 0xe5, 0x31, 0x79, 0x8f, 0x4d, 0x0d, + 0x2b, 0x91, 0xa8, 0x6b, 0x5c, 0x95, 0x52, 0x32, 0xc7, 0xa6, 0x5a, 0x52, 0x29, 0xd4, 0xf5, 0xcb, + 0x97, 0x80, 0xd1, 0x47, 0xdc, 0xae, 0x16, 0xfe, 0x24, 0x34, 0x79, 0x87, 0x71, 0x6d, 0xab, 0x87, + 0x86, 0xcd, 0xf1, 0xc3, 0x57, 0x03, 0xfb, 0xbe, 0x6b, 0xb2, 0x2b, 0x9d, 0x77, 0x21, 0x71, 0x17, + 0xdb, 0x83, 0x85, 0x76, 0x07, 0x0b, 0xdd, 0x1c, 0x2c, 0xf4, 0xf3, 0x68, 0x19, 0xbb, 0xa3, 0x65, + 0xfc, 0x3b, 0x5a, 0xc6, 0x97, 0xb7, 0x61, 0xa4, 0x97, 0xdf, 0x99, 0xcd, 0x21, 0x76, 0x4a, 0xe0, + 0x9a, 0x32, 0x35, 0x89, 0xe0, 0x9c, 0x3a, 0xe9, 0x1b, 0xe7, 0xc7, 0xc5, 0x61, 0xeb, 0x6c, 0x23, + 0x14, 0x33, 0xf3, 0x5b, 0x7c, 0x7d, 0x1b, 0x00, 0x00, 0xff, 0xff, 0x7d, 0xba, 0xa9, 0xb1, 0xfa, + 0x02, 0x00, 0x00, } func (m *Params) Marshal() (dAtA []byte, err error) { @@ -162,6 +265,93 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *Validator) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Validator) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Validator) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size := m.Shares.Size() + i -= size + if _, err := m.Shares.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintCostaking(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + { + size := m.Tokens.Size() + i -= size + if _, err := m.Tokens.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintCostaking(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + if len(m.Addr) > 0 { + i -= len(m.Addr) + copy(dAtA[i:], m.Addr) + i = encodeVarintCostaking(dAtA, i, uint64(len(m.Addr))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *ValidatorSet) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ValidatorSet) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ValidatorSet) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Validators) > 0 { + for iNdEx := len(m.Validators) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Validators[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintCostaking(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + func encodeVarintCostaking(dAtA []byte, offset int, v uint64) int { offset -= sovCostaking(v) base := offset @@ -188,6 +378,38 @@ func (m *Params) Size() (n int) { return n } +func (m *Validator) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Addr) + if l > 0 { + n += 1 + l + sovCostaking(uint64(l)) + } + l = m.Tokens.Size() + n += 1 + l + sovCostaking(uint64(l)) + l = m.Shares.Size() + n += 1 + l + sovCostaking(uint64(l)) + return n +} + +func (m *ValidatorSet) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Validators) > 0 { + for _, e := range m.Validators { + l = e.Size() + n += 1 + l + sovCostaking(uint64(l)) + } + } + return n +} + func sovCostaking(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -345,6 +567,241 @@ func (m *Params) Unmarshal(dAtA []byte) error { } return nil } +func (m *Validator) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCostaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Validator: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Validator: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Addr", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCostaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthCostaking + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthCostaking + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Addr = append(m.Addr[:0], dAtA[iNdEx:postIndex]...) + if m.Addr == nil { + m.Addr = []byte{} + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Tokens", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCostaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthCostaking + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthCostaking + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Tokens.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Shares", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCostaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthCostaking + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthCostaking + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Shares.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipCostaking(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthCostaking + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *ValidatorSet) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCostaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ValidatorSet: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ValidatorSet: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Validators", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowCostaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthCostaking + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthCostaking + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Validators = append(m.Validators, &Validator{}) + if err := m.Validators[len(m.Validators)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipCostaking(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthCostaking + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipCostaking(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/x/costaking/types/expected_keepers.go b/x/costaking/types/expected_keepers.go index 1efb50e38..beb127d69 100644 --- a/x/costaking/types/expected_keepers.go +++ b/x/costaking/types/expected_keepers.go @@ -30,6 +30,8 @@ type DistributionKeeper interface { // StakingKeeper expected staking keeper (noalias) type StakingKeeper interface { GetDelegation(ctx context.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) (stakingtypes.Delegation, error) + GetValidatorDelegations(ctx context.Context, valAddr sdk.ValAddress) ([]stakingtypes.Delegation, error) + IterateLastValidatorPowers(ctx context.Context, handler func(operator sdk.ValAddress, power int64) bool) error ValidatorByConsAddr(context.Context, sdk.ConsAddress) (stakingtypes.ValidatorI, error) - Validator(context.Context, sdk.ValAddress) (stakingtypes.ValidatorI, error) + GetValidator(ctx context.Context, addr sdk.ValAddress) (validator stakingtypes.Validator, err error) } diff --git a/x/costaking/types/genesis.go b/x/costaking/types/genesis.go index 630a38c49..7cb1e7fcc 100644 --- a/x/costaking/types/genesis.go +++ b/x/costaking/types/genesis.go @@ -17,6 +17,7 @@ func DefaultGenesis() *GenesisState { CurrentRewards: CurrentRewardsEntry{}, HistoricalRewards: []HistoricalRewardsEntry{}, CostakersRewardsTracker: []CostakerRewardsTrackerEntry{}, + ValidatorSet: ValidatorSet{}, } } @@ -37,6 +38,8 @@ func (gs GenesisState) Validate() error { return fmt.Errorf("invalid costakers rewards tracker: %w", err) } + // TODO validate validator set + return gs.Params.Validate() } diff --git a/x/costaking/types/genesis.pb.go b/x/costaking/types/genesis.pb.go index c9e766e64..fd0c012cd 100644 --- a/x/costaking/types/genesis.pb.go +++ b/x/costaking/types/genesis.pb.go @@ -36,6 +36,8 @@ type GenesisState struct { // costakers_rewards_tracker are the costaker rewards tracker stored by // costaker addresses. CostakersRewardsTracker []CostakerRewardsTrackerEntry `protobuf:"bytes,4,rep,name=costakers_rewards_tracker,json=costakersRewardsTracker,proto3" json:"costakers_rewards_tracker"` + // validator_set contains all validators and their delegated tokens. + ValidatorSet ValidatorSet `protobuf:"bytes,5,opt,name=validator_set,json=validatorSet,proto3" json:"validator_set"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -99,6 +101,13 @@ func (m *GenesisState) GetCostakersRewardsTracker() []CostakerRewardsTrackerEntr return nil } +func (m *GenesisState) GetValidatorSet() ValidatorSet { + if m != nil { + return m.ValidatorSet + } + return ValidatorSet{} +} + // CurrentRewardsEntry represents the chain current rewards. type CurrentRewardsEntry struct { // Rewards the costaker rewards @@ -269,36 +278,38 @@ func init() { } var fileDescriptor_926218777277d46b = []byte{ - // 459 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x53, 0x4f, 0x8b, 0xd3, 0x40, - 0x14, 0x4f, 0x76, 0x4b, 0x17, 0x67, 0xc5, 0xd5, 0xb1, 0xac, 0xd9, 0x2a, 0x71, 0x09, 0x0b, 0xae, - 0xb0, 0x9b, 0xd0, 0x55, 0x3c, 0x78, 0x10, 0xda, 0xe2, 0x9f, 0x63, 0x49, 0x15, 0xc4, 0x4b, 0x98, - 0x24, 0x43, 0x3a, 0xb4, 0xcd, 0x84, 0x99, 0x69, 0xb5, 0xdf, 0xc2, 0xcf, 0xe1, 0xd9, 0x0f, 0xd1, - 0x63, 0xf1, 0xe4, 0x49, 0xa4, 0xfd, 0x1a, 0x1e, 0xc4, 0xcc, 0x4c, 0xda, 0xca, 0x28, 0xdd, 0x5b, - 0x5f, 0xdf, 0xef, 0xcf, 0x7b, 0xbf, 0x97, 0x01, 0x5e, 0x8c, 0xe2, 0xd9, 0x88, 0xe6, 0x41, 0x42, - 0xb9, 0x40, 0x43, 0x92, 0x67, 0xc1, 0xb4, 0x15, 0x64, 0x38, 0xc7, 0x9c, 0x70, 0xbf, 0x60, 0x54, - 0x50, 0xd8, 0x50, 0x18, 0xbf, 0xc2, 0xf8, 0xd3, 0x56, 0xb3, 0x91, 0xd1, 0x8c, 0x96, 0x80, 0xe0, - 0xcf, 0x2f, 0x89, 0x6d, 0x9e, 0x19, 0xf5, 0xd6, 0x44, 0x89, 0x32, 0xbb, 0x32, 0xfc, 0x11, 0xb1, - 0x54, 0xb9, 0x36, 0x4f, 0x12, 0xca, 0xc7, 0x94, 0x47, 0xd2, 0x42, 0x16, 0xb2, 0xe5, 0xfd, 0xda, - 0x03, 0x37, 0x5f, 0xcb, 0x11, 0xfb, 0x02, 0x09, 0x0c, 0x9f, 0x83, 0x7a, 0x81, 0x18, 0x1a, 0x73, - 0xc7, 0x3e, 0xb5, 0xcf, 0x0f, 0xaf, 0x1e, 0xf8, 0xa6, 0x91, 0xfd, 0x5e, 0x89, 0xe9, 0xd4, 0xe6, - 0x3f, 0x1e, 0x5a, 0xa1, 0x62, 0xc0, 0xf7, 0xe0, 0x28, 0x99, 0x30, 0x86, 0x73, 0x11, 0xa9, 0x01, - 0x9c, 0xbd, 0x52, 0xe4, 0xb1, 0x59, 0xa4, 0x2b, 0xc1, 0xa1, 0xc4, 0xbe, 0xcc, 0x05, 0x9b, 0x29, - 0xc5, 0x5b, 0xc9, 0x56, 0x0b, 0x22, 0x00, 0x07, 0x84, 0x0b, 0xca, 0x48, 0x82, 0x46, 0x95, 0xf8, - 0xfe, 0xe9, 0xfe, 0xf9, 0xe1, 0xd5, 0x85, 0x59, 0xfc, 0x4d, 0x85, 0x37, 0xe8, 0xdf, 0x19, 0xfc, - 0xdd, 0x85, 0x1c, 0x9c, 0x48, 0x3e, 0x66, 0x5c, 0x3b, 0x44, 0x82, 0xa1, 0x64, 0x88, 0x99, 0x53, - 0x2b, 0x9d, 0x5a, 0xff, 0x58, 0x43, 0xd1, 0x94, 0xd2, 0x5b, 0xc9, 0xd9, 0xb4, 0xbb, 0x57, 0x29, - 0x6f, 0x63, 0xbc, 0x77, 0xe0, 0xae, 0x21, 0x04, 0xf8, 0x02, 0x1c, 0xe8, 0x1d, 0xe5, 0x15, 0xce, - 0x76, 0x09, 0x30, 0xd4, 0x24, 0x8f, 0x83, 0x63, 0xf3, 0xfa, 0xf0, 0x18, 0xd4, 0x0b, 0xcc, 0x08, - 0x4d, 0x4b, 0xe1, 0x5a, 0xa8, 0x2a, 0xd8, 0x5e, 0x3b, 0xca, 0x93, 0x3d, 0xda, 0x31, 0xd5, 0xb5, - 0xe9, 0x17, 0x1b, 0xdc, 0xff, 0x4f, 0x14, 0xb0, 0x0b, 0x6e, 0xeb, 0x18, 0x22, 0x94, 0xa6, 0x0c, - 0x73, 0xb9, 0xdd, 0x8d, 0x8e, 0xf3, 0xed, 0xeb, 0x65, 0x43, 0x7d, 0x96, 0x6d, 0xd9, 0xe9, 0x0b, - 0x46, 0xf2, 0x2c, 0x3c, 0xd2, 0x0c, 0xf5, 0x37, 0x7c, 0x05, 0x0e, 0xf4, 0x4d, 0xe4, 0x9c, 0x17, - 0xd7, 0xb9, 0x49, 0xa8, 0xc9, 0x9d, 0xde, 0x7c, 0xe9, 0xda, 0x8b, 0xa5, 0x6b, 0xff, 0x5c, 0xba, - 0xf6, 0xe7, 0x95, 0x6b, 0x2d, 0x56, 0xae, 0xf5, 0x7d, 0xe5, 0x5a, 0x1f, 0x9e, 0x65, 0x44, 0x0c, - 0x26, 0xb1, 0x9f, 0xd0, 0x71, 0xa0, 0xa4, 0x47, 0x28, 0xe6, 0x97, 0x84, 0xea, 0x32, 0x98, 0x3e, - 0x0d, 0x3e, 0x6d, 0xbc, 0x37, 0x31, 0x2b, 0x30, 0x8f, 0xeb, 0xe5, 0x83, 0x7a, 0xf2, 0x3b, 0x00, - 0x00, 0xff, 0xff, 0xbd, 0x3a, 0xda, 0xbc, 0x07, 0x04, 0x00, 0x00, + // 490 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x53, 0xcf, 0x6e, 0xd3, 0x30, + 0x18, 0x4f, 0x68, 0xe9, 0x84, 0x37, 0x18, 0x98, 0x6a, 0x64, 0x05, 0x85, 0x29, 0x9a, 0xc4, 0x90, + 0xb6, 0x44, 0x1d, 0x88, 0x03, 0x07, 0xa4, 0x75, 0xe2, 0xcf, 0x05, 0x69, 0x4a, 0x01, 0x21, 0x2e, + 0x91, 0x93, 0x58, 0xa9, 0xb5, 0x34, 0x8e, 0x6c, 0x2f, 0xd0, 0xb7, 0xe0, 0x39, 0x38, 0xf3, 0x10, + 0x93, 0xb8, 0x4c, 0x9c, 0x38, 0x21, 0xd4, 0xbe, 0x08, 0x22, 0xb6, 0xd3, 0x16, 0x19, 0xd4, 0xdd, + 0xf2, 0xe5, 0xfb, 0xfd, 0xf9, 0xfc, 0xfb, 0x6c, 0xe0, 0xc5, 0x28, 0x9e, 0xe4, 0xb4, 0x08, 0x12, + 0xca, 0x05, 0x3a, 0x25, 0x45, 0x16, 0x54, 0xfd, 0x20, 0xc3, 0x05, 0xe6, 0x84, 0xfb, 0x25, 0xa3, + 0x82, 0xc2, 0xae, 0xc2, 0xf8, 0x0d, 0xc6, 0xaf, 0xfa, 0xbd, 0x6e, 0x46, 0x33, 0x5a, 0x03, 0x82, + 0x3f, 0x5f, 0x12, 0xdb, 0xdb, 0x35, 0xea, 0xcd, 0x89, 0x12, 0x65, 0x76, 0x65, 0xf8, 0x23, 0x62, + 0xa9, 0x72, 0xed, 0x6d, 0x27, 0x94, 0x8f, 0x29, 0x8f, 0xa4, 0x85, 0x2c, 0x64, 0xcb, 0xfb, 0xd6, + 0x02, 0x1b, 0x2f, 0xe5, 0x88, 0x43, 0x81, 0x04, 0x86, 0x4f, 0x41, 0xa7, 0x44, 0x0c, 0x8d, 0xb9, + 0x63, 0xef, 0xd8, 0x7b, 0xeb, 0x87, 0xf7, 0x7c, 0xd3, 0xc8, 0xfe, 0x49, 0x8d, 0x19, 0xb4, 0xcf, + 0x7f, 0xde, 0xb7, 0x42, 0xc5, 0x80, 0xef, 0xc1, 0x66, 0x72, 0xc6, 0x18, 0x2e, 0x44, 0xa4, 0x06, + 0x70, 0xae, 0xd4, 0x22, 0x0f, 0xcd, 0x22, 0xc7, 0x12, 0x1c, 0x4a, 0xec, 0xf3, 0x42, 0xb0, 0x89, + 0x52, 0xbc, 0x91, 0x2c, 0xb5, 0x20, 0x02, 0x70, 0x44, 0xb8, 0xa0, 0x8c, 0x24, 0x28, 0x6f, 0xc4, + 0x5b, 0x3b, 0xad, 0xbd, 0xf5, 0xc3, 0x7d, 0xb3, 0xf8, 0xab, 0x06, 0x6f, 0xd0, 0xbf, 0x35, 0xfa, + 0xbb, 0x0b, 0x39, 0xd8, 0x96, 0x7c, 0xcc, 0xb8, 0x76, 0x88, 0x04, 0x43, 0xc9, 0x29, 0x66, 0x4e, + 0xbb, 0x76, 0xea, 0xff, 0xe3, 0x18, 0x8a, 0xa6, 0x94, 0xde, 0x48, 0xce, 0xa2, 0xdd, 0x9d, 0x46, + 0x79, 0x19, 0x03, 0x5f, 0x83, 0xeb, 0x15, 0xca, 0x49, 0x8a, 0x04, 0x65, 0x11, 0xc7, 0xc2, 0xb9, + 0x5a, 0xe7, 0xe5, 0x99, 0x8d, 0xde, 0x69, 0xe8, 0x10, 0x0b, 0xa5, 0xbc, 0x51, 0x2d, 0xfc, 0xf3, + 0xde, 0x82, 0xdb, 0x86, 0x4c, 0xe1, 0x33, 0xb0, 0xa6, 0x23, 0x93, 0x4b, 0xdd, 0x5d, 0x65, 0x1f, + 0xa1, 0x26, 0x79, 0x1c, 0x6c, 0x99, 0xd3, 0x84, 0x5b, 0xa0, 0x53, 0x62, 0x46, 0x68, 0x5a, 0x0b, + 0xb7, 0x43, 0x55, 0xc1, 0xa3, 0xb9, 0xa3, 0xbc, 0x01, 0x0f, 0x56, 0x5c, 0xd2, 0xdc, 0xf4, 0x8b, + 0x0d, 0xee, 0xfe, 0x27, 0x59, 0x78, 0x0c, 0x6e, 0xea, 0x54, 0x23, 0x94, 0xa6, 0x0c, 0x73, 0x79, + 0xba, 0x6b, 0x03, 0xe7, 0xfb, 0xd7, 0x83, 0xae, 0xba, 0xe5, 0x47, 0xb2, 0x33, 0x14, 0x8c, 0x14, + 0x59, 0xb8, 0xa9, 0x19, 0xea, 0x37, 0x7c, 0x01, 0xd6, 0xf4, 0x8a, 0xe5, 0x9c, 0xfb, 0x97, 0x59, + 0x71, 0xa8, 0xc9, 0x83, 0x93, 0xf3, 0xa9, 0x6b, 0x5f, 0x4c, 0x5d, 0xfb, 0xd7, 0xd4, 0xb5, 0x3f, + 0xcf, 0x5c, 0xeb, 0x62, 0xe6, 0x5a, 0x3f, 0x66, 0xae, 0xf5, 0xe1, 0x49, 0x46, 0xc4, 0xe8, 0x2c, + 0xf6, 0x13, 0x3a, 0x0e, 0x94, 0x74, 0x8e, 0x62, 0x7e, 0x40, 0xa8, 0x2e, 0x83, 0xea, 0x71, 0xf0, + 0x69, 0xe1, 0xf9, 0x8a, 0x49, 0x89, 0x79, 0xdc, 0xa9, 0xdf, 0xe7, 0xa3, 0xdf, 0x01, 0x00, 0x00, + 0xff, 0xff, 0xec, 0xd5, 0xd9, 0xac, 0x56, 0x04, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -321,6 +332,16 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + { + size, err := m.ValidatorSet.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x2a if len(m.CostakersRewardsTracker) > 0 { for iNdEx := len(m.CostakersRewardsTracker) - 1; iNdEx >= 0; iNdEx-- { { @@ -522,6 +543,8 @@ func (m *GenesisState) Size() (n int) { n += 1 + l + sovGenesis(uint64(l)) } } + l = m.ValidatorSet.Size() + n += 1 + l + sovGenesis(uint64(l)) return n } @@ -740,6 +763,39 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ValidatorSet", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.ValidatorSet.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/x/costaking/types/keys.go b/x/costaking/types/keys.go index 2b5ccc5c7..b2353ae0d 100644 --- a/x/costaking/types/keys.go +++ b/x/costaking/types/keys.go @@ -20,4 +20,5 @@ var ( HistoricalRewardsKeyPrefix = collections.NewPrefix(2) // key prefix for (period) => HistoricalRewards CurrentRewardsKeyPrefix = collections.NewPrefix(3) // key prefix for CurrentRewards CostakerRewardsTrackerKeyPrefix = collections.NewPrefix(4) // key prefix for (costaker_addr) => CostakerRewardsTracker + ValidatorsKeyPrefix = collections.NewPrefix(5) // key prefix for validator set ) diff --git a/x/costaking/types/mocked_keepers.go b/x/costaking/types/mocked_keepers.go index 309cdb714..5a04c8ba5 100644 --- a/x/costaking/types/mocked_keepers.go +++ b/x/costaking/types/mocked_keepers.go @@ -240,19 +240,48 @@ func (mr *MockStakingKeeperMockRecorder) GetDelegation(ctx, delAddr, valAddr int return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDelegation", reflect.TypeOf((*MockStakingKeeper)(nil).GetDelegation), ctx, delAddr, valAddr) } -// Validator mocks base method. -func (m *MockStakingKeeper) Validator(arg0 context.Context, arg1 types.ValAddress) (types0.ValidatorI, error) { +// GetValidator mocks base method. +func (m *MockStakingKeeper) GetValidator(ctx context.Context, addr types.ValAddress) (types0.Validator, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Validator", arg0, arg1) - ret0, _ := ret[0].(types0.ValidatorI) + ret := m.ctrl.Call(m, "GetValidator", ctx, addr) + ret0, _ := ret[0].(types0.Validator) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetValidator indicates an expected call of GetValidator. +func (mr *MockStakingKeeperMockRecorder) GetValidator(ctx, addr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidator", reflect.TypeOf((*MockStakingKeeper)(nil).GetValidator), ctx, addr) +} + +// GetValidatorDelegations mocks base method. +func (m *MockStakingKeeper) GetValidatorDelegations(ctx context.Context, valAddr types.ValAddress) ([]types0.Delegation, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetValidatorDelegations", ctx, valAddr) + ret0, _ := ret[0].([]types0.Delegation) ret1, _ := ret[1].(error) return ret0, ret1 } -// Validator indicates an expected call of Validator. -func (mr *MockStakingKeeperMockRecorder) Validator(arg0, arg1 interface{}) *gomock.Call { +// GetValidatorDelegations indicates an expected call of GetValidatorDelegations. +func (mr *MockStakingKeeperMockRecorder) GetValidatorDelegations(ctx, valAddr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatorDelegations", reflect.TypeOf((*MockStakingKeeper)(nil).GetValidatorDelegations), ctx, valAddr) +} + +// IterateLastValidatorPowers mocks base method. +func (m *MockStakingKeeper) IterateLastValidatorPowers(ctx context.Context, handler func(types.ValAddress, int64) bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IterateLastValidatorPowers", ctx, handler) + ret0, _ := ret[0].(error) + return ret0 +} + +// IterateLastValidatorPowers indicates an expected call of IterateLastValidatorPowers. +func (mr *MockStakingKeeperMockRecorder) IterateLastValidatorPowers(ctx, handler interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validator", reflect.TypeOf((*MockStakingKeeper)(nil).Validator), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IterateLastValidatorPowers", reflect.TypeOf((*MockStakingKeeper)(nil).IterateLastValidatorPowers), ctx, handler) } // ValidatorByConsAddr mocks base method. diff --git a/x/costaking/types/rewards.go b/x/costaking/types/rewards.go index 17b75ba1a..d58ae9f90 100644 --- a/x/costaking/types/rewards.go +++ b/x/costaking/types/rewards.go @@ -98,3 +98,11 @@ func (crt CostakerRewardsTracker) Validate() error { } return nil } + +func (crt *CostakerRewardsTracker) Sanitize() { + // Handle the case where ActiveBaby is -1 due to rounding when the validator + // lost its 1:1 ratio between shares and tokens due to slashing + if crt.ActiveBaby.Equal(sdkmath.NewInt(-1)) { + crt.ActiveBaby = sdkmath.ZeroInt() + } +} diff --git a/x/costaking/types/rewards_test.go b/x/costaking/types/rewards_test.go new file mode 100644 index 000000000..b83157cb9 --- /dev/null +++ b/x/costaking/types/rewards_test.go @@ -0,0 +1,100 @@ +package types + +import ( + "testing" + + sdkmath "cosmossdk.io/math" + "github.com/stretchr/testify/require" +) + +func TestCostakerRewardsTracker_Sanitize(t *testing.T) { + testCases := []struct { + name string + input CostakerRewardsTracker + expectedOutput CostakerRewardsTracker + }{ + { + name: "ActiveBaby is -1, should be set to 0", + input: CostakerRewardsTracker{ + StartPeriodCumulativeReward: 1, + ActiveSatoshis: sdkmath.NewInt(1000), + ActiveBaby: sdkmath.NewInt(-1), + TotalScore: sdkmath.NewInt(500), + }, + expectedOutput: CostakerRewardsTracker{ + StartPeriodCumulativeReward: 1, + ActiveSatoshis: sdkmath.NewInt(1000), + ActiveBaby: sdkmath.ZeroInt(), + TotalScore: sdkmath.NewInt(500), + }, + }, + { + name: "ActiveBaby is 0, should remain 0", + input: CostakerRewardsTracker{ + StartPeriodCumulativeReward: 1, + ActiveSatoshis: sdkmath.NewInt(1000), + ActiveBaby: sdkmath.ZeroInt(), + TotalScore: sdkmath.NewInt(500), + }, + expectedOutput: CostakerRewardsTracker{ + StartPeriodCumulativeReward: 1, + ActiveSatoshis: sdkmath.NewInt(1000), + ActiveBaby: sdkmath.ZeroInt(), + TotalScore: sdkmath.NewInt(500), + }, + }, + { + name: "ActiveBaby is positive, should remain unchanged", + input: CostakerRewardsTracker{ + StartPeriodCumulativeReward: 1, + ActiveSatoshis: sdkmath.NewInt(1000), + ActiveBaby: sdkmath.NewInt(100), + TotalScore: sdkmath.NewInt(500), + }, + expectedOutput: CostakerRewardsTracker{ + StartPeriodCumulativeReward: 1, + ActiveSatoshis: sdkmath.NewInt(1000), + ActiveBaby: sdkmath.NewInt(100), + TotalScore: sdkmath.NewInt(500), + }, + }, + { + name: "ActiveBaby is -2, should remain -2 (only -1 is sanitized)", + input: CostakerRewardsTracker{ + StartPeriodCumulativeReward: 1, + ActiveSatoshis: sdkmath.NewInt(1000), + ActiveBaby: sdkmath.NewInt(-2), + TotalScore: sdkmath.NewInt(500), + }, + expectedOutput: CostakerRewardsTracker{ + StartPeriodCumulativeReward: 1, + ActiveSatoshis: sdkmath.NewInt(1000), + ActiveBaby: sdkmath.NewInt(-2), + TotalScore: sdkmath.NewInt(500), + }, + }, + { + name: "All fields are zero, should remain unchanged", + input: CostakerRewardsTracker{ + StartPeriodCumulativeReward: 0, + ActiveSatoshis: sdkmath.ZeroInt(), + ActiveBaby: sdkmath.ZeroInt(), + TotalScore: sdkmath.ZeroInt(), + }, + expectedOutput: CostakerRewardsTracker{ + StartPeriodCumulativeReward: 0, + ActiveSatoshis: sdkmath.ZeroInt(), + ActiveBaby: sdkmath.ZeroInt(), + TotalScore: sdkmath.ZeroInt(), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tracker := tc.input + tracker.Sanitize() + require.Equal(t, tc.expectedOutput, tracker) + }) + } +} diff --git a/x/costaking/types/staking_cache.go b/x/costaking/types/staking_cache.go index 8ad7f45b3..ad124cc38 100644 --- a/x/costaking/types/staking_cache.go +++ b/x/costaking/types/staking_cache.go @@ -1,6 +1,8 @@ package types import ( + context "context" + "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -9,58 +11,128 @@ import ( // BeforeDelegationSharesModified sets the value and // AfterDelegationModified to calculate the delta change. type StakingCache struct { - // amtByValByDel stores the amount it had before the delegation - // was modified DelAddr => ValAddr => Amt - amtByValByDel map[string]map[string]math.LegacyDec + // stkInfoByValByDel stores the amount and shares it had before the delegation + // was modified DelAddr => ValAddr => StakeInfo (amount, shares) + stkInfoByValByDel map[string]map[string]StakeInfo + // activeValSet caches the current active validator set map + // ValAddr => Tokens + activeValSet map[string]ValidatorInfo +} + +type StakeInfo struct { + Amount math.LegacyDec + Shares math.LegacyDec +} + +type ValidatorInfo struct { + ValAddress sdk.ValAddress + OriginalTokens math.Int + OriginalShares math.LegacyDec + CurrentTokens math.Int + IsSlashed bool + DeltaSharesPerDelegator map[string][]math.LegacyDec // DelAddrStr => []DeltaShares +} + +var zeroStakeInfo = StakeInfo{ + Amount: math.LegacyZeroDec(), + Shares: math.LegacyZeroDec(), } func NewStakingCache() *StakingCache { return &StakingCache{ - amtByValByDel: make(map[string]map[string]math.LegacyDec), + stkInfoByValByDel: make(map[string]map[string]StakeInfo), } } -func (sc *StakingCache) SetStakedAmount(delAddr sdk.AccAddress, valAddr sdk.ValAddress, amtStaked math.LegacyDec) { +func (sc *StakingCache) SetStakedInfo(delAddr sdk.AccAddress, valAddr sdk.ValAddress, amtStaked math.LegacyDec, delShares math.LegacyDec) { delAddrStr := delAddr.String() valAddrStr := valAddr.String() - if sc.amtByValByDel[delAddrStr] == nil { - sc.amtByValByDel[delAddrStr] = make(map[string]math.LegacyDec) + if sc.stkInfoByValByDel[delAddrStr] == nil { + sc.stkInfoByValByDel[delAddrStr] = make(map[string]StakeInfo) + } + sc.stkInfoByValByDel[delAddrStr][valAddrStr] = StakeInfo{ + Amount: amtStaked, + Shares: delShares, } - sc.amtByValByDel[delAddrStr][valAddrStr] = amtStaked } -// GetStakedAmount gets the value in the cache if it is found. +// GetStakedInfo gets the value in the cache if it is found. // Note: If a value is not found it returns zero dec. -func (sc *StakingCache) GetStakedAmount(delAddr sdk.AccAddress, valAddr sdk.ValAddress) math.LegacyDec { +func (sc *StakingCache) GetStakedInfo(delAddr sdk.AccAddress, valAddr sdk.ValAddress) StakeInfo { delAddrStr := delAddr.String() valAddrStr := valAddr.String() - if sc.amtByValByDel[delAddrStr] == nil { - return math.LegacyZeroDec() + if sc.stkInfoByValByDel[delAddrStr] == nil { + return zeroStakeInfo } - amt, found := sc.amtByValByDel[delAddrStr][valAddrStr] + info, found := sc.stkInfoByValByDel[delAddrStr][valAddrStr] if !found { - return math.LegacyZeroDec() + return zeroStakeInfo } - return amt + return info +} + +// GetActiveValidatorSet returns the cached active validator set, fetching it if not present +func (sc *StakingCache) GetActiveValidatorSet(ctx context.Context, fetchFn func(ctx context.Context) (map[string]ValidatorInfo, error)) (map[string]ValidatorInfo, error) { + if sc.activeValSet != nil { + return sc.activeValSet, nil + } + + valSet, err := fetchFn(ctx) + if err != nil { + return nil, err + } + + sc.activeValSet = valSet + return sc.activeValSet, nil +} + +func (sc *StakingCache) AddDeltaShares(valAddr sdk.ValAddress, delAddr sdk.AccAddress, deltaShares math.LegacyDec) { + valInfo, ok := sc.activeValSet[valAddr.String()] + if !ok { + return + } + + if valInfo.DeltaSharesPerDelegator == nil { + valInfo.DeltaSharesPerDelegator = make(map[string][]math.LegacyDec) + } + + delAddrStr := delAddr.String() + valInfo.DeltaSharesPerDelegator[delAddrStr] = append(valInfo.DeltaSharesPerDelegator[delAddrStr], deltaShares) + sc.activeValSet[valAddr.String()] = valInfo +} + +func (sc *StakingCache) GetDeltaShares(valAddr sdk.ValAddress, delAddr sdk.AccAddress) []math.LegacyDec { + valInfo, ok := sc.activeValSet[valAddr.String()] + if !ok { + return nil + } + + if valInfo.DeltaSharesPerDelegator == nil { + return nil + } + + delAddrStr := delAddr.String() + return valInfo.DeltaSharesPerDelegator[delAddrStr] } // Clear removes all entries from the cache func (sc *StakingCache) Clear() { - sc.amtByValByDel = make(map[string]map[string]math.LegacyDec) + sc.stkInfoByValByDel = make(map[string]map[string]StakeInfo) + sc.activeValSet = nil } // Delete removes one entry from the cache func (sc *StakingCache) Delete(delAddr sdk.AccAddress, valAddr sdk.ValAddress) { delAddrStr := delAddr.String() - _, exists := sc.amtByValByDel[delAddrStr] + _, exists := sc.stkInfoByValByDel[delAddrStr] if !exists { return } valAddrStr := valAddr.String() - delete(sc.amtByValByDel[delAddrStr], valAddrStr) + delete(sc.stkInfoByValByDel[delAddrStr], valAddrStr) } diff --git a/x/costaking/types/staking_cache_test.go b/x/costaking/types/staking_cache_test.go index c78ca1fa2..bcf006cd0 100644 --- a/x/costaking/types/staking_cache_test.go +++ b/x/costaking/types/staking_cache_test.go @@ -11,7 +11,7 @@ import ( func TestNewStakingCacheIsNotNil(t *testing.T) { cache := NewStakingCache() require.NotNil(t, cache) - require.NotNil(t, cache.amtByValByDel) + require.NotNil(t, cache.stkInfoByValByDel) } func TestStakingCacheSetAndGetAndDeleteStakedAmount(t *testing.T) { @@ -27,40 +27,40 @@ func TestStakingCacheSetAndGetAndDeleteStakedAmount(t *testing.T) { amount3 := math.LegacyNewDec(300) // not found - result := cache.GetStakedAmount(delAddr1, valAddr1) - require.True(t, result.IsZero()) + result := cache.GetStakedInfo(delAddr1, valAddr1) + require.True(t, result.Amount.IsZero()) - cache.SetStakedAmount(delAddr1, valAddr1, amount1) - cache.SetStakedAmount(delAddr1, valAddr2, amount2) - cache.SetStakedAmount(delAddr2, valAddr1, amount3) + cache.SetStakedInfo(delAddr1, valAddr1, amount1, amount1) + cache.SetStakedInfo(delAddr1, valAddr2, amount2, amount2) + cache.SetStakedInfo(delAddr2, valAddr1, amount3, amount3) // Get and delete values, verifying they return correct amounts - result1 := cache.GetStakedAmount(delAddr1, valAddr1) - require.True(t, amount1.Equal(result1)) + result1 := cache.GetStakedInfo(delAddr1, valAddr1) + require.True(t, amount1.Equal(result1.Amount)) - result2 := cache.GetStakedAmount(delAddr1, valAddr2) - require.True(t, amount2.Equal(result2)) + result2 := cache.GetStakedInfo(delAddr1, valAddr2) + require.True(t, amount2.Equal(result2.Amount)) - result3 := cache.GetStakedAmount(delAddr2, valAddr1) - require.True(t, amount3.Equal(result3)) + result3 := cache.GetStakedInfo(delAddr2, valAddr1) + require.True(t, amount3.Equal(result3.Amount)) cache.Clear() // Verify all values are deleted (should return zero) - result = cache.GetStakedAmount(delAddr1, valAddr1) - require.True(t, result.IsZero()) + result = cache.GetStakedInfo(delAddr1, valAddr1) + require.True(t, result.Amount.IsZero()) - result = cache.GetStakedAmount(delAddr1, valAddr2) - require.True(t, result.IsZero()) + result = cache.GetStakedInfo(delAddr1, valAddr2) + require.True(t, result.Amount.IsZero()) - result = cache.GetStakedAmount(delAddr2, valAddr1) - require.True(t, result.IsZero()) + result = cache.GetStakedInfo(delAddr2, valAddr1) + require.True(t, result.Amount.IsZero()) - cache.SetStakedAmount(delAddr1, valAddr1, amount1) - cache.SetStakedAmount(delAddr1, valAddr1, amount2) + cache.SetStakedInfo(delAddr1, valAddr1, amount1, amount1) + cache.SetStakedInfo(delAddr1, valAddr1, amount2, amount2) // overwrite - result2 = cache.GetStakedAmount(delAddr1, valAddr1) - require.True(t, amount2.Equal(result2)) + result2 = cache.GetStakedInfo(delAddr1, valAddr1) + require.True(t, amount2.Equal(result2.Amount)) } func TestStakingCacheGetAndDeleteStakedAmountNilMap(t *testing.T) { @@ -71,10 +71,10 @@ func TestStakingCacheGetAndDeleteStakedAmountNilMap(t *testing.T) { // Test with manually setting nil map (edge case) delAddrStr := delAddr.String() - cache.amtByValByDel[delAddrStr] = nil + cache.stkInfoByValByDel[delAddrStr] = nil - result := cache.GetStakedAmount(delAddr, valAddr) - require.True(t, result.IsZero()) + result := cache.GetStakedInfo(delAddr, valAddr) + require.True(t, result.Amount.IsZero()) } func TestStakingCacheGetAndDeleteStakedAmountPreservesOtherValidators(t *testing.T) { @@ -90,19 +90,19 @@ func TestStakingCacheGetAndDeleteStakedAmountPreservesOtherValidators(t *testing amount3 := math.LegacyNewDec(300) // Set up multiple validators for the same delegator - cache.SetStakedAmount(delAddr, valAddr1, amount1) - cache.SetStakedAmount(delAddr, valAddr2, amount2) - cache.SetStakedAmount(delAddr, valAddr3, amount3) + cache.SetStakedInfo(delAddr, valAddr1, amount1, amount1) + cache.SetStakedInfo(delAddr, valAddr2, amount2, amount2) + cache.SetStakedInfo(delAddr, valAddr3, amount3, amount3) - result := cache.GetStakedAmount(delAddr, valAddr2) - require.True(t, amount2.Equal(result)) + result := cache.GetStakedInfo(delAddr, valAddr2) + require.True(t, amount2.Equal(result.Amount)) // check again the value - result = cache.GetStakedAmount(delAddr, valAddr2) - require.True(t, amount2.Equal(result)) + result = cache.GetStakedInfo(delAddr, valAddr2) + require.True(t, amount2.Equal(result.Amount)) // Verify the delegator's map still exists and contains the correct validators delAddrStr := delAddr.String() - valMap, exists := cache.amtByValByDel[delAddrStr] + valMap, exists := cache.stkInfoByValByDel[delAddrStr] require.True(t, exists) require.Equal(t, 3, len(valMap)) @@ -122,7 +122,7 @@ func TestStakingCacheGetAndDeleteStakedAmountPreservesOtherValidators(t *testing cache.Clear() // Verify the delegator's map was cleaned up - _, exists = cache.amtByValByDel[delAddrStr] + _, exists = cache.stkInfoByValByDel[delAddrStr] require.False(t, exists) } @@ -133,15 +133,15 @@ func TestStakingCacheClear(t *testing.T) { valAddr := sdk.ValAddress("valAddr") amount3 := math.LegacyNewDec(300) - cache.SetStakedAmount(delAddr, valAddr, amount3) + cache.SetStakedInfo(delAddr, valAddr, amount3, amount3) - result := cache.GetStakedAmount(delAddr, valAddr) - require.True(t, result.Equal(amount3)) + result := cache.GetStakedInfo(delAddr, valAddr) + require.True(t, amount3.Equal(result.Amount)) cache.Clear() - result = cache.GetStakedAmount(delAddr, valAddr) - require.True(t, result.IsZero()) + result = cache.GetStakedInfo(delAddr, valAddr) + require.True(t, result.Amount.IsZero()) } func TestStakingCacheDelete(t *testing.T) { @@ -151,15 +151,17 @@ func TestStakingCacheDelete(t *testing.T) { valAddr := sdk.ValAddress("valAddr") amount := math.LegacyNewDec(100) - cache.SetStakedAmount(delAddr, valAddr, amount) + cache.SetStakedInfo(delAddr, valAddr, amount, amount) - result := cache.GetStakedAmount(delAddr, valAddr) - require.True(t, result.Equal(amount)) + result := cache.GetStakedInfo(delAddr, valAddr) + require.True(t, amount.Equal(result.Amount)) + require.True(t, amount.Equal(result.Shares)) cache.Delete(delAddr, valAddr) - result = cache.GetStakedAmount(delAddr, valAddr) - require.True(t, result.IsZero()) + result = cache.GetStakedInfo(delAddr, valAddr) + require.True(t, result.Amount.IsZero()) + require.True(t, result.Shares.IsZero()) } func TestStakingCacheDeleteNonExistentDelegator(t *testing.T) { @@ -170,8 +172,9 @@ func TestStakingCacheDeleteNonExistentDelegator(t *testing.T) { cache.Delete(delAddr, valAddr) - result := cache.GetStakedAmount(delAddr, valAddr) - require.True(t, result.IsZero()) + result := cache.GetStakedInfo(delAddr, valAddr) + require.True(t, result.Amount.IsZero()) + require.True(t, result.Shares.IsZero()) } func TestStakingCacheDeleteNonExistentValidator(t *testing.T) { @@ -182,12 +185,13 @@ func TestStakingCacheDeleteNonExistentValidator(t *testing.T) { valAddr2 := sdk.ValAddress("valAddr2") amount := math.LegacyNewDec(100) - cache.SetStakedAmount(delAddr, valAddr1, amount) + cache.SetStakedInfo(delAddr, valAddr1, amount, amount) cache.Delete(delAddr, valAddr2) - result := cache.GetStakedAmount(delAddr, valAddr1) - require.True(t, result.Equal(amount)) + result := cache.GetStakedInfo(delAddr, valAddr1) + require.True(t, amount.Equal(result.Amount)) + require.True(t, amount.Equal(result.Shares)) } func TestStakingCacheDeletePreservesOtherValidators(t *testing.T) { @@ -202,18 +206,21 @@ func TestStakingCacheDeletePreservesOtherValidators(t *testing.T) { amount2 := math.LegacyNewDec(200) amount3 := math.LegacyNewDec(300) - cache.SetStakedAmount(delAddr, valAddr1, amount1) - cache.SetStakedAmount(delAddr, valAddr2, amount2) - cache.SetStakedAmount(delAddr, valAddr3, amount3) + cache.SetStakedInfo(delAddr, valAddr1, amount1, amount1) + cache.SetStakedInfo(delAddr, valAddr2, amount2, amount2) + cache.SetStakedInfo(delAddr, valAddr3, amount3, amount3) cache.Delete(delAddr, valAddr2) - result1 := cache.GetStakedAmount(delAddr, valAddr1) - require.True(t, result1.Equal(amount1)) + result1 := cache.GetStakedInfo(delAddr, valAddr1) + require.True(t, amount1.Equal(result1.Amount)) + require.True(t, amount1.Equal(result1.Shares)) - result2 := cache.GetStakedAmount(delAddr, valAddr2) - require.True(t, result2.IsZero()) + result2 := cache.GetStakedInfo(delAddr, valAddr2) + require.True(t, result2.Amount.IsZero()) + require.True(t, result2.Shares.IsZero()) - result3 := cache.GetStakedAmount(delAddr, valAddr3) - require.True(t, result3.Equal(amount3)) + result3 := cache.GetStakedInfo(delAddr, valAddr3) + require.True(t, amount3.Equal(result3.Amount)) + require.True(t, amount3.Equal(result3.Shares)) } diff --git a/x/epoching/abci.go b/x/epoching/abci.go index b2f076be1..b2e213ae8 100644 --- a/x/epoching/abci.go +++ b/x/epoching/abci.go @@ -82,6 +82,9 @@ func EndBlocker(ctx context.Context, k keeper.Keeper) ([]abci.ValidatorUpdate, e // if reaching an epoch boundary, then epoch := k.GetEpoch(ctx) if epoch.IsLastBlock(ctx) { + // trigger BeforeEpochEnds hook + k.BeforeEpochEnds(ctx, epoch.EpochNumber) + // finalise this epoch, i.e., record the current header and the Merkle root of all AppHashs in this epoch if err := k.RecordLastHeaderTime(ctx); err != nil { return nil, err diff --git a/x/epoching/keeper/hooks.go b/x/epoching/keeper/hooks.go index 2d9af7eca..331e1e7de 100644 --- a/x/epoching/keeper/hooks.go +++ b/x/epoching/keeper/hooks.go @@ -20,6 +20,13 @@ func (k Keeper) AfterEpochBegins(ctx context.Context, epoch uint64) { } } +// BeforeEpochEnds - call hook if registered +func (k Keeper) BeforeEpochEnds(ctx context.Context, epoch uint64) { + if k.hooks != nil { + k.hooks.BeforeEpochEnds(ctx, epoch) + } +} + // AfterEpochEnds - call hook if registered func (k Keeper) AfterEpochEnds(ctx context.Context, epoch uint64) { if k.hooks != nil { diff --git a/x/epoching/types/expected_keepers.go b/x/epoching/types/expected_keepers.go index 4ff4e37c3..b7edcebb0 100644 --- a/x/epoching/types/expected_keepers.go +++ b/x/epoching/types/expected_keepers.go @@ -63,6 +63,7 @@ type StakingKeeper interface { // EpochingHooks event hooks for epoching validator object (noalias) type EpochingHooks interface { AfterEpochBegins(ctx context.Context, epoch uint64) // Must be called after an epoch begins + BeforeEpochEnds(ctx context.Context, epoch uint64) // Must be called before an epoch ends AfterEpochEnds(ctx context.Context, epoch uint64) // Must be called after an epoch ends BeforeSlashThreshold(ctx context.Context, valSet ValidatorSet) // Must be called before a certain threshold (1/3 or 2/3) of validators are slashed in a single epoch } diff --git a/x/epoching/types/hooks.go b/x/epoching/types/hooks.go index ce921259e..eeb33b9f9 100644 --- a/x/epoching/types/hooks.go +++ b/x/epoching/types/hooks.go @@ -19,6 +19,12 @@ func (h MultiEpochingHooks) AfterEpochBegins(ctx context.Context, epoch uint64) } } +func (h MultiEpochingHooks) BeforeEpochEnds(ctx context.Context, epoch uint64) { + for i := range h { + h[i].BeforeEpochEnds(ctx, epoch) + } +} + func (h MultiEpochingHooks) AfterEpochEnds(ctx context.Context, epoch uint64) { for i := range h { h[i].AfterEpochEnds(ctx, epoch) diff --git a/x/monitor/keeper/hooks.go b/x/monitor/keeper/hooks.go index 5ae903328..c99ad6b42 100644 --- a/x/monitor/keeper/hooks.go +++ b/x/monitor/keeper/hooks.go @@ -27,6 +27,8 @@ func (h Hooks) AfterEpochEnds(ctx context.Context, epoch uint64) { h.k.updateBtcLightClientHeightForEpoch(ctx, epoch) } +func (h Hooks) BeforeEpochEnds(ctx context.Context, epoch uint64) {} + func (h Hooks) BeforeSlashThreshold(ctx context.Context, valSet etypes.ValidatorSet) {} func (h Hooks) AfterBlsKeyRegistered(ctx context.Context, valAddr sdk.ValAddress) error {