DotnsPopController

Git Source

Inherits: Initializable, UUPSUpgradeable, OwnableUpgradeable, ERC165Upgradeable, IDotnsPopController, IDotnsPopControllerLegacy

Title: DotnsPopController

Dedicated PoP controller that issues device names and personhood names on behalf of the dotNS gateway pallet.

Lives behind its own UUPS proxy with its own storage. Registered on DotnsRegistrar via addController, which is how multiple controllers coexist on the same registrar without interfering with each other. Enforcement: Devicehood and personhood are attested off-chain by the gateway before the call reaches this contract, so the on-chain personhood precompile is not re-queried on the gateway path. Every issuance path still calls @custom:function IPopRules.classifyName to reject governance-reserved labels (@custom:reverts InvalidPersonhoodLabel for personhood names,

Notes:

  • reverts: InvalidDeviceLabel for device names). A device name is accepted with any two-digit suffix whose stem is not governance-reserved, regardless of stem length. Native-token pricing is bypassed entirely; the gateway pays no rent. Decoupling: This contract does not import or call IDotnsRegistrarController. The public commit-reveal controller is equally unaware of this one. Cross-flow collision handling relies on two distinct properties, neither of which requires the two controllers to know about each other: (1) Device names (stem.NN) occupy a namespace the public path cannot reach: the separator is legal only on a device name, and the public path rejects it, so no public registration can spell one. A digit suffix is not exclusive, but an ordinary label carrying one is measured as written and so is simply a different name. The two flows therefore cannot contend for the same label. This holds of labels the contracts minted, not of an arbitrary string: a subname stored under a digit-only parent reads the same way, which is why provenance is published through @custom:function isPopIssued rather than inferred. (2) Personhood-name reservations are synchronised into IPopRules by base name. The head of this controller's reservation queue is written through IPopRules.reserveBaseNameForPop on every head transition; the slot is cleared through IPopRules.releaseBaseName when the queue empties (claim, final relinquish, final expiry). The public commit-reveal controller routes through IPopRules.priceWithCheck, which rejects any registration targeting a base name reserved for another user, so the public flow respects gateway reservations without ever importing this contract. PopRules is the single cross-flow authority; the queue here is the intra-PoP ordering layer on top of it. Shared primitives: labelhash / namehash via @custom:contract LabelUtils; the mint + forward-registry + store-write triad via @custom:contract RegistrationUtils; chat-key and device-link persistence via

  • contract: IDotnsPopResolver. Keeping per-name records on the resolver preserves the "Store = labels only" invariant.

  • security-contact: admin@parity.io

Constants

MAX_RESERVATION_QUEUE

Upper bound for the number of simultaneously queued reservations per label.

Keeps expireReservation gas bounded.

uint16 public constant MAX_RESERVATION_QUEUE = 64

MIN_RESERVATION_DURATION

Minimum value accepted by @custom:function setReservationDuration.

Prevents owner misconfiguration from instantly expiring every live queue and pending-claim entry. The actual production duration is governance-tuned higher.

uint64 public constant MIN_RESERVATION_DURATION = 1 hours

CHAT_KEY_LENGTH

Required byte length for a non-empty chat key.

Mirrors @custom:contract IDotnsPopResolver InvalidChatKeyLength so the controller can fail closed before the mint instead of bubbling the resolver's revert after partial state has been committed.

uint256 private constant CHAT_KEY_LENGTH = 65

State Variables

protocolRegistry

Protocol-level address registry for all dotNS contracts.

IDotnsProtocolRegistry public protocolRegistry

_reservationMeta

Per-label queue metadata (head/tail pointers).

mapping(bytes32 labelhash => ReservationQueueMeta meta) internal _reservationMeta

_reservationEntries

Per-label sparse entries keyed by monotonically-increasing index.

mapping(bytes32 labelhash => mapping(uint64 index => ReservationEntry entry)) internal
    _reservationEntries

_userReservations

Single per-user pointer into the reservation queues.

Keeps per-user reservation data behind one key and one struct value so callers read both fields in one call instead of two.

mapping(address user => UserReservation reservation) internal _userReservations

_reservedBaseLabel

Remembers the personhood name for each reserved labelhash so the PopRules sync path can address the reservation by its original string form (PopRules keys its reservations mapping by string).

Populated on first enqueue for a label, cleared when the queue empties. Exists only to bridge the queue's bytes32 key space to PopRules' string key space; nothing else reads it.

mapping(bytes32 labelhash => string reservedLabel) internal _reservedBaseLabel

reservationDuration

Duration (in seconds) after which a reservation entry is considered expired.

Sets the reservation duration, configurable by governance via setReservationDuration.

uint64 public override reservationDuration

_pendingClaimUsers

Enumeration set of users holding at least one pending claim.

Membership equals the set of users with a non-empty queue. Used by pendingClaimUserCount and pendingClaimUsers for paginated enumeration.

EnumerableSet.AddressSet private _pendingClaimUsers

_pendingClaimQueue

Per-user pile of deferred names awaiting a LabelStore.

The mint origin cannot deploy a LabelStore, so deferred names accumulate here until a signed-origin @custom:function settlePendingClaims deploys the store and writes the stashed labels. Entries never lapse and can be settled at any time.

mapping(address user => PendingClaim[] queue) internal _pendingClaimQueue

_popIssued

Labels this controller minted, keyed by the bare label without the TLD.

Provenance, not a transfer rule: written once at mint and never cleared, so it stays true if a name later becomes transferable. Keyed by the label text rather than the node because a reader holding only joseph.42 cannot derive the node without first deciding whether the separator is part of the label or a subname boundary, which is the question it is asking.

mapping(string label => bool issued) internal _popIssued

__gap

Reserved storage space to allow for layout changes in future upgrades.

uint256[50] private __gap

Functions

onlyRoot

Restricts calls to a Root origin.

modifier onlyRoot() ;

constructor

Note: oz-upgrades-unsafe-allow: constructor

constructor() ;

initialize

Initialises the PoP controller.

Called once through the UUPS proxy; _disableInitializers on the implementation makes direct calls revert with @custom:reverts InvalidInitialization, and any nested call outside an active initialiser scope reverts with @custom:reverts NotInitializing. Emits @custom:emits ReservationDurationSet so indexers observe the initial value through the same event the setter uses later.

function initialize(
    address initialOwner,
    IDotnsProtocolRegistry registry,
    uint64 reservationDuration_
)
    external
    initializer;

Parameters

NameTypeDescription
initialOwneraddressAddress that owns the contract once initialised.
registryIDotnsProtocolRegistry
reservationDuration_uint64

isPopIssued

Whether this controller issued label as a PoP identity.

Keyed by text, so it answers about a name rather than about a node. A device name is issued as a subname (joseph beneath its numeric container 42) and a personhood name as a second-level name, so a caller holding a node must check that the node is the one label resolves to under those rules before reading this answer as being about what it holds; node identity is what names the object. Set at mint and never cleared, so it is unaffected by a name later becoming transferable; the soulbound flag is a transfer rule and cannot stand in for it.

function isPopIssued(string calldata label) external view override returns (bool issued);

Parameters

NameTypeDescription
labelstringBare label without the TLD, for example joseph.42.

Returns

NameTypeDescription
issuedboolTrue when this controller issued label.

issueDeviceName

Issues a device name to the supplied user without touching the reservation queue.

Callable only under a Root origin (otherwise @custom:reverts NotRoot). The supplied label must satisfy the stem.NN shape and must classify outside the governance-reserved tier (otherwise @custom:reverts InvalidDeviceLabel); a supplied chat key whose length is neither zero nor CHAT_KEY_LENGTH reverts

Notes:

  • reverts: InvalidChatKey before mint and resolver writes run. A device name that has already been issued reverts @custom:reverts DeviceNameAlreadyIssued. On a warm-path mint

  • emits: DeviceNameIssued and @custom:emits NameRegistered. On a cold-path mint @custom:emits DeviceNameIssued and @custom:emits PendingClaimStashed, with

function issueDeviceName(DeviceNameIssuance calldata params) external override onlyRoot;

Parameters

NameTypeDescription
paramsDeviceNameIssuanceIssuance request; see @custom:struct DeviceNameIssuance.

reserveLiteName

function reserveLiteName(DeviceNameIssuance calldata params) external override onlyRoot;

issueDeviceNameWithReservation

Issues a device name to the supplied user and optionally enqueues a reservation for a personhood name they intend to claim later.

