diff --git a/app/upgrade_test.go b/app/upgrade_test.go index 08a0c269d7..5432515010 100644 --- a/app/upgrade_test.go +++ b/app/upgrade_test.go @@ -7,6 +7,7 @@ import ( "github.com/sei-protocol/sei-chain/app" "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/keys/secp256k1" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + stakingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" "github.com/sei-protocol/sei-chain/sei-cosmos/x/upgrade/types" storekeys "github.com/sei-protocol/sei-chain/sei-db/common/keys" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" @@ -114,3 +115,34 @@ func TestSkipOptimisticProcessingOnUpgrade(t *testing.T) { require.False(t, testWrapper.App.GetOptimisticProcessingInfo().Aborted) }) } + +func TestV67PopulatesDelegationByValIndex(t *testing.T) { + t.Setenv("UPGRADE_VERSION_LIST", "v6.7") + tm := time.Now().UTC() + valPub := secp256k1.GenPrivKey().PubKey() + testWrapper := app.NewTestWrapper(t, tm, valPub, false) + testWrapper.App.RegisterUpgradeHandlers() + + ctx := testWrapper.Ctx + stakingKeeper := testWrapper.App.StakingKeeper + delAddr := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + valAddr := sdk.ValAddress(secp256k1.GenPrivKey().PubKey().Address()) + delegation := stakingtypes.NewDelegation(delAddr, valAddr, sdk.NewDec(1)) + + // Seed through the store rather than SetDelegation, so the index entry can only + // come from the upgrade handler. + store := ctx.KVStore(stakingKeeper.GetStoreKey()) + store.Set( + stakingtypes.GetDelegationKey(delAddr, valAddr), + stakingtypes.MustMarshalDelegation(testWrapper.App.AppCodec(), delegation), + ) + require.False(t, stakingKeeper.DelegationByValIndexReady(ctx)) + + testWrapper.App.UpgradeKeeper.ApplyUpgrade(ctx, types.Plan{ + Name: "v6.7", + Height: ctx.BlockHeight(), + }) + + require.True(t, stakingKeeper.DelegationByValIndexReady(ctx)) + require.True(t, store.Has(stakingtypes.GetDelegationByValIndexKey(delAddr, valAddr))) +} diff --git a/app/upgrades.go b/app/upgrades.go index 1923f8a69f..6855eaa949 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -102,6 +102,9 @@ func (app *App) RegisterUpgradeHandlers() { app.UpgradeKeeper.DeleteModuleVersion(ctx, capabilityModuleName) app.UpgradeKeeper.DeleteModuleVersion(ctx, feegrantModuleName) app.UpgradeKeeper.DeleteModuleVersion(ctx, transferModuleName) + if err := migrateDelegationByValIndex(ctx, app); err != nil { + return nil, err + } return newVM, nil } @@ -111,3 +114,24 @@ func (app *App) RegisterUpgradeHandlers() { } const v606UpgradeHeight = 151573570 + +// migrateDelegationByValIndex populates the validator-indexed delegation store and +// marks it ready, so the staking precompile's validatorDelegations can answer from a +// per-validator prefix instead of scanning every delegation. +// +// It runs on an infinite gas meter: the cost is a property of chain size at the +// upgrade height, not of anything a transaction chose to spend. +func migrateDelegationByValIndex(ctx sdk.Context, app *App) error { + result, err := app.StakingKeeper.MigrateDelegationByValIndex(ctx.WithGasMeter(sdk.NewInfiniteGasMeter(1, 1))) + if err != nil { + return err + } + logger.Info( + "populated delegation-by-validator index", + "total_delegations", result.TotalDelegations, + "index_written", result.IndexWritten, + "already_ready", result.AlreadyReady, + "elapsed", result.Elapsed.String(), + ) + return nil +} diff --git a/precompiles/staking/legacy/v67/staking.go b/precompiles/staking/legacy/v67/staking.go index 2f21c16d1e..2da8cc99b8 100644 --- a/precompiles/staking/legacy/v67/staking.go +++ b/precompiles/staking/legacy/v67/staking.go @@ -1004,7 +1004,7 @@ func (p PrecompileExecutor) validatorDelegations(ctx sdk.Context, method *abi.Me }, } - response, err := p.stakingQuerier.ValidatorDelegations(sdk.WrapSDKContext(ctx), request) + response, err := p.stakingQuerier.ValidatorDelegationsIndexed(sdk.WrapSDKContext(ctx), request) if err != nil { return nil, 0, err } diff --git a/precompiles/staking/staking.go b/precompiles/staking/staking.go index d13b5d129a..cfd8e55cbf 100644 --- a/precompiles/staking/staking.go +++ b/precompiles/staking/staking.go @@ -1002,7 +1002,7 @@ func (p PrecompileExecutor) validatorDelegations(ctx sdk.Context, method *abi.Me }, } - response, err := p.stakingQuerier.ValidatorDelegations(sdk.WrapSDKContext(ctx), request) + response, err := p.stakingQuerier.ValidatorDelegationsIndexed(sdk.WrapSDKContext(ctx), request) if err != nil { return nil, 0, err } diff --git a/precompiles/staking/staking_test.go b/precompiles/staking/staking_test.go index 6b35604056..78fbb6aeee 100644 --- a/precompiles/staking/staking_test.go +++ b/precompiles/staking/staking_test.go @@ -297,6 +297,10 @@ func (tq *TestStakingQuerier) ValidatorDelegations(c context.Context, _ *staking return tq.ValidatorDelegationsResponse, tq.Err } +func (tq *TestStakingQuerier) ValidatorDelegationsIndexed(c context.Context, _ *stakingtypes.QueryValidatorDelegationsRequest) (*stakingtypes.QueryValidatorDelegationsResponse, error) { + return tq.ValidatorDelegationsResponse, tq.Err +} + func (tq *TestStakingQuerier) ValidatorUnbondingDelegations(c context.Context, _ *stakingtypes.QueryValidatorUnbondingDelegationsRequest) (*stakingtypes.QueryValidatorUnbondingDelegationsResponse, error) { return tq.ValidatorUnbondingDelegationsResponse, tq.Err } diff --git a/precompiles/utils/expected_keepers.go b/precompiles/utils/expected_keepers.go index 9668c01121..8d252fe687 100644 --- a/precompiles/utils/expected_keepers.go +++ b/precompiles/utils/expected_keepers.go @@ -183,6 +183,7 @@ type StakingQuerier interface { Validators(c context.Context, req *stakingtypes.QueryValidatorsRequest) (*stakingtypes.QueryValidatorsResponse, error) Validator(c context.Context, req *stakingtypes.QueryValidatorRequest) (*stakingtypes.QueryValidatorResponse, error) ValidatorDelegations(c context.Context, req *stakingtypes.QueryValidatorDelegationsRequest) (*stakingtypes.QueryValidatorDelegationsResponse, error) + ValidatorDelegationsIndexed(c context.Context, req *stakingtypes.QueryValidatorDelegationsRequest) (*stakingtypes.QueryValidatorDelegationsResponse, error) ValidatorUnbondingDelegations(c context.Context, req *stakingtypes.QueryValidatorUnbondingDelegationsRequest) (*stakingtypes.QueryValidatorUnbondingDelegationsResponse, error) UnbondingDelegation(c context.Context, req *stakingtypes.QueryUnbondingDelegationRequest) (*stakingtypes.QueryUnbondingDelegationResponse, error) DelegatorDelegations(c context.Context, req *stakingtypes.QueryDelegatorDelegationsRequest) (*stakingtypes.QueryDelegatorDelegationsResponse, error) diff --git a/sei-cosmos/x/staking/keeper/delegation.go b/sei-cosmos/x/staking/keeper/delegation.go index 51de4ad5ed..33baff53d6 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.DelegationByValIndexReady(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.DelegationByValIndexReady(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..00054349ae --- /dev/null +++ b/sei-cosmos/x/staking/keeper/delegation_index.go @@ -0,0 +1,67 @@ +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" +) + +// MigrateDelegationByValIndexResult reports the outcome of populating the +// validator-indexed delegation store. +type MigrateDelegationByValIndexResult struct { + TotalDelegations int + IndexWritten int + AlreadyReady bool + Elapsed time.Duration +} + +// DelegationByValIndexReady reports whether the validator-indexed delegation store +// is populated at the version this context reads. +// +// The marker is versioned state written by MigrateDelegationByValIndex, so a context +// reading a height before that migration observes it absent. That makes the answer +// correct for historical queries and re-traced blocks without the caller supplying +// an upgrade name or height. +func (k Keeper) DelegationByValIndexReady(ctx sdk.Context) bool { + return ctx.KVStore(k.storeKey).Has(types.DelegationByValIndexReadyKey) +} + +// MigrateDelegationByValIndex writes a validator-indexed key for every existing +// delegation and then marks the index ready. It is a no-op once the marker is set. +func (k Keeper) MigrateDelegationByValIndex(ctx sdk.Context) (MigrateDelegationByValIndexResult, error) { + start := time.Now() + store := ctx.KVStore(k.storeKey) + + if store.Has(types.DelegationByValIndexReadyKey) { + 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() { + delegation, err := types.UnmarshalDelegation(k.cdc, iterator.Value()) + if err != nil { + return result, fmt.Errorf("unmarshal delegation at key %X: %w", iterator.Key(), err) + } + delAddr, err := sdk.AccAddressFromBech32(delegation.DelegatorAddress) + if err != nil { + return result, fmt.Errorf("parse delegator address %q: %w", delegation.DelegatorAddress, err) + } + + result.TotalDelegations++ + indexKey := types.GetDelegationByValIndexKey(delAddr, delegation.GetValidatorAddr()) + if store.Has(indexKey) { + continue + } + store.Set(indexKey, []byte{}) + result.IndexWritten++ + } + + store.Set(types.DelegationByValIndexReadyKey, []byte{}) + result.Elapsed = time.Since(start) + return result, 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..8e2037906a --- /dev/null +++ b/sei-cosmos/x/staking/keeper/delegation_index_test.go @@ -0,0 +1,209 @@ +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), + ) + } +} + +func TestDelegationByValIndexNotReadyNoDualWrite(t *testing.T) { + _, app, ctx := createTestInput(t) + + 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) + + _, 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) + + 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, 3, result.TotalDelegations) + require.Equal(t, 3, result.IndexWritten) + 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.Equal(t, 0, repeat.IndexWritten) +} + +// 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) + + 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) +} + +// 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)) + +// TestGRPCQueryValidatorDelegationsIndexedBeyondScanLimit pins the defect the index +// closes: past query.MaxScanLimit entries the filtered scan reverts, and reading the +// per-validator prefix answers the same request. +func (suite *KeeperTestSuite) TestGRPCQueryValidatorDelegationsIndexedBeyondScanLimit() { + app, ctx := suite.app, suite.ctx + querier := keeper.Querier{Keeper: app.StakingKeeper} + + 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 indexed query falls through to the scan, so it reverts + // exactly as the query does at heights below the one that populated the index. + _, err := querier.ValidatorDelegations(sdk.WrapSDKContext(ctx), req) + suite.Error(err) + _, err = querier.ValidatorDelegationsIndexed(sdk.WrapSDKContext(ctx), req) + suite.Error(err) + + _, err = app.StakingKeeper.MigrateDelegationByValIndex(ctx) + suite.NoError(err) + + res, err := querier.ValidatorDelegationsIndexed(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()) + + // The unindexed query is untouched, so the legacy precompiles that call it keep + // the behavior they have today. + _, err = querier.ValidatorDelegations(sdk.WrapSDKContext(ctx), req) + suite.Error(err) +} + +// TestGRPCQueryValidatorDelegationsIndexedMatchesScan pins that the two read paths +// agree on a validator small enough for both to answer. +func (suite *KeeperTestSuite) TestGRPCQueryValidatorDelegationsIndexedMatchesScan() { + app, ctx := suite.app, suite.ctx + querier := keeper.Querier{Keeper: app.StakingKeeper} + + 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.ValidatorDelegationsIndexed(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..5acc9ee106 100644 --- a/sei-cosmos/x/staking/keeper/grpc_query.go +++ b/sei-cosmos/x/staking/keeper/grpc_query.go @@ -135,6 +135,61 @@ func (k Querier) ValidatorDelegations(c context.Context, req *types.QueryValidat DelegationResponses: delResponses, Pagination: pageRes}, nil } +// ValidatorDelegationsIndexed queries delegate info for a given validator, reading the +// validator-indexed delegation store when it is populated. +// +// The indexed prefix holds only this validator's delegations, so iteration tracks page +// size instead of the total delegation count. That keeps the scan inside the limit +// query.FilteredPaginateV66 enforces on consensus execution. +func (k Querier) ValidatorDelegationsIndexed(c context.Context, req *types.QueryValidatorDelegationsRequest) (*types.QueryValidatorDelegationsResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "empty request") + } + + if req.ValidatorAddr == "" { + return nil, status.Error(codes.InvalidArgument, "validator address cannot be empty") + } + + ctx := sdk.UnwrapSDKContext(c) + if !k.DelegationByValIndexReady(ctx) { + // Below the height that populated the index the entries do not exist, so the + // full scan is the only readable source. It reverts on validators too large to + // scan within the limit, which is what the same query did at those heights. + return k.ValidatorDelegations(c, req) + } + + valAddr, err := sdk.ValAddressFromBech32(req.ValidatorAddr) + if err != nil { + return nil, err + } + + var delegations []types.Delegation + store := ctx.KVStore(k.storeKey) + valPrefix := types.GetDelegationsByValIndexKey(valAddr) + indexStore := prefix.NewStore(store, valPrefix) + + pageRes, err := query.Paginate(indexStore, req.Pagination, func(key []byte, _ []byte) error { + storeKey := types.GetDelegationKeyFromValIndexKey(append(valPrefix, key...)) + 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, status.Error(codes.Internal, err.Error()) + } + + 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 +} + // ValidatorUnbondingDelegations queries unbonding delegations of a validator func (k Querier) ValidatorUnbondingDelegations(c context.Context, req *types.QueryValidatorUnbondingDelegationsRequest) (*types.QueryValidatorUnbondingDelegationsResponse, error) { if req == nil { diff --git a/sei-cosmos/x/staking/simulation/decoder.go b/sei-cosmos/x/staking/simulation/decoder.go index b6b6de2697..ee595a27b1 100644 --- a/sei-cosmos/x/staking/simulation/decoder.go +++ b/sei-cosmos/x/staking/simulation/decoder.go @@ -41,6 +41,9 @@ func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string { cdc.MustUnmarshal(kvB.Value, &delegationB) return fmt.Sprintf("%v\n%v", delegationA, delegationB) + case bytes.Equal(kvA.Key[:1], types.DelegationByValIndexKey), + bytes.Equal(kvA.Key[:1], types.DelegationByValIndexReadyKey): + return fmt.Sprintf("%X\n%X", kvA.Key, kvB.Key) case bytes.Equal(kvA.Key[:1], types.UnbondingDelegationKey), bytes.Equal(kvA.Key[:1], types.UnbondingDelegationByValIndexKey): var ubdA, ubdB types.UnbondingDelegation diff --git a/sei-cosmos/x/staking/types/keys.go b/sei-cosmos/x/staking/types/keys.go index 0712b53d36..76c0a64022 100644 --- a/sei-cosmos/x/staking/types/keys.go +++ b/sei-cosmos/x/staking/types/keys.go @@ -42,6 +42,8 @@ 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 UnbondingQueueKey = []byte{0x41} // prefix for the timestamps in unbonding queue RedelegationQueueKey = []byte{0x42} // prefix for the timestamps in redelegations queue @@ -198,6 +200,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