diff --git a/sei-cosmos/x/staking/keeper/delegation.go b/sei-cosmos/x/staking/keeper/delegation.go index 51de4ad5ed..5e89698460 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 delegationByValIndexActive(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 delegationByValIndexActive(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..0801658254 --- /dev/null +++ b/sei-cosmos/x/staking/keeper/delegation_index.go @@ -0,0 +1,109 @@ +package keeper + +import ( + "time" + + "golang.org/x/mod/semver" + + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" +) + +// DelegationByValIndexUpgrade is the upgrade at which SetDelegation and +// RemoveDelegation begin maintaining the validator-indexed delegation store. +const DelegationByValIndexUpgrade = "v6.7" + +// delegationByValIndexActive reports whether SetDelegation and RemoveDelegation +// should maintain the validator-indexed delegation store. +// +// Live execution always does: the current binary only ever executes at/after +// the upgrade that ships this behavior, so a non-tracing block is never a +// pre-upgrade block. The only place pre-upgrade behavior must be reproduced is +// when re-tracing a historical block, where the era is signaled through +// ClosestUpgradeName (see app.RPCContextProvider). +func delegationByValIndexActive(ctx sdk.Context) bool { + if !ctx.IsTracing() { + return true + } + return semver.Compare(ctx.ClosestUpgradeName(), DelegationByValIndexUpgrade) >= 0 +} + +// BackfillDelegationByValIndexResult reports the outcome of a delegation index backfill. +type BackfillDelegationByValIndexResult struct { + TotalDelegations int + IndexWritten int + AlreadyIndexed int + DryRun bool + Elapsed time.Duration +} + +// BackfillDelegationByValIndexProgress is a progress snapshot during backfill. +type BackfillDelegationByValIndexProgress struct { + TotalDelegations int + IndexWritten int + AlreadyIndexed int + Elapsed time.Duration +} + +// BackfillProgress reports incremental backfill progress. Nil disables callbacks. +type BackfillProgress func(BackfillDelegationByValIndexProgress) + +const ( + backfillProgressDelegationInterval = 100_000 + backfillProgressMinInterval = 10 * time.Second +) + +// BackfillDelegationByValIndex writes validator-indexed delegation keys for existing +// delegations. When dryRun is true, delegations are counted but no store writes occur. +func (k Keeper) BackfillDelegationByValIndex(ctx sdk.Context, dryRun bool, progress BackfillProgress) BackfillDelegationByValIndexResult { + start := time.Now() + result := BackfillDelegationByValIndexResult{DryRun: dryRun} + lastProgressReport := start + + reportProgress := func() { + if progress == nil { + return + } + progress(BackfillDelegationByValIndexProgress{ + TotalDelegations: result.TotalDelegations, + IndexWritten: result.IndexWritten, + AlreadyIndexed: result.AlreadyIndexed, + Elapsed: time.Since(start), + }) + lastProgressReport = time.Now() + } + + store := ctx.KVStore(k.storeKey) + iterator := sdk.KVStorePrefixIterator(store, types.DelegationKey) + defer func() { _ = iterator.Close() }() + + for ; iterator.Valid(); iterator.Next() { + delegation := types.MustUnmarshalDelegation(k.cdc, iterator.Value()) + result.TotalDelegations++ + + delegatorAddress := sdk.MustAccAddressFromBech32(delegation.DelegatorAddress) + valAddr := delegation.GetValidatorAddr() + indexKey := types.GetDelegationByValIndexKey(delegatorAddress, valAddr) + if store.Has(indexKey) { + result.AlreadyIndexed++ + } else { + if !dryRun { + store.Set(indexKey, []byte{}) + } + result.IndexWritten++ + } + + if progress == nil { + continue + } + now := time.Now() + if result.TotalDelegations == 1 || + result.TotalDelegations%backfillProgressDelegationInterval == 0 || + now.Sub(lastProgressReport) >= backfillProgressMinInterval { + reportProgress() + } + } + + result.Elapsed = time.Since(start) + return result +} 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..7f08080429 --- /dev/null +++ b/sei-cosmos/x/staking/keeper/delegation_index_test.go @@ -0,0 +1,111 @@ +package keeper_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/types" +) + +func TestDelegationByValIndexDualWrite(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, store.Has(indexKey)) + + 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 TestDelegationByValIndexTracingPreUpgradeNoDualWrite(t *testing.T) { + _, app, ctx := createTestInput(t) + ctx = ctx.WithIsTracing(true).WithClosestUpgradeName("v6.6") + + 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.False(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 TestDelegationByValIndexTracingPostUpgradeDualWrite(t *testing.T) { + _, app, ctx := createTestInput(t) + ctx = ctx.WithIsTracing(true).WithClosestUpgradeName("v6.7") + + 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)) +} + +func TestBackfillDelegationByValIndex(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)), + } + + store := ctx.KVStore(app.StakingKeeper.GetStoreKey()) + for _, delegation := range delegations { + delegatorAddress := sdk.MustAccAddressFromBech32(delegation.DelegatorAddress) + valAddr := delegation.GetValidatorAddr() + store.Set( + types.GetDelegationKey(delegatorAddress, valAddr), + types.MustMarshalDelegation(app.AppCodec(), delegation), + ) + } + + dryRunResult := app.StakingKeeper.BackfillDelegationByValIndex(ctx, true, nil) + require.Equal(t, 3, dryRunResult.TotalDelegations) + require.Equal(t, 3, dryRunResult.IndexWritten) + require.Equal(t, 0, dryRunResult.AlreadyIndexed) + require.True(t, dryRunResult.DryRun) + for _, delegation := range delegations { + delegatorAddress := sdk.MustAccAddressFromBech32(delegation.DelegatorAddress) + valAddr := delegation.GetValidatorAddr() + require.False(t, store.Has(types.GetDelegationByValIndexKey(delegatorAddress, valAddr))) + } + + writeResult := app.StakingKeeper.BackfillDelegationByValIndex(ctx, false, nil) + require.Equal(t, 3, writeResult.TotalDelegations) + require.Equal(t, 3, writeResult.IndexWritten) + require.Equal(t, 0, writeResult.AlreadyIndexed) + require.False(t, writeResult.DryRun) + for _, delegation := range delegations { + delegatorAddress := sdk.MustAccAddressFromBech32(delegation.DelegatorAddress) + valAddr := delegation.GetValidatorAddr() + require.True(t, store.Has(types.GetDelegationByValIndexKey(delegatorAddress, valAddr))) + } + + repeatResult := app.StakingKeeper.BackfillDelegationByValIndex(ctx, false, nil) + require.Equal(t, 3, repeatResult.TotalDelegations) + require.Equal(t, 0, repeatResult.IndexWritten) + require.Equal(t, 3, repeatResult.AlreadyIndexed) +} diff --git a/sei-cosmos/x/staking/simulation/decoder.go b/sei-cosmos/x/staking/simulation/decoder.go index b6b6de2697..b6c3dd9eaf 100644 --- a/sei-cosmos/x/staking/simulation/decoder.go +++ b/sei-cosmos/x/staking/simulation/decoder.go @@ -41,6 +41,8 @@ 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): + 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..94763d47e0 100644 --- a/sei-cosmos/x/staking/types/keys.go +++ b/sei-cosmos/x/staking/types/keys.go @@ -42,6 +42,7 @@ 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 UnbondingQueueKey = []byte{0x41} // prefix for the timestamps in unbonding queue RedelegationQueueKey = []byte{0x42} // prefix for the timestamps in redelegations queue @@ -198,6 +199,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 diff --git a/tools/cmd.go b/tools/cmd.go index 882056784d..d0902812b3 100644 --- a/tools/cmd.go +++ b/tools/cmd.go @@ -3,6 +3,7 @@ package tools import ( "github.com/spf13/cobra" + stakingcmd "github.com/sei-protocol/sei-chain/tools/staking/cmd" scanner "github.com/sei-protocol/sei-chain/tools/tx-scanner/cmd" ) @@ -12,5 +13,6 @@ func ToolCmd() *cobra.Command { Short: "A set of useful tools for sei chain", } toolsCmd.AddCommand(scanner.ScanCmd()) + toolsCmd.AddCommand(stakingcmd.StakingCmd()) return toolsCmd } diff --git a/tools/staking/cmd/backfill.go b/tools/staking/cmd/backfill.go new file mode 100644 index 0000000000..b83f88bd6b --- /dev/null +++ b/tools/staking/cmd/backfill.go @@ -0,0 +1,135 @@ +package cmd + +import ( + "bufio" + "fmt" + "os" + + tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + "github.com/spf13/cobra" + + "github.com/sei-protocol/sei-chain/app" + "github.com/sei-protocol/sei-chain/sei-cosmos/client/input" + stakingkeeper "github.com/sei-protocol/sei-chain/sei-cosmos/x/staking/keeper" + stakingtool "github.com/sei-protocol/sei-chain/tools/staking" +) + +const backfillDelegationIndexNotice = `This command is for benchmarking and analysis only. It measures how long it +takes to populate the validator-indexed delegation secondary index. Do NOT use +it to migrate live chain state. + +How to use: + 1. Stop the node whose state you want to measure. + 2. Copy the node home directory to a path outside your real Sei data directory + (for example: cp -a ~/.sei /tmp/sei-backfill-bench). + 3. Run a dry-run against the copy (default; does not modify state): + seid tools staking backfill-delegation-index --home /path/to/copy + 4. To measure write and commit cost on the copy only, add --write: + seid tools staking backfill-delegation-index --home /path/to/copy --write + 5. Review elapsed time and delegations_per_second in the output. + +Never point --home at a running node's directory. Never use --write on production +data. --write cannot be combined with --height.` + +// BackfillDelegationIndexCmd backfills validator-indexed delegation keys for benchmarking. +func BackfillDelegationIndexCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "backfill-delegation-index", + Short: "Benchmark validator-indexed delegation index population (not for live migration)", + Long: backfillDelegationIndexNotice, + RunE: runBackfillDelegationIndex, + } + + stakingtool.BindServerFlags(cmd, app.DefaultNodeHome) + cmd.Flags().Int64("height", 0, "Load state at this height on the copy (0 = latest committed)") + cmd.Flags().Bool("write", false, "Write index keys and commit state on the copy (benchmark only; never on live data)") + return cmd +} + +func runBackfillDelegationIndex(cmd *cobra.Command, _ []string) error { + height, err := cmd.Flags().GetInt64("height") + if err != nil { + return err + } + write, err := cmd.Flags().GetBool("write") + if err != nil { + return err + } + dryRun := !write + + if write && height > 0 { + return fmt.Errorf("--write cannot be used with --height") + } + + fmt.Fprintln(os.Stderr, backfillDelegationIndexNotice) + if write { + fmt.Fprintln(os.Stderr, "WARNING: --write mutates state in the directory passed to --home. Use a copy, not live data.") + } + + confirmed, err := input.GetConfirmation( + "Proceed with this benchmark run on the directory passed to --home?", + bufio.NewReader(cmd.InOrStdin()), + cmd.ErrOrStderr(), + ) + if err != nil { + return err + } + if !confirmed { + return fmt.Errorf("aborted") + } + + fmt.Fprintln(os.Stderr, "loading application state...") + seiApp, err := stakingtool.LoadApp(cmd, height) + if err != nil { + return err + } + defer func() { + if err := seiApp.Close(); err != nil { + fmt.Fprintf(os.Stderr, "Error closing sei app: %v", err) + } + }() + + blockHeight := seiApp.LastBlockHeight() + if blockHeight == 0 { + blockHeight = 1 + } + + ctx := seiApp.NewUncachedContext(false, tmproto.Header{Height: blockHeight}) + fmt.Fprintln(os.Stderr, "backfill started") + result := seiApp.StakingKeeper.BackfillDelegationByValIndex(ctx, dryRun, reportBackfillProgress) + + fmt.Printf("dry_run=%t height=%d\n", result.DryRun, blockHeight) + fmt.Printf("total_delegations=%d index_written=%d already_indexed=%d elapsed=%s\n", + result.TotalDelegations, + result.IndexWritten, + result.AlreadyIndexed, + result.Elapsed, + ) + if result.TotalDelegations > 0 && result.Elapsed > 0 { + perSec := float64(result.TotalDelegations) / result.Elapsed.Seconds() + fmt.Printf("delegations_per_second=%.2f\n", perSec) + } + + if !write { + return nil + } + + commitID := seiApp.CommitMultiStore().Commit(true) + fmt.Printf("committed_version=%d\n", commitID.Version) + return nil +} + +func reportBackfillProgress(p stakingkeeper.BackfillDelegationByValIndexProgress) { + perSec := 0.0 + if p.Elapsed > 0 { + perSec = float64(p.TotalDelegations) / p.Elapsed.Seconds() + } + fmt.Fprintf(os.Stderr, + "progress total_delegations=%d index_written=%d already_indexed=%d elapsed=%s delegations_per_second=%.2f\n", + p.TotalDelegations, + p.IndexWritten, + p.AlreadyIndexed, + p.Elapsed, + perSec, + ) +} diff --git a/tools/staking/cmd/backfill_test.go b/tools/staking/cmd/backfill_test.go new file mode 100644 index 0000000000..b12bf09cd6 --- /dev/null +++ b/tools/staking/cmd/backfill_test.go @@ -0,0 +1,204 @@ +package cmd + +import ( + "bytes" + "context" + "encoding/json" + "io" + "os" + "path/filepath" + "strings" + "testing" + + abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + + "github.com/sei-protocol/sei-chain/app" + "github.com/sei-protocol/sei-chain/sei-cosmos/client/flags" + "github.com/sei-protocol/sei-chain/sei-cosmos/server" + srvconfig "github.com/sei-protocol/sei-chain/sei-cosmos/server/config" + seidbconfig "github.com/sei-protocol/sei-chain/sei-db/config" + tmcfg "github.com/sei-protocol/sei-chain/sei-tendermint/config" +) + +type testAppToml struct { + srvconfig.Config + + StateCommit seidbconfig.StateCommitConfig `mapstructure:"state-commit"` + StateStore seidbconfig.StateStoreConfig `mapstructure:"state-store"` + ReceiptStore seidbconfig.ReceiptStoreConfig `mapstructure:"receipt-store"` +} + +func writeTestAppToml(t *testing.T, path string) { + t.Helper() + + cfg := testAppToml{ + Config: *srvconfig.DefaultConfig(), + StateCommit: seidbconfig.DefaultStateCommitConfig(), + StateStore: seidbconfig.DefaultStateStoreConfig(), + ReceiptStore: seidbconfig.DefaultReceiptStoreConfig(), + } + cfg.MinGasPrices = "0.01usei" + cfg.StateCommit.Enable = true + cfg.StateCommit.HashLogger.Enable = false + cfg.StateStore.Enable = true + + template := srvconfig.ManualConfigTemplate + + seidbconfig.StateCommitConfigTemplate + + seidbconfig.StateStoreConfigTemplate + + seidbconfig.ReceiptStoreConfigTemplate + srvconfig.SetConfigTemplate(template) + srvconfig.WriteConfigFile(path, cfg) +} + +func seedCommittedSeiDBApp(t *testing.T, home string) int64 { + t.Helper() + + encCfg := app.MakeEncodingConfig() + seiApp := app.New( + nil, + nil, + false, + map[int64]bool{}, + home, + 1, + true, + tmcfg.TestConfig(), + encCfg, + app.GetWasmEnabledProposals(), + app.TestAppOpts{UseSc: true}, + app.EmptyWasmOpts, + app.EmptyAppOptions, + ) + + genesisState := app.NewDefaultGenesisState(encCfg.Marshaler) + stateBytes, err := json.MarshalIndent(genesisState, "", " ") + require.NoError(t, err) + + _, err = seiApp.InitChain(&abci.RequestInitChain{ + ConsensusParams: app.DefaultConsensusParams, + ChainId: "sei-test", + AppStateBytes: stateBytes, + }) + require.NoError(t, err) + + commitID := seiApp.CommitMultiStore().Commit(true) + require.NoError(t, seiApp.Close()) + return commitID.Version +} + +func backfillCmdWithHome(t *testing.T, home string, args ...string) *cobra.Command { + t.Helper() + + cmd := BackfillDelegationIndexCmd() + serverCtx := server.NewDefaultContext() + cmd.SetContext(context.WithValue(context.Background(), server.ServerContextKey, serverCtx)) + cmd.SetIn(strings.NewReader("y\n")) + serverCtx.Viper.Set(flags.FlagHome, home) + serverCtx.Viper.Set("chain-id", "sei-test") + serverCtx.Viper.SetConfigFile(filepath.Join(home, "config", "app.toml")) + require.NoError(t, serverCtx.Viper.ReadInConfig()) + + cmd.SetArgs(args) + return cmd +} + +func captureCommandOutput(t *testing.T, fn func()) (stdout, stderr string) { + t.Helper() + + stdoutR, stdoutW, err := os.Pipe() + require.NoError(t, err) + stderrR, stderrW, err := os.Pipe() + require.NoError(t, err) + + oldStdout, oldStderr := os.Stdout, os.Stderr + os.Stdout, os.Stderr = stdoutW, stderrW + t.Cleanup(func() { + os.Stdout, os.Stderr = oldStdout, oldStderr + }) + + fn() + + require.NoError(t, stdoutW.Close()) + require.NoError(t, stderrW.Close()) + os.Stdout, os.Stderr = oldStdout, oldStderr + + var stdoutBuf, stderrBuf bytes.Buffer + _, err = io.Copy(&stdoutBuf, stdoutR) + require.NoError(t, err) + _, err = io.Copy(&stderrBuf, stderrR) + require.NoError(t, err) + return stdoutBuf.String(), stderrBuf.String() +} + +func TestBackfillDelegationIndexDryRun(t *testing.T) { + home := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(home, "config"), 0o755)) + writeTestAppToml(t, filepath.Join(home, "config", "app.toml")) + seedCommittedSeiDBApp(t, home) + + cmd := backfillCmdWithHome(t, home) + stdout, stderr := captureCommandOutput(t, func() { + require.NoError(t, cmd.Execute()) + }) + + require.Contains(t, stdout, "dry_run=true") + require.NotContains(t, stdout, "committed_version=") + require.Contains(t, stderr, "benchmarking and analysis only") + require.Contains(t, stderr, "--home /path/to/copy") +} + +func TestBackfillDelegationIndexWrite(t *testing.T) { + home := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(home, "config"), 0o755)) + writeTestAppToml(t, filepath.Join(home, "config", "app.toml")) + version := seedCommittedSeiDBApp(t, home) + + cmd := backfillCmdWithHome(t, home, "--write") + stdout, stderr := captureCommandOutput(t, func() { + require.NoError(t, cmd.Execute()) + }) + + require.Contains(t, stdout, "dry_run=false") + require.Contains(t, stdout, "committed_version=") + require.Contains(t, stderr, "benchmarking and analysis only") + require.Contains(t, stderr, "WARNING:") + require.Greater(t, version, int64(0)) +} + +func TestBackfillDelegationIndexWriteRejectsHeight(t *testing.T) { + home := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(home, "config"), 0o755)) + writeTestAppToml(t, filepath.Join(home, "config", "app.toml")) + + cmd := backfillCmdWithHome(t, home, "--write", "--height", "1") + err := cmd.Execute() + require.Error(t, err) + require.Contains(t, err.Error(), "--write cannot be used with --height") +} + +func TestBackfillDelegationIndexAbortsWithoutConfirmation(t *testing.T) { + home := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(home, "config"), 0o755)) + writeTestAppToml(t, filepath.Join(home, "config", "app.toml")) + seedCommittedSeiDBApp(t, home) + + cmd := BackfillDelegationIndexCmd() + serverCtx := server.NewDefaultContext() + cmd.SetContext(context.WithValue(context.Background(), server.ServerContextKey, serverCtx)) + cmd.SetIn(strings.NewReader("n\n")) + serverCtx.Viper.Set(flags.FlagHome, home) + serverCtx.Viper.Set("chain-id", "sei-test") + serverCtx.Viper.SetConfigFile(filepath.Join(home, "config", "app.toml")) + require.NoError(t, serverCtx.Viper.ReadInConfig()) + + stdout, stderr := captureCommandOutput(t, func() { + err := cmd.Execute() + require.Error(t, err) + require.Contains(t, err.Error(), "aborted") + }) + + require.Empty(t, stdout) + require.Contains(t, stderr, "benchmarking and analysis only") +} diff --git a/tools/staking/cmd/staking.go b/tools/staking/cmd/staking.go new file mode 100644 index 0000000000..f51b63ad9a --- /dev/null +++ b/tools/staking/cmd/staking.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// StakingCmd returns offline staking maintenance commands. +func StakingCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "staking", + Short: "Offline staking maintenance tools", + } + cmd.AddCommand(BackfillDelegationIndexCmd()) + return cmd +} diff --git a/tools/staking/loadapp.go b/tools/staking/loadapp.go new file mode 100644 index 0000000000..c6981b4f40 --- /dev/null +++ b/tools/staking/loadapp.go @@ -0,0 +1,53 @@ +package staking + +import ( + "fmt" + + "github.com/sei-protocol/sei-chain/app" + "github.com/sei-protocol/sei-chain/sei-cosmos/client/flags" + "github.com/sei-protocol/sei-chain/sei-cosmos/codec" + "github.com/sei-protocol/sei-chain/sei-cosmos/server" + "github.com/spf13/cast" + "github.com/spf13/cobra" +) + +// LoadApp opens the application at the latest committed height, or at height when +// height is positive. +func LoadApp(cmd *cobra.Command, height int64) (*app.App, error) { + serverCtx := server.GetServerContextFromCmd(cmd) + home := cast.ToString(serverCtx.Viper.Get(flags.FlagHome)) + if home == "" { + return nil, fmt.Errorf("home directory is required") + } + + encCfg := app.MakeEncodingConfig() + encCfg.Marshaler = codec.NewProtoCodec(encCfg.InterfaceRegistry) + + seiApp := app.New( + nil, + nil, + false, + map[int64]bool{}, + home, + 1, + true, + serverCtx.Config, + encCfg, + app.GetWasmEnabledProposals(), + serverCtx.Viper, + app.EmptyWasmOpts, + app.EmptyAppOptions, + ) + if height > 0 { + if err := seiApp.LoadHeight(height); err != nil { + return nil, fmt.Errorf("load height %d: %w", height, err) + } + } + return seiApp, nil +} + +// BindServerFlags wires the home flag for offline app commands. Config loading +// is handled by the root seid PersistentPreRunE. +func BindServerFlags(cmd *cobra.Command, defaultHome string) { + cmd.PersistentFlags().String(flags.FlagHome, defaultHome, "The application home directory") +}