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 applies PopRules' naming rules (@custom:contract PopClassification) 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; classification via

  • contract: PopClassification; 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

MAX_RESERVATION_DURATION

Maximum value accepted by @custom:function setReservationDuration.

PopRules holds the name for the user at the front of a queue for

Note: constant: PopRules.MAX_RESERVATION_TIME (12 weeks) from each change of front, and a PopRules reservation lapses at its last second while a queue entry is still active at its own. One second less keeps the front user's entry from outlasting the PopRules reservation written when they reached the front. Users behind them keep their own join time, so their entries can outlast it; a call that clears the queue re-stamps or releases the PopRules reservation, which is why apps clear a queue before using it.

uint64 public constant MAX_RESERVATION_DURATION = 12 weeks - 1

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

MAX_QUEUE_STEPS_PER_CALL

Most expired or emptied entries one gateway call clears from the front of a reservation queue.

A gateway call runs under a fixed weight limit, and each entry visited costs a storage read. A longer run is cleared over several calls, each keeping its progress, or at once by @custom:function expireReservation.

uint256 public constant MAX_QUEUE_STEPS_PER_CALL = 4

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

_deviceContainers

Numeric containers this contract has minted or verified as its own.

Lets device-name issuance skip the registrar and registry checks on every later issuance in the same container. See @custom:function _registerDeviceSubname. An entry is never cleared, because a recorded container cannot change hands: the registrar marks a name minted by this contract soulbound and rejects every transfer of it, release to the escrow included, and it has no burn. If a container ever did change hands, the registry's own parent check in @custom:function IDotnsRegistry.setSubnodeOwner would still reject issuance under it.

mapping(bytes32 parentNode => bool ready) internal _deviceContainers

__gap

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

uint256[49] 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 clears up to MAX_QUEUE_STEPS_PER_CALL expired entries from the front of the queue (@custom:emits ReservationExpired for each one cleared). When the user already holds an active reservation for reservedLabel, that reservation stays as it is, with the same place in the queue and the same expiry, and nothing else changes. Otherwise it 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 removal only empties the user's entry in the prior queue: the next call on that name clears it like an expired one. The enqueue rejects with

  • 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. When the user already holds an active reservation for label, that reservation stays as it is, with the same place in the queue and the same expiry, so the call is safe to retry. Otherwise moving the user out of a prior queue @custom:emits ReservationRelinquished and only empties their entry there, as in @custom:function issueDeviceNameWithReservation.

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: PersonhoodNameIssued. Up to MAX_QUEUE_STEPS_PER_CALL expired entries are cleared from the front of the queue first (@custom:emits ReservationExpired for each one); if the front is still expired after that, a claim

  • 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(
    Deps memory deps,
    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. Clears every expired entry at the front of the queue in one call, including a run longer than a gateway call clears, then moves PopRules to the next active reservation or releases it when the queue is empty.

function expireReservation(string calldata label) external override;

Parameters

NameTypeDescription
labelstringPersonhood name whose queue is reaped.

prepareDeviceContainer

Permissionlessly mints the numeric container suffix that device names stem.suffix live beneath, owned by this contract, or records an existing one.

The first device name issued in a container otherwise mints it inside the gateway's call, which runs under a fixed weight limit. Preparing containers ahead of time, from any signed account, takes that cost out of the issuance. A container already recorded is a no-op. suffix must be exactly two ASCII digits (otherwise

Note: reverts: InvalidDeviceLabel), and a container held by anyone else

function prepareDeviceContainer(string calldata suffix) external override;

Parameters

NameTypeDescription
suffixstringTwo-digit container label, e.g. 01.

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(
    Deps memory deps,
    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). A duration below MIN_RESERVATION_DURATION @custom:reverts ReservationDurationTooLow, and one above MAX_RESERVATION_DURATION @custom:reverts ReservationDurationTooHigh. 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.

Reads only the live window [head, tail) of @custom:function reservationMeta: an index outside it, whose entry expired, was claimed or was never written, returns zero, and so does an entry its owner relinquished.

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 runs against the registrar and registry this call already resolved, so the gateway's call reads each address once. 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

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

