feat(server): pool allowance slots across personhood collections - #431
Merged
Conversation
Every ring read in the allowance path was scoped to LitePeople, and the slot budget came from `LiteStmtStoreSlotsPerPeriod` alone. A person with full personhood holds a second membership whose alias space is entirely separate, so the core reported `no free StatementStore slot` while seqs sat free in the `People` collection. `PersonhoodCollection` now carries the collection identifier, the extension variant, and the slot-budget constant, and it is threaded through ring reads, slot scans, the `AsResources` and `AsPgas` extensions, and the CLI. Registration pools across every collection the device can prove: a free slot anywhere beats evicting a live one, and when everything is full the victim is the globally oldest replaceable slot rather than the oldest in whichever collection was scanned first. Exhaustion reports the summed budget. Membership is settled on chain by looking for a ring that includes each member key, so nothing depends on local personhood state and a chain that does not run a collection is skipped rather than raised. The existing-allocation check runs across all collections before any ring snapshot is fetched, which keeps the established-product path off the expensive read and stops a target holding a slot in one collection from being handed a second one in another.
…d to one Resolving memberships treats each collection independently. A collection whose ring storage is unreadable, whose exponent has no proof domain, or whose slot budget this chain does not declare is logged and skipped, so a device that can only prove light personhood still gets its allowance. An error is raised only when every candidate failed, which keeps an RPC outage from being reported as "not a member". Validating the ring exponent while the membership set is built matters because selection happens before the proof: an unusable ring reached at proof time would fail the whole call with no collection left to fall back to. A preselected slot now declares whether claiming it evicts a live one. Pooled registration chooses the eviction victim itself, so without that the takeover guard stayed unset: a refused takeover surfaced as a raw RPC string instead of `ReplacementRefused`, and a duplicate-submit retry could evict a second slot while the first submission was still in flight. One registration revokes at most one allowance, and the pooled path now holds to that. Exhaustion raised by the inner rescan is restated as the pooled budget so the same error never means two different things.
Asset Hub declares `Pgas.MaxClaimsPerPeriodPerPerson` alongside `MaxClaimsPerPeriodPerLitePerson`, so the claim budget is per collection like the statement-store slot budget. A PGAS claim proved against the `People` ring is scanned against the full-person budget, which is what a full person is entitled to spend.
Imod7
reviewed
Aug 17, 2026
Imod7
approved these changes
Aug 17, 2026
Reading the period's slot tables is now one pass, `scan_collections`, whose result is handed to registration instead of being read again. On-demand allocation previously scanned every collection and then had pooled registration scan them all over, since the pooled parameters had no equivalent of the `preselected` field that used to keep the tables read once. The pass stops as soon as it finds an allowance the target already holds, and per-collection failures are logged and skipped so an unreadable collection cannot fail an allocation another could satisfy. Replacing a live slot is now gated on `allow_eviction`, off for on-demand allocation and on for renewal, the pairing-time grant, and `alloc-check --submit`. Pooling had given a product connect the power to revoke another product's allowance, which was never the on-demand path's job: a full period is reported as exhaustion, and reclaiming space belongs to the renewal pass, which only ever replaces on behalf of its own ledger. Bulletin long-term storage prefers the light collection. It has a single budget with no per-collection variant, yet its spent-alias counters still derive from the claiming collection's entropy, so claiming as a full person would hide the counters already spent as a light one and restart the scan at zero. Full personhood remains the fallback for a device without light personhood. The exhaustion figure is summed over the collections in play rather than over the ones that reported full, so the retry path reports the device's budget instead of zero when the first choice was a free slot.
…m the runtime bounds
filip-parity
added a commit
that referenced
this pull request
Aug 18, 2026
main's #431 introduced PersonhoodCollection and made the ring, root and subscriber-root helpers take it; the branch's identifier-based generalisation of the same helpers folds into that API. read_member_ring_index and read_subscriber_ring_exponent take a PersonhoodCollection; register-name uses PersonhoodCollection::People. ChainState literals carry restrict_origins.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every ring read in the allowance path is scoped to a
PersonhoodCollection, which carries the collection identifier, the transaction-extension variant, and the per-collection budgets. Registration pools slots across every collection thedevice can prove membership in.
A person with full personhood holds a second membership whose alias space is entirely separate, with its own budget. Pooling makes that capacity reachable, so a full
Peopletable no longer reportsno free StatementStore slotwhile seqs sit free inLitePeople.Behaviour
PeoplebeforeLitePeople.ReplacementRefused.protectedis scoped per collection, because the sameseqin two collections is two unrelated slots.Pgas.MaxClaimsPerPeriodPerPersonalongsideMaxClaimsPerPeriodPerLitePerson.alloc-checkreports both collections' member keys, ring indices, and slot tables.Live
Paseo People spec 1000032 and Asset Hub spec 2000036:
live People: slots=20 ring_exponent=9 current_ring_index=0 extension_variant=0
live LitePeople: slots=10 ring_exponent=9 current_ring_index=14 extension_variant=1
live pooled budget=30
live AsPgas: Claim=0 People=0 | Claim=0 LitePeople=1
live PGAS claims per day: People=100 LitePeople=40
alloc-check → People: free seq=0 | LitePeople: free seq=2
Collections[People].ring_sizedecodes, so the space-padded collection identifieraddresses a real entry rather than silently reading nothing. The battery's
allowance rows (
Resource Allocation/request,auto_signing_e2e, all fourStatement Storerows) pass, so a real registration goes through end to end.