DotnsRegistry
Inherits: Initializable, UUPSUpgradeable, OwnableUpgradeable, IDotnsRegistry
Title: dotNS Registry
Author: Parity
Upgradeable on-chain registry for hierarchical name ownership and resolution.
Tokenised second-level nodes store owner == address(0) as a sentinel and defer to
IDotnsRegistrar.ownerOf; subnodes carry an explicit owner address in records.
Note: security-contact: admin@parity.io
Constants
MAX_SUBNAME_DEPTH
Deepest a new subname may sit below its second-level name: a subname directly under
alice is at depth 1, one under child.alice at depth 2.
Bounds the read cost, not the clear cost. Clearing is constant at any depth, but deciding whether a subname still resolves walks every level above it, and every read of a subname (owner, authority, each resolver record) pays that walk. Checked only when a subname is created, so a deeper subname that already exists keeps working.
uint256 public constant MAX_SUBNAME_DEPTH = 8
State Variables
records
Mapping of node identifiers to records.
mapping(bytes32 node => Record record) private records
protocolRegistry
Protocol-level address registry for all dotNS contracts.
IDotnsProtocolRegistry public protocolRegistry
_versions
Node => its versions.
mapping(bytes32 node => NodeVersions versions) private _versions
_parentOf
Subname => its parent. Zero for tokenised names, for deleted subnames, and for a subname whose parent was never recorded until it is linked (@custom:function linkSubname).
mapping(bytes32 node => bytes32 parent) private _parentOf
__gap
uint256[48] private __gap
Functions
authorised
Restricts access to the current owner of node.
modifier authorised(bytes32 node) ;
onlyRegistrarController
Restricts access to the configured registrar controller.
modifier onlyRegistrarController() ;
constructor
Note: oz-upgrades-unsafe-allow: constructor
constructor() ;
initialize
Initialises the registry.
Callable exactly once via Initializable, otherwise
Notes:
-
reverts: InvalidInitialization.
registrymust be non-zero, otherwise -
reverts: NotAllowed.
function initialize(
address initialOwner,
IDotnsProtocolRegistry registry
)
external
initializer;
Parameters
| Name | Type | Description |
|---|---|---|
initialOwner | address | Address that owns the contract once initialised. |
registry | IDotnsProtocolRegistry | Protocol-level address registry used to resolve sibling contracts. |
setSubnodeOwner
Creates or reassigns a subnode and assigns its owner.
Callable only by the current owner of record.parentNode, otherwise
Notes:
-
reverts: NotAuthorised. The new owner address must be non-zero, otherwise
-
emits: RecordsCleared and @custom:emits SubnamesCleared, so none of its old records or subnames come back. Emits @custom:emits NewOwner on each successful assignment.
function setSubnodeOwner(SubnodeRecord calldata record)
external
override
authorised(record.parentNode)
returns (bytes32 subnode);
setOwner
Creates or resets a node record for a tokenised second-level registration.
Restricted to the registrar's controllers, otherwise @custom:reverts NotAuthorised.
newOwner must be non-zero (otherwise @custom:reverts NotAllowed) and must match
the ERC-721 owner reported by the registrar (otherwise
Notes:
-
reverts: NotAuthorised). The function is callable both on a fresh registration and on every reclaim from escrow: each call rewrites
records[node].resolverto the protocol-registered default reverse resolver so a prior owner's resolver pointer (and the records keyed under it) cannot be inherited by the next holder across that recycle. A secondary-market ERC-721transferFromdoes not call the registry, so a name sold directly keeps the seller's resolver pointer until the buyer overwrites it. That covers the node itself and not the nodes beneath it: a subname stores its own owner, and @custom:function isAuthorised returns on that owner before consulting the registrar, so after the sale the seller keeps write authority over every subname whose stored owner is still the seller, until the buyer deletes each one through @custom:function deleteSubname or clears them all at once through @custom:function clearSubnames. Reassigning one through -
function: setSubnodeOwner keeps its records and the subnames beneath it. A subname created for someone else stays with that owner and leaves the seller no write path. The set is derivable from
-
emits: NewOwner, which indexes the parent node. Stores
owner = address(0)as a sentinel so reads delegate toIDotnsRegistrar.ownerOfand ERC-721 transfers remain authoritative. Emits
function setOwner(bytes32 node, address newOwner) external override onlyRegistrarController;
setResolver
Sets or clears the resolver for a node.
Callable only by the current node owner, otherwise @custom:reverts NotAuthorised.
For tokenised nodes, authorisation falls back to ERC-721 owner / approved /
operator-for-all via the registrar. The registry does not validate
resolverAddr against any interface or code presence; off-chain consumers must
verify resolver shape before trusting reads. Emits @custom:emits NewResolver on
success.
function setResolver(bytes32 node, address newResolver) external override authorised(node);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | |
newResolver | address |
setSubnodeResolver
Sets the resolver for an existing subnode.
Callable only by the current owner of record.parentNode, otherwise
Note:
reverts: NotAuthorised. The subnode owner can still update the resolver
directly via setResolver. Both entry points emit the same NewResolver(subnode, ...) event, so the parent can silently override a subnode owner's chosen resolver:
this is the parent-sovereign hierarchy applied to resolution. Off-chain consumers
that surface trust signals to subnode owners should treat any resolver rotation as
a re-attestation prompt. record.subLabel must be a single canonical DNS label
(otherwise @custom:reverts InvalidLabel) and record.parentLabel must be a name
path of at most StringUtils.MAX_NAME_PATH_OCTETS octets whose namehash matches
record.parentNode (otherwise
function setSubnodeResolver(SubnodeResolverRecord calldata record)
external
override
authorised(record.parentNode);
clearRecords
Clears the records of node on the resolvers that check recordVersion
(DotnsResolver and DotnsContentResolver), and points it back at the default
resolver.
Callable by anyone @custom:function isAuthorised admits for node, otherwise
Notes:
-
reverts: NotAuthorised. Constant cost whatever the number of records: the node's record version is bumped rather than any record being visited. Subnames are untouched; see @custom:function clearSubnames. Emits @custom:emits RecordsCleared and
-
emits: NewResolver.
function clearRecords(bytes32 node) external override authorised(node);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node to clear. |
clearSubnames
Clears every subname beneath node, at any depth, together with their records.
Callable by anyone @custom:function isAuthorised admits for node, otherwise
Note: reverts: NotAuthorised. Constant cost whatever the size or depth of the subtree: the node's subname version is bumped, and a subname resolves only while the version it was created under is still current at every level above it. A cleared subname can be created again through @custom:function setSubnodeOwner, and starts with no records and no subnames. The node's own records are untouched; see @custom:function clearRecords. Emits @custom:emits SubnamesCleared.
function clearSubnames(bytes32 node) external override authorised(node);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node whose subnames are cleared. |
clear
Clears every record and every subname of node.
Equivalent to @custom:function clearRecords followed by @custom:function clearSubnames in one call, with the same authority. The escrow calls it on release, once it holds the token, which is what makes a released name start clean for whoever gets it next.
function clear(bytes32 node) external override authorised(node);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node to clear. |
deleteSubname
Deletes one subname of parentNode, together with its records and every subname
beneath it.
Callable by anyone @custom:function isAuthorised admits for parentNode, otherwise
Notes:
-
reverts: NotAuthorised: subnames are parent-sovereign, so the parent decides, not the subname's holder. The subname's record and parent link are zeroed, releasing their storage deposit to the signer of the transaction; one version slot stays so the subname, created again, starts empty. The subname need not resolve: one a clear has already cut off can still be deleted by whoever controls
parentNodenow, after a transfer included. Subnames beneath it are cleared but keep their storage, and once their own parent stops resolving nobody can delete them, so freeing every deposit means deleting from the deepest subname up, each by whoever holds that subname's parent, before clearing anything above them. A subname must exist there, otherwise -
emits: SubnamesCleared for the subname.
function deleteSubname(
bytes32 parentNode,
bytes32 labelhash
)
external
override
authorised(parentNode);
Parameters
| Name | Type | Description |
|---|---|---|
parentNode | bytes32 | Parent of the subname. |
labelhash | bytes32 | Labelhash of the subname under parentNode. |
revokeName
Clears a tokenised name that is being revoked and deletes its record.
Callable only by the registrar, otherwise @custom:reverts NotRegistrar. The registrar calls it from @custom:function IDotnsRegistrar.revoke and
Notes:
-
function: IDotnsRegistrar.revokeIssued before burning the token. Bumps both versions of
node, so its records read as unset and every linked subname beneath it stops resolving, including after the name is registered again. A subname that was never linked keeps resolving until @custom:function linkSubname links it. Deletes the record, so the node reads as unregistered (@custom:function owner returns zero) rather than deferring to the burned token. Emits @custom:emits RecordsCleared, -
emits: SubnamesCleared, @custom:emits NewResolver with a zero resolver and
function revokeName(bytes32 node) external override;
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node of the name being revoked. |
revokeSubname
Deletes one subname of parentNode, together with its records and every subname
beneath it, on governance's authority rather than the parent's.
Reads no msg.sender (see SystemUtils.originIsRoot).
Notes:
-
function: deleteSubname does, with the same events and the same
-
reverts: SubnameNotFound, whoever holds the parent. A device name, a subname of a container the PoP controller holds, reverts @custom:reverts IssuedByGateway. A second-level name has no stored owner in this contract, so it reverts
function revokeSubname(bytes32 parentNode, bytes32 labelhash) external override;
Parameters
| Name | Type | Description |
|---|---|---|
parentNode | bytes32 | Parent of the subname. |
labelhash | bytes32 | Labelhash of the subname under parentNode. |
linkSubname
Records the parent of a subname whose parent was never recorded, so that clearing an ancestor reaches it.
Permissionless: the parent is proven by the namehash, since subnode is
keccak256(parentNode, labelhash), so a caller cannot link a subname anywhere else.
The subname is linked as created under its parent's first subname version, so a subname
whose ancestor was cleared before it was linked reads as cleared once it and every
level between it and that ancestor are linked; an unlinked level still ends the walk
as resolving. Every successful write path that keeps the subname and names its parent
(@custom:function setSubnodeOwner, @custom:function setSubnodeResolver) links it as a
side effect; this entry point covers the rest. @custom:reverts SubnameNotFound when
no subname exists there. A subname already linked is left as it is. Emits
Note: emits: SubnameLinked when it links.
function linkSubname(bytes32 parentNode, bytes32 labelhash) external override;
Parameters
| Name | Type | Description |
|---|---|---|
parentNode | bytes32 | Parent of the subname. |
labelhash | bytes32 | Labelhash of the subname under parentNode. |
recordVersion
Returns whether node resolves and the record version its records are written
under.
Resolvers stamp each write with recordsVersion and treat a record whose stamp
differs, or any record of a node that does not resolve, as unset. A tokenised name
resolves while its record exists; a subname resolves while no ancestor has cleared or
deleted it.
Version 0 is the version every node starts at, and a record stamped 0 matches it, so a
node that was never cleared reads every record stamped 0.
Checking a subname walks its ancestors, so the cost grows with its depth.
function recordVersion(bytes32 node)
external
view
override
returns (bool live, uint64 recordsVersion);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node identifier. |
Returns
| Name | Type | Description |
|---|---|---|
live | bool | True when node currently resolves. |
recordsVersion | uint64 | Record version of node. |
parentOf
Returns the recorded parent of a subname.
Zero for a tokenised name, for a node that was never created or was deleted, and for a subname whose parent was never recorded and that has not been linked since (@custom:function linkSubname). A backfill reads it to find the subnames still to link.
function parentOf(bytes32 node) external view override returns (bytes32 parent);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node identifier. |
Returns
| Name | Type | Description |
|---|---|---|
parent | bytes32 | Recorded parent node, or zero. |
owner
Returns the owner of a node.
For tokenised nodes the stored owner is the zero sentinel; the implementation falls
back to IDotnsRegistrar.ownerOf(uint256(node)). A subname that no longer resolves,
because it or a name above it was cleared or deleted, has no owner and returns zero, as
do @custom:function resolver, @custom:function recordExists and
Note: function: isAuthorised for it.
function owner(bytes32 node) external view override returns (address);
_owner
Body of @custom:function owner.
function _owner(bytes32 node) internal view returns (address);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node identifier. |
Returns
| Name | Type | Description |
|---|---|---|
<none> | address | The address that owns node, or zero when it does not resolve. |
resolver
Returns the resolver of a node, or zero when the node does not resolve.
function resolver(bytes32 node) external view override returns (address);
recordExists
Returns whether a node exists and resolves.
function recordExists(bytes32 node) external view override returns (bool);
isAuthorised
Returns whether account is authorised to manage node.
For subnodes, authority is the explicit stored owner. For tokenised nodes it is the ERC-721 owner, an address approved for the token, or an operator approved for all of the owner's tokens via the registrar. This is the canonical authorisation check the registry enforces on owner-gated entry points; sibling contracts may consult it so a single registrar-level approval delegates management across the protocol. Returns false for a node that does not exist.
function isAuthorised(
bytes32 node,
address account
)
external
view
override
returns (bool authorisedFlag);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node identifier. |
account | address | Address whose authority is being checked. |
Returns
| Name | Type | Description |
|---|---|---|
authorisedFlag | bool | True when account may manage node. |
_writeSubnodeToStore
Writes subnode registration to the owner's LabelStore.
Keys the entry by node (full namehash) rather than labelhash so a single store
lookup yields the canonical full name without re-walking the parent chain.
function _writeSubnodeToStore(
address storeOwner,
bytes32 node,
string memory fullName
)
internal;
_parentNamehash
Computes the namehash of parentLabel rooted at the network's TLD node.
Walks the label right-to-left in calldata using memory-safe assembly to avoid the
cost of slicing into intermediate bytes and to keep gas linear in the label depth.
Reads the TLD node from the protocol registry, so it is a view rather than pure.
function _parentNamehash(string calldata parentLabel) internal view returns (bytes32 node);
_authorised
Internal authorisation check for node ownership.
Reverts with NotAuthorised when msg.sender is not authorised for node.
function _authorised(bytes32 node) internal view;
_isAuthorised
Canonical authorisation rule for a node, parameterised by account.
Honours the sentinel-zero pattern: if the registry has no explicit owner, fall back
to the registrar's ERC-721 owner / approved / operator-for-all chain. This is the
single source of truth _authorised and isAuthorised both delegate to.
function _isAuthorised(bytes32 node, address account) internal view returns (bool);
_isLive
Returns whether node currently resolves.
A tokenised name, or a node that was never created or was deleted, carries a zero stored owner, and resolves exactly when its record exists. A subname resolves while the subname version it was created under is still its parent's, and its parent resolves in turn, so the walk ends at the tokenised name above it. Clearing therefore costs the same at any depth, and the depth is paid on reads instead. A subname with no recorded parent ends the walk as resolving, because nothing above it can be checked, until
Note: function: linkSubname or a write through its parent links it.
function _isLive(bytes32 node) internal view returns (bool live);
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node identifier. |
Returns
| Name | Type | Description |
|---|---|---|
live | bool | True when node resolves. |
_labelCount
Returns the number of labels in a dotted name path, which is the depth below its second-level name of a subname created under it.
path has already passed isNamePath, so it has no empty segment and no leading or
trailing dot, and the count is one more than the number of dots.
function _labelCount(string calldata path) internal pure returns (uint256 count);
Parameters
| Name | Type | Description |
|---|---|---|
path | string | Dotted parent path without the TLD. |
Returns
| Name | Type | Description |
|---|---|---|
count | uint256 | Number of labels in path. |
_deleteSubname
Deletes one subname of parentNode, its records and every subname beneath it.
Shared by @custom:function deleteSubname and @custom:function revokeSubname, which
differ only in who may call them. Reads no msg.sender, so it also runs under a Root
origin (see SystemUtils.originIsRoot).
function _deleteSubname(bytes32 parentNode, bytes32 labelhash) internal;
Parameters
| Name | Type | Description |
|---|---|---|
parentNode | bytes32 | Parent of the subname. |
labelhash | bytes32 | Labelhash of the subname under parentNode. |
_linkIfUnlinked
Links subnode to parentNode when it is a subname with no recorded parent.
The caller derives subnode from parentNode, which is what proves the parent. The
subname is linked under its parent's first subname version, which is the version it
was created under: every path that creates a subname records its parent, so one with no
recorded parent was created while its parent's subname version was still zero.
function _linkIfUnlinked(bytes32 subnode, bytes32 parentNode) internal;
Parameters
| Name | Type | Description |
|---|---|---|
subnode | bytes32 | The subname. |
parentNode | bytes32 | Its parent. |
_clearRecords
Clears every record of node and returns its resolver pointer to the default.
function _clearRecords(bytes32 node) internal;
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node to clear. |
_clearRecordVersion
Bumps the record version of node.
function _clearRecordVersion(bytes32 node) internal;
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node identifier. |
_clearSubnameVersion
Bumps the subname version of node.
function _clearSubnameVersion(bytes32 node) internal;
Parameters
| Name | Type | Description |
|---|---|---|
node | bytes32 | Node identifier. |
_onlyRegistrarController
Internal check for registrar-authorised controller privileges.
The registry trusts every controller the registrar trusts. Routing controller
authorisation through the registrar's controllers mapping keeps the trust list
in one place and lets commit-reveal and PoP controllers coexist without registry
reconfiguration on each addition.
function _onlyRegistrarController() internal view;
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. |
_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
NodeVersions
Versions that let a node, or everything beneath it, be cleared at constant cost.
Neither records nor subnames can be listed, so clearing never visits them. It bumps a version instead, and readers compare against it:
recordsis the version a node's resolver records are stamped with. Bumping it makes every record of the node read as unset.subnamesis the version a new child is created under. Bumping it makes every child, and through them every deeper subname, stop resolving.parentSubnamesis the parent'ssubnamesat the moment this subname was created. Kept apart fromrecordsbecause @custom:function setOwner rewrites a wholeRecord, and a version reset to zero would bring back every record written under it.
struct NodeVersions {
uint64 records;
uint64 subnames;
uint64 parentSubnames;
}