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 still calls @custom:function IPopRules.classifyName to reject
governance-reserved labels (@custom:reverts InvalidPersonhoodLabel for personhood names,
Notes:
-
reverts: InvalidDeviceLabel for device names). A device name is accepted with any two-digit suffix whose stem is not governance-reserved, regardless of stem length. Native-token pricing is bypassed entirely; the gateway pays no rent. Decoupling: This contract does not import or call
IDotnsRegistrarController. The public commit-reveal controller is equally unaware of this one. Cross-flow collision handling relies on two distinct properties, neither of which requires the two controllers to know about each other: (1) Device names (stem.NN) occupy a namespace the public path cannot reach: the separator is legal only on a device name, and the public path rejects it, so no public registration can spell one. A digit suffix is not exclusive, but an ordinary label carrying one is measured as written and so is simply a different name. The two flows therefore cannot contend for the same label. This holds of labels the contracts minted, not of an arbitrary string: a subname stored under a digit-only parent reads the same way, which is why provenance is published through @custom:function isPopIssued rather than inferred. (2) Personhood-name reservations are synchronised 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; the mint + forward-registry + store-write triad via @custom:contract RegistrationUtils; chat-key and device-link persistence via -
contract: IDotnsPopResolver. Keeping per-name records on the resolver preserves the "Store = labels only" invariant.
-
security-contact: admin@parity.io
Constants
MAX_RESERVATION_QUEUE
Upper bound for the number of simultaneously queued reservations per label.
Keeps expireReservation gas bounded.
uint16 public constant MAX_RESERVATION_QUEUE = 64
MIN_RESERVATION_DURATION
Minimum value accepted by @custom:function setReservationDuration.
Prevents owner misconfiguration from instantly expiring every live queue and pending-claim entry. The actual production duration is governance-tuned higher.
uint64 public constant MIN_RESERVATION_DURATION = 1 hours
CHAT_KEY_LENGTH
Required byte length for a non-empty chat key.
Mirrors @custom:contract IDotnsPopResolver InvalidChatKeyLength so the controller
can fail closed before the mint instead of bubbling the resolver's revert after partial
state has been committed.
uint256 private constant CHAT_KEY_LENGTH = 65
State Variables
protocolRegistry
Protocol-level address registry for all dotNS contracts.
IDotnsProtocolRegistry public protocolRegistry
_reservationMeta
Per-label queue metadata (head/tail pointers).
mapping(bytes32 labelhash => ReservationQueueMeta meta) internal _reservationMeta
_reservationEntries
Per-label sparse entries keyed by monotonically-increasing index.
mapping(bytes32 labelhash => mapping(uint64 index => ReservationEntry entry)) internal
_reservationEntries
_userReservations
Single per-user pointer into the reservation queues.
Keeps per-user reservation data behind one key and one struct value so callers read both fields in one call instead of two.
mapping(address user => UserReservation reservation) internal _userReservations
_reservedBaseLabel
Remembers the personhood name for each reserved labelhash so the PopRules
sync path can address the reservation by its original string form (PopRules keys its
reservations mapping by string).
Populated on first enqueue for a label, cleared when the queue empties. Exists
only to bridge the queue's bytes32 key space to PopRules' string key space;
nothing else reads it.
mapping(bytes32 labelhash => string reservedLabel) internal _reservedBaseLabel
reservationDuration
Duration (in seconds) after which a reservation entry is considered expired.
Sets the reservation duration, configurable by
governance via setReservationDuration.
uint64 public override reservationDuration
_pendingClaimUsers
Enumeration set of users holding at least one pending claim.
Membership equals the set of users with a non-empty queue. Used by
pendingClaimUserCount and pendingClaimUsers for paginated enumeration.
EnumerableSet.AddressSet private _pendingClaimUsers
_pendingClaimQueue
Per-user pile of deferred names awaiting a LabelStore.
The mint origin cannot deploy a LabelStore, so deferred names accumulate here until a
signed-origin @custom:function settlePendingClaims deploys the store and writes the stashed
labels. Entries never lapse and can be settled at any time.
mapping(address user => PendingClaim[] queue) internal _pendingClaimQueue
_popIssued
Labels this controller minted, keyed by the bare label without the TLD.
Provenance, not a transfer rule: written once at mint and never cleared, so it
stays true if a name later becomes transferable. Keyed by the label text rather
than the node because a reader holding only joseph.42 cannot derive the node
without first deciding whether the separator is part of the label or a subname
boundary, which is the question it is asking.
mapping(string label => bool issued) internal _popIssued
__gap
Reserved storage space to allow for layout changes in future upgrades.
uint256[50] private __gap
Functions
onlyRoot
Restricts calls to a Root origin.
modifier onlyRoot() ;
constructor
Note: oz-upgrades-unsafe-allow: constructor
constructor() ;
initialize
Initialises the PoP controller.
Called once through the UUPS proxy; _disableInitializers on the implementation
makes direct calls revert with @custom:reverts InvalidInitialization, and any nested
call outside an active initialiser scope reverts with @custom:reverts NotInitializing.
Emits @custom:emits ReservationDurationSet so indexers observe the initial value
through the same event the setter uses later.
function initialize(
address initialOwner,
IDotnsProtocolRegistry registry,
uint64 reservationDuration_
)
external
initializer;
Parameters
| 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 advances the head past expired entries (@custom:emits ReservationExpired for each one), removes the user from any prior queue position (@custom:emits ReservationRelinquished) so a single user holds at most one live reservation across all labels, and enqueues a fresh entry (@custom:emits ReservationQueued). The enqueue rejects with @custom:reverts AlreadyReserved when the user already holds a reservation that was not cleared by the prior removal and with @custom:reverts QueueFull when the per-label queue has reachedMAX_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. Moving the user out of a prior queue @custom:emits ReservationRelinquished. The caller remains agnostic about backend batching; it simply exposes a small retryable primitive.
function reservePersonhoodName(PersonhoodNameReservation calldata params)
external
override
onlyRoot;
Parameters
| 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: ReservationClaimed; a non-claim drops any pending entry the user holds (@custom:emits ReservationRelinquished). Either way the issuance
-
function: settlePendingClaims. Cross-chain callers pass the ABI-encoded issuance tuple as the call's payload, which Solidity decodes directly.
function issuePersonhoodName(PersonhoodNameIssuance calldata params)
external
override
onlyRoot;
Parameters
| 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(
IPopRules rules,
DeviceNameIssuance calldata params,
bool legacy
)
internal;
_issuePersonhoodName
Personhood-name issuance shared by @custom:function issuePersonhoodName and its legacy entrypoint.
legacy selects @custom:reverts InvalidLiteLabel / @custom:reverts InvalidBaseLabel
over the new label errors for the callers that decode the legacy ones.
function _issuePersonhoodName(PersonhoodNameIssuance calldata params, bool legacy) internal;
expireReservation
Permissionlessly removes expired entries from the head of a reservation queue.
Permissionless on purpose: anyone (typically a UI or a bot) can poke a stale queue
so the next live head takes over without waiting for the next gateway call. Validates
label as a letters-only personhood label (otherwise @custom:reverts
InvalidPersonhoodLabel) and @custom:emits ReservationExpired for every expired entry reaped
from the head. A label carrying a digit or a hyphen is not a personhood label and
Note: reverts: InvalidPersonhoodLabel, as does a device name, since a separator is not one either. Only a letters-only label reaches the queue, and one that was never reserved resolves to an empty queue so the call is a no-op.
function expireReservation(string calldata label) external override;
Parameters
| Name | Type | Description |
|---|---|---|
label | string | Personhood name whose queue is reaped. |
relinquishReservation
Lets the caller voluntarily drop their own active reservation.
Reverts with @custom:reverts NoActiveReservation when the caller holds no live reservation. On success the caller's entry is removed from its queue and
Note: emits: ReservationRelinquished is emitted; if the removed entry was the queue head, head advancement may additionally @custom:emits ReservationExpired for any stale entries reaped behind it.
function relinquishReservation() external override;
claimLabelStore
Settles the caller's own pending claims into their LabelStore.
Convenience for a user settling their own store: equivalent to
Notes:
-
function: settlePendingClaims with
msg.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(
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); 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.
Sparse storage: a zero entryOwner means the slot was relinquished, expired and
reaped, or never written. Callers pair this with @custom:function reservationMeta to walk
the live window [head, tail).
function reservationEntry(
bytes32 labelhash,
uint64 index
)
external
view
override
returns (address entryOwner, uint64 joinedAt);
Parameters
| 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 is delegated to
Notes:
-
function: RegistrationUtils.registerAndStore so this flow and the public commit-reveal flow share exactly one implementation of that sequence. The label is passed empty so the registrar does not deploy a
LabelStore; the mint origin cannot run theLabelStoreconstructor. 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 aLabelStore. The Store stays labels-only. Warm path emits @custom:emits NameRegistered immediately; the cold path emits @custom:emits PendingClaimStashed at mint and defers -
emits: NameRegistered to @custom:function settlePendingClaims when the claim settles.
function _completeGatewayRegistration(
address user,
string memory label,
bytes32 labelhash,
bytes32 node,
bytes memory chatKeyBytes,
bytes32 deviceLabelhash
)
internal;
_writeRecord
Writes a name's label into store.
Single canonical persistence step shared by the warm gateway path and
Note:
function: settlePendingClaims. The store key is node, matching
the registrar's _writeOwnerLabel convention. Idempotent on already-locked slots so a
user whose store was pre-populated under the same node (e.g. by a sibling protocol
flow) can still settle their pending claim without bricking on LabelAlreadyExists.
function _writeRecord(address store, bytes32 node, string memory label) internal;
Parameters
| 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(
IPopRules rules,
bytes32 labelhash,
string memory reservedLabel,
address user
)
internal;
_clearQueue
Wipes the entire reservation queue for labelhash and releases the
corresponding PopRules reservation.
Used when claimant claims their reservation: every other waiter is evicted
(@custom:emits ReservationEvicted for each one) and their per-user tracking state is
cleared, and PopRules is told the slot is free so future public registrations are unblocked
(the claim itself just minted the name, so there is nothing left to reserve).
function _clearQueue(bytes32 labelhash, address claimant) internal;
_advanceExpiredHead
Advances the queue head past every expired entry at the head of the queue.
Reset semantics matter: when the queue empties (head catches tail), the meta slot is deleted AND the PopRules base-name slot is released, so the public commit-reveal flow can register the label again. When a new live head emerges, PopRules is re-synced to that head so reservations cannot be paid around by another address. Emits
Note: emits: ReservationExpired once per expired entry reaped from the head.
function _advanceExpiredHead(bytes32 labelhash) internal;
_dropUserReservation
Drops user's own reservation entry, if any, and records that it happened.
The single path by which a user's entry leaves a queue other than by expiry or by a claim: an explicit relinquish, a standalone personhood-name issuance, and a re-reservation that moves the user to another queue. @custom:emits ReservationRelinquished only when the user held an entry, so an indexer can rebuild every queue from events alone.
function _dropUserReservation(address user) internal;
_removeUserFromQueue
Removes user from whichever reservation queue they currently occupy.
For a head removal, we delete the entry without bumping meta.head and delegate
the advance to _advanceExpiredHead. Its existing zero-owner skip walks past the
freshly-deleted slot, and its head != meta.head branch fires the PopRules resync
in the one place head promotion is actually handled. Non-head removals leave the
queue shape intact, so no advance or resync is needed. Emits nothing itself; callers go
through @custom:function _dropUserReservation.
function _removeUserFromQueue(address user) internal;
_validateDeviceLabel
Validates a device name stem.NN and derives (labelhash, node).
The stem is lowercase letters only, so this rejects a stem carrying a digit or a
hyphen before any node is derived. node is the hierarchical subnode stem under the
numeric container NN, the node a resolver reaches by walking the dotted name, and
labelhash stays the keccak of the whole label so it is a stable text identifier for events
and the reservation queue. legacy selects the label error, as in
Note: function: _requireDeviceLabel.
function _validateDeviceLabel(
string memory deviceLabel,
bool legacy
)
internal
view
returns (bytes32 labelhash, bytes32 node);
_deviceSubnode
Derives the hierarchical subnode for a device name <stem>.<suffix>.
Splits at the separator and walks suffix.tld then stem under it, so a device name
resolves as stem beneath its numeric container rather than as a hash of the whole label.
Shared by @custom:function _validateDeviceLabel and pending-claim settlement so every
consumer agrees on one node.
function _deviceSubnode(string memory deviceLabel) internal view returns (bytes32 subnode);
Parameters
| Name | Type | Description |
|---|---|---|
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(
string calldata label,
bool legacy
)
internal
view
returns (bytes32 labelhash, bytes32 node);
_validateReservablePersonhoodLabel
Validates a personhood name as reservable and returns its hashes.
Shared by both reservation paths so the guard cannot drift between them. Runs three
checks and reverts on the first failure, before any reservation state is mutated: the label
must be a letters-only personhood label, which makes it its own base name, classify outside
the governance-reserved tier, and have no owner on the registrar. The last check is the fix
for a reservation queued over an already-registered name: the queue keys by base name, so
such a reservation could never be redeemed yet would hold the base name, and so every device
name with that stem, for the full reservation window. exists (owner set) mirrors exactly
what makes the eventual claim's mint revert, so a label that passes here is one a claim can
still register.
function _validateReservablePersonhoodLabel(
IPopRules rules,
string calldata label,
bool legacy
)
internal
view
returns (bytes32 labelhash, bytes32 node);
_requireDeviceLabel
Reverts with the device-label error the caller decodes when ok is false.
legacy is set only on the @custom:contract IDotnsPopControllerLegacy entrypoints,
whose callers decode @custom:reverts InvalidLiteLabel; every other path reverts
Note: reverts: InvalidDeviceLabel.
function _requireDeviceLabel(bool ok, bool legacy) internal pure;
_requirePersonhoodLabel
Reverts with the personhood-label error the caller decodes when ok is false.
legacy is set only on the @custom:contract IDotnsPopControllerLegacy entrypoints,
whose callers decode @custom:reverts InvalidBaseLabel; every other path reverts
Note: reverts: InvalidPersonhoodLabel.
function _requirePersonhoodLabel(bool ok, bool legacy) internal pure;
_requireValidChatKey
Reverts when a non-empty chat key is not exactly CHAT_KEY_LENGTH bytes.
Mirrors the resolver's own length gate so the gateway sees a controller-local
InvalidChatKey revert before any mint state is written.
function _requireValidChatKey(bytes memory chatKey) internal pure;
_popResolver
Resolves the PoP resolver via the protocol registry.
function _popResolver() internal view returns (IDotnsPopResolver);
_popRules
Resolves the PopRules contract via the protocol registry.
function _popRules() internal view returns (IPopRules);
_storeFactory
Resolves the Store factory via the protocol registry.
function _storeFactory() internal view returns (IStoreFactory);
_registrar
Resolves the registrar via the protocol registry.
function _registrar() internal view returns (IDotnsRegistrar);
_registry
Resolves the registry via the protocol registry.
function _registry() internal view returns (IDotnsRegistry);
_syncPopRulesToHead
Writes the new head of the queue into PopRules so the public commit-reveal flow
rejects registrations of this base name for anyone other than newHead.
Callers guarantee newHead is non-zero (the queue holds a live entry) and that
_reservedBaseLabel[labelhash] is non-empty (any non-empty queue had its first head
write the slot). The release-then-reserve pair satisfies PopRules' ownership gate on
reserveBaseNameForPop.
function _syncPopRulesToHead(bytes32 labelhash, address newHead) internal;
_releasePopRulesSlot
Clears the PopRules slot and the local label bookkeeping when the queue empties (claim, last-relinquish, last-expire).
function _releasePopRulesSlot(bytes32 labelhash) internal;
_onlyRoot
Internal check enforcing a Root origin.
Authorises a call when @custom:function SystemUtils.originIsRoot is true, and
reverts with NotRoot otherwise. msg.sender is deliberately not consulted: a
Root origin has no account behind it, so reading msg.sender traps. That holds
for this frame and any delegatecall sharing it; a nested call sees the calling
contract as its sender and reads normally.
The check also holds for the whole Root transaction rather than the entry frame
alone, so nothing reachable from an onlyRoot entrypoint may call a
user-controlled address: such a callee could re-enter a gated function and still
pass. Every call out of this contract goes to a protocol contract resolved
through the registry.
function _onlyRoot() internal view;
_authorizeUpgrade
Function that should revert when msg.sender is not authorized to upgrade the contract.
Called by
{upgradeToAndCall}.
Normally, this function will use an xref:access.adoc[access control] modifier such as
{Ownable-onlyOwner}.
function _authorizeUpgrade(address) internal onlyOwner {}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner;