Component
Proof of Personhood
Priority
P0
What happened?
An attestation can queue a reservation for a name that is already registered. The reservation can never be claimed, and because PopRules keys reservations by stem, it locks every two-digit variant of that stem for 12 weeks (MAX_RESERVATION_TIME). There is no operator or owner revoke on that queue: only the holder relinquishing, or expiry.
reserveBaseName and reserveBaseNameOnly validate the reservedBaseLabel for tier and shape but never ask whether the name is already registered:
(IPopRules.PopStatus required,) = rules.classifyName(params.reservedBaseLabel);
require(
required != IPopRules.PopStatus.Reserved && rules.isBaseName(params.reservedBaseLabel),
InvalidBaseLabel()
);
(reservedHash,) = _validateBaseLabel(params.reservedBaseLabel);
_validateBaseLabel is pure: it checks isSingleLabel() and derives the node. _enqueueReservation then checks only the caller's own reservation slot and the queue bound. Nothing on either path consults the registrar.
The precondition arises on the happy path. A successful claim deliberately frees the stem slot and _advanceExpiredHead documents the reasoning, "when the queue empties (head catches tail), the meta slot is deleted AND the PopRules base-name slot is released, so the public commit-reveal flow can register the label again". That is correct for a reservation that expired or was abandoned, but after a claim the label is registered and nothing left behind records it.
Worked example. george is 6 characters with no digit suffix, so it classifies PopFull; george01 through george99 have a 6-character stem with a 2-digit suffix, so they classify PopLite. All share the stem george.
- George Brown is attested as a lite person with lite_label = "george42" and reserved_base_label = "george", becoming queue head for the stem.
- Brown later becomes a full person and claims george through register_name. Brown owns george; the stem is free.
- George Taylor, a new lite candidate, is attested with lite_label = "george07" and reserved_base_label = "george". The stem is free, so
_writeReservation succeeds and Taylor becomes queue head for a name Brown already owns.
- Taylor's eventual register_name("george") reverts at mint, so the reservation can never be redeemed.
- Meanwhile no other lite candidate can be issued any george* variant.
_enforceReservationRules keys on the stem and rejects every caller except Taylor, so every other George is refused for 12 weeks by a reservation that can never be claimed. Since two-digit variants are what the gateway issues to lite persons, one such reservation removes an entire stem family from the lite namespace.
Expected behavior
A reserved base label that is already registered is rejected when the reservation is created, rather than accepted and discovered to be unusable at claim time. No reservation should occupy a stem whose base name already has an owner.
Reproduction
With george already registered to George Smith:
# Taylor's attestation succeeds and enqueues a reservation for a name Brown owns
reserve_name(candidate = Taylor, lite_label = "george07", reserved_base_label = "george", ...)
# The reservation is live and held by Taylor
PopRules.isBaseNameReserved("george") # (true, Taylor, now + 12 weeks)
# No other George can be issued a variant of the stem
reserve_name(candidate = Smith, lite_label = "george55", ...) # reverts: stem held by another user
# And Taylor cannot claim what they reserved
register_name(who = Taylor, label = "george", link = ...) # reverts at mint, token exists
Additional context
Add an existence check to both entry points before enqueueing, with its own error so the cause is legible on chain: Releasing the PopRules slot on claim should stay as it is. The slot means "reserved for a future claim", and a claimed name has no future claim; the registrar is the right authority for "already taken", which is what this check consults.
The client-side pre-flight is tracked on paritytech/host-rust-core#349 as a review comment, but it is not the security boundary: the attester submits the extrinsic, so any other client or a direct submission bypasses a client-side check. This guard is the real fix.
One consequence to decide alongside it: after this lands, an invalid reserved_base_label reverts the whole attestation, and because the pallet performs the lite mint and the reservation in one call, the candidate loses their lite username too. Either the client validates first (the truapi change), or the gateway skips an unusable reservation while letting the lite mint proceed. The first is preferable; the second hides the failure.
Separately, whether a PopFull registration should reserve its own stem is a policy question with wider consequences, since it would remove all 100 two-digit variants of every registered full name from the lite namespace. It would close the second route above but is not needed for this fix, and belongs in its own issue.
Component
Proof of Personhood
Priority
P0
What happened?
An attestation can queue a reservation for a name that is already registered. The reservation can never be claimed, and because
PopRuleskeys reservations by stem, it locks every two-digit variant of that stem for 12 weeks (MAX_RESERVATION_TIME). There is no operator or owner revoke on that queue: only the holder relinquishing, or expiry.reserveBaseNameandreserveBaseNameOnlyvalidate thereservedBaseLabelfor tier and shape but never ask whether the name is already registered:_validateBaseLabelis pure: it checksisSingleLabel()and derives the node._enqueueReservationthen checks only the caller's own reservation slot and the queue bound. Nothing on either path consults the registrar.The precondition arises on the happy path. A successful claim deliberately frees the stem slot and
_advanceExpiredHeaddocuments the reasoning, "when the queue empties (head catches tail), the meta slot is deleted AND the PopRules base-name slot is released, so the public commit-reveal flow can register the label again". That is correct for a reservation that expired or was abandoned, but after a claim the label is registered and nothing left behind records it.Worked example. george is 6 characters with no digit suffix, so it classifies PopFull; george01 through george99 have a 6-character stem with a 2-digit suffix, so they classify PopLite. All share the stem george.
_writeReservationsucceeds and Taylor becomes queue head for a name Brown already owns._enforceReservationRuleskeys on the stem and rejects every caller except Taylor, so every other George is refused for 12 weeks by a reservation that can never be claimed. Since two-digit variants are what the gateway issues to lite persons, one such reservation removes an entire stem family from the lite namespace.Expected behavior
A reserved base label that is already registered is rejected when the reservation is created, rather than accepted and discovered to be unusable at claim time. No reservation should occupy a stem whose base name already has an owner.
Reproduction
With george already registered to George Smith:
Additional context
Add an existence check to both entry points before enqueueing, with its own error so the cause is legible on chain: Releasing the PopRules slot on claim should stay as it is. The slot means "reserved for a future claim", and a claimed name has no future claim; the registrar is the right authority for "already taken", which is what this check consults.
The client-side pre-flight is tracked on paritytech/host-rust-core#349 as a review comment, but it is not the security boundary: the attester submits the extrinsic, so any other client or a direct submission bypasses a client-side check. This guard is the real fix.
One consequence to decide alongside it: after this lands, an invalid reserved_base_label reverts the whole attestation, and because the pallet performs the lite mint and the reservation in one call, the candidate loses their lite username too. Either the client validates first (the truapi change), or the gateway skips an unusable reservation while letting the lite mint proceed. The first is preferable; the second hides the failure.
Separately, whether a PopFull registration should reserve its own stem is a policy question with wider consequences, since it would remove all 100 two-digit variants of every registered full name from the lite namespace. It would close the second route above but is not needed for this fix, and belongs in its own issue.