informalsystems / informalsystems/hydro
all_user_lockups_with_tranche_infos runs out of query gas: historic vote lookup iterates every round since round 0
- Dominant language
- Rust
- Stars
- 11
- Forks
- 10
- Avg merge
- 17h 45m
- Merged PRs (30d)
- 4
Description
## Problem
`historic_voted_on_proposals` (contracts/hydro/src/utils.rs) scans **every round from 0 to `current_round_id`**, per lock, per tranche:
```rust
for round_id in 0..current_round_id {
if let Some(vote) = get_lock_vote(storage, round_id, tranche_id, lock_id)? { ... }
}
```
`all_user_lockups_with_tranche_infos` calls this for each lockup in the page, so a single query performs `rounds × tranches × page_size` storage lookups. The per-lockup cost grows unboundedly with the round number.
## Observed on Cosmos Hub staging (2026-08-14)
Staging runs 1-day rounds and is at `round_id` 503 with 2 tranches, i.e. **~1,000 storage reads per lockup**:
- `cosmos1mrjex8rz5yjep9yhhs09p6ku0y22cppkp8h47d` (4 lockups) with `limit: 5` or `limit: 3` → `out of gas: gasWanted: 3000000, gasUsed: 3000000` on every public endpoint tested (cosmosrescue, polkachu, lava — all enforce the default 3M query gas limit). `limit: 2` fits, barely.
- This silently froze the auctions frontend's round-data pipeline for everyone (the FE now shrinks its page size adaptively — hydro-fe `832b32b7` — but that only stretches the runway: once a *single* lockup exceeds 3M there is nothing the caller can do).
Prod is currently fine only because Neutron mainnet is at round ~21 with monthly rounds — but the cost grows every round forever, and the Hub deployment inherits whatever starting round it is seeded with (staging was seeded at ~497, so rounds 0..496 are guaranteed-empty lookups that still burn gas).
## Suggested fix: paginate the history
The code already anticipates this — the comment above the loop reads *"In future, we might want to add fields like history_start_from and history_limit when querying lockups."*
Add optional `history_start_from` / `history_limit` fields to `AllUserLockupsWithTrancheInfos` (and `SpecificUserLockupsWithTrancheInfos`), threaded through to `historic_voted_on_proposals`, so the per-call work is bounded and callers can page through vote history. Defaulting `history_limit` to a sane bound (rather than unlimited) keeps existing integrations from hitting the gas ceiling as rounds accumulate; callers that want full history page until an empty result.
Two cheap complementary wins, independent of API changes:
1. **Floor the scan at the lock's creation round** — a lock cannot have voted before it existed, so `lock_entry.lock_start` bounds the loop from below. On the Hub this alone eliminates the ~497 guaranteed-empty reads per lock/tranche.
2. Skip the scan entirely when the caller doesn't need history (e.g. a `with_history: bool` flag), since several consumers only use `next_round_lockup_can_vote` / `current_voted_on_proposal`.
Longer term, storing a per-lock vote-history index at vote time would make the query O(votes) instead of O(rounds), at the cost of a small write amplification and a migration.
## Related
Same scaling family as the `outstanding_lockup_claimable_coins` query on the tribute contract, which walks a lock's full vote history with cross-contract queries (~300k gas per entry) and exceeds 3M at ~10 entries.
Contributor guide
Research direction
Start in contracts/hydro/src/utils.rs at historic_voted_on_proposals, then trace its callers and the AllUserLockupsWithTrancheInfos and SpecificUserLockupsWithTrancheInfos definitions. Determine the intended pagination and default behavior before changing the query interfaces. Done means history work is bounded per call, callers can page through it, and lockups do not scan rounds before lock_start.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- blockchain
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100