Skip to content

Commit 796dd12

Browse files
committed
fix: request the full pending outbound set; the server pages by offset and returns no next key (F-2026-18817)
1 parent 19c3b6f commit 796dd12

2 files changed

Lines changed: 71 additions & 281 deletions

File tree

universalClient/pushcore/pushCore.go

Lines changed: 36 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"fmt"
1010
"math/big"
1111
"strings"
12-
"sync"
1312
"sync/atomic"
1413

1514
cmtservice "github.com/cosmos/cosmos-sdk/client/grpc/cmtservice"
@@ -41,13 +40,6 @@ type Client struct {
4140
authClients []authtypes.QueryClient // Auth query clients
4241
conns []*grpc.ClientConn // Owned gRPC connections (for cleanup)
4342
rr uint32 // Round-robin counter for endpoint selection
44-
45-
// Pagination cursor carried between pending-outbound polls. The page budget
46-
// bounds one poll's work; this makes the budget cost latency rather than
47-
// coverage, so a set larger than the budget is still walked in full over
48-
// successive ticks. Nil means start from the beginning.
49-
pendingMu sync.Mutex
50-
pendingCursor []byte
5143
}
5244

5345
// New creates a new Client by dialing the provided gRPC URLs.
@@ -405,8 +397,14 @@ func (c *Client) GetPendingFundMigrations(ctx context.Context) ([]*utsstypes.Fun
405397
// Page size and page cap for the pending-outbound walk. The cap bounds a single
406398
// poll; the remainder is picked up on the next tick.
407399
const (
408-
pendingOutboundPageSize = 1000
409-
pendingOutboundMaxPages = 20
400+
// AllPendingOutbounds pages by offset and returns only Total, never a NextKey,
401+
// so a key-based walk stops after one page. It also loads and sorts the whole
402+
// collection per call, so paging saves the server nothing. The sort is not
403+
// stable and orders on CreatedAt, a block height, so rows sharing a height can
404+
// change relative order between calls — which makes offset paging able to skip
405+
// a row outright. One generous request plus a Total check is the only shape
406+
// that is both correct and cheap against that server.
407+
pendingOutboundLimit = 100_000
410408

411409
chainConfigPageSize = 200
412410
chainConfigMaxPages = 20
@@ -415,68 +413,39 @@ const (
415413
// GetAllPendingOutbounds retrieves pending outbound transactions from Push Chain,
416414
// sorted by created_at (block height) ascending — oldest first.
417415
//
418-
// The result is paged rather than a single query. An outbound only leaves the
419-
// pending set once a quorum vote terminalizes it, so any row that cannot reach
420-
// one — for example a destination execution whose observation was never seen —
421-
// stays at the head of an oldest-first list forever. Reading one fixed page
422-
// would let such a prefix hide every newer outbound from signing, on every
423-
// chain, since this query is not chain-scoped.
416+
// An outbound only leaves the pending set once a quorum vote terminalizes it, so
417+
// a row that cannot reach one stays at the head of an oldest-first list forever.
418+
// The request must therefore cover the whole set: a fixed small page would let
419+
// such a prefix hide every newer outbound from signing, on every chain, since
420+
// this query is not chain-scoped.
424421
func (c *Client) GetAllPendingOutbounds(ctx context.Context) ([]*uexecutortypes.PendingOutboundEntry, []*uexecutortypes.OutboundTx, error) {
425-
c.pendingMu.Lock()
426-
defer c.pendingMu.Unlock()
427-
428-
var (
429-
entries []*uexecutortypes.PendingOutboundEntry
430-
outbounds []*uexecutortypes.OutboundTx
431-
nextKey = c.pendingCursor
422+
resp, err := retryWithRoundRobin(
423+
len(c.uexecutorClients),
424+
&c.rr,
425+
func(idx int) (*uexecutortypes.QueryAllPendingOutboundsResponse, error) {
426+
return c.uexecutorClients[idx].AllPendingOutbounds(ctx, &uexecutortypes.QueryAllPendingOutboundsRequest{
427+
Pagination: &query.PageRequest{Limit: pendingOutboundLimit},
428+
})
429+
},
430+
"GetAllPendingOutbounds",
431+
c.logger,
432432
)
433+
if err != nil {
434+
return nil, nil, err
435+
}
433436

434-
for page := 0; page < pendingOutboundMaxPages; page++ {
435-
key := nextKey
436-
resp, err := retryWithRoundRobin(
437-
len(c.uexecutorClients),
438-
&c.rr,
439-
func(idx int) (*uexecutortypes.QueryAllPendingOutboundsResponse, error) {
440-
return c.uexecutorClients[idx].AllPendingOutbounds(ctx, &uexecutortypes.QueryAllPendingOutboundsRequest{
441-
Pagination: &query.PageRequest{Key: key, Limit: pendingOutboundPageSize},
442-
})
443-
},
444-
"GetAllPendingOutbounds",
445-
c.logger,
446-
)
447-
if err != nil {
448-
// Return what we have rather than nothing: a later page failing must
449-
// not stop the caller acting on the pages that did arrive. Resume from
450-
// the failed page next time instead of losing the ground already made.
451-
if len(entries) > 0 {
452-
c.pendingCursor = key
453-
c.logger.Warn().Err(err).Int("page", page).Msg("pending outbound page failed, using pages fetched so far")
454-
return entries, outbounds, nil
455-
}
456-
c.pendingCursor = nil
457-
return nil, nil, err
458-
}
459-
460-
entries = append(entries, resp.Entries...)
461-
outbounds = append(outbounds, resp.Outbounds...)
462-
463-
if resp.Pagination == nil || len(resp.Pagination.NextKey) == 0 {
464-
// Reached the end; the next poll starts from the beginning again so
465-
// rows added at the tail since the walk began are picked up.
466-
c.pendingCursor = nil
467-
return entries, outbounds, nil
468-
}
469-
nextKey = resp.Pagination.NextKey
437+
// Total is authoritative for the size of the pending set, so a shortfall means
438+
// rows we will not act on this poll. Loud rather than silent: those outbounds
439+
// are invisible to signing until the set shrinks.
440+
if resp.Pagination != nil && resp.Pagination.Total > uint64(len(resp.Entries)) {
441+
c.logger.Error().
442+
Uint64("total", resp.Pagination.Total).
443+
Int("received", len(resp.Entries)).
444+
Uint64("limit", pendingOutboundLimit).
445+
Msg("pending outbound set exceeds the request limit; the remainder is not being signed")
470446
}
471447

472-
// Budget spent mid-set. Park the cursor so the next tick continues from here
473-
// rather than re-reading the same prefix forever.
474-
c.pendingCursor = nextKey
475-
c.logger.Warn().
476-
Int("max_pages", pendingOutboundMaxPages).
477-
Int("fetched", len(entries)).
478-
Msg("pending outbound page budget spent; continuing from this cursor next poll")
479-
return entries, outbounds, nil
448+
return resp.Entries, resp.Outbounds, nil
480449
}
481450

482451
// createGRPCConnection creates a gRPC connection with appropriate transport security.

0 commit comments

Comments
 (0)