Skip to content

refactor(be): allocate application numbers from a monotonic cell - #4233

Open
sea-snake wants to merge 22 commits into
refactor/account-reference-write-pathfrom
refactor/monotonic-application-numbers
Open

refactor(be): allocate application numbers from a monotonic cell#4233
sea-snake wants to merge 22 commits into
refactor/account-reference-write-pathfrom
refactor/monotonic-application-numbers

Conversation

@sea-snake

@sea-snake sea-snake commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Design: #4222. Overview: #4230.

ApplicationNumber is derived from lookup_application_with_origin_memory.len(), which is only safe while nothing is ever removed. Once an application can be removed, len() collides with a live number rather than merely reusing a retired one:

applications {0, 1, 2} exist        len() == 3
remove 1                            len() == 2
next new origin is assigned 2       already owned by a live application

Both origins would then share one account universe: every (anchor, 2) reference list and config list would belong to both.

Numbers now come from a monotonic StableCell<u64> at memory index 33. The counter is what guarantees a number is never reissued, because it only climbs — a number it has passed is not offered again even after that application's list is retired.

The counter cannot be the whole answer on its own, because it postdates the applications numbered before it existed. So allocation takes max(counter, highest_stored + 1), and the map being keyed by ApplicationNumber means last_key_value gives that highest number exactly, holes in the lists or not. Nor can the floor stand alone: removing the highest list walks it backwards, which is what the counter is there to prevent.

Both steps are checked and refuse at the ceiling rather than saturating, for the reason allocate_account_number refuses — the number keys both the application list and the origin index, so reissuing one puts two origins on a single list. allocate_application_number and lookup_or_insert_application_number_with_origin return Result for that; all three production callers propagate.

ApplicationNumber never appears in internet_identity.did, so this is invisible to clients.

Rollback needs a release note. The cell at index 33 is exactly what a rollback discards, restoring len() as the allocator over a map that removals have punched holes in. Rolling forward recovers a safe allocator, since the cell can only be raised, but a collision created during the rollback window has already happened.

Tests: application_number_allocator_tests (7). The two the design exists for are a_gap_below_the_highest_number_is_not_handed_out_again — a hole in the lists while the counter is behind, the one state a list count gets wrong — and a_removal_before_the_first_allocation_does_not_collide_with_a_live_number. Both fail if the floor goes back to len().

🤖 Generated with Claude Code

https://claude.ai/code/session_01LVi99RYo2jyi2kCurgovNJ

@sea-snake
sea-snake requested a review from a team as a code owner August 19, 2026 00:05
@zeropath-ai

zeropath-ai Bot commented Aug 19, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to d9034cf.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► src/internet_identity/src/account_management.rs
    Modify set_default_account_for_origin to map errors to SetDefaultAccountError
Enhancement ► src/internet_identity/src/storage.rs
    Add NEXT_APPLICATION_NUMBER_MEMORY_INDEX and NEXT_APPLICATION_NUMBER_MEMORY_ID
    Introduce next_application_number_memory in Storage and initialize it
    Update allocation logic with allocate_application_number to manage application numbers
    Use allocate_application_number in lookup_or_insert_application_number_with_origin and related paths
    Commentary and error handling enhancements around application number allocation
    Extend StorageError with ApplicationsCounterOverflow and ErrorUpdatingApplicationNumberAllocator and corresponding Display text
Enhancement ► src/internet_identity/src/storage/account/tests.rs
    Update test to unwrap result from lookup_or_insert_application_number_with_origin
Enhancement ► src/internet_identity/src/storage/tests.rs
    Update tests to unwrap results from lookup_or_insert_application_number_with_origin across multiple scenarios

Copilot AI lite review requested due to automatic review settings August 19, 2026 07:07
@sea-snake
sea-snake force-pushed the refactor/monotonic-application-numbers branch from 2d8007b to 9f9c380 Compare August 19, 2026 07:07

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sea-snake
sea-snake force-pushed the refactor/monotonic-application-numbers branch from 9f9c380 to 233d32c Compare August 19, 2026 08:37
@sea-snake
sea-snake force-pushed the refactor/monotonic-application-numbers branch 2 times, most recently from 782f91d to e9b957f Compare August 20, 2026 17:24
@sea-snake sea-snake added the feature:tracked-default-accounts Design: tracked default accounts, application removal, principal lookup label Aug 22, 2026
`lookup_application_with_origin_memory.len()` is only a safe source of new
application numbers while nothing is ever removed. Once an application can be
reaped, `len()` does not merely reuse a retired number, it collides with a live
one: with `{0, 1, 2}`, removing `1` leaves `len() == 2`, which is owned.

Numbers now come from a cell at memory index 33, seeded when storage is loaded
rather than at the first allocation. Existing numbers are dense from zero, so the
row count is the first free number at that moment; deferring the seed would let a
removal shrink the count first and hand out a live number.

Implements docs/ongoing/tracked-default-accounts.md §8.3 (D17).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sea-snake
sea-snake force-pushed the refactor/monotonic-application-numbers branch from e9b957f to e06729b Compare August 22, 2026 18:42
Comment thread src/internet_identity/src/storage.rs Outdated
The row count is exact only for data written before this counter existed. Once
retiring an application removes a row without reissuing its number the count
undershoots, and the maximum is what keeps allocation monotonic across both.
Comment thread src/internet_identity/src/storage.rs Outdated
Comment thread src/internet_identity/src/storage.rs Outdated
Comment thread src/internet_identity/src/storage.rs Outdated
aterga
aterga previously requested changes Sep 4, 2026

@aterga aterga left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

PR description isn't self-contained.

If apps can be removed, why not store the index of the next-free app ID rather than try to derive from the length of a collection?

…notonic-application-numbers

# Conflicts:
#	src/internet_identity/src/storage/tests.rs
Comment thread src/internet_identity/src/storage.rs Outdated
sea-snake and others added 3 commits September 4, 2026 17:33
Seeding ran at load and took `max(stored_counter, stable_application_memory.len())`.
The row count is only the first free number while numbering is dense from zero,
so it needed three sentences of doc explaining when it could be trusted, and it
had to be seeded eagerly — deferring would let a removal shrink the count first.

The map is keyed by `ApplicationNumber`, so `last_key_value` gives the highest
number outright. That is exact whether or not the rows have holes, which removes
the density assumption rather than documenting around it, and with it the reason
to seed eagerly: the allocator takes `max(counter, highest + 1)` where it
allocates. `seed_application_number_allocator` is gone, and so is its
constructor-time `expect` — there was nowhere for it to report to.

The counter is still what guarantees a number is never reissued, because it only
climbs; the highest stored number is a floor for the applications that predate
it. Removing the highest row walks that floor backwards, which is why it cannot
be the answer on its own.

Both arithmetic steps are checked, refusing rather than saturating for the reason
`allocate_account_number` refuses: the number keys the application row and the
origin index, so reissuing one would put two origins on a single row and have
them share its accounts and counters. `allocate_application_number` and
`lookup_or_insert_application_number_with_origin` return `Result` for that.

`a_gap_below_the_highest_number_is_not_handed_out_again` covers the state a row
count gets wrong: a hole in the rows while the counter knows nothing.
`a_removal_before_the_first_allocation_does_not_collide_with_a_live_number` now
covers it too — it used to pass under a row count only because the removal
happened after seeding, and there is no seeding step for it to follow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVi99RYo2jyi2kCurgovNJ
@sea-snake
sea-snake requested a review from aterga September 4, 2026 17:41
sea-snake and others added 7 commits September 5, 2026 14:36
Both create paths allocated a number and stored the account, then asked for the
application number — a fallible step after two writes, in functions whose whole
shape is about not doing that. Nothing in between depends on the number.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVi99RYo2jyi2kCurgovNJ
"Row" was this stack's own word: `origin/main` uses it for something else.
The storage layer already names this thing — the account reference list — in
its types and functions, so the comments now say what the code says.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVi99RYo2jyi2kCurgovNJ
"Row" was this stack's own word: `origin/main` uses it for something else.
The storage layer already names this thing — the account reference list — in
its types and functions, so the comments now say what the code says.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVi99RYo2jyi2kCurgovNJ
@sea-snake
sea-snake dismissed aterga’s stale review September 7, 2026 23:27

Code has changed since

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature:tracked-default-accounts Design: tracked default accounts, application removal, principal lookup

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants