Skip to content

feat(server): pool allowance slots across personhood collections - #431

Merged
TarikGul merged 11 commits into
mainfrom
tg/ring-collections
Aug 17, 2026
Merged

feat(server): pool allowance slots across personhood collections#431
TarikGul merged 11 commits into
mainfrom
tg/ring-collections

Conversation

@TarikGul

@TarikGul TarikGul commented Aug 17, 2026

Copy link
Copy Markdown
Member

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 the
device 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 People table no longer reports no free StatementStore slot while seqs sit free in LitePeople.

Behaviour

  • Collections are tried widest budget first, so a full person spends People before LitePeople.
  • A free slot in any collection is taken before any live slot is evicted. Eviction revokes an allowance someone is using, so it happens only once every collection is full.
  • When everything is full, the slot replaced is the globally oldest replaceable one across all collections. A preselected takeover still counts against the one-revocation-per-registration limit, so a duplicate-submit retry gives up rather than evicting a second slot, and a refused takeover is named as ReplacementRefused.
  • protected is scoped per collection, because the same seq in two collections is two unrelated slots.
  • Exhaustion reports the summed budget, including when raised by the inner rescan.
  • PGAS claims are scanned against the budget of the collection they are proved against: Asset Hub declares Pgas.MaxClaimsPerPeriodPerPerson alongside MaxClaimsPerPeriodPerLitePerson.
  • Each collection resolves independently. One whose ring storage is unreadable, whose exponent has no proof domain, or whose 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".
  • The existing-allocation check covers every collection before any ring snapshot is fetched. That keeps the established-product path off the ring pages, and stops a target holding a slot in one collection from being handed a second one in another.

alloc-check reports 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_size decodes, so the space-padded collection identifier
addresses a real entry rather than silently reading nothing. The battery's
allowance rows (Resource Allocation/request, auto_signing_e2e, all four
Statement Store rows) pass, so a real registration goes through end to end.

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.
@TarikGul
TarikGul requested a review from a team August 17, 2026 20:33
…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.
Comment thread rust/crates/truapi-server/src/runtime/statement_allowance.rs Outdated
Comment thread rust/crates/truapi-server/src/runtime/statement_allowance/slot.rs Outdated
Comment thread rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs
Comment thread rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs Outdated
Comment thread rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs Outdated
Comment thread rust/crates/truapi-server/src/runtime/statement_allowance.rs Outdated
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.
@TarikGul
TarikGul added this pull request to the merge queue Aug 17, 2026
Merged via the queue into main with commit bc074ba Aug 17, 2026
16 checks passed
@TarikGul
TarikGul deleted the tg/ring-collections branch August 17, 2026 22:48
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.
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