Skip to content

account_objects unconditionally walks NFT pages even when type filter excludes NFTokenPage #3060

@RaymondSeven

Description

@RaymondSeven

When a caller passes a type filter (e.g. type=offer) to account_objects, Clio still traverses the entire NFT-page linked list before starting the directory walk. Rippled skips NFT iteration entirely in this case via iterateNFTPages. The result: up to limit (max 400) wasted backend reads per request that return zero matching objects and consume the pagination budget.

Current behavior

AccountObjectsHandler::process calls traverseOwnedNodes(..., nftIncluded=true) unconditionally. Inside traverseOwnedNodes (RPCHelpers.cpp:668), the branch enters traverseNFTObjects based solely on nftIncluded and the marker shape — the caller's typeFilter is never consulted. Each NFT page costs one fetchLedgerObject. The limit is decremented by pages fetched (limit -= nftsCount), so a type=offer limit=400 request on an account with ≥400 NFT pages returns an empty array with a marker, having exhausted the entire budget on irrelevant reads.

Expected behavior

Mirror rippled's guard at AccountObjects.cpp:60-62:

bool iterateNFTPages =
    (!typeFilter.has_value() || typeMatchesFilter(typeFilter.value(), ltNFTOKEN_PAGE)) &&
    dirIndex == beast::zero;

When the type filter excludes ltNFTOKEN_PAGE, skip the NFT traversal entirely and give the full limit budget to the directory walk.

Impact

  • Filtered requests against NFT-heavy accounts are orders of magnitude more expensive on Clio than on rippled for zero useful output.
  • Pagination forces ceil(N_pages / limit) round trips of pure waste before any matching object is returned.
  • Public Clio endpoints absorb unnecessary Cassandra/storage load from any client using type filters on NFT-holding accounts.

Suggested fix

Option A — plumb typeFilter into traverseOwnedNodes and gate the NFT branch:

bool const filterAllowsNFT =
    !typeFilter.has_value() ||
    std::find(typeFilter->begin(), typeFilter->end(), ripple::ltNFTOKEN_PAGE) != typeFilter->end();

if (nftIncluded and filterAllowsNFT and (!jsonCursor or isNftMarkerNonZero)) {

Option B — in the handler, set nftIncluded = false when typeFilter excludes ltNFTOKEN_PAGE.

Affected code

  • src/rpc/RPCHelpers.cpptraverseOwnedNodes (8-arg overload), line 668
  • src/rpc/handlers/AccountObjects.cppprocess, line 92 (hardcoded true)
  • tests/unit/rpc/handlers/AccountObjectsTests.cppTypeFilter test (line 572) mocks the wasted NFT fetch as expected; should assert Times(0) after fix

Rippled reference

src/xrpld/rpc/handlers/account/AccountObjects.cpp:60-62 — the iterateNFTPages short-circuit that Clio is missing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    📋 Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions