diff --git a/.env.example b/.env.example index c9240b3e5..3c8525b06 100644 --- a/.env.example +++ b/.env.example @@ -55,7 +55,7 @@ DOTNS_TLD=dot DEPLOYMENT_NETWORK= # Optional. Address of a pre-deployed CREATE3 factory to reuse. When set, -# DeployCore adopts it instead of minting a new one, so every DotNS address is +# DeployCore adopts it instead of minting a new one, so every dotNS address is # pinned to that factory and stays the same across chain resets. `bun run # deploy:all` sets this automatically from the factory step; set it by hand only # when running `bun run deploy` (the pipeline on its own). diff --git a/.github/abi-contracts.txt b/.github/abi-contracts.txt index c7cc76914..8511dde30 100644 --- a/.github/abi-contracts.txt +++ b/.github/abi-contracts.txt @@ -44,6 +44,7 @@ IPopRules IDotnsProtocolRegistry IDotnsNameEscrow IDotnsPopController +IDotnsPopControllerLegacy IDotnsPopResolver IDotnsPopLens IDotnsController diff --git a/.github/labels.yml b/.github/labels.yml index b959e161e..a08724bc0 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -120,7 +120,7 @@ - name: "dotns-sdk" color: "5319e7" - description: "DotNS SDK (UI, CLI, libraries)" + description: "dotNS SDK (UI, CLI, libraries)" - name: "smartcontracts" color: "e3a21a" diff --git a/.github/workflows/deploy-contracts.yml b/.github/workflows/deploy-contracts.yml index 0165adef7..07588396b 100644 --- a/.github/workflows/deploy-contracts.yml +++ b/.github/workflows/deploy-contracts.yml @@ -282,7 +282,7 @@ jobs: fi # This CI deploy reproduces the expected address set. The canonical factory - # was deployed above, and every DotNS address is a pure function of that + # was deployed above, and every dotNS address is a pure function of that # factory plus a fixed salt, so the freshly deployed manifest must equal the # committed expected set. Assert that, print the expected-vs-actual table, then # rerun the pipeline to prove the deploy is resumable: a rerun adopts every diff --git a/.github/workflows/issue-add-to-project.yml b/.github/workflows/issue-add-to-project.yml index dfbac4605..23e3e0833 100644 --- a/.github/workflows/issue-add-to-project.yml +++ b/.github/workflows/issue-add-to-project.yml @@ -1,4 +1,4 @@ -name: Add Issues to DotNS Project +name: Add Issues to dotNS Project on: issues: diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index 6e40e24e7..35d759bad 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -214,7 +214,7 @@ jobs: ASSET_BASE="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/$TAG" cat > release-body.md << 'ENDOFBODY' - ## DotNS ABI Package (Pre-release) + ## dotNS ABI Package (Pre-release) > **This is a pre-release.** ABIs may change before the stable release. diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 4f73e3c21..1fb0af90a 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -138,7 +138,7 @@ jobs: FOUNDRY_DISABLE_NIGHTLY_WARNING: "1" run: forge test -vv - # pallet-revive genesis, so a chain can carry DotNS from block zero rather than + # pallet-revive genesis, so a chain can carry dotNS from block zero rather than # deploying it afterwards. Built here rather than downstream: this repo owns the # contracts, the deploy scripts and the CREATE3 factory key that fixes every address, # and the artifact then ships from the same commit as the ABIs beside it. @@ -208,7 +208,7 @@ jobs: ASSET_BASE="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/$TAG" cat > release-body.md << 'ENDOFBODY' - ## DotNS ABI Package + ## dotNS ABI Package **Contracts** | Contract | ABI | @@ -260,7 +260,7 @@ jobs: echo "" echo "### Genesis" echo "" - echo "Pallet-revive genesis state, for a chain that should carry DotNS from block zero" + echo "Pallet-revive genesis state, for a chain that should carry dotNS from block zero" echo "rather than deploying it afterwards." echo "" for tld in $DOTNS_TLDS; do diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12c1b6897..8aafe7900 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to DotNS +# Contributing to dotNS -These guidelines apply to the DotNS repository ("dotns"). Contributions are welcome via issues, pull requests, reviews, and testing feedback. Protocol behaviour is documented in [README.md](./README.md) and network addresses in `deployments//.json`; this file is the contributor mechanics. +These guidelines apply to the dotNS repository ("dotns"). Contributions are welcome via issues, pull requests, reviews, and testing feedback. Protocol behaviour is documented in [README.md](./README.md) and network addresses in `deployments//.json`; this file is the contributor mechanics. ## Types of contributing @@ -74,7 +74,7 @@ Before opening a pull request: ## Feature design -Treat the chain as the database. Assume no servers and no indexers. If a feature needs an offchain service to be usable, it is not a DotNS feature. +Treat the chain as the database. Assume no servers and no indexers. If a feature needs an offchain service to be usable, it is not a dotNS feature. This has a practical implication: every feature must come with an explicit query path. A client should be able to start from a small set of known contracts and find everything it needs with a bounded number of calls. Every getter is `external view`; controllers and resolvers check authorisation on writes, never on reads; governance key rotation does not break existing read paths because consumers re-resolve their siblings through the protocol registry on every call. @@ -118,12 +118,12 @@ Example query paths. Each row starts from a small set of known contracts; every | Lookup | Path | | --- | --- | -| Lite labelhash => full-person node | Protocol registry => PoP resolver => `fullClaim(liteLabelhash)` | -| Full-person node => lite labelhash | Protocol registry => PoP resolver => `liteLink(fullNode)` | +| Device-name labelhash => personhood-name node | Protocol registry => PoP resolver => `personhoodNodeOf(deviceLabelhash)` | +| Personhood-name node => device-name labelhash | Protocol registry => PoP resolver => `deviceLabelhashOf(personhoodNode)` | | Node => chat key | Protocol registry => PoP resolver => `chatKey(node)` | | Node or tokenId => registered label | Protocol registry => registrar => `labelOf(uint256(node))` | -| Base stem => gateway-reservation state | Protocol registry => PoP controller => `isReservedForClaim(baseLabel)` | -| Base stem => cross-flow reservation state | Protocol registry => PopRules => `isBaseNameReserved(baseLabel)` | +| Personhood name => gateway-reservation state | Protocol registry => PoP controller => `isReservedForClaim(label)` | +| Base name => cross-flow reservation state | Protocol registry => PopRules => `isBaseNameReserved(baseName)` | | Node => ERC721 owner | Protocol registry => registrar => `ownerOf(uint256(node))` | | Subnode => forward-registry owner | Protocol registry => registry => `owner(subnode)` | | Node => forward address record | Protocol registry => forward resolver => address record | @@ -191,7 +191,7 @@ Do not cache the new address in storage on the existing contract during the upgr ## Static analysis and security tooling -DotNS uses automated checks (including static analysis) on pull requests. +dotNS uses automated checks (including static analysis) on pull requests. Important caveats: @@ -284,4 +284,4 @@ forge test --no-match-path 'test/fork/**' Be respectful and constructive. - Harassment, abuse, or aggressive behaviour is not acceptable. -- Spam issues/PRs, or contributions unrelated to DotNS, may be closed. +- Spam issues/PRs, or contributions unrelated to dotNS, may be closed. diff --git a/DEPLOYMENTS.md b/DEPLOYMENTS.md index 4c764fc02..630e0e9f9 100644 --- a/DEPLOYMENTS.md +++ b/DEPLOYMENTS.md @@ -1,10 +1,10 @@ -# DotNS Deployments +# dotNS Deployments Current deployment addresses and developer deployment notes for dotNS contracts. ## What this file is for -This file is the operational companion to the README. It explains how to run the local ETH-RPC adapter, how to deploy DotNS, where deployment manifests are written, and which addresses are currently live on the supported Paseo environments. +This file is the operational companion to the README. It explains how to run the local ETH-RPC adapter, how to deploy dotNS, where deployment manifests are written, and which addresses are currently live on the supported Paseo environments. > For a short, do-this-in-order checklist (including how to target **any** Polkadot chain, not just the Paseo environments), see [`DEPLOYMENT_CHECKLIST.md`](./DEPLOYMENT_CHECKLIST.md). diff --git a/DEPLOYMENT_CHECKLIST.md b/DEPLOYMENT_CHECKLIST.md index 0bb92b668..0cc935183 100644 --- a/DEPLOYMENT_CHECKLIST.md +++ b/DEPLOYMENT_CHECKLIST.md @@ -1,6 +1,6 @@ -# DotNS Deployment Checklist +# dotNS Deployment Checklist -A step-by-step, copy/paste checklist for deploying DotNS to **any** Polkadot +A step-by-step, copy/paste checklist for deploying dotNS to **any** Polkadot chain (any PolkaVM / `revive`-backed Asset Hub-style chain that exposes an ETH-RPC adapter). diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 7980a4d1f..fa67c2a62 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -1,6 +1,6 @@ # Known issues -DotNS carries a small set of constraints worth knowing before deploying or building against it. Most stem from the current pallet-revive runtime rather than from protocol design, and collapse to a no-op once the runtime gains the corresponding capability. Each is described in full where the relevant contract is documented in the [README](./README.md#contracts); this file is the consolidated index. +dotNS carries a small set of constraints worth knowing before deploying or building against it. Most stem from the current pallet-revive runtime rather than from protocol design, and collapse to a no-op once the runtime gains the corresponding capability. Each is described in full where the relevant contract is documented in the [README](./README.md#contracts); this file is the consolidated index. For the security and audit status of the codebase, see [SECURITY.md](./SECURITY.md). @@ -13,9 +13,9 @@ For the security and audit status of the codebase, see [SECURITY.md](./SECURITY. **Type:** runtime limitation. -Substrate Root cannot deploy a contract on behalf of an account it does not control, so a per-user `LabelStore` cannot be created at the moment a Pop-gateway issuance writes the name. The controller stamps a pending-claim entry instead, and the user settles it later by calling `claimLabelStore` once from their own address. Pending-claim entries carry a bounded TTL and `expirePendingClaim` is permissionless, so the slot frees itself if a user never claims. +Substrate Root cannot deploy a contract on behalf of an account it does not control, so a per-user `LabelStore` cannot be created at the moment a gateway-path issuance writes the name. The controller stamps a pending-claim entry instead, and the user settles it later by calling `claimLabelStore` from their own address. Settlement is permissionless: `settlePendingClaims` lets anyone settle a given owner's entries and pay the cost, and settlement always writes the label, so a pending name is never stranded. -**Workaround:** `claimLabelStore` (user-signed) settles the whole pending pile and deploys the store. +**Workaround:** `claimLabelStore` (user-signed) settles a bounded batch of the caller's pending claims, deploying the store on the first write; the caller calls it again while it reports `moreRemaining`. **Resolution:** when the runtime supports root-origin contract deployment, the deferred path collapses to a no-op and issuance becomes one transaction end-to-end. @@ -25,7 +25,7 @@ See [README → DotnsPopController](./README.md#early-testnet-quirk-labelstore-d **Type:** current implementation. -The Pop gateway does not write a dedicated user-status mapping. It materialises the PoP flow through gateway-issued labels, PoP resolver records, and reservation queue state; user tier checks for public pricing read status from the personhood precompile and context, not from stored state. +The gateway path does not write a dedicated user-status mapping. It materialises that path through gateway-issued labels, PoP resolver records, and reservation queue state; user tier checks for public pricing read status from the personhood precompile and context, not from stored state. **Resolution:** a dedicated status mapping could be added if a use case requires it; the current design is deliberate, not a defect. diff --git a/README.md b/README.md index fda3199c9..1b30b0452 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ > If you experience problems with any product or service that was built on or deployed from this code, you should contact the third party who deployed the code in its amended form, not Parity. -# Dotns +# dotNS Smart contracts for registering .dot names on Polkadot. -DotNS is a naming system for Polkadot. An account can register a .dot name, receive an ERC721 token that represents ownership of that name, attach records to it (addresses, text, content hashes, chat keys), and create subnames beneath it. Two independent issuance paths coexist on the same underlying registrar: a public commit-reveal path for anyone who wants a name, and a Proof-of-Personhood gateway path that issues lite-person and full-person usernames on behalf of verified users. Every piece of state the protocol surfaces is readable through public view functions on the chain itself, so a client needs only a node and a small set of well-known contract addresses to answer any question about the system. +dotNS is a naming system for Polkadot. An account can register a .dot name, receive an ERC721 token that represents ownership of that name, attach records to it (addresses, text, content hashes, chat keys), and create subnames beneath it. Two independent issuance paths coexist on the same underlying registrar: a public commit-reveal path for anyone who wants a name, and a gateway path, driven by the [dotNS gateway pallet](#dotnspopcontroller) in the Asset Hub runtime, that issues device names and personhood names on behalf of verified users. Every piece of state the protocol surfaces is readable through public view functions on the chain itself, so a client needs only a node and a small set of well-known contract addresses to answer any question about the system. ## Diagrams @@ -23,7 +23,7 @@ DotNS is a naming system for Polkadot. An account can register a .dot name, rece | Actor | What they may do | | --- | --- | | **Anyone with an account** | Register an available name on the public paid path, within the bands in [Who can register a name](#who-can-register-a-name). Own it, transfer it, attach records, issue subnames beneath it. | -| **A personhood-verified person** | Receive a lite-person or full-person username issued through the gateway, at no cost. See [DotnsPopController](#dotnspopcontroller). | +| **An account with devicehood or personhood** | Receive a device name or a personhood name issued through the dotNS gateway pallet, at no cost. See [DotnsPopController](#dotnspopcontroller). | | **Root (governance)** | Gates every name that skips pricing or personhood: issues per-name grants, mints a reserved name directly, drives the gateway, and opens the short-name band. It cannot seize, reassign, or destroy a name anyone already holds — see [What governance controls](#what-governance-controls). | | **The contract owner** | Deploys, upgrades, and wires the protocol registry. Not an allocator by design, but `DotnsProtocolRegistry.set` and `DotnsRegistrar.addController` both reach the same outcome, so treat the owner as trusted infrastructure rather than a constrained role. | | **Anyone, as upkeep** | Permissionless maintenance: expire a stale reservation, settle another user's deferred label write, submit a granted registration on the beneficiary's behalf. None of these confer any claim on a name. | @@ -51,7 +51,7 @@ Every name admitted to public sale costs the same refundable deposit: 10 DOT at ### Base length and the digit rule -Pricing and eligibility read a name's base length. An ordinary name is measured as written, digits included, so `andrew` is six characters and `andrew01` is eight. A lite-person username issued by the PoP gateway carries a separator and two allocated digits (`andrew.01`), and those come off first, so it measures its six-character stem. Base length decides which band a name falls in and who may register it; under the flat model it does not change the amount. +Pricing and eligibility read a name's base length: the length of its base name, which is the name with any device suffix removed. An ordinary name is its own base name, measured as written, digits included, so `andrew` is six characters and `andrew01` is eight. A device name issued by the gateway pallet carries a separator and two allocated digits (`andrew.01`); its base name is the stem before them, so it measures six. Base length decides which band a name falls in and who may register it; under the flat model it does not change the amount. ### What a name costs @@ -73,9 +73,9 @@ Three bands share the one deposit. | 6 to 8 | a verified person, and only while the short-name switch is on | 10 DOT | | 5 or fewer | nobody on the public path | not sold; issued at zero base cost through the reserved path | -Personhood unlocks only the six-to-eight band. A no-digit name there needs full-person verification; a two-digit name needs lite-person verification. It gates who may buy, not the price: the deposit is the same one everyone pays. +On the public paid path, the six-to-eight band opens only to proof of personhood. Devicehood reaches short names through the gateway pallet instead: it issues a device name such as `andrew.01` and can reserve a personhood name, such as `andrew`, to claim once the user proves personhood. Status decides who may register; the deposit is the same one everyone pays. -Names shorter than nine characters are closed on the public paid path by default. A paid registration below nine reverts until governance opens the short-name market with a single switch. The switch gates the public paid path alone: the personhood gateway issues names of any length without it, and the reserved path does not consult it. It defaults off, so at launch only names of nine characters or more are for sale. +Names shorter than nine characters are closed on the public paid path by default. A paid registration below nine reverts until governance opens the short-name market with a single switch. The switch is designed for testnets, where turning it on makes short names easier to acquire: a verified person buys a six-to-eight-character name directly on the public path. Polkadot mainnet is meant to keep it off, so the public path there sells only names of nine characters or more. The switch gates the public paid path alone: the gateway pallet issues names of any length without it, and the reserved path does not consult it. Names of five characters or fewer are never sold on the public path, which rejects a reserved-tier label outright. Such a name enters circulation only through the reserved path, which mints an available label at zero base cost with no deposit and no personhood check. That path is gated on a grant: the label must be bound to the intended owner on `DotnsNameWhitelist`, or the dispatch must be substrate Root. There is no treasury: no value moves when a reserved name is issued. @@ -88,9 +88,9 @@ The path decides whether the amount is a refundable deposit, a non-refundable fe | `gavinwood` | 9 | public, own key | 10 DOT | refundable deposit | | `gavinwood` | 9 | public, someone else pays | 10 DOT | protocol fee | | `andrewsays` | 10 | public, own key | 10 DOT | refundable deposit | -| `andrew` | 6 | public, own key (switch on, full person) | 10 DOT | refundable deposit | -| `alicebob42` | 8 | public, own key (switch on, lite person) | 10 DOT | refundable deposit | -| any six-to-eight name | 6 to 8 | personhood gateway grant | none | no deposit, no fee | +| `andrew` | 6 | public, own key (switch on, proof of personhood) | 10 DOT | refundable deposit | +| `alicebob42` | 10 | public, own key | 10 DOT | refundable deposit | +| any six-to-eight name | 6 to 8 | gateway path | none | no deposit, no fee | | `andrew`, moved to a wallet that cannot clear its band | 6 | transfer | 10 DOT | protocol fee | ### Deposits and protocol fees @@ -103,7 +103,7 @@ A name someone else pays for, and a transfer, pay a non-refundable fee instead. Publicly registered names transfer freely and charge the name's own price, but only in two cases: the recipient cannot clear the name's band, or the move is a personhood downgrade, where the recipient's tier is lower than the sender's. Passing a six-character name to a wallet that could never have registered it costs the name's own price, so there is no cheap way to hand a band-gated name to a party who could not have earned it. A move between two wallets that both clear the band, and a move to the same address, cost nothing. The fee, when one is owed, settles into the protocol fee pot. The deposit, when present, rides with the name: the escrow position rebinds to the new holder rather than refunding, and only releasing the name back to escrow unlocks the locked deposit. -Names minted through the PoP gateway are soulbound: they stay bound to the person who earned them and cannot be transferred at all. Any transfer of a gateway-issued name reverts, and quoting a transfer fee for one reverts rather than returning a price. Everything else about the name works normally, so its owner still sets records, issues subnames, and manages the name. +Names minted through the gateway pallet are soulbound: they stay bound to the person who earned them and cannot be transferred at all. Any transfer of a gateway-issued name reverts, and quoting a transfer fee for one reverts rather than returning a price. Everything else about the name works normally, so its owner still sets records, issues subnames, and manages the name. A bare transfer does not reset the name's records. Registration and reclaim from escrow both point the registry record back at the default reverse resolver, but a direct ERC-721 `transferFrom` never calls the registry, so a name sold privately arrives still pointing at the seller's resolver. A buyer should overwrite the records they care about rather than assume they start clean. @@ -131,11 +131,11 @@ Gateway-issued (soulbound) names never enter this lifecycle. Releasing a name tr ### Cost-model versioning -D and F are fixed for the life of a pricing model. Changing either means deploying a fresh model with the new values and registering it, at which point it becomes the current version; there is no live setter that edits the numbers in place. Governance can also point the current version back at an earlier registered model to roll a change back. An in-flight registration prices at the version it committed to, so a model change between commit and reveal leaves its cost unchanged. +A pricing model's values are fixed for its life: the flat model's deposit, or the scarcity candidate's base fee D and floor F. Changing them means deploying a fresh model with the new values and registering it, at which point it becomes the current version; there is no live setter that edits the numbers in place. Governance can also point the current version back at an earlier registered model to roll a change back. An in-flight registration prices at the version it committed to, so a model change between commit and reveal leaves its cost unchanged. ### What governance controls -Governance sets D and F to whatever values it chooses. The contracts hold them coherent and nothing more: D above zero, F above zero and no greater than D, and D within a ceiling that keeps the six-character multiplication from overflowing. There is no cap on how high or low D goes and no limit on how fast it moves, so any rate limit or advance notice comes from the governance process rather than from these contracts. Governance also opens or closes the short-name market with the switch, tunes the release cooldown within its one-hour bound, sets the redeem window between 1 and 30 days, and sets the gateway's reservation duration. None of these controls lets governance seize, reassign, or destroy a name anyone already holds. +Governance chooses a model's values when it deploys one, and the contracts hold them coherent and nothing more: a flat deposit above zero; for the scarcity candidate, D and F above zero, F no greater than D, and D within a ceiling that keeps the multiplication below nine characters from overflowing. Beyond these checks there is no cap on how high or low the amounts go and no limit on how fast they move, so any rate limit or advance notice comes from the governance process rather than from these contracts. Governance also opens or closes the short-name market with the switch, tunes the release cooldown within its one-hour bound, sets the redeem window between 1 and 30 days, and sets the gateway's reservation duration. None of these controls lets governance seize, reassign, or destroy a name anyone already holds. ### Refund ledgers @@ -147,47 +147,62 @@ Clients that need the exact moment a released name becomes registrable should re Two controllers sit on top of a single registrar and a single protocol registry. The registrar holds the ERC721 token per name; the registry holds the forward node => (owner, resolver) mapping and subname hierarchy; the resolvers hold per-name records; the protocol registry is the indirection layer through which every contract resolves its siblings at runtime, an address book read on each call rather than a set of sibling addresses fixed in every caller. Controllers are the entry points: they mint names and drive the side effects. Neither controller imports the other. The layers underneath arbitrate collision handling: ERC721 uniqueness on the registrar, and a single reservation table on PopRules that both flows read through. +### Identifiers + +Past the entry points, contracts identify a name by hashes of its text. Four identifiers recur, and the second and third are easy to confuse. + +- **Label**: the text of a name without the TLD, as submitted: `victoria`, or `michael.01` for a device name. +- **Labelhash**: `keccak256(label)`, a hash of the text alone with no position in the name tree. The controllers' events, the reservation queue and `isPopIssued` identify names by label or labelhash. +- **Node**: the namehash, `keccak256(parentNode ++ labelhash)`, applied one segment at a time from the TLD's node (`tldNode` on the protocol registry) down. It fixes where a name sits in the tree and keys the registry and every resolver record. The node of `victoria` is `namehash(tldNode, keccak256("victoria"))`. +- **TokenId**: `uint256(node)`, for a second-level name tokenised on the registrar. A subname, device names included, has no token and lives in the registry alone. + +A device name is where the labelhash and the node diverge. `michael.01` is registered as the subname `michael` beneath the numeric container `01`, so its node is `namehash(namehash(tldNode, keccak256("01")), keccak256("michael"))`. Its labelhash, as the PoP controller and the PoP resolver use it, is `keccak256("michael.01")` over the whole text. That value is no step of the device name's node, and neither can be computed from the other without the label. + +Hashing is one-way: the text of a name cannot be recovered from its node or its labelhash, which is why `LabelStore` keeps it (see [StoreFactory, LabelStore, and UserStore](#storefactory-labelstore-and-userstore)). Base name and base length are defined under [Base length and the digit rule](#base-length-and-the-digit-rule); a device name's stem is the letters before its `.NN` suffix. + ### DotnsRegistrarController -Commit-reveal controller for the public registration path. A caller first submits a commitment hash, waits out the minimum commitment age, then reveals the registration parameters alongside the payment. The controller validates the commitment, routes price and eligibility through PopRules, and orchestrates every side effect of a successful registration: the mint on the registrar, the forward wire-up on the registry, the reverse record on the reverse resolver, the immutable Store write, and any refund owed on overpayment. Acceptable input is a single DNS label of at least the minimum-length policy; shorter labels revert with `LabelTooShort`. Labels classified as governance-reserved revert with `GovernanceReserved`; a base stem held by another user reverts with `NameReserved`. On the cross-payer path the owner's recorded PoP tier must meet the label's required tier, so verified-payer-for-unverified-owner sponsorship is rejected with `OwnerStatusInsufficient` and the direct-path personhood guarantee carries over to sponsored registrations. +Commit-reveal controller for the public registration path. A caller first submits a commitment hash, waits out the minimum commitment age, then reveals the registration parameters alongside the payment. The controller validates the commitment, routes price and eligibility through PopRules, and orchestrates every side effect of a successful registration: the mint on the registrar, the forward wire-up on the registry, the reverse record on the reverse resolver, the immutable Store write, and any refund owed on overpayment. Acceptable input is a single DNS label of at least the minimum-length policy; shorter labels revert with `LabelTooShort`. On the cross-payer path, a governance-reserved label reverts with `GovernanceReserved` and a label whose base name another user holds with `NameReserved`; the direct path rejects both through PopRules' price check with a `PopError`. On the cross-payer path the owner's recorded PoP tier must meet the label's required tier, so verified-payer-for-unverified-owner sponsorship is rejected with `OwnerStatusInsufficient` and the direct-path personhood guarantee carries over to sponsored registrations. ### DotnsPopController -Dedicated controller for the Proof-of-Personhood gateway flow. Lives behind its own UUPS proxy with its own storage and is registered on the registrar via addController alongside the commit-reveal controller. Its gated entry points are callable only under a substrate Root origin, which the controller verifies itself by reading `originIsRoot` from the revive System precompile. +Dedicated controller for the gateway path. Its caller is the dotNS gateway pallet (`DotnsGateway`), a pallet in the Asset Hub runtime maintained in the [individuality-community](https://github.com/paritytech/individuality-community/tree/main/pallets/dotns-gateway) repository. The pallet verifies a user's devicehood (an attester reservation) or personhood (a ring-membership proof), then calls this controller through a dispatcher contract. The controller lives behind its own UUPS proxy with its own storage and is registered on the registrar via addController alongside the commit-reveal controller. Its gated entry points are callable only under a substrate Root origin, which the controller verifies itself by reading `originIsRoot` from the revive System precompile. + +Today the gateway path does not write a standalone user-status mapping. It materialises that path through gateway-issued labels, PoP resolver records, and reservation queue state; user tier checks for public pricing still come from the personhood precompile/context read. -Today the Pop gateway does not write a standalone user-status mapping. It materialises the PoP flow through gateway-issued labels, PoP resolver records, and reservation queue state; user tier checks for public pricing still come from the personhood precompile/context read. +The first, issueDeviceName, issues a device name to a user. The gateway-facing input is a stem.suffix shape: a stem of lowercase ASCII letters, exactly one dot, then exactly two digits (for example michael.03). The stem is stricter than a DNS label, because it is the name a person chose and People Chain restricts that to letters: no digits, no hyphens, no uppercase. The same rule applies to a personhood name, which is the other name a person chooses, so `alice-bob` and `michae3l` are ordinary public names but cannot be issued as identities. How short a stem may be is not part of the shape; that is the governance-reserved band, applied by classification. The label is stored, minted and shown in that form, which is the form People Chain holds and the gateway pallet sends, so nothing is normalised at this boundary. Inputs with more than one dot, no dot, a non-digit suffix, or a suffix length other than two digits are rejected. The name is registered as the stem beneath its numeric container (`michael` under `03`), so it resolves like any other subname. The stem is not limited to the 6 to 8 of the Devicehood tier: a longer one is accepted, and a device name whose stem is nine letters or more classifies as NoStatus for public pricing, so devicehood is an issuance property here rather than an economic tier. A stem of five letters or fewer classifies as Reserved and is rejected on this path, which is the same governance gate that applies to short flat names. The call also persists the user's chat key on the PoP resolver. issueDeviceNameWithReservation does the same and also enqueues a reservation for a personhood name the user intends to claim later; reservePersonhoodName enqueues only the reservation. -The first, reserveBaseName, mints a lite-person username to a user. The gateway-facing input is a stem.suffix shape: a stem of lowercase ASCII letters, exactly one dot, then exactly two digits (for example michal.03). The stem is stricter than a DNS label, because it is the name a person chose and People Chain restricts that to letters: no digits, no hyphens, no uppercase. The same rule applies to a full-person name, which is the other name a person chooses, so `alice-bob` and `micha3l` are ordinary public names but cannot be issued as identities. How short a stem may be is not part of the shape; that is the governance-reserved band, applied by classification. The label is stored, minted and shown in that form, which is the form People Chain holds and the gateway pallet sends, so nothing is normalised at this boundary. Inputs with more than one dot, no dot, a non-digit suffix, or a suffix length other than two digits are rejected. The node is the hash of the whole string, so it can never collide with a subname built from the same characters. The stem is not limited to the 6 to 8 of the PopLite tier: a longer one is accepted, and a lite username whose stem is nine letters or more classifies as NoStatus for public pricing, so its lite status is an issuance property rather than an economic tier. A stem of five letters or fewer classifies as Reserved and is rejected on this path, which is the same governance gate that applies to short flat names. The call also persists the user's chat key on the PoP resolver and optionally enqueues a reservation for a full-person base name the user intends to claim later. +The second, issuePersonhoodName, issues a personhood name. The label is lowercase ASCII letters only, the same rule the device-name stem follows, so a hyphen or an interior digit is rejected even though both are valid in a public name. Whether the call is a claim against a prior reservation or a fresh standalone issuance is derived from on-chain reservation state; the caller does not choose. The link argument selects the chat-key source: inherit from a prior device name, or accept a fresh one in the payload. When inheriting, the call also writes the device link (personhood => device) and personhood link (device => personhood) records on the PoP resolver in the same transaction so downstream consumers can resolve either direction without scanning events. -The second, registerBaseName, mints a full-person username. The label is lowercase ASCII letters only, the same rule the lite stem follows, so a hyphen or an interior digit is rejected even though both are valid in a public name. Whether the call is a claim against a prior lite reservation or a fresh standalone registration is derived from on-chain reservation state; the caller does not choose. The link argument selects the chat-key source: inherit from a prior lite label, or accept a fresh one in the payload. When inheriting, the call also writes the liteLink (full => lite) and fullClaim (lite => full) records on the PoP resolver in the same transaction so downstream consumers can resolve either direction without scanning events. +Each personhood name carries a head/tail-indexed reservation queue with a capacity of MAX_RESERVATION_QUEUE and a governance-configurable reservationDuration. The queue head is mirrored into PopRules on every head transition (enqueue-from-empty, expiry-driven promotion, non-expiry head removal, claim-wipes-queue), so the public commit-reveal flow sees the same cross-flow lock through its existing PopRules price check. The gateway path is symmetric: issuePersonhoodName consults the live PopRules slot before mint and rejects with `NotHolder` when another user holds the base name, so PopRules is the single cross-flow authority in both directions. issuePersonhoodName additionally rejects device-name-classified and governance-reserved labels with `InvalidPersonhoodLabel`. Every exit from a queue emits an event (`ReservationClaimed`, `ReservationEvicted`, `ReservationRelinquished`, `ReservationExpired`), so an indexer can rebuild the queues from events alone. Expiry advancement is permissionless: anyone can call expireReservation to garbage-collect a stale head, which is what the pallet does on its own cadence. -Each base label carries a head/tail-indexed reservation queue with a capacity of MAX_RESERVATION_QUEUE and a governance-configurable reservationDuration. The queue head is mirrored into PopRules on every head transition (enqueue-from-empty, expiry-driven promotion, non-expiry head removal, claim-wipes-queue), so the public commit-reveal flow sees the same cross-flow lock through its existing PopRules price check. The gateway path is symmetric: registerBaseName consults the live PopRules slot before mint and rejects with `NotHolder` when another user holds the stem, so PopRules is the single cross-flow authority in both directions. registerBaseName additionally rejects lite-classified labels (those belong on reserveBaseName) and governance-reserved labels with `InvalidBaseLabel`. Expiry advancement is permissionless: anyone can call expireReservation to garbage-collect a stale head, which is what the pallet does on its own cadence. +The gateway pallet binds three deprecated entrypoints, reserveLiteName, reserveBaseName and registerBaseName, through `IDotnsPopControllerLegacy`. Each forwards to issueDeviceName, issueDeviceNameWithReservation and issuePersonhoodName respectively with the same selector it always had, and its label errors are `InvalidLiteLabel` and `InvalidBaseLabel`, the ones the pallet decodes. New integrations use the current entrypoints. #### Early testnet quirk: LabelStore deployment -Pop-gateway issuances mint the name and persist its label, but LabelStore deployment is deferred for users who have not yet interacted with the protocol from their own address. The current pallet-revive runtime does not let substrate Root deploy contracts on behalf of an account it does not control, so the per-user LabelStore cannot be created at the moment the gateway writes. The controller stamps a pending-claim entry instead, and settlement writes the label into the owner's store, deploying the store on the first write. Settlement is permissionless via settlePendingClaims: the owner settles their own store, or after the claim window anyone settles a given owner's entry and pays the cost. Settlement always writes the label rather than dropping the entry, so a pending name is never stranded. When the runtime supports root-origin contract deployment, the deferred path collapses to a no-op and the issuance flow becomes one transaction end-to-end. This is a runtime limitation, not a protocol design choice. +Pop-gateway issuances mint the name and persist its label, but LabelStore deployment is deferred for users who have not yet interacted with the protocol from their own address. The current pallet-revive runtime does not let substrate Root deploy contracts on behalf of an account it does not control, so the per-user LabelStore cannot be created at the moment the gateway writes. The controller stamps a pending-claim entry instead, and settlement writes the label into the owner's store, deploying the store on the first write. Settlement is permissionless via settlePendingClaims: the owner settles their own store, or anyone settles a given owner's entries at any time and pays the cost. Settlement always writes the label rather than dropping the entry, so a pending name is never stranded. When the runtime supports root-origin contract deployment, the deferred path collapses to a no-op and the issuance flow becomes one transaction end-to-end. This is a runtime limitation, not a protocol design choice. Deferred settlement has no transfer-pricing consequence, because gateway-issued names are soulbound and cannot be transferred at all. The transfer-floor price is derived by reading the label from the sender's LabelStore, so a name held before its label is settled would have no readable label to price against; making gateway names non-transferable removes that path entirely rather than relying on settlement to close it. ### DotnsRegistrar -ERC721-backed registrar that mints ownership of label IDs (labelhashes). Minting is restricted to every address in the controllers mapping; the mapping is owner-gated through addController and removeController. Every other contract in the system that needs to check "is this address authorised to drive name state?" consults this mapping rather than keeping a parallel list, which is what lets multiple controllers coexist on the same registrar without per-contract configuration changes. +ERC721-backed registrar that tokenises second-level names: a name's tokenId is `uint256(node)` (see [Identifiers](#identifiers)). Minting is restricted to every address in the controllers mapping; the mapping is owner-gated through addController and removeController. Every other contract in the system that needs to check "is this address authorised to drive name state?" consults this mapping rather than keeping a parallel list, which is what lets multiple controllers coexist on the same registrar without per-contract configuration changes. -The registrar owns transferability. A name is marked soulbound at mint when the caller is the address registered under POP_CONTROLLER, so only the PoP gateway can issue a soulbound name and no other controller can lock a public one. The marker is set once and never cleared, and isSoulbound reports it. The transfer hook rejects every transfer of a soulbound name, including a move to the holder's own address and a release into escrow, so a gateway-issued name can never leave its holder's wallet. Only the mint is exempt, because the marker is set just after it. +The registrar owns transferability. A name is marked soulbound at mint when the caller is the address registered under POP_CONTROLLER, so only the gateway path can issue a soulbound name and no other controller can lock a public one. The marker is set once and never cleared, and isSoulbound reports it. The transfer hook rejects every transfer of a soulbound name, including a move to the holder's own address and a release into escrow, so a gateway-issued name can never leave its holder's wallet. Only the mint is exempt, because the marker is set just after it. ### DotnsRegistry -Forward registry mapping node to (owner, resolver) and supporting subnode creation. When a base name is minted on the registrar, the matching controller wires the node to the new owner through this registry. Privileged node wiring defers to the same controllers mapping on the registrar, so both controllers can write without the registry tracking controllers of its own. +Forward registry mapping node to (owner, resolver) and supporting subnode creation. When a second-level name is minted on the registrar, the matching controller wires the node to the new owner through this registry. Privileged node wiring defers to the same controllers mapping on the registrar, so both controllers can write without the registry tracking controllers of its own. -Subnames are created by the base-name owner. A subname carries its own (owner, resolver) and can in turn carry subnames, so the registry is the place the name hierarchy actually lives. Nesting is bounded by the whole path rather than by a depth counter: the dotted parent path a caller submits is capped at 255 octets. That is a protocol limit rather than the RFC 1035 figure it resembles, since the RFC bound counts a wire-format name including its length bytes and its TLD. Each level of a subname is stored as its full dotted name in the owner's LabelStore, and that row cannot be deleted, so an uncapped path would let a parent owner write an arbitrarily long permanent row into an address they chose. +Subnames are created by the parent name's owner. A subname carries its own (owner, resolver) and can in turn carry subnames, so the registry is the place the name hierarchy actually lives. Nesting is bounded by the whole path rather than by a depth counter: the dotted parent path a caller submits is capped at 255 octets. That is a protocol limit rather than the RFC 1035 figure it resembles, since the RFC bound counts a wire-format name including its length bytes and its TLD. Each level of a subname is stored as its full dotted name in the owner's LabelStore, and that row cannot be deleted, so an uncapped path would let a parent owner write an arbitrarily long permanent row into an address they chose. The registry exposes isAuthorised(node, account) as the canonical check for whether an address may manage a node: the stored owner for a subname, or the ERC-721 holder, a single-token approvee, or an operator-for-all on the registrar for a tokenised name. Sibling contracts consult this view so a single registrar-level approval delegates management across the protocol rather than each contract maintaining its own approval list. ### PopRules -PoP-aware name classification and pricing. Classification reads the label's **base length** and whether the label carries the gateway's separator, then maps to one of four tiers: NoStatus (base length 9 or more, open to anyone at the cost-model price), PopFull (base length 6-8, requires full-person verification), PopLite (a separated `stem.NN` label whose stem is 6-8 characters, gateway-issued to lite-verified users), and Reserved (base length 5 or fewer, governed by the protocol). +PoP-aware name classification and pricing. Classification reads the label's **base length** and whether the label carries the gateway's separator, then maps to one of four tiers: NoStatus (base length 9 or more, open to anyone at the cost-model price), Personhood (base length 6-8, requires proof of personhood), Devicehood (a separated `stem.NN` device name whose stem is 6-8 characters, issued by the gateway to accounts with devicehood), and Reserved (base length 5 or fewer, governed by the protocol). -A lite-person label is a stem of lowercase letters, one separator, then exactly two digits (`joseph.42`), mirroring what People Chain issues; the stem's length is bounded by the bands below rather than by the shape. Base length is the label measured as written, except for a lite label, whose separator and two allocated digits are removed first. The gateway allocates those digits to distinguish people who chose the same stem, so removing them recovers what the candidate picked; no such allocation stands behind the digits in an ordinary name, so `web3` is a four-character word rather than `web` with a counter. Digits carry no protocol meaning outside the separated form: any count is accepted on an ordinary label, and none makes it PopLite. The classification determines the price and the eligibility gate the commit-reveal controller enforces. +A device name is a stem of lowercase letters, one separator, then exactly two digits (`joseph.42`), mirroring what People Chain issues; the stem's length is bounded by the bands below rather than by the shape. Base length is the label measured as written, except for a device name, whose separator and two allocated digits are removed first. The gateway allocates those digits to distinguish people who chose the same stem, so removing them recovers what the candidate picked; no such allocation stands behind the digits in an ordinary name, so `web3` is a four-character word rather than `web` with a counter. Digits carry no protocol meaning outside the separated form: any count is accepted on an ordinary label, and none makes it Devicehood. A label's base name is the label with any device suffix removed: a device name's stem, or any other label as written. PopRules keys reservations by it. The classification determines the price and the eligibility gate the commit-reveal controller enforces. #### Classification examples and failure modes @@ -196,29 +211,29 @@ An ordinary label is measured as written; only a separated `stem.NN` label is sh | Label | Base length | Classification | Eligible public path | Price | Notes | | --- | ---: | --- | --- | ---: | --- | | alice | 5 | Reserved | Grant or Root only | Not sold; issued at 0 | Five characters or fewer is governance-reserved. | -| andrew.01 | 6 | PopLite | Pop gateway only | Gateway grant is free | The separated form. Its stem is six characters, and the separator with its two digits is the suffix. | -| alicebob.42 | 8 | PopLite | Pop gateway only | Gateway grant is free | Eight-character stem; the whole label is eleven characters. | -| andrewsays.01 | 10 | NoStatus | Pop gateway only | Gateway grant is free | A lite name with a long stem classifies NoStatus, so its lite status is an issuance property rather than an economic tier. | -| andrew | 6 | PopFull | PopFull user | 10 DOT | Canonical full-person base name; priced only while the short-name switch is on. | -| andrew01 | 8 | PopFull | PopFull user | 10 DOT | Measured whole. The digits are part of the name and do not make it lite. | -| andrew1 | 7 | PopFull | PopFull user | 10 DOT | Any digit count is accepted on an ordinary label. | +| andrew.01 | 6 | Devicehood | Gateway path only | Gateway grant is free | The separated form. Its stem is six characters, and the separator with its two digits is the suffix. | +| alicebob.42 | 8 | Devicehood | Gateway path only | Gateway grant is free | Eight-character stem; the whole label is eleven characters. | +| andrewsays.01 | 10 | NoStatus | Gateway path only | Gateway grant is free | A device name with a long stem classifies NoStatus, so devicehood is an issuance property here rather than an economic tier. | +| andrew | 6 | Personhood | Personhood account | 10 DOT | Canonical personhood-name shape; priced only while the short-name switch is on. | +| andrew01 | 8 | Personhood | Personhood account | 10 DOT | Measured whole. The digits are part of the name and do not make it a device name. | +| andrew1 | 7 | Personhood | Personhood account | 10 DOT | Any digit count is accepted on an ordinary label. | | web3 | 4 | Reserved | Grant or Root only | Not sold; issued at 0 | A four-character word, not `web` with a counter. | | andrewsays | 10 | NoStatus | Anyone | 10 DOT | The amount is the flat refundable deposit. | | andrewsays01 | 12 | NoStatus | Anyone | 10 DOT | Measured whole and still NoStatus, at the same flat deposit. | | Andrew01 | n/a | Rejected by canonical label validator | None | n/a | Labels must be lowercase ASCII DNS labels. | -| andrew.4 | n/a | Rejected | None | n/a | A separator is legal only on a lite label, which carries exactly two digits. | -| andrew.123 | n/a | Rejected | None | n/a | Three digits is not the lite suffix. | -| alice.42 | 5 | Reserved | Pop gateway only | Not sold; not issued | A well-formed lite label whose five-letter stem is governance-reserved, so the gateway does not issue it. | -| andr3w.01 | n/a | Rejected | None | n/a | The stem is letters only, so a digit in it is not a lite label. | -| andrew-x.01 | n/a | Rejected | None | n/a | Hyphens are valid in a DNS label but not in a lite stem. | +| andrew.4 | n/a | Rejected | None | n/a | A separator is legal only on a device name, which carries exactly two digits. | +| andrew.123 | n/a | Rejected | None | n/a | Three digits is not the device-name suffix. | +| alice.42 | 5 | Reserved | Gateway path only | Not sold; not issued | A well-formed device name whose five-letter stem is governance-reserved, so the gateway path does not issue it. | +| andr3w.01 | n/a | Rejected | None | n/a | The stem is letters only, so a digit in it is not a device name. | +| andrew-x.01 | n/a | Rejected | None | n/a | Hyphens are valid in a DNS label but not in a device-name stem. | -A separated label reaches the chain only through the PoP gateway: the public path requires a single DNS label, which admits no separator. That is what reserves the dotted space to the gateway, and it is why a reader cannot take the separator alone as proof of personhood for an arbitrary string. The controller publishes `isPopIssued(label)` for that. +A separated label reaches the chain only through the gateway path: the public path requires a single DNS label, which admits no separator. That is what reserves the dotted space to the gateway path, and it is why a reader cannot take the separator alone as proof of personhood for an arbitrary string. The controller publishes `isPopIssued(label)` for that. -Tier assignment is read on every pricing call, not stored: PopRules queries the alias-accounts personhood precompile at DotnsConstants.PERSONHOOD with the dotns context (bytes32("dotns")), and translates the returned status byte into a PopStatus (0=NoStatus, 1=PopLite, 2=PopFull). Unknown tier bytes collapse to NoStatus, so a future precompile addition fails closed rather than silently being treated as a higher tier. There is no on-chain self-attestation; users obtain personhood off-chain through the People-chain ring proof and the alias-accounts pallet propagates the result via XCM. +Tier assignment is read on every pricing call, not stored: PopRules queries the alias-accounts personhood precompile at DotnsConstants.PERSONHOOD with the dotns context (bytes32("dotns")), and translates the returned status byte into a PopStatus (0=NoStatus, 1=Devicehood, 2=Personhood; the precompile itself names them None, Lite and Full). Unknown tier bytes collapse to NoStatus, so a future precompile addition fails closed rather than silently being treated as a higher tier. There is no on-chain self-attestation; users obtain personhood off-chain through the People-chain ring proof and the alias-accounts pallet propagates the result via XCM. -Classification is not the same thing as effective holder context. A long label such as andrewsays is always a NoStatus-tier label by shape, but it may be held by a PopFull, PopLite, or NoStatus account. Consumers that need to know what rules apply to that live name should combine three reads: classify the label, query the registrar owner, then query the owner's dotns-context PoP status through the precompile or gateway-written state. The escrow position is the economic qualifier: if the token has an active release position with a non-zero amount, it came through the refundable NoStatus deposit path; if no such deposit exists, a verified holder can own the same long label without it being deposit-backed. In other words, andrewsays does not become a PopFull-tier label when a PopFull user owns it, but the owner can still be PopFull for transfer pricing, reverse resolution, and UI display. +Classification is not the same thing as effective holder context. A long label such as andrewsays is always a NoStatus-tier label by shape, but it may be held by a Personhood, Devicehood, or NoStatus account. Consumers that need to know what rules apply to that live name should combine three reads: classify the label, query the registrar owner, then query the owner's dotns-context PoP status through the precompile (`PopRules.popStatusOf`). The escrow position is the economic qualifier: if the token has an active release position with a non-zero amount, it came through the refundable NoStatus deposit path; if no such deposit exists, a verified holder can own the same long label without it being deposit-backed. In other words, andrewsays does not become a Personhood-tier label when a Personhood account owns it, but the owner can still hold personhood for transfer pricing, reverse resolution, and UI display. -Name grants are the exception path for users or organisations that need to register without satisfying the live PoP tier check. DotNS still does not accept self-attestation: the contracts only consume PoP status from the personhood precompile, and a user cannot set or prove their own status inside DotNS. Instead, `DotnsNameWhitelist` binds a specific label to a specific beneficiary, and registerReserved mints it at zero base cost, bypassing the PoP pricing gate while still using the normal commit-reveal and availability checks. Only governance issues a grant: every admin action on the whitelist requires a substrate Root dispatch, so no key can grant a name, the contract owner included. A Root dispatch can also mint a reserved name directly, without a grant. That covers the whitelist path; the registrar's controller set and the protocol registry's keys are still owner-managed, so an owner retains other routes to the same outcome. +Name grants are the exception path for users or organisations that need to register without satisfying the live PoP tier check. dotNS still does not accept self-attestation: the contracts only consume PoP status from the personhood precompile, and a user cannot set or prove their own status inside dotNS. Instead, `DotnsNameWhitelist` binds a specific label to a specific beneficiary, and registerReserved mints it at zero base cost, bypassing the PoP pricing gate while still using the normal commit-reveal and availability checks. Only governance issues a grant: every admin action on the whitelist requires a substrate Root dispatch, so no key can grant a name, the contract owner included. A Root dispatch can also mint a reserved name directly, without a grant. That covers the whitelist path; the registrar's controller set and the protocol registry's keys are still owner-managed, so an owner retains other routes to the same outcome. A grant is deliberately narrow. It names one label rather than permitting any available name, it names the address the name must mint to, and it is spent by that mint, so it cannot seed a second registration. The gate reads the intended owner rather than the caller, so a relayer may submit the registration on the beneficiary's behalf; the name still lands on the beneficiary. The reserved path writes no reverse record, because the submitter is not necessarily the beneficiary and setReverseName overwrites unconditionally; the owner claims their own primary afterwards through claimReverseRecord. @@ -226,9 +241,9 @@ A grant is a governance action. `grantName` is Root-only, so on a production net A grant does not register a name or bypass ownership rules; it only permits the named address to register that one label without a PoP status. -PopRules also holds the cross-flow reservation table for base names, keyed by the bare stem. The PoP controller writes it on every reservation-queue head transition: it takes a bare stem directly and rejects the update when the slot is held by a different user, so the caller's local queue bookkeeping never silently diverges from the PopRules state. The commit-reveal controller has a second write path, gated on a PopLite price; since PopLite is the separated form and the public path cannot submit one, that path never fires. +PopRules also holds the cross-flow reservation table, keyed by base name. The PoP controller writes it on every reservation-queue head transition: it takes a base name directly and rejects the update when the slot is held by a different user, so the caller's local queue bookkeeping never silently diverges from the PopRules state. The commit-reveal controller writes no reservation: it reads the slot when it prices a name, and clears a reclaimed name's stale slot. `reserveBaseName` is the write path for any other authorised controller, bounded to base lengths 6 to 8. -Two read paths, priceWithCheck and priceWithoutCheck, are what the public flow consults. Both look the reservation up by stem, and only a separated label is shortened to reach one. So a live entry on `andrew` blocks `andrew` and `andrew.01`, while `andrew01` is a different name and is unaffected. Reservations run for 12 weeks by default. +Two read paths, priceWithCheck and priceWithoutCheck, are what the public flow consults. Both look the reservation up by base name, and only a separated label is shortened to reach one. So a live entry on `andrew` blocks `andrew` and `andrew.01`, while `andrew01` is a different name and is unaffected. Reservations run for 12 weeks by default. ### DotnsReverseResolver @@ -256,13 +271,13 @@ Stores forward-resolution address records per node. This is the conventional "na ### DotnsPopResolver -Per-node resolver for records produced by the Proof-of-Personhood flow. Three record kinds. The chat key is ECDH public-key bytes keyed by node; it is written by the PoP controller during a lite reservation and during any claim path that inherits from a prior lite entry, and is what gives verified users an on-chain discovery channel for end-to-end encrypted messaging. The lite link answers "which lite username did this full name claim from?" and is keyed by the full-person node. The full claim is the reverse direction: it answers "which full name did this lite user claim?" and is keyed by the lite labelhash. The forward and reverse links are written by the same call, so they stay in lockstep; downstream consumers that look up by lite username (Nova's pallet, for one) resolve the full name without scanning events. +Per-node resolver for records produced by the gateway path. Three record kinds. The chat key is ECDH public-key bytes keyed by node; it is written by the PoP controller when it issues a device name and during any personhood issuance, either inherited from a prior device name or supplied fresh in the payload, and is what gives verified users an on-chain discovery channel for end-to-end encrypted messaging. The two link directions take and return different identifiers. `deviceLabelhashOf(personhoodNode)` answers "which device name is this personhood name linked to?": it takes a personhood-name node and returns a device-name labelhash. `personhoodNodeOf(deviceLabelhash)` is the reverse direction, "which personhood name is this device name linked to?": it takes a device-name labelhash, the hash of the full `stem.NN` text (see [Identifiers](#identifiers)), and returns a personhood-name node. The forward and reverse links are written by the same call, so they stay in lockstep; downstream consumers that look up by device name (Nova's pallet, for one) resolve the personhood name without scanning events. Writer authorisation is dynamic: the PoP controller address is fetched from the protocol registry on every write. Rotating the PoP controller is a single set call on the protocol registry with no resolver upgrade required. ### DotnsProtocolRegistry -On-chain lookup table mapping well-known bytes32 keys (declared in DotnsConstants) to contract addresses. Every DotNS contract resolves its siblings through this registry at runtime. +On-chain lookup table mapping well-known bytes32 keys (declared in DotnsConstants) to contract addresses. Every dotNS contract resolves its siblings through this registry at runtime. Without it, each contract would store direct addresses to every contract it calls. An upgrade that changes one address would require a separate owner transaction for every contract that references it. The protocol registry reduces this to one: update the key in the registry, and every caller picks up the new address on its next call. The indirection also means a governance-driven rotation of, say, the PoP controller does not break any consumer that has already been deployed. @@ -278,7 +293,7 @@ For read batching, Multicall3 lets clients collect several results from the same Stores are the per-user storage layer. They exist because two query paths the rest of the system needs are not answerable from anywhere else: "what names has this address ever held?" cannot be served by resolvers (keyed per-node) or the registry (live ownership only, no history), and "what user-controlled records does this address publish?" has nowhere on a resolver to live since the data is not bound to any one name. Each address gets at most one of each store, forever, and the factory is the single source of truth for which store belongs to which user. -LabelStore is the protocol-managed half. The registrar and the controller set write a label entry once and the slot is permanently locked. The invariant is labels only: every per-name record category (reverse, content, forward address, chat key, lite link) goes to a dedicated resolver, and the Store stays the durable per-owner registration ledger. Because entries are append-only, transferring a name writes a fresh entry on the recipient and leaves the sender's locked entry in place, so LabelStore doubles as the address's lifetime-of-ownership ledger while the registry continues to answer for live ownership. +LabelStore is the protocol-managed half. The registrar and the controller set write a label entry once and the slot is permanently locked. The invariant is labels only: every per-name record category (reverse, content, forward address, chat key, device link) goes to a dedicated resolver, and the Store stays the durable per-owner registration ledger. Because entries are append-only, transferring a name writes a fresh entry on the recipient and leaves the sender's locked entry in place, so LabelStore doubles as the address's lifetime-of-ownership ledger while the registry continues to answer for live ownership. The reason a label needs storing at all is that the chain never sees it. A name is hashed into a node before it reaches any contract, and hashing is one-way: from `joseph.dot` you can always compute the node, but from the node you can never recover the text. Since pricing, classification, and the transfer floor all read the label's length and shape, the string has to be kept deliberately. `LabelStore` is that record, and the only route back from a node to the name it came from. diff --git a/RELEASE_ARTIFACTS.md b/RELEASE_ARTIFACTS.md index d46e3cf80..dd544d13a 100644 --- a/RELEASE_ARTIFACTS.md +++ b/RELEASE_ARTIFACTS.md @@ -1,4 +1,4 @@ -# DotNS Release Artifacts +# dotNS Release Artifacts What each release publishes, what the files guarantee, and how to consume them. @@ -11,7 +11,7 @@ What each release publishes, what the files guarantee, and how to consume them. | `release-manifest.json` | What this release contains, machine readable | | `codehashes.json` | Stripped-metadata hash of each contract's built runtime bytecode | | `abi-diff.json` | Selector-level ABI changes since the previous release, machine readable | -| `dotns-genesis-.json` | pallet-revive genesis with DotNS deployed, one per TLD (`testnet` for previewnet, `paseo` for Paseo Asset Hub Next V2) | +| `dotns-genesis-.json` | pallet-revive genesis with dotNS deployed, one per TLD (`testnet` for previewnet, `paseo` for Paseo Asset Hub Next V2) | | `dotns-genesis-addresses.json` | The addresses a chain booted from those genesis files carries, a copy of `deployments/expected.json` | | `dotns-abis-.zip` | The same files in one archive | @@ -101,7 +101,7 @@ Two ways to protect yourself. Resolve addresses through the protocol registry at ## Consuming it -Prefer resolving addresses at runtime. Every DotNS contract exposes `protocolRegistry`, and `DotnsProtocolRegistry.get(key)` resolves each well-known key in `DotnsConstants`, so one address from the artifact is enough to reach the rest and the chain remains the authority. Pin the whole set only when a runtime lookup is not possible. +Prefer resolving addresses at runtime. Every dotNS contract exposes `protocolRegistry`, and `DotnsProtocolRegistry.get(key)` resolves each well-known key in `DotnsConstants`, so one address from the artifact is enough to reach the rest and the chain remains the authority. Pin the whole set only when a runtime lookup is not possible. Note that a `deployments.json` entry states where a contract was deployed, not that it is currently the live one for a role. The registry is the only answer to that question. diff --git a/SECURITY.md b/SECURITY.md index b93c8ebbf..f53bcb370 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## Security status -This repository contains the DotNS smart-contract suite: reference and proof-of-concept code and +This repository contains the dotNS smart-contract suite: reference and proof-of-concept code and patterns for a Polkadot naming system. It is intended for reference and experimentation, not as a production-ready artefact. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 7281886a1..de8f20f86 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -6,7 +6,7 @@ summary; the licence text shipped with each dependency is authoritative. ## Code derived from ENS -Parts of the DotNS contract suite are adapted from the Ethereum Name Service (ENS) contracts +Parts of the dotNS contract suite are adapted from the Ethereum Name Service (ENS) contracts (registry / registrar / resolver / reverse-resolver structure, namehash and labelhash, and the commit-reveal registration flow). The ENS contracts are MIT-licensed and copyright True Names Limited. The MIT licence requires the original copyright and permission notice to be retained; it is diff --git a/contracts/deploy/Create3Factory.sol b/contracts/deploy/Create3Factory.sol index a47195dbe..3f22fa0c7 100644 --- a/contracts/deploy/Create3Factory.sol +++ b/contracts/deploy/Create3Factory.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.34; import {CREATE3} from "solady/utils/CREATE3.sol"; -/// @title Dotns CREATE3 Factory +/// @title dotNS CREATE3 Factory /// @notice Permissionless wrapper around Solady's audited CREATE3 library. /// @dev Anyone may call @custom:function deploy. CREATE3 addresses are a pure function of /// `(factory_address, salt)` — the caller never enters the derivation — so caller-gating @@ -14,7 +14,7 @@ import {CREATE3} from "solady/utils/CREATE3.sol"; /// /// An occupied salt is handled by the caller. `BaseDeployer._deployCreate3` adopts an occupied /// address only when the occupant's runtime code is the artefact that run would have deployed, -/// and `DOTNS_SALT_VERSION` moves the DotNS address set to fresh addresses when it is not. +/// and `DOTNS_SALT_VERSION` moves the dotNS address set to fresh addresses when it is not. /// @custom:security-contact admin@parity.io contract Create3Factory { /// @notice Emitted on every successful CREATE3 deployment through this factory. diff --git a/contracts/escrow/DotnsNameEscrow.sol b/contracts/escrow/DotnsNameEscrow.sol index 2ddb0c8d8..99507e3e0 100644 --- a/contracts/escrow/DotnsNameEscrow.sol +++ b/contracts/escrow/DotnsNameEscrow.sol @@ -17,7 +17,7 @@ import {IDotnsRegistrar} from "../registrars/IDotnsRegistrar.sol"; import {IDotnsProtocolRegistry} from "../registry/IDotnsProtocolRegistry.sol"; import {DotnsConstants} from "../utils/DotnsConstants.sol"; -/// @title Dotns Name Escrow +/// @title dotNS Name Escrow /// @notice Holds refundable deposits for registered names and manages the release/reclaim /// lifecycle. @custom:security-contact admin@parity.io contract DotnsNameEscrow is @@ -460,10 +460,9 @@ contract DotnsNameEscrow is // Nothing to settle: return before touching `claimed`. That flag is what `redeem` reads to // decide whether the holder has already been paid for the name, so setting it here would // make a zero-amount `withdraw`, which pays nothing and emits nothing, silently forfeit - // the holder's right to recover their own name for no consideration at all. Free PopFull - // and PopLite registrations seed exactly these positions, and `withdraw` is the step the - // old contract required before a name could be recycled, so that is a path holders will - // take. + // the holder's right to recover their own name for no consideration at all. Cross-paid + // registrations seed exactly these positions, as does any name whose curve price is zero, + // and `withdraw` is a path holders will take. if (owed == 0) return; // Effects: from here the deposit really is being handed over, so the flag is set. @@ -786,7 +785,7 @@ contract DotnsNameEscrow is } /// @notice Returns the release this network declares it runs, read live from the protocol - /// registry so every DotNS contract reports one synchronised value. + /// registry so every dotNS contract reports one synchronised value. /// @dev 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 diff --git a/contracts/escrow/IDotnsNameEscrow.sol b/contracts/escrow/IDotnsNameEscrow.sol index e9862c9c5..cd49a758d 100644 --- a/contracts/escrow/IDotnsNameEscrow.sol +++ b/contracts/escrow/IDotnsNameEscrow.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: © 2026 Parity Technologies pragma solidity ^0.8.34; -/// @title Dotns Name Escrow Interface +/// @title dotNS Name Escrow Interface /// @notice Escrows refundable deposits for registered names and manages the release lifecycle. /// @custom:security-contact admin@parity.io interface IDotnsNameEscrow { diff --git a/contracts/pop/DotnsCostModelRegistry.sol b/contracts/pop/DotnsCostModelRegistry.sol index 95d03ad04..668451f83 100644 --- a/contracts/pop/DotnsCostModelRegistry.sol +++ b/contracts/pop/DotnsCostModelRegistry.sol @@ -7,7 +7,7 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IDotnsCostModelRegistry} from "./IDotnsCostModelRegistry.sol"; import {IDotnsPricing} from "./IDotnsPricing.sol"; -/// @title DotNS Cost Model Registry +/// @title dotNS Cost Model Registry /// @notice Keeps every registered cost model addressable by version and tracks the current one. /// @dev Holds only pointers, so it stays a plain owner-gated contract. `PopRules` resolves it once /// through `DotnsConstants.COST_MODEL` and prices the current version for fresh reads and a diff --git a/contracts/pop/DotnsFlatPricing.sol b/contracts/pop/DotnsFlatPricing.sol index b2a51fe95..bb8c0007a 100644 --- a/contracts/pop/DotnsFlatPricing.sol +++ b/contracts/pop/DotnsFlatPricing.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.34; import {IDotnsPricing} from "./IDotnsPricing.sol"; -/// @title DotNS Flat Pricing +/// @title dotNS Flat Pricing /// @notice Prices every registration at a single deposit, whatever the base length. /// @dev The launch cost model: one constant amount for any name the bands admit, so a nine-plus /// character name costs the same flat deposit and shorter names stay gated by `PopRules`. The diff --git a/contracts/pop/DotnsScarcityPricing.sol b/contracts/pop/DotnsScarcityPricing.sol index f0d718697..7ed44cb06 100644 --- a/contracts/pop/DotnsScarcityPricing.sol +++ b/contracts/pop/DotnsScarcityPricing.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.34; import {IDotnsPricing} from "./IDotnsPricing.sol"; -/// @title DotNS Scarcity Pricing +/// @title dotNS Scarcity Pricing /// @notice Prices a registration on a geometric scarcity curve driven by base length. /// @dev The curve doubles the base fee for each character below nine and halves it for each /// character from nine upward, never below the floor. The base fee is the curve's value at @@ -29,8 +29,9 @@ contract DotnsScarcityPricing is IDotnsPricing { /// @dev Carries the curve invariants: the base fee and floor are both strictly positive, the /// floor does not exceed the base fee, and the base fee stays within /// `type(uint256).max / 512` so the multiplication below nine characters cannot overflow. - /// An all-digit label such as "42" strips to base length 0 and reaches the 2**9 - /// multiplier, which sets the /512 ceiling. Any breach triggers @custom:reverts PricingError. + /// A base length of 0 reaches the 2**9 multiplier, which sets the /512 ceiling: no label + /// measures zero, but the pricing function accepts any length. Any breach triggers + /// @custom:reverts PricingError. /// @param baseFeeValue Base fee D in wei. /// @param minPriceValue Price floor F in wei. constructor(uint256 baseFeeValue, uint256 minPriceValue) { diff --git a/contracts/pop/IDotnsCostModelRegistry.sol b/contracts/pop/IDotnsCostModelRegistry.sol index ef493af12..3986f2f2f 100644 --- a/contracts/pop/IDotnsCostModelRegistry.sol +++ b/contracts/pop/IDotnsCostModelRegistry.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.34; import {IDotnsPricing} from "./IDotnsPricing.sol"; -/// @title DotNS Cost Model Registry +/// @title dotNS Cost Model Registry /// @notice Holds every cost model the protocol has run and names the current one. /// @dev The address registered under `DotnsConstants.COST_MODEL` points here, set once and never /// repointed. Changing the live curve registers a new model, which adds its version and moves @@ -69,14 +69,16 @@ interface IDotnsCostModelRegistry { function current() external view returns (IDotnsPricing model); /// @notice Prices a base length at the current version. - /// @param baseLength Digit-stripped length of the label being priced. + /// @param baseLength Base length of the label being priced: its length as written, less a + /// device name's suffix. /// @return weiPrice Registration cost in wei at the current version. function priceForBaseLength(uint256 baseLength) external view returns (uint256 weiPrice); /// @notice Prices a base length at a specific version. /// @dev @custom:reverts UnknownVersion when no model is registered for `version`. /// @param version The version to price against. - /// @param baseLength Digit-stripped length of the label being priced. + /// @param baseLength Base length of the label being priced: its length as written, less a + /// device name's suffix. /// @return weiPrice Registration cost in wei at that version. function priceForBaseLengthAtVersion( uint256 version, diff --git a/contracts/pop/IDotnsPricing.sol b/contracts/pop/IDotnsPricing.sol index f2efd080b..8b950389d 100644 --- a/contracts/pop/IDotnsPricing.sol +++ b/contracts/pop/IDotnsPricing.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: © 2026 Parity Technologies pragma solidity ^0.8.34; -/// @title DotNS Pricing Cost Model +/// @title dotNS Pricing Cost Model /// @notice Prices a registration from the base length of its label alone. /// @dev The seam between name policy and the wei amount a registration costs. `PopRules` and the /// public commit-reveal controller keep the classification, reservation, and tier rules; the @@ -18,10 +18,11 @@ interface IDotnsPricing { error PricingError(string reason); /// @notice Returns the registration cost in wei for a label of the given base length. - /// @dev Pure amount lookup: the caller supplies the digit-stripped base length and the model + /// @dev Pure amount lookup: the caller supplies the base length and the model /// returns the curve value for it. Runs on the ERC721 transfer floor read, so it stays a /// view with no state writes. - /// @param baseLength Digit-stripped length of the label being priced. + /// @param baseLength Base length of the label being priced: its length as written, less a + /// device name's suffix. /// @return weiPrice Registration cost in wei for that base length. function priceForBaseLength(uint256 baseLength) external view returns (uint256 weiPrice); diff --git a/contracts/pop/IPopRules.sol b/contracts/pop/IPopRules.sol index 15ea8680a..b5a5dc160 100644 --- a/contracts/pop/IPopRules.sol +++ b/contracts/pop/IPopRules.sol @@ -2,16 +2,20 @@ // SPDX-FileCopyrightText: © 2026 Parity Technologies pragma solidity ^0.8.34; -/// @title Proof of Personhood Rules for Dotns -/// @notice Proof of personhood interface defining Dotns price calculation, PoP-tier requirements, +/// @title Proof of Personhood Rules for dotNS +/// @notice Proof of personhood interface defining dotNS price calculation, PoP-tier requirements, /// and base-name reservation rules. -/// @dev Classifies labels into the PoP tier required for registration and exposes reservation -/// metadata. Every label is measured as written, except a gateway lite label, whose -/// separator and allocated digits are removed first. Base length <= 5 is reserved for -/// governance; 6-8 requires PopFull; >= 9 is open to every caller as NoStatus. PopLite is -/// the separated form alone, so digits in an ordinary label carry no personhood meaning. -/// Reservations are keyed by that same stem, so `joseph` and `joseph.42` share a slot while -/// `joseph42` is an unrelated name. +/// @dev A label's base name is the label with any device suffix removed: the stem of a device +/// name, or any other label as written. `joseph` is the base name of both `joseph` and +/// `joseph.42`, and a label's base length is the length of its base name. +/// +/// Classifies labels into the PoP tier required for registration and exposes reservation +/// metadata. Every label is measured as written, except a device name, whose separator and +/// allocated digits are removed first. Base length <= 5 is reserved for governance; 6-8 +/// requires personhood; >= 9 is open to every caller as NoStatus. Devicehood applies to the +/// separated form alone, so digits in an ordinary label carry no meaning. Reservations are +/// keyed by that same base name, so `joseph` and `joseph.42` share a slot while `joseph42` is +/// an unrelated name. /// /// Amounts come from the cost model registered under `DotnsConstants.COST_MODEL`, which owns /// the curve; only the base length crosses that seam. Every caller pays the same amount for a @@ -19,18 +23,18 @@ pragma solidity ^0.8.34; /// @custom:security-contact admin@parity.io interface IPopRules { /// @notice Proof-of-Personhood eligibility tier. - /// @dev `NoStatus` is the default for unverified users; `PopLite` and `PopFull` are the two - /// personhood tiers; `Reserved` covers both governance-held names and base stems held by - /// another user through the reservation table. + /// @dev `NoStatus` is the default for unverified users; `Devicehood` and `Personhood` are the + /// two proofs; `Reserved` covers both governance-held names and base names held by another + /// user through the reservation table. enum PopStatus { NoStatus, - PopLite, - PopFull, + Devicehood, + Personhood, Reserved } /// @notice Emitted when a base name receives a reservation. - /// @param baseName The digit-stripped label receiving the reservation. + /// @param baseName The base name receiving the reservation. /// @param owner Address obtaining the reservation right. /// @param expires UNIX timestamp when the reservation expires. event BaseNameReserved(string indexed baseName, address indexed owner, uint64 expires); @@ -54,9 +58,9 @@ interface IPopRules { /// @notice Thrown when a caller is not an authorised controller on the registrar. error NotRegistry(); - /// @notice Thrown when registering a name whose base stem is held as a live reservation by + /// @notice Thrown when registering a name whose base name is held as a live reservation by /// another user. - /// @param label Caller-supplied label whose stem is reserved. + /// @param label Caller-supplied label whose base name is reserved. error NameReserved(string label); /// @notice Thrown when registering a label that classifies as governance-reserved at the @@ -86,7 +90,7 @@ interface IPopRules { string message; } - /// @notice Reservation metadata for a base name (digits removed). + /// @notice Reservation metadata for a base name. /// @param owner Address holding exclusive claim rights during the reservation window. /// @param expires UNIX timestamp when the reservation expires. /// @param controller Address that wrote the reservation; the only address permitted to release @@ -97,11 +101,12 @@ interface IPopRules { address controller; } - /// @notice Classifies a name into a required PoP tier per DotNS naming rules. + /// @notice Classifies a name into a required PoP tier per dotNS naming rules. /// @dev Pure; inputs are the label bytes only. Callers use the returned tier to decide which /// pricing and verification branch applies. A label that is neither a single lowercase - /// ASCII DNS label nor a lite label triggers @custom:reverts PopError; a trailing-digit - /// suffix of any length is accepted and classified by the length it leaves. + /// ASCII DNS label nor a device name triggers @custom:reverts PopError. Trailing digits in + /// an ordinary label count towards its length; only a device name's separator and suffix + /// are removed before it is classified. /// @param name The name label being evaluated. /// @return requirement Required tier for registration. /// @return message Explanation of the classification result. @@ -112,8 +117,8 @@ interface IPopRules { /// @notice Opens or closes the public market for names shorter than nine characters. /// @dev Restricted to a substrate Root origin; any other caller triggers @custom:reverts - /// NotRoot. Short names are otherwise issued through the PoP gateway, so this flag is the - /// Root-only lever that additionally admits them on the public paid path. + /// NotRoot. Short names are otherwise issued through the dotNS gateway pallet, so this + /// flag is the Root-only lever that additionally admits them on the public paid path. /// While closed, which is the deploy default, @custom:function priceWithCheck and /// @custom:function priceWithoutCheck trigger @custom:reverts PopError for a base length /// below nine, so no public caller buys a short name. The gateway free grant and the @@ -126,55 +131,55 @@ interface IPopRules { /// @dev Reads the account's dotns-scoped tier from the personhood precompile and maps it to a /// `PopStatus`. This is the direct account-tier read; the same tier otherwise surfaces /// only as the `userStatus` field of a pricing query. Never returns `Reserved`, so the - /// result is one of `NoStatus`, `PopLite`, or `PopFull`. + /// result is one of `NoStatus`, `Devicehood`, or `Personhood`. /// @param account Address whose tier is read. /// @return tier The account's personhood tier. - function personhoodOf(address account) external view returns (PopStatus tier); + function popStatusOf(address account) external view returns (PopStatus tier); - /// @notice Creates or refreshes a reservation entry for a stem in the 6 to 8 band. + /// @notice Creates or refreshes a reservation entry for a base name in the 6 to 8 band. /// @dev Authorised-controller entry point: only a controller in the registrar's `controllers` /// set may call this, otherwise @custom:reverts NotRegistry. The gateway queue writes /// through @custom:function reserveBaseNameForPop and the public commit-reveal flow /// reads the slot rather than writing one, so this is the entry point for a sibling - /// controller. Its length window bounds the stem and does not name a tier: PopLite is - /// decided by the separated label shape. The caller passes the already-stripped stem; a + /// controller. Its length window bounds the base name and does not name a tier: + /// Devicehood is decided by the separated label shape. The caller passes the base name; a /// non-canonical label, a trailing digit, or a length outside `[6, 8]` triggers /// @custom:reverts PopError. Cross-user collision on a live slot triggers @custom:reverts /// PopError so the caller cannot silently overwrite another user's reservation; same-user /// refresh and writes into an empty or expired slot emit @custom:emits BaseNameReserved. - /// @param stem The base label with no trailing digits. + /// @param baseName The base name, with no trailing digits. /// @param user The address receiving reservation rights. - function reserveBaseName(string calldata stem, address user) external; + function reserveBaseName(string calldata baseName, address user) external; /// @notice Emitted when a base-name reservation is cleared. - /// @param baseName The base label whose reservation was released. + /// @param baseName The base name whose reservation was released. event BaseNameReleased(string indexed baseName); - /// @notice Writes or refreshes a reservation for a bare base-name stem. + /// @notice Writes or refreshes a reservation for a base name. /// @dev Gateway-driven reservation path used by the PoP controller. Only a controller in the /// registrar's `controllers` set may call this, otherwise @custom:reverts NotRegistry. - /// Does not apply the lite-format length window that @custom:function reserveBaseName - /// enforces, but does require the input to be canonical and stem-shaped (no trailing - /// digits); a non-canonical or non-stem label triggers @custom:reverts PopError. If the - /// slot is already live and held by a different user, @custom:reverts PopError so the + /// Does not apply the 6-8 base-length window that @custom:function reserveBaseName + /// enforces, but does require the input to be canonical with no trailing digits; a + /// non-canonical label or one with trailing digits triggers @custom:reverts PopError. If + /// the slot is already live and held by a different user, @custom:reverts PopError so the /// caller's local bookkeeping and PopRules state stay in lockstep; if it is live for the /// same user, expiry is refreshed to `block.timestamp + MAX_RESERVATION_TIME`. Emits /// @custom:emits BaseNameReserved on every successful write. - /// @param stem The base label with no trailing digits. + /// @param baseName The base name, with no trailing digits. /// @param user The address receiving reservation rights. - function reserveBaseNameForPop(string calldata stem, address user) external; + function reserveBaseNameForPop(string calldata baseName, address user) external; - /// @notice Clears a reservation for a base-name stem. + /// @notice Clears a reservation for a base name. /// @dev Only a controller in the registrar's `controllers` set may call this, otherwise - /// @custom:reverts NotRegistry. Non-canonical or non-stem labels trigger - /// @custom:reverts PopError. Live reservations may only be cleared by the same controller - /// that wrote them; another authorised controller attempting to clear a live slot triggers - /// @custom:reverts PopError. Expired reservations may be cleared by any authorised - /// controller as garbage collection. Used by the PoP controller when a reservation is - /// claimed, relinquished, or a queue head promotion leaves the slot empty. Emits - /// @custom:emits BaseNameReleased once the slot is cleared. - /// @param stem The base label whose reservation should be cleared (no trailing digits). - function releaseBaseName(string calldata stem) external; + /// @custom:reverts NotRegistry. Non-canonical labels and labels with trailing digits + /// trigger @custom:reverts PopError. Live reservations may only be cleared by the same + /// controller that wrote them; another authorised controller attempting to clear a live + /// slot triggers @custom:reverts PopError. Expired reservations may be cleared by any + /// authorised controller as garbage collection. Used by the PoP controller when a + /// reservation is claimed, relinquished, or a queue head promotion leaves the slot empty. + /// Emits @custom:emits BaseNameReleased once the slot is cleared. + /// @param baseName The base name whose reservation should be cleared (no trailing digits). + function releaseBaseName(string calldata baseName) external; /// @notice Clears a reservation when the slot owner matches `expectedOwner`, allowing any /// registrar-authorised controller (not only the stamping one) to release the slot. @@ -184,19 +189,20 @@ interface IPopRules { /// a prior occupant has handed the name back to escrow and the new registrant needs /// the cross-flow guard cleared regardless of which controller originally stamped it. /// Only a registrar-authorised controller may call this (@custom:reverts NotRegistry). - /// Non-canonical or non-stem labels trigger @custom:reverts PopError. A live reservation + /// Non-canonical labels and labels with trailing digits trigger @custom:reverts PopError. + /// A live reservation /// whose owner does not match `expectedOwner` triggers @custom:reverts PopError; expired /// reservations are cleared regardless. Emits @custom:emits BaseNameReleased. - /// @param stem The base label whose reservation should be cleared (no trailing digits). + /// @param baseName The base name whose reservation should be cleared (no trailing digits). /// @param expectedOwner The address the caller expects to be the current reservation owner. - function releaseReservationForReclaim(string calldata stem, address expectedOwner) external; + function releaseReservationForReclaim(string calldata baseName, address expectedOwner) external; /// @notice Retrieves reservation information for a base name. /// @dev Raw accessor: returns the stored slot regardless of expiry. Use /// @custom:function isBaseNameReserved /// when live-window semantics are needed. Non-canonical labels trigger /// @custom:reverts PopError. - /// @param baseName The base label without trailing digits. + /// @param baseName The base name, without trailing digits. /// @return owner The address assigned to the reservation. /// @return expires UNIX timestamp when the reservation expires. function getBaseNameReservation(string calldata baseName) @@ -204,21 +210,21 @@ interface IPopRules { view returns (address owner, uint64 expires); - /// @notice Returns the reservation stem of a label: a lite label without its allocated - /// suffix, or any other label unchanged. + /// @notice Returns the base name of a label, its reservation key: a device name without its + /// allocated suffix, or any other label unchanged. /// @dev Mirrors the normalisation applied before a reservation is written, so callers can - /// look up or release one by passing the full label. Only a lite label is shortened, + /// look up or release one by passing the whole label. Only a device name is shortened, /// because only the gateway allocates the digits it carries: `joseph.42` yields /// `joseph` while `joseph42` is an unrelated name and yields itself. Non-canonical /// labels trigger @custom:reverts PopError. - /// @param name Full label, lite or otherwise. - /// @return stem The reservation stem of `name`. - function stripDigits(string calldata name) external pure returns (string memory stem); + /// @param name Whole label, device name or otherwise. + /// @return baseName The base name of `name`. + function stripDigits(string calldata name) external pure returns (string memory baseName); /// @notice Indicates whether a base name is currently reserved. /// @dev Applies the live-window predicate to the stored slot so an expired reservation reads /// as free. Non-canonical labels trigger @custom:reverts PopError. - /// @param baseName The base label without trailing digits. + /// @param baseName The base name, without trailing digits. /// @return reservedStatus True if a live reservation is active. /// @return owner The reservation holder (zero when not reserved). /// @return expires UNIX timestamp when the reservation expires. @@ -231,7 +237,7 @@ interface IPopRules { /// @dev Reverting pricing path used by the commit-reveal controller. Price is the scarcity /// curve for the label's base length and is charged to every caller, verified or not; /// personhood only unlocks the premium band. Non-canonical - /// labels, a base stem held live by another user, a governance-reserved label, or a + /// labels, a base name held live by another user, a governance-reserved label, or a /// `userAddress` whose personhood tier does not meet the label's required tier each /// trigger @custom:reverts PopError. /// @param name Domain label. @@ -269,7 +275,7 @@ interface IPopRules { /// @notice Calculates price with PoP classification and reservation metadata, without /// reverting on conflicts. /// @dev Non-reverting counterpart to `priceWithCheck`: surfaces the same fields, but reports - /// a `Reserved` status through `metadata` instead of reverting when the base stem is + /// a `Reserved` status through `metadata` instead of reverting when the base name is /// held by another user. Used by front-ends that need to present a price and eligibility /// preview without forcing a transaction attempt. Governance-reserved names are not /// rejected here either; the caller decides what to do. Non-canonical labels still @@ -314,7 +320,7 @@ interface IPopRules { /// the name's own curve price. The two components overlap on pure /// tier mismatches, so the function takes their maximum rather than their sum to avoid /// double-charging. Consumed by @custom:function DotnsRegistrar.quoteTransferFee. - /// A label that is neither a single lowercase ASCII DNS label nor a lite label triggers + /// A label that is neither a single lowercase ASCII DNS label nor a device name triggers /// @custom:reverts PopError. /// @param name Domain label being transferred. /// @param from Current holder of the name. @@ -329,9 +335,8 @@ interface IPopRules { view returns (uint256 floor); - /// @notice Returns whether `name` is a base name under PoP rules. - /// @dev A base name has no trailing digits, so it is what a reservation may be keyed by. A - /// lite label always ends in two, so it is never a base name. Non-canonical labels + /// @notice Returns whether `name` can key a reservation: a base name with no trailing digits. + /// @dev A device name always ends in two digits, so it never qualifies. Non-canonical labels /// trigger @custom:reverts PopError. /// @param name The label to check. /// @return isBase True when the label has no trailing digits. @@ -341,7 +346,7 @@ interface IPopRules { /// @dev Prices the label by its base length through the cost model registered under /// `DotnsConstants.COST_MODEL`. Ignores the caller's personhood status and reservation /// state. A non-canonical label triggers @custom:reverts PopError. An ordinary label is - /// priced as written; only a lite label's allocated suffix is removed first. + /// priced as written; only a device name's allocated suffix is removed first. /// @param name Domain label to price. /// @return cost Registration cost in wei. function price(string calldata name) external view returns (uint256 cost); diff --git a/contracts/pop/PopRules.sol b/contracts/pop/PopRules.sol index 841440054..e31e8c05c 100644 --- a/contracts/pop/PopRules.sol +++ b/contracts/pop/PopRules.sol @@ -21,13 +21,14 @@ import {DotnsConstants} from "../utils/DotnsConstants.sol"; import {IPersonhood} from "../external/personhood/IPersonhood.sol"; /// @title PopRules -/// @notice Implements DotNS classification, cost-model-driven pricing, and base-name reservations. -/// @dev Tiers are set by base length. Every label is measured as written, except a lite label, -/// whose separator and allocated digits are not part of the name the candidate chose, so +/// @notice Implements dotNS classification, cost-model-driven pricing, and base-name reservations. +/// @dev Tiers are set by base length, the length of a label's base name: the label with any +/// device suffix removed. Every label is measured as written, except a device name, whose +/// separator and allocated digits are not part of the name the candidate chose, so /// `joseph.42` measures six and `joseph42` measures eight. -/// Base lengths <= 5 are governance-reserved, base lengths 6-8 require PopFull, and base -/// lengths >= 9 are open to any caller as NoStatus. PopLite is the separated form alone: a -/// digit suffix on an ordinary label says nothing about personhood. +/// Base lengths <= 5 are governance-reserved, base lengths 6-8 require personhood, and base +/// lengths >= 9 are open to any caller as NoStatus. Devicehood applies to the separated form +/// alone: a digit suffix on an ordinary label says nothing about a proof. /// Every caller pays the same amount for a given base length. The amount comes from the cost /// model registered under `DotnsConstants.COST_MODEL`, which owns the curve; this contract /// passes it only the base length and keeps the classification, reservation, and tier rules. @@ -44,13 +45,13 @@ contract PopRules is { using StringUtils for *; - /// @notice Active reservations keyed by stem. + /// @notice Active reservations keyed by base name. mapping(string baseName => Reservation reservation) public reservations; /// @notice Maximum time a base name can be reserved. uint256 public constant MAX_RESERVATION_TIME = 12 weeks; - /// @notice Protocol-level address registry for all DotNS contracts. + /// @notice Protocol-level address registry for all dotNS contracts. IDotnsProtocolRegistry public protocolRegistry; /// @notice Whether the public paid path may register names shorter than nine characters. @@ -106,20 +107,20 @@ contract PopRules is /// @inheritdoc IPopRules function reserveBaseName( - string calldata stem, + string calldata baseName, address userAddress ) external override onlyRegistry { - _requireStem(stem); - uint256 stemLength = bytes(stem).length; + _requireBaseName(baseName); + uint256 baseLength = bytes(baseName).length; require( - stemLength >= 6 && stemLength <= 8 && _countTrailingDigits(stem) == 0, - PopError("Reservation stem must be 6-8 chars with no trailing digits") + baseLength >= 6 && baseLength <= 8 && _countTrailingDigits(baseName) == 0, + PopError("Reservation base name must be 6-8 chars with no trailing digits") ); - _writeReservation(stem, userAddress); + _writeReservation(baseName, userAddress); } /// @inheritdoc IPopRules @@ -136,7 +137,7 @@ contract PopRules is override returns (address reservationOwner, uint64 expiryTimestamp) { - _requireStem(baseName); + _requireBaseName(baseName); Reservation memory reserved = reservations[baseName]; return (reserved.owner, reserved.expires); } @@ -148,7 +149,7 @@ contract PopRules is override returns (bool isReserved, address reservationOwner, uint64 expiryTimestamp) { - _requireStem(baseName); + _requireBaseName(baseName); Reservation memory reservation = reservations[baseName]; return (_isLive(reservation), reservation.owner, reservation.expires); } @@ -274,7 +275,7 @@ contract PopRules is Reservation memory reservation = reservations[baseName]; if (_isLive(reservation) && reservation.owner != userAddress) { - metadata.message = "Base name reserved for original Lite registrant"; + metadata.message = "Reserved for a device-name holder's personhood claim"; metadata.status = IPopRules.PopStatus.Reserved; } @@ -312,41 +313,41 @@ contract PopRules is uint256 reachComponent = _meetsReach(required, toTier) ? 0 : ownPrice; PopStatus fromTier = _personhoodTier(from); - // `_personhoodTier` never returns Reserved, so users are in {NoStatus, PopLite, PopFull} - // and enum comparison reflects tier ordering directly. + // `_personhoodTier` never returns Reserved, so users are in {NoStatus, Devicehood, + // Personhood} and enum comparison reflects tier ordering directly. uint256 downgradeComponent = toTier < fromTier ? ownPrice : 0; return reachComponent > downgradeComponent ? reachComponent : downgradeComponent; } /// @inheritdoc IPopRules - function personhoodOf(address account) external view override returns (PopStatus tier) { + function popStatusOf(address account) external view override returns (PopStatus tier) { return _personhoodTier(account); } /// @notice Reads `account`'s dotns-scoped personhood tier from the alias-accounts /// precompile and translates it into a `PopStatus`. /// @dev Single source of truth so callers cannot read the precompile directly and - /// drift on the status mapping. Tiers are defined incrementally on the - /// precompile side: 0=None, 1=Lite, 2=Full. Anything outside that range - /// collapses to `NoStatus` so a future tier addition fails closed instead of - /// silently being treated as a higher level than it actually is. + /// drift on the status mapping. The precompile defines its statuses incrementally and + /// names them 0=None, 1=Lite, 2=Full; this maps 1 to `Devicehood` and 2 to `Personhood`. + /// Anything outside that range collapses to `NoStatus` so a future tier addition fails + /// closed instead of silently being treated as a higher level than it actually is. function _personhoodTier(address account) private view returns (PopStatus) { IPersonhood.PersonhoodInfo memory info = IPersonhood(DotnsConstants.PERSONHOOD) .personhoodStatus(account, DotnsConstants.PERSONHOOD_CONTEXT); - if (info.status == 2) return PopStatus.PopFull; - if (info.status == 1) return PopStatus.PopLite; + if (info.status == 2) return PopStatus.Personhood; + if (info.status == 1) return PopStatus.Devicehood; return PopStatus.NoStatus; } /// @notice Single canonical "is `userStatus` at reach for `required`?" predicate. /// @dev Both `priceWithCheck` and `transferFloor` build on this so the tier-eligibility rule /// lives in exactly one place and the callers cannot disagree about who clears a given label. - /// `_personhoodTier` never returns `Reserved`, so `userStatus` is in `{NoStatus, PopLite, - /// PopFull}` and the enum comparison reflects tier ordering directly. A `Reserved` `required` - /// (governance label) is unreachable by any verified user, so the comparison returns false and - /// the caller charges the friction fee, providing defence-in-depth if a Reserved label ever - /// enters circulation. + /// `_personhoodTier` never returns `Reserved`, so `userStatus` is in `{NoStatus, Devicehood, + /// Personhood}` and the enum comparison reflects tier ordering directly. A `Reserved` + /// `required` (governance label) is unreachable by any verified user, so the comparison returns + /// false and the caller charges the friction fee, providing defence-in-depth if a Reserved + /// label ever enters circulation. function _meetsReach(PopStatus required, PopStatus userStatus) private pure returns (bool) { return userStatus >= required; } @@ -393,26 +394,26 @@ contract PopRules is require(shortNamesEnabled || baseLength >= 9, PopError("Short names are not for sale")); } - /// @notice Index one past `name`'s stem: a lite label without its suffix, or the whole of - /// any other label. - /// @dev Only a lite label has a suffix to remove. The gateway allocates those two digits to + /// @notice Index one past `name`'s base name: a device name without its suffix, or the whole + /// of any other label. + /// @dev Only a device name has a suffix to remove. The gateway allocates those two digits to /// tell apart people who chose the same stem, so removing them recovers what the /// candidate actually picked. No such allocation stands behind the digits in an /// ordinary label, where they are part of the name: `web3` is a four-character word, /// not `web` with a counter. - function _stemEnd(string calldata name) private pure returns (uint256 stemEnd) { - stemEnd = bytes(name).length; - if (!name.isLitePersonLabel()) return stemEnd; - return stemEnd - StringUtils.LITE_SUFFIX_DIGITS - 1; + function _baseNameEnd(string calldata name) private pure returns (uint256 end) { + end = bytes(name).length; + if (!name.isDeviceLabel()) return end; + return end - StringUtils.DEVICE_SUFFIX_DIGITS - 1; } /// @notice The base length that pricing and classification both use to place a name in its - /// band, which is the length of the name's stem. - /// @dev Every label is measured as written, except a lite label, whose allocated suffix is + /// band, which is the length of the name's base name. + /// @dev Every label is measured as written, except a device name, whose allocated suffix is /// not part of the name the candidate chose. So `web3` and `blink182` are measured /// whole and no digit count is privileged or rejected. function _validatedBaseLength(string calldata name) internal pure returns (uint256 baseLength) { - return _stemEnd(name); + return _baseNameEnd(name); } /// @notice Enforces base-name reservation rules. @@ -425,7 +426,7 @@ contract PopRules is if (_isLive(reservation)) { require( reservation.owner == userAddress, - PopError("Base name reserved for original Lite registrant") + PopError("Reserved for a device-name holder's personhood claim") ); } } @@ -453,14 +454,14 @@ contract PopRules is } } - /// @notice Returns `name`'s stem: a lite label without its allocated suffix, or any other - /// label verbatim. - /// @dev The reservation key. Because only a lite label is shortened, `joseph.42` contends + /// @notice Returns `name`'s base name: a device name without its allocated suffix, or any + /// other label verbatim. + /// @dev The reservation key. Because only a device name is shortened, `joseph.42` contends /// with `joseph` while `joseph42` is an unrelated name and contends with nothing. /// @param name Domain label. function _stripDigits(string calldata name) internal pure returns (string memory baseName) { bytes calldata bytesName = bytes(name); - uint256 endPosition = _stemEnd(name); + uint256 endPosition = _baseNameEnd(name); // No suffix to strip: return the input verbatim and skip the manual copy. if (endPosition == bytesName.length) return name; @@ -485,37 +486,37 @@ contract PopRules is } if (baseLength >= 6 && baseLength <= 8) { - // PopLite is the gateway's separated form. Digits in an ordinary label say nothing + // Devicehood is the gateway's separated form. Digits in an ordinary label say nothing // about personhood, so such a label sits in the band its length earns. - if (name.isLitePersonLabel()) { - return (PopStatus.PopLite, "Requires Lite personhood verification", baseLength); + if (name.isDeviceLabel()) { + return (PopStatus.Devicehood, "Requires devicehood", baseLength); } - return (PopStatus.PopFull, "Requires Full personhood verification", baseLength); + return (PopStatus.Personhood, "Requires personhood", baseLength); } - // Base length >= 9 is open to any caller, and is reached by a lite label whose stem is + // Base length >= 9 is open to any caller, and is reached by a device name whose stem is // nine or more through the gateway. return (PopStatus.NoStatus, "Available to all", baseLength); } - /// @notice Requires `stem` to be a canonical DNS label, carrying no separator. - /// @dev Reservation keys are stems, so a separator here is a caller error rather than a lite - /// name. A digit suffix passes this check, because a DNS label admits digits; the entry - /// points that write a reservation reject one themselves. - /// @custom:function _requireLabel is the guard for full labels. - function _requireStem(string calldata stem) internal pure { - require(stem.isSingleLabel(), PopError("Name must be lowercase ASCII DNS label")); + /// @notice Requires `baseName` to be a canonical DNS label, carrying no separator. + /// @dev Reservation keys are base names, and a base name carries no separator, so a separator + /// here is a caller error. A digit suffix passes this check, because a DNS label admits + /// digits; the entry points that write a reservation reject one themselves. + /// @custom:function _requireLabel is the guard for whole labels. + function _requireBaseName(string calldata baseName) internal pure { + require(baseName.isSingleLabel(), PopError("Name must be lowercase ASCII DNS label")); } - /// @notice Requires `name` to be a label DotNS can issue: a canonical DNS label, or a lite - /// label carrying its separator. + /// @notice Requires `name` to be a label dotNS can issue: a canonical DNS label, or a device + /// name carrying its separator. /// @dev The union is the full set of issuable labels, so a near miss such as `alice.4` or - /// `a.b.42` still reverts. @custom:function _requireStem is the stricter guard for + /// `a.b.42` still reverts. @custom:function _requireBaseName is the stricter guard for /// reservation keys, which never carry a separator. function _requireLabel(string calldata name) internal pure { require( - name.isSingleLabel() || name.isLitePersonLabel(), - PopError("Name must be a lowercase ASCII DNS label or a lite label") + name.isSingleLabel() || name.isDeviceLabel(), + PopError("Name must be a lowercase ASCII DNS label or a device name") ); } @@ -534,7 +535,7 @@ contract PopRules is function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} /// @notice Returns the release this network declares it runs, read live from the protocol - /// registry so every DotNS contract reports one synchronised value. + /// registry so every dotNS contract reports one synchronised value. /// @dev 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 @@ -552,35 +553,40 @@ contract PopRules is /// @inheritdoc IPopRules function reserveBaseNameForPop( - string calldata stem, + string calldata baseName, address userAddress ) external override onlyRegistry { - _requireStem(stem); + _requireBaseName(baseName); require( - _countTrailingDigits(stem) == 0, - PopError("Reservation stem must have no trailing digits") + _countTrailingDigits(baseName) == 0, + PopError("Reservation base name must have no trailing digits") ); - _writeReservation(stem, userAddress); + _writeReservation(baseName, userAddress); } /// @inheritdoc IPopRules - function stripDigits(string calldata name) external pure override returns (string memory stem) { + function stripDigits(string calldata name) + external + pure + override + returns (string memory baseName) + { _requireLabel(name); return _stripDigits(name); } /// @inheritdoc IPopRules - function releaseBaseName(string calldata stem) external override onlyRegistry { - _requireStem(stem); + function releaseBaseName(string calldata baseName) external override onlyRegistry { + _requireBaseName(baseName); require( - _countTrailingDigits(stem) == 0, - PopError("Reservation stem must have no trailing digits") + _countTrailingDigits(baseName) == 0, + PopError("Reservation base name must have no trailing digits") ); - Reservation memory reservation = reservations[stem]; + Reservation memory reservation = reservations[baseName]; // Live reservations can only be cleared by the controller that wrote // them, so one registrar-authorised controller cannot wipe another's // active slot. Expired reservations are dead weight and may be cleared @@ -591,45 +597,45 @@ contract PopRules is PopError("Only reserving controller can release") ); } - delete reservations[stem]; - emit BaseNameReleased(stem); + delete reservations[baseName]; + emit BaseNameReleased(baseName); } /// @inheritdoc IPopRules function releaseReservationForReclaim( - string calldata stem, + string calldata baseName, address expectedOwner ) external override onlyRegistry { - _requireStem(stem); + _requireBaseName(baseName); require( - _countTrailingDigits(stem) == 0, - PopError("Reservation stem must have no trailing digits") + _countTrailingDigits(baseName) == 0, + PopError("Reservation base name must have no trailing digits") ); - Reservation memory reservation = reservations[stem]; + Reservation memory reservation = reservations[baseName]; // Cross-controller release is gated on owner match rather than controller match, // so the public registrar controller can clear a PoP-stamped slot during reclaim // when the prior occupant is the reservation owner. if (_isLive(reservation)) { require(reservation.owner == expectedOwner, PopError("Reservation owner mismatch")); } - delete reservations[stem]; - emit BaseNameReleased(stem); + delete reservations[baseName]; + emit BaseNameReleased(baseName); } - /// @notice Internal single-source-of-truth writer for stem reservations. + /// @notice Internal single-source-of-truth writer for base-name reservations. /// @dev Routes both @custom:function reserveBaseName and @custom:function reserveBaseNameForPop /// through one path so the cross-user collision semantics stay identical: a live slot held /// by a different user @custom:reverts PopError, and any other case writes a fresh expiry /// and emits @custom:emits BaseNameReserved. Same-owner re-reservations refresh the expiry /// to `block.timestamp + MAX_RESERVATION_TIME`. Callers are responsible for validating - /// `stem` is canonical and stem-shaped (no trailing digits); this helper does no input + /// `baseName` is canonical and carries no trailing digits; this helper does no input /// validation of its own so each public entry can layer additional eligibility checks. - function _writeReservation(string calldata stem, address userAddress) internal { - Reservation memory existing = reservations[stem]; + function _writeReservation(string calldata baseName, address userAddress) internal { + Reservation memory existing = reservations[baseName]; bool liveSlot = _isLive(existing); if (liveSlot) { require(existing.owner == userAddress, PopError("Base name held by another user")); @@ -641,12 +647,12 @@ contract PopRules is // forge-lint: disable-next-line(unsafe-typecast) uint64 expiryTime = uint64(block.timestamp + MAX_RESERVATION_TIME); // Preserve the original stamping `controller` on same-owner refresh so a sibling controller - // tracking the same stem (e.g. the PoP queue head) retains the right to release. Without - // this, a same-user re-reservation through a different controller silently steals the slot - // and bricks the original controller's release/advance/claim paths. + // tracking the same base name (e.g. the PoP queue head) retains the right to release. + // Without this, a same-user re-reservation through a different controller silently steals + // the slot and bricks the original controller's release/advance/claim paths. address stampingController = liveSlot ? existing.controller : msg.sender; - reservations[stem] = + reservations[baseName] = Reservation({owner: userAddress, expires: expiryTime, controller: stampingController}); - emit BaseNameReserved(stem, userAddress, expiryTime); + emit BaseNameReserved(baseName, userAddress, expiryTime); } } diff --git a/contracts/registrars/DotnsPopController.sol b/contracts/registrars/DotnsPopController.sol index bb2052fab..d0b01853a 100644 --- a/contracts/registrars/DotnsPopController.sol +++ b/contracts/registrars/DotnsPopController.sol @@ -15,6 +15,7 @@ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {IDotnsPopController} from "./IDotnsPopController.sol"; +import {IDotnsPopControllerLegacy} from "./IDotnsPopControllerLegacy.sol"; import {IDotnsRegistrar} from "./IDotnsRegistrar.sol"; import {IDotnsProtocolRegistry} from "../registry/IDotnsProtocolRegistry.sol"; import {IDotnsPopResolver} from "../resolvers/IDotnsPopResolver.sol"; @@ -30,45 +31,45 @@ import {DotnsConstants} from "../utils/DotnsConstants.sol"; import {SystemUtils} from "../utils/SystemUtils.sol"; /// @title DotnsPopController -/// @notice Dedicated PoP controller orchestrating lite-person and full-person username -/// issuance on behalf of the PoP gateway. +/// @notice Dedicated PoP controller that issues device names and personhood names on behalf of +/// the dotNS gateway pallet. /// @dev 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: -/// Personhood is attested off-chain by the gateway before the call reaches this +/// 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 base-label mint path still calls @custom:function IPopRules.classifyName to reject -/// governance-reserved labels (@custom:reverts InvalidBaseLabel on the base path, -/// @custom:reverts InvalidLiteLabel on the lite path). The lite leg accepts any two-digit lite -/// label whose stem is not governance-reserved, regardless of stem length. Native-token pricing -/// is bypassed entirely; the gateway pays no rent. +/// Every issuance path still calls @custom:function IPopRules.classifyName to reject +/// governance-reserved labels (@custom:reverts InvalidPersonhoodLabel for personhood names, +/// @custom: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) Lite-person labels (`stem.NN`) occupy a namespace the public path cannot reach: the -/// separator is legal only on a lite label, and the public path rejects it, so no public +/// (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) Base-name reservations are synchronised into `IPopRules`. The head of this -/// controller's reservation queue is written through `IPopRules.reserveBaseNameForPop` on +/// (2) Personhood-name reservations are synchronised into `IPopRules` by base name. The head of +/// this controller's reservation queue is written through `IPopRules.reserveBaseNameForPop` on /// every head transition; the slot is cleared through `IPopRules.releaseBaseName` when the /// queue empties (claim, final relinquish, final expiry). The public commit-reveal /// controller routes through `IPopRules.priceWithCheck`, which rejects any registration -/// targeting a base-name stem reserved for another user, so the public flow respects +/// 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 -/// lite-to-full link persistence via +/// device-link persistence via /// @custom:contract IDotnsPopResolver. Keeping per-name records on the resolver preserves the /// "Store = labels only" invariant. /// @custom:security-contact admin@parity.io @@ -77,7 +78,8 @@ contract DotnsPopController is UUPSUpgradeable, OwnableUpgradeable, ERC165Upgradeable, - IDotnsPopController + IDotnsPopController, + IDotnsPopControllerLegacy { using StringUtils for *; using EnumerableSet for EnumerableSet.AddressSet; @@ -97,7 +99,7 @@ contract DotnsPopController is /// state has been committed. uint256 private constant CHAT_KEY_LENGTH = 65; - /// @notice Protocol-level address registry for all DotNS contracts. + /// @notice Protocol-level address registry for all dotNS contracts. IDotnsProtocolRegistry public protocolRegistry; /// @notice Per-label queue metadata (head/tail pointers). @@ -112,13 +114,13 @@ contract DotnsPopController is /// read both fields in one call instead of two. mapping(address user => UserReservation reservation) internal _userReservations; - /// @notice Remembers the base-label string for each reserved labelhash so the PopRules + /// @notice 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). /// @dev 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 baseLabel) internal _reservedBaseLabel; + mapping(bytes32 labelhash => string reservedLabel) internal _reservedBaseLabel; /// @notice Duration (in seconds) after which a reservation entry is considered expired. /// @dev Sets the reservation duration, configurable by @@ -132,9 +134,8 @@ contract DotnsPopController is /// @notice Per-user pile of deferred names awaiting a `LabelStore`. /// @dev 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. Each - /// entry's deadline is measured from its own `mintedAt` against `reservationDuration`. + /// 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; /// @notice Labels this controller minted, keyed by the bare label without the TLD. @@ -191,79 +192,136 @@ contract DotnsPopController is } /// @inheritdoc IDotnsPopController - function reserveLiteName(LiteRegistration calldata params) external override onlyRoot { - _reserveLite(_popRules(), params); + function issueDeviceName(DeviceNameIssuance calldata params) external override onlyRoot { + _issueDeviceName(_popRules(), params, false); + } + + /// @inheritdoc IDotnsPopControllerLegacy + function reserveLiteName(DeviceNameIssuance calldata params) external override onlyRoot { + _issueDeviceName(_popRules(), params, true); + } + + /// @inheritdoc IDotnsPopController + function issueDeviceNameWithReservation(DeviceNameIssuanceWithReservation calldata params) + external + override + onlyRoot + { + _issueDeviceNameWithReservation(params, false); + } + + /// @inheritdoc IDotnsPopControllerLegacy + function reserveBaseName(DeviceNameIssuanceWithReservation calldata params) + external + override + onlyRoot + { + _issueDeviceNameWithReservation(params, true); + } + + /// @inheritdoc IDotnsPopController + function reservePersonhoodName(PersonhoodNameReservation calldata params) + external + override + onlyRoot + { + IPopRules rules = _popRules(); + (bytes32 reservedHash,) = _validateReservablePersonhoodLabel(rules, params.label, false); + _advanceExpiredHead(reservedHash); + _dropUserReservation(params.user); + _enqueueReservation(rules, reservedHash, params.label, params.user); } /// @inheritdoc IDotnsPopController - function reserveBaseName(BaseReservation calldata params) external override onlyRoot { + function issuePersonhoodName(PersonhoodNameIssuance calldata params) + external + override + onlyRoot + { + _issuePersonhoodName(params, false); + } + + /// @inheritdoc IDotnsPopControllerLegacy + function registerBaseName(PersonhoodNameIssuance calldata params) external override onlyRoot { + _issuePersonhoodName(params, true); + } + + /// @notice Body shared by @custom:function issueDeviceNameWithReservation and its legacy + /// entrypoint. + /// @dev 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 + { IPopRules rules = _popRules(); bytes32 reservedHash; - bool hasReservation = bytes(params.reservedBaseLabel).length != 0; + bool hasReservation = bytes(params.reservedLabel).length != 0; if (hasReservation) { - (reservedHash,) = _validateReservableBaseLabel(rules, params.reservedBaseLabel); + (reservedHash,) = + _validateReservablePersonhoodLabel(rules, params.reservedLabel, legacy); } - _reserveLite(rules, params.lite); + _issueDeviceName(rules, params.issuance, legacy); if (hasReservation) { _advanceExpiredHead(reservedHash); - _removeUserFromQueue(params.lite.user); - _enqueueReservation(rules, reservedHash, params.reservedBaseLabel, params.lite.user); + _dropUserReservation(params.issuance.user); + _enqueueReservation(rules, reservedHash, params.reservedLabel, params.issuance.user); } } - /// @inheritdoc IDotnsPopController - function reserveBaseNameOnly(BaseNameReservation calldata params) external override onlyRoot { - IPopRules rules = _popRules(); - (bytes32 reservedHash,) = _validateReservableBaseLabel(rules, params.reservedBaseLabel); - _advanceExpiredHead(reservedHash); - _removeUserFromQueue(params.user); - _enqueueReservation(rules, reservedHash, params.reservedBaseLabel, params.user); - } - - /// @notice Lite-only mint shared by @custom:function reserveLiteName and the lite leg - /// of @custom:function reserveBaseName. - /// @dev Gateway attestation is the authority for personhood on this path; the on-chain + /// @notice Device-name issuance shared by @custom:function issueDeviceName, the issuance of + /// @custom:function issueDeviceNameWithReservation, and their legacy entrypoints. + /// @dev 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 - /// @custom:reverts InvalidLiteLabel, which the gateway decodes by selector; letting - /// `classifyName` catch it instead would surface an undecodable PopRules string. - /// Takes the @custom:struct LiteRegistration struct directly so both call sites pass the same - /// payload shape: the typed entrypoint forwards its own `params`, the `reserveBaseName` - /// entrypoint forwards `params.lite`. - function _reserveLite(IPopRules rules, LiteRegistration calldata params) internal { - require(params.liteLabel.isLitePersonLabel(), InvalidLiteLabel()); + /// 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 + /// @custom:reverts InvalidDeviceLabel for the callers that decode the legacy error. + function _issueDeviceName( + IPopRules rules, + DeviceNameIssuance calldata params, + bool legacy + ) + internal + { + _requireDeviceLabel(params.label.isDeviceLabel(), legacy); _requireValidChatKey(params.chatKey); - (IPopRules.PopStatus required,) = rules.classifyName(params.liteLabel); - // The shape check fixes the suffix, so classification lands on PopLite (stem 6-8), + (IPopRules.PopStatus required,) = rules.classifyName(params.label); + // The shape check fixes the suffix, so classification lands on Devicehood (stem 6-8), // NoStatus (stem 9 or more), or Reserved (stem 5 or fewer). Accept the first two; a // stem short enough to be governance-reserved is not issued from this path. - require(required != IPopRules.PopStatus.Reserved, InvalidLiteLabel()); - (bytes32 labelhash, bytes32 node) = _validateLiteLabel(params.liteLabel); + _requireDeviceLabel(required != IPopRules.PopStatus.Reserved, legacy); + (bytes32 labelhash, bytes32 node) = _validateDeviceLabel(params.label, legacy); _completeGatewayRegistration( - params.user, params.liteLabel, labelhash, node, params.chatKey, bytes32(0) + params.user, params.label, labelhash, node, params.chatKey, bytes32(0) ); - emit LiteNameReserved(labelhash, params.user, params.liteLabel); + emit DeviceNameIssued(labelhash, params.user, params.label); } - /// @inheritdoc IDotnsPopController - function registerBaseName(FullRegistration calldata params) external override onlyRoot { + /// @notice Personhood-name issuance shared by @custom:function issuePersonhoodName and its + /// legacy entrypoint. + /// @dev `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 { Link calldata link = params.link; address user = params.user; string calldata label = params.label; - (bytes32 labelhash, bytes32 node) = _validateBaseLabel(label); + (bytes32 labelhash, bytes32 node) = _validatePersonhoodLabel(label, legacy); IPopRules rules = _popRules(); (IPopRules.PopStatus required,) = rules.classifyName(label); - require( - required != IPopRules.PopStatus.Reserved && required != IPopRules.PopStatus.PopLite, - InvalidBaseLabel() + _requirePersonhoodLabel( + required != IPopRules.PopStatus.Reserved && required != IPopRules.PopStatus.Devicehood, + legacy ); _advanceExpiredHead(labelhash); @@ -293,52 +351,53 @@ contract DotnsPopController is } if (isClaim) { - _clearQueue(labelhash); + _clearQueue(labelhash, user); } else { - _removeUserFromQueue(user); + _dropUserReservation(user); } - bytes32 liteLabelhash; - bytes32 liteNode; + bytes32 deviceLabelhash; + bytes32 deviceNode; bytes memory chatKeyToPersist; - if (link.kind == LinkKind.LiteUsername) { - require(link.liteLabel.isLitePersonLabel(), InvalidLiteLabel()); - (liteLabelhash, liteNode) = _validateLiteLabel(link.liteLabel); - // A lite username is a subnode, so its owner lives in the registry record rather than + if (link.kind == LinkKind.DeviceName) { + _requireDeviceLabel(link.deviceLabel.isDeviceLabel(), legacy); + (deviceLabelhash, deviceNode) = _validateDeviceLabel(link.deviceLabel, legacy); + // A device name is a subnode, so its owner lives in the registry record rather than // the registrar's ERC-721 ledger. require( - _registry().owner(liteNode) == user, LiteLabelNotOwnedByUser(user, liteLabelhash) + _registry().owner(deviceNode) == user, DeviceNameNotOwned(user, deviceLabelhash) ); - chatKeyToPersist = _popResolver().chatKey(liteNode); + chatKeyToPersist = _popResolver().chatKey(deviceNode); } else { _requireValidChatKey(link.chatKey); chatKeyToPersist = link.chatKey; } - _completeGatewayRegistration(user, label, labelhash, node, chatKeyToPersist, liteLabelhash); + _completeGatewayRegistration( + user, label, labelhash, node, chatKeyToPersist, deviceLabelhash + ); + emit PersonhoodNameIssued(labelhash, user, label); if (isClaim) { - emit BaseNameClaimed(labelhash, user, label); - } else { - emit StandaloneNameRegistered(labelhash, user, label); + emit ReservationClaimed(labelhash, user); } - if (link.kind == LinkKind.LiteUsername) { - emit LiteToFullLinked(labelhash, liteLabelhash); + if (link.kind == LinkKind.DeviceName) { + emit DeviceNameLinked(labelhash, deviceLabelhash); } } /// @inheritdoc IDotnsPopController - function expireReservation(string calldata reservedBaseLabel) external override { - (bytes32 labelhash,) = _validateBaseLabel(reservedBaseLabel); + function expireReservation(string calldata label) external override { + (bytes32 labelhash,) = _validatePersonhoodLabel(label, false); _advanceExpiredHead(labelhash); } /// @inheritdoc IDotnsPopController function relinquishReservation() external override { - UserReservation memory userRes = _userReservations[msg.sender]; - require(userRes.labelhash != bytes32(0), NoActiveReservation(msg.sender)); - _removeUserFromQueue(msg.sender); - emit ReservationRelinquished(userRes.labelhash, msg.sender); + require( + _userReservations[msg.sender].labelhash != bytes32(0), NoActiveReservation(msg.sender) + ); + _dropUserReservation(msg.sender); } /// @inheritdoc IDotnsPopController @@ -407,10 +466,10 @@ contract DotnsPopController is returns (address) { bytes32 labelhash = LabelUtils.labelhashMemory(label); - // A lite label settled its ownership as a subnode, so its store entry keys the same - // hierarchical node; a full label keys the second-level node under the TLD. - bytes32 node = label.isLitePersonLabelMemory() - ? _liteSubnode(label) + // A device name settled its ownership as a subnode, so its store entry keys the same + // hierarchical node; a personhood name keys the second-level node under the TLD. + bytes32 node = label.isDeviceLabelMemory() + ? _deviceSubnode(label) : LabelUtils.namehashUnder(protocolRegistry.tldNode(), labelhash); if (store == address(0)) { store = factory.deployLabelStoreFor(user); @@ -422,13 +481,13 @@ contract DotnsPopController is } /// @inheritdoc IDotnsPopController - function isReservedForClaim(string calldata reservedBaseLabel) + function isReservedForClaim(string calldata label) external view override returns (bool reserved, address holder) { - (bytes32 labelhash,) = _validateBaseLabel(reservedBaseLabel); + (bytes32 labelhash,) = _validatePersonhoodLabel(label, false); ReservationQueueMeta memory meta = _reservationMeta[labelhash]; if (meta.head >= meta.tail) return (false, address(0)); @@ -540,11 +599,11 @@ contract DotnsPopController is } /// @inheritdoc IDotnsPopController - function reservedBaseLabelOf(bytes32 labelhash) + function reservedLabelOf(bytes32 labelhash) external view override - returns (string memory baseLabel) + returns (string memory label) { return _reservedBaseLabel[labelhash]; } @@ -557,11 +616,12 @@ contract DotnsPopController is returns (bool) { return interfaceId == type(IDotnsPopController).interfaceId + || interfaceId == type(IDotnsPopControllerLegacy).interfaceId || super.supportsInterface(interfaceId); } /// @notice Returns the release this network declares it runs, read live from the protocol - /// registry so every DotNS contract reports one synchronised value. + /// registry so every dotNS contract reports one synchronised value. /// @dev 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 @@ -572,14 +632,14 @@ contract DotnsPopController is } /// @notice Mints a name, wires forward registry, persists PoP-flow records (chat key, - /// lite link) on the PoP resolver, and either writes the label into the owner's + /// 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. /// @dev The mint + forward-registry pair is delegated to /// @custom: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 the `LabelStore` constructor. PoP-flow per-name records - /// (chat key, lite link) are persisted eagerly on @custom:contract IDotnsPopResolver + /// (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 @@ -592,21 +652,22 @@ contract DotnsPopController is bytes32 labelhash, bytes32 node, bytes memory chatKeyBytes, - bytes32 liteLabelhash + bytes32 deviceLabelhash ) internal { _popIssued[label] = true; - // A lite username is a subname under its numeric container, so it takes the subnode path - // and never mints a token. A full-person name is a tokenised second-level registration and + // A device name is a subname under its numeric container, so it takes the subnode path + // and never mints a token. A personhood name is a tokenised second-level registration and // keeps the shared token triad untouched. `persist` is false because the store write is // deferred to the pending-claim queue below and the user syncs it later. - if (label.isLitePersonLabelMemory()) { - // A lite name is issued once. Its subnode already existing means a duplicate issuance, - // which would rehome the identity and overwrite its records, so it is rejected. - require(!_registry().recordExists(node), LiteNameAlreadyIssued()); - (string memory stem, string memory suffix) = label.splitLiteLabel(); + if (label.isDeviceLabelMemory()) { + // A device name is issued once. Its subnode already existing means a duplicate + // issuance, which would rehome the identity and overwrite its records, so it is + // rejected. + require(!_registry().recordExists(node), DeviceNameAlreadyIssued()); + (string memory stem, string memory suffix) = label.splitDeviceLabel(); // Take the node from the registry write itself, so the chat-key and store writes below // land on exactly the node the record was created at rather than a separately derived // one that could drift from it. @@ -631,13 +692,13 @@ contract DotnsPopController is ); } - if (chatKeyBytes.length != 0 || liteLabelhash != bytes32(0)) { + if (chatKeyBytes.length != 0 || deviceLabelhash != bytes32(0)) { IDotnsPopResolver resolver = _popResolver(); if (chatKeyBytes.length != 0) { resolver.setChatKey(node, chatKeyBytes); } - if (liteLabelhash != bytes32(0)) { - resolver.setLiteLink(node, liteLabelhash); + if (deviceLabelhash != bytes32(0)) { + resolver.setDeviceLink(node, deviceLabelhash); } } @@ -657,9 +718,9 @@ contract DotnsPopController is /// 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`. /// @param store Owner's `LabelStore` proxy. - /// @param node The name's node. A lite label resolves to its stem beneath its numeric + /// @param node 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. - /// @param label Bare label without the TLD, which is appended on write. A lite label + /// @param label 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. function _writeRecord(address store, bytes32 node, string memory label) internal { if (ILabelStore(store).isLocked(node)) return; @@ -694,7 +755,7 @@ contract DotnsPopController is function _enqueueReservation( IPopRules rules, bytes32 labelhash, - string memory baseLabel, + string memory reservedLabel, address user ) internal @@ -714,8 +775,8 @@ contract DotnsPopController is _userReservations[user] = UserReservation({labelhash: labelhash, index: index}); if (becomesHead) { - _reservedBaseLabel[labelhash] = baseLabel; - rules.reserveBaseNameForPop(baseLabel, user); + _reservedBaseLabel[labelhash] = reservedLabel; + rules.reserveBaseNameForPop(reservedLabel, user); } emit ReservationQueued(labelhash, user, index - meta.head); @@ -723,16 +784,19 @@ contract DotnsPopController is /// @notice Wipes the entire reservation queue for `labelhash` and releases the /// corresponding PopRules reservation. - /// @dev Used when a holder claims their reservation: every waiter is evicted 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) internal { + /// @dev 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 { ReservationQueueMeta memory meta = _reservationMeta[labelhash]; for (uint64 i = meta.head; i < meta.tail; i++) { ReservationEntry memory entry = _reservationEntries[labelhash][i]; if (entry.owner != address(0)) { delete _userReservations[entry.owner]; + if (entry.owner != claimant) { + emit ReservationEvicted(labelhash, entry.owner); + } } delete _reservationEntries[labelhash][i]; } @@ -777,12 +841,25 @@ contract DotnsPopController is } } + /// @notice Drops `user`'s own reservation entry, if any, and records that it happened. + /// @dev 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 { + bytes32 labelhash = _userReservations[user].labelhash; + if (labelhash == bytes32(0)) return; + _removeUserFromQueue(user); + emit ReservationRelinquished(labelhash, user); + } + /// @notice Removes `user` from whichever reservation queue they currently occupy. /// @dev 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. + /// queue shape intact, so no advance or resync is needed. Emits nothing itself; callers go + /// through @custom:function _dropUserReservation. function _removeUserFromQueue(address user) internal { UserReservation memory userRes = _userReservations[user]; bytes32 labelhash = userRes.labelhash; @@ -799,74 +876,103 @@ contract DotnsPopController is } } - /// @notice Validates a lite-person `stem.NN` label and derives `(labelhash, node)`. + /// @notice Validates a device name `stem.NN` and derives `(labelhash, node)`. /// @dev 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. - function _validateLiteLabel(string memory liteLabel) + /// and the reservation queue. `legacy` selects the label error, as in + /// @custom:function _requireDeviceLabel. + function _validateDeviceLabel( + string memory deviceLabel, + bool legacy + ) internal view returns (bytes32 labelhash, bytes32 node) { - require(liteLabel.isLitePersonLabelMemory(), InvalidLiteLabel()); - labelhash = LabelUtils.labelhashMemory(liteLabel); - node = _liteSubnode(liteLabel); + _requireDeviceLabel(deviceLabel.isDeviceLabelMemory(), legacy); + labelhash = LabelUtils.labelhashMemory(deviceLabel); + node = _deviceSubnode(deviceLabel); } - /// @notice Derives the hierarchical subnode for a lite label `.`. - /// @dev Splits at the separator and walks `suffix.tld` then `stem` under it, so a lite name + /// @notice Derives the hierarchical subnode for a device name `.`. + /// @dev 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 _validateLiteLabel and pending-claim settlement so every lite + /// Shared by @custom:function _validateDeviceLabel and pending-claim settlement so every /// consumer agrees on one node. - /// @param liteLabel Lite label held in memory, e.g. `alice.01`. + /// @param deviceLabel Device name held in memory, e.g. `alice.01`. /// @return subnode Namehash of `stem` under `suffix.tld`. - function _liteSubnode(string memory liteLabel) internal view returns (bytes32 subnode) { - subnode = SubnodeUtils.liteSubnodeOf(protocolRegistry.tldNode(), liteLabel); + function _deviceSubnode(string memory deviceLabel) internal view returns (bytes32 subnode) { + subnode = SubnodeUtils.deviceSubnodeOf(protocolRegistry.tldNode(), deviceLabel); } - /// @notice Validates a base (full-person) label and derives `(labelhash, node)`. + /// @notice Validates a personhood name and derives `(labelhash, node)`. /// @dev 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. - function _validateBaseLabel(string calldata baseLabel) + /// `legacy` selects the label error, as in @custom:function _requirePersonhoodLabel. + function _validatePersonhoodLabel( + string calldata label, + bool legacy + ) internal view returns (bytes32 labelhash, bytes32 node) { - // Letters only, matching the gateway's full-person label rule: a - // full-person label is a name a person chose, so it admits no digits and no hyphens. - // Classification does not cover this on its own, since a suffixed label with nine or - // more characters lands on NoStatus and would otherwise pass. - require(baseLabel.isPersonLabel(), InvalidBaseLabel()); - (labelhash, node) = LabelUtils.deriveNode(protocolRegistry.tldNode(), baseLabel); - } - - /// @notice Validates a base label as reservable and returns its hashes. - /// @dev Shared by both reservation entrypoints 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 classify outside the governance-reserved tier and be a base name, be a - /// letters-only person label, 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 stem, so such a - /// reservation could never be redeemed yet would hold the stem, and so every lite name built - /// on it, 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 _validateReservableBaseLabel( + // Letters only, matching the gateway's personhood-name rule: a personhood name is a name + // a person chose, so it admits no digits and no hyphens. Classification does not cover + // this on its own: a label carrying a digit or a hyphen is measured as written, so one of + // six or more characters classifies as Personhood or NoStatus and would otherwise pass. + _requirePersonhoodLabel(label.isPersonhoodLabel(), legacy); + (labelhash, node) = LabelUtils.deriveNode(protocolRegistry.tldNode(), label); + } + + /// @notice Validates a personhood name as reservable and returns its hashes. + /// @dev 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 baseLabel + string calldata label, + bool legacy ) internal view returns (bytes32 labelhash, bytes32 node) { - (labelhash, node) = _validateBaseLabel(baseLabel); + (labelhash, node) = _validatePersonhoodLabel(label, legacy); - (IPopRules.PopStatus required,) = rules.classifyName(baseLabel); - require( - required != IPopRules.PopStatus.Reserved && rules.isBaseName(baseLabel), - InvalidBaseLabel() + (IPopRules.PopStatus required,) = rules.classifyName(label); + _requirePersonhoodLabel( + required != IPopRules.PopStatus.Reserved && rules.isBaseName(label), legacy ); - require(!_registrar().exists(uint256(node)), BaseNameAlreadyRegistered()); + require(!_registrar().exists(uint256(node)), PersonhoodNameUnavailable()); + } + + /// @notice Reverts with the device-label error the caller decodes when `ok` is false. + /// @dev `legacy` is set only on the @custom:contract IDotnsPopControllerLegacy entrypoints, + /// whose callers decode @custom:reverts InvalidLiteLabel; every other path reverts + /// @custom:reverts InvalidDeviceLabel. + function _requireDeviceLabel(bool ok, bool legacy) internal pure { + if (ok) return; + if (legacy) revert InvalidLiteLabel(); + revert InvalidDeviceLabel(); + } + + /// @notice Reverts with the personhood-label error the caller decodes when `ok` is false. + /// @dev `legacy` is set only on the @custom:contract IDotnsPopControllerLegacy entrypoints, + /// whose callers decode @custom:reverts InvalidBaseLabel; every other path reverts + /// @custom:reverts InvalidPersonhoodLabel. + function _requirePersonhoodLabel(bool ok, bool legacy) internal pure { + if (ok) return; + if (legacy) revert InvalidBaseLabel(); + revert InvalidPersonhoodLabel(); } /// @notice Reverts when a non-empty chat key is not exactly `CHAT_KEY_LENGTH` bytes. @@ -910,19 +1016,19 @@ contract DotnsPopController is /// write the slot). The release-then-reserve pair satisfies PopRules' ownership gate on /// `reserveBaseNameForPop`. function _syncPopRulesToHead(bytes32 labelhash, address newHead) internal { - string memory baseLabel = _reservedBaseLabel[labelhash]; + string memory reservedLabel = _reservedBaseLabel[labelhash]; IPopRules rules = _popRules(); - rules.releaseBaseName(baseLabel); - rules.reserveBaseNameForPop(baseLabel, newHead); + rules.releaseBaseName(reservedLabel); + rules.reserveBaseNameForPop(reservedLabel, newHead); emit ReservationHeadAdvanced(labelhash, newHead); } /// @notice Clears the PopRules slot and the local label bookkeeping when the queue empties /// (claim, last-relinquish, last-expire). function _releasePopRulesSlot(bytes32 labelhash) internal { - string memory baseLabel = _reservedBaseLabel[labelhash]; - if (bytes(baseLabel).length == 0) return; - _popRules().releaseBaseName(baseLabel); + string memory reservedLabel = _reservedBaseLabel[labelhash]; + if (bytes(reservedLabel).length == 0) return; + _popRules().releaseBaseName(reservedLabel); delete _reservedBaseLabel[labelhash]; } diff --git a/contracts/registrars/DotnsPopLens.sol b/contracts/registrars/DotnsPopLens.sol index c9f28fb41..506976e6a 100644 --- a/contracts/registrars/DotnsPopLens.sol +++ b/contracts/registrars/DotnsPopLens.sol @@ -23,7 +23,7 @@ import {DotnsConstants} from "../utils/DotnsConstants.sol"; /// controller's pending queue, ownership from the registry, chat keys and links from the PoP /// resolver, and label classification from PopRules. The registry is the single ownership /// authority: it delegates a tokenised name to the registrar and owns a subname directly, so a -/// lite username, which is a subname, resolves the same way as a full-person name. Living outside +/// device name, which is a subname, resolves the same way as a personhood name. Living outside /// the controller keeps the controller within the contract-size limit. Deployed as a plain /// contract through the CREATE3 factory, so its address is deterministic and it can be redeployed /// on a read change without touching stored state. @@ -47,7 +47,7 @@ contract DotnsPopLens is IDotnsPopLens { } /// @inheritdoc IDotnsPopLens - function liteNamesOf( + function namesOf( address user, uint256 offset, uint256 limit @@ -57,31 +57,12 @@ contract DotnsPopLens is IDotnsPopLens { override returns (Name[] memory names) { - return _pageNames(user, offset, limit, true); + return _pageNames(user, offset, limit); } /// @inheritdoc IDotnsPopLens - function fullNamesOf( - address user, - uint256 offset, - uint256 limit - ) - external - view - override - returns (Name[] memory names) - { - return _pageNames(user, offset, limit, false); - } - - /// @inheritdoc IDotnsPopLens - function liteNameCountOf(address user) external view override returns (uint256 count) { - return _countNames(user, true); - } - - /// @inheritdoc IDotnsPopLens - function fullNameCountOf(address user) external view override returns (uint256 count) { - return _countNames(user, false); + function nameCountOf(address user) external view override returns (uint256 count) { + return _countNames(user); } /// @inheritdoc IDotnsPopLens @@ -90,8 +71,8 @@ contract DotnsPopLens is IDotnsPopLens { // label from the node alone, and classification must see the label before the detail is // returned. NameDetail memory detail = _detail(_nodeOf(name), name); - // Holding the label means holding its labelhash, so the lite-to-full link resolves here. - detail.fullClaim = _popResolver().fullClaim(LabelUtils.labelhash(name)); + // Holding the label means holding its labelhash, so the personhood link resolves here. + detail.personhoodNode = _popResolver().personhoodNodeOf(LabelUtils.labelhash(name)); return detail; } @@ -100,10 +81,11 @@ contract DotnsPopLens is IDotnsPopLens { // No label is supplied: the node cannot recover a pending subname's label, so it stays // empty. NameDetail memory detail = _detail(node, ""); - // The node cannot be inverted to a labelhash, so `fullClaim` resolves only when the label - // is independently recoverable (a settled name whose label the registrar returns). + // The node cannot be inverted to a labelhash, so `personhoodNode` resolves only when the + // label is independently recoverable (a settled name whose label the registrar returns). if (bytes(detail.label).length != 0) { - detail.fullClaim = _popResolver().fullClaim(LabelUtils.labelhashMemory(detail.label)); + detail.personhoodNode = + _popResolver().personhoodNodeOf(LabelUtils.labelhashMemory(detail.label)); } return detail; } @@ -116,29 +98,27 @@ contract DotnsPopLens is IDotnsPopLens { profile.reservationLabelhash = controller.userReservation(user).labelhash; } - /// @notice Whether `label` belongs in the lite listing (`wantLite`) or the full listing. - /// @dev Two questions and one guard the caller already applied. Whether a name is an identity - /// at all is provenance, so each listing is gated on + /// @notice Whether `label` belongs in the listing: a name the gateway issued. + /// @dev Whether a name is an identity at all is provenance, so the listing is gated on /// @custom:function IDotnsPopController.isPopIssued: characters alone would admit a public - /// registration spelled `joseph42`, which reads as a full-person name and is not one. Which - /// kind of identity it is, lite or full, is spelling: a lite name carries its separator and a - /// full-person name does not, and provenance covers both. A lite name is a subname and a - /// full-person name is a tokenised second-level name, and the callers resolve ownership through - /// the registry, which covers both, so both listings reach their names. Provenance is keyed by - /// text, so a subname a `user` created under a name they own does not enter a listing unless - /// the controller issued it. The two listings together cover the names the gateway issued and - /// `user` holds, one kind each, rather than everything the account holds. - function _belongsToListing(string memory label, bool wantLite) internal view returns (bool) { + /// registration spelled `joseph42`, which passes the single-label shape check yet was never + /// issued by the gateway. The label shape then confirms it is one of the two kinds the gateway + /// issues, a device name with its separator or a personhood name without one. A device name is + /// a subname and a personhood name is a tokenised second-level name, and the callers resolve + /// ownership through the registry, which covers both. Provenance is keyed by text, so a + /// subname a `user` created under a name they own does not enter the listing unless the + /// controller issued it. + function _belongsToListing(string memory label) internal view returns (bool) { if (!_controller().isPopIssued(label)) return false; - return wantLite ? label.isLitePersonLabelMemory() : label.isSingleLabelMemory(); + return label.isDeviceLabelMemory() || label.isSingleLabelMemory(); } - /// @notice Counts the names currently owned by `user` that belong to the requested listing. + /// @notice Counts the gateway-issued names currently owned by `user`. /// @dev Walks the user's `LabelStore` (settled names) then their pending claims, keeping only - /// entries that belong to the listing and are still owned by `user` on the registrar. A pending + /// entries that belong to the listing and are still owned by `user` in the registry. A pending /// entry already written into the store by a sibling flow is skipped so it is not counted /// twice. - function _countNames(address user, bool wantLite) internal view returns (uint256 count) { + function _countNames(address user) internal view returns (uint256 count) { string memory tld = _protocolRegistry.tld(); address store = _storeFactory().getLabelStore(user); @@ -152,7 +132,7 @@ contract DotnsPopLens is IDotnsPopLens { // The store keys ownership by node and provenance by text separately; bind them so // a row whose key is not its own text's node is neither counted nor listed. if (node != _nodeOf(label)) continue; - if (_belongsToListing(label, wantLite)) ++count; + if (_belongsToListing(label)) ++count; } } @@ -160,14 +140,14 @@ contract DotnsPopLens is IDotnsPopLens { uint256 pending = queue.length; for (uint256 j; j < pending; ++j) { string memory label = queue[j].label; - if (!_belongsToListing(label, wantLite)) continue; + if (!_belongsToListing(label)) continue; bytes32 node = _nodeOf(label); if (store != address(0) && ILabelStore(store).isLocked(node)) continue; if (_ownedBy(node, user)) ++count; } } - /// @notice Returns a page of `user`'s owned names belonging to the requested listing. + /// @notice Returns a page of `user`'s owned gateway-issued names. /// @dev Same ownership-verified walk as @custom:function _countNames, in the same order /// (store then pending), skipping the first `offset` matches and returning up to `limit` /// entries. `limit` is clamped to `DotnsConstants.MAX_PAGE_SIZE` to bound the memory and the @@ -175,8 +155,7 @@ contract DotnsPopLens is IDotnsPopLens { function _pageNames( address user, uint256 offset, - uint256 limit, - bool wantLite + uint256 limit ) internal view @@ -202,25 +181,22 @@ contract DotnsPopLens is IDotnsPopLens { // Bind the row's node key to its own text, so a row whose key is not its text's // node is neither counted nor listed. if (node != _nodeOf(label)) continue; - if (!_belongsToListing(label, wantLite)) continue; + if (!_belongsToListing(label)) continue; if (seen++ < offset) continue; - page[filled++] = Name({node: node, label: label, settled: true, deadline: 0}); + page[filled++] = Name({node: node, label: label, settled: true}); } } IDotnsPopController.PendingClaim[] memory queue = _pendingClaims(user); uint256 pending = queue.length; - uint64 duration = _controller().reservationDuration(); for (uint256 j; j < pending && filled < limit; ++j) { string memory label = queue[j].label; - if (!_belongsToListing(label, wantLite)) continue; + if (!_belongsToListing(label)) continue; bytes32 node = _nodeOf(label); if (store != address(0) && ILabelStore(store).isLocked(node)) continue; if (!_ownedBy(node, user)) continue; if (seen++ < offset) continue; - page[filled++] = Name({ - node: node, label: label, settled: false, deadline: queue[j].mintedAt + duration - }); + page[filled++] = Name({node: node, label: label, settled: false}); } if (filled == limit) return page; @@ -238,13 +214,14 @@ contract DotnsPopLens is IDotnsPopLens { return _registry().owner(node) == user; } - /// @notice Gathers a name's record from the registrar, PoP resolver, and PopRules. + /// @notice Gathers a name's record from the registry, registrar, PoP resolver, and PopRules. /// @dev Reads defensively so an unminted or unsettled name yields zeroed fields instead of - /// reverting. `fullClaim` is left for the caller because it needs the labelhash, which is - /// recoverable from the label string but not from the node alone. `tier` classifies the label - /// shape, so `knownLabel` supplies the label for a pending subname the node cannot recover, - /// letting classification run before the detail is returned; it is ignored when the label is - /// otherwise recoverable, and an empty `knownLabel` leaves an unrecoverable label unclassified. + /// reverting. `personhoodNode` is left for the caller because it needs the labelhash, which is + /// recoverable from the label string but not from the node alone. `requiredTier` classifies the + /// label shape, so `knownLabel` supplies the label for an unsettled name the node cannot + /// recover, letting classification run before the detail is returned; it is ignored when the + /// label is otherwise recoverable, and an empty `knownLabel` leaves an unrecoverable label + /// unclassified. /// @param node The name's node. /// @param knownLabel Label the caller already holds, used only when the node cannot recover it. function _detail( @@ -263,16 +240,17 @@ contract DotnsPopLens is IDotnsPopLens { address store = _storeFactory().getLabelStore(owner); bool settled = store != address(0) && ILabelStore(store).isLocked(node); detail.settled = settled; - // A tokenised name carries its label on the registrar; a subname does not, so its - // label is read back from the owner's store once settled. A pending subname has no - // recoverable label from the node alone, so it is taken from `knownLabel` when the - // caller supplied one. + // The registrar reads a tokenised name's label from the holder's store, and a subname's + // label is read from the owner's store directly, so either is recoverable from the node + // only once settled. Until then it is taken from `knownLabel` when the caller supplied + // one. if (_registrar().exists(uint256(node))) { detail.label = _registrar().labelOf(uint256(node)); } else if (settled) { detail.label = LabelUtils.stripTld(_protocolRegistry.tld(), ILabelStore(store).getLabel(node)); - } else if (bytes(knownLabel).length != 0) { + } + if (bytes(detail.label).length == 0 && bytes(knownLabel).length != 0) { detail.label = knownLabel; } } @@ -282,12 +260,12 @@ contract DotnsPopLens is IDotnsPopLens { try _popRules().classifyName(detail.label) returns ( IPopRules.PopStatus tier, string memory ) { - detail.tier = tier; + detail.requiredTier = tier; } catch {} } IDotnsPopResolver resolver = _popResolver(); detail.chatKey = resolver.chatKey(node); - detail.liteLink = resolver.liteLink(node); + detail.deviceLabelhash = resolver.deviceLabelhashOf(node); } /// @notice Reads a bounded page of `user`'s pending claims from the controller. @@ -316,15 +294,15 @@ contract DotnsPopLens is IDotnsPopLens { return IDotnsRegistry(_protocolRegistry.get(DotnsConstants.REGISTRY)); } - /// @notice Derives the node a name resolves to, whether tokenised or a lite subname. - /// @dev A lite name is `stem` beneath its numeric container, so it hashes as a subnode; any + /// @notice Derives the node a name resolves to, whether tokenised or a device-name subname. + /// @dev A device name is `stem` beneath its numeric container, so it hashes as a subnode; any /// other name hashes as a second-level label under the TLD. /// @param label Bare label without the TLD, e.g. `alice` or `alice.01`. /// @return node The node the name resolves to. function _nodeOf(string memory label) internal view returns (bytes32 node) { bytes32 tldNode = _protocolRegistry.tldNode(); - if (label.isLitePersonLabelMemory()) { - return SubnodeUtils.liteSubnodeOf(tldNode, label); + if (label.isDeviceLabelMemory()) { + return SubnodeUtils.deviceSubnodeOf(tldNode, label); } node = LabelUtils.namehashUnder(tldNode, LabelUtils.labelhashMemory(label)); } diff --git a/contracts/registrars/DotnsRegistrar.sol b/contracts/registrars/DotnsRegistrar.sol index 719a6ba3b..a1106534e 100644 --- a/contracts/registrars/DotnsRegistrar.sol +++ b/contracts/registrars/DotnsRegistrar.sol @@ -24,13 +24,13 @@ import {IDotnsNameEscrow} from "../escrow/IDotnsNameEscrow.sol"; import {IPopRules} from "../pop/IPopRules.sol"; import {DotnsConstants} from "../utils/DotnsConstants.sol"; -/// @title Dotns Registrar +/// @title dotNS Registrar /// @notice ERC721-backed registrar implementing permanent name ownership. /// @dev Deliberately policy-free on pricing, reservations, and PoP gating; those live in the /// controllers and @custom:contract IPopRules. The registrar owns transferability itself: publicly -/// registered names transfer freely, while names minted through the PoP gateway are soulbound and -/// revert on transfer. The `_update` hook enforces both the soulbound gate and the fee-on-transfer -/// settlement that consults the escrow. +/// registered names transfer freely, while names minted through the dotNS gateway pallet are +/// soulbound and revert on transfer. The `_update` hook enforces both the soulbound gate and the +/// fee-on-transfer settlement that consults the escrow. /// @custom:security-contact admin@parity.io contract DotnsRegistrar is Initializable, @@ -50,12 +50,12 @@ contract DotnsRegistrar is /// @custom:oz-retyped-from mapping(IDotnsRegistrarController => bool) mapping(IDotnsController controller => bool exists) public controllers; - /// @notice Protocol-level address registry for all DotNS contracts. + /// @notice Protocol-level address registry for all dotNS contracts. /// @dev Used to resolve sibling contract addresses (store factory, controller, registry) /// without storing individual references. IDotnsProtocolRegistry public protocolRegistry; - /// @notice Marks a token as soulbound: minted through the PoP gateway and non-transferable. + /// @notice Marks a token as soulbound: minted through the gateway pallet and non-transferable. /// @dev Set at mint by @custom:function register when the caller is the address registered /// under `DotnsConstants.POP_CONTROLLER`. Write-once and never cleared: a name's soulbound /// state is fixed at registration. Read by the `_update` transfer gate and by @@ -144,7 +144,7 @@ contract DotnsRegistrar is // `LabelStore` under `pallet-revive`, so the controller stashes a pending claim and the // user settles via @custom:function IDotnsPopController.claimLabelStore later). Non-empty // labels must still be canonical so the transfer-floor lookup in `_quoteTransferFee` - // cannot brick the token by reverting on a malformed stem. + // cannot brick the token by reverting on a malformed label. require(bytes(label).length == 0 || label.isSingleLabel(), InvalidLabel()); _mint(owner, id); // Provenance is verified here rather than trusted from a caller-supplied flag: only the @@ -225,7 +225,7 @@ contract DotnsRegistrar is } /// @notice Returns the release this network declares it runs, read live from the protocol - /// registry so every DotNS contract reports one synchronised value. + /// registry so every dotNS contract reports one synchronised value. /// @dev 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 diff --git a/contracts/registrars/DotnsRegistrarController.sol b/contracts/registrars/DotnsRegistrarController.sol index b2e36f16a..c91d4001f 100644 --- a/contracts/registrars/DotnsRegistrarController.sol +++ b/contracts/registrars/DotnsRegistrarController.sol @@ -30,7 +30,7 @@ import {RegistrationUtils} from "../utils/RegistrationUtils.sol"; import {StoreUtils} from "../utils/StoreUtils.sol"; import {SystemUtils} from "../utils/SystemUtils.sol"; -/// @title Dotns Registrar Controller +/// @title dotNS Registrar Controller /// @notice Allocates top-level labels using a commit reveal scheme. /// @dev Orchestrates allocation, PoP validation, pricing enforcement, forward registry /// wiring, default reverse resolution, and immutable store writing. @@ -70,7 +70,7 @@ contract DotnsRegistrarController is /// from this stamp. mapping(bytes32 hash => uint256 version) public committedPricingVersion; - /// @notice Protocol-level address registry for all DotNS contracts. + /// @notice Protocol-level address registry for all dotNS contracts. IDotnsProtocolRegistry public protocolRegistry; /// @dev Reserved storage space to allow for layout changes in the future. @@ -175,19 +175,19 @@ contract DotnsRegistrarController is uint256 tokenId = uint256(node); bool isReclaim = registrar.exists(tokenId); - string memory stem = rules.stripDigits(registration.label); - bool stemCanonical = stem.isSingleLabelMemory(); + string memory baseName = rules.stripDigits(registration.label); + bool baseNameCanonical = baseName.isSingleLabelMemory(); // Reclaim hands the name back from a prior occupant who may hold a sibling-controller's - // stem reservation, which is garbage once the name moves on, so clear it. Non-reclaim + // base-name reservation, which is garbage once the name moves on, so clear it. Non-reclaim // paths intentionally leave an existing reservation in place: the slot belongs to the // sibling controller that wrote it (e.g. the PoP queue head stamp), and clearing it from // here would brick that controller's release and advance paths. - if (stemCanonical && isReclaim) { - (address reservationOwner,) = rules.getBaseNameReservation(stem); + if (baseNameCanonical && isReclaim) { + (address reservationOwner,) = rules.getBaseNameReservation(baseName); address expectedOwner = IDotnsNameEscrow(payable(escrow)).getReleasePosition(tokenId).recipient; if (reservationOwner != address(0) && reservationOwner == expectedOwner) { - rules.releaseReservationForReclaim(stem, expectedOwner); + rules.releaseReservationForReclaim(baseName, expectedOwner); } } @@ -393,7 +393,7 @@ contract DotnsRegistrarController is /// Store. /// @dev On a fresh mint the triad of mint + forward-registry + store-write is delegated /// to @custom:function RegistrationUtils.registerAndStore, the single canonical implementation - /// shared across every DotNS registration flow. On a reclaim the mint step is skipped (the + /// shared across every dotNS registration flow. On a reclaim the mint step is skipped (the /// escrow has already moved custody) and only the registry wiring and store write run. /// Reverse-record setting and the priced-registration event stay here because they are /// commit-reveal-specific policy. @@ -444,7 +444,7 @@ contract DotnsRegistrarController is } /// @notice Returns the release this network declares it runs, read live from the protocol - /// registry so every DotNS contract reports one synchronised value. + /// registry so every dotNS contract reports one synchronised value. /// @dev 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 diff --git a/contracts/registrars/IDotnsController.sol b/contracts/registrars/IDotnsController.sol index f0417ed5b..0258ca7c4 100644 --- a/contracts/registrars/IDotnsController.sol +++ b/contracts/registrars/IDotnsController.sol @@ -7,7 +7,7 @@ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; /// @title IDotnsController /// @notice Baseline interface implemented by every controller authorised on `DotnsRegistrar`. /// @dev Marker interface that types `DotnsRegistrar.controllers` so every authorised caller -/// (public commit-reveal, PoP gateway, any future privileged flow) fits the same mapping and +/// (public commit-reveal, gateway path, any future privileged flow) fits the same mapping and /// the same `addController` / `removeController` signatures without forcing a common call /// surface. Extending @custom:contract IERC165 lets the registrar (or any observer) runtime-check /// which concrete controller interface a given address implements. diff --git a/contracts/registrars/IDotnsPopController.sol b/contracts/registrars/IDotnsPopController.sol index be1995b7c..efdaeeb87 100644 --- a/contracts/registrars/IDotnsPopController.sol +++ b/contracts/registrars/IDotnsPopController.sol @@ -5,54 +5,53 @@ pragma solidity ^0.8.34; import {IDotnsController} from "./IDotnsController.sol"; /// @title IDotnsPopController -/// @notice Interface for the dedicated PoP controller orchestrating lite-person and full-person -/// username issuance on behalf of the PoP gateway. +/// @notice Interface for the dedicated PoP controller that issues device names and personhood +/// names on behalf of the dotNS gateway pallet. /// @dev Deliberately disjoint from @custom:contract IDotnsRegistrarController. The two /// controllers coexist on @custom:contract DotnsRegistrar via its multi-controller affordance -/// and neither imports the other. A full-person username collides through the registrar's ERC721 -/// availability check (first-to-mint wins); a lite username is not a token, so it collides through +/// and neither imports the other. A personhood name collides through the registrar's ERC721 +/// availability check (first-to-mint wins); a device name is not a token, so it collides through /// @custom:function IDotnsRegistry.recordExists at its stem-under-container node -/// (@custom:reverts LiteNameAlreadyIssued). Reservation queuing for `reservedBaseLabel` -/// mirrors its live head into PopRules, so a queued stem also blocks the public +/// (@custom:reverts DeviceNameAlreadyIssued). Reservation queuing for `reservedLabel` +/// mirrors its live head into PopRules, so a queued base name also blocks the public /// commit-reveal flow, which reads that slot when it prices a name. /// /// Label formats: -/// Lite-person usernames (first argument to @custom:function reserveBaseName and the -/// `liteLabel` of a `LinkKind.LiteUsername` link) are a stem of lowercase ASCII letters, a -/// separator, then exactly two digits (e.g. `joseph.42`) per -/// @custom:function StringUtils.isLitePersonLabel. The stem is stricter than a DNS label -/// because the name a person chooses is restricted to letters; a stem short enough to -/// be governance-reserved is rejected by classification, not by the shape. The label is stored in -/// the form the gateway sends, which is the canonical form of the name, so nothing here -/// normalises it. -/// Full-person usernames (the `label` of @custom:function registerBaseName and the -/// optional `reservedBaseLabel` of @custom:function reserveBaseName) are lowercase ASCII -/// letters only, per @custom:function StringUtils.isPersonLabel (e.g. `alice`). That is the -/// same rule a lite stem follows and is stricter than a DNS label: no hyphens and no interior -/// digits, because a full-person name is also a name a person chose. A separator marks a lite -/// label and is rejected everywhere else, so only the gateway can create a dotted name; a +/// Device names (the `label` of @custom:function issueDeviceName and the `deviceLabel` of a +/// `LinkKind.DeviceName` link) are a stem of lowercase ASCII letters, a separator, then exactly +/// two digits (e.g. `joseph.42`) per @custom:function StringUtils.isDeviceLabel. The stem is +/// stricter than a DNS label because the name a person chooses is restricted to letters; a stem +/// short enough to be governance-reserved is rejected by classification, not by the shape. The +/// label is stored in the form the gateway sends, which is the canonical form of the name, so +/// nothing here normalises it. +/// Personhood names (the `label` of @custom:function issuePersonhoodName and the optional +/// `reservedLabel` of @custom:function issueDeviceNameWithReservation) are lowercase ASCII +/// letters only, per @custom:function StringUtils.isPersonhoodLabel (e.g. `alice`). That is the +/// same rule a device-name stem follows and is stricter than a DNS label: no hyphens and no +/// interior digits, because a personhood name is also a name a person chose. A separator marks a +/// device name and is rejected everywhere else, so only the gateway can create a dotted name; a /// digit suffix on its own is not exclusive, since a public label may carry one directly. -/// Cross-flow priority on the base stem is arbitrated by +/// Cross-flow priority on the base name is arbitrated by /// @custom:function IPopRules.reserveBaseNameForPop. /// @custom:security-contact admin@parity.io interface IDotnsPopController is IDotnsController { - /// @notice Discriminant for the `Link` union supplied to `registerBaseName`. - /// @dev 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. + /// @notice Discriminant for the `Link` union supplied to `issuePersonhoodName`. + /// @dev Selects the chat-key source for the personhood name. Orthogonal to whether the + /// issuance 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`. `DeviceName` means the + /// personhood name is linked to a prior device name (`link.deviceLabel`) and inherits its chat + /// key. The member order is part of the ABI: the gateway pallet encodes `DeviceName` as `1`. enum LinkKind { None, - LiteUsername + DeviceName } - /// @notice Tagged union selecting the chat-key source for a full-person registration. - /// @param liteLabel Lite-person `stem.NN` label (only read when `kind == LiteUsername`). + /// @notice Tagged union selecting the chat-key source for a personhood-name issuance. + /// @param deviceLabel Device name `stem.NN` (only read when `kind == DeviceName`). /// @param chatKey Chat key bytes (only read when `kind == None`). struct Link { LinkKind kind; - string liteLabel; + string deviceLabel; bytes chatKey; } @@ -83,79 +82,76 @@ interface IDotnsPopController is IDotnsController { /// @notice Deferred per-user binding of a freshly minted name to its `LabelStore`. /// @dev 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 + /// origin and writes the stashed label. PoP-resolver records (chat key, device 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`. - /// @param label Bare label without the TLD, which is appended at settlement time. A lite - /// claim carries its separator, so this is not always a single DNS label. + /// the store and settles the entries. Entries never lapse and can be settled at any time. + /// @param label Bare label without the TLD, which is appended at settlement time. A device + /// name carries its separator, so this is not always a single DNS label. /// @param mintedAt Timestamp of the originating mint. struct PendingClaim { string label; uint64 mintedAt; } - /// @notice Lite-person registration payload. + /// @notice Device-name issuance payload. /// @dev 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. - /// @param liteLabel Lite-person `stem.NN` label being minted. + /// @param label Device name `stem.NN` being issued. /// @param user Beneficiary account on this chain. /// @param chatKey Chat-key bytes persisted on the PoP resolver. Empty leaves the slot unset. - struct LiteRegistration { - string liteLabel; + struct DeviceNameIssuance { + string label; address user; bytes chatKey; } - /// @notice Lite-person registration combined with an optional base-name reservation. - /// @dev `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. - /// @param lite Lite-person registration request; see LiteRegistration. - /// @param reservedBaseLabel Base label to enqueue for a later full-person claim. Empty - /// string skips the reservation leg. - struct BaseReservation { - LiteRegistration lite; - string reservedBaseLabel; + /// @notice Device-name issuance combined with an optional personhood-name reservation. + /// @dev Composition of a @custom:struct DeviceNameIssuance and a reservation slot, so + /// internal helpers consume the issuance via `params.issuance` without unpacking. The issuance + /// always runs; the reservation only runs when `reservedLabel` is non-empty. + /// @param issuance Device-name issuance request; see DeviceNameIssuance. + /// @param reservedLabel Personhood name to enqueue for a later claim. Empty string skips the + /// reservation. + struct DeviceNameIssuanceWithReservation { + DeviceNameIssuance issuance; + string reservedLabel; } - /// @notice Base-name reservation payload for the split gateway flow. - /// @dev This is the reservation-only primitive. The lite username mint is handled by - /// @custom:function reserveLiteName, and LabelStore settlement is handled by + /// @notice Personhood-name reservation payload. + /// @dev The reservation-only primitive. Device-name issuance is handled by + /// @custom:function issueDeviceName, and LabelStore settlement by /// @custom:function settlePendingClaims. /// @param user Beneficiary account that will hold the reservation. - /// @param reservedBaseLabel Base label to enqueue for a later full-person claim. - struct BaseNameReservation { + /// @param label Personhood name to enqueue for a later claim. + struct PersonhoodNameReservation { address user; - string reservedBaseLabel; + string label; } - /// @notice Full-person registration payload. - /// @param label Base DNS label being minted. + /// @notice Personhood-name issuance payload. + /// @param label Personhood name being issued. /// @param user Beneficiary account on this chain. /// @param link Chat-key source for the new entry; see @custom:struct Link. - struct FullRegistration { + struct PersonhoodNameIssuance { string label; address user; Link link; } - /// @notice Emitted when a lite-person username is registered via the PoP gateway. - event LiteNameReserved(bytes32 indexed labelhash, address indexed user, string label); + /// @notice Emitted when the gateway pallet issues a device name. + event DeviceNameIssued(bytes32 indexed labelhash, address indexed user, string label); - /// @notice Emitted when a full-person username is claimed out of an existing reservation. - event BaseNameClaimed(bytes32 indexed labelhash, address indexed user, string label); + /// @notice Emitted when the gateway pallet issues a personhood name. + /// @dev Fires whether or not the user held a reservation for it; a claim of the live + /// reservation also @custom:emits ReservationClaimed. + event PersonhoodNameIssued(bytes32 indexed labelhash, address indexed user, string label); - /// @notice Emitted when a standalone full-person username is registered via the PoP gateway. - event StandaloneNameRegistered(bytes32 indexed labelhash, address indexed user, string label); - - /// @notice Emitted when a reservation entry is added to the queue for a base name. + /// @notice Emitted when a reservation entry is added to the queue for a personhood name. /// @param position Position in the queue at the time of joining (0 = active holder). event ReservationQueued( bytes32 indexed reservedLabelhash, address indexed user, uint64 position @@ -164,11 +160,19 @@ interface IDotnsPopController is IDotnsController { /// @notice Emitted when a reservation entry is removed due to expiry. event ReservationExpired(bytes32 indexed reservedLabelhash, address indexed user); - /// @notice Emitted when a user voluntarily relinquishes their reservation. + /// @notice Emitted when a user's own reservation entry is dropped: an explicit relinquish, a + /// standalone personhood-name issuance, or a re-reservation that moves the user to another + /// queue. event ReservationRelinquished(bytes32 indexed reservedLabelhash, address indexed user); - /// @notice Emitted when a full-person username is linked to a lite-person username. - event LiteToFullLinked(bytes32 indexed fullLabelhash, bytes32 indexed liteLabelhash); + /// @notice Emitted when the holder of a queue's live head claims the reserved name. + event ReservationClaimed(bytes32 indexed reservedLabelhash, address indexed user); + + /// @notice Emitted for each waiter removed from a queue when its head is claimed. + event ReservationEvicted(bytes32 indexed reservedLabelhash, address indexed user); + + /// @notice Emitted when a personhood name is linked to a device name. + event DeviceNameLinked(bytes32 indexed personhoodLabelhash, bytes32 indexed deviceLabelhash); /// @notice Emitted when the reservation duration is updated. event ReservationDurationSet(uint64 duration); @@ -197,7 +201,7 @@ interface IDotnsPopController is IDotnsController { /// @notice 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. - /// @param labelhash Base-label hash whose queue head changed. + /// @param labelhash Personhood-name hash whose queue head changed. /// @param newHead Address now holding the head slot. event ReservationHeadAdvanced(bytes32 indexed labelhash, address indexed newHead); @@ -206,20 +210,22 @@ interface IDotnsPopController is IDotnsController { /// and reading `msg.sender` under one traps. error NotRoot(); - /// @notice Thrown when a supplied lite-person label does not match `stem.NN`. - error InvalidLiteLabel(); + /// @notice Thrown when a supplied device name does not match `stem.NN`, or its stem is + /// governance-reserved. + error InvalidDeviceLabel(); - /// @notice Thrown when a supplied base label is not a canonical DNS label. - error InvalidBaseLabel(); + /// @notice Thrown when a supplied personhood name is not lowercase ASCII letters only, or + /// classifies outside what the gateway may issue or reserve. + error InvalidPersonhoodLabel(); - /// @notice 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(); + /// @notice Thrown when a personhood name to reserve already has an owner on the registrar, so + /// the queued reservation could never be claimed. + error PersonhoodNameUnavailable(); - /// @notice Thrown when a lite username is issued again while its subname already exists. - /// @dev A lite name is issued once; re-issuing it would rehome the identity to a new owner and - /// overwrite its records, so an existing subname is rejected rather than reassigned. - error LiteNameAlreadyIssued(); + /// @notice Thrown when a device name is issued again while its subname already exists. + /// @dev A device name is issued once; re-issuing it would rehome the identity to a new owner + /// and overwrite its records, so an existing subname is rejected rather than reassigned. + error DeviceNameAlreadyIssued(); /// @notice Thrown when a supplied chat key is non-empty and not exactly 65 bytes long. /// @dev Mirrors the resolver's `InvalidChatKeyLength` so the controller surfaces a @@ -236,87 +242,84 @@ interface IDotnsPopController is IDotnsController { /// @notice Thrown when attempting to enqueue a user who already has an active reservation. error AlreadyReserved(address user, bytes32 labelhash); - /// @notice Thrown when someone tries to mint a base label in standalone mode while another user - /// holds the live head-of-queue reservation. + /// @notice Thrown when someone tries to issue a personhood name standalone while another user + /// holds the live head-of-queue reservation for it. error NotHolder(address user, bytes32 labelhash); - /// @notice Thrown when a lite-link inheritance does not match the registrar-side owner - /// of the lite label. - /// @dev Prevents identity hijack by ensuring the registrant on the full-name leg actually - /// holds the prior lite identity whose chat key is being inherited. + /// @notice Thrown when a device link names a device name the registrant does not own. + /// @dev Prevents identity hijack by ensuring the registrant of the personhood name actually + /// holds the device name whose chat key is being inherited. /// @param user Registrant supplied by the gateway. - /// @param liteLabelhash Lite label whose ownership did not match. - error LiteLabelNotOwnedByUser(address user, bytes32 liteLabelhash); + /// @param deviceLabelhash Device name whose ownership did not match. + error DeviceNameNotOwned(address user, bytes32 deviceLabelhash); /// @notice Thrown when @custom:function setReservationDuration is called with a value below /// the protocol minimum. /// @param duration Caller-supplied duration, in seconds. error ReservationDurationTooLow(uint64 duration); - /// @notice 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. - /// @dev Callable only under a Root origin (otherwise @custom:reverts NotRoot). The - /// lite leg validates the `stem.NN` shape and requires the label to classify outside the - /// governance-reserved tier (otherwise @custom:reverts InvalidLiteLabel), and rejects a + /// @notice Issues a device name to the supplied user and optionally enqueues a reservation + /// for a personhood name they intend to claim later. + /// @dev 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 LiteNameReserved and @custom:emits NameRegistered; - /// on a cold-path mint it @custom:emits LiteNameReserved and + /// `LabelStore`) it @custom:emits DeviceNameIssued and @custom:emits NameRegistered; + /// on a cold-path mint it @custom:emits DeviceNameIssued and /// @custom:emits PendingClaimStashed, with @custom:emits NameRegistered deferred to - /// @custom:function settlePendingClaims when the claim settles. The base-name leg only runs - /// when `reservedBaseLabel` is non-empty: it requires a letters-only person label, which is - /// therefore also a true base label (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:function settlePendingClaims when the claim settles. The reservation only runs + /// when `reservedLabel` is non-empty: it requires a letters-only personhood label + /// (otherwise @custom:reverts InvalidPersonhoodLabel) with no owner on the registrar + /// (otherwise @custom:reverts PersonhoodNameUnavailable), since a name that already has an + /// owner could never be claimed. This validation runs before both the issuance and any queue + /// mutation, so an already-registered `reservedLabel` aborts the whole call and the candidate + /// receives no device name either; callers should validate the reserved label before + /// attesting rather than relying on this revert. It then 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 reached - /// `MAX_RESERVATION_QUEUE`. Cross-chain callers pass the ABI-encoded reservation tuple as - /// the call's payload, which Solidity decodes directly. - /// @param params Reservation request; see @custom:struct BaseReservation. - function reserveBaseName(BaseReservation calldata params) external; - - /// @notice Enqueues only the full/base-name reservation for a user. - /// @dev Callable only under a 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, - /// is not lowercase ASCII letters (so a hyphen or any digit rejects it), or is - /// governance-reserved, and with - /// @custom: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. - /// @param params Reservation request; see @custom:struct BaseNameReservation. - function reserveBaseNameOnly(BaseNameReservation calldata params) external; - - /// @notice Registers a lite-person username on behalf of the supplied - /// user without touching the base-name reservation queue. + /// `MAX_RESERVATION_QUEUE`. Cross-chain callers pass the ABI-encoded tuple as the call's + /// payload, which Solidity decodes directly. + /// @param params Issuance and reservation request; see + /// @custom:struct DeviceNameIssuanceWithReservation. + function issueDeviceNameWithReservation(DeviceNameIssuanceWithReservation calldata params) + external; + + /// @notice Enqueues only a personhood-name reservation for a user. + /// @dev 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. + /// @param params Reservation request; see @custom:struct PersonhoodNameReservation. + function reservePersonhoodName(PersonhoodNameReservation calldata params) external; + + /// @notice Issues a device name to the supplied user without touching the reservation queue. /// @dev 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 InvalidLiteLabel); a supplied chat + /// governance-reserved tier (otherwise @custom:reverts InvalidDeviceLabel); a supplied chat /// key whose length is neither zero nor `CHAT_KEY_LENGTH` reverts - /// @custom:reverts InvalidChatKey before mint and resolver writes run. A username that has - /// already been issued reverts @custom:reverts LiteNameAlreadyIssued. On a warm-path mint - /// @custom:emits LiteNameReserved and @custom:emits NameRegistered. On a cold-path - /// mint @custom:emits LiteNameReserved and @custom:emits PendingClaimStashed, with + /// @custom: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 + /// @custom:emits DeviceNameIssued and @custom:emits NameRegistered. On a cold-path + /// mint @custom:emits DeviceNameIssued and @custom:emits PendingClaimStashed, with /// @custom: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. - /// @param params Registration request; see @custom:struct LiteRegistration. - function reserveLiteName(LiteRegistration calldata params) external; + /// claim settles. Cross-chain callers pass the ABI-encoded issuance tuple as the call's + /// payload, which Solidity decodes directly. + /// @param params Issuance request; see @custom:struct DeviceNameIssuance. + function issueDeviceName(DeviceNameIssuance calldata params) external; /// @notice Whether this controller issued `label` as a PoP identity. - /// @dev Keyed by text, so it answers about a name rather than about a node. A lite label is - /// issued as a subname (`joseph` beneath its numeric container `42`) and a full-person label as + /// @dev 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 @@ -326,51 +329,50 @@ interface IDotnsPopController is IDotnsController { /// @return issued True when this controller issued `label`. function isPopIssued(string calldata label) external view returns (bool issued); - /// @notice Registers a full-person username on behalf of the supplied user. - /// @dev Callable only under a Root origin (otherwise @custom:reverts NotRoot). The - /// base label must be a letters-only person label, and therefore a true base label, - /// (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 (this controller's prior queue head, - /// or a sibling controller's write), 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 - /// @custom:reverts InvalidChatKey). The `LiteUsername` branch validates the lite label's - /// `stem.NN` 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 - /// @custom:emits NameRegistered first (from the inner mint), then - /// @custom:emits BaseNameClaimed or @custom:emits StandaloneNameRegistered, then - /// @custom:emits LiteToFullLinked when applicable. On a cold-path mint - /// @custom: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. - /// @param params Registration request; see @custom:struct FullRegistration. - function registerBaseName(FullRegistration calldata params) external; + /// @notice Issues a personhood name to the supplied user. + /// @dev 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 + /// @custom: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 + /// @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 label: a claim wipes the entire queue + /// (@custom:emits ReservationEvicted for every other waiter), releases the PopRules slot, and + /// @custom:emits ReservationClaimed; a non-claim drops any pending entry the user holds + /// (@custom:emits ReservationRelinquished). Either way the issuance + /// @custom:emits PersonhoodNameIssued. 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 device name. + /// The fresh-key branch rejects a chat key whose length is neither zero nor `CHAT_KEY_LENGTH` + /// (otherwise @custom:reverts InvalidChatKey). The `DeviceName` branch validates the device + /// name's `stem.NN` shape (otherwise @custom:reverts InvalidDeviceLabel), requires the + /// registrant to own the device name in the registry (otherwise + /// @custom:reverts DeviceNameNotOwned), reads its chat key from the resolver and copies it + /// across; if the device name carries no chat key the inherited value is empty and the + /// personhood name's chat-key write is silently skipped (the `DeviceNameLinked` event still + /// fires). @custom:emits DeviceNameLinked alongside the issuance event. On a warm-path mint + /// the event order is @custom:emits NameRegistered first (from the inner mint), then + /// @custom:emits PersonhoodNameIssued, then @custom:emits DeviceNameLinked when applicable. On + /// a cold-path mint @custom: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 issuance + /// tuple as the call's payload, which Solidity decodes directly. + /// @param params Issuance request; see @custom:struct PersonhoodNameIssuance. + function issuePersonhoodName(PersonhoodNameIssuance calldata params) external; /// @notice Permissionlessly removes expired entries from the head of a reservation queue. /// @dev 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 - /// `reservedBaseLabel` as a letters-only person label (otherwise - /// @custom:reverts InvalidBaseLabel) - /// and @custom:emits ReservationExpired for every expired entry reaped from the - /// head. A label carrying a digit or a hyphen is not a person label and - /// @custom:reverts InvalidBaseLabel, as does a lite label, since a separator is not one + /// `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 + /// @custom: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 reservedBaseLabel) external; + /// @param label Personhood name whose queue is reaped. + function expireReservation(string calldata label) external; /// @notice Lets the caller voluntarily drop their own active reservation. /// @dev Reverts with @custom:reverts NoActiveReservation when the caller holds no live @@ -381,9 +383,10 @@ interface IDotnsPopController is IDotnsController { function relinquishReservation() external; /// @notice Returns whether a label currently has a live reservation at the queue head. - /// @dev Validates `reservedBaseLabel` as a letters-only person label (otherwise - /// @custom:reverts InvalidBaseLabel) before inspecting the queue. - function isReservedForClaim(string calldata reservedBaseLabel) + /// @dev Validates `label` as a letters-only personhood label (otherwise + /// @custom:reverts InvalidPersonhoodLabel) before inspecting the queue. + /// @param label Personhood name whose queue is inspected. + function isReservedForClaim(string calldata label) external view returns (bool reserved, address holder); @@ -398,7 +401,7 @@ interface IDotnsPopController is IDotnsController { /// 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. - /// @param labelhash Keccak-256 of the base label whose queue is being read. + /// @param labelhash Keccak-256 of the personhood name whose queue is being read. /// @return head Index of the live queue head. /// @return tail Index one past the last queued entry. function reservationMeta(bytes32 labelhash) external view returns (uint64 head, uint64 tail); @@ -407,7 +410,7 @@ interface IDotnsPopController is IDotnsController { /// @dev 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)`. - /// @param labelhash Keccak-256 of the base label whose queue is being read. + /// @param labelhash Keccak-256 of the personhood name whose queue is being read. /// @param index Queue index to look up. /// @return entryOwner Owner of the slot (zero if empty/relinquished). /// @return joinedAt Timestamp the entry was enqueued (only meaningful when @@ -430,18 +433,18 @@ interface IDotnsPopController is IDotnsController { view returns (UserReservation memory reservation); - /// @notice Returns the base label a reservation queue is keyed under. + /// @notice Returns the personhood name a reservation queue is keyed under. /// @dev 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`. - /// @param labelhash Keccak-256 of the base label. - /// @return baseLabel The base label string, or empty when unknown. - function reservedBaseLabelOf(bytes32 labelhash) external view returns (string memory baseLabel); + /// @param labelhash Keccak-256 of the personhood name. + /// @return label The personhood name, or empty when unknown. + function reservedLabelOf(bytes32 labelhash) external view returns (string memory label); - /// @notice Returns the window, in seconds, after which a queue or pending-claim entry lapses. - /// @dev Governance-configurable via @custom:function setReservationDuration. Read by the lens - /// to compute each pending claim's settlement deadline. + /// @notice Returns the window, in seconds, after which a reservation-queue entry lapses. + /// @dev Governance-configurable via @custom:function setReservationDuration. Pending claims do + /// not lapse. /// @return duration Reservation duration in seconds. function reservationDuration() external view returns (uint64 duration); @@ -483,8 +486,8 @@ interface IDotnsPopController is IDotnsController { /// @notice Returns a paginated slice of a user's pending claims in queue order. /// @dev 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 + /// 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. /// @param user Account whose pending claims are read. /// @param offset Start index into the queue. diff --git a/contracts/registrars/IDotnsPopControllerLegacy.sol b/contracts/registrars/IDotnsPopControllerLegacy.sol new file mode 100644 index 000000000..727ed563f --- /dev/null +++ b/contracts/registrars/IDotnsPopControllerLegacy.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: © 2026 Parity Technologies +pragma solidity ^0.8.34; + +import {IDotnsPopController} from "./IDotnsPopController.sol"; + +/// @title IDotnsPopControllerLegacy +/// @notice Deprecated PoP controller entrypoints, kept with their original selectors for callers +/// bound to them. +/// @dev Each function forwards to its replacement on @custom:contract IDotnsPopController and has +/// the same effect. They take the replacement's struct types: a selector hashes only the function +/// name and the tuple shape, so the selectors are unchanged. Label validation reached through these +/// entrypoints reverts with @custom:reverts InvalidLiteLabel and @custom:reverts InvalidBaseLabel, +/// the errors their callers decode; every other error is shared with the replacements. +/// @custom:security-contact admin@parity.io +interface IDotnsPopControllerLegacy { + /// @notice Thrown on the legacy entrypoints where the replacements throw + /// @custom:reverts InvalidDeviceLabel. + error InvalidLiteLabel(); + + /// @notice Thrown on the legacy entrypoints where the replacements throw + /// @custom:reverts InvalidPersonhoodLabel. + error InvalidBaseLabel(); + + /// @notice Deprecated: use @custom:function IDotnsPopController.issueDeviceName. + /// @dev Selector `reserveLiteName((string,address,bytes))`. + /// @param params Issuance request; see @custom:struct IDotnsPopController.DeviceNameIssuance. + function reserveLiteName(IDotnsPopController.DeviceNameIssuance calldata params) external; + + /// @notice Deprecated: use @custom:function IDotnsPopController.issueDeviceNameWithReservation. + /// @dev Selector `reserveBaseName(((string,address,bytes),string))`. + /// @param params Issuance and reservation request; see + /// @custom:struct IDotnsPopController.DeviceNameIssuanceWithReservation. + function reserveBaseName(IDotnsPopController.DeviceNameIssuanceWithReservation calldata params) + external; + + /// @notice Deprecated: use @custom:function IDotnsPopController.issuePersonhoodName. + /// @dev Selector `registerBaseName((string,address,(uint8,string,bytes)))`. + /// @param params Issuance request; see + /// @custom:struct IDotnsPopController.PersonhoodNameIssuance. + function registerBaseName(IDotnsPopController.PersonhoodNameIssuance calldata params) external; +} diff --git a/contracts/registrars/IDotnsPopLens.sol b/contracts/registrars/IDotnsPopLens.sol index d31149654..a231efbdb 100644 --- a/contracts/registrars/IDotnsPopLens.sol +++ b/contracts/registrars/IDotnsPopLens.sol @@ -9,65 +9,66 @@ import {IPopRules} from "../pop/IPopRules.sol"; /// the store factory, the PoP resolver, and PopRules. /// @dev Holds no state of its own beyond the protocol registry it resolves siblings through, and /// takes no part in issuance. It exists so the query surface lives outside the controller, which -/// keeps the controller within the contract-size limit and keeps ownership on the registrar. +/// keeps the controller within the contract-size limit. Ownership is read from the registry, which +/// covers device-name subnames and tokenised names alike. /// @custom:security-contact admin@parity.io interface IDotnsPopLens { /// @notice One row in a per-account name listing: the name and the node used to look it up. /// @dev Computed on read; not stored. `settled` is false while the name still sits in the /// temporary pending-claim queue and true once its label is written into a `LabelStore`. - /// `deadline` is the pending settlement deadline (`mintedAt + reservationDuration`) and is - /// zero for a settled name. /// @param node namehash of the name; the key for chat-key, link, and detail lookups. - /// @param label Full name string. + /// @param label Bare label without the TLD; a device name carries its separator. /// @param settled Whether the label is written into a `LabelStore`. - /// @param deadline Pending settlement deadline, or zero when settled. struct Name { bytes32 node; string label; bool settled; - uint64 deadline; } - /// @notice The full on-chain record for a single name, gathered from the registrar, the PoP - /// resolver, and PopRules in one read. + /// @notice The full on-chain record for a single name, gathered from the registry, the + /// registrar, the PoP resolver, and PopRules in one read. /// @dev Computed on read; not stored. Never reverts on an unminted or unsettled name: absent - /// fields read as zero or empty. `tier` classifies the label shape (the tier the name - /// requires), not the owner's personhood. `fullClaim` is keyed by the lite labelhash, which - /// cannot be recovered from a node alone, so it is populated by @custom:function nameDetail - /// and left zero by @custom:function nameDetailByNode unless the label is independently - /// resolvable. + /// fields read as zero or empty. `requiredTier` classifies the label shape (the tier the name + /// requires), not the owner's proof. `personhoodNode` is looked up by the device-name + /// labelhash, which cannot be recovered from a node alone, so it is populated by + /// @custom:function nameDetail and left zero by @custom:function nameDetailByNode unless the + /// label is independently resolvable. /// @param node namehash of the name. - /// @param label Full name string, or empty when the name is unminted or its claim is unsettled. - /// @param owner Current registrar owner, or the zero address when the name does not exist. - /// @param exists Whether the name is minted. + /// @param label Bare label without the TLD. Empty when the name has no owner, or when + /// @custom:function nameDetailByNode reads a name whose claim is unsettled. + /// @param owner Current owner in the registry, or the zero address when the name has none. + /// @param exists Whether the name has an owner in the registry. /// @param settled Whether the label is written into the current owner's `LabelStore`. - /// @param tier PopRules classification of the label. + /// @param requiredTier PopRules classification of the label. Classification needs the label, so + /// this reads `NoStatus` whenever `label` is empty: for a name with no owner, and for an + /// unsettled claim read through @custom:function nameDetailByNode. /// @param chatKey Chat-key bytes recorded on the PoP resolver for the node. - /// @param liteLink For a full name, the linked lite labelhash; zero otherwise. - /// @param fullClaim For a lite name, the promoted full node; zero otherwise or when - /// unresolvable from a node. + /// @param deviceLabelhash For a personhood name, the linked device-name labelhash; zero + /// otherwise. + /// @param personhoodNode For a device name, the linked personhood-name node; zero otherwise or + /// when unresolvable from a node. struct NameDetail { bytes32 node; string label; address owner; bool exists; bool settled; - IPopRules.PopStatus tier; + IPopRules.PopStatus requiredTier; bytes chatKey; - bytes32 liteLink; - bytes32 fullClaim; + bytes32 deviceLabelhash; + bytes32 personhoodNode; } /// @notice An account-level summary of PoP state, gathered in one read. - /// @dev Computed on read; not stored, and never reverts. Name counts are excluded because - /// counting scans the account's holdings; read them with @custom:function liteNameCountOf and - /// @custom:function fullNameCountOf when required. The account's personhood tier is read - /// separately via @custom:function IPopRules.personhoodOf, which consults the personhood - /// precompile and so does not belong in this precompile-free summary. + /// @dev Computed on read; not stored, and never reverts. The name count is excluded because + /// counting scans the account's holdings; read it with @custom:function nameCountOf when + /// required. The account's proof is read separately via @custom:function + /// IPopRules.popStatusOf, which consults the personhood precompile and so does not belong in + /// this precompile-free summary. /// @param hasLabelStore Whether the account has a deployed `LabelStore`. /// @param pendingClaimCount Number of claims still staged in the pending queue. - /// @param reservationLabelhash The base label the account holds a live reservation on, or - /// zero when none. + /// @param reservationLabelhash The personhood name the account holds a live reservation on, + /// or zero when none. struct PopProfile { bool hasLabelStore; uint256 pendingClaimCount; @@ -78,28 +79,28 @@ interface IDotnsPopLens { /// @return registry The protocol registry address. function protocolRegistry() external view returns (address registry); - /// @notice Lists the lite-person names currently owned by `user`. - /// @dev Reads the user's `LabelStore` labels and pending claims, keeps the gateway-issued - /// ones carrying a separator, and re-checks each against `registrar.ownerOf` so a name - /// transferred away - /// drops out and a name transferred in shows under its current owner. Ordering follows the - /// store then the pending queue. An `offset` past the end returns an empty array rather than - /// reverting, and a short return means the slice ended. A gateway name transferred before it - /// settles has its label in no store, so it cannot appear here and is reachable only by node - /// via @custom:function nameDetailByNode. Gas grows with the account's holdings, so call it + /// @notice Lists the gateway-issued names currently owned by `user`: device names and + /// personhood names together. + /// @dev Reads the user's `LabelStore` labels and pending claims, keeps the ones the gateway + /// issued, and re-checks each against the registry owner so a name transferred away drops out + /// and a name transferred in shows under its current owner. Ordering follows the store then + /// the pending queue. An `offset` past the end returns an empty array rather than reverting, + /// and a short return means the slice ended. A gateway name transferred before it settles has + /// its label in no store, so it cannot appear here and is reachable only by node via + /// @custom:function nameDetailByNode. Gas grows with the account's holdings, so call it /// off-chain. A page holds at most `DotnsConstants.MAX_PAGE_SIZE` entries, and the pending /// portion covers up to that many staged claims. /// A name is listed only when @custom:function IDotnsPopController.isPopIssued confirms the - /// gateway minted it, so a name that merely resembles a lite label is not listed: neither a - /// public registration spelled `joseph42` nor a subname stored as `joseph.42`. Among issued - /// names the separator is what marks a lite one, since provenance covers full-person names - /// too. Identities minted before provenance was recorded have none to confirm, so they are - /// not listed and are re-issued through the gateway. - /// @param user Account whose lite names are listed. + /// gateway minted it, so a public registration is never listed, even one spelled `joseph42`, + /// and neither is a subname stored as `joseph.42` outside the gateway. Among issued names the + /// label shape tells the kinds apart: a device name carries its separator and a personhood + /// name doesn't. Identities minted before provenance was recorded have none to confirm, so + /// they are not listed and are re-issued through the gateway. + /// @param user Account whose names are listed. /// @param offset Start index into the filtered sequence. /// @param limit Maximum entries to return. - /// @return names Page of the account's lite names; see @custom:struct Name. - function liteNamesOf( + /// @return names Page of the account's gateway-issued names; see @custom:struct Name. + function namesOf( address user, uint256 offset, uint256 limit @@ -108,60 +109,33 @@ interface IDotnsPopLens { view returns (Name[] memory names); - /// @notice Lists the full-person names currently owned by `user`. - /// @dev Same ownership-verified read as @custom:function liteNamesOf, and the same - /// provenance requirement: a name is listed only when the gateway minted it. It keeps the - /// labels without a separator, which is the form the gateway issues a full-person name in. - /// A public registration is not an identity and appears in neither listing, so the two - /// together cover what the gateway issued rather than everything the account holds. - /// @param user Account whose full names are listed. - /// @param offset Start index into the filtered sequence. - /// @param limit Maximum entries to return. - /// @return names Page of the account's full names; see @custom:struct Name. - function fullNamesOf( - address user, - uint256 offset, - uint256 limit - ) - external - view - returns (Name[] memory names); - - /// @notice Counts the lite-person names currently owned by `user`. - /// @dev Uses the same ownership-verified read as @custom:function liteNamesOf; counting scans - /// the account's holdings, so gas grows with them. Call it off-chain. - /// @param user Account whose lite names are counted. - /// @return count Number of lite names currently owned. - function liteNameCountOf(address user) external view returns (uint256 count); - - /// @notice Counts the full-person names currently owned by `user`. - /// @dev Uses the same ownership-verified read as @custom:function fullNamesOf; counting scans - /// the account's holdings, so gas grows with them. Call it off-chain. - /// @param user Account whose full names are counted. - /// @return count Number of full names currently owned. - function fullNameCountOf(address user) external view returns (uint256 count); + /// @notice Counts the gateway-issued names currently owned by `user`. + /// @dev Uses the same ownership-verified read as @custom:function namesOf; counting scans the + /// account's holdings, so gas grows with them. Call it off-chain. + /// @param user Account whose names are counted. + /// @return count Number of gateway-issued names currently owned. + function nameCountOf(address user) external view returns (uint256 count); /// @notice Returns the full on-chain record for a name given its label string. /// @dev Resolves the node internally, so a caller holding only the string needs no namehash /// implementation. Never reverts on an unknown name: absent fields read as zero or empty. - /// This overload can populate `fullClaim` because it holds the label and so its labelhash. - /// @param name Bare label without the TLD. A lite label carries its separator and resolves - /// here too, since the node is the hash of the whole string. + /// This overload can populate `personhoodNode` because it holds the label and so its labelhash. + /// @param name Bare label without the TLD. A device name carries its separator and resolves + /// here too, to its stem beneath its numeric container. /// @return detail The name's record; see @custom:struct NameDetail. function nameDetail(string calldata name) external view returns (NameDetail memory detail); /// @notice Returns the full on-chain record for a name given its node. - /// @dev The node cannot be inverted to its labelhash, so `fullClaim` is populated only when - /// the label is independently resolvable from the node and reads zero otherwise; every other - /// field is resolved directly. Never reverts on an unknown node. + /// @dev The node cannot be inverted to its labelhash, so `personhoodNode` is populated only + /// when the label is independently resolvable from the node and reads zero otherwise; every + /// other field is resolved directly. Never reverts on an unknown node. /// @param node namehash of the name. /// @return detail The name's record; see @custom:struct NameDetail. function nameDetailByNode(bytes32 node) external view returns (NameDetail memory detail); /// @notice Returns an account-level summary of a user's PoP state. - /// @dev O(1) facts only; lite and full name counts are read separately via - /// @custom:function liteNameCountOf and @custom:function fullNameCountOf because those scan - /// the account's holdings. Never reverts. + /// @dev O(1) facts only; the name count is read separately via @custom:function nameCountOf + /// because it scans the account's holdings. Never reverts. /// @param user Account being summarised. /// @return profile The account summary; see @custom:struct PopProfile. function profileOf(address user) external view returns (PopProfile memory profile); diff --git a/contracts/registrars/IDotnsRegistrar.sol b/contracts/registrars/IDotnsRegistrar.sol index 1a41645b9..97b337aa0 100644 --- a/contracts/registrars/IDotnsRegistrar.sol +++ b/contracts/registrars/IDotnsRegistrar.sol @@ -5,8 +5,8 @@ pragma solidity ^0.8.34; import {IERC721} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; import {IDotnsController} from "./IDotnsController.sol"; -/// @title Dotns Registrar -/// @notice ERC721-backed ownership for DotNS names with controller-gated registration. +/// @title dotNS Registrar +/// @notice ERC721-backed ownership for dotNS names with controller-gated registration. /// @dev Intentionally minimal and policy-free. Provides ERC721 ownership for registered name /// token IDs and controller-gated registration; pricing, PoP enforcement, and flow-specific /// policy live in the controllers. @@ -43,7 +43,7 @@ interface IDotnsRegistrar is IERC721 { error InvalidLabel(); /// @notice Thrown when a transfer or a transfer-fee quote targets a soulbound name. - /// @dev Soulbound names are minted through the PoP gateway and are permanently + /// @dev Soulbound names are minted through the dotNS gateway pallet and are permanently /// non-transferable. Raised by the `_update` transfer gate and by /// @custom:function quoteTransferFee. error NameSoulbound(uint256 tokenId); @@ -109,7 +109,7 @@ interface IDotnsRegistrar is IERC721 { /// cleared. A `true` result means every transfer overload reverts with /// @custom:reverts NameSoulbound and @custom:function quoteTransferFee reverts likewise. /// @param tokenId The name's token id. - /// @return soulbound True when the name was minted through the PoP gateway. + /// @return soulbound True when the name was minted through the gateway pallet. function isSoulbound(uint256 tokenId) external view returns (bool soulbound); /// @notice Returns whether a given token id has been minted. @@ -117,7 +117,7 @@ interface IDotnsRegistrar is IERC721 { /// @notice Adds an authorised controller. /// @dev Typed against the baseline `IDotnsController` (not a concrete subtype) so a single - /// authorisation surface accepts every controller flavour (commit-reveal, PoP gateway, + /// authorisation surface accepts every controller flavour (commit-reveal, gateway path, /// future variants) without per-flavour setters. Owner-gated (otherwise /// @custom:reverts OwnableUnauthorizedAccount); emits @custom:emits ControllerAdded on /// success. diff --git a/contracts/registrars/IDotnsRegistrarController.sol b/contracts/registrars/IDotnsRegistrarController.sol index 6475ce13a..254d4913f 100644 --- a/contracts/registrars/IDotnsRegistrarController.sol +++ b/contracts/registrars/IDotnsRegistrarController.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.34; import {IDotnsController} from "./IDotnsController.sol"; import {IPopRules} from "../pop/IPopRules.sol"; -/// @title Dotns Registrar Controller +/// @title dotNS Registrar Controller /// @notice Interface for registering top-level labels using a commit reveal scheme. /// @dev Defines allocation only; forward resolution, reverse lookup, pricing mechanics, PoP /// validation, and store writing are handled by external contracts. Users commit a hash of @@ -151,14 +151,14 @@ interface IDotnsRegistrarController is IDotnsController { /// @custom:reverts CommitmentTooOld past `maxCommitmentAge`, and finally resolves the /// configured escrow address from the protocol registry (otherwise /// @custom:reverts EscrowNotConfigured). Splits on direct vs cross-payer at - /// `msg.sender == registration.owner`. The direct path runs `priceWithCheck` (personhood + /// `msg.sender == registration.owner`. The direct path runs `priceWithCheck` (PoP-status /// + reservation gate) and routes the charge to a refundable escrow deposit owned by - /// `registration.owner`. The cross-payer path skips the personhood revert in + /// `registration.owner`. The cross-payer path skips the PoP-status revert in /// `priceWithCheck` but applies it directly via @custom:reverts OwnerStatusInsufficient /// when the owner's recorded tier does not meet the label's required tier, and still /// rejects governance-reserved labels with @custom:reverts GovernanceReserved and live - /// cross-user stem reservations with @custom:reverts NameReserved. The cross-payer charge is - /// the owner-side registration price; the path applies no separate transfer friction. The + /// cross-user base-name reservations with @custom:reverts NameReserved. The cross-payer charge + /// is the owner-side registration price; the path applies no separate transfer friction. The /// charge routes to the escrow protocol fee pot while seeding a zero-amount deposit slot so /// the release lifecycle stays reachable. The reveal prices the name at the committed /// `pricingVersion`, so a model change between commit and reveal leaves the amount unchanged, diff --git a/contracts/registry/DotnsProtocolRegistry.sol b/contracts/registry/DotnsProtocolRegistry.sol index 3dc33d004..248754e36 100644 --- a/contracts/registry/DotnsProtocolRegistry.sol +++ b/contracts/registry/DotnsProtocolRegistry.sol @@ -12,9 +12,9 @@ import {IDotnsProtocolRegistry} from "./IDotnsProtocolRegistry.sol"; import {LabelUtils} from "../utils/LabelUtils.sol"; import {StringUtils} from "../utils/StringUtils.sol"; -/// @title Dotns Protocol Registry +/// @title dotNS Protocol Registry /// @author Parity -/// @notice Upgradeable address registry for all DotNS protocol contracts, and the authority for +/// @notice Upgradeable address registry for all dotNS protocol contracts, and the authority for /// the network's top-level domain. /// @dev Single source of truth for sibling-contract lookups. All siblings resolve each other via /// well-known `bytes32` constants in `DotnsConstants` rather than holding direct addresses, @@ -144,7 +144,7 @@ contract DotnsProtocolRegistry is } /// @notice Returns the declared release, mirroring `protocolVersion` under the historical - /// `version()` selector every DotNS contract exposes. + /// `version()` selector every dotNS contract exposes. /// @dev Sibling contracts mirror the same stored value by reading it from here, so /// `version()` answers identically network-wide; this contract is where the value /// lives, so it reads its own storage. diff --git a/contracts/registry/DotnsRegistry.sol b/contracts/registry/DotnsRegistry.sol index ab4f06bf0..092b9fdb6 100644 --- a/contracts/registry/DotnsRegistry.sol +++ b/contracts/registry/DotnsRegistry.sol @@ -17,7 +17,7 @@ import {IDotnsProtocolRegistry} from "./IDotnsProtocolRegistry.sol"; import {StringUtils} from "../utils/StringUtils.sol"; import {DotnsConstants} from "../utils/DotnsConstants.sol"; -/// @title Dotns Registry +/// @title dotNS Registry /// @author Parity /// @notice Upgradeable on-chain registry for hierarchical name ownership and resolution. /// @dev Tokenised second-level nodes store `owner == address(0)` as a sentinel and defer to @@ -30,7 +30,7 @@ contract DotnsRegistry is Initializable, UUPSUpgradeable, OwnableUpgradeable, ID /// @notice Mapping of node identifiers to records. mapping(bytes32 node => Record record) private records; - /// @notice Protocol-level address registry for all DotNS contracts. + /// @notice Protocol-level address registry for all dotNS contracts. IDotnsProtocolRegistry public protocolRegistry; uint256[50] private __gap; @@ -316,7 +316,7 @@ contract DotnsRegistry is Initializable, UUPSUpgradeable, OwnableUpgradeable, ID } /// @notice Returns the release this network declares it runs, read live from the protocol - /// registry so every DotNS contract reports one synchronised value. + /// registry so every dotNS contract reports one synchronised value. /// @dev 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 diff --git a/contracts/registry/IDotnsProtocolRegistry.sol b/contracts/registry/IDotnsProtocolRegistry.sol index 8e71c50a5..60c3f6641 100644 --- a/contracts/registry/IDotnsProtocolRegistry.sol +++ b/contracts/registry/IDotnsProtocolRegistry.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.34; /// @title IDotnsProtocolRegistry /// @author Parity -/// @notice Interface for the DotNS protocol-level address registry. +/// @notice Interface for the dotNS protocol-level address registry. /// @dev Single source of truth for sibling lookups. Contracts resolve each other via well-known /// `bytes32` constants in `DotnsConstants` so an upgrade or rewire only mutates the /// registry, never the consumers. The registry also holds the network's top-level domain, diff --git a/contracts/registry/IDotnsRegistry.sol b/contracts/registry/IDotnsRegistry.sol index 7265b9e63..27029b735 100644 --- a/contracts/registry/IDotnsRegistry.sol +++ b/contracts/registry/IDotnsRegistry.sol @@ -132,7 +132,7 @@ interface IDotnsRegistry { /// success. function setSubnodeResolver(SubnodeResolverRecord calldata record) external; - /// @notice Creates or resets a node record for a tokenised base registration. + /// @notice Creates or resets a node record for a tokenised second-level registration. /// @dev 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 diff --git a/contracts/resolvers/DotnsContentResolver.sol b/contracts/resolvers/DotnsContentResolver.sol index fe074ee50..00894f81a 100644 --- a/contracts/resolvers/DotnsContentResolver.sol +++ b/contracts/resolvers/DotnsContentResolver.sol @@ -16,7 +16,7 @@ import {IDotnsContentResolver} from "./IDotnsContentResolver.sol"; import {IDotnsProtocolRegistry} from "../registry/IDotnsProtocolRegistry.sol"; import {DotnsConstants} from "../utils/DotnsConstants.sol"; -/// @title Dotns Content Resolver +/// @title dotNS Content Resolver /// @notice Implements `IDotnsContentResolver` interface with content hash, text records, and /// operator approvals. /// @dev Writes are gated on the registry's authorisation for the node (owner or registrar-level @@ -41,7 +41,7 @@ contract DotnsContentResolver is /// @notice Store all approval mapping mapping(address owner => mapping(address operator => bool approved)) private operators; - /// @notice Protocol-level address registry for all DotNS contracts. + /// @notice Protocol-level address registry for all dotNS contracts. IDotnsProtocolRegistry public protocolRegistry; /// @dev Reserved storage space to allow for layout changes in the future. @@ -143,7 +143,7 @@ contract DotnsContentResolver is } /// @notice Returns the release this network declares it runs, read live from the protocol - /// registry so every DotNS contract reports one synchronised value. + /// registry so every dotNS contract reports one synchronised value. /// @dev 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 diff --git a/contracts/resolvers/DotnsPopResolver.sol b/contracts/resolvers/DotnsPopResolver.sol index 6daae5e55..00bd03a65 100644 --- a/contracts/resolvers/DotnsPopResolver.sol +++ b/contracts/resolvers/DotnsPopResolver.sol @@ -35,17 +35,19 @@ contract DotnsPopResolver is /// @notice Stored chat-key bytes keyed by node. mapping(bytes32 node => bytes chatKey) private _chatKeys; - /// @notice Stored lite-person labelhash keyed by full-person node. - /// @dev Forward direction (full => lite): maps a full-person node to the - /// labelhash of the lite username it was claimed from. - mapping(bytes32 fullNode => bytes32 liteLabelhash) private _liteLinks; - - /// @notice Reverse index mapping a lite labelhash to the full-person node - /// it was promoted to. - /// @dev Written alongside `_liteLinks` on every claim so consumers that look - /// up by lite username resolve the full name without scanning events. - /// Zero when the lite label has never been linked to a full claim. - mapping(bytes32 liteLabelhash => bytes32 fullNode) private _fullClaims; + /// @notice Stored device-name labelhash keyed by personhood-name node. + /// @dev Forward direction: maps a personhood-name node to the labelhash of the device name it + /// is linked to. + /// @custom:oz-renamed-from _liteLinks + mapping(bytes32 personhoodNode => bytes32 deviceLabelhash) private _deviceLinks; + + /// @notice Reverse index mapping a device-name labelhash to the personhood-name node it is + /// linked to. + /// @dev Written alongside `_deviceLinks` on every link so consumers that look up by device + /// name resolve the personhood name without scanning events. Zero when the device name + /// has never been linked. + /// @custom:oz-renamed-from _fullClaims + mapping(bytes32 deviceLabelhash => bytes32 personhoodNode) private _personhoodNodes; /// @dev Reserved storage space to allow for layout changes in the future. uint256[50] private __gap; @@ -97,25 +99,25 @@ contract DotnsPopResolver is } /// @inheritdoc IDotnsPopResolver - function setLiteLink( - bytes32 fullNode, - bytes32 liteLabelhash + function setDeviceLink( + bytes32 personhoodNode, + bytes32 deviceLabelhash ) external override onlyPopController { - bytes32 oldLite = _liteLinks[fullNode]; - bytes32 oldFull = _fullClaims[liteLabelhash]; - if (oldLite != bytes32(0) && oldLite != liteLabelhash) { - delete _fullClaims[oldLite]; + bytes32 oldDevice = _deviceLinks[personhoodNode]; + bytes32 oldPersonhood = _personhoodNodes[deviceLabelhash]; + if (oldDevice != bytes32(0) && oldDevice != deviceLabelhash) { + delete _personhoodNodes[oldDevice]; } - if (oldFull != bytes32(0) && oldFull != fullNode) { - delete _liteLinks[oldFull]; + if (oldPersonhood != bytes32(0) && oldPersonhood != personhoodNode) { + delete _deviceLinks[oldPersonhood]; } - _liteLinks[fullNode] = liteLabelhash; - _fullClaims[liteLabelhash] = fullNode; - emit LiteLinkUpdated(fullNode, liteLabelhash); + _deviceLinks[personhoodNode] = deviceLabelhash; + _personhoodNodes[deviceLabelhash] = personhoodNode; + emit DeviceLinkUpdated(personhoodNode, deviceLabelhash); } /// @inheritdoc IDotnsPopResolver @@ -124,17 +126,17 @@ contract DotnsPopResolver is } /// @inheritdoc IDotnsPopResolver - function liteLink(bytes32 fullNode) external view override returns (bytes32) { - return _liteLinks[fullNode]; + function deviceLabelhashOf(bytes32 personhoodNode) external view override returns (bytes32) { + return _deviceLinks[personhoodNode]; } /// @inheritdoc IDotnsPopResolver - function fullClaim(bytes32 liteLabelhash) external view override returns (bytes32) { - return _fullClaims[liteLabelhash]; + function personhoodNodeOf(bytes32 deviceLabelhash) external view override returns (bytes32) { + return _personhoodNodes[deviceLabelhash]; } /// @notice Returns the release this network declares it runs, read live from the protocol - /// registry so every DotNS contract reports one synchronised value. + /// registry so every dotNS contract reports one synchronised value. /// @dev 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 diff --git a/contracts/resolvers/DotnsResolver.sol b/contracts/resolvers/DotnsResolver.sol index 7f09a232b..b6dfb6839 100644 --- a/contracts/resolvers/DotnsResolver.sol +++ b/contracts/resolvers/DotnsResolver.sol @@ -16,8 +16,8 @@ import {IDotnsRegistry} from "../registry/IDotnsRegistry.sol"; import {IDotnsProtocolRegistry} from "../registry/IDotnsProtocolRegistry.sol"; import {DotnsConstants} from "../utils/DotnsConstants.sol"; -/// @title Dotns Resolver -/// @notice Stores forward-resolution address records for DotNS nodes +/// @title dotNS Resolver +/// @notice Stores forward-resolution address records for dotNS nodes /// @dev Writes are gated on node ownership in the forward registry, not on a /// privileged writer address. Address records describe where a name points /// and only the current node owner has the authority to set that target. @@ -29,7 +29,7 @@ contract DotnsResolver is ERC165Upgradeable, IDotnsResolver { - /// @notice Protocol-level address registry for all DotNS contracts. + /// @notice Protocol-level address registry for all dotNS contracts. IDotnsProtocolRegistry public protocolRegistry; /// @dev Reserved storage space to allow for layout changes in the future. @@ -97,7 +97,7 @@ contract DotnsResolver is } /// @notice Returns the release this network declares it runs, read live from the protocol - /// registry so every DotNS contract reports one synchronised value. + /// registry so every dotNS contract reports one synchronised value. /// @dev 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 diff --git a/contracts/resolvers/DotnsReverseResolver.sol b/contracts/resolvers/DotnsReverseResolver.sol index bf8bbb553..b197edd83 100644 --- a/contracts/resolvers/DotnsReverseResolver.sol +++ b/contracts/resolvers/DotnsReverseResolver.sol @@ -18,7 +18,7 @@ import {LabelUtils} from "../utils/LabelUtils.sol"; import {SubnodeUtils} from "../utils/SubnodeUtils.sol"; import {StringUtils} from "../utils/StringUtils.sol"; -/// @title Dotns Reverse Resolver +/// @title dotNS Reverse Resolver /// @notice Resolves an address to its associated name under the network TLD. /// @dev Writes are gated on a fixed writer address resolved from the protocol /// registry (the registrar or its controller), not on node ownership. @@ -36,7 +36,7 @@ contract DotnsReverseResolver is /// that no reverse name is set. mapping(address owner => string name) private reverseNames; - /// @notice Protocol-level address registry for all DotNS contracts. + /// @notice Protocol-level address registry for all dotNS contracts. IDotnsProtocolRegistry public protocolRegistry; /// @dev Reserved storage space to allow for layout changes in the future. @@ -103,8 +103,8 @@ contract DotnsReverseResolver is return stored; } - /// @notice Resolves the node a name maps to, whether tokenised or a lite subname. - /// @dev A lite name is `stem` beneath its numeric container, so it hashes as a subnode; any + /// @notice Resolves the node a name maps to, whether tokenised or a device-name subname. + /// @dev A device name is `stem` beneath its numeric container, so it hashes as a subnode; any /// other name hashes as a second-level label under the TLD. Ownership of either is read /// through the registry, which delegates a tokenised name to the registrar and holds a /// subname directly. @@ -112,8 +112,8 @@ contract DotnsReverseResolver is /// @return node The node the name resolves to. function _nodeOf(string memory label) internal view returns (bytes32 node) { bytes32 tldNode = protocolRegistry.tldNode(); - if (StringUtils.isLitePersonLabelMemory(label)) { - return SubnodeUtils.liteSubnodeOf(tldNode, label); + if (StringUtils.isDeviceLabelMemory(label)) { + return SubnodeUtils.deviceSubnodeOf(tldNode, label); } node = LabelUtils.namehashUnder(tldNode, LabelUtils.labelhashMemory(label)); } @@ -144,7 +144,7 @@ contract DotnsReverseResolver is } /// @notice Returns the release this network declares it runs, read live from the protocol - /// registry so every DotNS contract reports one synchronised value. + /// registry so every dotNS contract reports one synchronised value. /// @dev 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 diff --git a/contracts/resolvers/IDotnsContentResolver.sol b/contracts/resolvers/IDotnsContentResolver.sol index 4022a792d..54c9cc5ce 100644 --- a/contracts/resolvers/IDotnsContentResolver.sol +++ b/contracts/resolvers/IDotnsContentResolver.sol @@ -2,9 +2,9 @@ // SPDX-FileCopyrightText: © 2026 Parity Technologies pragma solidity ^0.8.34; -/// @title Dotns Content Resolver +/// @title dotNS Content Resolver /// @notice Defines storage and retrieval for content hash, text records, and operator approvals for -/// DotNS nodes. @dev Content hash and text records point to off-chain content such as IPFS CIDs or +/// dotNS nodes. @dev Content hash and text records point to off-chain content such as IPFS CIDs or /// future schemes; interpretation is handled off-chain. Operator approvals allow /// third parties to manage records on behalf of the owner. /// @custom:security-contact admin@parity.io @@ -32,7 +32,7 @@ interface IDotnsContentResolver { error NotAuthorised(bytes32 node, address caller); /// @notice Sets the content hash for a node. - /// @dev The caller must own the node in the DotNS registry or be an approved operator, + /// @dev The caller must own the node in the dotNS registry or be an approved operator, /// otherwise @custom:reverts NotAuthorised. Content hashes are opaque bytes (e.g. an /// IPFS CID); the resolver stores them as-is and never interprets the payload. Emits /// @custom:emits ContentHashUpdated on every successful write. @@ -46,7 +46,7 @@ interface IDotnsContentResolver { function contenthash(bytes32 node) external view returns (bytes memory hash); /// @notice Sets a text record for a node. - /// @dev The caller must own the node in the DotNS registry or be an approved operator, + /// @dev The caller must own the node in the dotNS registry or be an approved operator, /// otherwise @custom:reverts NotAuthorised. Text records are arbitrary key/value strings /// (e.g. `avatar`, `url`, `description`). Emits @custom:emits TextUpdated on every /// successful write. diff --git a/contracts/resolvers/IDotnsPopResolver.sol b/contracts/resolvers/IDotnsPopResolver.sol index 52c37ec9b..5843223c2 100644 --- a/contracts/resolvers/IDotnsPopResolver.sol +++ b/contracts/resolvers/IDotnsPopResolver.sol @@ -3,14 +3,14 @@ pragma solidity ^0.8.34; /// @title IDotnsPopResolver -/// @notice Resolver for per-name records produced by the PoP username flow. +/// @notice Resolver for per-name records produced by the dotNS gateway pallet. /// @dev Holds three record kinds: /// - Chat key: ECDH public-key bytes used for end-to-end encrypted messaging. -/// - Lite link: for a full-person node, the labelhash of the lite-person -/// username it was minted from (when the link was made). -/// - Full claim: reverse index mapping a lite labelhash to the full-person -/// node it was promoted to. Mirrors `liteLink` on every write so a -/// caller that holds a lite labelhash can resolve the full-person node +/// - Device link (`deviceLabelhashOf`): for a personhood-name node, the labelhash of the +/// device name it was linked to when it was issued. +/// - Personhood link (`personhoodNodeOf`): reverse index mapping a device-name labelhash to +/// the personhood-name node it is linked to. Mirrors `deviceLabelhashOf` on every write so a +/// caller that holds a device-name labelhash can resolve the personhood name /// without scanning events. /// /// Lives separately from the per-user `LabelStore` so that the store can remain @@ -29,10 +29,10 @@ interface IDotnsPopResolver { /// @param chatKey The new chat key bytes. event ChatKeyUpdated(bytes32 indexed node, bytes chatKey); - /// @notice Emitted when a full-person node's lite link is set or updated. - /// @param fullNode The full-person node carrying the link. - /// @param liteLabelhash The labelhash of the linked lite-person username. - event LiteLinkUpdated(bytes32 indexed fullNode, bytes32 indexed liteLabelhash); + /// @notice Emitted when a personhood name's device link is set or updated. + /// @param personhoodNode The personhood-name node carrying the link. + /// @param deviceLabelhash The labelhash of the linked device name. + event DeviceLinkUpdated(bytes32 indexed personhoodNode, bytes32 indexed deviceLabelhash); /// @notice Thrown when the caller is not the authorised PoP controller. /// @param caller The address that attempted the write. @@ -53,35 +53,40 @@ interface IDotnsPopResolver { /// @param chatKey ECDH public key bytes (pallet-side type is `[u8; 65]`). function setChatKey(bytes32 node, bytes calldata chatKey) external; - /// @notice Sets the lite-person link for a full-person `node`. + /// @notice Sets the device link for a personhood-name node. /// @dev Callable only by the authorised PoP controller, otherwise /// @custom:reverts NotPopController. Overwrites any previous link. When overwriting, the - /// stale inverse entry is nulled so both the forward (`liteLink`) and reverse - /// (`fullClaim`) indices remain consistent: re-linking the same `fullNode` to a new - /// `liteLabelhash` clears `fullClaim(oldLite)`, and re-linking the same `liteLabelhash` - /// to a new `fullNode` clears `liteLink(oldFull)`. The invariant - /// `fullClaim(liteLink(node)) == node` always holds after the call. Emits - /// @custom:emits LiteLinkUpdated on every successful write. - /// @param fullNode The full-person node carrying the link. - /// @param liteLabelhash The labelhash of the linked lite-person username. - function setLiteLink(bytes32 fullNode, bytes32 liteLabelhash) external; + /// stale inverse entry is nulled so both the forward (`deviceLabelhashOf`) and reverse + /// (`personhoodNodeOf`) indices remain consistent: re-linking the same `personhoodNode` to + /// a new `deviceLabelhash` clears `personhoodNodeOf(oldDevice)`, and re-linking the same + /// `deviceLabelhash` to a new `personhoodNode` clears `deviceLabelhashOf(oldPersonhood)`. + /// The invariant `personhoodNodeOf(deviceLabelhashOf(node)) == node` always holds after + /// the call. Emits @custom:emits DeviceLinkUpdated on every successful write. + /// @param personhoodNode The personhood-name node carrying the link. + /// @param deviceLabelhash The labelhash of the linked device name. + function setDeviceLink(bytes32 personhoodNode, bytes32 deviceLabelhash) external; /// @notice Returns the chat key associated with a node. /// @param node The node to query. /// @return chatKey The stored chat key bytes, or empty if unset. function chatKey(bytes32 node) external view returns (bytes memory chatKey); - /// @notice Returns the lite-person labelhash linked to a full-person node. - /// @param fullNode The full-person node to query. - /// @return liteLabelhash The linked lite-person labelhash, or zero if unset. - function liteLink(bytes32 fullNode) external view returns (bytes32 liteLabelhash); + /// @notice Returns the device-name labelhash linked to a personhood-name node. + /// @param personhoodNode The personhood-name node to query. + /// @return deviceLabelhash The linked device-name labelhash, or zero if unset. + function deviceLabelhashOf(bytes32 personhoodNode) + external + view + returns (bytes32 deviceLabelhash); - /// @notice Returns the full-person node a given lite label has claimed. - /// @dev Reverse of @custom:function liteLink. Written by the same `setLiteLink` call so the - /// two directions stay in lockstep. Returns zero when the lite label - /// has never been linked to a full claim. - /// @param liteLabelhash The labelhash of the lite-person username to query. - /// @return fullNode The full-person node claimed from this lite label, or - /// zero if unset. - function fullClaim(bytes32 liteLabelhash) external view returns (bytes32 fullNode); + /// @notice Returns the personhood-name node a device name is linked to. + /// @dev Reverse of @custom:function deviceLabelhashOf. Written by the same `setDeviceLink` call + /// so the two directions stay in lockstep. Returns zero when the device name has never + /// been linked to a personhood name. + /// @param deviceLabelhash The labelhash of the device name to query. + /// @return personhoodNode The linked personhood-name node, or zero if unset. + function personhoodNodeOf(bytes32 deviceLabelhash) + external + view + returns (bytes32 personhoodNode); } diff --git a/contracts/resolvers/IDotnsResolver.sol b/contracts/resolvers/IDotnsResolver.sol index 90a29a556..ccde119c9 100644 --- a/contracts/resolvers/IDotnsResolver.sol +++ b/contracts/resolvers/IDotnsResolver.sol @@ -2,8 +2,8 @@ // SPDX-FileCopyrightText: © 2026 Parity Technologies pragma solidity ^0.8.34; -/// @title Dotns Resolver -/// @notice Defines forward-resolution address records for DotNS nodes. +/// @title dotNS Resolver +/// @notice Defines forward-resolution address records for dotNS nodes. /// @dev Forward-address records describe where a name points. Authority therefore /// follows node ownership in the forward registry, not a privileged writer. /// @custom:security-contact admin@parity.io diff --git a/contracts/resolvers/IDotnsReverseResolver.sol b/contracts/resolvers/IDotnsReverseResolver.sol index b52362d65..99bded638 100644 --- a/contracts/resolvers/IDotnsReverseResolver.sol +++ b/contracts/resolvers/IDotnsReverseResolver.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: © 2026 Parity Technologies pragma solidity ^0.8.34; -/// @title Dotns Reverse Resolver +/// @title dotNS Reverse Resolver /// @notice Interface for writing and reading reverse name records for addresses. /// @dev Reverse records bind to an EOA rather than a registry node. Two write paths exist: /// a registrar-only setter used by the controller during reserved registration, and a @@ -18,7 +18,7 @@ interface IDotnsReverseResolver { /// @notice Thrown when a caller attempts to claim a reverse record for a name they do not own. /// @param caller The address attempting the claim. /// @param node The claimed name's node: a token id for a tokenised name, and the - /// stem-under-container subnode for a lite name. + /// stem-under-container subnode for a device name. error NotNameOwner(address caller, uint256 node); /// @notice Emitted when a name is associated with an address. @@ -36,7 +36,7 @@ interface IDotnsReverseResolver { /// @notice Self-service claim: associates `msg.sender` with `