function _completeGatewayRegistration(
    Deps memory deps,
    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(
    Deps memory deps,
    bytes32 labelhash,
    string memory reservedLabel,
    address user,
    bool slotKept
)
    internal;

_clearQueue

Ends the reservation queue for labelhash when its head claims the name, and releases the corresponding PopRules reservation.

The claim mints the name, so nothing is left to reserve and PopRules is told the slot is free. Every entry, the claimant's and each waiter's, is evicted lazily, so the claim costs the same whatever the queue holds: the head moves to the tail, which ends the queue, and entries and pointers stay where they are, below the new head.

Note: function: _isEvicted reads such a pointer as no reservation, and the user's next reservation clears it. @custom:emits ReservationClaimed, emitted by the caller, therefore ends the reservation of every waiter in the queue.

function _clearQueue(Deps memory deps, bytes32 labelhash) internal;

_isEvicted

Returns whether reservation points at an entry the queue's head has moved past.

Expiry and claims leave entries and pointers in place and move the head past them (see @custom:function _advanceExpiredHead and @custom:function _clearQueue), so a pointer below its queue's head is no reservation. Queue indices only ever grow, so such a pointer never matches an entry queued later under the same label.

function _isEvicted(UserReservation memory reservation) internal view returns (bool);

_keepOrDropReservation

Prepares a gateway reservation of labelhash for user: keeps the user's reservation when it is an active one for the same name, and otherwise drops whatever reservation they hold so a fresh one can be queued.

A repeated reservation of the same name, such as one made again after the user recovers their account onto a new device, keeps the user's place and expiry. The expiry is not refreshed, so repeating a reservation never extends how long a user holds a name. Reads the user's reservation once and hands it to @custom:function _dropReservation. Dropping a reservation for another name only empties the user's entry in that queue: the next call on that name clears it like an expired one. A gateway call then does queue work in one queue only, the name being reserved, so its cost stays within the weight limit.

function _keepOrDropReservation(
    Deps memory deps,
    bytes32 labelhash,
    address user
)
    internal
    returns (bool kept);

Returns

NameTypeDescription
keptboolWhether the existing reservation was kept, so nothing is to be queued.

_advanceExpiredHead

Clears up to maxSteps expired or emptied entries from the front of the queue.

Every entry visited costs a storage read, and a gateway call runs under a fixed weight limit, so a call clears at most maxSteps entries and keeps that progress; a later call carries on. Clearing writes nothing per entry: entries and pointers stay below the new front, where they read as gone (@custom:function _isEvicted), and queue indices only ever grow. When clearing reaches an active reservation, PopRules moves to that user, unless it is claimant, whose claim in the same call releases the slot. When it empties the queue, the PopRules slot is released, unless the caller is refilling it in the same call: then the slot and its label are kept and slotKept is returned, so the slot moves to the new user in one PopRules call. When it stops before either, PopRules is left as it is, naming a user the queue may have moved past. That user gains nothing: the public flow rejects a reserved name for everyone, and while a name has a queue, @custom:function issuePersonhoodName decides from the queue alone. @custom:emits ReservationExpired once per expired entry cleared.

function _advanceExpiredHead(
    Deps memory deps,
    bytes32 labelhash,
    uint256 maxSteps,
    bool refilling,
    address claimant
)
    internal
    returns (bool slotKept);

_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. maxSteps bounds the clearing that follows when the user was at the front of their queue: a gateway call passes 0, leaving the emptied entry for the next call on that name, and a signed relinquish clears the whole queue.

function _dropUserReservation(Deps memory deps, address user, uint256 maxSteps) internal;

_dropReservation

Body of @custom:function _dropUserReservation for a caller that has already read user's reservation.

function _dropReservation(
    Deps memory deps,
    address user,
    UserReservation memory reservation,
    uint256 maxSteps
)
    internal;

_removeUserFromQueue

Removes user from whichever reservation queue they currently occupy.

The user's entry is deleted in place. When it was the front of the queue and maxSteps is not zero, @custom:function _advanceExpiredHead clears up to maxSteps entries from there, the emptied entry among them, and moves PopRules to the next user with an active reservation or releases it when none is left. Otherwise the front and PopRules stay unchanged. Emits nothing itself; callers go through

Note: function: _dropUserReservation. reservation is the user's live reservation, already read by the caller.

function _removeUserFromQueue(
    Deps memory deps,
    address user,
    UserReservation memory reservation,
    uint256 maxSteps
)
    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(
    Deps memory deps,
    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(
    Deps memory deps,
    string memory deviceLabel
)
    internal
    view
    returns (bytes32 subnode);

Parameters

NameTypeDescription
depsDeps
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(
    Deps memory deps,
    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(
    Deps memory deps,
    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);

_rulesOf

Returns PopRules, reading it from the protocol registry on first use in deps.

function _rulesOf(Deps memory deps) internal view returns (IPopRules rules);

_registrarOf

Returns the registrar, reading it from the protocol registry on first use in deps.

function _registrarOf(Deps memory deps) internal view returns (IDotnsRegistrar registrar);

_registryOf

Returns the registry, reading it from the protocol registry on first use in deps.

function _registryOf(Deps memory deps) internal view returns (IDotnsRegistry registry);

_popResolverOf

Returns the PoP resolver, reading it from the protocol registry on first use in deps.

function _popResolverOf(Deps memory deps) internal view returns (IDotnsPopResolver resolver);

_storeFactoryOf

Returns the store factory, reading it from the protocol registry on first use in deps.

function _storeFactoryOf(Deps memory deps) internal view returns (IStoreFactory factory);

_tldNodeOf

Returns the TLD node, reading it from the protocol registry on first use in deps.

The TLD node is a namehash, so zero never names a real TLD and serves as "not read yet".

function _tldNodeOf(Deps memory deps) internal view returns (bytes32 tldNode);

_registerDeviceSubname

Registers the device name stem.suffix for owner beneath its numeric container.

The container is checked, and minted on first use, only until this contract has seen it once; after that the registry's own parent authorisation in

Note: function: IDotnsRegistry.setSubnodeOwner is the guard. A container this contract minted is soulbound to it, so it never changes hands afterwards.

function _registerDeviceSubname(
    Deps memory deps,
    string memory suffix,
    string memory stem,
    address owner
)
    internal
    returns (bytes32 subnode);

_ensureDeviceContainer

Mints the numeric container parentNode for this contract when it does not exist yet, or checks that this contract owns it, and records that it is ready.

Reverts with @custom:reverts IDotnsRegistry.NotAuthorised when someone else holds the container, so a name registered elsewhere is never used as a device container.

function _ensureDeviceContainer(Deps memory deps, bytes32 parentNode) internal;

_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). @custom:function IPopRules.reassignBaseNameForPop moves the slot in one call; this contract stamped it, so PopRules' ownership gate admits the move.

function _syncPopRulesToHead(Deps memory deps, 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(Deps memory deps, 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;

Structs

Deps

Contract addresses one call reads from the protocol registry, each at most once.

Filled on first use by the _...Of getters and passed by reference through the call. A gateway call runs under a fixed weight limit and every registry lookup is a proxied external call, so each address is read once per call and reused.

struct Deps {
    IPopRules rules;
    IDotnsRegistrar registrar;
    IDotnsRegistry registry;
    IDotnsPopResolver popResolver;
    IStoreFactory storeFactory;
    bytes32 tldNode;
}