IDotnsPopController

Git Source

Inherits: IDotnsController

Title: IDotnsPopController

Interface for the dedicated PoP controller orchestrating lite-person and full-person username issuance on behalf of the PoP gateway pallet.

Deliberately disjoint from @custom:contract IDotnsRegistrarController. The two controllers coexist on @custom:contract DotnsRegistrar via its multi-controller affordance and neither imports the other. Collision handling reduces to the registrar's ERC721 availability check (first-to-mint wins). Reservation queuing for reservedBaseLabel is an intra-PoP coordination mechanism only; it does not block public registrations. Label formats: Lite-person usernames (first argument to @custom:function reserveBaseName and the liteLabel of a LinkKind.LiteUsername link) are DNS labels with exactly two trailing digits (e.g. alice42) per @custom:function StringUtils.isLitePersonLabel. The gateway strips any separator before calling so the on-chain label is flat. Full-person usernames (the label of @custom:function registerBaseName and the optional reservedBaseLabel of @custom:function reserveBaseName) follow the DNS-label rules enforced by @custom:function StringUtils.isSingleLabel (e.g. alice). Lite and public registrations share one namespace; first-to-mint wins at the ERC721 layer. Cross-flow priority on the stripped base stem is arbitrated by

Notes:

  • function: IPopRules.reserveBaseNameForPop.

  • security-contact: admin@parity.io

Functions

reserveBaseName

Registers a lite-person username on behalf of the supplied user and optionally enqueues a reservation for a base name they intend to claim as a full person later.

Callable only under a substrate Root origin (otherwise @custom:reverts NotRoot). The lite leg validates the dotted stem.NN shape and requires the flattened label to classify as PopLite (otherwise @custom:reverts InvalidLiteLabel), 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 LiteNameReserved and @custom:emits NameRegistered; on a cold-path mint it @custom:emits LiteNameReserved and

Notes:

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

  • function: settlePendingClaims when the claim settles. The base-name leg only runs when reservedBaseLabel is non-empty: it validates the DNS-label shape and requires a true base label with no trailing digits (otherwise @custom:reverts InvalidBaseLabel) and with no owner on the registrar (otherwise @custom:reverts BaseNameAlreadyRegistered), since a name that already has an owner could never be claimed. This validation runs before both the lite mint and any queue mutation, so an already-registered reservedBaseLabel aborts the whole call and the candidate receives no lite username 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 so a single user holds at most one live reservation across all labels, and enqueues a fresh entry (@custom:emits ReservationQueued). The enqueue rejects with @custom:reverts AlreadyReserved when the user already holds a reservation that was not cleared by the prior removal and with @custom:reverts QueueFull when the per-label queue has reached MAX_RESERVATION_QUEUE. Cross-chain callers pass the ABI-encoded reservation tuple as the call's payload, which Solidity decodes directly.

function reserveBaseName(BaseReservation calldata params) external;

Parameters

NameTypeDescription
paramsBaseReservationReservation request; see @custom:struct BaseReservation.

reserveBaseNameOnly

Enqueues only the full/base-name reservation for a user.

Callable only under a substrate Root origin (otherwise @custom:reverts NotRoot). This is the second step of the split gateway flow: @custom:function reserveLiteName mints the lite username first, then this function reserves the full/base label in a separate transaction so proof-size stays below per-call limits. Reverts with @custom:reverts InvalidBaseLabel when the label is empty, non-canonical, digit-suffixed, or governance-reserved, and with

Note: reverts: BaseNameAlreadyRegistered when the label already has an owner on the registrar and so could never be claimed. The caller remains agnostic about backend batching; it simply exposes a small retryable primitive.

function reserveBaseNameOnly(BaseNameReservation calldata params) external;

Parameters

NameTypeDescription
paramsBaseNameReservationReservation request; see @custom:struct BaseNameReservation.

reserveLiteName

Registers a lite-person username on behalf of the supplied user without touching the base-name reservation queue.

Callable only under a substrate Root origin (otherwise @custom:reverts NotRoot). The supplied label must satisfy the dotted stem.NN shape and the flattened label must classify as PopLite (otherwise @custom:reverts InvalidLiteLabel); a supplied chat key whose length is neither zero nor CHAT_KEY_LENGTH reverts

Notes:

  • reverts: InvalidChatKey before mint and resolver writes run. On a warm-path mint

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

  • emits: NameRegistered deferred to @custom:function settlePendingClaims when the claim settles. Cross-chain callers pass the ABI-encoded lite-registration tuple as the call's payload, which Solidity decodes directly.

function reserveLiteName(LiteRegistration calldata params) external;

Parameters

NameTypeDescription
paramsLiteRegistrationRegistration request; see @custom:struct LiteRegistration.

registerBaseName

Registers a full-person username on behalf of the supplied user.

Callable only under a substrate Root origin (otherwise @custom:reverts NotRoot). The base label must satisfy the DNS-label shape and be a true base label with no trailing digits (otherwise @custom:reverts InvalidBaseLabel), and the label must not classify as governance-reserved (otherwise @custom:reverts InvalidBaseLabel). The gateway also defers to PopRules as the single cross-flow authority: when PopRules carries a live base-name slot held by another user (stamped by the public commit-reveal flow or this controller's prior queue head), the call reverts @custom:reverts NotHolder before any queue mutation. Two orthogonal axes drive the state machine. The reservation axis treats the user as claiming if and only if they hold the live head-of-queue reservation on the base label: a claim wipes the entire queue, releases the PopRules slot, and @custom:emits BaseNameClaimed; a non-claim silently relinquishes any pending entry the user holds and @custom:emits StandaloneNameRegistered. Advancing the queue head past expired entries @custom:emits ReservationExpired for each one. The chat-key axis selects whether a fresh key is persisted on the resolver or the new entry inherits its key from a prior lite-person username. The fresh-key branch rejects a chat key whose length is neither zero nor CHAT_KEY_LENGTH (otherwise

Notes:

  • reverts: InvalidChatKey). The LiteUsername branch validates the lite label's NAMEXX shape (otherwise @custom:reverts InvalidLiteLabel), requires the registrant to own the lite token (otherwise @custom:reverts LiteLabelNotOwnedByUser), reads the lite node's chat key from the resolver and copies it across; if the lite node carries no chat key the inherited value is empty and the full node's chat-key write is silently skipped (the LiteToFullLinked event still fires). @custom:emits LiteToFullLinked alongside the registration event. On a warm-path mint the event order is

  • emits: NameRegistered first (from the inner mint), then

  • emits: BaseNameClaimed or @custom:emits StandaloneNameRegistered, then

  • emits: LiteToFullLinked when applicable. On a cold-path mint

  • emits: PendingClaimStashed replaces the initial @custom:emits NameRegistered; the deferred @custom:emits NameRegistered fires later from @custom:function settlePendingClaims. Cross-chain callers pass the ABI-encoded full-registration tuple as the call's payload, which Solidity decodes directly.

function registerBaseName(FullRegistration calldata params) external;

Parameters

NameTypeDescription
paramsFullRegistrationRegistration request; see @custom:struct FullRegistration.

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 the DNS-label shape of reservedBaseLabel (otherwise @custom:reverts InvalidBaseLabel) and @custom:emits ReservationExpired for every expired entry reaped from the head. Only base-shaped labels (no trailing digits) ever key a reservation queue, so a lite-shaped label still passes the shape check but resolves to an empty queue and the call is a no-op.

function expireReservation(string calldata reservedBaseLabel) external;

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;

isReservedForClaim

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

Validates the DNS-label shape of reservedBaseLabel (otherwise

Note: reverts: InvalidBaseLabel) before inspecting the queue.

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

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;

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 (dotli, dweb) use it to enumerate live queue state without scanning storage.

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

Parameters

NameTypeDescription
labelhashbytes32Keccak-256 of the base label whose queue is being read.

Returns

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

reservationEntry

Returns the queue entry at index for labelhash.

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

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

Parameters

NameTypeDescription
labelhashbytes32Keccak-256 of the base label 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
    returns (UserReservation memory reservation);

Parameters

NameTypeDescription
useraddressAccount whose reservation pointer is being read.

Returns

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

reservedBaseLabelOf

Returns the base label 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 reservedBaseLabelOf(bytes32 labelhash) external view returns (string memory baseLabel);

Parameters

NameTypeDescription
labelhashbytes32Keccak-256 of the base label.

Returns

NameTypeDescription
baseLabelstringThe base label string, or empty when unknown.

reservationDuration

Returns the window, in seconds, after which a queue or pending-claim entry lapses.

Governance-configurable via @custom:function setReservationDuration. Read by the lens to compute each pending claim's settlement deadline.

function reservationDuration() external view returns (uint64 duration);

Returns

NameTypeDescription
durationuint64Reservation duration in seconds.

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 pallet-revive charges 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
    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.

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: settlePendingClaims.

function claimLabelStore() external returns (bool moreRemaining);

Returns

NameTypeDescription
moreRemainingboolWhether the caller still holds unsettled entries.

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; the settlement deadline is mintedAt + reservationDuration. 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
    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 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 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
    returns (address[] memory users);

Parameters

NameTypeDescription
offsetuint256Start index.
limituint256Maximum entries to return.

Returns

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

Events

LiteNameReserved

Emitted when a lite-person username is registered via the PoP gateway.

event LiteNameReserved(bytes32 indexed labelhash, address indexed user, string label);

BaseNameClaimed

Emitted when a full-person username is claimed out of an existing reservation.

event BaseNameClaimed(bytes32 indexed labelhash, address indexed user, string label);

StandaloneNameRegistered

Emitted when a standalone full-person username is registered via the PoP gateway.

event StandaloneNameRegistered(bytes32 indexed labelhash, address indexed user, string label);

ReservationQueued

Emitted when a reservation entry is added to the queue for a base name.

event ReservationQueued(
    bytes32 indexed reservedLabelhash, address indexed user, uint64 position
);

Parameters

NameTypeDescription
reservedLabelhashbytes32
useraddress
positionuint64Position in the queue at the time of joining (0 = active holder).

ReservationExpired

Emitted when a reservation entry is removed due to expiry.

event ReservationExpired(bytes32 indexed reservedLabelhash, address indexed user);

ReservationRelinquished

Emitted when a user voluntarily relinquishes their reservation.

event ReservationRelinquished(bytes32 indexed reservedLabelhash, address indexed user);

LiteToFullLinked

Emitted when a full-person username is linked to a lite-person username.

event LiteToFullLinked(bytes32 indexed fullLabelhash, bytes32 indexed liteLabelhash);

ReservationDurationSet

Emitted when the reservation duration is updated.

event ReservationDurationSet(uint64 duration);

NameRegistered

Emitted when a name is successfully registered via the PoP controller.

event NameRegistered(
    string indexed label, bytes32 indexed labelhash, address indexed owner, address store
);

Parameters

NameTypeDescription
labelstring
labelhashbytes32
owneraddress
storeaddressThe Store instance used to persist the immutable registration record.

PendingClaimStashed

Emitted when a gateway-path mint defers its LabelStore write into the pending-claim mapping because the user has no store yet.

event PendingClaimStashed(address indexed user, bytes32 indexed labelhash, string label);

PendingClaimSettled

Emitted when a pending claim is written into a LabelStore.

Fires once per settled entry from @custom:function settlePendingClaims. settledBy is the caller: it equals user for a self-settlement and is any other address for a third-party settlement, so consumers can tell the two apart from the log alone.

event PendingClaimSettled(
    address indexed user, bytes32 indexed labelhash, address store, address indexed settledBy
);

Parameters

NameTypeDescription
useraddressAccount the settled name belongs to.
labelhashbytes32Labelhash of the settled name.
storeaddressThe LabelStore the label was written into.
settledByaddressCaller that performed and paid for the settlement.

ReservationHeadAdvanced

Emitted when a reservation queue's head transitions to a new user, either via expiry of the prior head or via the explicit relinquish path.

event ReservationHeadAdvanced(bytes32 indexed labelhash, address indexed newHead);

Parameters

NameTypeDescription
labelhashbytes32Base-label hash whose queue head changed.
newHeadaddressAddress now holding the head slot.

Errors

NotRoot

Thrown when a gated entrypoint is reached without a substrate Root origin.

Carries no caller parameter: a Root origin has no account to report, and reading msg.sender under one traps.

error NotRoot();

InvalidLiteLabel

Thrown when a supplied lite-person label does not match NAMEXX.

error InvalidLiteLabel();

InvalidBaseLabel

Thrown when a supplied base label is not a canonical DNS label.

error InvalidBaseLabel();

BaseNameAlreadyRegistered

Thrown when a reserved base label already has an owner on the registrar, so the queued reservation could never be redeemed at mint time.

error BaseNameAlreadyRegistered();

InvalidChatKey

Thrown when a supplied chat key is non-empty and not exactly 65 bytes long.

Mirrors the resolver's InvalidChatKeyLength so the controller surfaces a controller-local error before the mint runs.

error InvalidChatKey(uint256 length);

Parameters

NameTypeDescription
lengthuint256Caller-supplied chat key length, in bytes.

NoActiveReservation

Thrown when a user tries to claim or relinquish a reservation that they do not hold.

error NoActiveReservation(address user);

QueueFull

Thrown when a reservation queue has reached its capacity.

error QueueFull(bytes32 labelhash);

AlreadyReserved

Thrown when attempting to enqueue a user who already has an active reservation.

error AlreadyReserved(address user, bytes32 labelhash);

NotHolder

Thrown when someone tries to mint a base label in standalone mode while another user holds the live head-of-queue reservation.

error NotHolder(address user, bytes32 labelhash);

LiteLabelNotOwnedByUser

Thrown when a lite-link inheritance does not match the registrar-side owner of the lite label.

Prevents identity hijack by ensuring the registrant on the full-name leg actually holds the prior lite identity whose chat key is being inherited.

error LiteLabelNotOwnedByUser(address user, bytes32 liteLabelhash);

Parameters

NameTypeDescription
useraddressRegistrant supplied by the gateway.
liteLabelhashbytes32Lite label whose ownership did not match.

ReservationDurationTooLow

Thrown when @custom:function setReservationDuration is called with a value below the protocol minimum.

error ReservationDurationTooLow(uint64 duration);

Parameters

NameTypeDescription
durationuint64Caller-supplied duration, in seconds.

Structs

Tagged union selecting the chat-key source for a full-person registration.

struct Link {
    LinkKind kind;
    string liteLabel;
    bytes chatKey;
}

Properties

NameTypeDescription
kindLinkKind
liteLabelstringLite-person NAMEXX label (only read when kind == LiteUsername).
chatKeybytesChat key bytes (only read when kind == None).

UserReservation

Per-user reservation pointer: which queue the user sits in and where.

struct UserReservation {
    bytes32 labelhash;
    uint64 index;
}

Properties

NameTypeDescription
labelhashbytes32Non-zero when the user holds a live reservation; zero otherwise.
indexuint64Monotonic queue index, meaningful only when labelhash is non-zero.

ReservationEntry

Reservation queue entry: a user and the timestamp they joined the queue.

Packs into a single storage slot (20 + 8 bytes).

struct ReservationEntry {
    address owner;
    uint64 joinedAt;
}

ReservationQueueMeta

Metadata describing the occupied range of a reservation queue.

Uses monotonically increasing indices. Active entries occupy [head, tail); length = tail - head. Slots past head are deleted as the head advances so garbage never accumulates.

struct ReservationQueueMeta {
    uint64 head;
    uint64 tail;
}

PendingClaim

Deferred per-user binding of a freshly minted name to its LabelStore.

Recorded by the gateway path when the user has no LabelStore. The binding later settles via @custom:function settlePendingClaims, which deploys the store from a signed origin and writes the stashed label. PoP-resolver records (chat key, lite link) are persisted eagerly at mint time on @custom:contract IDotnsPopResolver, not at settlement, so the resolver carries the full identity record regardless of whether the user has settled their Store. A user accumulates one entry per deferred name: the Root gateway path cannot deploy a LabelStore (contract creation is forbidden from the Root origin), so it keeps stashing entries until a signed-origin @custom:function settlePendingClaims deploys the store and settles the entries. Each entry's deadline is measured from its own mintedAt against reservationDuration.

struct PendingClaim {
    string label;
    uint64 mintedAt;
}

Properties

NameTypeDescription
labelstringBare DNS label (no TLD); the TLD is appended at settlement time.
mintedAtuint64Timestamp of the originating mint.

LiteRegistration

Lite-person registration payload.

Single struct so the gateway can ABI-encode one tuple as the cross-chain payload and the contract decodes it directly out of msg.data. All fields are required; chatKey may be empty bytes to skip the resolver write.

struct LiteRegistration {
    string liteLabel;
    address user;
    bytes chatKey;
}

Properties

NameTypeDescription
liteLabelstringLite-person NAMEXX label being minted.
useraddressBeneficiary account on this chain.
chatKeybytesChat-key bytes persisted on the PoP resolver. Empty leaves the slot unset.

BaseReservation

Lite-person registration combined with an optional base-name reservation.

BaseReservation is a @custom:struct LiteRegistration plus a base-label reservation slot, expressed as composition rather than duplicated fields so internal helpers can consume the lite leg via params.lite without unpacking. The lite leg always runs; the reservation leg only runs when reservedBaseLabel is non-empty.

struct BaseReservation {
    LiteRegistration lite;
    string reservedBaseLabel;
}

Properties

NameTypeDescription
liteLiteRegistrationLite-person registration request; see LiteRegistration.
reservedBaseLabelstringBase label to enqueue for a later full-person claim. Empty string skips the reservation leg.

BaseNameReservation

Base-name reservation payload for the split gateway flow.

This is the reservation-only primitive. The lite username mint is handled by

Notes:

  • function: reserveLiteName, and LabelStore settlement is handled by

  • function: settlePendingClaims.

struct BaseNameReservation {
    address user;
    string reservedBaseLabel;
}

Properties

NameTypeDescription
useraddressBeneficiary account that will hold the reservation.
reservedBaseLabelstringBase label to enqueue for a later full-person claim.

FullRegistration

Full-person registration payload.

struct FullRegistration {
    string label;
    address user;
    Link link;
}

Properties

NameTypeDescription
labelstringBase DNS label being minted.
useraddressBeneficiary account on this chain.
linkLinkChat-key source for the new entry; see @custom:struct Link.

Enums

LinkKind

Discriminant for the Link union supplied to registerBaseName.

Selects the chat-key source for the full-person username. Orthogonal to whether the registration is a claim or standalone; that is derived from on-chain reservation state. None means the caller supplies a fresh chat key in link.chatKey. LiteUsername means the full-person username is linked to a prior lite-person username (link.liteLabel) and inherits its chat key.

enum LinkKind {
    None,
    LiteUsername
}