Callable only under a Root origin (otherwise @custom:reverts NotRoot). The issuance validates the stem.NN shape and requires the label to classify outside the governance-reserved tier (otherwise @custom:reverts InvalidDeviceLabel), and rejects a supplied chat key whose length is neither zero nor CHAT_KEY_LENGTH (otherwise @custom:reverts InvalidChatKey). On a warm-path mint (user already has a LabelStore) it @custom:emits DeviceNameIssued and @custom:emits NameRegistered; on a cold-path mint it @custom:emits DeviceNameIssued and

Notes:

  • emits: PendingClaimStashed, with @custom:emits NameRegistered deferred to

  • function: settlePendingClaims when the claim settles. The reservation only runs when reservedLabel is non-empty: it requires a letters-only personhood label (otherwise @custom:reverts InvalidPersonhoodLabel) with no owner on the registrar (otherwise @custom:reverts PersonhoodNameUnavailable), since a name that already has an owner could never be claimed. This validation runs before both the issuance and any queue mutation, so an already-registered reservedLabel aborts the whole call and the candidate receives no device name either; callers should validate the reserved label before attesting rather than relying on this revert. It then advances the head past expired entries (@custom:emits ReservationExpired for each one), removes the user from any prior queue position (@custom:emits ReservationRelinquished) so a single user holds at most one live reservation across all labels, and enqueues a fresh entry (@custom:emits ReservationQueued). The enqueue rejects with @custom:reverts AlreadyReserved when the user already holds a reservation that was not cleared by the prior removal and with @custom:reverts QueueFull when the per-label queue has reached MAX_RESERVATION_QUEUE. Cross-chain callers pass the ABI-encoded tuple as the call's payload, which Solidity decodes directly.

  • struct: DeviceNameIssuanceWithReservation.

function issueDeviceNameWithReservation(DeviceNameIssuanceWithReservation calldata params)
    external
    override
    onlyRoot;

Parameters

NameTypeDescription
paramsDeviceNameIssuanceWithReservationIssuance and reservation request; see

reserveBaseName

function reserveBaseName(DeviceNameIssuanceWithReservation calldata params)
    external
    override
    onlyRoot;

reservePersonhoodName

Enqueues only a personhood-name reservation for a user.

Callable only under a Root origin (otherwise @custom:reverts NotRoot). This is the second step of the split gateway flow: @custom:function issueDeviceName issues the device name first, then this function reserves the personhood name in a separate transaction so proof-size stays below per-call limits. Reverts with @custom:reverts InvalidPersonhoodLabel when the label is empty, is not lowercase ASCII letters (so a hyphen or any digit rejects it), or is governance-reserved, and with @custom:reverts PersonhoodNameUnavailable when the label already has an owner on the registrar and so could never be claimed. Moving the user out of a prior queue @custom:emits ReservationRelinquished. The caller remains agnostic about backend batching; it simply exposes a small retryable primitive.

function reservePersonhoodName(PersonhoodNameReservation calldata params)
    external
    override
    onlyRoot;

Parameters

NameTypeDescription
paramsPersonhoodNameReservationReservation request; see @custom:struct PersonhoodNameReservation.

issuePersonhoodName

Issues a personhood name to the supplied user.

Callable only under a Root origin (otherwise @custom:reverts NotRoot). The label must be a letters-only personhood label (otherwise @custom:reverts InvalidPersonhoodLabel), and must not classify as governance-reserved or as a device-name shape (otherwise

Notes:

  • reverts: InvalidPersonhoodLabel). The gateway also defers to PopRules as the single cross-flow authority: when PopRules carries a live base-name slot held by another user (this controller's prior queue head, or a sibling controller's write), the call reverts

  • emits: ReservationClaimed; a non-claim drops any pending entry the user holds (@custom:emits ReservationRelinquished). Either way the issuance

  • function: settlePendingClaims. Cross-chain callers pass the ABI-encoded issuance tuple as the call's payload, which Solidity decodes directly.

function issuePersonhoodName(PersonhoodNameIssuance calldata params)
    external
    override
    onlyRoot;

Parameters

NameTypeDescription
paramsPersonhoodNameIssuanceIssuance request; see @custom:struct PersonhoodNameIssuance.

registerBaseName

function registerBaseName(PersonhoodNameIssuance calldata params) external override onlyRoot;

_issueDeviceNameWithReservation

Body shared by @custom:function issueDeviceNameWithReservation and its legacy entrypoint.

The reservation is validated before the issuance so an unreservable label aborts the whole call. legacy selects the label errors, as in @custom:function _issueDeviceName.

function _issueDeviceNameWithReservation(
    DeviceNameIssuanceWithReservation calldata params,
    bool legacy
)
    internal;

_issueDeviceName

Device-name issuance shared by @custom:function issueDeviceName, the issuance of

Gateway attestation is the authority for devicehood on this path; the on-chain precompile is not consulted. The label is stored in the stem.NN form the gateway sends, which is the canonical form of the name, so no normalisation happens here. The shape check runs before classification so a malformed label reverts with a controller error, which the gateway decodes by selector; letting classifyName catch it instead would surface an undecodable PopRules string. legacy selects @custom:reverts InvalidLiteLabel over

Notes:

  • function: issueDeviceNameWithReservation, and their legacy entrypoints.

  • reverts: InvalidDeviceLabel for the callers that decode the legacy error.

function _issueDeviceName(
    IPopRules rules,
    DeviceNameIssuance calldata params,
    bool legacy
)
    internal;

_issuePersonhoodName

Personhood-name issuance shared by @custom:function issuePersonhoodName and its legacy entrypoint.

legacy selects @custom:reverts InvalidLiteLabel / @custom:reverts InvalidBaseLabel over the new label errors for the callers that decode the legacy ones.

function _issuePersonhoodName(PersonhoodNameIssuance calldata params, bool legacy) internal;

expireReservation

Permissionlessly removes expired entries from the head of a reservation queue.

Permissionless on purpose: anyone (typically a UI or a bot) can poke a stale queue so the next live head takes over without waiting for the next gateway call. Validates label as a letters-only personhood label (otherwise @custom:reverts InvalidPersonhoodLabel) and @custom:emits ReservationExpired for every expired entry reaped from the head. A label carrying a digit or a hyphen is not a personhood label and

Note: reverts: InvalidPersonhoodLabel, as does a device name, since a separator is not one either. Only a letters-only label reaches the queue, and one that was never reserved resolves to an empty queue so the call is a no-op.

function expireReservation(string calldata label) external override;

Parameters

NameTypeDescription
labelstringPersonhood name whose queue is reaped.

relinquishReservation

Lets the caller voluntarily drop their own active reservation.

Reverts with @custom:reverts NoActiveReservation when the caller holds no live reservation. On success the caller's entry is removed from its queue and

Note: emits: ReservationRelinquished is emitted; if the removed entry was the queue head, head advancement may additionally @custom:emits ReservationExpired for any stale entries reaped behind it.

function relinquishReservation() external override;

claimLabelStore

Settles the caller's own pending claims into their LabelStore.

Convenience for a user settling their own store: equivalent to

Notes:

  • function: settlePendingClaims with msg.sender and a bounded batch. The caller deploys and pays for their store on the first write. Settles at most one bounded batch so the call cannot exceed the block gas limit; moreRemaining reports whether the caller still holds unsettled entries, in which case they call again. Emits the same

  • emits: PendingClaimSettled and @custom:emits NameRegistered as

function claimLabelStore() external override returns (bool moreRemaining);

Returns

NameTypeDescription
moreRemainingboolWhether the caller still holds unsettled entries.

settlePendingClaims

Settles up to limit of a user's pending claims, writing each stashed label into the user's LabelStore and deploying that store when the user has none yet.

Permissionless: any caller may settle any user's claims and bears the full cost, including the LabelStore storage deposit, which is charged to the transaction signer. Settlement is never destructive: the name is already minted, so this only completes the deferred label write. Each settled entry is removed from the queue and the user leaves the pending-claim enumeration set once their queue empties. At most limit entries are processed so a large queue cannot exceed the block gas limit; moreRemaining reports whether entries are left for a follow-up call, and a limit of zero settles nothing. Writes are idempotent on an already-locked store slot, so a claim whose label was independently written settles harmlessly. Emits

Note: emits: PendingClaimSettled and @custom:emits NameRegistered per settled entry, with settledBy set to the caller so a third-party settlement is distinguishable from a self-settlement.

function settlePendingClaims(
    address user,
    uint256 limit
)
    external
    override
    returns (uint256 settledCount, bool moreRemaining);

Parameters

NameTypeDescription
useraddressAccount whose pending claims are settled.
limituint256Maximum number of entries to settle in this call.

Returns

NameTypeDescription
settledCountuint256Number of entries settled.
moreRemainingboolWhether the user still holds unsettled entries.

_settlePending

Shared settlement loop behind @custom:function claimLabelStore and

Settles up to limit of the user's pending claims, deploying the store on the first write, and removes the user from the enumeration set once their queue empties.

Note: function: settlePendingClaims.

function _settlePending(
    address user,
    uint256 limit
)
    internal
    returns (uint256 settledCount, bool moreRemaining);

_settlePendingLabel

Writes a single pending label into the user's store, deploying the store lazily.

The store is created only when there is a label to write, so a caller who settles an empty queue never leaves a fresh store behind with nothing in it. Returns the (possibly newly deployed) store so the caller threads it through the remaining entries.

function _settlePendingLabel(
    IStoreFactory factory,
    address store,
    address user,
    string memory label
)
    internal
    returns (address);

isReservedForClaim

Returns whether a label currently has a live reservation at the queue head.

Validates label as a letters-only personhood label (otherwise

Note: reverts: InvalidPersonhoodLabel) before inspecting the queue.

function isReservedForClaim(string calldata label)
    external
    view
    override
    returns (bool reserved, address holder);

Parameters

NameTypeDescription
labelstringPersonhood name whose queue is inspected.

setReservationDuration

Updates the reservation duration used to decide when queue entries expire.

Owner-gated (otherwise @custom:reverts OwnableUnauthorizedAccount); emits

Note: emits: ReservationDurationSet on success.

function setReservationDuration(uint64 duration) external override onlyOwner;

reservationMeta

Returns the queue metadata (head, tail) for labelhash.

Read-only accessor over the per-label reservation queue. head == tail means the queue is empty; active entries occupy [head, tail). Exposed on the interface because invariant tests and off-chain consumers use it to enumerate live queue state without scanning storage.

function reservationMeta(bytes32 labelhash)
    external
    view
    override
    returns (uint64 head, uint64 tail);

Parameters

NameTypeDescription
labelhashbytes32Keccak-256 of the personhood name whose queue is being read.

Returns

NameTypeDescription
headuint64Index of the live queue head.
tailuint64Index one past the last queued entry.

reservationEntry

Returns the queue entry at index for labelhash.

Sparse storage: a zero entryOwner means the slot was relinquished, expired and reaped, or never written. Callers pair this with @custom:function reservationMeta to walk the live window [head, tail).

function reservationEntry(
    bytes32 labelhash,
    uint64 index
)
    external
    view
    override
    returns (address entryOwner, uint64 joinedAt);

Parameters

NameTypeDescription
labelhashbytes32Keccak-256 of the personhood name whose queue is being read.
indexuint64Queue index to look up.

Returns

NameTypeDescription
entryOwneraddressOwner of the slot (zero if empty/relinquished).
joinedAtuint64Timestamp the entry was enqueued (only meaningful when entryOwner != address(0)).

userReservation

Returns user's current reservation pointer.

A zero labelhash on the returned struct means the user holds no reservation; index is meaningful only when labelhash is non-zero.

function userReservation(address user)
    external
    view
    override
    returns (UserReservation memory reservation);

Parameters

NameTypeDescription
useraddressAccount whose reservation pointer is being read.

Returns

NameTypeDescription
reservationUserReservationPer-user reservation pointer; see @custom:struct UserReservation.

pendingClaims

Returns a paginated slice of a user's pending claims in queue order.

An empty array means the user has no pending claims at offset. Each entry carries its mintedAt, and every entry stays settleable whatever its age. An offset past the end returns an empty array rather than reverting, and a page holds at most DotnsConstants.MAX_PAGE_SIZE entries.

function pendingClaims(
    address user,
    uint256 offset,
    uint256 limit
)
    external
    view
    override
    returns (PendingClaim[] memory claims);

Parameters

NameTypeDescription
useraddressAccount whose pending claims are read.
offsetuint256Start index into the queue.
limituint256Maximum entries to return.

Returns

NameTypeDescription
claimsPendingClaim[]Page of the user's pending claims; see @custom:struct PendingClaim.

pendingClaimCountOf

Returns the number of pending claims currently staged for user.

function pendingClaimCountOf(address user) external view override returns (uint256 count);

Parameters

NameTypeDescription
useraddressAccount whose pending claims are counted.

Returns

NameTypeDescription
countuint256Number of staged pending claims.

pendingClaimUserCount

Returns the number of users with at least one live pending claim.

Exact live count, not an all-time tally: fully settled users are removed from the enumeration set so off-chain consumers can page through every stalled user without filtering.

function pendingClaimUserCount() external view override returns (uint256 count);

Returns

NameTypeDescription
countuint256Number of users currently holding a pending claim.

pendingClaimUsers

Returns a paginated slice of users with at least one live pending claim.

Pair with @custom:function pendingClaims to read each user's stashed entries. Ordering is not chronological; callers MUST NOT assume mintedAt is monotonic across the slice. Returns an empty array when offset is past the live count, and a page holds at most DotnsConstants.MAX_PAGE_SIZE entries.

function pendingClaimUsers(
    uint256 offset,
    uint256 limit
)
    external
    view
    override
    returns (address[] memory users);

Parameters

NameTypeDescription
offsetuint256Start index.
limituint256Maximum entries to return.

Returns

NameTypeDescription
usersaddress[]Slice of users currently holding a pending claim.

reservedLabelOf

Returns the personhood name a reservation queue is keyed under.

Reverse lookup from the bytes32 queue key to its label string, so a consumer that observed a queue by labelhash (for example from a reservation event) can recover the human-readable label without holding its preimage. Returns an empty string when no reservation was ever enqueued under labelhash.

function reservedLabelOf(bytes32 labelhash)
    external
    view
    override
    returns (string memory label);

Parameters

NameTypeDescription
labelhashbytes32Keccak-256 of the personhood name.

Returns

NameTypeDescription
labelstringThe personhood name, or empty when unknown.

supportsInterface

function supportsInterface(bytes4 interfaceId)
    public
    view
    override(ERC165Upgradeable, IERC165)
    returns (bool);

version

Returns the release this network declares it runs, read live from the protocol registry so every dotNS contract reports one synchronised value.

Mirror of IDotnsProtocolRegistry.protocolVersion, kept under the historical version() selector for ABI compatibility. It reports the network's declaration, not this contract's build; per-contract identity is the codehash declared on the registry.

function version() external view virtual returns (string memory versionString);

Returns

NameTypeDescription
versionStringstringDeclared release as bare semver, empty when never declared.

_completeGatewayRegistration

Mints a name, wires forward registry, persists PoP-flow records (chat key, device link) on the PoP resolver, and either writes the label into the owner's existing LabelStore or stashes a pending claim when the owner has none yet.

The mint + forward-registry pair is delegated to

Notes:

  • function: RegistrationUtils.registerAndStore so this flow and the public commit-reveal flow share exactly one implementation of that sequence. The label is passed empty so the registrar does not deploy a LabelStore; the mint origin cannot run the LabelStore constructor. PoP-flow per-name records (chat key, device link) are persisted eagerly on @custom:contract IDotnsPopResolver here, before the label is written, so the resolver carries the full identity record from mint time regardless of whether the owner already has a LabelStore. The Store stays labels-only. Warm path emits @custom:emits NameRegistered immediately; the cold path emits @custom:emits PendingClaimStashed at mint and defers

  • emits: NameRegistered to @custom:function settlePendingClaims when the claim settles.

function _completeGatewayRegistration(
    address user,
    string memory label,
    bytes32 labelhash,
    bytes32 node,
    bytes memory chatKeyBytes,
    bytes32 deviceLabelhash
)
    internal;

_writeRecord

Writes a name's label into store.

Single canonical persistence step shared by the warm gateway path and

Note: function: settlePendingClaims. The store key is node, matching the registrar's _writeOwnerLabel convention. Idempotent on already-locked slots so a user whose store was pre-populated under the same node (e.g. by a sibling protocol flow) can still settle their pending claim without bricking on LabelAlreadyExists.

function _writeRecord(address store, bytes32 node, string memory label) internal;

Parameters

NameTypeDescription
storeaddressOwner's LabelStore proxy.
nodebytes32The name's node. A device name resolves to its stem beneath its numeric container, so this is not always namehash(tldNode, keccak(label)) for the whole label.
labelstringBare label without the TLD, which is appended on write. A device name carries its separator, so this is not always a single DNS label.

_stashPendingClaim

Appends a deferred binding for user and adds them to the enumeration set.

The mint origin cannot deploy the user's LabelStore, so deferred names pile up in _pendingClaimQueue until a signed-origin @custom:function settlePendingClaims writes them. Adding the user to the set is idempotent, so repeat stashes keep a single enumeration entry. Emits @custom:emits PendingClaimStashed.

function _stashPendingClaim(address user, string memory label, bytes32 labelhash) internal;

_isExpired

Returns whether a queue entry is expired relative to block.timestamp.

function _isExpired(uint64 joinedAt) internal view returns (bool);

_enqueueReservation

Appends a new reservation entry to the tail of the queue for labelhash.

Reverts if the queue is full or the user already holds a reservation. When the enqueued entry is the new head of an empty queue, the controller also reserves the base name on PopRules so the public commit-reveal flow sees the reservation through its existing priceWithCheck guard. Subsequent waiters only live in the local queue until they are promoted.

function _enqueueReservation(
    IPopRules rules,
    bytes32 labelhash,
    string memory reservedLabel,
    address user
)
    internal;

_clearQueue

Wipes the entire reservation queue for labelhash and releases the corresponding PopRules reservation.

Used when claimant claims their reservation: every other waiter is evicted (@custom:emits ReservationEvicted for each one) and their per-user tracking state is cleared, and PopRules is told the slot is free so future public registrations are unblocked (the claim itself just minted the name, so there is nothing left to reserve).

function _clearQueue(bytes32 labelhash, address claimant) internal;

_advanceExpiredHead

Advances the queue head past every expired entry at the head of the queue.

Reset semantics matter: 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. When a new live head emerges, PopRules is re-synced to that head so reservations cannot be paid around by another address. Emits

Note: emits: ReservationExpired once per expired entry reaped from the head.

function _advanceExpiredHead(bytes32 labelhash) internal;

_dropUserReservation

Drops user's own reservation entry, if any, and records that it happened.

The single path by which a user's entry leaves a queue other than by expiry or by a claim: an explicit relinquish, a standalone personhood-name issuance, and a re-reservation that moves the user to another queue. @custom:emits ReservationRelinquished only when the user held an entry, so an indexer can rebuild every queue from events alone.

function _dropUserReservation(address user) internal;

_removeUserFromQueue

Removes user from whichever reservation queue they currently occupy.

For a head removal, we delete the entry without bumping meta.head and delegate the advance to _advanceExpiredHead. Its existing zero-owner skip walks past the freshly-deleted slot, and its head != meta.head branch fires the PopRules resync in the one place head promotion is actually handled. Non-head removals leave the queue shape intact, so no advance or resync is needed. Emits nothing itself; callers go through @custom:function _dropUserReservation.

function _removeUserFromQueue(address user) internal;

_validateDeviceLabel

Validates a device name stem.NN and derives (labelhash, node).

The stem is lowercase letters only, so this rejects a stem carrying a digit or a hyphen before any node is derived. node is the hierarchical subnode stem under the numeric container NN, the node a resolver reaches by walking the dotted name, and labelhash stays the keccak of the whole label so it is a stable text identifier for events and the reservation queue. legacy selects the label error, as in

Note: function: _requireDeviceLabel.

function _validateDeviceLabel(
    string memory deviceLabel,
    bool legacy
)
    internal
    view
    returns (bytes32 labelhash, bytes32 node);

_deviceSubnode

Derives the hierarchical subnode for a device name <stem>.<suffix>.

Splits at the separator and walks suffix.tld then stem under it, so a device name resolves as stem beneath its numeric container rather than as a hash of the whole label. Shared by @custom:function _validateDeviceLabel and pending-claim settlement so every consumer agrees on one node.

function _deviceSubnode(string memory deviceLabel) internal view returns (bytes32 subnode);

Parameters

NameTypeDescription
deviceLabelstringDevice name held in memory, e.g. alice.01.

Returns

NameTypeDescription
subnodebytes32Namehash of stem under suffix.tld.

_validatePersonhoodLabel

Validates a personhood name and derives (labelhash, node).

Letters only, so this is stricter than a DNS label: a hyphen or an interior digit is rejected here even though @custom:function StringUtils.isSingleLabel would admit it. legacy selects the label error, as in @custom:function _requirePersonhoodLabel.

function _validatePersonhoodLabel(
    string calldata label,
    bool legacy
)
    internal
    view
    returns (bytes32 labelhash, bytes32 node);

_validateReservablePersonhoodLabel

Validates a personhood name as reservable and returns its hashes.

Shared by both reservation paths so the guard cannot drift between them. Runs three checks and reverts on the first failure, before any reservation state is mutated: the label must be a letters-only personhood label, which makes it its own base name, classify outside the governance-reserved tier, and have no owner on the registrar. The last check is the fix for a reservation queued over an already-registered name: the queue keys by base name, so such a reservation could never be redeemed yet would hold the base name, and so every device name with that stem, for the full reservation window. exists (owner set) mirrors exactly what makes the eventual claim's mint revert, so a label that passes here is one a claim can still register.

function _validateReservablePersonhoodLabel(
    IPopRules rules,
    string calldata label,
    bool legacy
)
    internal
    view
    returns (bytes32 labelhash, bytes32 node);

_requireDeviceLabel

Reverts with the device-label error the caller decodes when ok is false.

legacy is set only on the @custom:contract IDotnsPopControllerLegacy entrypoints, whose callers decode @custom:reverts InvalidLiteLabel; every other path reverts

Note: reverts: InvalidDeviceLabel.

function _requireDeviceLabel(bool ok, bool legacy) internal pure;

_requirePersonhoodLabel

Reverts with the personhood-label error the caller decodes when ok is false.

legacy is set only on the @custom:contract IDotnsPopControllerLegacy entrypoints, whose callers decode @custom:reverts InvalidBaseLabel; every other path reverts

Note: reverts: InvalidPersonhoodLabel.

function _requirePersonhoodLabel(bool ok, bool legacy) internal pure;

_requireValidChatKey

Reverts when a non-empty chat key is not exactly CHAT_KEY_LENGTH bytes.

Mirrors the resolver's own length gate so the gateway sees a controller-local InvalidChatKey revert before any mint state is written.

function _requireValidChatKey(bytes memory chatKey) internal pure;

_popResolver

Resolves the PoP resolver via the protocol registry.

function _popResolver() internal view returns (IDotnsPopResolver);

_popRules

Resolves the PopRules contract via the protocol registry.

function _popRules() internal view returns (IPopRules);

_storeFactory

Resolves the Store factory via the protocol registry.

function _storeFactory() internal view returns (IStoreFactory);

_registrar

Resolves the registrar via the protocol registry.

function _registrar() internal view returns (IDotnsRegistrar);

_registry

Resolves the registry via the protocol registry.

function _registry() internal view returns (IDotnsRegistry);

_syncPopRulesToHead

Writes the new head of the queue into PopRules so the public commit-reveal flow rejects registrations of this base name for anyone other than newHead.

Callers guarantee newHead is non-zero (the queue holds a live entry) and that _reservedBaseLabel[labelhash] is non-empty (any non-empty queue had its first head write the slot). The release-then-reserve pair satisfies PopRules' ownership gate on reserveBaseNameForPop.

function _syncPopRulesToHead(bytes32 labelhash, address newHead) internal;

_releasePopRulesSlot

Clears the PopRules slot and the local label bookkeeping when the queue empties (claim, last-relinquish, last-expire).

function _releasePopRulesSlot(bytes32 labelhash) internal;

_onlyRoot

Internal check enforcing a Root origin.

Authorises a call when @custom:function SystemUtils.originIsRoot is true, and reverts with NotRoot otherwise. msg.sender is deliberately not consulted: a Root origin has no account behind it, so reading msg.sender traps. That holds for this frame and any delegatecall sharing it; a nested call sees the calling contract as its sender and reads normally. The check also holds for the whole Root transaction rather than the entry frame alone, so nothing reachable from an onlyRoot entrypoint may call a user-controlled address: such a callee could re-enter a gated function and still pass. Every call out of this contract goes to a protocol contract resolved through the registry.

function _onlyRoot() internal view;

_authorizeUpgrade

Function that should revert when msg.sender is not authorized to upgrade the contract. Called by {upgradeToAndCall}. Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}.

function _authorizeUpgrade(address) internal onlyOwner {}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner;