diff --git a/app/testdata/upgrade_v68_offline_source_test.go b/app/testdata/upgrade_v68_offline_source_test.go index 1a37886a41..6e37c0aca1 100644 --- a/app/testdata/upgrade_v68_offline_source_test.go +++ b/app/testdata/upgrade_v68_offline_source_test.go @@ -22,6 +22,7 @@ import ( vestingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/vesting/types" banktypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/bank/types" govtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" + stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" upgradetypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/upgrade/types" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" @@ -60,6 +61,7 @@ func TestV68OfflineUpgradeSource(t *testing.T) { seedV68IBCProposal(t, testApp, ctx, &retained) seedV68UpgradedIBCState(t, testApp, ctx, &retained) seedV68Voucher(t, testApp, ctx, &retained) + seedV68OfflineDelegations(testApp, ctx) requireV68OfflineBalancesLocked(t, testApp, ctx, retained) upgradeHeight := ctx.BlockHeight() + 2 require.NoError(t, testApp.UpgradeKeeper.ScheduleUpgrade(ctx, upgradetypes.Plan{ @@ -204,6 +206,20 @@ func seedV68Voucher(t *testing.T, testApp *App, ctx sdk.Context, retained *offli retained.VoucherSupply = testApp.BankKeeper.GetSupply(ctx, voucher.Denom).Amount.String() } +// seedV68OfflineDelegations stores delegations to two validators, which v6.7 +// writes without any delegation-by-validator index entry. +func seedV68OfflineDelegations(testApp *App, ctx sdk.Context) { + validators := []sdk.ValAddress{ + sdk.ValAddress(secp256k1.GenPrivKey().PubKey().Address()), + sdk.ValAddress(secp256k1.GenPrivKey().PubKey().Address()), + } + for i := 0; i < 3; i++ { + delegator := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + testApp.StakingKeeper.SetDelegation(ctx, + stakingtypes.NewDelegation(delegator, validators[i%len(validators)], sdk.NewDec(int64(i+1)))) + } +} + // seedV68OfflineVestingAccounts writes one account of every vesting type // through the v6.7 keepers, each keyed and holding a balance its schedule // locks. The delayed vesting account is recorded as the sender of a diff --git a/app/upgrade_v68_offline_target_test.go b/app/upgrade_v68_offline_target_test.go index 9ebe5e9b97..e2a18c4215 100644 --- a/app/upgrade_v68_offline_target_test.go +++ b/app/upgrade_v68_offline_target_test.go @@ -23,6 +23,7 @@ import ( distrtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/distribution/types" govtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" paramstypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/params/types" + stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" upgradetypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/upgrade/types" "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/memiavl" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" @@ -137,7 +138,8 @@ func applyV68OfflineUpgradeCrashReplay(t *testing.T, root string, artifact offli // accounts are rewritten, the retired IBC proposal reads back as a text // proposal, the upgraded IBC client record, the denom allowlists and the // tokenfactory parameters are pruned, the IBC voucher balance and supply are -// intact and spendable, and nothing else in the retained stores changed. +// intact and spendable, every delegation is indexed by validator, and nothing +// else in the retained stores changed. func requireV68OfflineMigrated(t *testing.T, testApp *App, artifact offlineUpgradeArtifact) { t.Helper() requireV68OfflineAppliedName(t, testApp, artifact) @@ -149,6 +151,7 @@ func requireV68OfflineMigrated(t *testing.T, testApp *App, artifact offlineUpgra requireV68OfflineDenomAllowListsPruned(t, testApp, artifact.Stores[banktypes.StoreKey]) requireV68OfflineTokenfactoryParamsPruned(t, testApp, artifact.Stores[paramstypes.StoreKey]) requireV68OfflineVoucher(t, testApp, artifact.Retained) + requireV68OfflineDelegationIndex(t, testApp) requireOfflineUpgradeRetainedStoresExcept(t, testApp, artifact.Stores, v68OfflineTouchedKey(t, artifact)) } @@ -281,6 +284,31 @@ func requireV68OfflineStoresDeleted(t *testing.T, testApp *App) { } } +// requireV68OfflineDelegationIndex requires the delegation-by-validator index +// to be ready and to hold exactly one entry per stored delegation. +func requireV68OfflineDelegationIndex(t *testing.T, testApp *App) { + t.Helper() + ctx := offlineUpgradeReadContext(testApp, testApp.LastBlockHeight()) + require.True(t, testApp.StakingKeeper.DelegationByValIndexReady(ctx)) + store := ctx.KVStore(testApp.GetKey(stakingtypes.StoreKey)) + + delegations := map[string]struct{}{} + iterator := sdk.KVStorePrefixIterator(store, stakingtypes.DelegationKey) + for ; iterator.Valid(); iterator.Next() { + delegations[string(iterator.Key())] = struct{}{} + } + require.NoError(t, iterator.Close()) + require.NotEmpty(t, delegations, "the source phase stored no delegations") + + indexed := map[string]struct{}{} + iterator = sdk.KVStorePrefixIterator(store, stakingtypes.DelegationByValIndexKey) + for ; iterator.Valid(); iterator.Next() { + indexed[string(stakingtypes.GetDelegationKeyFromValIndexKey(iterator.Key()))] = struct{}{} + } + require.NoError(t, iterator.Close()) + require.Equal(t, delegations, indexed, "the index does not hold exactly one entry per delegation") +} + func requireV68OfflineProposalRewritten(t *testing.T, testApp *App, retained offlineUpgradeRetainedState) { t.Helper() ctx := offlineUpgradeReadContext(testApp, testApp.LastBlockHeight()) diff --git a/app/upgrade_v68_test.go b/app/upgrade_v68_test.go index a10969a2bd..41ec049db2 100644 --- a/app/upgrade_v68_test.go +++ b/app/upgrade_v68_test.go @@ -23,6 +23,9 @@ import ( banktypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/bank/types" govtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" paramstypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/params/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking" + stakingkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/keeper" + stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" upgradetypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/upgrade/types" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/testutil/processblock" @@ -41,7 +44,9 @@ import ( // base accounts they embed. These tests cover the rewrite of every vesting // account type, spending a balance a schedule had locked, the messages the // removed modules served, the allowlist and parameter pruning, genesis export -// over the migrated store, and the handler itself. +// over the migrated store, and the handler itself. The handler also starts the +// staking delegation-by-validator index backfill, which the staking EndBlocker +// completes. // v68RemovedStoreModules are the module version map entries the v6.8 handler // deletes along with their stores. @@ -331,6 +336,87 @@ func TestV68ApplyUpgradeTwice(t *testing.T) { "second ApplyUpgrade is not a no-op: ApplyUpgrade increments protocol version on every call") } +// unreadyV68DelegationIndex deletes the delegation-by-validator index and its +// markers, leaving the staking store as v6.7 left it. +func unreadyV68DelegationIndex(t *testing.T, a *processblock.App) { + t.Helper() + store := a.Ctx().KVStore(a.GetKey(stakingtypes.StoreKey)) + iterator := sdk.KVStorePrefixIterator(store, stakingtypes.DelegationByValIndexKey) + var keys [][]byte + for ; iterator.Valid(); iterator.Next() { + keys = append(keys, iterator.Key()) + } + require.NoError(t, iterator.Close()) + for _, key := range keys { + store.Delete(key) + } + store.Delete(stakingtypes.DelegationByValIndexReadyKey) + store.Delete(stakingtypes.DelegationByValIndexCursorKey) +} + +// v68DelegationKeys returns the keys of every stored delegation. +func v68DelegationKeys(t *testing.T, store sdk.KVStore) map[string]struct{} { + t.Helper() + return v68PrefixKeys(t, store, stakingtypes.DelegationKey, func(key []byte) []byte { return key }) +} + +// v68IndexedDelegationKeys returns the delegation key every index entry points at. +func v68IndexedDelegationKeys(t *testing.T, store sdk.KVStore) map[string]struct{} { + t.Helper() + return v68PrefixKeys(t, store, stakingtypes.DelegationByValIndexKey, stakingtypes.GetDelegationKeyFromValIndexKey) +} + +func v68PrefixKeys(t *testing.T, store sdk.KVStore, prefix []byte, resolve func([]byte) []byte) map[string]struct{} { + t.Helper() + iterator := sdk.KVStorePrefixIterator(store, prefix) + defer func() { require.NoError(t, iterator.Close()) }() + keys := map[string]struct{}{} + for ; iterator.Valid(); iterator.Next() { + keys[string(resolve(iterator.Key()))] = struct{}{} + } + return keys +} + +func TestV68PopulatesDelegationByValIndex(t *testing.T) { + a := newV68Chain(t) + unreadyV68DelegationIndex(t, a) + ctx := a.Ctx() + store := ctx.KVStore(a.GetKey(stakingtypes.StoreKey)) + + validators := a.StakingKeeper.GetAllValidators(ctx) + require.NotEmpty(t, validators) + valAddr := validators[0].GetOperator() + delAddr := a.NewAccount() + a.StakingKeeper.SetDelegation(ctx, stakingtypes.NewDelegation(delAddr, valAddr, sdk.NewDec(68))) + require.False(t, store.Has(stakingtypes.GetDelegationByValIndexKey(delAddr, valAddr)), + "a delegation written before v6.8 is indexed") + + applyV68(t, a) + + require.False(t, a.StakingKeeper.DelegationByValIndexReady(ctx), + "v6.8 populated the index inside the upgrade handler") + require.True(t, store.Has(stakingtypes.DelegationByValIndexCursorKey), + "v6.8 did not start the index backfill") + staking.EndBlocker(ctx, a.StakingKeeper) + + require.True(t, a.StakingKeeper.DelegationByValIndexReady(ctx)) + require.False(t, store.Has(stakingtypes.DelegationByValIndexCursorKey)) + delegations := v68DelegationKeys(t, store) + require.Contains(t, delegations, string(stakingtypes.GetDelegationKey(delAddr, valAddr))) + require.Equal(t, delegations, v68IndexedDelegationKeys(t, store), + "the index does not hold exactly one entry per delegation") + + querier := stakingkeeper.Querier{Keeper: a.StakingKeeper} + res, err := querier.ValidatorDelegations(sdk.WrapSDKContext(ctx), + &stakingtypes.QueryValidatorDelegationsRequest{ValidatorAddr: valAddr.String()}) + require.NoError(t, err) + var delegators []string + for _, response := range res.DelegationResponses { + delegators = append(delegators, response.Delegation.DelegatorAddress) + } + require.Contains(t, delegators, delAddr.String()) +} + // TestV68RejectsOracleTxsWithoutCharging pins that retired oracle transactions // are refused before fees are charged. func TestV68RejectsOracleTxsWithoutCharging(t *testing.T) { diff --git a/app/upgrades.go b/app/upgrades.go index cdd0d07584..c2fe435509 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -169,6 +169,7 @@ func (app *App) RegisterUpgradeHandlers() { app.pruneUpgradedIBCState(ctx) app.pruneDenomAllowLists(ctx) app.pruneTokenfactoryParams(ctx) + app.StakingKeeper.StartDelegationByValIndexBackfill(ctx) return newVM, nil } diff --git a/evmrpc/tests/regression_test.go b/evmrpc/tests/regression_test.go index 0456616aa2..c24093fd53 100644 --- a/evmrpc/tests/regression_test.go +++ b/evmrpc/tests/regression_test.go @@ -13,6 +13,7 @@ import ( "github.com/sei-protocol/sei-chain/evmrpc" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" govtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/gov/types" + stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" "github.com/stretchr/testify/require" ) @@ -387,7 +388,7 @@ func testTx(t *testing.T, txHash string, version string, expectedGasUsed string, blockHeight := mockStatesFromTxJson(ctx, txHash, a, mc) ctx = setLegacySstoreIfNeeded(ctx, a, version) ctx = withCapturedConsensusParams(ctx, mc, blockHeight) - removeFutureGovernanceActivation(ctx, a, version) + removeFutureActivations(ctx, a, version) return ctx.WithBlockHeight(blockHeight) }) s.Run( @@ -428,7 +429,7 @@ func testBlock( ) ctx = setLegacySstoreIfNeeded(ctx, a, version) ctx = withCapturedConsensusParams(ctx, mc, blockHeight) - removeFutureGovernanceActivation(ctx, a, version) + removeFutureActivations(ctx, a, version) return ctx.WithBlockHeight(blockHeight) }, ) @@ -460,9 +461,12 @@ func setLegacySstoreIfNeeded(ctx sdk.Context, a *app.App, version string) sdk.Co return ctx } -func removeFutureGovernanceActivation(ctx sdk.Context, a *app.App, version string) { +// removeFutureActivations clears the markers genesis sets for features that mainnet +// only turns on at a later upgrade than version. +func removeFutureActivations(ctx sdk.Context, a *app.App, version string) { if semver.Compare(version, "v6.8") < 0 { ctx.KVStore(a.GetKey(govtypes.StoreKey)).Delete(govtypes.IncrementalTallyEnabledKey) + ctx.KVStore(a.GetKey(stakingtypes.StoreKey)).Delete(stakingtypes.DelegationByValIndexReadyKey) } } diff --git a/sei-cosmos/x/staking/abci.go b/sei-cosmos/x/staking/abci.go index 74caf3590c..828abf28ec 100644 --- a/sei-cosmos/x/staking/abci.go +++ b/sei-cosmos/x/staking/abci.go @@ -19,12 +19,28 @@ func BeginBlocker(ctx sdk.Context, k keeper.Keeper) { k.TrackHistoricalInfo(ctx) } -// Called every block, update validator set +// backfillDelegationByValIndex indexes the next batch of delegations while the +// delegation-by-validator index is backfilled. It runs after every transaction in the +// block, so no transaction iterates the staking store while the batch's writes are +// pending in it. A delegation that cannot be decoded halts the chain, as it would at +// any other read. +func backfillDelegationByValIndex(ctx sdk.Context, k keeper.Keeper) { + // The batch is block-level work, so it is not charged to any gas meter. + ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter(1, 1)) + if _, err := k.BackfillDelegationByValIndex(ctx, keeper.DelegationByValIndexBackfillBatchSize); err != nil { + panic(err) + } +} + +// Called every block, update validator set. It also advances the +// delegation-by-validator index backfill while one is in progress. func EndBlocker(ctx sdk.Context, k keeper.Keeper) []abci.ValidatorUpdate { endBlockerStart := time.Now() defer func() { stakingMetrics.endBlockerDuration.Record(ctx.Context(), time.Since(endBlockerStart).Seconds()) }() - return k.BlockValidatorUpdates(ctx) + updates := k.BlockValidatorUpdates(ctx) + backfillDelegationByValIndex(ctx, k) + return updates } diff --git a/sei-cosmos/x/staking/genesis.go b/sei-cosmos/x/staking/genesis.go index d0f29315bd..48d6451113 100644 --- a/sei-cosmos/x/staking/genesis.go +++ b/sei-cosmos/x/staking/genesis.go @@ -76,6 +76,10 @@ func InitGenesis( } } + if _, err := keeper.MigrateDelegationByValIndex(ctx); err != nil { + panic(err) + } + for _, ubd := range data.UnbondingDelegations { keeper.SetUnbondingDelegation(ctx, ubd) diff --git a/sei-cosmos/x/staking/genesis_test.go b/sei-cosmos/x/staking/genesis_test.go index 9608dd5906..f8638e9fb2 100644 --- a/sei-cosmos/x/staking/genesis_test.go +++ b/sei-cosmos/x/staking/genesis_test.go @@ -97,6 +97,36 @@ func TestInitGenesis(t *testing.T) { require.Equal(t, abcivals, vals) } +func TestInitGenesisIndexesDelegationsByValidator(t *testing.T) { + app, ctx, addrs := bootstrapGenesisTest(t, 2) + store := ctx.KVStore(app.StakingKeeper.GetStoreKey()) + store.Delete(types.DelegationByValIndexReadyKey) + + valTokens := app.StakingKeeper.TokensFromConsensusPower(ctx, 1) + params := app.StakingKeeper.GetParams(ctx) + pk, err := codectypes.NewAnyWithValue(PKs[0]) + require.NoError(t, err) + valAddr := sdk.ValAddress(addrs[0]) + validator := types.Validator{ + OperatorAddress: valAddr.String(), + ConsensusPubkey: pk, + Status: types.Bonded, + Tokens: valTokens, + DelegatorShares: valTokens.ToDec(), + Description: types.NewDescription("hoop", "", "", "", ""), + } + validators := append(app.StakingKeeper.GetAllValidators(ctx), validator) + require.NoError(t, apptesting.FundModuleAccount(app.BankKeeper, ctx, types.BondedPoolName, + sdk.NewCoins(sdk.NewCoin(params.BondDenom, valTokens.MulRaw(int64(len(validators))))))) + delegation := types.NewDelegation(addrs[1], valAddr, valTokens.ToDec()) + + staking.InitGenesis(ctx, app.StakingKeeper, app.AccountKeeper, app.BankKeeper, + types.NewGenesisState(params, validators, []types.Delegation{delegation})) + + require.True(t, app.StakingKeeper.DelegationByValIndexReady(ctx)) + require.True(t, store.Has(types.GetDelegationByValIndexKey(addrs[1], valAddr))) +} + func TestInitGenesis_PoolsBalanceMismatch(t *testing.T) { app := seiapp.Setup(t, false, false, false) ctx := app.NewContext(false, tmproto.Header{}) diff --git a/sei-cosmos/x/staking/keeper/delegation.go b/sei-cosmos/x/staking/keeper/delegation.go index 2edf53be56..467d2c5854 100644 --- a/sei-cosmos/x/staking/keeper/delegation.go +++ b/sei-cosmos/x/staking/keeper/delegation.go @@ -98,19 +98,27 @@ func (k Keeper) GetDelegatorDelegations(ctx sdk.Context, delegator sdk.AccAddres // SetDelegation sets a delegation. func (k Keeper) SetDelegation(ctx sdk.Context, delegation types.Delegation) { delegatorAddress := sdk.MustAccAddressFromBech32(delegation.DelegatorAddress) + valAddr := delegation.GetValidatorAddr() store := ctx.KVStore(k.storeKey) b := types.MustMarshalDelegation(k.cdc, delegation) - store.Set(types.GetDelegationKey(delegatorAddress, delegation.GetValidatorAddr()), b) + store.Set(types.GetDelegationKey(delegatorAddress, valAddr), b) + if k.delegationByValIndexMaintained(ctx) { + store.Set(types.GetDelegationByValIndexKey(delegatorAddress, valAddr), []byte{}) // index, store empty bytes + } } // RemoveDelegation removes a delegation. func (k Keeper) RemoveDelegation(ctx sdk.Context, delegation types.Delegation) { delegatorAddress := sdk.MustAccAddressFromBech32(delegation.DelegatorAddress) + valAddr := delegation.GetValidatorAddr() - k.BeforeDelegationRemoved(ctx, delegatorAddress, delegation.GetValidatorAddr()) + k.BeforeDelegationRemoved(ctx, delegatorAddress, valAddr) store := ctx.KVStore(k.storeKey) - store.Delete(types.GetDelegationKey(delegatorAddress, delegation.GetValidatorAddr())) + store.Delete(types.GetDelegationKey(delegatorAddress, valAddr)) + if k.delegationByValIndexMaintained(ctx) { + store.Delete(types.GetDelegationByValIndexKey(delegatorAddress, valAddr)) + } } // GetUnbondingDelegations returns a given amount of all the delegator unbonding-delegations. diff --git a/sei-cosmos/x/staking/keeper/delegation_index.go b/sei-cosmos/x/staking/keeper/delegation_index.go new file mode 100644 index 0000000000..3fea21e45b --- /dev/null +++ b/sei-cosmos/x/staking/keeper/delegation_index.go @@ -0,0 +1,137 @@ +package keeper + +import ( + "fmt" + "time" + + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" +) + +// DelegationByValIndexBackfillBatchSize is the number of delegations the staking +// EndBlocker indexes per block while the delegation-by-validator index is backfilled. +const DelegationByValIndexBackfillBatchSize = 1000 + +// MigrateDelegationByValIndexResult reports the outcome of populating the +// delegation-by-validator index. +type MigrateDelegationByValIndexResult struct { + TotalDelegations int + AlreadyReady bool + Elapsed time.Duration +} + +// DelegationByValIndexReady reports whether the delegation-by-validator index is +// populated in the state this context reads. It consumes no gas. +func (k Keeper) DelegationByValIndexReady(ctx sdk.Context) bool { + return k.hasGasless(ctx, types.DelegationByValIndexReadyKey) +} + +// delegationByValIndexMaintained reports whether delegation writes keep the +// delegation-by-validator index current: the index is ready, or its backfill has started. +// It consumes no gas. +func (k Keeper) delegationByValIndexMaintained(ctx sdk.Context) bool { + return k.DelegationByValIndexReady(ctx) || k.hasGasless(ctx, types.DelegationByValIndexCursorKey) +} + +// hasGasless reports whether key is in the staking store without charging for the read. +func (k Keeper) hasGasless(ctx sdk.Context, key []byte) bool { + // Delegation writes check the index markers at every height, so charging for the read + // would change the gas of pre-upgrade transactions that touch a delegation. + gasless := ctx.WithGasMeter(sdk.NewInfiniteGasMeter(1, 1)) + return gasless.KVStore(k.storeKey).Has(key) +} + +// StartDelegationByValIndexBackfill begins populating the delegation-by-validator +// index across blocks. From this call on, delegation writes maintain the index, and +// BackfillDelegationByValIndex indexes the delegations stored before it. It is a no-op +// once the index is ready or its backfill has started. +func (k Keeper) StartDelegationByValIndexBackfill(ctx sdk.Context) { + if k.delegationByValIndexMaintained(ctx) { + return + } + ctx.KVStore(k.storeKey).Set(types.DelegationByValIndexCursorKey, []byte{}) +} + +// BackfillDelegationByValIndex indexes up to limit stored delegations that follow the +// backfill cursor, and marks the index ready once every delegation is indexed. It +// returns the number of delegations indexed, and is a no-op when no backfill is in +// progress. limit must be positive. +func (k Keeper) BackfillDelegationByValIndex(ctx sdk.Context, limit int) (int, error) { + if limit <= 0 { + return 0, fmt.Errorf("delegation-by-validator index backfill limit must be positive, got %d", limit) + } + store := ctx.KVStore(k.storeKey) + cursor := store.Get(types.DelegationByValIndexCursorKey) + if cursor == nil { + return 0, nil + } + + start := types.DelegationKey + if len(cursor) > 0 { + // The cursor holds the last delegation key indexed; the next batch starts just after it. + start = append(append([]byte{}, cursor...), 0x00) + } + iterator := store.Iterator(start, sdk.PrefixEndBytes(types.DelegationKey)) + defer func() { _ = iterator.Close() }() + + indexed := 0 + var last []byte + for ; iterator.Valid() && indexed < limit; iterator.Next() { + if err := k.indexStoredDelegation(store, iterator.Key(), iterator.Value()); err != nil { + return indexed, err + } + last = append([]byte{}, iterator.Key()...) + indexed++ + } + + if iterator.Valid() { + store.Set(types.DelegationByValIndexCursorKey, last) + return indexed, nil + } + store.Delete(types.DelegationByValIndexCursorKey) + store.Set(types.DelegationByValIndexReadyKey, []byte{}) + return indexed, nil +} + +// MigrateDelegationByValIndex writes an index entry for every stored delegation in one +// pass and marks the index ready. Its cost grows with the number of delegations, so it +// is for genesis only; an upgrade starts a paced backfill with +// StartDelegationByValIndexBackfill instead. It is a no-op once the index is ready. +func (k Keeper) MigrateDelegationByValIndex(ctx sdk.Context) (MigrateDelegationByValIndexResult, error) { + start := time.Now() + store := ctx.KVStore(k.storeKey) + + if k.DelegationByValIndexReady(ctx) { + return MigrateDelegationByValIndexResult{AlreadyReady: true, Elapsed: time.Since(start)}, nil + } + + result := MigrateDelegationByValIndexResult{} + iterator := sdk.KVStorePrefixIterator(store, types.DelegationKey) + defer func() { _ = iterator.Close() }() + + for ; iterator.Valid(); iterator.Next() { + if err := k.indexStoredDelegation(store, iterator.Key(), iterator.Value()); err != nil { + return result, err + } + result.TotalDelegations++ + } + + store.Delete(types.DelegationByValIndexCursorKey) + store.Set(types.DelegationByValIndexReadyKey, []byte{}) + result.Elapsed = time.Since(start) + return result, nil +} + +// indexStoredDelegation writes the index entry for the delegation stored at key. +func (k Keeper) indexStoredDelegation(store sdk.KVStore, key, value []byte) error { + delegation, err := types.UnmarshalDelegation(k.cdc, value) + if err != nil { + return fmt.Errorf("unmarshal delegation at key %X: %w", key, err) + } + delAddr, err := sdk.AccAddressFromBech32(delegation.DelegatorAddress) + if err != nil { + return fmt.Errorf("parse delegator address %q: %w", delegation.DelegatorAddress, err) + } + store.Set(types.GetDelegationByValIndexKey(delAddr, delegation.GetValidatorAddr()), []byte{}) + return nil +} diff --git a/sei-cosmos/x/staking/keeper/delegation_index_test.go b/sei-cosmos/x/staking/keeper/delegation_index_test.go new file mode 100644 index 0000000000..2d768cd29b --- /dev/null +++ b/sei-cosmos/x/staking/keeper/delegation_index_test.go @@ -0,0 +1,388 @@ +package keeper_test + +import ( + "bytes" + "encoding/binary" + "testing" + + "github.com/stretchr/testify/require" + + seiapp "github.com/sei-protocol/sei-chain/app" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/types/query" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/keeper" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" +) + +// seedDelegations writes delegations straight to the store, bypassing SetDelegation, +// so the index state under test is only what the migration produced. +func seedDelegations(t *testing.T, app *seiapp.App, ctx sdk.Context, delegations []types.Delegation) { + t.Helper() + store := ctx.KVStore(app.StakingKeeper.GetStoreKey()) + for _, delegation := range delegations { + store.Set( + types.GetDelegationKey(sdk.MustAccAddressFromBech32(delegation.DelegatorAddress), delegation.GetValidatorAddr()), + types.MustMarshalDelegation(app.AppCodec(), delegation), + ) + } +} + +// unreadyDelegationByValIndex deletes the index and its markers, leaving the store as +// a chain that has not run the migration holds it. +func unreadyDelegationByValIndex(t *testing.T, app *seiapp.App, ctx sdk.Context) { + t.Helper() + store := ctx.KVStore(app.StakingKeeper.GetStoreKey()) + iterator := sdk.KVStorePrefixIterator(store, types.DelegationByValIndexKey) + var keys [][]byte + for ; iterator.Valid(); iterator.Next() { + keys = append(keys, iterator.Key()) + } + require.NoError(t, iterator.Close()) + for _, key := range keys { + store.Delete(key) + } + store.Delete(types.DelegationByValIndexReadyKey) + store.Delete(types.DelegationByValIndexCursorKey) + require.False(t, app.StakingKeeper.DelegationByValIndexReady(ctx)) +} + +func TestDelegationByValIndexNotReadyNoDualWrite(t *testing.T) { + _, app, ctx := createTestInput(t) + unreadyDelegationByValIndex(t, app, ctx) + + addrDels, valAddrs := generateAddresses(app, ctx, 1) + delegation := types.NewDelegation(addrDels[0], valAddrs[0], sdk.NewDec(1)) + + store := ctx.KVStore(app.StakingKeeper.GetStoreKey()) + indexKey := types.GetDelegationByValIndexKey(addrDels[0], valAddrs[0]) + + require.False(t, app.StakingKeeper.DelegationByValIndexReady(ctx)) + + app.StakingKeeper.SetDelegation(ctx, delegation) + require.False(t, store.Has(indexKey)) + require.NotNil(t, store.Get(types.GetDelegationKey(addrDels[0], valAddrs[0]))) +} + +func TestDelegationByValIndexDualWriteAfterMigration(t *testing.T) { + _, app, ctx := createTestInput(t) + unreadyDelegationByValIndex(t, app, ctx) + + _, err := app.StakingKeeper.MigrateDelegationByValIndex(ctx) + require.NoError(t, err) + require.True(t, app.StakingKeeper.DelegationByValIndexReady(ctx)) + + addrDels, valAddrs := generateAddresses(app, ctx, 1) + delegation := types.NewDelegation(addrDels[0], valAddrs[0], sdk.NewDec(1)) + + store := ctx.KVStore(app.StakingKeeper.GetStoreKey()) + indexKey := types.GetDelegationByValIndexKey(addrDels[0], valAddrs[0]) + + app.StakingKeeper.SetDelegation(ctx, delegation) + require.True(t, store.Has(indexKey)) + require.NotNil(t, store.Get(types.GetDelegationKey(addrDels[0], valAddrs[0]))) + + app.StakingKeeper.RemoveDelegation(ctx, delegation) + require.False(t, store.Has(indexKey)) + require.Nil(t, store.Get(types.GetDelegationKey(addrDels[0], valAddrs[0]))) +} + +func TestMigrateDelegationByValIndex(t *testing.T) { + _, app, ctx := createTestInput(t) + unreadyDelegationByValIndex(t, app, ctx) + genesisDelegations := len(app.StakingKeeper.GetAllDelegations(ctx)) + + addrDels, valAddrs := generateAddresses(app, ctx, 2) + delegations := []types.Delegation{ + types.NewDelegation(addrDels[0], valAddrs[0], sdk.NewDec(1)), + types.NewDelegation(addrDels[0], valAddrs[1], sdk.NewDec(2)), + types.NewDelegation(addrDels[1], valAddrs[0], sdk.NewDec(3)), + } + seedDelegations(t, app, ctx, delegations) + + result, err := app.StakingKeeper.MigrateDelegationByValIndex(ctx) + require.NoError(t, err) + require.Equal(t, genesisDelegations+3, result.TotalDelegations) + require.False(t, result.AlreadyReady) + require.True(t, app.StakingKeeper.DelegationByValIndexReady(ctx)) + + store := ctx.KVStore(app.StakingKeeper.GetStoreKey()) + for _, delegation := range delegations { + delAddr := sdk.MustAccAddressFromBech32(delegation.DelegatorAddress) + require.True(t, store.Has(types.GetDelegationByValIndexKey(delAddr, delegation.GetValidatorAddr()))) + } + + repeat, err := app.StakingKeeper.MigrateDelegationByValIndex(ctx) + require.NoError(t, err) + require.True(t, repeat.AlreadyReady) + require.Zero(t, repeat.TotalDelegations) +} + +// TestMigrateDelegationByValIndexNoOrphans pins the invariant the index exists to +// uphold: every delegation is indexed, and every index entry resolves to a delegation. +func TestMigrateDelegationByValIndexNoOrphans(t *testing.T) { + _, app, ctx := createTestInput(t) + unreadyDelegationByValIndex(t, app, ctx) + + addrDels, valAddrs := generateAddresses(app, ctx, 3) + seedDelegations(t, app, ctx, []types.Delegation{ + types.NewDelegation(addrDels[0], valAddrs[0], sdk.NewDec(1)), + types.NewDelegation(addrDels[1], valAddrs[0], sdk.NewDec(2)), + types.NewDelegation(addrDels[2], valAddrs[1], sdk.NewDec(3)), + }) + + _, err := app.StakingKeeper.MigrateDelegationByValIndex(ctx) + require.NoError(t, err) + + store := ctx.KVStore(app.StakingKeeper.GetStoreKey()) + + delegationKeys := map[string]bool{} + delIter := sdk.KVStorePrefixIterator(store, types.DelegationKey) + for ; delIter.Valid(); delIter.Next() { + delegationKeys[string(delIter.Key())] = true + } + require.NoError(t, delIter.Close()) + + indexed := map[string]bool{} + idxIter := sdk.KVStorePrefixIterator(store, types.DelegationByValIndexKey) + for ; idxIter.Valid(); idxIter.Next() { + resolved := types.GetDelegationKeyFromValIndexKey(idxIter.Key()) + require.True(t, store.Has(resolved), "index entry resolves to no delegation") + indexed[string(resolved)] = true + } + require.NoError(t, idxIter.Close()) + + require.Equal(t, delegationKeys, indexed) +} + +// delegatorAt returns a delegator address whose last byte is b and whose other bytes +// are fill, so its delegation keys sort by fill and then b. +func delegatorAt(fill, b byte) sdk.AccAddress { + addr := sdk.AccAddress(bytes.Repeat([]byte{fill}, 20)) + addr[19] = b + return addr +} + +// requireIndexMatchesDelegations requires the index to hold exactly one entry per +// stored delegation. +func requireIndexMatchesDelegations(t *testing.T, store sdk.KVStore) { + t.Helper() + delegationKeys := map[string]bool{} + delIter := sdk.KVStorePrefixIterator(store, types.DelegationKey) + for ; delIter.Valid(); delIter.Next() { + delegationKeys[string(delIter.Key())] = true + } + require.NoError(t, delIter.Close()) + + indexed := map[string]bool{} + idxIter := sdk.KVStorePrefixIterator(store, types.DelegationByValIndexKey) + for ; idxIter.Valid(); idxIter.Next() { + indexed[string(types.GetDelegationKeyFromValIndexKey(idxIter.Key()))] = true + } + require.NoError(t, idxIter.Close()) + + require.Equal(t, delegationKeys, indexed) +} + +func TestStartDelegationByValIndexBackfillMaintainsWrites(t *testing.T) { + _, app, ctx := createTestInput(t) + unreadyDelegationByValIndex(t, app, ctx) + + app.StakingKeeper.StartDelegationByValIndexBackfill(ctx) + require.False(t, app.StakingKeeper.DelegationByValIndexReady(ctx)) + + addrDels, valAddrs := generateAddresses(app, ctx, 1) + delegation := types.NewDelegation(addrDels[0], valAddrs[0], sdk.NewDec(1)) + store := ctx.KVStore(app.StakingKeeper.GetStoreKey()) + indexKey := types.GetDelegationByValIndexKey(addrDels[0], valAddrs[0]) + + app.StakingKeeper.SetDelegation(ctx, delegation) + require.True(t, store.Has(indexKey)) + + app.StakingKeeper.RemoveDelegation(ctx, delegation) + require.False(t, store.Has(indexKey)) +} + +func TestStartDelegationByValIndexBackfillOnceReady(t *testing.T) { + _, app, ctx := createTestInput(t) + require.True(t, app.StakingKeeper.DelegationByValIndexReady(ctx)) + + app.StakingKeeper.StartDelegationByValIndexBackfill(ctx) + + store := ctx.KVStore(app.StakingKeeper.GetStoreKey()) + require.False(t, store.Has(types.DelegationByValIndexCursorKey)) + indexed, err := app.StakingKeeper.BackfillDelegationByValIndex(ctx, 1) + require.NoError(t, err) + require.Zero(t, indexed) +} + +func TestBackfillDelegationByValIndexNotStarted(t *testing.T) { + _, app, ctx := createTestInput(t) + unreadyDelegationByValIndex(t, app, ctx) + + indexed, err := app.StakingKeeper.BackfillDelegationByValIndex(ctx, 100) + require.NoError(t, err) + require.Zero(t, indexed) + require.False(t, app.StakingKeeper.DelegationByValIndexReady(ctx)) +} + +func TestBackfillDelegationByValIndexRejectsNonPositiveLimit(t *testing.T) { + _, app, ctx := createTestInput(t) + unreadyDelegationByValIndex(t, app, ctx) + addrDels, valAddrs := generateAddresses(app, ctx, 1) + seedDelegations(t, app, ctx, []types.Delegation{types.NewDelegation(addrDels[0], valAddrs[0], sdk.NewDec(1))}) + app.StakingKeeper.StartDelegationByValIndexBackfill(ctx) + + for _, limit := range []int{0, -1} { + indexed, err := app.StakingKeeper.BackfillDelegationByValIndex(ctx, limit) + require.Error(t, err) + require.Zero(t, indexed) + } + store := ctx.KVStore(app.StakingKeeper.GetStoreKey()) + require.Equal(t, []byte{}, store.Get(types.DelegationByValIndexCursorKey)) + require.False(t, store.Has(types.GetDelegationByValIndexKey(addrDels[0], valAddrs[0]))) +} + +// TestBackfillDelegationByValIndexInBatches pins that the backfill indexes at most +// limit delegations per call, and that delegations written, changed or removed while it +// runs, on either side of the cursor, leave the finished index exact. +func TestBackfillDelegationByValIndexInBatches(t *testing.T) { + _, app, ctx := createTestInput(t) + unreadyDelegationByValIndex(t, app, ctx) + store := ctx.KVStore(app.StakingKeeper.GetStoreKey()) + _, valAddrs := generateAddresses(app, ctx, 1) + valAddr := valAddrs[0] + + // The 0x00 delegators sort before every other delegation and the 0xff ones after. + first, second := delegatorAt(0x00, 1), delegatorAt(0x00, 2) + ahead, last := delegatorAt(0xff, 0xfe), delegatorAt(0xff, 0xff) + seedDelegations(t, app, ctx, []types.Delegation{ + types.NewDelegation(first, valAddr, sdk.NewDec(1)), + types.NewDelegation(second, valAddr, sdk.NewDec(2)), + types.NewDelegation(ahead, valAddr, sdk.NewDec(3)), + types.NewDelegation(last, valAddr, sdk.NewDec(4)), + }) + total := len(app.StakingKeeper.GetAllDelegations(ctx)) + + app.StakingKeeper.StartDelegationByValIndexBackfill(ctx) + indexed, err := app.StakingKeeper.BackfillDelegationByValIndex(ctx, 2) + require.NoError(t, err) + require.Equal(t, 2, indexed) + require.False(t, app.StakingKeeper.DelegationByValIndexReady(ctx)) + require.Equal(t, types.GetDelegationKey(second, valAddr), store.Get(types.DelegationByValIndexCursorKey)) + require.True(t, store.Has(types.GetDelegationByValIndexKey(first, valAddr))) + require.False(t, store.Has(types.GetDelegationByValIndexKey(last, valAddr))) + + // Behind the cursor: a removal of an indexed delegation and a new delegation. + app.StakingKeeper.RemoveDelegation(ctx, types.NewDelegation(first, valAddr, sdk.NewDec(1))) + behind := delegatorAt(0x00, 0) + app.StakingKeeper.SetDelegation(ctx, types.NewDelegation(behind, valAddr, sdk.NewDec(5))) + // Ahead of the cursor: a change and a removal of delegations not yet indexed. + app.StakingKeeper.SetDelegation(ctx, types.NewDelegation(ahead, valAddr, sdk.NewDec(6))) + app.StakingKeeper.RemoveDelegation(ctx, types.NewDelegation(last, valAddr, sdk.NewDec(4))) + + remaining := total - 2 - 1 // the two indexed and the one removed ahead of the cursor + for calls := 0; !app.StakingKeeper.DelegationByValIndexReady(ctx); calls++ { + require.Less(t, calls, total, "the backfill did not finish") + indexed, err = app.StakingKeeper.BackfillDelegationByValIndex(ctx, 2) + require.NoError(t, err) + require.LessOrEqual(t, indexed, 2) + remaining -= indexed + } + require.Zero(t, remaining) + require.False(t, store.Has(types.DelegationByValIndexCursorKey)) + require.False(t, store.Has(types.GetDelegationByValIndexKey(first, valAddr))) + require.False(t, store.Has(types.GetDelegationByValIndexKey(last, valAddr))) + requireIndexMatchesDelegations(t, store) +} + +// TestBackfillDelegationByValIndexEndsOnBatchBoundary pins that a batch that indexes +// the last delegation finishes the backfill without a further call. +func TestBackfillDelegationByValIndexEndsOnBatchBoundary(t *testing.T) { + _, app, ctx := createTestInput(t) + unreadyDelegationByValIndex(t, app, ctx) + addrDels, valAddrs := generateAddresses(app, ctx, 2) + seedDelegations(t, app, ctx, []types.Delegation{ + types.NewDelegation(addrDels[0], valAddrs[0], sdk.NewDec(1)), + types.NewDelegation(addrDels[1], valAddrs[1], sdk.NewDec(2)), + }) + total := len(app.StakingKeeper.GetAllDelegations(ctx)) + + app.StakingKeeper.StartDelegationByValIndexBackfill(ctx) + indexed, err := app.StakingKeeper.BackfillDelegationByValIndex(ctx, total) + require.NoError(t, err) + require.Equal(t, total, indexed) + require.True(t, app.StakingKeeper.DelegationByValIndexReady(ctx)) + requireIndexMatchesDelegations(t, ctx.KVStore(app.StakingKeeper.GetStoreKey())) +} + +// maxScanDelegator sorts after every test delegator address, so a filtered scan for +// its validator must traverse the whole delegation keyspace to reach it. +var maxScanDelegator = sdk.AccAddress(bytes.Repeat([]byte{0xff}, 20)) + +// TestGRPCQueryValidatorDelegationsBeyondScanLimit pins the defect the index closes: +// past query.MaxScanLimit entries the filtered scan reverts, and once the index is +// ready the same request is answered from the per-validator prefix. +func (suite *KeeperTestSuite) TestGRPCQueryValidatorDelegationsBeyondScanLimit() { + app, ctx := suite.app, suite.ctx + querier := keeper.Querier{Keeper: app.StakingKeeper} + unreadyDelegationByValIndex(suite.T(), app, ctx) + + targetVal := suite.vals[1].GetOperator() + fillerVal := suite.vals[0].GetOperator() + store := ctx.KVStore(app.StakingKeeper.GetStoreKey()) + + for i := 0; i <= int(query.MaxScanLimit); i++ { + delAddr := make(sdk.AccAddress, 20) + binary.BigEndian.PutUint64(delAddr[12:], uint64(i)) + delegation := types.NewDelegation(delAddr, fillerVal, sdk.NewDec(1)) + store.Set( + types.GetDelegationKey(delAddr, fillerVal), + types.MustMarshalDelegation(app.AppCodec(), delegation), + ) + } + target := types.NewDelegation(maxScanDelegator, targetVal, sdk.NewDec(1)) + store.Set( + types.GetDelegationKey(maxScanDelegator, targetVal), + types.MustMarshalDelegation(app.AppCodec(), target), + ) + + req := &types.QueryValidatorDelegationsRequest{ValidatorAddr: targetVal.String()} + + // Before the migration the query scans, so it reverts exactly as it does at + // heights below the one that populated the index. + _, err := querier.ValidatorDelegations(sdk.WrapSDKContext(ctx), req) + suite.Error(err) + + _, err = app.StakingKeeper.MigrateDelegationByValIndex(ctx) + suite.NoError(err) + + res, err := querier.ValidatorDelegations(sdk.WrapSDKContext(ctx), req) + suite.NoError(err) + delegators := make([]string, 0, len(res.DelegationResponses)) + for _, dr := range res.DelegationResponses { + suite.Equal(targetVal.String(), dr.Delegation.ValidatorAddress) + delegators = append(delegators, dr.Delegation.DelegatorAddress) + } + suite.Contains(delegators, maxScanDelegator.String()) +} + +// TestGRPCQueryValidatorDelegationsIndexMatchesScan pins that the scan and the index +// agree on a validator small enough for both to answer. +func (suite *KeeperTestSuite) TestGRPCQueryValidatorDelegationsIndexMatchesScan() { + app, ctx := suite.app, suite.ctx + querier := keeper.Querier{Keeper: app.StakingKeeper} + + unreadyDelegationByValIndex(suite.T(), app, ctx) + req := &types.QueryValidatorDelegationsRequest{ValidatorAddr: suite.vals[1].GetOperator().String()} + + scanned, err := querier.ValidatorDelegations(sdk.WrapSDKContext(ctx), req) + suite.NoError(err) + + _, err = app.StakingKeeper.MigrateDelegationByValIndex(ctx) + suite.NoError(err) + + indexed, err := querier.ValidatorDelegations(sdk.WrapSDKContext(ctx), req) + suite.NoError(err) + + suite.ElementsMatch(scanned.DelegationResponses, indexed.DelegationResponses) +} diff --git a/sei-cosmos/x/staking/keeper/grpc_query.go b/sei-cosmos/x/staking/keeper/grpc_query.go index 110912ba8f..802066df37 100644 --- a/sei-cosmos/x/staking/keeper/grpc_query.go +++ b/sei-cosmos/x/staking/keeper/grpc_query.go @@ -94,9 +94,65 @@ func (k Querier) ValidatorDelegations(c context.Context, req *types.QueryValidat if req.ValidatorAddr == "" { return nil, status.Error(codes.InvalidArgument, "validator address cannot be empty") } - var delegations []types.Delegation ctx := sdk.UnwrapSDKContext(c) + var ( + delegations []types.Delegation + pageRes *query.PageResponse + err error + ) + if k.DelegationByValIndexReady(ctx) { + delegations, pageRes, err = k.validatorDelegationsFromIndex(ctx, req) + } else { + delegations, pageRes, err = k.validatorDelegationsFromScan(ctx, req) + } + if err != nil { + return nil, err + } + + delResponses, err := DelegationsToDelegationResponses(ctx, k.Keeper, delegations) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &types.QueryValidatorDelegationsResponse{ + DelegationResponses: delResponses, Pagination: pageRes}, nil +} + +// validatorDelegationsFromIndex pages over the delegation-by-validator index prefix of +// the requested validator. +func (k Querier) validatorDelegationsFromIndex(ctx sdk.Context, req *types.QueryValidatorDelegationsRequest) ([]types.Delegation, *query.PageResponse, error) { + valAddr, err := sdk.ValAddressFromBech32(req.ValidatorAddr) + if err != nil { + return nil, nil, err + } + + var delegations []types.Delegation + store := ctx.KVStore(k.storeKey) + valPrefix := types.GetDelegationsByValIndexKey(valAddr) + indexStore := prefix.NewStore(store, valPrefix) + + pageRes, err := query.Paginate(ctx, indexStore, req.Pagination, func(key []byte, _ []byte) error { + indexKey := append(append([]byte{}, valPrefix...), key...) + storeKey := types.GetDelegationKeyFromValIndexKey(indexKey) + delegation, err := types.UnmarshalDelegation(k.cdc, store.Get(storeKey)) + if err != nil { + return err + } + delegations = append(delegations, delegation) + return nil + }) + if err != nil { + return nil, nil, query.WrapGRPCError(err) + } + return delegations, pageRes, nil +} + +// validatorDelegationsFromScan filters the whole delegation store for the requested +// validator. It is the only source at heights before the delegation-by-validator index +// is ready, and reproduces the results and failures those heights had. +func (k Querier) validatorDelegationsFromScan(ctx sdk.Context, req *types.QueryValidatorDelegationsRequest) ([]types.Delegation, *query.PageResponse, error) { + var delegations []types.Delegation store := ctx.KVStore(k.storeKey) valStore := prefix.NewStore(store, types.DelegationKey) @@ -123,16 +179,9 @@ func (k Querier) ValidatorDelegations(c context.Context, req *types.QueryValidat return true, nil }) if err != nil { - return nil, query.WrapGRPCError(err) + return nil, nil, query.WrapGRPCError(err) } - - delResponses, err := DelegationsToDelegationResponses(ctx, k.Keeper, delegations) - if err != nil { - return nil, status.Error(codes.Internal, err.Error()) - } - - return &types.QueryValidatorDelegationsResponse{ - DelegationResponses: delResponses, Pagination: pageRes}, nil + return delegations, pageRes, nil } // ValidatorUnbondingDelegations queries unbonding delegations of a validator diff --git a/sei-cosmos/x/staking/types/keys.go b/sei-cosmos/x/staking/types/keys.go index 0c7f4409f4..9b7d104dd3 100644 --- a/sei-cosmos/x/staking/types/keys.go +++ b/sei-cosmos/x/staking/types/keys.go @@ -42,6 +42,9 @@ var ( RedelegationKey = []byte{0x34} // key for a redelegation RedelegationByValSrcIndexKey = []byte{0x35} // prefix for each key for an redelegation, by source validator operator RedelegationByValDstIndexKey = []byte{0x36} // prefix for each key for an redelegation, by destination validator operator + DelegationByValIndexKey = []byte{0x37} // prefix for each key for a delegation, by validator operator + DelegationByValIndexReadyKey = []byte{0x38} // marker: the delegation-by-validator index is populated + DelegationByValIndexCursorKey = []byte{0x39} // last delegation key indexed while the delegation-by-validator index is backfilled UnbondingQueueKey = []byte{0x41} // prefix for the timestamps in unbonding queue RedelegationQueueKey = []byte{0x42} // prefix for the timestamps in redelegations queue @@ -192,6 +195,31 @@ func GetDelegationsKey(delAddr sdk.AccAddress) []byte { return append(DelegationKey, address.MustLengthPrefix(delAddr)...) } +// GetDelegationByValIndexKey creates the index-key for a delegation, stored by validator-index. +// VALUE: none (key rearrangement used) +func GetDelegationByValIndexKey(delAddr sdk.AccAddress, valAddr sdk.ValAddress) []byte { + return append(GetDelegationsByValIndexKey(valAddr), address.MustLengthPrefix(delAddr)...) +} + +// GetDelegationKeyFromValIndexKey rearranges the ValIndexKey to get the DelegationKey. +func GetDelegationKeyFromValIndexKey(indexKey []byte) []byte { + kv.AssertKeyAtLeastLength(indexKey, 2) + addrs := indexKey[1:] // remove prefix bytes + + valAddrLen := addrs[0] + kv.AssertKeyAtLeastLength(addrs, 2+int(valAddrLen)) + valAddr := addrs[1 : 1+valAddrLen] + kv.AssertKeyAtLeastLength(addrs, 3+int(valAddrLen)) + delAddr := addrs[valAddrLen+2:] + + return GetDelegationKey(delAddr, valAddr) +} + +// GetDelegationsByValIndexKey creates the prefix keyspace for delegation indexes by validator. +func GetDelegationsByValIndexKey(valAddr sdk.ValAddress) []byte { + return append(DelegationByValIndexKey, address.MustLengthPrefix(valAddr)...) +} + // GetUBDKey creates the key for an unbonding delegation by delegator and validator addr // VALUE: staking/UnbondingDelegation func GetUBDKey(delAddr sdk.AccAddress, valAddr sdk.ValAddress) []byte { diff --git a/sei-cosmos/x/staking/types/keys_test.go b/sei-cosmos/x/staking/types/keys_test.go index 778d16bdbc..932ccae823 100644 --- a/sei-cosmos/x/staking/types/keys_test.go +++ b/sei-cosmos/x/staking/types/keys_test.go @@ -49,6 +49,16 @@ func TestGetValidatorPowerRank(t *testing.T) { } } +func TestGetDelegationKeyFromValIndexKey(t *testing.T) { + delAddr := sdk.AccAddress(keysAddr1) + valAddr := sdk.ValAddress(keysAddr2) + + delegationKey := types.GetDelegationKey(delAddr, valAddr) + indexKey := types.GetDelegationByValIndexKey(delAddr, valAddr) + + require.Equal(t, delegationKey, types.GetDelegationKeyFromValIndexKey(indexKey)) +} + func TestGetREDByValDstIndexKey(t *testing.T) { tests := []struct { delAddr sdk.AccAddress