From 59a6c0a6811a8994b2145861edcedbea30d2f323 Mon Sep 17 00:00:00 2001 From: giuseppere Date: Mon, 28 Sep 2026 15:06:48 +0200 Subject: [PATCH 1/6] feat: rename lite/full to device and personhood names, with legacy gateway entrypoints --- .github/abi-contracts.txt | 1 + CONTRIBUTING.md | 8 +- KNOWN_ISSUES.md | 4 +- README.md | 89 +- contracts/escrow/DotnsNameEscrow.sol | 4 +- contracts/pop/DotnsScarcityPricing.sol | 5 +- contracts/pop/IDotnsCostModelRegistry.sol | 6 +- contracts/pop/IDotnsPricing.sol | 5 +- contracts/pop/IPopRules.sol | 132 +- contracts/pop/PopRules.sol | 188 +- contracts/registrars/DotnsPopController.sol | 419 +++-- contracts/registrars/DotnsPopLens.sol | 103 +- contracts/registrars/DotnsRegistrar.sol | 10 +- .../registrars/DotnsRegistrarController.sol | 12 +- contracts/registrars/IDotnsController.sol | 2 +- contracts/registrars/IDotnsPopController.sol | 382 ++-- .../registrars/IDotnsPopControllerLegacy.sol | 42 + contracts/registrars/IDotnsPopLens.sol | 127 +- contracts/registrars/IDotnsRegistrar.sol | 6 +- .../registrars/IDotnsRegistrarController.sol | 4 +- contracts/resolvers/DotnsPopResolver.sol | 56 +- contracts/resolvers/DotnsReverseResolver.sol | 12 +- contracts/resolvers/IDotnsPopResolver.sol | 63 +- contracts/resolvers/IDotnsReverseResolver.sol | 4 +- contracts/store/ILabelStore.sol | 4 +- contracts/store/LabelStore.sol | 8 +- contracts/utils/DotnsConstants.sol | 17 +- contracts/utils/RegistrationUtils.sol | 2 +- contracts/utils/StringUtils.sol | 76 +- contracts/utils/SubnodeUtils.sol | 20 +- scripts/deploy/DeployPopSystem.s.sol | 4 +- test/base/BaseDotns.t.sol | 200 ++- test/fuzz/pop/PopFuzz.t.sol | 24 +- .../registrar/DotnsPopControllerFuzz.t.sol | 79 +- .../DotnsRegistrarControllerFuzz.t.sol | 24 +- test/intergration/BasicDotns.reverts.t.sol | 18 +- test/intergration/BasicDotns.t.sol | 14 +- test/intergration/PopLifecycleFlow.t.sol | 230 +-- test/intergration/StoreIntegration.t.sol | 8 +- test/invariant/escrow/EscrowHandler.t.sol | 20 +- .../DotnsPopControllerInvariant.t.sol | 113 +- .../DotnsRegistrarControllerInvariant.t.sol | 8 +- .../registrar/PopControllerHandler.t.sol | 225 +-- .../RegistrarControllerHandler.t.sol | 8 +- .../registry/DotnsRegistryInvariant.t.sol | 6 +- test/invariant/registry/RegistryHandler.t.sol | 4 +- test/unit/escrow/DotnsNameEscrow.t.sol | 36 +- test/unit/escrow/DotnsNameEscrowRedeem.t.sol | 10 +- test/unit/pop/PopRules.t.sol | 76 +- test/unit/pop/PopRulesClassification.t.sol | 73 +- test/unit/registrar/DotnsPopController.t.sol | 1596 +++++++++-------- .../registrar/DotnsPopControllerLegacy.t.sol | 666 +++++++ test/unit/registrar/DotnsRegistrar.t.sol | 8 +- .../registrar/DotnsRegistrarController.t.sol | 41 +- .../DotnsRegistrarControllerLifecycle.t.sol | 20 +- .../DotnsProtocolRegistryDeclarations.t.sol | 2 +- test/unit/registry/DotnsRegistry.t.sol | 10 +- test/unit/resolver/DotnsPopResolver.t.sol | 206 +-- test/unit/resolver/DotnsReverseResolver.t.sol | 18 +- test/unit/utils/StringUtils.t.sol | 122 +- 60 files changed, 3317 insertions(+), 2363 deletions(-) create mode 100644 contracts/registrars/IDotnsPopControllerLegacy.sol create mode 100644 test/unit/registrar/DotnsPopControllerLegacy.t.sol 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/CONTRIBUTING.md b/CONTRIBUTING.md index 12c1b6897..d97b05d59 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 => `personhoodLink(deviceLabelhash)` | +| Personhood-name node => device-name labelhash | Protocol registry => PoP resolver => `deviceLink(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)` | +| Base 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 | diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 7980a4d1f..308c3b05a 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -13,7 +13,7 @@ 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` 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. **Workaround:** `claimLabelStore` (user-signed) settles the whole pending pile and deploys the store. @@ -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..40dbcc3b5 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ 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. @@ -147,21 +147,36 @@ 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`. Labels classified as governance-reserved revert with `GovernanceReserved`; a label whose base name another user holds 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. ### 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 deviceLink (personhood => device) and personhoodLink (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 @@ -171,9 +186,9 @@ Deferred settlement has no transfer-pricing consequence, because gateway-issued ### 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 @@ -185,9 +200,9 @@ The registry exposes isAuthorised(node, account) as the canonical check for whet ### 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,27 +211,27 @@ 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 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 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. @@ -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 has a second write path, gated on a Devicehood price; since Devicehood is the separated form and the public path cannot submit one, that path never fires. -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,7 +271,7 @@ 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 that inherits from a prior device name, and is what gives verified users an on-chain discovery channel for end-to-end encrypted messaging. The device link (`deviceLink`) answers "which device name is this personhood name linked to?" and is keyed by the personhood-name node. The personhood link (`personhoodLink`) is the reverse direction: it answers "which personhood name is this device name linked to?" and is keyed by the device-name labelhash, the hash of the full `stem.NN` text (see [Identifiers](#identifiers)). 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. @@ -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/contracts/escrow/DotnsNameEscrow.sol b/contracts/escrow/DotnsNameEscrow.sol index 2ddb0c8d8..252bae884 100644 --- a/contracts/escrow/DotnsNameEscrow.sol +++ b/contracts/escrow/DotnsNameEscrow.sol @@ -460,8 +460,8 @@ 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 + // the holder's right to recover their own name for no consideration at all. Free Personhood + // and Devicehood 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. if (owed == 0) return; diff --git a/contracts/pop/DotnsScarcityPricing.sol b/contracts/pop/DotnsScarcityPricing.sol index f0d718697..76be9ea67 100644 --- a/contracts/pop/DotnsScarcityPricing.sol +++ b/contracts/pop/DotnsScarcityPricing.sol @@ -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..1b4c272b5 100644 --- a/contracts/pop/IDotnsCostModelRegistry.sol +++ b/contracts/pop/IDotnsCostModelRegistry.sol @@ -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..b66ca392e 100644 --- a/contracts/pop/IDotnsPricing.sol +++ b/contracts/pop/IDotnsPricing.sol @@ -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..c3aee2b65 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 @@ -100,7 +104,7 @@ interface IPopRules { /// @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 + /// ASCII DNS label nor a device name triggers @custom:reverts PopError; a trailing-digit /// suffix of any length is accepted and classified by the length it leaves. /// @param name The name label being evaluated. /// @return requirement Required tier for registration. @@ -112,8 +116,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 +130,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 device-name 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 +188,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 +209,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 +236,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 +274,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 +319,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 +334,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 +345,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..e348f3395 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,7 +45,7 @@ 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. @@ -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 baseName 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") ); } @@ -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 baseName 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 baseName 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 baseName 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..32f22595d 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; @@ -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 @@ -191,79 +193,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 +352,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 +467,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 +482,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 +600,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,6 +617,7 @@ contract DotnsPopController is returns (bool) { return interfaceId == type(IDotnsPopController).interfaceId + || interfaceId == type(IDotnsPopControllerLegacy).interfaceId || super.supportsInterface(interfaceId); } @@ -572,14 +633,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 +653,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 +693,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 +719,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 +756,7 @@ contract DotnsPopController is function _enqueueReservation( IPopRules rules, bytes32 labelhash, - string memory baseLabel, + string memory reservedLabel, address user ) internal @@ -714,8 +776,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 +785,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 +842,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 +877,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, since a suffixed label with nine or more characters lands on 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 +1017,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..11036c887 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.personhoodLink = _popResolver().personhoodLink(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 `personhoodLink` 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.personhoodLink = + _popResolver().personhoodLink(LabelUtils.labelhashMemory(detail.label)); } return detail; } @@ -116,29 +98,26 @@ 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 reads as a personhood name and is not one. 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 /// 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 +131,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 +139,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 +154,7 @@ contract DotnsPopLens is IDotnsPopLens { function _pageNames( address user, uint256 offset, - uint256 limit, - bool wantLite + uint256 limit ) internal view @@ -202,7 +180,7 @@ 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}); } @@ -213,7 +191,7 @@ contract DotnsPopLens is IDotnsPopLens { 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; @@ -240,11 +218,12 @@ contract DotnsPopLens is IDotnsPopLens { /// @notice Gathers a name's record from the 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. `personhoodLink` 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 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. /// @param node The name's node. /// @param knownLabel Label the caller already holds, used only when the node cannot recover it. function _detail( @@ -282,12 +261,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.deviceLink = resolver.deviceLink(node); } /// @notice Reads a bounded page of `user`'s pending claims from the controller. @@ -316,15 +295,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..2012ff097 100644 --- a/contracts/registrars/DotnsRegistrar.sol +++ b/contracts/registrars/DotnsRegistrar.sol @@ -28,9 +28,9 @@ import {DotnsConstants} from "../utils/DotnsConstants.sol"; /// @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, @@ -55,7 +55,7 @@ contract DotnsRegistrar is /// 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 diff --git a/contracts/registrars/DotnsRegistrarController.sol b/contracts/registrars/DotnsRegistrarController.sol index b2e36f16a..bcc60028f 100644 --- a/contracts/registrars/DotnsRegistrarController.sol +++ b/contracts/registrars/DotnsRegistrarController.sol @@ -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); } } 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..5b154be5d 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,7 +82,7 @@ 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 @@ -91,71 +90,69 @@ interface IDotnsPopController is IDotnsController { /// 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. + /// @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 +161,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 +202,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 +211,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 +243,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 +330,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 +384,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 +402,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 +411,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,14 +434,14 @@ 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 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..343c79c55 100644 --- a/contracts/registrars/IDotnsPopLens.sol +++ b/contracts/registrars/IDotnsPopLens.sol @@ -31,43 +31,43 @@ interface IDotnsPopLens { /// @notice The full on-chain record for a single name, gathered from 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. `personhoodLink` is keyed 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 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. /// @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 deviceLink For a personhood name, the linked device-name labelhash; zero otherwise. + /// @param personhoodLink 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 deviceLink; + bytes32 personhoodLink; } /// @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 +78,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 +108,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 `personhoodLink` 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 `personhoodLink` 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..af7698ae8 100644 --- a/contracts/registrars/IDotnsRegistrar.sol +++ b/contracts/registrars/IDotnsRegistrar.sol @@ -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..1f1c440be 100644 --- a/contracts/registrars/IDotnsRegistrarController.sol +++ b/contracts/registrars/IDotnsRegistrarController.sol @@ -157,8 +157,8 @@ interface IDotnsRegistrarController is IDotnsController { /// `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/resolvers/DotnsPopResolver.sol b/contracts/resolvers/DotnsPopResolver.sol index 6daae5e55..20448cc6f 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,13 +126,13 @@ contract DotnsPopResolver is } /// @inheritdoc IDotnsPopResolver - function liteLink(bytes32 fullNode) external view override returns (bytes32) { - return _liteLinks[fullNode]; + function deviceLink(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 personhoodLink(bytes32 deviceLabelhash) external view override returns (bytes32) { + return _personhoodNodes[deviceLabelhash]; } /// @notice Returns the release this network declares it runs, read live from the protocol diff --git a/contracts/resolvers/DotnsReverseResolver.sol b/contracts/resolvers/DotnsReverseResolver.sol index bf8bbb553..43cd90d3e 100644 --- a/contracts/resolvers/DotnsReverseResolver.sol +++ b/contracts/resolvers/DotnsReverseResolver.sol @@ -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/IDotnsPopResolver.sol b/contracts/resolvers/IDotnsPopResolver.sol index 52c37ec9b..f805b17f2 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: for a personhood-name node, the labelhash of the device name it +/// was linked to when it was issued. +/// - Personhood link: reverse index mapping a device-name labelhash to the +/// personhood-name node it is linked to. Mirrors `deviceLink` 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,34 @@ 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 (`deviceLink`) and reverse + /// (`personhoodLink`) indices remain consistent: re-linking the same `personhoodNode` to a + /// new `deviceLabelhash` clears `personhoodLink(oldDevice)`, and re-linking the same + /// `deviceLabelhash` to a new `personhoodNode` clears `deviceLink(oldPersonhood)`. The + /// invariant `personhoodLink(deviceLink(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 deviceLink(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 deviceLink. 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 personhoodLink(bytes32 deviceLabelhash) external view returns (bytes32 personhoodNode); } diff --git a/contracts/resolvers/IDotnsReverseResolver.sol b/contracts/resolvers/IDotnsReverseResolver.sol index b52362d65..096d14f6f 100644 --- a/contracts/resolvers/IDotnsReverseResolver.sol +++ b/contracts/resolvers/IDotnsReverseResolver.sol @@ -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 `