Skip to content

fix: F-2026-18817 | [Dual Defense] SVM Log Truncation + Resolver No Success-Vote Starves Oldest-1000 Pending Queue - #318

Open
Aman035 wants to merge 8 commits into
audit-fixesfrom
F-2026-18817
Open

fix: F-2026-18817 | [Dual Defense] SVM Log Truncation + Resolver No Success-Vote Starves Oldest-1000 Pending Queue#318
Aman035 wants to merge 8 commits into
audit-fixesfrom
F-2026-18817

Conversation

@Aman035

@Aman035 Aman035 commented Aug 20, 2026

Copy link
Copy Markdown
Member

F-2026-18817 Pending outbound poll reads a single page

Issue

  • GetAllPendingOutbounds requested one Limit: 1000 page and ignored Pagination. The query is oldest-first and not chain-scoped.
  • An outbound only leaves the pending set on a quorum vote, so a row whose success was never observed sits at the head permanently.
  • A stuck prefix therefore hides every newer outbound from signing — on every chain, not just SVM.

Approach taken

  • Walk Pagination.NextKey instead of reading one page.
  • The page budget bounds one poll's work, and the cursor is carried between polls so it costs latency rather than coverage. Budget spent mid-set parks the cursor and the next tick resumes there; reaching the end resets it so tail additions are picked up. Without the carry a fixed budget is just a higher wall — 20k instead of 1k — and everything past it is dropped on every poll.
  • A page failing part-way returns what was already fetched and parks at the failed page, rather than discarding signable rows or losing ground. A first-page failure is still an error.

Also found while auditing the other list queries: GetAllChainConfigs sent no PageRequest at all, and the server uses query.CollectionPaginate, which applies the SDK default of 100 when the request is nil. Chain configs past the 100th were silently dropped, and a chain missing from that list is simply never watched. Now paged.

Left alone: GetPendingTssEvents (PendingTssEvents is only written by InitiateTssKeyProcess, which rejects concurrent processes, so the set is 0 or 1).

Not changed

The resolver still votes failure only; success observation stays with the event listener by design. A success vote derived from chain state cannot supply a deterministic GasFeeUsed, and that value is part of the outbound ballot key, so validators would have to agree on something ExecutedSubTx does not expose — it is existence-only. Making the success signal durable belongs on the emitting side, raised with the gateway team separately.

Tests

  • Follows NextKey across pages, asserting the cursor is threaded through rather than re-queried.
  • Cursor carry: first poll spends the budget and parks, second resumes after it rather than re-reading the prefix, and the end of the walk resets it.
  • Page failure parks at the failed page and keeps earlier results; first-page failure errors.
  • Stops immediately on a single page and on nil Pagination.
  • Chain configs: follows NextKey, always sends a PageRequest, propagates errors.
  • Mutation checked: stopping after the first page fails the walk tests, and resetting the cursor instead of parking fails the carry test.

Note for core

AllUniversalValidators and PendingFundMigrations ignore pagination entirely — they Walk the whole collection and return it. No truncation risk, but no bound either, so the response grows with the set until it hits the gRPC message limit and the query fails outright. AllUniversalValidators feeds IsKnownPeer, the TSS admission gate from F-2026-18136. Worth moving both to CollectionPaginate like their siblings; the client already walks pages.

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.

1 participant