Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions app/upgrade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)))
}
24 changes: 24 additions & 0 deletions app/upgrades.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
}
2 changes: 1 addition & 1 deletion precompiles/staking/legacy/v67/staking.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion precompiles/staking/staking.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 4 additions & 0 deletions precompiles/staking/staking_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions precompiles/utils/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 11 additions & 3 deletions sei-cosmos/x/staking/keeper/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Genesis never activates the index

Medium Severity

SetDelegation only dual-writes after DelegationByValIndexReady, and that marker is set solely by the v6.7 upgrade handler. InitGenesis never sets the marker, and ExportGenesis does not persist the marker or 0x37 keys, so new chains and genesis imports never activate the index and validatorDelegations keeps taking the capped full scan.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 09f745d. Configure here.

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.
Expand Down
67 changes: 67 additions & 0 deletions sei-cosmos/x/staking/keeper/delegation_index.go
Original file line number Diff line number Diff line change
@@ -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())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The delegator address is parsed with sdk.AccAddressFromBech32 and its error returned, but the validator address goes through delegation.GetValidatorAddr(), which panics on a parse failure (sei-cosmos/x/staking/types/delegation.go:66-72). So the "returns an error rather than panicking on a malformed proto or address" property the PR describes holds for one of the two addresses only, and a malformed ValidatorAddress in state panics inside the upgrade handler instead of surfacing as an upgrade error. Use sdk.ValAddressFromBech32(delegation.ValidatorAddress) with the same error wrapping for symmetry.

if store.Has(indexKey) {
continue
}
store.Set(indexKey, []byte{})
result.IndexWritten++
}

store.Set(types.DelegationByValIndexReadyKey, []byte{})
result.Elapsed = time.Since(start)
return result, nil
}
Loading
Loading