DotnsPopController
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 intoIPopRulesby base name. The head of this controller's reservation queue is written throughIPopRules.reserveBaseNameForPopon every head transition; the slot is cleared throughIPopRules.releaseBaseNamewhen the queue empties (claim, final relinquish, final expiry). The public commit-reveal controller routes throughIPopRules.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
| Name | Type | Description |
|---|---|---|
initialOwner | address | Address that owns the contract once initialised. |
registry | IDotnsProtocolRegistry | |
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
| Name | Type | Description |
|---|---|---|
label | string | Bare label without the TLD, for example joseph.42. |
Returns
| Name | Type | Description |
|---|---|---|
issued | bool | True 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
| Name | Type | Description |
|---|---|---|
params | DeviceNameIssuance | Issuance 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
reservedLabelis 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-registeredreservedLabelaborts 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 toMAX_QUEUE_STEPS_PER_CALLexpired entries from the front of the queue (@custom:emits ReservationExpired for each one cleared). When the user already holds an active reservation forreservedLabel, 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
| Name | Type | Description |
|---|---|---|
params | DeviceNameIssuanceWithReservation | Issuance 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
| Name | Type | Description |
|---|---|---|
params | PersonhoodNameReservation | Reservation 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_CALLexpired 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
| Name | Type | Description |
|---|---|---|
params | PersonhoodNameIssuance | Issuance 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
| Name | Type | Description |
|---|---|---|
label | string | Personhood 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
| Name | Type | Description |
|---|---|---|
suffix | string | Two-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.senderand 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;moreRemainingreports 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
| Name | Type | Description |
|---|---|---|
moreRemaining | bool | Whether 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
| Name | Type | Description |
|---|---|---|
user | address | Account whose pending claims are settled. |
limit | uint256 | Maximum number of entries to settle in this call. |
Returns
| Name | Type | Description |
|---|---|---|
settledCount | uint256 | Number of entries settled. |
moreRemaining | bool | Whether 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
| Name | Type | Description |
|---|---|---|
label | string | Personhood 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
| Name | Type | Description |
|---|---|---|
labelhash | bytes32 | Keccak-256 of the personhood name whose queue is being read. |
Returns
| Name | Type | Description |
|---|---|---|
head | uint64 | Index of the live queue head. |
tail | uint64 | Index 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
| Name | Type | Description |
|---|---|---|
labelhash | bytes32 | Keccak-256 of the personhood name whose queue is being read. |
index | uint64 | Queue index to look up. |
Returns
| Name | Type | Description |
|---|---|---|
entryOwner | address | Owner of the slot (zero if empty/relinquished). |
joinedAt | uint64 | Timestamp 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
| Name | Type | Description |
|---|---|---|
user | address | Account whose reservation pointer is being read. |
Returns
| Name | Type | Description |
|---|---|---|
reservation | UserReservation | Per-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
| Name | Type | Description |
|---|---|---|
user | address | Account whose pending claims are read. |
offset | uint256 | Start index into the queue. |
limit | uint256 | Maximum entries to return. |
Returns
| Name | Type | Description |
|---|---|---|
claims | PendingClaim[] | 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
| Name | Type | Description |
|---|---|---|
user | address | Account whose pending claims are counted. |
Returns
| Name | Type | Description |
|---|---|---|
count | uint256 | Number 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
| Name | Type | Description |
|---|---|---|
count | uint256 | Number 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
| Name | Type | Description |
|---|---|---|
offset | uint256 | Start index. |
limit | uint256 | Maximum entries to return. |
Returns
| Name | Type | Description |
|---|---|---|
users | address[] | 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
| Name | Type | Description |
|---|---|---|
labelhash | bytes32 | Keccak-256 of the personhood name. |
Returns
| Name | Type | Description |
|---|---|---|
label | string | The 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
| Name | Type | Description |
|---|---|---|
versionString | string | Declared 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
| Name | Type | Description |
|---|---|---|
store | address | Owner's LabelStore proxy. |
node | bytes32 | The 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. |
label | string | Bare 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
| Name | Type | Description |
|---|---|---|
kept | bool | Whether 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
| Name | Type | Description |
|---|---|---|
deps | Deps | |
deviceLabel | string | Device name held in memory, e.g. alice.01. |
Returns
| Name | Type | Description |
|---|---|---|
subnode | bytes32 | Namehash 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;
}