Skip to content

feat: staking end-block queue optimization#26023

Open
randy-cro wants to merge 7 commits intocosmos:mainfrom
randy-cro:fix/staking-iterators
Open

feat: staking end-block queue optimization#26023
randy-cro wants to merge 7 commits intocosmos:mainfrom
randy-cro:fix/staking-iterators

Conversation

@randy-cro
Copy link
Contributor

@randy-cro randy-cro commented Mar 2, 2026

Pending Queue Slots Optimization

Overview

The pending queue slots optimization is a performance improvement for the staking module's end-block processing. Instead of using expensive iterators to scan through all queue entries (which can be very slow on archival nodes with large amounts of historical data), the system maintains compact indexes of pending queue slots that need to be processed.

This optimization eliminates the need for full-range iteration in end-block, replacing it with direct key-value lookups using Get/Set operations, which are significantly faster.

Architecture

The optimization maintains three separate pending slot indexes:

  1. Validator Queue Pending Slots - Tracks (time, height) slots for validator unbonding
  2. UBD Queue Pending Slots - Tracks time slots for unbonding delegations
  3. Redelegation Queue Pending Slots - Tracks time slots for redelegations

Each index is stored as a single key-value pair in the KV store, with a compact binary encoding that minimizes storage overhead.

Store Keys

The pending slot indexes are stored under the following keys (defined in x/staking/types/keys.go):

  • ValidatorQueuePendingSlotsKey = []byte{0x44} - Validator queue pending slots
  • UBDQueuePendingSlotsKey = []byte{0x45} - UBD queue pending slots
  • RedelegationQueuePendingSlotsKey = []byte{0x46} - Redelegation queue pending slots

Binary Encoding Format

Constants

The encoding uses the following constants (defined in x/staking/keeper/pending_queue_slots.go):

countBytes              = 4  // bytes for slot count (uint32 big-endian)
timeSlotSizeBytes       = 8  // uint64 bytes per slot for time-only queues (UBD, redelegation)
heightSlotSizeBytes     = 8  // uint64 bytes for height used for unbonding validators
timeHeightSlotSizeBytes = 16 // timeSlotSizeBytes + heightSlotSizeBytes

Validator Queue Pending Slots

The validator queue tracks both time and height, requiring a more complex encoding.

Byte Layout

[countBytes: 4 bytes] [slot1: 16 bytes] [slot2: 16 bytes] ... [slotN: 16 bytes]

Where:

  • countBytes (4 bytes): uint32 in big-endian format representing the number of slots
  • Each slot (16 bytes):
    • Time (8 bytes): uint64 in big-endian format representing Unix nanoseconds since epoch
    • Height (8 bytes): uint64 in big-endian format representing the block height

Example

For 2 slots:

  • Slot 1: Time = 2024-01-01 00:00:00 UTC (1704067200000000000 nanoseconds), Height = 100
  • Slot 2: Time = 2024-01-02 00:00:00 UTC (1704153600000000000 nanoseconds), Height = 200
Byte Layout:
[00 00 00 02] [17 04 06 72 00 00 00 00] [00 00 00 00 00 00 00 64] [17 04 15 36 00 00 00 00] [00 00 00 00 00 00 00 C8]
|--count--|  |--------time1 (8 bytes)--------|  |--height1 (8 bytes)--|  |--------time2 (8 bytes)--------|  |--height2 (8 bytes)--|

Detailed Breakdown

  1. Header (bytes 0-3): Count of slots

    • 00 00 00 02 = 2 slots
  2. Slot 1 (bytes 4-19):

    • Time (bytes 4-11): 17 04 06 72 00 00 00 00 = 1704067200000000000 nanoseconds
    • Height (bytes 12-19): 00 00 00 00 00 00 00 64 = 100
  3. Slot 2 (bytes 20-35):

    • Time (bytes 20-27): 17 04 15 36 00 00 00 00 = 1704153600000000000 nanoseconds
    • Height (bytes 28-35): 00 00 00 00 00 00 00 C8 = 200

Total Size Calculation

For N validator queue slots:

Total bytes = 4 + (N × 16)

UBD Queue Pending Slots

