Skip to content

PLT-980: Add validator-indexed delegation store (phase 1) - #3979

Open
amir-deris wants to merge 9 commits into
mainfrom
amir/plt-980-validator-delegations-query-second-index
Open

PLT-980: Add validator-indexed delegation store (phase 1)#3979
amir-deris wants to merge 9 commits into
mainfrom
amir/plt-980-validator-delegations-query-second-index

Conversation

@amir-deris

Copy link
Copy Markdown
Contributor

Summary

  • Add a validator→delegator secondary index (0x37) for staking delegations, following the existing unbonding-delegation index pattern (0x33).
  • Dual-write index entries in SetDelegation / RemoveDelegation so new mutations stay indexed.
  • Add BackfillDelegationByValIndex keeper helper for one-shot backfill of existing delegations.
  • Add seid tools staking backfill-delegation-index dev CLI to benchmark backfill duration before choosing halt vs incremental migration.

This is phase 1 only. It does not yet switch ValidatorDelegations to the fast query path, add migration state, EndBlocker incremental migration, or an upgrade handler.

Linear: https://linear.app/seilabs/issue/PLT-980/

Test plan

  • Unit tests for index key round-trip, dual-write, and backfill (dry-run / write / idempotent re-run)
    go test ./sei-cosmos/x/staking/types/... ./sei-cosmos/x/staking/keeper/... \
      -run 'TestDelegationByValIndex|TestBackfillDelegation|TestGetDelegationKeyFromValIndexKey'
  • Build CLI
    go build -o build/seid ./cmd/seid
    build/seid tools staking backfill-delegation-index --help

RPC node backfill benchmark (manual)

Use this to measure mainnet-scale backfill time and decide between one-shot upgrade backfill vs incremental EndBlocker migration.

  1. Build this branch

    go build -o build/seid ./cmd/seid
  2. Prepare state (do not run against a live writing node)

    • Stop the RPC node, or take a filesystem snapshot/copy of its home directory.
    • Work on a copy of $HOME/data when using --write.
  3. Dry-run first (read-only; safe on a copy)

    build/seid tools staking backfill-delegation-index \
      --home /path/to/node-copy

    Note total_delegations, elapsed, and delegations_per_second.

  4. Optional write benchmark (mutates state; copy only)

    build/seid tools staking backfill-delegation-index \
      --home /path/to/node-copy \
      --write

    This writes missing index keys and commits state. Expect output including index_written and committed_version.

  5. Optional: pin height

    build/seid tools staking backfill-delegation-index \
      --home /path/to/node-copy \
      --height 12345678
  6. Decision gate

    • If dry-run/write elapsed time fits acceptable upgrade halt budget → follow-up PR can wire the same backfill at upgrade height.
    • If too slow → follow-up PR adds incremental EndBlocker migration + fast query path behind a completion flag.

Made with Cursor

Dual-write a validator→delegator secondary index on delegation mutations and
expose a dev CLI to benchmark one-shot backfill before choosing migration strategy.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedAug 25, 2026, 10:17 AM

@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.67630% with 49 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.58%. Comparing base (1a9dda1) to head (9b66144).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
tools/staking/cmd/backfill.go 62.50% 20 Missing and 7 partials ⚠️
sei-cosmos/x/staking/keeper/delegation_index.go 64.28% 14 Missing and 1 partial ⚠️
tools/staking/loadapp.go 82.14% 3 Missing and 2 partials ⚠️
sei-cosmos/x/staking/simulation/decoder.go 0.00% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3979      +/-   ##
==========================================
- Coverage   59.05%   57.58%   -1.47%     
==========================================
  Files        2297     2229      -68     
  Lines      196538   186699    -9839     
==========================================
- Hits       116074   107519    -8555     
+ Misses      69722    69354     -368     
+ Partials    10742     9826     -916     
Flag Coverage Δ
sei-chain-pr 69.12% <71.67%> (?)
sei-db 69.80% <ø> (ø)
sei-db-state-db ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
sei-cosmos/x/staking/keeper/delegation.go 84.14% <100.00%> (+0.17%) ⬆️
sei-cosmos/x/staking/types/keys.go 82.84% <100.00%> (+1.42%) ⬆️
tools/cmd.go 100.00% <100.00%> (ø)
tools/staking/cmd/staking.go 100.00% <100.00%> (ø)
sei-cosmos/x/staking/simulation/decoder.go 2.94% <0.00%> (-0.19%) ⬇️
tools/staking/loadapp.go 82.14% <82.14%> (ø)
sei-cosmos/x/staking/keeper/delegation_index.go 64.28% <64.28%> (ø)
tools/staking/cmd/backfill.go 62.50% <62.50%> (ø)

... and 229 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@amir-deris amir-deris self-assigned this Aug 21, 2026
@amir-deris
amir-deris marked this pull request as ready for review August 21, 2026 13:10
Comment thread tools/staking/cmd/backfill.go Outdated
seidroid[bot]
seidroid Bot previously requested changes Aug 21, 2026

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Phase-1 validator→delegator index is well-structured and mirrors the existing UBD index, but the dual-write into the committed staking store is ungated (changing the app hash despite the non-app-hash-breaking label), and the CLI's --write path calls CommitMultiStore().Commit(false), which unconditionally panics under SeiDB.

Findings: 2 blocking | 6 non-blocking | 5 posted inline

Blockers

  • None at the file/PR level.
  • 2 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • [suggestion] No test exercises the --write path of seid tools staking backfill-delegation-index. The keeper-level backfill is covered, but the CLI wiring (app load, uncached context, commit) is untested — which is why the Commit(false) panic is latent. Even a smoke test over a temporary home directory would catch it.
  • [suggestion] Dual-writing without a backfill leaves the index partially populated on existing chains (only delegations touched after the upgrade get an entry), with no completion marker in state. Phase 2 must not treat a missing 0x37 entry as "no delegation"; consider landing the migration-state flag in the same release as the dual-write so the two can never be observed out of sync.
  • [suggestion] DelegationByValIndexKey = 0x37 is declared out of numeric order between 0x33 and 0x34 in the prefix block. Since the surrounding block is ordered by byte value and is the only place collisions can be spotted by eye, place it after RedelegationByValDstIndexKey = 0x36.
  • 3 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread tools/staking/cmd/backfill.go Outdated
Comment thread sei-cosmos/x/staking/keeper/delegation.go Outdated
Comment thread tools/staking/cmd/backfill.go
Comment thread tools/staking/loadapp.go Outdated
Comment thread tools/staking/loadapp.go Outdated
Comment thread tools/staking/cmd/backfill.go Outdated
amir-deris and others added 4 commits August 24, 2026 15:49
Use Commit(true) so storev2 does not panic, reject --write with --height,
and close the app so SeiDB flushes before exit.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep SetDelegation and RemoveDelegation app-hash neutral before the
upgrade by writing the validator index only once ClosestUpgradeName
reaches v6.7.

Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the subcommand PersistentPreRunE that shadowed the root seid hook
and collapsed duplicate app.New paths into one call with optional
LoadHeight.

Co-authored-by: Cursor <cursoragent@cursor.com>
Place 0x37 after the other delegation prefixes and smoke-test dry-run,
--write commit, and incompatible flag handling for the backfill tool.

Co-authored-by: Cursor <cursoragent@cursor.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 24c0ccc. Configure here.

Comment thread sei-cosmos/x/staking/keeper/delegation_index.go
Dual-write on DeliverTx and gate pre-v6.7 behavior only for tracing
replay via ClosestUpgradeName, matching the distribution keeper pattern.

Co-authored-by: Cursor <cursoragent@cursor.com>
@amir-deris

Copy link
Copy Markdown
Contributor Author

@seidroid review

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The validator→delegator index is correctly keyed, gated with the same tracing/semver pattern x/distribution already uses, and all five previously flagged blockers/suggestions are genuinely resolved. What remains is non-blocking: the benchmark CLI's reported throughput excludes the commit that does the actual work, and its --write test runs against a genesis with zero delegations.

Findings: 0 blocking | 3 non-blocking | 2 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] Turning the dual-write on at the upgrade without a backfill in the same release leaves the index permanently incomplete for pre-upgrade delegations that are never touched again. Phase 2 must not read 0x37 as authoritative until a completion marker exists in state; landing the migration-state flag in the same release as the read-path switch is what makes the two impossible to observe out of sync. (Repeat of an earlier non-blocking note — the PR body acknowledges this as phase-2 scope, so it is recorded rather than pressed.)
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

return nil
}

commitID := seiApp.CommitMultiStore().Commit(true)

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] elapsed / delegations_per_second are printed before this line, so they exclude the commit — which is where nearly all the work happens on the --write path.

NewUncachedContext hands out app.cms, and storev2/rootmulti.GetKVStore returns the raw commitment.Store (no cache wrap). Its Set only appends to an in-memory changeset (sei-cosmos/storev2/commitment/store.go:80-86); the memiavl insertion and the SC/SS flush all happen inside Commit(true) here. So the --write number the tool reports is effectively the dry-run number plus some appends, and the decision gate in the PR description ("does elapsed fit the upgrade halt budget?") would be sized against the wrong figure.

Time the commit as well and print it (e.g. commit_elapsed= plus a total), so the halt-budget comparison uses persist-inclusive numbers. Worth noting in the same breath: because every write is buffered until this point, peak RSS scales with the delegation count — useful to report alongside the timing.

Separately, Commit(true) writes version LastBlockHeight()+1 with no corresponding block, so the data copy is not resumable as a node afterwards. The Long help says to use a copy, but calling that out explicitly there would save someone a confusing restart failure.

})

require.Contains(t, stdout, "dry_run=false")
require.Contains(t, stdout, "committed_version=")

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] This test never exercises a write. seedCommittedSeiDBApp runs InitChain with NewDefaultGenesisState and no gentxs, so the staking genesis has no delegations — the command reports total_delegations=0 index_written=0 and Commit(true) flushes an empty changeset. The assertions here only prove the command reached the commit call.

That does guard the previous Commit(false) panic, which is the main thing. But the behaviour the flag exists for — index keys surviving the commit — is unverified. Seed at least one delegation into the staking store before committing, then after cmd.Execute() reopen the app from home and assert store.Has(types.GetDelegationByValIndexKey(del, val)). That is also the only test that would catch the commitment-store buffering semantics (Set invisible to Get/Has until commit) regressing.

@seidroid
seidroid Bot dismissed their stale review August 24, 2026 14:44

Superseded: latest AI review found no blocking issues.

amir-deris and others added 2 commits August 25, 2026 11:12
…ress.

Clarify that the CLI is for analysis only, require confirmation before running, and emit stderr heartbeats while scanning delegations so long mainnet runs are observable.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant