Skip to content

fix(frontend): stop the transactions worker from paging forever - #8050

Open
yhabib wants to merge 3 commits into
mainfrom
fix/unbounded-recursion-icrc-transactions
Open

fix(frontend): stop the transactions worker from paging forever#8050
yhabib wants to merge 3 commits into
mainfrom
fix/unbounded-recursion-icrc-transactions

Conversation

@yhabib

@yhabib yhabib commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Motivation

The transactions worker pages through the index canister until a page stops giving new transaction IDs. It trusts the oldest ID from each page, with no limit and no check for forward progress. A hostile index canister behind an imported token can return pages that never end. The worker loop then never stops, and memory grows without a bound.

Changes

  • Changed getIcrcAccountTransactions from recursion to a loop, and flattened the fetched pages once instead of on each unwind.
  • Added a progress check: the loop stops when a page does not lower the oldest transaction ID below the start of its own call.
  • Added a hard cap, DEFAULT_INDEX_TRANSACTION_MAX_PAGES, at 10 pages per sync.
  • Added unit tests for a hostile index that repeats a page, a hostile index that makes slow progress, and the honest multi-page path.
  • Added an e2e test that counts index canister requests per sync against the cap.
  • Added a changelog entry under Security.

Tests

  • npm run check in frontend: pass, 0 errors and 0 warnings.
  • CI=true npm run test in frontend: pass, 5908 tests.
  • Four mutation checks on the new unit tests: each expected test fails when the fix is removed piece by piece.
  • The new e2e spec type-checks and lists with Playwright, but did not run against a live replica; no local replica is available here. The CI e2e job must confirm it.

Todos

  • Accessibility (a11y) – no impact, this change is inside a web worker, with no screen or control changed.
  • Changelog – added, under Security.

The transactions worker asked the Index canister for the next page of
transactions in a recursion. Nothing capped the number of pages, and nothing
checked that a page moved the oldest transaction id down. An Index canister
that ignores "start" made the worker fetch pages without an end. The worker
memory and the emitted payload grew until the tab froze.

Rewrite getIcrcAccountTransactions as a loop.
- Stop after DEFAULT_INDEX_TRANSACTION_MAX_PAGES pages in one sync.
- Stop when a page does not move the oldest transaction id below the start of
  the call that produced it.
- Collect the pages and flatten them once, which removes the O(n^2) copy of
  the previous unwind.
…tion

The spec watches the index canister traffic of an imported token. It groups
the get_account_transactions requests into syncs and checks that no sync
sends more than DEFAULT_INDEX_TRANSACTION_MAX_PAGES requests.
@yhabib
yhabib requested a review from a team as a code owner September 4, 2026 22:23
@zeropath-ai

zeropath-ai Bot commented Sep 4, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to 09ee557.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► frontend/src/lib/constants/constants.ts
    Add DEFAULT_INDEX_TRANSACTION_MAX_PAGES constant
► frontend/src/lib/worker-services/icrc-transactions.worker-services.ts
    Import and use DEFAULT_INDEX_TRANSACTION_MAX_PAGES along with page limit
► frontend/src/tests/lib/worker-services/icrc-transactions.worker-services.spec.ts
    Add tests for max pages and paging behavior
► frontend/src/tests/e2e/transactions-worker-pagination.spec.ts
    Add end-to-end test to verify bounded number of index calls per sync

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new loop’s oldestTxId computation sorts bigint IDs using Number(...), which can overflow/lose precision and undermine the pagination stop/progress logic.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR hardens the ICRC transactions web worker against hostile/misbehaving index canisters by ensuring pagination cannot run indefinitely (preventing unbounded memory growth during sync).

Changes:

  • Reworked ICRC index pagination from recursion to an iterative loop with a “forward progress” check and a hard per-sync page cap.
  • Added unit tests covering repeated pages, slow progress, and multi-page pagination behavior.
  • Added a Playwright e2e spec that verifies index-canister request bursts stay within the configured page cap, plus a Security changelog entry.
File summaries
File Description
frontend/src/lib/worker-services/icrc-transactions.worker-services.ts Replaces recursive paging with a bounded loop and adds progress/cap stopping conditions.
frontend/src/lib/constants/constants.ts Introduces DEFAULT_INDEX_TRANSACTION_MAX_PAGES to cap pages per sync.
frontend/src/tests/lib/worker-services/icrc-transactions.worker-services.spec.ts Adds unit tests for hostile/no-progress pages, max-pages cap, and correct start chaining.
frontend/src/tests/e2e/transactions-worker-pagination.spec.ts Adds an e2e check that sync bursts do not exceed the max-pages cap.
CHANGELOG-Nns-Dapp-unreleased.md Documents the security hardening of transactions sync pagination.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread frontend/src/lib/worker-services/icrc-transactions.worker-services.ts Outdated
The pagination loop found the oldest transaction id with a sort
comparator that converted a bigint difference to Number. A hostile
index canister can answer an id above Number.MAX_SAFE_INTEGER, and
that conversion loses precision. A wrong oldest id can pick a wrong
result and defeat the progress and page-cap checks.

Find the oldest id with a reduce that compares the bigint ids
directly. No Number conversion, no lost precision.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approved

The termination/cap logic and test coverage align with the stated security motivation, and the remaining findings are minor documentation wording/accuracy nits.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

frontend/src/lib/constants/constants.ts:10

  • The trade-off comment says the UI will show a gap "until the user reloads the page", but older transactions can also be fetched via the existing "load next/older transactions" path (pagination). Consider wording that doesn’t imply a full reload is required.
    frontend/src/lib/worker-services/icrc-transactions.worker-services.ts:139
  • The comment refers to a non-existent/incorrect constant name (DEFAULT_ICRC_TRANSACTION_PAGE_LIMIT); this file uses DEFAULT_INDEX_TRANSACTION_PAGE_LIMIT, so the comment is misleading.
  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

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.

2 participants