The UBD (Unbonding Delegation) queue tracks only time, using a simpler encoding.

Byte Layout

[countBytes: 4 bytes] [slot1: 8 bytes] [slot2: 8 bytes] ... [slotN: 8 bytes]

Where:

  • countBytes (4 bytes): uint32 in big-endian format representing the number of slots
  • Each slot (8 bytes): uint64 in big-endian format representing Unix nanoseconds since epoch

Example

For 3 slots:

  • Slot 1: 2024-01-01 00:00:00 UTC (1704067200000000000 nanoseconds)
  • Slot 2: 2024-01-02 00:00:00 UTC (1704153600000000000 nanoseconds)
  • Slot 3: 2024-01-03 00:00:00 UTC (1704240000000000000 nanoseconds)
Byte Layout:
[00 00 00 03] [17 04 06 72 00 00 00 00] [17 04 15 36 00 00 00 00] [17 04 24 00 00 00 00 00]
|--count--|  |--------time1 (8 bytes)--------|  |--------time2 (8 bytes)--------|  |--------time3 (8 bytes)--------|

Detailed Breakdown

  1. Header (bytes 0-3): Count of slots

    • 00 00 00 03 = 3 slots
  2. Slot 1 (bytes 4-11): 17 04 06 72 00 00 00 00 = 1704067200000000000 nanoseconds

  3. Slot 2 (bytes 12-19): 17 04 15 36 00 00 00 00 = 1704153600000000000 nanoseconds

  4. Slot 3 (bytes 20-27): 17 04 24 00 00 00 00 00 = 1704240000000000000 nanoseconds

Total Size Calculation

For N UBD queue slots:

Total bytes = 4 + (N × 8)

Redelegation Queue Pending Slots

The redelegation queue uses the same encoding format as the UBD queue (time-only).

Byte Layout

Identical to UBD queue:

[countBytes: 4 bytes] [slot1: 8 bytes] [slot2: 8 bytes] ... [slotN: 8 bytes]

Example

Same format as UBD queue example above.

Total Size Calculation

For N redelegation queue slots:

Total bytes = 4 + (N × 8)

Data Structure Properties

Sorting

All pending slot lists are maintained in sorted order:

  • Validator slots: Sorted first by time (ascending), then by height (ascending)
  • Time-only slots (UBD/Redelegation): Sorted by time (ascending)

This ensures efficient processing during end-block, as slots can be processed in order and early termination is possible when encountering future slots.

Uniqueness

Duplicate slots are automatically deduplicated when setting the pending slots:

  • Validator slots: Uniqueness is determined by the combination of (time, height)
  • Time-only slots: Uniqueness is determined by time alone

Empty Lists

When a pending slot list becomes empty, the key is deleted from the store rather than storing an empty byte array. This minimizes storage overhead.

Usage in End-Block

Validator Queue Processing

The end-block handler (UnbondAllMatureValidators) uses the pending slots as follows:

  1. Read pending slots: GetValidatorQueuePendingSlots() retrieves all pending (time, height) slots
  2. Filter mature slots: For each slot, check if slot.Height <= blockHeight AND slot.Time <= blockTime
  3. Direct lookup: For each mature slot, construct the queue key using GetValidatorQueueKey(slot.Time, slot.Height) and perform a direct Get operation
  4. Process entries: If the queue entry exists, unmarshal and process the validator addresses
  5. Cleanup: Remove processed slots from the pending list using RemoveValidatorQueuePendingSlot()

This replaces the previous approach of iterating through all keys with the ValidatorQueueKey prefix.

UBD Queue Processing

Similar pattern for unbonding delegations:

  1. Read pending slots: GetUBDQueuePendingSlots() retrieves all pending time slots
  2. Filter mature slots: For each slot, check if slot.Time <= blockTime
  3. Direct lookup: For each mature slot, construct the queue key using GetUnbondingDelegationTimeKey(slot.Time) and perform a direct Get operation
  4. Process entries: Process the unbonding delegations for that time slot
  5. Cleanup: Remove processed slots from the pending list

Redelegation Queue Processing

Same pattern as UBD queue:

  1. Read pending slots: GetRedelegationQueuePendingSlots() retrieves all pending time slots
  2. Filter mature slots: For each slot, check if slot.Time <= blockTime
  3. Direct lookup: For each mature slot, construct the queue key using GetRedelegationTimeKey(slot.Time) and perform a direct Get operation
  4. Process entries: Process the redelegations for that time slot
  5. Cleanup: Remove processed slots from the pending list

Slot Management

Adding Slots

When a new queue entry is created, the corresponding slot is added to the pending list:

  • Validator: AddValidatorQueuePendingSlot(ctx, endTime, endHeight)
  • UBD: AddUBDQueuePendingSlot(ctx, completionTime)
  • Redelegation: AddRedelegationQueuePendingSlot(ctx, completionTime)

These functions:

  1. Read the current pending slots
  2. Append the new slot
  3. Write back the updated list (which automatically sorts and deduplicates)

Removing Slots

When a queue entry is processed or deleted, the slot is removed:

  • Validator: RemoveValidatorQueuePendingSlot(ctx, endTime, endHeight)
  • UBD/Redelegation: Handled by setting the updated list after processing

Migration

The migration from v5 to v6 (x/staking/migrations/v6/store.go) populates the pending slot indexes by:

  1. Iterating through existing queue entries (one-time operation during upgrade)
  2. Extracting unique slots from the queue keys
  3. Setting the pending slot indexes

This ensures that the optimization is immediately effective after the upgrade, without requiring a full iteration in the first end-block after upgrade.

Performance Benefits

Before Optimization

  • End-block processing: Required full-range iteration over all queue keys with a prefix
  • Time complexity: O(N) where N is the total number of queue entries (including historical ones)
  • Storage I/O: High - scanning through potentially millions of keys on archival nodes
  • Block time impact: Significant delays, especially on nodes with large historical data
image

After Optimization

  • End-block processing: Direct lookups using only pending slots
  • Time complexity: O(M) where M is the number of pending slots (typically much smaller than N)
  • Storage I/O: Low - single Get per pending slot, plus one Get for the pending slots list
  • Block time impact: Minimal - only processes slots that are actually pending
image

Typical Improvement

On an archival node with millions of historical queue entries but only dozens of pending slots, the improvement can be orders of magnitude faster, reducing end-block processing time from seconds to milliseconds.

Summary

The pending queue slots optimization provides a significant performance improvement for staking module end-block processing by:

  1. Eliminating expensive iterators - Replaces full-range scans with direct lookups
  2. Minimizing storage overhead - Compact binary encoding with only essential data
  3. Maintaining data integrity - Automatic sorting and deduplication
  4. Enabling efficient processing - Only processes slots that are actually pending

The binary encoding is designed for efficiency, using fixed-size fields and big-endian byte order for consistent parsing across different architectures.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 2, 2026

Greptile Summary

This PR optimizes staking end-block processing by replacing expensive iterator-based queue scanning with a pending slot index. Instead of iterating from time 0 to current time for each queue (validators, unbonding delegations, redelegations), the code now maintains an index of slots that have entries and only checks those specific slots during end-block.

Key Changes:

  • Introduced pending slot tracking using binary-encoded indices for three queues (validator, UBD, redelegation)
  • Refactored DequeueAllMatureUBDQueue, DequeueAllMatureRedelegationQueue, and UnbondAllMatureValidators to use slot lookups instead of iterators
  • Added v5→v6 migration that populates pending slot indices from existing queue state
  • Updated tests to use relative completion times instead of Unix epoch zero

Issues Found:

  • Grammar error in x/staking/types/errors.go line 19: "cannot be decrease" should be "cannot be decreased"

Confidence Score: 4/5

  • This PR is safe to merge after fixing the grammar error
  • The optimization is well-designed with comprehensive test coverage and proper migration logic. The pending slot indices remain synchronized with queue state through all operations. Minor grammar error reduces score from 5 to 4.
  • Fix x/staking/types/errors.go line 19 grammar error before merging

Important Files Changed

Filename Overview
x/staking/keeper/pending_queue_slots.go New file implementing pending slot tracking for validator, UBD, and redelegation queues with proper binary encoding and deduplication
x/staking/keeper/delegation.go Replaced iterator-based queue processing with pending slot lookups for UBD and redelegation queues; maintains consistency between queue and index
x/staking/keeper/validator.go Refactored UnbondAllMatureValidators to use pending slot index; properly handles cleanup of deleted queue entries
x/staking/migrations/v6/store.go Migration code to populate pending slot indices from existing queue state; uses iterators once during migration
x/staking/module.go Bumped consensus version from 5 to 6 and registered migration handler
x/staking/types/errors.go Added two new error types for pending slot validation; contains one grammar error in existing error message

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    Start[End-Block Starts] --> GetSlots[Get Pending Slots from Index]
    GetSlots --> CheckEmpty{Slots Empty?}
    CheckEmpty -->|Yes| Done[Return - No Work]
    CheckEmpty -->|No| IterSlots[Iterate Pending Slots]
    
    IterSlots --> CheckMature{Slot Mature?<br/>time <= currTime<br/>height <= currHeight}
    CheckMature -->|No| IterSlots
    CheckMature -->|Yes| GetQueue[Get Queue Entry for Slot]
    
    GetQueue --> CheckNil{Queue Entry<br/>Exists?}
    CheckNil -->|No - Already Deleted| RemoveSlot[Remove from Pending Index]
    RemoveSlot --> IterSlots
    
    CheckNil -->|Yes| ProcessEntries[Process Queue Entries<br/>Unbond Validators/Delegations]
    ProcessEntries --> DeleteQueue[Delete Queue Entry]
    DeleteQueue --> UpdatePending[Update Pending Slots<br/>Remove Processed Slot]
    UpdatePending --> IterSlots
    
    IterSlots --> AllDone{More Slots?}
    AllDone -->|No| SaveIndex[Save Updated Pending Index]
    SaveIndex --> Complete[Complete]
    
    style Start fill:#e1f5e1
    style Complete fill:#e1f5e1
    style GetSlots fill:#e3f2fd
    style UpdatePending fill:#fff9c4
    style SaveIndex fill:#fff9c4
Loading

Last reviewed commit: 612cf7c

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

15 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@codecov
Copy link

codecov bot commented Mar 2, 2026

Codecov Report

❌ Patch coverage is 79.32331% with 55 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.85%. Comparing base (2fbcc8d) to head (850aff2).

Files with missing lines Patch % Lines
x/staking/keeper/delegation.go 50.98% 25 Missing ⚠️
x/staking/keeper/pending_queue_slots.go 91.26% 11 Missing ⚠️
x/staking/keeper/validator.go 82.60% 8 Missing ⚠️
x/staking/migrations/v6/store.go 81.57% 7 Missing ⚠️
x/staking/keeper/migrations.go 0.00% 3 Missing ⚠️
x/staking/module.go 50.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main   #26023      +/-   ##
==========================================
- Coverage   59.88%   59.85%   -0.04%     
==========================================
  Files         981      967      -14     
  Lines       65140    64439     -701     
==========================================
- Hits        39011    38570     -441     
+ Misses      26129    25869     -260     
Files with missing lines Coverage Δ
x/staking/types/keys.go 82.02% <ø> (ø)
x/staking/module.go 67.54% <50.00%> (-0.32%) ⬇️
x/staking/keeper/migrations.go 21.05% <0.00%> (-3.95%) ⬇️
x/staking/migrations/v6/store.go 81.57% <81.57%> (ø)
x/staking/keeper/validator.go 80.64% <82.60%> (-0.38%) ⬇️
x/staking/keeper/pending_queue_slots.go 91.26% <91.26%> (ø)
x/staking/keeper/delegation.go 73.94% <50.98%> (-1.09%) ⬇️

... and 20 files with indirect coverage changes

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@aljo242
Copy link
Contributor

aljo242 commented Mar 2, 2026

@randy-cro looks like the linter is failing!

@randy-cro
Copy link
Contributor Author

@randy-cro looks like the linter is failing!

fixed!

@randy-cro randy-cro force-pushed the fix/staking-iterators branch from 758e1e9 to 9d9a4fd Compare March 3, 2026 09:18
@randy-cro randy-cro force-pushed the fix/staking-iterators branch from 9d9a4fd to 773c8da Compare March 3, 2026 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants