From 74349f7713ba80e336be42cd470f2e14391e8ecb Mon Sep 17 00:00:00 2001 From: Tiago Tavares Date: Thu, 20 Aug 2026 11:37:03 +0100 Subject: [PATCH 01/10] docs: define TLD-free name identifiers and amend the derivation docs --- docs/design/name-identifiers.md | 69 +++++++++++++++++ docs/rfcs/0022-account-derivations.md | 38 ++++++---- docs/rfcs/0024-personhood-as-product.md | 24 +++--- rust/crates/truapi-host-cli/README.md | 99 ++----------------------- rust/crates/truapi-host-cli/SPEC.md | 50 +++---------- rust/crates/truapi-server/README.md | 2 +- 6 files changed, 123 insertions(+), 159 deletions(-) create mode 100644 docs/design/name-identifiers.md diff --git a/docs/design/name-identifiers.md b/docs/design/name-identifiers.md new file mode 100644 index 000000000..23949f070 --- /dev/null +++ b/docs/design/name-identifiers.md @@ -0,0 +1,69 @@ +--- +title: "Name Identifiers" +type: design +--- + +# Name Identifiers + +Name identifiers are unique pointers of Products and Users. They are +labels registered in the [DotNS protocol](https://github.com/paritytech/dotns). + +The protocol handles a name as its +[ENS-style namehash](https://docs.ens.domains/resolution/names#namehash), +originally specified in [EIP-137](https://eips.ethereum.org/EIPS/eip-137). It is computed as +`namehash(tldNode, keccak256(label))` in +[`LabelUtils`](https://github.com/paritytech/dotns/blob/main/contracts/utils/LabelUtils.sol), +so the stored identifier covers both the label and the Top Level Domain (TLD): + +``` +namehash("dot") = keccak256(0x00…00 ++ keccak256("dot")) + = 0x3fce7d1364a893e213bc4212792b517ffc88f5b13b86c8ef9c8d390c3a1370ce +namehash("example.dot") = keccak256(namehash("dot") ++ keccak256("example")) + = 0x50cef3746492e11fe07821077c650ed11a908315a91b3a85b4a12afd21249605 +``` + +## Convention + +Each network declares its own TLD, fixed at registry initialisation and +exposed by the +[`tld()` view function](https://github.com/paritytech/dotns/blob/main/contracts/registry/DotnsProtocolRegistry.sol) +of the DotNS protocol registry. The protocol owns the bare label, and the TLD +is appended when a label is rendered as a full name, so the same label is +served differently per network. + +The served, TLD-full form is for routing only. For identity, the host +normalizes once through +[`normalize_product_identifier`](../../rust/crates/truapi-platform/src/lib.rs): +trim, NFC-normalize, lowercase, strip the recognized TLD listed in +`DOTNS_TLDS`. `localhost` and `localhost:{port}` pass through. Everything else +is rejected. All uses above except navigation and username display take the +canonical result. + +The label is the identity, the TLD is routing. Hosts MUST derive and scope by +the TLD-free canonical form, so a product keeps its accounts, aliases, +permissions, and storage when it graduates from testnet to mainnet or the user +switches networks. ENS gets the same continuity with one `.eth` everywhere +plus a disposable `.test`. + +Every party that derives keys MUST apply the identical normalization. The +[mobile Account Holder](https://github.com/Polkadot-Community-Foundation/polkadot-app-ios-v2) +mirrors it, and +[host-spec C.5 to C.7](https://github.com/paritytech/host-spec/blob/main/spec/C-account-derivation.md) +plus the +[interop vectors](../../rust/crates/truapi-server/tests/wasm_crypto_vectors.rs) +pin it byte-for-byte. Everything keyed by a name id re-keys if the rule +drifts, so it MUST NOT be reimplemented outside +[`normalize_product_identifier`](../../rust/crates/truapi-platform/src/lib.rs) +and the +[reserved-id table](../../rust/crates/truapi-server/src/host_logic/product_account.rs). + +#### Use Cases + +| Use Case | Description | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Navigation | A host resolves the name a user typed or followed into the content it should load, via [`NavigateDecision`](../../rust/crates/truapi-server/src/host_logic/dotns.rs). This is the only use that keeps the served name intact, TLD included, because here the name is an address. | +| Product accounts | The account tree of a product hangs off its identifier: `//product//{nameId}/{index}` ([RFC-0022](../rfcs/0022-account-derivations.md)), implemented in [`product_account.rs`](../../rust/crates/truapi-server/src/host_logic/product_account.rs). The built-ins `uid` and `peopl` are reserved identifiers in the same tree. | +| Ring contexts | A personhood proof carries the identifier of the product it was made for, so no other product can replay it. [The ring-VRF signer](../../rust/crates/truapi-server/src/runtime/signing_host/ring_vrf.rs) builds the proof context ([RFC-0004](../rfcs/0004-ringlocation-redesign.md)), and the [ring-VRF registry](../../rust/crates/truapi-server/src/runtime/ring_vrf_registry.rs) records which keys belong to which identifier ([RFC-0024](../rfcs/0024-personhood-as-product.md)). | +| Per-product entropy | Each product gets deterministic secret material ([RFC-0007](../rfcs/0007-derive-entropy.md)), and the identifier is what separates one product entropy space from another, in [`entropy.rs`](../../rust/crates/truapi-server/src/host_logic/entropy.rs). | +| Permissions and storage | Everything a host remembers about a product, from consent grants to stored values, sits under a `CoreStorageKey` built from the identifier, in [`truapi-platform`](../../rust/crates/truapi-platform/src/lib.rs). | +| User identity | The primary username a product may request ([RFC-0015](../rfcs/0015-get-user-id.md)) is itself a name identifier, and it points at the `uid` identity account in [`product_account.rs`](../../rust/crates/truapi-server/src/host_logic/product_account.rs). | diff --git a/docs/rfcs/0022-account-derivations.md b/docs/rfcs/0022-account-derivations.md index 8f9b2db25..f82837c42 100644 --- a/docs/rfcs/0022-account-derivations.md +++ b/docs/rfcs/0022-account-derivations.md @@ -96,8 +96,12 @@ secret components, no intermediate hashing layer. - `//product` — **hard** namespace junction separating product accounts from the root keypair's other derivations. -- `//{productId}` — **hard** junction; `productId` is the product's dotNS - identifier (e.g. `browse.dot`). +- `//{productId}` — **hard** junction; `productId` is the product's canonical + TLD-free identifier (e.g. `browse`). On the wire products name themselves by + the dotNS name their network serves (e.g. `browse.dot`, `browse.paseo`); + hosts strip the recognized network TLD during normalization, so the same + product derives the same subtree on every network and derivation paths never + contain a TLD. - `/{index}` — **soft** junction carrying the 32-byte derivation index. The hard junction is the security firewall: leaking the `//product//{productId}` @@ -147,7 +151,8 @@ forms: ```rust ProductAccountId { - /// A dotNS domain name identifier (e.g., `"my-product.dot"`). + /// A dotNS domain name identifier (e.g., `"my-product.dot"`); the host + /// strips the network TLD to form the canonical product id. dot_ns_identifier: String, /// Account selector within the product subtree: /// Left — a plain index (primary form); Right — a raw 32-byte index. @@ -165,7 +170,8 @@ amended to: ```rust ProductProofContext { - /// dotNS product identifier (e.g. `"my-product.dot"`) scoping the context. + /// dotNS product identifier (e.g. `"my-product.dot"`) scoping the context; + /// normalized to the canonical TLD-free id before hashing. product_id: String, /// Selector distinguishing contexts within the product; expands to the /// same 32-byte derivation index as `ProductAccountId.derivation_index`. @@ -217,7 +223,7 @@ gap: ```rust /// Host → Account Holder. ApProductSubtreeRequest { - /// dotNS identifier of the product whose subtree is requested. + /// Canonical TLD-free id of the product whose subtree is requested. product_id: String, } @@ -267,11 +273,11 @@ reserved product identities as their `productId`: | Category | Feature | `productId` | Protection | | ------------------------------------ | ---------------------------- | ------------ |------------------------------------------------------------------------------------------------| -| Migrating to a product soon | Game (DIM2) | `dim2.dot` | Governance-reserved 3–5 char name | -| Migrating long-term / product-shaped | PoI (DIM1) | `poi.dot` | Governance-reserved 3–5 char name | -| Migrating long-term / product-shaped | Funding | `fund.dot` | Governance-reserved 3–5 char name | -| Migrating long-term / product-shaped | Public light person identity | `uid.dot` | Governance-reserved 3–5 char name | -| Migrating long-term / product-shaped | Personhood | `peopl.dot` | Governance-reserved 3–5 char name | +| Migrating to a product soon | Game (DIM2) | `dim2` | Governance-reserved 3–5 char name | +| Migrating long-term / product-shaped | PoI (DIM1) | `poi` | Governance-reserved 3–5 char name | +| Migrating long-term / product-shaped | Funding | `fund` | Governance-reserved 3–5 char name | +| Migrating long-term / product-shaped | Public light person identity | `uid` | Governance-reserved 3–5 char name | +| Migrating long-term / product-shaped | Personhood | `peopl` | Governance-reserved 3–5 char name | | Not coercible to a product | Coinage | — | Deferred to a separate RFC (own layout today: `//pps//coin/{index}`, `//pps//ring-vrf/{index}`) | ### Well-known alias accounts @@ -322,14 +328,14 @@ reserved product identity from the table above. `DerivationIndex` is the same 32-byte index format as product accounts, so each domain gets its own index space. -The personhood keys live under the `peopl.dot` domain: +The personhood keys live under the `peopl` domain: ```rust // Full personhood ring-VRF key -full_personhood_key = //peopl.dot//index_bytes(0) +full_personhood_key = //peopl//index_bytes(0) // Light personhood ring-VRF key -light_personhood_key = //peopl.dot//index_bytes(1) +light_personhood_key = //peopl//index_bytes(1) ``` Existing keys migrate to these paths. Coinage's ring-VRF keys @@ -376,7 +382,7 @@ game_domain = "game" ``` > **Note:** the `game` domain is expected to go away soon. The Game is -> migrating to the `dim2.dot` product, which will obtain its key material +> migrating to the `dim2` product, which will obtain its key material > via `host_derive_entropy` (RFC-0007) instead. ### Compatibility @@ -384,7 +390,7 @@ game_domain = "game" There are no production deployments of secret-component derivations or of the `u32`-index wire types; the selector change is wire-breaking for `ProductAccountId`, `ProductProofContext`, `PaymentTopUpSource`, and -`AllocatableResource`, and is made freely, with no migration path. Existing ring-VRF keys move to their `peopl.dot` +`AllocatableResource`, and is made freely, with no migration path. Existing ring-VRF keys move to their `peopl` paths; deployed encryption keys are handled by the encryption RFC. ## Drawbacks @@ -392,7 +398,7 @@ paths; deployed encryption keys are handled by the encryption RFC. - **One new Accounts Protocol message**, amortized to one round trip per product per Host. - **No path-string tooling round trip.** The 32-byte index junction cannot be - typed as a path segment, so `//product//browse.dot/5` in stock tooling + typed as a path segment, so `//product//browse/5` in stock tooling (`polkadot-js`, `subkey`) does not derive index `5` (`index_bytes(5)`) ## Alternatives diff --git a/docs/rfcs/0024-personhood-as-product.md b/docs/rfcs/0024-personhood-as-product.md index 6ca217b6c..67e7504ff 100644 --- a/docs/rfcs/0024-personhood-as-product.md +++ b/docs/rfcs/0024-personhood-as-product.md @@ -24,7 +24,7 @@ A proof is a bearer token for its context's alias and a signature is a bearer to **Personhood is welded into the Host.** RFC-0004 §"Host member-key selection" requires every Host to define the PoP ring collection internally, choose a member key corresponding to the requested `RingLocation`, fall back to the PoP key when correspondence is undeterminable, and tiebreak stably. `truapi-server` implements exactly that with the ring identities compiled in (`rust/crates/truapi-server/src/runtime/signing_host/ring_vrf.rs`: `FULL_PERSON_COLLECTION`, `LITE_PERSON_COLLECTION`, `enum PersonKey { Full, Lite }`). So every change to how a person key is derived, registered, renewed, or recovered is a Host release. -A personhood product must instead own the full and light keys — under RFC-0022, the `peopl.dot` domain of the ring-VRF tree — while telling the Host and Account Holder enough to keep serving the app's own personhood-dependent features, and lending its keys and aliases to other products. The binding constraint across all of it: **no consumer may know which key is used**, not the app and not a calling product. +A personhood product must instead own the full and light keys — under RFC-0022, the `peopl` domain of the ring-VRF tree — while telling the Host and Account Holder enough to keep serving the app's own personhood-dependent features, and lending its keys and aliases to other products. The binding constraint across all of it: **no consumer may know which key is used**, not the app and not a calling product. **The obstacle** is that the member keys serve three overlapping classes of work, and only one is not extractable: @@ -108,7 +108,7 @@ fn list_ring_vrf_keys( - **Registration declares intent, not membership.** It means "this is the key I will use for that ring", not "the user is a person"; membership is still discovered only by attempting a proof, which returns `NotMember` (RFC-0004). This keeps the registry from being a personhood oracle. - **The public key is owner-visible by default, permissioned cross-product**, because a member public key is linkable across every ring it appears in. -RFC-0022 already pins `//peopl.dot//index_bytes(0)` as the full personhood key and `index_bytes(1)` as the light one. Under this RFC those constants are the personhood product's own implementation detail, expressed to everyone else as two registry entries. +RFC-0022 already pins `//peopl//index_bytes(0)` as the full personhood key and `index_bytes(1)` as the light one. Under this RFC those constants are the personhood product's own implementation detail, expressed to everyone else as two registry entries. ### Proofs, aliases, and signatures take an explicit key handle @@ -186,18 +186,18 @@ enum RingVrfSignErr { ### Cross-product discovery -A game product producing a proof with the full personhood key, under its own airdrop context — abstracted by the product SDK, not the Host. It works because `peopl.dot` has allowlisted `game.dot`; see [Using a foreign key](#using-a-foreign-key-means-trusting-the-caller). +A game product producing a proof with the full personhood key, under its own airdrop context — abstracted by the product SDK, not the Host. It works because `peopl` has allowlisted `game`; see [Using a foreign key](#using-a-foreign-key-means-trusting-the-caller). ```mermaid sequenceDiagram - participant G as game.dot + participant G as game participant H as Host - participant P as peopl.dot registry + participant P as peopl registry - G->>H: list_ring_vrf_keys("peopl.dot", Anonymized) - H-->>G: [ { handle: (peopl.dot, ?), rings: [People, PeopleLite] } ] + G->>H: list_ring_vrf_keys("peopl", Anonymized) + H-->>G: [ { handle: (peopl, ?), rings: [People, PeopleLite] } ] G->>G: select the entry whose rings contain the People ring - G->>H: create_account_proof(handle, game.dot/airdrop, People, message) + G->>H: create_account_proof(handle, game/airdrop, People, message) H-->>G: proof + contextual_alias + ring_index + ring_revision ``` @@ -205,7 +205,7 @@ sequenceDiagram No product needs "try full, fall back to light" today, so this RFC does not specify one. If a product ever does, the fallback belongs in the **product SDK**, not in the Host and not reimplemented per product — the Host no longer has the information to choose, and duplicating the retry across consumers is how the selection contract became fragile in the first place. -**No product should assume a key index of another product.** The index is the owner's implementation detail; consumers select by declared `RingLocation` and treat the handle as opaque. Hardcoding `(peopl.dot, 0)` breaks the moment the owner adds a key. +**No product should assume a key index of another product.** The index is the owner's implementation detail; consumers select by declared `RingLocation` and treat the handle as opaque. Hardcoding `(peopl, 0)` breaks the moment the owner adds a key. This is a **convention, not an enforceable rule**, and the RFC does not pretend otherwise. The index is part of the handle, so any caller that can list the registry can read it and hardcode it; `Anonymized` disclosure withholds the member public key, not the index. Hiding the index would mean the handle could no longer name a derivation slot, which is the whole point of it. So this lands as an implementation note for the **product SDK**, which should expose selection-by-ring and never surface a raw index to product code. @@ -335,7 +335,7 @@ A Host holding a current registry snapshot answers `list` locally. `RingVrfProof > **A Host MUST NOT derive a member secret for a `(product, index)` pair absent from its registry.** -This needs saying because domain entropy makes derivation _unconditional_: given the entropy of `//peopl.dot`, a Host can compute the member secret at index 7, or 4711, or any other, since derivation is pure arithmetic and nothing about holding the entropy distinguishes a meaningful index from a meaningless one. The registry supplies that distinction. Serve an unregistered index and the phone has no record the key exists — it cannot include it in slot assignment, list it in the inventory, or answer "what is this key used for". So the entropy lets a Host **derive** a key the registry already lists; only registration, which always reaches the phone, brings one into **existence**. +This needs saying because domain entropy makes derivation _unconditional_: given the entropy of `//peopl`, a Host can compute the member secret at index 7, or 4711, or any other, since derivation is pure arithmetic and nothing about holding the entropy distinguishes a meaningful index from a meaningless one. The registry supplies that distinction. Serve an unregistered index and the phone has no record the key exists — it cannot include it in slot assignment, list it in the inventory, or answer "what is this key used for". So the entropy lets a Host **derive** a key the registry already lists; only registration, which always reaches the phone, brings one into **existence**. #### Answering while the phone is backgrounded @@ -363,7 +363,7 @@ AutoSigning { - **Cross-product key use is an all-or-nothing trust decision.** An allowlisted product can do anything the owner's key can do — prove under any context, sign any message — when what an owner wants to express is narrower. The blind-signing risk is contained by whom the owner trusts rather than by what the caller can ask for, which is why this is explicitly an interim position. - **Bundling ring VRF entropy into AutoSigning widens one grant.** "Sign transactions without prompting me" and "produce personhood proofs offline" become one decision, and the second is arguably the stronger. Accepted deliberately: two grants would mean two authorization surfaces for what the user experiences as one relationship. - **The registry is new distributed state**, agreed between the registering product, the caching Host, and the owning Account Holder; a stale Host returns `KeyNotRegistered` for a key that exists. Idempotent registration and a single authority keep it diagnosable, but it replaces a compile-time constant. -- **Registration leaks intent.** An anonymized listing still says "`peopl.dot` has a key it intends for the People ring" — not proof of membership, but a consumer learns the user has attempted full personhood before any proof is requested. The one privacy cost accepted for cheap discovery. +- **Registration leaks intent.** An anonymized listing still says "`peopl` has a key it intends for the People ring" — not proof of membership, but a consumer learns the user has attempted full personhood before any proof is requested. The one privacy cost accepted for cheap discovery. - **The silent happy path depends on the manifest RFC**: until the allowlist exists, each cross-product call in the alias flow produces a one-time prompt. - **The key handle overloads `ProductAccountId`**, which now names both an sr25519 product account and a ring VRF slot in a different tree at the same `(product, index)`. @@ -379,7 +379,7 @@ AutoSigning { ## Prior Art and References - [RFC-0004 — Redesign `account_create_account_proof`](0004-ringlocation-redesign.md) — `RingLocation`, `ProductProofContext`, the context derivation, and the member-key selection contract this RFC deletes. Its "Out of scope: explicit member-key management … left to a future RFC" is this RFC. -- **RFC-0022 — Account key derivations** ([PR #296](https://github.com/paritytech/host-rust-core/pull/296)) — the ring-VRF tree, `Either` indices, the reserved `peopl.dot` identity, and the `AutoSigning` payload this RFC extends. Its deferral of well-known alias accounts is resolved here. +- **RFC-0022 — Account key derivations** ([PR #296](https://github.com/paritytech/host-rust-core/pull/296)) — the ring-VRF tree, `Either` indices, the reserved `peopl` identity, and the `AutoSigning` payload this RFC extends. Its deferral of well-known alias accounts is resolved here. - **RFC-0023 — sr25519 VRF signing for product accounts** ([PR #301](https://github.com/paritytech/host-rust-core/pull/301)) — the complementary non-member path, where this RFC's ring VRF path serves members. - [RFC-0020 — `signing_create_transaction` and its AP mirror](0020-create-transaction.md) — the pattern of specifying a TrUAPI call together with its AP companion, followed here. - [RFC-0010 — W3S Allowance Management](0010-allowance.md) — AutoSigning and the PGAS / Bulletin / SSS flows that consume the person key. diff --git a/rust/crates/truapi-host-cli/README.md b/rust/crates/truapi-host-cli/README.md index 79717b852..7abbaa89b 100644 --- a/rust/crates/truapi-host-cli/README.md +++ b/rust/crates/truapi-host-cli/README.md @@ -21,7 +21,7 @@ One binary, `truapi-host`: | --- | --- | | `pairing-host` | Seedless host: serves product frames, emits pairing deeplinks, and can run product scripts. | | `signing-host` | Wallet-local host: owns signer identity, can run product scripts, accepts pairing deeplinks, registers statement allowance on-chain, signs. | -| `identity-check` | Probe the root and canonical `uid.dot` identity account for a registered username. | +| `identity-check` | Probe the root and canonical `uid` identity account for a registered username. | | `alloc-check` | Diagnose (or `--submit`) on-chain statement-store allowance: ring membership, chosen slot, and the `set_statement_store_account` extrinsic. On a full period it prints each occupied slot's age and which one would be replaced. | | `pgas-check` | Diagnose (or `--submit`) an Asset Hub PGAS allowance claim: ring membership on People, whether Asset Hub has imported that ring revision, the day's first unclaimed slot, and the `Pgas.claim_pgas` extrinsic. | @@ -42,34 +42,6 @@ default, so starting either host does not reserve a TCP port. Pass `--frame-listen 127.0.0.1:0` to expose an ordinary loopback WebSocket instead; this is required for browser clients, which cannot open filesystem sockets. -### Browser products - -A browser product reaches that socket through `@parity/truapi`'s sandbox. Start -the host on a fixed port, and point the product at it before anything else -touches the client: - -```bash -truapi-host signing-host --frame-listen 127.0.0.1:9955 --product-id my-product.dot -``` - -```ts -import { connectWebSocketHost } from "@parity/truapi/sandbox"; - -connectWebSocketHost("ws://127.0.0.1:9955"); -``` - -The product is then detected as hosted and holds the real product account for -its own `.dot` name, so signing, statements, entropy, permissions and storage -all take their production code paths with no phone involved. `--product-id` is -not optional: the host derives the product account from it and refuses to *sign* -for any other product id, and a mismatch only surfaces later, as a -`PermissionDenied` on the first signature. - -Two players on one machine means two hosts, each with its own session and port -(`--session bob --frame-listen 127.0.0.1:9956`), and a second product instance -pointed at the second port. Sessions isolate the signer, the storage and the -permissions. - The signing host opens an interactive terminal where you can paste a pairing link, type `/pair `, run `/script`, or use `/help` to discover the available commands. It uses `--mnemonic` / `HOST_CLI_SIGNER_MNEMONIC` if set. @@ -248,7 +220,8 @@ res.match( `--product-id` (a dotNS name ending in `.dot` or `.paseo`, or a `localhost` identifier; default -`headless-playground.dot`) sets the initial product. `/product ` changes it +`headless-playground.dot`) sets the initial product. The dotNS TLD is stripped +during normalization, so `.dot` and `.paseo` names scope the same product. `/product ` changes it for the lifetime of the process. Switching disconnects active product WebSockets so clients reconnect with a new product context; the network, pairing relationship, signing-host session, and wallet identity stay active. @@ -406,7 +379,7 @@ The real statement store enforces per-account allowance. Before pairing, the signing host grants it on-chain exactly as a real client does: it proves its personhood ring membership with a bandersnatch ring-VRF and submits an unsigned General (v5) `Resources.set_statement_store_account` extrinsic for each account -that submits statements — its RFC-0022 `uid.dot` identity account and the +that submits statements — its RFC-0022 `uid` identity account and the pairing host's per-pairing device key. The shared native implementation lives in `truapi-server/src/runtime/statement_allowance/` (metadata-driven signed-extension encoding, ring fetch, slot scan, ring-VRF proof, extrinsic @@ -453,69 +426,13 @@ HOST_CLI_SIGNER_MNEMONIC="spin battle …" truapi-host signing-host --deeplink ' truapi-host alloc-check --mnemonic "spin battle …" --lookback 100 ``` -Both hosts take `--network`, either `paseo-next-v2` (default) or `previewnet`. -The network preset owns the identity backend URL, the People, Bulletin and Asset -Hub RPCs, and their genesis hashes; there is -no public `--statement-store` flag. Pick `previewnet` when a product's runtime -descriptors target previewnet, so its statements, its host chain routes and its -own chain reads all land on one network. Sessions are per preset, so each network -gets its own signer identity on the same machine. - -One limit on `previewnet` today: its identity backend requires a bearer token for -write requests, so auto-provisioning a fresh lite username fails with - -``` -username registration failed (401 Unauthorized): Missing Authorization Header -``` - -Reads against it work, and everything that does not go through the backend works -normally, so use `--mnemonic` (or `HOST_CLI_SIGNER_MNEMONIC`) with an account that -already carries a previewnet username. `paseo-next-v2` still provisions on its -own, unauthenticated. Both also accept `--frame-listen
` +Both hosts take `--network` (default `paseo-next-v2`). The network preset owns +the identity backend URL, the People, Bulletin and Asset Hub RPCs, and their +genesis hashes; there is +no public `--statement-store` flag. Both also accept `--frame-listen
` to opt into a TCP product-frame WebSocket; without it, the CLI creates and cleans up a unique temporary Unix socket. -## Serving a dev server (one process, no terminal) - -`signing-host --serve` runs the host as a background service instead of a -terminal UI, so a dev server or test harness can supervise it: - -```bash -truapi-host signing-host --serve \ - --frame-listen 127.0.0.1:9955 \ - --product-id myapp.dot \ - --auto-accept -``` - -It needs no TTY, initialises the signer, and stays up until stopped. Output is -one line per event: - -``` -✓ Paired with headlessyvqhet.43 -✓ Signing host ready -• Listening for product frames - ws://127.0.0.1:9955 -• Serving product frames until stopped - ws://127.0.0.1:9955 - Confirmations are approved automatically -``` - -Wait for `Serving product frames until stopped` before pointing a product at the -endpoint. That line is last in every case, and it is the only one that means -both halves are up: the frame socket accepts connections well before a signer -exists, and `Signing host ready` can arrive either side of it depending on -whether the session was cached or is being registered. A first run registers a -lite username and the statement-store allowance on-chain, which can take -minutes. - -Stopping it: Ctrl-C is handled, so the host logs its own shutdown. `SIGTERM` -ends the process, which is what a supervising dev server sends. - -`--auto-accept` is effectively required, because a process with no terminal has -nowhere to prompt: confirmations are denied instead, and the startup line says -so. `--serve` cannot be combined with `--script` or `exec`, which are the -one-shot modes. - ## Scope / gaps - **Chain methods** route to real `wss://` nodes from the selected `--network`. diff --git a/rust/crates/truapi-host-cli/SPEC.md b/rust/crates/truapi-host-cli/SPEC.md index e6d9d6b96..1abdee337 100644 --- a/rust/crates/truapi-host-cli/SPEC.md +++ b/rust/crates/truapi-host-cli/SPEC.md @@ -344,8 +344,10 @@ Accepted product identifiers are: - `localhost`; or - a string beginning with `localhost:`. -Identifiers are trimmed, Unicode-NFC normalized, and lowercased. For example, -`" Dotli.DOT "` becomes `dotli.dot`. +Identifiers are trimmed, Unicode-NFC normalized, and lowercased, and a +recognized dotNS TLD is stripped, so the canonical product id is TLD-free and +identical on every network. For example, `" Dotli.DOT "` and `dotli.paseo` +both become `dotli`. Other identifiers, including an ordinary `example.com`, are rejected. @@ -724,9 +726,9 @@ Before a signing host answers a link, it: 1. ensures a signer; 2. decodes the V2 handshake; -3. derives its RFC-0022 `uid.dot` identity account; +3. derives its RFC-0022 `uid` identity account; 4. reads the pairing device Statement Store account from the proposal; -5. finds the signer's rings through the pairing-attestation bootstrap `peopl.dot` +5. finds the signer's rings through the pairing-attestation bootstrap `peopl` keys, index 0 for `People` and index 1 for `LitePeople`, scanning back from the current ring in each (RFC-0024 operational key selection uses the registry instead); @@ -809,7 +811,7 @@ A new auto account: 1. acquires `accounts.json.lock`; 2. generates a 12-word mnemonic; -3. derives the RFC-0022 `uid.dot` index-0 sr25519 identity account; +3. derives the RFC-0022 `uid` index-0 sr25519 identity account; 4. chooses `auto-` as its local name; 5. tries up to eight available Lite username bases; 6. saves a pending account record; @@ -1065,7 +1067,7 @@ state, and other role-owned runtime data. - network id; - plaintext BIP-39 mnemonic; - final Lite username; -- RFC-0022 `uid.dot` index-0 public key and address; +- RFC-0022 `uid` index-0 public key and address; - creation timestamp; - attested state; and - exhausted Statement Store periods. @@ -1097,11 +1099,9 @@ selection files. ## 14. Network and transport -### 14.1 Network presets +### 14.1 Network preset -`--network` selects one of two presets. `paseo-next-v2` is the default. - -#### `paseo-next-v2` +v0.1 supports only `paseo-next-v2`. | Purpose | Value | | --- | --- | @@ -1113,34 +1113,6 @@ selection files. | Asset Hub RPC | `wss://paseo-asset-hub-next-rpc.polkadot.io` | | Asset Hub genesis | `0x23e730eb1c6fecae09c917439a5038cb6122d0d48980e8b9bbf0ff56f94a2ca6` | -#### `previewnet` - -The network that front-runs `paseo-next-v2`: it carries the runtime that reaches -nextv2 later, and it is where products with previewnet descriptors do their -on-chain testing. Its identity backend is the same service on its staging -environment (`/api/v1/version` reports `"environment": "staging"`). - -| Purpose | Value | -| --- | --- | -| Identity backend | `https://polkadot-app-stg.parity.io/api/v1` | -| People RPC | `wss://previewnet.substrate.dev/people` | -| People genesis | `0x34999c298555e25bf17a7f3ea20efe7f6fdab1dfec7f808fbcfd36ca8aa5d220` | -| Bulletin RPC | `wss://previewnet.substrate.dev/bulletin` | -| Bulletin genesis | `0x1144acd27f0e5b2c88da7dc12c111e396983dec036ccfb42da5bbb0dd7104e89` | -| Asset Hub RPC | `wss://previewnet.substrate.dev/asset-hub` | -| Asset Hub genesis | `0x627f54413120c81161261b2ca87f60f0020963107dc28367491e09ec2dd29659` | - -Sessions are per network (`SessionCatalog::new` keys on the preset id), so a -signer provisioned on one preset is not visible from the other. Two presets means -two identities on one machine, which is deliberate: the lite username and the -statement-store allowance are per chain. - -`previewnet`'s identity backend requires a bearer token for write requests, which -the CLI does not send, so auto-managed account creation fails there with -`401 Unauthorized (Missing Authorization Header)`. Reads against the backend -succeed, and every non-backend path is unaffected, so a `previewnet` signer needs -`--mnemonic` for an account that already holds a username on that chain. - There are no public endpoint override flags. Every role the preset serves — People, Bulletin and Asset Hub — is always routed, @@ -1468,7 +1440,7 @@ truapi-host identity-check \ The command derives and queries two accounts: - root; and -- RFC-0022 `//product//uid.dot/index_bytes(0)`. +- RFC-0022 `//product//uid/index_bytes(0)`. For each it prints one of: diff --git a/rust/crates/truapi-server/README.md b/rust/crates/truapi-server/README.md index 388997266..eb4ef5d3b 100644 --- a/rust/crates/truapi-server/README.md +++ b/rust/crates/truapi-server/README.md @@ -190,7 +190,7 @@ role-specific lifecycle, so no method exists on a role that can't mean it: - **`SigningHost`** (wallet-local): signs on device from local BIP-39 entropy, no pairing flow. `signing_host/local_activation.rs` establishes a session from host-held secret material. Its public identity is the RFC-0022 - `uid.dot` index-0 product account. RFC-0024 ring-VRF keys are explicit, + `uid` index-0 product account (RFC-0022 TLD-free id). RFC-0024 ring-VRF keys are explicit, product-owned registry entries; aliases, proofs, direct signatures, and internal personhood flows use the requested or user-selected registered key without a compiled-in fallback. It resolves RFC-0004 `RingLocation` values From 4bf41f4a73673f6b48be9df54308a484f02adf65 Mon Sep 17 00:00:00 2001 From: Tiago Tavares Date: Thu, 20 Aug 2026 13:37:08 +0100 Subject: [PATCH 02/10] docs: make the full served name the identity and recognize the previewnet test TLD --- docs/design/name-identifiers.md | 66 ++++++++++++---- docs/rfcs/0022-account-derivations.md | 38 ++++----- docs/rfcs/0024-personhood-as-product.md | 24 +++--- rust/crates/truapi-host-cli/README.md | 101 +++++++++++++++++++++--- rust/crates/truapi-host-cli/SPEC.md | 52 +++++++++--- rust/crates/truapi-host-cli/src/main.rs | 2 +- rust/crates/truapi-platform/src/lib.rs | 7 +- rust/crates/truapi-server/README.md | 2 +- 8 files changed, 218 insertions(+), 74 deletions(-) diff --git a/docs/design/name-identifiers.md b/docs/design/name-identifiers.md index 23949f070..1db168dc3 100644 --- a/docs/design/name-identifiers.md +++ b/docs/design/name-identifiers.md @@ -22,6 +22,28 @@ namehash("example.dot") = keccak256(namehash("dot") ++ keccak256("example")) = 0x50cef3746492e11fe07821077c650ed11a908315a91b3a85b4a12afd21249605 ``` +#### Motivation + +Three motivations argue for a well defined design: + +- A test product must not be able to act as the mainnet product. If identity + ignores the TLD, `game.test` controls the same accounts, permissions, and + storage as `game.dot`, and a throwaway test deployment can mislead users + when using real value. +- A developer who reuses one root mnemonic across networks gets the same + product addresses on every network when identity ignores the TLD, which + links their activity across networks. Deriving with the TLD keeps those + address spaces unlinkable. +- Distinct identities per TLD reduce confusion across Polkadot App versions + and web domains, because what the user sees named differently is also keyed + differently. + +The Individuality runtime takes this side for ring contexts: +[`build_product_context`](https://github.com/paritytech/individuality/blob/be61b7720e5345afff53f28b924f8bc129938e24/support/src/context.rs#L61-L80) +hashes the preimage `product/{name}.{tld}/{suffix}`, with the network suffix +an explicit argument. A host that derived ring contexts TLD-free would +disagree with the chain. + ## Convention Each network declares its own TLD, fixed at registry initialisation and @@ -31,19 +53,18 @@ of the DotNS protocol registry. The protocol owns the bare label, and the TLD is appended when a label is rendered as a full name, so the same label is served differently per network. -The served, TLD-full form is for routing only. For identity, the host -normalizes once through -[`normalize_product_identifier`](../../rust/crates/truapi-platform/src/lib.rs): -trim, NFC-normalize, lowercase, strip the recognized TLD listed in -`DOTNS_TLDS`. `localhost` and `localhost:{port}` pass through. Everything else -is rejected. All uses above except navigation and username display take the -canonical result. +The identity is the full served name, TLD included. `game.test` and +`game.dot` are different products with different accounts, ring contexts, +entropy, permissions, and storage. Hosts MUST NOT strip or rewrite the TLD +when deriving or scoping, so nothing carries over between networks implicitly. +A product graduating from testnet to mainnet starts fresh, and any carry-over +MUST be an explicit migration. -The label is the identity, the TLD is routing. Hosts MUST derive and scope by -the TLD-free canonical form, so a product keeps its accounts, aliases, -permissions, and storage when it graduates from testnet to mainnet or the user -switches networks. ENS gets the same continuity with one `.eth` everywhere -plus a disposable `.test`. +The host still normalizes the spelling once, through +[`normalize_product_identifier`](../../rust/crates/truapi-platform/src/lib.rs): +trim, NFC-normalize, lowercase. A name MUST end in a TLD the host recognizes +(`DOTNS_TLDS`), with `localhost` and `localhost:{port}` accepted for +development. Everything else is rejected. Every party that derives keys MUST apply the identical normalization. The [mobile Account Holder](https://github.com/Polkadot-Community-Foundation/polkadot-app-ios-v2) @@ -57,13 +78,28 @@ drifts, so it MUST NOT be reimplemented outside and the [reserved-id table](../../rust/crates/truapi-server/src/host_logic/product_account.rs). +#### Open Questions + +- The built-in identifiers `uid.dot` and `peopl.dot` are pinned to `.dot` on + every network, which gives the user one shared identity account and + personhood domain across networks. Consistency with this convention says + `uid.{tld}` per network, but that re-pins the mobile interop vectors and + needs the Account Holder to move in lockstep. +- The recognized TLD list is compiled in (`DOTNS_TLDS`) while the registry + already exposes the truth per network through `tld()`. The host should + eventually learn the TLD from its configured network instead of a hardcoded + list. +- Graduation needs its own design: a product that wants to carry state from + testnet to mainnet needs a registered migration or alias, not implicit + identity. + #### Use Cases | Use Case | Description | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Navigation | A host resolves the name a user typed or followed into the content it should load, via [`NavigateDecision`](../../rust/crates/truapi-server/src/host_logic/dotns.rs). This is the only use that keeps the served name intact, TLD included, because here the name is an address. | -| Product accounts | The account tree of a product hangs off its identifier: `//product//{nameId}/{index}` ([RFC-0022](../rfcs/0022-account-derivations.md)), implemented in [`product_account.rs`](../../rust/crates/truapi-server/src/host_logic/product_account.rs). The built-ins `uid` and `peopl` are reserved identifiers in the same tree. | +| Navigation | A host resolves the name a user typed or followed into the content it should load, via [`NavigateDecision`](../../rust/crates/truapi-server/src/host_logic/dotns.rs). Here the name is an address rather than an identity, and it is used verbatim. | +| Product accounts | The account tree of a product hangs off its identifier: `//product//{nameId}/{index}` ([RFC-0022](../rfcs/0022-account-derivations.md)), implemented in [`product_account.rs`](../../rust/crates/truapi-server/src/host_logic/product_account.rs). The built-ins `uid.dot` and `peopl.dot` are reserved identifiers in the same tree. | | Ring contexts | A personhood proof carries the identifier of the product it was made for, so no other product can replay it. [The ring-VRF signer](../../rust/crates/truapi-server/src/runtime/signing_host/ring_vrf.rs) builds the proof context ([RFC-0004](../rfcs/0004-ringlocation-redesign.md)), and the [ring-VRF registry](../../rust/crates/truapi-server/src/runtime/ring_vrf_registry.rs) records which keys belong to which identifier ([RFC-0024](../rfcs/0024-personhood-as-product.md)). | | Per-product entropy | Each product gets deterministic secret material ([RFC-0007](../rfcs/0007-derive-entropy.md)), and the identifier is what separates one product entropy space from another, in [`entropy.rs`](../../rust/crates/truapi-server/src/host_logic/entropy.rs). | | Permissions and storage | Everything a host remembers about a product, from consent grants to stored values, sits under a `CoreStorageKey` built from the identifier, in [`truapi-platform`](../../rust/crates/truapi-platform/src/lib.rs). | -| User identity | The primary username a product may request ([RFC-0015](../rfcs/0015-get-user-id.md)) is itself a name identifier, and it points at the `uid` identity account in [`product_account.rs`](../../rust/crates/truapi-server/src/host_logic/product_account.rs). | +| User identity | The primary username a product may request ([RFC-0015](../rfcs/0015-get-user-id.md)) is itself a name identifier, and it points at the `uid.dot` identity account in [`product_account.rs`](../../rust/crates/truapi-server/src/host_logic/product_account.rs). | diff --git a/docs/rfcs/0022-account-derivations.md b/docs/rfcs/0022-account-derivations.md index f82837c42..8f9b2db25 100644 --- a/docs/rfcs/0022-account-derivations.md +++ b/docs/rfcs/0022-account-derivations.md @@ -96,12 +96,8 @@ secret components, no intermediate hashing layer. - `//product` — **hard** namespace junction separating product accounts from the root keypair's other derivations. -- `//{productId}` — **hard** junction; `productId` is the product's canonical - TLD-free identifier (e.g. `browse`). On the wire products name themselves by - the dotNS name their network serves (e.g. `browse.dot`, `browse.paseo`); - hosts strip the recognized network TLD during normalization, so the same - product derives the same subtree on every network and derivation paths never - contain a TLD. +- `//{productId}` — **hard** junction; `productId` is the product's dotNS + identifier (e.g. `browse.dot`). - `/{index}` — **soft** junction carrying the 32-byte derivation index. The hard junction is the security firewall: leaking the `//product//{productId}` @@ -151,8 +147,7 @@ forms: ```rust ProductAccountId { - /// A dotNS domain name identifier (e.g., `"my-product.dot"`); the host - /// strips the network TLD to form the canonical product id. + /// A dotNS domain name identifier (e.g., `"my-product.dot"`). dot_ns_identifier: String, /// Account selector within the product subtree: /// Left — a plain index (primary form); Right — a raw 32-byte index. @@ -170,8 +165,7 @@ amended to: ```rust ProductProofContext { - /// dotNS product identifier (e.g. `"my-product.dot"`) scoping the context; - /// normalized to the canonical TLD-free id before hashing. + /// dotNS product identifier (e.g. `"my-product.dot"`) scoping the context. product_id: String, /// Selector distinguishing contexts within the product; expands to the /// same 32-byte derivation index as `ProductAccountId.derivation_index`. @@ -223,7 +217,7 @@ gap: ```rust /// Host → Account Holder. ApProductSubtreeRequest { - /// Canonical TLD-free id of the product whose subtree is requested. + /// dotNS identifier of the product whose subtree is requested. product_id: String, } @@ -273,11 +267,11 @@ reserved product identities as their `productId`: | Category | Feature | `productId` | Protection | | ------------------------------------ | ---------------------------- | ------------ |------------------------------------------------------------------------------------------------| -| Migrating to a product soon | Game (DIM2) | `dim2` | Governance-reserved 3–5 char name | -| Migrating long-term / product-shaped | PoI (DIM1) | `poi` | Governance-reserved 3–5 char name | -| Migrating long-term / product-shaped | Funding | `fund` | Governance-reserved 3–5 char name | -| Migrating long-term / product-shaped | Public light person identity | `uid` | Governance-reserved 3–5 char name | -| Migrating long-term / product-shaped | Personhood | `peopl` | Governance-reserved 3–5 char name | +| Migrating to a product soon | Game (DIM2) | `dim2.dot` | Governance-reserved 3–5 char name | +| Migrating long-term / product-shaped | PoI (DIM1) | `poi.dot` | Governance-reserved 3–5 char name | +| Migrating long-term / product-shaped | Funding | `fund.dot` | Governance-reserved 3–5 char name | +| Migrating long-term / product-shaped | Public light person identity | `uid.dot` | Governance-reserved 3–5 char name | +| Migrating long-term / product-shaped | Personhood | `peopl.dot` | Governance-reserved 3–5 char name | | Not coercible to a product | Coinage | — | Deferred to a separate RFC (own layout today: `//pps//coin/{index}`, `//pps//ring-vrf/{index}`) | ### Well-known alias accounts @@ -328,14 +322,14 @@ reserved product identity from the table above. `DerivationIndex` is the same 32-byte index format as product accounts, so each domain gets its own index space. -The personhood keys live under the `peopl` domain: +The personhood keys live under the `peopl.dot` domain: ```rust // Full personhood ring-VRF key -full_personhood_key = //peopl//index_bytes(0) +full_personhood_key = //peopl.dot//index_bytes(0) // Light personhood ring-VRF key -light_personhood_key = //peopl//index_bytes(1) +light_personhood_key = //peopl.dot//index_bytes(1) ``` Existing keys migrate to these paths. Coinage's ring-VRF keys @@ -382,7 +376,7 @@ game_domain = "game" ``` > **Note:** the `game` domain is expected to go away soon. The Game is -> migrating to the `dim2` product, which will obtain its key material +> migrating to the `dim2.dot` product, which will obtain its key material > via `host_derive_entropy` (RFC-0007) instead. ### Compatibility @@ -390,7 +384,7 @@ game_domain = "game" There are no production deployments of secret-component derivations or of the `u32`-index wire types; the selector change is wire-breaking for `ProductAccountId`, `ProductProofContext`, `PaymentTopUpSource`, and -`AllocatableResource`, and is made freely, with no migration path. Existing ring-VRF keys move to their `peopl` +`AllocatableResource`, and is made freely, with no migration path. Existing ring-VRF keys move to their `peopl.dot` paths; deployed encryption keys are handled by the encryption RFC. ## Drawbacks @@ -398,7 +392,7 @@ paths; deployed encryption keys are handled by the encryption RFC. - **One new Accounts Protocol message**, amortized to one round trip per product per Host. - **No path-string tooling round trip.** The 32-byte index junction cannot be - typed as a path segment, so `//product//browse/5` in stock tooling + typed as a path segment, so `//product//browse.dot/5` in stock tooling (`polkadot-js`, `subkey`) does not derive index `5` (`index_bytes(5)`) ## Alternatives diff --git a/docs/rfcs/0024-personhood-as-product.md b/docs/rfcs/0024-personhood-as-product.md index 67e7504ff..6ca217b6c 100644 --- a/docs/rfcs/0024-personhood-as-product.md +++ b/docs/rfcs/0024-personhood-as-product.md @@ -24,7 +24,7 @@ A proof is a bearer token for its context's alias and a signature is a bearer to **Personhood is welded into the Host.** RFC-0004 §"Host member-key selection" requires every Host to define the PoP ring collection internally, choose a member key corresponding to the requested `RingLocation`, fall back to the PoP key when correspondence is undeterminable, and tiebreak stably. `truapi-server` implements exactly that with the ring identities compiled in (`rust/crates/truapi-server/src/runtime/signing_host/ring_vrf.rs`: `FULL_PERSON_COLLECTION`, `LITE_PERSON_COLLECTION`, `enum PersonKey { Full, Lite }`). So every change to how a person key is derived, registered, renewed, or recovered is a Host release. -A personhood product must instead own the full and light keys — under RFC-0022, the `peopl` domain of the ring-VRF tree — while telling the Host and Account Holder enough to keep serving the app's own personhood-dependent features, and lending its keys and aliases to other products. The binding constraint across all of it: **no consumer may know which key is used**, not the app and not a calling product. +A personhood product must instead own the full and light keys — under RFC-0022, the `peopl.dot` domain of the ring-VRF tree — while telling the Host and Account Holder enough to keep serving the app's own personhood-dependent features, and lending its keys and aliases to other products. The binding constraint across all of it: **no consumer may know which key is used**, not the app and not a calling product. **The obstacle** is that the member keys serve three overlapping classes of work, and only one is not extractable: @@ -108,7 +108,7 @@ fn list_ring_vrf_keys( - **Registration declares intent, not membership.** It means "this is the key I will use for that ring", not "the user is a person"; membership is still discovered only by attempting a proof, which returns `NotMember` (RFC-0004). This keeps the registry from being a personhood oracle. - **The public key is owner-visible by default, permissioned cross-product**, because a member public key is linkable across every ring it appears in. -RFC-0022 already pins `//peopl//index_bytes(0)` as the full personhood key and `index_bytes(1)` as the light one. Under this RFC those constants are the personhood product's own implementation detail, expressed to everyone else as two registry entries. +RFC-0022 already pins `//peopl.dot//index_bytes(0)` as the full personhood key and `index_bytes(1)` as the light one. Under this RFC those constants are the personhood product's own implementation detail, expressed to everyone else as two registry entries. ### Proofs, aliases, and signatures take an explicit key handle @@ -186,18 +186,18 @@ enum RingVrfSignErr { ### Cross-product discovery -A game product producing a proof with the full personhood key, under its own airdrop context — abstracted by the product SDK, not the Host. It works because `peopl` has allowlisted `game`; see [Using a foreign key](#using-a-foreign-key-means-trusting-the-caller). +A game product producing a proof with the full personhood key, under its own airdrop context — abstracted by the product SDK, not the Host. It works because `peopl.dot` has allowlisted `game.dot`; see [Using a foreign key](#using-a-foreign-key-means-trusting-the-caller). ```mermaid sequenceDiagram - participant G as game + participant G as game.dot participant H as Host - participant P as peopl registry + participant P as peopl.dot registry - G->>H: list_ring_vrf_keys("peopl", Anonymized) - H-->>G: [ { handle: (peopl, ?), rings: [People, PeopleLite] } ] + G->>H: list_ring_vrf_keys("peopl.dot", Anonymized) + H-->>G: [ { handle: (peopl.dot, ?), rings: [People, PeopleLite] } ] G->>G: select the entry whose rings contain the People ring - G->>H: create_account_proof(handle, game/airdrop, People, message) + G->>H: create_account_proof(handle, game.dot/airdrop, People, message) H-->>G: proof + contextual_alias + ring_index + ring_revision ``` @@ -205,7 +205,7 @@ sequenceDiagram No product needs "try full, fall back to light" today, so this RFC does not specify one. If a product ever does, the fallback belongs in the **product SDK**, not in the Host and not reimplemented per product — the Host no longer has the information to choose, and duplicating the retry across consumers is how the selection contract became fragile in the first place. -**No product should assume a key index of another product.** The index is the owner's implementation detail; consumers select by declared `RingLocation` and treat the handle as opaque. Hardcoding `(peopl, 0)` breaks the moment the owner adds a key. +**No product should assume a key index of another product.** The index is the owner's implementation detail; consumers select by declared `RingLocation` and treat the handle as opaque. Hardcoding `(peopl.dot, 0)` breaks the moment the owner adds a key. This is a **convention, not an enforceable rule**, and the RFC does not pretend otherwise. The index is part of the handle, so any caller that can list the registry can read it and hardcode it; `Anonymized` disclosure withholds the member public key, not the index. Hiding the index would mean the handle could no longer name a derivation slot, which is the whole point of it. So this lands as an implementation note for the **product SDK**, which should expose selection-by-ring and never surface a raw index to product code. @@ -335,7 +335,7 @@ A Host holding a current registry snapshot answers `list` locally. `RingVrfProof > **A Host MUST NOT derive a member secret for a `(product, index)` pair absent from its registry.** -This needs saying because domain entropy makes derivation _unconditional_: given the entropy of `//peopl`, a Host can compute the member secret at index 7, or 4711, or any other, since derivation is pure arithmetic and nothing about holding the entropy distinguishes a meaningful index from a meaningless one. The registry supplies that distinction. Serve an unregistered index and the phone has no record the key exists — it cannot include it in slot assignment, list it in the inventory, or answer "what is this key used for". So the entropy lets a Host **derive** a key the registry already lists; only registration, which always reaches the phone, brings one into **existence**. +This needs saying because domain entropy makes derivation _unconditional_: given the entropy of `//peopl.dot`, a Host can compute the member secret at index 7, or 4711, or any other, since derivation is pure arithmetic and nothing about holding the entropy distinguishes a meaningful index from a meaningless one. The registry supplies that distinction. Serve an unregistered index and the phone has no record the key exists — it cannot include it in slot assignment, list it in the inventory, or answer "what is this key used for". So the entropy lets a Host **derive** a key the registry already lists; only registration, which always reaches the phone, brings one into **existence**. #### Answering while the phone is backgrounded @@ -363,7 +363,7 @@ AutoSigning { - **Cross-product key use is an all-or-nothing trust decision.** An allowlisted product can do anything the owner's key can do — prove under any context, sign any message — when what an owner wants to express is narrower. The blind-signing risk is contained by whom the owner trusts rather than by what the caller can ask for, which is why this is explicitly an interim position. - **Bundling ring VRF entropy into AutoSigning widens one grant.** "Sign transactions without prompting me" and "produce personhood proofs offline" become one decision, and the second is arguably the stronger. Accepted deliberately: two grants would mean two authorization surfaces for what the user experiences as one relationship. - **The registry is new distributed state**, agreed between the registering product, the caching Host, and the owning Account Holder; a stale Host returns `KeyNotRegistered` for a key that exists. Idempotent registration and a single authority keep it diagnosable, but it replaces a compile-time constant. -- **Registration leaks intent.** An anonymized listing still says "`peopl` has a key it intends for the People ring" — not proof of membership, but a consumer learns the user has attempted full personhood before any proof is requested. The one privacy cost accepted for cheap discovery. +- **Registration leaks intent.** An anonymized listing still says "`peopl.dot` has a key it intends for the People ring" — not proof of membership, but a consumer learns the user has attempted full personhood before any proof is requested. The one privacy cost accepted for cheap discovery. - **The silent happy path depends on the manifest RFC**: until the allowlist exists, each cross-product call in the alias flow produces a one-time prompt. - **The key handle overloads `ProductAccountId`**, which now names both an sr25519 product account and a ring VRF slot in a different tree at the same `(product, index)`. @@ -379,7 +379,7 @@ AutoSigning { ## Prior Art and References - [RFC-0004 — Redesign `account_create_account_proof`](0004-ringlocation-redesign.md) — `RingLocation`, `ProductProofContext`, the context derivation, and the member-key selection contract this RFC deletes. Its "Out of scope: explicit member-key management … left to a future RFC" is this RFC. -- **RFC-0022 — Account key derivations** ([PR #296](https://github.com/paritytech/host-rust-core/pull/296)) — the ring-VRF tree, `Either` indices, the reserved `peopl` identity, and the `AutoSigning` payload this RFC extends. Its deferral of well-known alias accounts is resolved here. +- **RFC-0022 — Account key derivations** ([PR #296](https://github.com/paritytech/host-rust-core/pull/296)) — the ring-VRF tree, `Either` indices, the reserved `peopl.dot` identity, and the `AutoSigning` payload this RFC extends. Its deferral of well-known alias accounts is resolved here. - **RFC-0023 — sr25519 VRF signing for product accounts** ([PR #301](https://github.com/paritytech/host-rust-core/pull/301)) — the complementary non-member path, where this RFC's ring VRF path serves members. - [RFC-0020 — `signing_create_transaction` and its AP mirror](0020-create-transaction.md) — the pattern of specifying a TrUAPI call together with its AP companion, followed here. - [RFC-0010 — W3S Allowance Management](0010-allowance.md) — AutoSigning and the PGAS / Bulletin / SSS flows that consume the person key. diff --git a/rust/crates/truapi-host-cli/README.md b/rust/crates/truapi-host-cli/README.md index 7abbaa89b..1e50ffa06 100644 --- a/rust/crates/truapi-host-cli/README.md +++ b/rust/crates/truapi-host-cli/README.md @@ -21,7 +21,7 @@ One binary, `truapi-host`: | --- | --- | | `pairing-host` | Seedless host: serves product frames, emits pairing deeplinks, and can run product scripts. | | `signing-host` | Wallet-local host: owns signer identity, can run product scripts, accepts pairing deeplinks, registers statement allowance on-chain, signs. | -| `identity-check` | Probe the root and canonical `uid` identity account for a registered username. | +| `identity-check` | Probe the root and canonical `uid.dot` identity account for a registered username. | | `alloc-check` | Diagnose (or `--submit`) on-chain statement-store allowance: ring membership, chosen slot, and the `set_statement_store_account` extrinsic. On a full period it prints each occupied slot's age and which one would be replaced. | | `pgas-check` | Diagnose (or `--submit`) an Asset Hub PGAS allowance claim: ring membership on People, whether Asset Hub has imported that ring revision, the day's first unclaimed slot, and the `Pgas.claim_pgas` extrinsic. | @@ -42,6 +42,34 @@ default, so starting either host does not reserve a TCP port. Pass `--frame-listen 127.0.0.1:0` to expose an ordinary loopback WebSocket instead; this is required for browser clients, which cannot open filesystem sockets. +### Browser products + +A browser product reaches that socket through `@parity/truapi`'s sandbox. Start +the host on a fixed port, and point the product at it before anything else +touches the client: + +```bash +truapi-host signing-host --frame-listen 127.0.0.1:9955 --product-id my-product.dot +``` + +```ts +import { connectWebSocketHost } from "@parity/truapi/sandbox"; + +connectWebSocketHost("ws://127.0.0.1:9955"); +``` + +The product is then detected as hosted and holds the real product account for +its own `.dot` name, so signing, statements, entropy, permissions and storage +all take their production code paths with no phone involved. `--product-id` is +not optional: the host derives the product account from it and refuses to *sign* +for any other product id, and a mismatch only surfaces later, as a +`PermissionDenied` on the first signature. + +Two players on one machine means two hosts, each with its own session and port +(`--session bob --frame-listen 127.0.0.1:9956`), and a second product instance +pointed at the second port. Sessions isolate the signer, the storage and the +permissions. + The signing host opens an interactive terminal where you can paste a pairing link, type `/pair `, run `/script`, or use `/help` to discover the available commands. It uses `--mnemonic` / `HOST_CLI_SIGNER_MNEMONIC` if set. @@ -218,10 +246,9 @@ res.match( ); ``` -`--product-id` (a dotNS name ending in `.dot` or `.paseo`, or a `localhost` +`--product-id` (a dotNS name ending in `.dot`, `.paseo`, or `.test`, or a `localhost` identifier; default -`headless-playground.dot`) sets the initial product. The dotNS TLD is stripped -during normalization, so `.dot` and `.paseo` names scope the same product. `/product ` changes it +`headless-playground.dot`) sets the initial product. `/product ` changes it for the lifetime of the process. Switching disconnects active product WebSockets so clients reconnect with a new product context; the network, pairing relationship, signing-host session, and wallet identity stay active. @@ -379,7 +406,7 @@ The real statement store enforces per-account allowance. Before pairing, the signing host grants it on-chain exactly as a real client does: it proves its personhood ring membership with a bandersnatch ring-VRF and submits an unsigned General (v5) `Resources.set_statement_store_account` extrinsic for each account -that submits statements — its RFC-0022 `uid` identity account and the +that submits statements — its RFC-0022 `uid.dot` identity account and the pairing host's per-pairing device key. The shared native implementation lives in `truapi-server/src/runtime/statement_allowance/` (metadata-driven signed-extension encoding, ring fetch, slot scan, ring-VRF proof, extrinsic @@ -426,13 +453,69 @@ HOST_CLI_SIGNER_MNEMONIC="spin battle …" truapi-host signing-host --deeplink ' truapi-host alloc-check --mnemonic "spin battle …" --lookback 100 ``` -Both hosts take `--network` (default `paseo-next-v2`). The network preset owns -the identity backend URL, the People, Bulletin and Asset Hub RPCs, and their -genesis hashes; there is -no public `--statement-store` flag. Both also accept `--frame-listen
` +Both hosts take `--network`, either `paseo-next-v2` (default) or `previewnet`. +The network preset owns the identity backend URL, the People, Bulletin and Asset +Hub RPCs, and their genesis hashes; there is +no public `--statement-store` flag. Pick `previewnet` when a product's runtime +descriptors target previewnet, so its statements, its host chain routes and its +own chain reads all land on one network. Sessions are per preset, so each network +gets its own signer identity on the same machine. + +One limit on `previewnet` today: its identity backend requires a bearer token for +write requests, so auto-provisioning a fresh lite username fails with + +``` +username registration failed (401 Unauthorized): Missing Authorization Header +``` + +Reads against it work, and everything that does not go through the backend works +normally, so use `--mnemonic` (or `HOST_CLI_SIGNER_MNEMONIC`) with an account that +already carries a previewnet username. `paseo-next-v2` still provisions on its +own, unauthenticated. Both also accept `--frame-listen
` to opt into a TCP product-frame WebSocket; without it, the CLI creates and cleans up a unique temporary Unix socket. +## Serving a dev server (one process, no terminal) + +`signing-host --serve` runs the host as a background service instead of a +terminal UI, so a dev server or test harness can supervise it: + +```bash +truapi-host signing-host --serve \ + --frame-listen 127.0.0.1:9955 \ + --product-id myapp.dot \ + --auto-accept +``` + +It needs no TTY, initialises the signer, and stays up until stopped. Output is +one line per event: + +``` +✓ Paired with headlessyvqhet.43 +✓ Signing host ready +• Listening for product frames + ws://127.0.0.1:9955 +• Serving product frames until stopped + ws://127.0.0.1:9955 + Confirmations are approved automatically +``` + +Wait for `Serving product frames until stopped` before pointing a product at the +endpoint. That line is last in every case, and it is the only one that means +both halves are up: the frame socket accepts connections well before a signer +exists, and `Signing host ready` can arrive either side of it depending on +whether the session was cached or is being registered. A first run registers a +lite username and the statement-store allowance on-chain, which can take +minutes. + +Stopping it: Ctrl-C is handled, so the host logs its own shutdown. `SIGTERM` +ends the process, which is what a supervising dev server sends. + +`--auto-accept` is effectively required, because a process with no terminal has +nowhere to prompt: confirmations are denied instead, and the startup line says +so. `--serve` cannot be combined with `--script` or `exec`, which are the +one-shot modes. + ## Scope / gaps - **Chain methods** route to real `wss://` nodes from the selected `--network`. diff --git a/rust/crates/truapi-host-cli/SPEC.md b/rust/crates/truapi-host-cli/SPEC.md index 1abdee337..c96c79dc0 100644 --- a/rust/crates/truapi-host-cli/SPEC.md +++ b/rust/crates/truapi-host-cli/SPEC.md @@ -340,14 +340,12 @@ use `exec '/script '` instead. `/copy` is unavailable. `/clear` and Accepted product identifiers are: -- a name ending in a dotNS TLD (`.dot` or `.paseo`); +- a name ending in a dotNS TLD (`.dot`, `.paseo`, or `.test`); - `localhost`; or - a string beginning with `localhost:`. -Identifiers are trimmed, Unicode-NFC normalized, and lowercased, and a -recognized dotNS TLD is stripped, so the canonical product id is TLD-free and -identical on every network. For example, `" Dotli.DOT "` and `dotli.paseo` -both become `dotli`. +Identifiers are trimmed, Unicode-NFC normalized, and lowercased. For example, +`" Dotli.DOT "` becomes `dotli.dot`. Other identifiers, including an ordinary `example.com`, are rejected. @@ -726,9 +724,9 @@ Before a signing host answers a link, it: 1. ensures a signer; 2. decodes the V2 handshake; -3. derives its RFC-0022 `uid` identity account; +3. derives its RFC-0022 `uid.dot` identity account; 4. reads the pairing device Statement Store account from the proposal; -5. finds the signer's rings through the pairing-attestation bootstrap `peopl` +5. finds the signer's rings through the pairing-attestation bootstrap `peopl.dot` keys, index 0 for `People` and index 1 for `LitePeople`, scanning back from the current ring in each (RFC-0024 operational key selection uses the registry instead); @@ -811,7 +809,7 @@ A new auto account: 1. acquires `accounts.json.lock`; 2. generates a 12-word mnemonic; -3. derives the RFC-0022 `uid` index-0 sr25519 identity account; +3. derives the RFC-0022 `uid.dot` index-0 sr25519 identity account; 4. chooses `auto-` as its local name; 5. tries up to eight available Lite username bases; 6. saves a pending account record; @@ -1067,7 +1065,7 @@ state, and other role-owned runtime data. - network id; - plaintext BIP-39 mnemonic; - final Lite username; -- RFC-0022 `uid` index-0 public key and address; +- RFC-0022 `uid.dot` index-0 public key and address; - creation timestamp; - attested state; and - exhausted Statement Store periods. @@ -1099,9 +1097,11 @@ selection files. ## 14. Network and transport -### 14.1 Network preset +### 14.1 Network presets -v0.1 supports only `paseo-next-v2`. +`--network` selects one of two presets. `paseo-next-v2` is the default. + +#### `paseo-next-v2` | Purpose | Value | | --- | --- | @@ -1113,6 +1113,34 @@ v0.1 supports only `paseo-next-v2`. | Asset Hub RPC | `wss://paseo-asset-hub-next-rpc.polkadot.io` | | Asset Hub genesis | `0x23e730eb1c6fecae09c917439a5038cb6122d0d48980e8b9bbf0ff56f94a2ca6` | +#### `previewnet` + +The network that front-runs `paseo-next-v2`: it carries the runtime that reaches +nextv2 later, and it is where products with previewnet descriptors do their +on-chain testing. Its identity backend is the same service on its staging +environment (`/api/v1/version` reports `"environment": "staging"`). + +| Purpose | Value | +| --- | --- | +| Identity backend | `https://polkadot-app-stg.parity.io/api/v1` | +| People RPC | `wss://previewnet.substrate.dev/people` | +| People genesis | `0x34999c298555e25bf17a7f3ea20efe7f6fdab1dfec7f808fbcfd36ca8aa5d220` | +| Bulletin RPC | `wss://previewnet.substrate.dev/bulletin` | +| Bulletin genesis | `0x1144acd27f0e5b2c88da7dc12c111e396983dec036ccfb42da5bbb0dd7104e89` | +| Asset Hub RPC | `wss://previewnet.substrate.dev/asset-hub` | +| Asset Hub genesis | `0x627f54413120c81161261b2ca87f60f0020963107dc28367491e09ec2dd29659` | + +Sessions are per network (`SessionCatalog::new` keys on the preset id), so a +signer provisioned on one preset is not visible from the other. Two presets means +two identities on one machine, which is deliberate: the lite username and the +statement-store allowance are per chain. + +`previewnet`'s identity backend requires a bearer token for write requests, which +the CLI does not send, so auto-managed account creation fails there with +`401 Unauthorized (Missing Authorization Header)`. Reads against the backend +succeed, and every non-backend path is unaffected, so a `previewnet` signer needs +`--mnemonic` for an account that already holds a username on that chain. + There are no public endpoint override flags. Every role the preset serves — People, Bulletin and Asset Hub — is always routed, @@ -1440,7 +1468,7 @@ truapi-host identity-check \ The command derives and queries two accounts: - root; and -- RFC-0022 `//product//uid/index_bytes(0)`. +- RFC-0022 `//product//uid.dot/index_bytes(0)`. For each it prints one of: diff --git a/rust/crates/truapi-host-cli/src/main.rs b/rust/crates/truapi-host-cli/src/main.rs index 09d5ff779..0cc544729 100644 --- a/rust/crates/truapi-host-cli/src/main.rs +++ b/rust/crates/truapi-host-cli/src/main.rs @@ -57,7 +57,7 @@ use crate::terminal_ui::{ }; /// Default product served by the pairing host's frame endpoint. Product ids -/// must be a dotNS name (`.dot` or `.paseo`) or a `localhost` identifier +/// must be a dotNS name (`.dot`, `.paseo`, or `.test`) or a `localhost` identifier /// (host-spec product id). const DEFAULT_PRODUCT_ID: &str = "headless-playground.dot"; /// Deeplink scheme advertised by the pairing host. diff --git a/rust/crates/truapi-platform/src/lib.rs b/rust/crates/truapi-platform/src/lib.rs index 7defcb6e2..dea4953f7 100644 --- a/rust/crates/truapi-platform/src/lib.rs +++ b/rust/crates/truapi-platform/src/lib.rs @@ -250,8 +250,11 @@ pub fn is_product_identifier(identifier: &str) -> bool { normalize_product_identifier(identifier).is_ok() } -/// Top-level domains that dotNS deployments register product names under. -pub const DOTNS_TLDS: &[&str] = &["dot", "paseo"]; +/// Top-level domains that dotNS deployments register product names under: +/// `dot` on Polkadot, `paseo` on Paseo, `test` on Previewnet. Each network +/// declares its TLD via the dotNS registry `tld()` view; this list mirrors +/// the deployed networks. +pub const DOTNS_TLDS: &[&str] = &["dot", "paseo", "test"]; /// Whether `normalized` ends in one of [`DOTNS_TLDS`]. Expects an /// already-lowercased host with no trailing root dot. diff --git a/rust/crates/truapi-server/README.md b/rust/crates/truapi-server/README.md index eb4ef5d3b..388997266 100644 --- a/rust/crates/truapi-server/README.md +++ b/rust/crates/truapi-server/README.md @@ -190,7 +190,7 @@ role-specific lifecycle, so no method exists on a role that can't mean it: - **`SigningHost`** (wallet-local): signs on device from local BIP-39 entropy, no pairing flow. `signing_host/local_activation.rs` establishes a session from host-held secret material. Its public identity is the RFC-0022 - `uid` index-0 product account (RFC-0022 TLD-free id). RFC-0024 ring-VRF keys are explicit, + `uid.dot` index-0 product account. RFC-0024 ring-VRF keys are explicit, product-owned registry entries; aliases, proofs, direct signatures, and internal personhood flows use the requested or user-selected registered key without a compiled-in fallback. It resolves RFC-0004 `RingLocation` values From beffa2b96ad1f36b3460a0869424d5d2e5032fce Mon Sep 17 00:00:00 2001 From: Tiago Tavares Date: Thu, 20 Aug 2026 13:49:51 +0100 Subject: [PATCH 03/10] docs: link the hardcoded built-in ids and TLD list from the open questions --- docs/design/name-identifiers.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/design/name-identifiers.md b/docs/design/name-identifiers.md index 1db168dc3..f0eb9eb19 100644 --- a/docs/design/name-identifiers.md +++ b/docs/design/name-identifiers.md @@ -80,15 +80,17 @@ and the #### Open Questions -- The built-in identifiers `uid.dot` and `peopl.dot` are pinned to `.dot` on - every network, which gives the user one shared identity account and - personhood domain across networks. Consistency with this convention says - `uid.{tld}` per network, but that re-pins the mobile interop vectors and - needs the Account Holder to move in lockstep. -- The recognized TLD list is compiled in (`DOTNS_TLDS`) while the registry - already exposes the truth per network through `tld()`. The host should - eventually learn the TLD from its configured network instead of a hardcoded - list. +- The built-in identifiers + [`uid.dot` and `peopl.dot`](../../rust/crates/truapi-server/src/host_logic/product_account.rs) + are hardcoded with `.dot` on every network, which gives the user one shared + identity account and personhood domain across networks. Consistency with + this convention says `uid.{tld}` per network, but that re-pins the mobile + interop vectors and needs the Account Holder to move in lockstep. +- The recognized TLD list is compiled in + ([`DOTNS_TLDS`](../../rust/crates/truapi-platform/src/lib.rs)) while the + registry already exposes the truth per network through `tld()`. The host + should eventually learn the TLD from its configured network instead of a + hardcoded list. - Graduation needs its own design: a product that wants to carry state from testnet to mainnet needs a registered migration or alias, not implicit identity. From 78dc5c91c032ad9c7dadd014b9d30ba39639dd1f Mon Sep 17 00:00:00 2001 From: Tiago Tavares Date: Thu, 20 Aug 2026 14:16:13 +0100 Subject: [PATCH 04/10] feat(platform): declare the network dotNS TLD in HostChainSet --- docs/design/name-identifiers.md | 14 +++++++++----- .../Sources/TrUAPIHost/truapi_platform.swift | 12 ++++++++++++ .../truapi-codegen/tests/golden/host-callbacks.ts | 7 +++++++ rust/crates/truapi-host-cli/src/network.rs | 5 +++++ rust/crates/truapi-platform/src/lib.rs | 3 +++ .../truapi-server/src/host_logic/features.rs | 2 ++ rust/crates/truapi-server/src/native.rs | 3 +++ rust/crates/truapi-server/src/test_support.rs | 1 + rust/crates/truapi-server/tests/common/mod.rs | 1 + 9 files changed, 43 insertions(+), 5 deletions(-) diff --git a/docs/design/name-identifiers.md b/docs/design/name-identifiers.md index f0eb9eb19..152a827ad 100644 --- a/docs/design/name-identifiers.md +++ b/docs/design/name-identifiers.md @@ -88,9 +88,13 @@ and the interop vectors and needs the Account Holder to move in lockstep. - The recognized TLD list is compiled in ([`DOTNS_TLDS`](../../rust/crates/truapi-platform/src/lib.rs)) while the - registry already exposes the truth per network through `tld()`. The host - should eventually learn the TLD from its configured network instead of a - hardcoded list. + registry already exposes the truth per network through `tld()`. Hosts now + declare their network TLD through `HostChainSet.tld` in the + `supported_chains` syscall, mirrored from the + [network presets](../../rust/crates/truapi-host-cli/src/network.rs). The + remaining step is consuming it for validation, which stays on the compiled + union list because SCALE decode and the exported `parse_navigate` FFI have + no configuration in scope. - Graduation needs its own design: a product that wants to carry state from testnet to mainnet needs a registered migration or alias, not implicit identity. @@ -100,8 +104,8 @@ and the | Use Case | Description | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Navigation | A host resolves the name a user typed or followed into the content it should load, via [`NavigateDecision`](../../rust/crates/truapi-server/src/host_logic/dotns.rs). Here the name is an address rather than an identity, and it is used verbatim. | -| Product accounts | The account tree of a product hangs off its identifier: `//product//{nameId}/{index}` ([RFC-0022](../rfcs/0022-account-derivations.md)), implemented in [`product_account.rs`](../../rust/crates/truapi-server/src/host_logic/product_account.rs). The built-ins `uid.dot` and `peopl.dot` are reserved identifiers in the same tree. | +| Product accounts | The account tree of a product hangs off its identifier: `//product//{nameId}/{index}` ([RFC-0022](../rfcs/0022-account-derivations.md)), implemented in [`product_account.rs`](../../rust/crates/truapi-server/src/host_logic/product_account.rs). The built-ins `uid.dot` and `peopl.dot` are reserved identifiers in the same tree. | | Ring contexts | A personhood proof carries the identifier of the product it was made for, so no other product can replay it. [The ring-VRF signer](../../rust/crates/truapi-server/src/runtime/signing_host/ring_vrf.rs) builds the proof context ([RFC-0004](../rfcs/0004-ringlocation-redesign.md)), and the [ring-VRF registry](../../rust/crates/truapi-server/src/runtime/ring_vrf_registry.rs) records which keys belong to which identifier ([RFC-0024](../rfcs/0024-personhood-as-product.md)). | | Per-product entropy | Each product gets deterministic secret material ([RFC-0007](../rfcs/0007-derive-entropy.md)), and the identifier is what separates one product entropy space from another, in [`entropy.rs`](../../rust/crates/truapi-server/src/host_logic/entropy.rs). | | Permissions and storage | Everything a host remembers about a product, from consent grants to stored values, sits under a `CoreStorageKey` built from the identifier, in [`truapi-platform`](../../rust/crates/truapi-platform/src/lib.rs). | -| User identity | The primary username a product may request ([RFC-0015](../rfcs/0015-get-user-id.md)) is itself a name identifier, and it points at the `uid.dot` identity account in [`product_account.rs`](../../rust/crates/truapi-server/src/host_logic/product_account.rs). | +| User identity | The primary username a product may request ([RFC-0015](../rfcs/0015-get-user-id.md)) is itself a name identifier, and it points at the `uid.dot` identity account in [`product_account.rs`](../../rust/crates/truapi-server/src/host_logic/product_account.rs). | diff --git a/ios/truapi-host/Sources/TrUAPIHost/truapi_platform.swift b/ios/truapi-host/Sources/TrUAPIHost/truapi_platform.swift index 9e3c170f6..963c8e0b0 100644 --- a/ios/truapi-host/Sources/TrUAPIHost/truapi_platform.swift +++ b/ios/truapi-host/Sources/TrUAPIHost/truapi_platform.swift @@ -858,6 +858,11 @@ public struct HostChainSet: Equatable, Hashable { * Ecosystem the host is configured for, e.g. "polkadot", "paseo". */ public var network: String + /** + * dotNS TLD the network's registry declares via its `tld()` view, e.g. + * `dot`, `paseo`, `test`. `None` when the host does not know it. + */ + public var tld: String? /** * Chains this host serves, keyed by protocol role. */ @@ -869,10 +874,15 @@ public struct HostChainSet: Equatable, Hashable { /** * Ecosystem the host is configured for, e.g. "polkadot", "paseo". */network: String, + /** + * dotNS TLD the network's registry declares via its `tld()` view, e.g. + * `dot`, `paseo`, `test`. `None` when the host does not know it. + */tld: String?, /** * Chains this host serves, keyed by protocol role. */chains: [HostChainEntry]) { self.network = network + self.tld = tld self.chains = chains } @@ -893,12 +903,14 @@ public struct FfiConverterTypeHostChainSet: FfiConverterRustBuffer { return try HostChainSet( network: FfiConverterString.read(from: &buf), + tld: FfiConverterOptionString.read(from: &buf), chains: FfiConverterSequenceTypeHostChainEntry.read(from: &buf) ) } public static func write(_ value: HostChainSet, into buf: inout [UInt8]) { FfiConverterString.write(value.network, into: &buf) + FfiConverterOptionString.write(value.tld, into: &buf) FfiConverterSequenceTypeHostChainEntry.write(value.chains, into: &buf) } } diff --git a/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts b/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts index 75a025d0f..ff9cd154d 100644 --- a/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts +++ b/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts @@ -232,6 +232,12 @@ export interface HostChainSet { */ network: string; + /** + * dotNS TLD the network's registry declares via its `tld()` view, e.g. + * `dot`, `paseo`, `test`. ``undefined`` when the host does not know it. + */ + tld?: string; + /** * Chains this host serves, keyed by protocol role. */ @@ -615,6 +621,7 @@ export const HostChainSet: S.Codec = S.lazy( (): S.Codec => S.Struct({ network: S.str, + tld: S.Option(S.str), chains: S.Vector(HostChainEntry), }) as S.Codec, ); diff --git a/rust/crates/truapi-host-cli/src/network.rs b/rust/crates/truapi-host-cli/src/network.rs index f38854b75..4b3c06e4e 100644 --- a/rust/crates/truapi-host-cli/src/network.rs +++ b/rust/crates/truapi-host-cli/src/network.rs @@ -25,6 +25,7 @@ impl Network { match self { Self::PaseoNextV2 => NetworkConfig { id: "paseo-next-v2", + tld: "paseo", identity_backend_base: "https://identity-backend-next.parity-testnet.parity.io/api/v1", people_ws: "wss://paseo-people-next-system-rpc.polkadot.io", bulletin_ws: "wss://paseo-bulletin-next-rpc.polkadot.io", @@ -42,6 +43,7 @@ impl Network { }, Self::Previewnet => NetworkConfig { id: "previewnet", + tld: "test", identity_backend_base: "https://polkadot-app-stg.parity.io/api/v1", people_ws: "wss://previewnet.substrate.dev/people", bulletin_ws: "wss://previewnet.substrate.dev/bulletin", @@ -113,6 +115,8 @@ const PREVIEWNET_CHAIN_ENDPOINTS: &[ChainEndpoint] = &[ #[derive(Debug, Clone, Copy)] pub struct NetworkConfig { pub id: &'static str, + /// dotNS TLD the network's registry declares, mirroring its `tld()` view. + pub tld: &'static str, pub identity_backend_base: &'static str, pub people_ws: &'static str, #[allow(dead_code)] @@ -155,6 +159,7 @@ impl NetworkConfig { pub fn host_chain_set(&self) -> HostChainSet { HostChainSet { network: self.id.to_string(), + tld: Some(self.tld.to_string()), chains: vec![ HostChainEntry { identifier: ChainIdentifier::People, diff --git a/rust/crates/truapi-platform/src/lib.rs b/rust/crates/truapi-platform/src/lib.rs index dea4953f7..b6a708bdb 100644 --- a/rust/crates/truapi-platform/src/lib.rs +++ b/rust/crates/truapi-platform/src/lib.rs @@ -1069,6 +1069,9 @@ pub struct HostChainEntry { pub struct HostChainSet { /// Ecosystem the host is configured for, e.g. "polkadot", "paseo". pub network: String, + /// dotNS TLD the network's registry declares via its `tld()` view, e.g. + /// `dot`, `paseo`, `test`. `None` when the host does not know it. + pub tld: Option, /// Chains this host serves, keyed by protocol role. pub chains: Vec, } diff --git a/rust/crates/truapi-server/src/host_logic/features.rs b/rust/crates/truapi-server/src/host_logic/features.rs index f4e44c43d..2255204ab 100644 --- a/rust/crates/truapi-server/src/host_logic/features.rs +++ b/rust/crates/truapi-server/src/host_logic/features.rs @@ -65,6 +65,7 @@ mod tests { fn paseo_set() -> HostChainSet { HostChainSet { network: "paseo".to_string(), + tld: Some("paseo".to_string()), chains: vec![ HostChainEntry { identifier: ChainIdentifier::AssetHub, @@ -174,6 +175,7 @@ mod genesis_lookup_tests { fn set() -> HostChainSet { HostChainSet { network: "paseo".to_string(), + tld: Some("paseo".to_string()), chains: vec![HostChainEntry { identifier: ChainIdentifier::AssetHub, genesis_hash: [0xab; 32], diff --git a/rust/crates/truapi-server/src/native.rs b/rust/crates/truapi-server/src/native.rs index 0d22741c4..3ad7f2284 100644 --- a/rust/crates/truapi-server/src/native.rs +++ b/rust/crates/truapi-server/src/native.rs @@ -2187,6 +2187,7 @@ mod tests { fn supported_chains(&self) -> Result { Ok(truapi_platform::HostChainSet { network: "paseo".to_string(), + tld: Some("paseo".to_string()), chains: Vec::new(), }) } @@ -3212,6 +3213,7 @@ mod tests { fn supported_chains(&self) -> Result { Ok(truapi_platform::HostChainSet { network: "paseo".to_string(), + tld: Some("paseo".to_string()), chains: Vec::new(), }) } @@ -3360,6 +3362,7 @@ mod tests { fn supported_chains(&self) -> Result { Ok(truapi_platform::HostChainSet { network: "paseo".to_string(), + tld: Some("paseo".to_string()), chains: Vec::new(), }) } diff --git a/rust/crates/truapi-server/src/test_support.rs b/rust/crates/truapi-server/src/test_support.rs index af42352dc..096e7b0ca 100644 --- a/rust/crates/truapi-server/src/test_support.rs +++ b/rust/crates/truapi-server/src/test_support.rs @@ -933,6 +933,7 @@ impl PlatformFeatures for StubPlatform { async fn supported_chains(&self) -> Result { Ok(truapi_platform::HostChainSet { network: "paseo".to_string(), + tld: Some("paseo".to_string()), chains: vec![truapi_platform::HostChainEntry { identifier: v01::ChainIdentifier::AssetHub, genesis_hash: [0xaa; 32], diff --git a/rust/crates/truapi-server/tests/common/mod.rs b/rust/crates/truapi-server/tests/common/mod.rs index 78c30a95c..9c4cd5899 100644 --- a/rust/crates/truapi-server/tests/common/mod.rs +++ b/rust/crates/truapi-server/tests/common/mod.rs @@ -135,6 +135,7 @@ impl Features for WireShapePlatform { async fn supported_chains(&self) -> Result { Ok(truapi_platform::HostChainSet { network: "paseo".to_string(), + tld: Some("paseo".to_string()), chains: vec![truapi_platform::HostChainEntry { identifier: v01::ChainIdentifier::AssetHub, genesis_hash: [0xaa; 32], From e2eb721d7aaea4d42ce8eae247c1077493bf797b Mon Sep 17 00:00:00 2001 From: Tiago Tavares Date: Thu, 20 Aug 2026 14:45:43 +0100 Subject: [PATCH 05/10] feat(server): scope the reserved built-in derivations to the configured network TLD --- docs/design/name-identifiers.md | 13 +-- .../Sources/TrUAPIHost/truapi_server.swift | 64 ++++++++++++--- js/packages/truapi-host/src/runtime.ts | 5 ++ rust/crates/truapi-host-cli/README.md | 4 +- rust/crates/truapi-host-cli/SPEC.md | 10 +-- rust/crates/truapi-host-cli/src/accounts.rs | 36 +++++---- .../crates/truapi-host-cli/src/attestation.rs | 27 +++++-- rust/crates/truapi-host-cli/src/main.rs | 33 +++++--- rust/crates/truapi-platform/src/lib.rs | 34 ++++++++ rust/crates/truapi-server/README.md | 2 +- rust/crates/truapi-server/src/core.rs | 5 +- rust/crates/truapi-server/src/host_core.rs | 5 +- .../src/host_logic/attestation.rs | 20 +++-- .../src/host_logic/product_account.rs | 81 +++++++++++++------ rust/crates/truapi-server/src/native.rs | 21 +++++ rust/crates/truapi-server/src/runtime.rs | 8 +- .../truapi-server/src/runtime/services.rs | 16 ++++ .../truapi-server/src/runtime/signing_host.rs | 12 +-- .../runtime/signing_host/local_activation.rs | 2 +- .../src/runtime/signing_host/sso_responder.rs | 16 ++-- .../src/runtime/statement_store.rs | 8 +- rust/crates/truapi-server/src/wasm.rs | 25 +++++- 22 files changed, 337 insertions(+), 110 deletions(-) diff --git a/docs/design/name-identifiers.md b/docs/design/name-identifiers.md index 152a827ad..3f18d9f5f 100644 --- a/docs/design/name-identifiers.md +++ b/docs/design/name-identifiers.md @@ -80,12 +80,13 @@ and the #### Open Questions -- The built-in identifiers - [`uid.dot` and `peopl.dot`](../../rust/crates/truapi-server/src/host_logic/product_account.rs) - are hardcoded with `.dot` on every network, which gives the user one shared - identity account and personhood domain across networks. Consistency with - this convention says `uid.{tld}` per network, but that re-pins the mobile - interop vectors and needs the Account Holder to move in lockstep. +- The built-in identifiers are network-scoped: + [`uid.{tld}` and `peopl.{tld}`](../../rust/crates/truapi-server/src/host_logic/product_account.rs) + follow the `dotns_tld` a host sets in its runtime config, with `dot` as the + default so unconfigured hosts keep the mobile-pinned derivations. A paired + phone still derives `uid.dot` on every network, so hosts configured for a + non-dot TLD diverge from the Account Holder until the mobile app ships the + same rule. - The recognized TLD list is compiled in ([`DOTNS_TLDS`](../../rust/crates/truapi-platform/src/lib.rs)) while the registry already exposes the truth per network through `tld()`. Hosts now diff --git a/ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift b/ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift index 750111c8b..d8cd32bf0 100644 --- a/ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift +++ b/ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift @@ -3835,6 +3835,11 @@ public struct NativeHostRuntimeConfig: Equatable, Hashable { * Optional lite username attached to the local signing-host session. */ public var localSessionLiteUsername: String? + /** + * dotNS TLD of the host's configured network, scoping the reserved + * built-in derivations. `None` means `dot`. + */ + public var dotnsTld: String? // Default memberwise initializers are never public by default, so we // declare one manually. @@ -3865,7 +3870,11 @@ public struct NativeHostRuntimeConfig: Equatable, Hashable { */localSessionSecret: Data?, /** * Optional lite username attached to the local signing-host session. - */localSessionLiteUsername: String?) { + */localSessionLiteUsername: String?, + /** + * dotNS TLD of the host's configured network, scoping the reserved + * built-in derivations. `None` means `dot`. + */dotnsTld: String?) { self.hostName = hostName self.hostIcon = hostIcon self.hostVersion = hostVersion @@ -3875,6 +3884,7 @@ public struct NativeHostRuntimeConfig: Equatable, Hashable { self.bulletinChainGenesisHash = bulletinChainGenesisHash self.localSessionSecret = localSessionSecret self.localSessionLiteUsername = localSessionLiteUsername + self.dotnsTld = dotnsTld } @@ -3901,7 +3911,8 @@ public struct FfiConverterTypeNativeHostRuntimeConfig: FfiConverterRustBuffer { peopleChainGenesisHash: FfiConverterData.read(from: &buf), bulletinChainGenesisHash: FfiConverterData.read(from: &buf), localSessionSecret: FfiConverterOptionData.read(from: &buf), - localSessionLiteUsername: FfiConverterOptionString.read(from: &buf) + localSessionLiteUsername: FfiConverterOptionString.read(from: &buf), + dotnsTld: FfiConverterOptionString.read(from: &buf) ) } @@ -3915,6 +3926,7 @@ public struct FfiConverterTypeNativeHostRuntimeConfig: FfiConverterRustBuffer { FfiConverterData.write(value.bulletinChainGenesisHash, into: &buf) FfiConverterOptionData.write(value.localSessionSecret, into: &buf) FfiConverterOptionString.write(value.localSessionLiteUsername, into: &buf) + FfiConverterOptionString.write(value.dotnsTld, into: &buf) } } @@ -4055,6 +4067,11 @@ public struct NativeRuntimeConfig: Equatable, Hashable { * Deeplink scheme used in pairing QR payloads. */ public var pairingDeeplinkScheme: NativePairingDeeplinkScheme + /** + * dotNS TLD of the host's configured network, scoping the reserved + * built-in derivations. `None` means `dot`. + */ + public var dotnsTld: String? // Default memberwise initializers are never public by default, so we // declare one manually. @@ -4094,7 +4111,11 @@ public struct NativeRuntimeConfig: Equatable, Hashable { */localSessionLiteUsername: String?, /** * Deeplink scheme used in pairing QR payloads. - */pairingDeeplinkScheme: NativePairingDeeplinkScheme) { + */pairingDeeplinkScheme: NativePairingDeeplinkScheme, + /** + * dotNS TLD of the host's configured network, scoping the reserved + * built-in derivations. `None` means `dot`. + */dotnsTld: String?) { self.productId = productId self.executionKind = executionKind self.hostName = hostName @@ -4107,6 +4128,7 @@ public struct NativeRuntimeConfig: Equatable, Hashable { self.localSessionSecret = localSessionSecret self.localSessionLiteUsername = localSessionLiteUsername self.pairingDeeplinkScheme = pairingDeeplinkScheme + self.dotnsTld = dotnsTld } @@ -4136,7 +4158,8 @@ public struct FfiConverterTypeNativeRuntimeConfig: FfiConverterRustBuffer { bulletinChainGenesisHash: FfiConverterData.read(from: &buf), localSessionSecret: FfiConverterOptionData.read(from: &buf), localSessionLiteUsername: FfiConverterOptionString.read(from: &buf), - pairingDeeplinkScheme: FfiConverterTypeNativePairingDeeplinkScheme.read(from: &buf) + pairingDeeplinkScheme: FfiConverterTypeNativePairingDeeplinkScheme.read(from: &buf), + dotnsTld: FfiConverterOptionString.read(from: &buf) ) } @@ -4153,6 +4176,7 @@ public struct FfiConverterTypeNativeRuntimeConfig: FfiConverterRustBuffer { FfiConverterOptionData.write(value.localSessionSecret, into: &buf) FfiConverterOptionString.write(value.localSessionLiteUsername, into: &buf) FfiConverterTypeNativePairingDeeplinkScheme.write(value.pairingDeeplinkScheme, into: &buf) + FfiConverterOptionString.write(value.dotnsTld, into: &buf) } } @@ -4919,6 +4943,14 @@ enum NativeRuntimeConfigError: Swift.Error, Equatable, Hashable, Foundation.Loca * Parse failure reason. */reason: String ) + /** + * Configured dotNS TLD is not a recognized entry. + */ + case UnknownDotnsTld( + /** + * Rejected TLD value. + */tld: String + ) /** * Host icon URL used a non-HTTPS scheme. */ @@ -4992,16 +5024,19 @@ public struct FfiConverterTypeNativeRuntimeConfigError: FfiConverterRustBuffer { case 4: return .InvalidHostIcon( reason: try FfiConverterString.read(from: &buf) ) - case 5: return .InsecureHostIcon( + case 5: return .UnknownDotnsTld( + tld: try FfiConverterString.read(from: &buf) + ) + case 6: return .InsecureHostIcon( scheme: try FfiConverterString.read(from: &buf) ) - case 6: return .InvalidDeeplinkScheme( + case 7: return .InvalidDeeplinkScheme( scheme: try FfiConverterString.read(from: &buf) ) - case 7: return .InvalidProductId( + case 8: return .InvalidProductId( productId: try FfiConverterString.read(from: &buf) ) - case 8: return .LocalSessionActivation( + case 9: return .LocalSessionActivation( reason: try FfiConverterString.read(from: &buf) ) @@ -5036,23 +5071,28 @@ public struct FfiConverterTypeNativeRuntimeConfigError: FfiConverterRustBuffer { FfiConverterString.write(reason, into: &buf) - case let .InsecureHostIcon(scheme): + case let .UnknownDotnsTld(tld): writeInt(&buf, Int32(5)) + FfiConverterString.write(tld, into: &buf) + + + case let .InsecureHostIcon(scheme): + writeInt(&buf, Int32(6)) FfiConverterString.write(scheme, into: &buf) case let .InvalidDeeplinkScheme(scheme): - writeInt(&buf, Int32(6)) + writeInt(&buf, Int32(7)) FfiConverterString.write(scheme, into: &buf) case let .InvalidProductId(productId): - writeInt(&buf, Int32(7)) + writeInt(&buf, Int32(8)) FfiConverterString.write(productId, into: &buf) case let .LocalSessionActivation(reason): - writeInt(&buf, Int32(8)) + writeInt(&buf, Int32(9)) FfiConverterString.write(reason, into: &buf) } diff --git a/js/packages/truapi-host/src/runtime.ts b/js/packages/truapi-host/src/runtime.ts index e1cba8673..326563eb3 100644 --- a/js/packages/truapi-host/src/runtime.ts +++ b/js/packages/truapi-host/src/runtime.ts @@ -92,6 +92,11 @@ export interface ProductRuntimeConfig { /** URI scheme used for wallet pairing deeplinks. */ deeplinkScheme: string; }; + /** + * dotNS TLD of the host's configured network, scoping the reserved + * built-in derivations. Defaults to `dot`. + */ + dotnsTld?: string; } export interface TrUApiProductProvider extends WireProvider, CoreAdmin { diff --git a/rust/crates/truapi-host-cli/README.md b/rust/crates/truapi-host-cli/README.md index 1e50ffa06..8f8c90a90 100644 --- a/rust/crates/truapi-host-cli/README.md +++ b/rust/crates/truapi-host-cli/README.md @@ -21,7 +21,7 @@ One binary, `truapi-host`: | --- | --- | | `pairing-host` | Seedless host: serves product frames, emits pairing deeplinks, and can run product scripts. | | `signing-host` | Wallet-local host: owns signer identity, can run product scripts, accepts pairing deeplinks, registers statement allowance on-chain, signs. | -| `identity-check` | Probe the root and canonical `uid.dot` identity account for a registered username. | +| `identity-check` | Probe the root and canonical `uid.{tld}` identity account for a registered username. | | `alloc-check` | Diagnose (or `--submit`) on-chain statement-store allowance: ring membership, chosen slot, and the `set_statement_store_account` extrinsic. On a full period it prints each occupied slot's age and which one would be replaced. | | `pgas-check` | Diagnose (or `--submit`) an Asset Hub PGAS allowance claim: ring membership on People, whether Asset Hub has imported that ring revision, the day's first unclaimed slot, and the `Pgas.claim_pgas` extrinsic. | @@ -406,7 +406,7 @@ The real statement store enforces per-account allowance. Before pairing, the signing host grants it on-chain exactly as a real client does: it proves its personhood ring membership with a bandersnatch ring-VRF and submits an unsigned General (v5) `Resources.set_statement_store_account` extrinsic for each account -that submits statements — its RFC-0022 `uid.dot` identity account and the +that submits statements — its RFC-0022 `uid.{tld}` identity account and the pairing host's per-pairing device key. The shared native implementation lives in `truapi-server/src/runtime/statement_allowance/` (metadata-driven signed-extension encoding, ring fetch, slot scan, ring-VRF proof, extrinsic diff --git a/rust/crates/truapi-host-cli/SPEC.md b/rust/crates/truapi-host-cli/SPEC.md index c96c79dc0..de7de7b3d 100644 --- a/rust/crates/truapi-host-cli/SPEC.md +++ b/rust/crates/truapi-host-cli/SPEC.md @@ -724,9 +724,9 @@ Before a signing host answers a link, it: 1. ensures a signer; 2. decodes the V2 handshake; -3. derives its RFC-0022 `uid.dot` identity account; +3. derives its RFC-0022 `uid.{tld}` identity account; 4. reads the pairing device Statement Store account from the proposal; -5. finds the signer's rings through the pairing-attestation bootstrap `peopl.dot` +5. finds the signer's rings through the pairing-attestation bootstrap `peopl.{tld}` keys, index 0 for `People` and index 1 for `LitePeople`, scanning back from the current ring in each (RFC-0024 operational key selection uses the registry instead); @@ -809,7 +809,7 @@ A new auto account: 1. acquires `accounts.json.lock`; 2. generates a 12-word mnemonic; -3. derives the RFC-0022 `uid.dot` index-0 sr25519 identity account; +3. derives the RFC-0022 `uid.{tld}` index-0 sr25519 identity account; 4. chooses `auto-` as its local name; 5. tries up to eight available Lite username bases; 6. saves a pending account record; @@ -1065,7 +1065,7 @@ state, and other role-owned runtime data. - network id; - plaintext BIP-39 mnemonic; - final Lite username; -- RFC-0022 `uid.dot` index-0 public key and address; +- RFC-0022 `uid.{tld}` index-0 public key and address; - creation timestamp; - attested state; and - exhausted Statement Store periods. @@ -1468,7 +1468,7 @@ truapi-host identity-check \ The command derives and queries two accounts: - root; and -- RFC-0022 `//product//uid.dot/index_bytes(0)`. +- RFC-0022 `//product//uid.{tld}/index_bytes(0)`. For each it prints one of: diff --git a/rust/crates/truapi-host-cli/src/accounts.rs b/rust/crates/truapi-host-cli/src/accounts.rs index 538abdccf..a090dcc12 100644 --- a/rust/crates/truapi-host-cli/src/accounts.rs +++ b/rust/crates/truapi-host-cli/src/accounts.rs @@ -374,7 +374,7 @@ async fn create_auto_account( let mnemonic = Mnemonic::generate(12) .context("generate BIP-39 mnemonic")? .to_string(); - let identity = identity_from_mnemonic(&mnemonic)?; + let identity = identity_from_mnemonic(&mnemonic, network.tld)?; for attempt in 0..8 { let lite_username = generated_username(username_prefix, attempt); @@ -408,7 +408,7 @@ async fn create_auto_account( ); record.lite_username = attest_record(network, &record).await?; - wait_for_ring_membership(network.people_ws, &identity.entropy).await?; + wait_for_ring_membership(network.people_ws, &identity.entropy, network.tld).await?; record.attested = true; store.upsert(record.clone()); store.save()?; @@ -423,16 +423,19 @@ async fn ensure_record_ready( network: NetworkConfig, record: &AccountRecord, ) -> Result { - let identity = identity_from_mnemonic(&record.mnemonic)?; + let identity = identity_from_mnemonic(&record.mnemonic, network.tld)?; let mut record = record.clone(); if !record.attested { record.lite_username = attest_record(network, &record).await?; record.attested = true; } else { - record.lite_username = - attestation::registered_lite_username(network.people_ws, &identity.entropy) - .await - .with_context(|| format!("resolve Lite username for account {}", record.name))?; + record.lite_username = attestation::registered_lite_username( + network.people_ws, + &identity.entropy, + network.tld, + ) + .await + .with_context(|| format!("resolve Lite username for account {}", record.name))?; } if store .get(network.id, &record.name) @@ -441,7 +444,7 @@ async fn ensure_record_ready( store.upsert(record.clone()); store.save()?; } - wait_for_ring_membership(network.people_ws, &identity.entropy).await?; + wait_for_ring_membership(network.people_ws, &identity.entropy, network.tld).await?; Ok(record) } @@ -452,6 +455,7 @@ async fn attest_record(network: NetworkConfig, record: &AccountRecord) -> Result people_ws: network.people_ws.to_string(), entropy, username_base: record.lite_username.clone(), + dotns_tld: network.tld.to_string(), }) .await .with_context(|| format!("attest account {}", record.name))?; @@ -473,24 +477,24 @@ fn resolved_lite_username(username: &str) -> bool { /// Every personhood collection candidate for `entropy`, widest slot budget first. /// /// Both are always offered; membership is settled on chain, not from local state. -pub(crate) fn collection_candidates(entropy: &[u8]) -> Vec { +pub(crate) fn collection_candidates(entropy: &[u8], tld: &str) -> Vec { vec![ alloc::CollectionCandidate { collection: PersonhoodCollection::People, - entropy: derive_full_person_ring_vrf_entropy(entropy), + entropy: derive_full_person_ring_vrf_entropy(entropy, tld), }, alloc::CollectionCandidate { collection: PersonhoodCollection::LitePeople, - entropy: derive_lite_person_ring_vrf_entropy(entropy), + entropy: derive_lite_person_ring_vrf_entropy(entropy, tld), }, ] } -async fn wait_for_ring_membership(people_ws: &str, entropy: &[u8]) -> Result<()> { +async fn wait_for_ring_membership(people_ws: &str, entropy: &[u8], tld: &str) -> Result<()> { const MAX_ATTEMPTS: usize = 10; const SLEEP: Duration = Duration::from_secs(4); - let candidates = collection_candidates(entropy); + let candidates = collection_candidates(entropy, tld); let mut metadata = None; for attempt in 1..=MAX_ATTEMPTS { crate::terminal_ui::update_activity( @@ -587,10 +591,10 @@ struct SignerIdentity { address: String, } -fn identity_from_mnemonic(mnemonic: &str) -> Result { +fn identity_from_mnemonic(mnemonic: &str, tld: &str) -> Result { let entropy = mnemonic_entropy(mnemonic)?; - let candidate = derive_identity_keypair(&entropy) - .map_err(|err| anyhow::anyhow!("uid.dot identity derivation failed: {err}"))?; + let candidate = derive_identity_keypair(&entropy, tld) + .map_err(|err| anyhow::anyhow!("identity derivation failed: {err}"))?; let public_key = candidate.public.to_bytes(); Ok(SignerIdentity { entropy, diff --git a/rust/crates/truapi-host-cli/src/attestation.rs b/rust/crates/truapi-host-cli/src/attestation.rs index ad906e270..a413e4852 100644 --- a/rust/crates/truapi-host-cli/src/attestation.rs +++ b/rust/crates/truapi-host-cli/src/attestation.rs @@ -30,6 +30,8 @@ pub struct AttestConfig { pub entropy: Vec, /// Requested lite username base (6+ lowercase letters, no digits). pub username_base: String, + /// dotNS TLD of the target network, scoping the identity derivation. + pub dotns_tld: String, } /// Check whether a lite username base is available through the identity @@ -67,8 +69,13 @@ pub async fn attest(config: &AttestConfig) -> Result { .build()?; let verifier = fetch_verifier(&client, &config.backend_base).await?; - let registration = build_lite_registration(&config.entropy, verifier, &config.username_base) - .map_err(|reason| anyhow::anyhow!("failed to build registration params: {reason}"))?; + let registration = build_lite_registration( + &config.entropy, + verifier, + &config.username_base, + &config.dotns_tld, + ) + .map_err(|reason| anyhow::anyhow!("failed to build registration params: {reason}"))?; debug!( candidate = %registration.candidate_account_id, "attesting lite username '{}'", @@ -101,9 +108,13 @@ pub async fn attest(config: &AttestConfig) -> Result { /// Older CLI account records stored the requested username base rather than /// the final `name.discriminator` assigned by the People chain. Reading the /// consumer record repairs those records without re-attesting the account. -pub async fn registered_lite_username(people_ws: &str, entropy: &[u8]) -> Result { - let identity = derive_identity_keypair(entropy) - .map_err(|err| anyhow::anyhow!("uid.dot identity derivation failed: {err}"))?; +pub async fn registered_lite_username( + people_ws: &str, + entropy: &[u8], + tld: &str, +) -> Result { + let identity = derive_identity_keypair(entropy, tld) + .map_err(|err| anyhow::anyhow!("identity derivation failed: {err}"))?; let storage_key = format!( "0x{}", hex::encode(resources_consumers_storage_key(&identity.public.to_bytes())) @@ -119,11 +130,11 @@ pub async fn registered_lite_username(people_ws: &str, entropy: &[u8]) -> Result /// Probe the People chain for the bare root and canonical RFC-0022 `uid.dot` /// identity account, printing any `Resources.Consumers` record. Used to /// confirm a pre-onboarded account. -pub async fn check_identity(people_ws: &str, entropy: &[u8]) -> Result<()> { +pub async fn check_identity(people_ws: &str, entropy: &[u8], tld: &str) -> Result<()> { let root = derive_root_keypair_from_entropy(entropy) .map_err(|err| anyhow::anyhow!("invalid entropy: {err}"))?; - let identity = derive_identity_keypair(entropy) - .map_err(|err| anyhow::anyhow!("uid.dot identity derivation failed: {err}"))?; + let identity = derive_identity_keypair(entropy, tld) + .map_err(|err| anyhow::anyhow!("identity derivation failed: {err}"))?; for (label, public) in [ ("", root.public.to_bytes()), diff --git a/rust/crates/truapi-host-cli/src/main.rs b/rust/crates/truapi-host-cli/src/main.rs index 0cc544729..3f3b5f111 100644 --- a/rust/crates/truapi-host-cli/src/main.rs +++ b/rust/crates/truapi-host-cli/src/main.rs @@ -361,7 +361,8 @@ async fn main() -> Result<()> { let entropy = bip39::Mnemonic::parse(mnemonic.trim()) .context("invalid BIP-39 mnemonic")? .to_entropy(); - attestation::check_identity(network.config().people_ws, &entropy).await + let config = network.config(); + attestation::check_identity(config.people_ws, &entropy, config.tld).await } Command::AllocCheck { mnemonic, @@ -400,7 +401,7 @@ async fn run_pgas_check( let entropy = bip39::Mnemonic::parse(mnemonic.trim()) .context("invalid BIP-39 mnemonic")? .to_entropy(); - let candidates = accounts::collection_candidates(&entropy); + let candidates = accounts::collection_candidates(&entropy, network.tld); if submit && target.is_none() { bail!("--target is required with --submit; a claim has to credit an account"); @@ -558,7 +559,7 @@ async fn run_alloc_check( let entropy = bip39::Mnemonic::parse(mnemonic.trim()) .context("invalid BIP-39 mnemonic")? .to_entropy(); - let candidates = accounts::collection_candidates(&entropy); + let candidates = accounts::collection_candidates(&entropy, network.tld); if submit && target.is_none() { bail!("--target is required with --submit; the all-zero default is read-only"); @@ -812,6 +813,7 @@ async fn run_pairing_host( network.bulletin_genesis, DEEPLINK_SCHEME.to_string(), ) + .and_then(|config| config.with_dotns_tld(network.tld.to_string())) .context("invalid pairing host config")?; let storage_platform = platform.clone(); let chat_host = args.execution_kind.chat_host(); @@ -1113,8 +1115,12 @@ async fn start_signing_host( lite_username_prefix: None, }) .await?; - match attestation::registered_lite_username(network.people_ws, &explicit_signer.entropy) - .await + match attestation::registered_lite_username( + network.people_ws, + &explicit_signer.entropy, + network.tld, + ) + .await { Ok(user_id) => explicit_signer.lite_username = Some(user_id), Err(error) => { @@ -1212,6 +1218,7 @@ fn build_signing_runtime( network.people_genesis, network.bulletin_genesis, ) + .and_then(|config| config.with_dotns_tld(network.tld.to_string())) .context("invalid signing host config")?; let runtime = Arc::new(SigningHostRuntime::with_chat_platform( platform, @@ -1442,7 +1449,14 @@ async fn prepare_pairing_response(session: &mut SigningHostSession, deeplink: &s signer.account_name.clone(), ) }; - match register_pairing_allowances(session.network.people_ws, &entropy, deeplink).await { + match register_pairing_allowances( + session.network.people_ws, + &entropy, + deeplink, + session.network.tld, + ) + .await + { Ok(device) => { track_pairing_renewal_targets(session, device).await; return Ok(()); @@ -1787,21 +1801,22 @@ async fn register_pairing_allowances( statement_store_url: &str, entropy: &[u8], deeplink: &str, + tld: &str, ) -> Result<[u8; 32]> { use truapi_server::host_logic::product_account::derive_identity_keypair; use truapi_server::host_logic::sso::pairing::{ VersionedHandshakeProposal, decode_pairing_deeplink, }; - let identity = derive_identity_keypair(entropy) - .map_err(|e| anyhow::anyhow!("uid.dot identity derivation failed: {e}"))? + let identity = derive_identity_keypair(entropy, tld) + .map_err(|e| anyhow::anyhow!("identity derivation failed: {e}"))? .public .to_bytes(); let VersionedHandshakeProposal::V2(proposal) = decode_pairing_deeplink(deeplink).map_err(anyhow::Error::msg)?; let device = proposal.device.statement_account_id; - let candidates = accounts::collection_candidates(entropy); + let candidates = accounts::collection_candidates(entropy, tld); let rpc = alloc::rpc::RpcClient::connect(statement_store_url) .await .map_err(anyhow::Error::msg)?; diff --git a/rust/crates/truapi-platform/src/lib.rs b/rust/crates/truapi-platform/src/lib.rs index b6a708bdb..2e3284df7 100644 --- a/rust/crates/truapi-platform/src/lib.rs +++ b/rust/crates/truapi-platform/src/lib.rs @@ -54,6 +54,9 @@ pub struct HostRuntimeConfig { pub host_info: HostInfo, /// Platform metadata. pub platform_info: PlatformInfo, + /// dotNS TLD of the host's configured network, scoping the reserved + /// built-in derivations (`uid.{tld}`, `peopl.{tld}`). `None` means `dot`. + pub dotns_tld: Option, } /// Pairing-host runtime configuration supplied by the embedding host. @@ -162,11 +165,29 @@ impl HostRuntimeConfig { Ok(Self { host_info, platform_info, + dotns_tld: None, }) } + + /// Scope the reserved built-in derivations to `tld`, the dotNS TLD the + /// host's network declares. Must be a recognized entry of [`DOTNS_TLDS`]. + pub fn with_dotns_tld(mut self, tld: String) -> Result { + if !DOTNS_TLDS.contains(&tld.as_str()) { + return Err(RuntimeConfigValidationError::UnknownDotnsTld { tld }); + } + self.dotns_tld = Some(tld); + Ok(self) + } } impl PairingHostConfig { + /// Scope the reserved built-in derivations to `tld`. See + /// [`HostRuntimeConfig::with_dotns_tld`]. + pub fn with_dotns_tld(mut self, tld: String) -> Result { + self.host = self.host.with_dotns_tld(tld)?; + Ok(self) + } + /// Build a pairing-host runtime config, validating fields whose /// representation cannot be made invalid by Rust types alone. pub fn new( @@ -193,6 +214,13 @@ impl PairingHostConfig { } impl SigningHostConfig { + /// Scope the reserved built-in derivations to `tld`. See + /// [`HostRuntimeConfig::with_dotns_tld`]. + pub fn with_dotns_tld(mut self, tld: String) -> Result { + self.host = self.host.with_dotns_tld(tld)?; + Ok(self) + } + /// Build a signing-host runtime config, validating fields whose /// representation cannot be made invalid by Rust types alone. pub fn new( @@ -803,6 +831,12 @@ pub enum RuntimeConfigValidationError { /// Actual URL scheme. scheme: String, }, + /// Configured dotNS TLD is not a recognized [`DOTNS_TLDS`] entry. + #[display("dotns_tld must be one of the recognized dotNS TLDs, got {tld:?}")] + UnknownDotnsTld { + /// Rejected TLD value. + tld: String, + }, /// Pairing deeplink scheme included a URL separator. #[display("pairing_deeplink_scheme must not include ://, got {scheme:?}")] InvalidDeeplinkScheme { diff --git a/rust/crates/truapi-server/README.md b/rust/crates/truapi-server/README.md index 388997266..22587ef6f 100644 --- a/rust/crates/truapi-server/README.md +++ b/rust/crates/truapi-server/README.md @@ -190,7 +190,7 @@ role-specific lifecycle, so no method exists on a role that can't mean it: - **`SigningHost`** (wallet-local): signs on device from local BIP-39 entropy, no pairing flow. `signing_host/local_activation.rs` establishes a session from host-held secret material. Its public identity is the RFC-0022 - `uid.dot` index-0 product account. RFC-0024 ring-VRF keys are explicit, + `uid.{tld}` index-0 product account for the configured network TLD (default `dot`). RFC-0024 ring-VRF keys are explicit, product-owned registry entries; aliases, proofs, direct signatures, and internal personhood flows use the requested or user-selected registered key without a compiled-in fallback. It resolves RFC-0004 `RingLocation` values diff --git a/rust/crates/truapi-server/src/core.rs b/rust/crates/truapi-server/src/core.rs index dcd452117..6433c7037 100644 --- a/rust/crates/truapi-server/src/core.rs +++ b/rust/crates/truapi-server/src/core.rs @@ -14,7 +14,9 @@ use crate::dispatcher::Dispatcher; use crate::frame::ProtocolMessage; use crate::generated::dispatcher; use crate::host_logic::session::SessionState; -use crate::runtime::{PairingHostRole, ProductAuthority, ProductRuntimeHost, RuntimeServices}; +use crate::runtime::{ + PairingHostRole, ProductAuthority, ProductRuntimeHost, RuntimeServices, dotns_tld, +}; use crate::subscription::Spawner; use crate::transport::Transport; @@ -60,6 +62,7 @@ impl TrUApiCore { host_config.people_chain_genesis_hash, host_config.bulletin_chain_genesis_hash, spawner.clone(), + dotns_tld(&host_config.host), ); let pairing_host = PairingHostRole::new(services.clone(), host_config); pairing_host.clone().start_session_store_sync(spawner); diff --git a/rust/crates/truapi-server/src/host_core.rs b/rust/crates/truapi-server/src/host_core.rs index 34c729942..38da1c19d 100644 --- a/rust/crates/truapi-server/src/host_core.rs +++ b/rust/crates/truapi-server/src/host_core.rs @@ -30,7 +30,8 @@ use crate::frame::ProtocolMessage; use crate::host_logic::sso::messages::{RemoteMessage, RemoteMessageData, SsoRequestOutcome, v1}; use crate::runtime::{ ChatConnection, LocalActivation, PairingHostRole, ProductAuthority, ProductRuntimeHost, - ResponderExit, RuntimeServices, SigningHostRole, answer_remote_message, respond_to_pairing, + ResponderExit, RuntimeServices, SigningHostRole, answer_remote_message, dotns_tld, + respond_to_pairing, }; use crate::subscription::{HostInitiatedSubscriptionManager, Spawner}; use crate::transport::Transport; @@ -117,6 +118,7 @@ impl PairingHostRuntime { config.people_chain_genesis_hash, config.bulletin_chain_genesis_hash, spawner.clone(), + dotns_tld(&config.host), chat_platform, ); let pairing_host = PairingHostRole::new(services.clone(), config); @@ -405,6 +407,7 @@ impl SigningHostRuntime { config.people_chain_genesis_hash, config.bulletin_chain_genesis_hash, spawner, + dotns_tld(&config.host), chat_platform, ); let signing_host = SigningHostRole::new(services.clone()); diff --git a/rust/crates/truapi-server/src/host_logic/attestation.rs b/rust/crates/truapi-server/src/host_logic/attestation.rs index 098edec4f..3049521c7 100644 --- a/rust/crates/truapi-server/src/host_logic/attestation.rs +++ b/rust/crates/truapi-server/src/host_logic/attestation.rs @@ -82,13 +82,14 @@ pub fn build_lite_registration( entropy: &[u8], verifier_account_id: [u8; 32], username_base: &str, + dotns_tld: &str, ) -> Result { // Registration, local activation, and the SSO responder all use the - // RFC-0022 `uid.dot` default product account. - let candidate = derive_identity_keypair(entropy)?; + // RFC-0022 `uid.{tld}` default product account. + let candidate = derive_identity_keypair(entropy, dotns_tld)?; let candidate_public_key = candidate.public.to_bytes(); - let vrf_entropy = derive_lite_person_ring_vrf_entropy(entropy); + let vrf_entropy = derive_lite_person_ring_vrf_entropy(entropy, dotns_tld); let vrf_secret = BandersnatchVrfVerifiable::new_secret(vrf_entropy); let ring_vrf_key = BandersnatchVrfVerifiable::member_from_secret(&vrf_secret); @@ -172,13 +173,16 @@ mod tests { #[test] fn registration_params_have_expected_shapes_and_verify() { let verifier = [0x11u8; 32]; - let reg = build_lite_registration(&ENTROPY, verifier, "headlesstester").unwrap(); + let reg = build_lite_registration(&ENTROPY, verifier, "headlesstester", "dot").unwrap(); assert_eq!( reg.candidate_public_key, - derive_identity_keypair(&ENTROPY).unwrap().public.to_bytes(), + derive_identity_keypair(&ENTROPY, "dot") + .unwrap() + .public + .to_bytes(), "registration uses the canonical uid.dot identity account" ); - let lite_entropy = derive_lite_person_ring_vrf_entropy(&ENTROPY); + let lite_entropy = derive_lite_person_ring_vrf_entropy(&ENTROPY, "dot"); assert_eq!( reg.ring_vrf_key, BandersnatchVrfVerifiable::member_from_secret(&BandersnatchVrfVerifiable::new_secret( @@ -266,8 +270,8 @@ mod tests { #[test] fn registration_is_deterministic_per_entropy_and_username() { let verifier = [0x22u8; 32]; - let first = build_lite_registration(&ENTROPY, verifier, "aliceheadless").unwrap(); - let again = build_lite_registration(&ENTROPY, verifier, "aliceheadless").unwrap(); + let first = build_lite_registration(&ENTROPY, verifier, "aliceheadless", "dot").unwrap(); + let again = build_lite_registration(&ENTROPY, verifier, "aliceheadless", "dot").unwrap(); assert_eq!(first.candidate_public_key, again.candidate_public_key); assert_eq!(first.ring_vrf_key, again.ring_vrf_key); assert_eq!(first.candidate_account_id, again.candidate_account_id); diff --git a/rust/crates/truapi-server/src/host_logic/product_account.rs b/rust/crates/truapi-server/src/host_logic/product_account.rs index 67c9d58cc..832de7b1b 100644 --- a/rust/crates/truapi-server/src/host_logic/product_account.rs +++ b/rust/crates/truapi-server/src/host_logic/product_account.rs @@ -3,9 +3,11 @@ //! Product subtrees use hard HDKD at `//product//{product_id}`. Individual //! accounts use one soft junction carrying the RFC-0022 32-byte derivation //! index, so a paired host can derive children from the subtree public key. -//! Reserved built-ins additionally pin the `uid.dot` identity account and the -//! legacy `peopl.dot` full/lite ring-VRF keyed-hash paths used by pairing -//! attestation. RFC-0024 operational key selection comes from the registry. +//! Reserved built-ins additionally pin the `uid.{tld}` identity account and +//! the legacy `peopl.{tld}` full/lite ring-VRF keyed-hash paths used by +//! pairing attestation, where `{tld}` is the host's configured network TLD +//! (default `dot`). RFC-0024 operational key selection comes from the +//! registry. //! Host-spec C.5-C.7 define the product-account derivation, SS58 address, and //! `ProductAccountId` shape: //! @@ -18,10 +20,19 @@ use thiserror::Error; const JUNCTION_ID_LEN: usize = 32; const PRODUCT_JUNCTION: &str = "product"; -/// Reserved RFC-0022 product id for the public light-person identity account. -pub const IDENTITY_PRODUCT_ID: &str = "uid.dot"; -/// Reserved RFC-0022 ring-VRF domain for full and light personhood. -pub const PERSONHOOD_PRODUCT_ID: &str = "peopl.dot"; +/// dotNS TLD assumed when a host does not configure one, keeping today's +/// mobile-pinned `uid.dot` and `peopl.dot` derivations. +pub const DEFAULT_DOTNS_TLD: &str = "dot"; +/// Reserved RFC-0022 product id for the public light-person identity account +/// on the network serving `tld`. +pub fn identity_product_id(tld: &str) -> String { + format!("uid.{tld}") +} +/// Reserved RFC-0022 ring-VRF domain for full and light personhood on the +/// network serving `tld`. +pub fn personhood_product_id(tld: &str) -> String { + format!("peopl.{tld}") +} const RING_VRF_ROOT_KEY: &[u8] = b"ring-vrf"; /// Substrate sr25519 signing-context string, shared by every sr25519 signature @@ -86,29 +97,30 @@ pub fn derivation_index_bytes(index: &truapi::v01::DerivationIndex) -> [u8; 32] } } /// Derive the RFC-0022 public light-person identity account: -/// `//product//uid.dot/index_bytes(0)`. -pub fn derive_identity_keypair(entropy: &[u8]) -> Result { +/// `//product//uid.{tld}/index_bytes(0)`. +pub fn derive_identity_keypair(entropy: &[u8], tld: &str) -> Result { let root = derive_root_keypair_from_entropy(entropy)?; - let subtree = derive_hard_path_from_keypair(root, &[PRODUCT_JUNCTION, IDENTITY_PRODUCT_ID])?; + let subtree = + derive_hard_path_from_keypair(root, &[PRODUCT_JUNCTION, &identity_product_id(tld)])?; Ok(subtree.derived_key_simple(ChainCode(index_bytes(0)), []).0) } /// Derive the RFC-0022 full-person ring-VRF entropy at -/// `hash(root_entropy, "ring-vrf")//peopl.dot//index_bytes(0)`. -pub fn derive_full_person_ring_vrf_entropy(root_entropy: &[u8]) -> [u8; 32] { - derive_person_ring_vrf_entropy(root_entropy, 0) +/// `hash(root_entropy, "ring-vrf")//peopl.{tld}//index_bytes(0)`. +pub fn derive_full_person_ring_vrf_entropy(root_entropy: &[u8], tld: &str) -> [u8; 32] { + derive_person_ring_vrf_entropy(root_entropy, tld, 0) } /// Derive the RFC-0022 light-person ring-VRF entropy at -/// `hash(root_entropy, "ring-vrf")//peopl.dot//index_bytes(1)`. -pub fn derive_lite_person_ring_vrf_entropy(root_entropy: &[u8]) -> [u8; 32] { - derive_person_ring_vrf_entropy(root_entropy, 1) +/// `hash(root_entropy, "ring-vrf")//peopl.{tld}//index_bytes(1)`. +pub fn derive_lite_person_ring_vrf_entropy(root_entropy: &[u8], tld: &str) -> [u8; 32] { + derive_person_ring_vrf_entropy(root_entropy, tld, 1) } -fn derive_person_ring_vrf_entropy(root_entropy: &[u8], index: u32) -> [u8; 32] { +fn derive_person_ring_vrf_entropy(root_entropy: &[u8], tld: &str, index: u32) -> [u8; 32] { derive_ring_vrf_entropy( root_entropy, - PERSONHOOD_PRODUCT_ID, + &personhood_product_id(tld), &truapi::v01::DerivationIndex::Index(index), ) .expect("the reserved personhood product id is a valid junction") @@ -389,11 +401,17 @@ mod tests { "372b08255c7798fe3193756296005adc4c44adb9f3986fb718aa98a48b4bf725" ); assert_eq!( - hex::encode(derive_full_person_ring_vrf_entropy(&root_entropy)), + hex::encode(derive_full_person_ring_vrf_entropy( + &root_entropy, + DEFAULT_DOTNS_TLD + )), "c47086f94a7f4c05b7afd9f2339d3fea168f3823b5424ba1f7b31043d8ef60af" ); assert_eq!( - hex::encode(derive_lite_person_ring_vrf_entropy(&root_entropy)), + hex::encode(derive_lite_person_ring_vrf_entropy( + &root_entropy, + DEFAULT_DOTNS_TLD + )), "8d7f5e1510a7e8d813887e100f5a260ec9de60e68695477b93360ee7e3d16a9f" ); } @@ -415,10 +433,13 @@ mod tests { #[test] fn identity_is_uid_dot_default_product_account_and_signs() { let entropy = [0xAB; 16]; - let identity = derive_identity_keypair(&entropy).unwrap(); + let identity = derive_identity_keypair(&entropy, DEFAULT_DOTNS_TLD).unwrap(); let root = derive_root_keypair_from_entropy(&entropy).unwrap(); - let uid_subtree = - derive_hard_path_from_keypair(root, &[PRODUCT_JUNCTION, IDENTITY_PRODUCT_ID]).unwrap(); + let uid_subtree = derive_hard_path_from_keypair( + root, + &[PRODUCT_JUNCTION, &identity_product_id(DEFAULT_DOTNS_TLD)], + ) + .unwrap(); let expected = uid_subtree .derived_key_simple(ChainCode(index_bytes(0)), []) .0; @@ -437,6 +458,20 @@ mod tests { ); } + #[test] + fn built_in_derivations_are_network_scoped() { + let entropy = [0xAB; 16]; + let dot = derive_identity_keypair(&entropy, "dot").unwrap(); + let test = derive_identity_keypair(&entropy, "test").unwrap(); + assert_ne!(dot.public, test.public); + + let root_entropy: Vec = (1..=32).collect(); + assert_ne!( + derive_full_person_ring_vrf_entropy(&root_entropy, "dot"), + derive_full_person_ring_vrf_entropy(&root_entropy, "test"), + ); + } + #[test] fn raw_index_space_is_disjoint_from_plain_indexes() { // A raw all-zero index must not collide with plain index 0: the magic diff --git a/rust/crates/truapi-server/src/native.rs b/rust/crates/truapi-server/src/native.rs index 3ad7f2284..ebf6513ea 100644 --- a/rust/crates/truapi-server/src/native.rs +++ b/rust/crates/truapi-server/src/native.rs @@ -202,6 +202,9 @@ pub struct NativeRuntimeConfig { pub local_session_lite_username: Option, /// Deeplink scheme used in pairing QR payloads. pub pairing_deeplink_scheme: NativePairingDeeplinkScheme, + /// dotNS TLD of the host's configured network, scoping the reserved + /// built-in derivations. `None` means `dot`. + pub dotns_tld: Option, } /// Process-owned native host configuration shared by every product execution. @@ -225,6 +228,9 @@ pub struct NativeHostRuntimeConfig { pub local_session_secret: Option>, /// Optional lite username attached to the local signing-host session. pub local_session_lite_username: Option, + /// dotNS TLD of the host's configured network, scoping the reserved + /// built-in derivations. `None` means `dot`. + pub dotns_tld: Option, } /// Trusted identity attached by a native host to one executable connection. @@ -276,6 +282,12 @@ pub enum NativeRuntimeConfigError { /// Parse failure reason. reason: String, }, + /// Configured dotNS TLD is not a recognized entry. + #[error("dotns_tld must be one of the recognized dotNS TLDs, got {tld:?}")] + UnknownDotnsTld { + /// Rejected TLD value. + tld: String, + }, /// Host icon URL used a non-HTTPS scheme. #[error("host_icon must use https scheme, got {scheme:?}")] InsecureHostIcon { @@ -319,6 +331,7 @@ impl TryFrom for NativeResolvedRuntimeConfig { local_session_secret, local_session_lite_username, pairing_deeplink_scheme: _, + dotns_tld, } = config; let host: NativeResolvedHostRuntimeConfig = NativeHostRuntimeConfig { host_name, @@ -330,6 +343,7 @@ impl TryFrom for NativeResolvedRuntimeConfig { bulletin_chain_genesis_hash, local_session_secret, local_session_lite_username, + dotns_tld, } .try_into()?; let product = NativeProductExecutionConfig { @@ -370,6 +384,10 @@ impl TryFrom for NativeResolvedHostRuntimeConfig { people_chain_genesis_hash, bulletin_chain_genesis_hash, )?; + let signing = match config.dotns_tld { + Some(tld) => signing.with_dotns_tld(tld)?, + None => signing, + }; Ok(Self { signing, local_session_secret: config.local_session_secret, @@ -398,6 +416,7 @@ impl From for NativeRuntimeConfigError { RuntimeConfigValidationError::InvalidHostIcon { source } => Self::InvalidHostIcon { reason: source.to_string(), }, + RuntimeConfigValidationError::UnknownDotnsTld { tld } => Self::UnknownDotnsTld { tld }, RuntimeConfigValidationError::InsecureHostIcon { scheme } => { Self::InsecureHostIcon { scheme } } @@ -2314,6 +2333,7 @@ mod tests { local_session_secret: None, local_session_lite_username: None, pairing_deeplink_scheme: NativePairingDeeplinkScheme::PolkadotApp, + dotns_tld: None, } } @@ -2327,6 +2347,7 @@ mod tests { people_chain_genesis_hash: vec![0xa2; 32], bulletin_chain_genesis_hash: vec![0xbb; 32], local_session_secret: Some(vec![7; 32]), + dotns_tld: None, local_session_lite_username: Some("alice".to_string()), } } diff --git a/rust/crates/truapi-server/src/runtime.rs b/rust/crates/truapi-server/src/runtime.rs index a56627db7..8505f06bc 100644 --- a/rust/crates/truapi-server/src/runtime.rs +++ b/rust/crates/truapi-server/src/runtime.rs @@ -63,7 +63,7 @@ pub(crate) use chat::{ChatConnection, chat_platform_for}; #[cfg(test)] use pairing_host::PairingHost; pub(crate) use pairing_host::PairingHost as PairingHostRole; -pub(crate) use services::RuntimeServices; +pub(crate) use services::{RuntimeServices, dotns_tld}; pub use signing_host::ResponderExit; #[cfg(not(target_arch = "wasm32"))] pub use signing_host::StatementRenewalTarget; @@ -445,6 +445,7 @@ impl ProductRuntimeHost { host_config.people_chain_genesis_hash, host_config.bulletin_chain_genesis_hash, spawner.clone(), + dotns_tld(&host_config.host), ); let pairing_host = PairingHost::new(services.clone(), host_config); let core_instance = services.next_core_instance(); @@ -3047,6 +3048,7 @@ mod tests { host_config.people_chain_genesis_hash, host_config.bulletin_chain_genesis_hash, spawner.clone(), + "dot".to_string(), ); let chat_platform = Arc::new(RecordingChatPlatform::default()); let pairing_host = PairingHost::new(services.clone(), host_config); @@ -3189,6 +3191,7 @@ mod tests { host_config.people_chain_genesis_hash, host_config.bulletin_chain_genesis_hash, spawner.clone(), + "dot".to_string(), ); let chat_platform = Arc::new(RecordingChatPlatform::default()); let pairing_host = PairingHost::new(services.clone(), host_config); @@ -3272,6 +3275,7 @@ mod tests { host_config.people_chain_genesis_hash, host_config.bulletin_chain_genesis_hash, spawner.clone(), + "dot".to_string(), ); let chat_platform = Arc::new(RecordingChatPlatform::default()); let pairing_host = PairingHost::new(services.clone(), host_config); @@ -3356,6 +3360,7 @@ mod tests { host_config.people_chain_genesis_hash, host_config.bulletin_chain_genesis_hash, spawner.clone(), + "dot".to_string(), ); let chat_platform = Arc::new(RecordingChatPlatform::default()); let pairing_host = PairingHost::new(services.clone(), host_config); @@ -3402,6 +3407,7 @@ mod tests { host_config.people_chain_genesis_hash, host_config.bulletin_chain_genesis_hash, spawner.clone(), + dotns_tld(&host_config.host), ); let pairing_host = PairingHost::new(services.clone(), host_config); let first = ProductRuntimeHost::from_services( diff --git a/rust/crates/truapi-server/src/runtime/services.rs b/rust/crates/truapi-server/src/runtime/services.rs index bb093146a..00dfc2c8c 100644 --- a/rust/crates/truapi-server/src/runtime/services.rs +++ b/rust/crates/truapi-server/src/runtime/services.rs @@ -49,6 +49,9 @@ pub(crate) struct RuntimeServices { statement_cache: Mutex, /// Task spawner for background runtime work. pub(crate) spawner: Spawner, + /// dotNS TLD of the host's configured network, scoping the reserved + /// built-in derivations. Defaults to `dot`. + pub(crate) dotns_tld: String, /// Serializes the read-or-create of the persisted device encryption key. /// Concurrent first-time readers would otherwise each generate a secret and /// persist it, leaving peers addressing an overwritten key. @@ -56,6 +59,15 @@ pub(crate) struct RuntimeServices { next_core_instance: AtomicU64, } +/// The dotNS TLD a host config scopes built-in derivations to, `dot` unless +/// configured otherwise. +pub(crate) fn dotns_tld(config: &truapi_platform::HostRuntimeConfig) -> String { + config + .dotns_tld + .clone() + .unwrap_or_else(|| crate::host_logic::product_account::DEFAULT_DOTNS_TLD.to_string()) +} + impl RuntimeServices { /// Build role-neutral runtime services from the platform, the People-chain /// genesis hash used by statement-store backed protocols, and the @@ -65,6 +77,7 @@ impl RuntimeServices { people_chain_genesis_hash: [u8; 32], bulletin_chain_genesis_hash: [u8; 32], spawner: Spawner, + dotns_tld: String, ) -> Arc { let chain_provider = Arc::new(HostChainProvider { platform: platform.clone(), @@ -84,6 +97,7 @@ impl RuntimeServices { preimage_cache: Mutex::new(PreimageCache::default()), statement_cache: Mutex::new(StatementCache::default()), spawner, + dotns_tld, device_encryption_key: futures::lock::Mutex::new(()), next_core_instance: AtomicU64::new(1), }) @@ -95,6 +109,7 @@ impl RuntimeServices { people_chain_genesis_hash: [u8; 32], bulletin_chain_genesis_hash: [u8; 32], spawner: Spawner, + dotns_tld: String, chat_platform: Option>, ) -> Arc { let services = Self::new( @@ -102,6 +117,7 @@ impl RuntimeServices { people_chain_genesis_hash, bulletin_chain_genesis_hash, spawner, + dotns_tld, ); let Some(chat_platform) = chat_platform else { return services; diff --git a/rust/crates/truapi-server/src/runtime/signing_host.rs b/rust/crates/truapi-server/src/runtime/signing_host.rs index ecb312be4..44ff61c5b 100644 --- a/rust/crates/truapi-server/src/runtime/signing_host.rs +++ b/rust/crates/truapi-server/src/runtime/signing_host.rs @@ -150,6 +150,7 @@ impl SigningHost { [0; 32], [0xbb; 32], crate::test_support::test_spawner(), + "dot".to_string(), ); Arc::new(Self { services, @@ -302,7 +303,7 @@ impl SigningHost { fn identity_keypair(&self) -> Result { let entropy = self.root_entropy()?; - derive_identity_keypair(&entropy).map_err(product_authority_error) + derive_identity_keypair(&entropy, &self.services.dotns_tld).map_err(product_authority_error) } fn install_local_session(&self, secret: Zeroizing>, session: SessionInfo) { @@ -398,11 +399,11 @@ impl SigningHost { Ok(vec![ CollectionCandidate { collection: PersonhoodCollection::People, - entropy: derive_full_person_ring_vrf_entropy(&root), + entropy: derive_full_person_ring_vrf_entropy(&root, &self.services.dotns_tld), }, CollectionCandidate { collection: PersonhoodCollection::LitePeople, - entropy: derive_lite_person_ring_vrf_entropy(&root), + entropy: derive_lite_person_ring_vrf_entropy(&root, &self.services.dotns_tld), }, ]) } @@ -1301,6 +1302,7 @@ mod tests { config.people_chain_genesis_hash, config.bulletin_chain_genesis_hash, test_spawner(), + "dot".to_string(), ); let signing_host = SigningHostRole::new(services.clone()); (services, signing_host) @@ -1639,7 +1641,7 @@ mod tests { .expect("activation succeeds"); let session = authority.current_session().expect("active session"); - let identity = derive_identity_keypair(&ENTROPY) + let identity = derive_identity_keypair(&ENTROPY, "dot") .expect("uid.dot identity derivation") .public .to_bytes(); @@ -2084,7 +2086,7 @@ mod tests { .expect("activation succeeds"); let session = authority.current_session().expect("active session"); let cx = CallContext::default(); - let identity = derive_identity_keypair(&ENTROPY).unwrap(); + let identity = derive_identity_keypair(&ENTROPY, "dot").unwrap(); let request = |account| SignRawAuthorityRequest::LegacyAccount { account, request: v01::HostSignRawWithLegacyAccountRequest { diff --git a/rust/crates/truapi-server/src/runtime/signing_host/local_activation.rs b/rust/crates/truapi-server/src/runtime/signing_host/local_activation.rs index df9a0c922..6eec78cd4 100644 --- a/rust/crates/truapi-server/src/runtime/signing_host/local_activation.rs +++ b/rust/crates/truapi-server/src/runtime/signing_host/local_activation.rs @@ -44,7 +44,7 @@ impl LocalActivation for SigningHost { let secret = Zeroizing::new(secret); let root = derive_root_keypair_from_entropy(&secret).map_err(product_authority_error)?; let public_key = root.public.to_bytes(); - let identity_account_id = derive_identity_keypair(&secret) + let identity_account_id = derive_identity_keypair(&secret, &self.services.dotns_tld) .map_err(product_authority_error)? .public .to_bytes(); diff --git a/rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs b/rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs index afb299dd2..a844c6470 100644 --- a/rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs +++ b/rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs @@ -77,8 +77,9 @@ const MAX_SERVED_REQUEST_IDS: usize = 1024; fn derive_responder_identity( entropy: &[u8], + dotns_tld: &str, ) -> Result<(ResponderIdentity, [u8; 32]), ProductAccountError> { - let statement = derive_identity_keypair(entropy)?; + let statement = derive_identity_keypair(entropy, dotns_tld)?; let (encryption_secret_key, encryption_public_key) = derive_x25519_keypair_from_entropy(entropy, SSO_ENCRYPTION_DOMAIN); let identity_chat_private_key = derive_identity_chat_private_key(entropy); @@ -230,11 +231,13 @@ pub(crate) async fn respond_to_pairing( .root_entropy() .map_err(|err| format!("signing host has no active local session: {err}"))?; // Product accounts and the SSO statement identity derive from the - // canonical root key; the identity is the RFC-0022 uid.dot default account. + // canonical root key; the identity is the RFC-0022 uid.{tld} default + // account. let root = derive_root_keypair_from_entropy(&entropy) .map_err(|err| format!("root account derivation failed: {err}"))?; - let (identity, identity_chat_private_key) = derive_responder_identity(&entropy) - .map_err(|err| format!("responder identity derivation failed: {err}"))?; + let (identity, identity_chat_private_key) = + derive_responder_identity(&entropy, &services.dotns_tld) + .map_err(|err| format!("responder identity derivation failed: {err}"))?; let device_enc_pub_key = x25519_public_key(services.device_encryption_secret().await?); let session = establish_responder_session_info( &identity, @@ -1590,6 +1593,7 @@ mod tests { config.people_chain_genesis_hash, config.bulletin_chain_genesis_hash, test_spawner(), + "dot".to_string(), ); let signing_host = SigningHost::new(services.clone()); futures::executor::block_on(signing_host.activate_local_session(ENTROPY.to_vec())) @@ -1716,7 +1720,7 @@ mod tests { .unwrap() .identity_account_id .unwrap(); - let (identity, _) = derive_responder_identity(&ENTROPY).unwrap(); + let (identity, _) = derive_responder_identity(&ENTROPY, "dot").unwrap(); assert_eq!(identity.statement_public_key, local_identity); let (_, host_encryption_public_key) = @@ -1966,7 +1970,7 @@ mod tests { create_transaction_confirmed: true, ..StubPlatform::default() })); - let identity = derive_identity_keypair(&ENTROPY).unwrap(); + let identity = derive_identity_keypair(&ENTROPY, "dot").unwrap(); let payload = api::LegacyAccountTxPayload { signer: identity.public.to_bytes(), genesis_hash: [0xaa; 32], diff --git a/rust/crates/truapi-server/src/runtime/statement_store.rs b/rust/crates/truapi-server/src/runtime/statement_store.rs index 97da98390..86ce13920 100644 --- a/rust/crates/truapi-server/src/runtime/statement_store.rs +++ b/rust/crates/truapi-server/src/runtime/statement_store.rs @@ -457,7 +457,13 @@ mod tests { fn signing_host_runtime(product_id: &str) -> (ProductRuntimeHost, Arc) { let platform: Arc = Arc::new(StubPlatform::default()); - let services = RuntimeServices::new(platform.clone(), [0; 32], [0xbb; 32], test_spawner()); + let services = RuntimeServices::new( + platform.clone(), + [0; 32], + [0xbb; 32], + test_spawner(), + "dot".to_string(), + ); let signing_host = SigningHostRole::new(services.clone()); futures::executor::block_on(signing_host.activate_local_session(ENTROPY.to_vec())) .expect("activation succeeds"); diff --git a/rust/crates/truapi-server/src/wasm.rs b/rust/crates/truapi-server/src/wasm.rs index 1c33d6742..b29d635d6 100644 --- a/rust/crates/truapi-server/src/wasm.rs +++ b/rust/crates/truapi-server/src/wasm.rs @@ -466,7 +466,7 @@ fn pairing_host_config_from_js(value: &JsValue) -> Result Result config + .with_dotns_tld(tld) + .map_err(runtime_config_validation_to_js), + None => Ok(config), + } } #[cfg(feature = "wasm-signing-host")] @@ -510,7 +517,7 @@ fn signing_host_config_from_js(value: &JsValue) -> Result Result config + .with_dotns_tld(tld) + .map_err(runtime_config_validation_to_js), + None => Ok(config), + } } fn product_context_from_js(value: &JsValue) -> Result { @@ -590,6 +604,9 @@ fn runtime_config_validation_to_js(err: RuntimeConfigValidationError) -> JsValue "runtimeConfig.productId must be a dotNS or localhost product identifier, got {product_id:?}" )) } + RuntimeConfigValidationError::UnknownDotnsTld { tld } => JsValue::from_str(&format!( + "runtimeConfig.dotnsTld must be a recognized dotNS TLD, got {tld:?}" + )), } } From d4b552be58fedf9d4bbc434f41038edfa2bc8426 Mon Sep 17 00:00:00 2001 From: Tiago Tavares Date: Thu, 20 Aug 2026 17:08:31 +0100 Subject: [PATCH 06/10] docs: condense the open questions to one-sentence questions --- docs/design/name-identifiers.md | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/docs/design/name-identifiers.md b/docs/design/name-identifiers.md index 3f18d9f5f..c71172661 100644 --- a/docs/design/name-identifiers.md +++ b/docs/design/name-identifiers.md @@ -80,25 +80,15 @@ and the #### Open Questions -- The built-in identifiers are network-scoped: +- When does the mobile Account Holder derive [`uid.{tld}` and `peopl.{tld}`](../../rust/crates/truapi-server/src/host_logic/product_account.rs) - follow the `dotns_tld` a host sets in its runtime config, with `dot` as the - default so unconfigured hosts keep the mobile-pinned derivations. A paired - phone still derives `uid.dot` on every network, so hosts configured for a - non-dot TLD diverge from the Account Holder until the mobile app ships the - same rule. -- The recognized TLD list is compiled in - ([`DOTNS_TLDS`](../../rust/crates/truapi-platform/src/lib.rs)) while the - registry already exposes the truth per network through `tld()`. Hosts now - declare their network TLD through `HostChainSet.tld` in the - `supported_chains` syscall, mirrored from the - [network presets](../../rust/crates/truapi-host-cli/src/network.rs). The - remaining step is consuming it for validation, which stays on the compiled - union list because SCALE decode and the exported `parse_navigate` FFI have - no configuration in scope. -- Graduation needs its own design: a product that wants to carry state from - testnet to mainnet needs a registered migration or alias, not implicit - identity. + per network instead of its hardcoded `uid.dot`, so paired hosts on non-dot + networks stop diverging from it? +- Can name validation and navigation learn the TLD from the registry `tld()` + view (now carried by `HostChainSet.tld`) instead of the compiled + [`DOTNS_TLDS`](../../rust/crates/truapi-platform/src/lib.rs) list, given + SCALE decode and the exported `parse_navigate` FFI have no configuration in + scope? #### Use Cases From 8d6c2a4ddcaaeb4bb0195ff6c4df496ebc920fdb Mon Sep 17 00:00:00 2001 From: Tiago Tavares Date: Thu, 20 Aug 2026 17:28:14 +0100 Subject: [PATCH 07/10] docs: drop the open questions from the name-identifiers design doc --- docs/design/name-identifiers.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/design/name-identifiers.md b/docs/design/name-identifiers.md index c71172661..c4f0095f4 100644 --- a/docs/design/name-identifiers.md +++ b/docs/design/name-identifiers.md @@ -78,18 +78,6 @@ drifts, so it MUST NOT be reimplemented outside and the [reserved-id table](../../rust/crates/truapi-server/src/host_logic/product_account.rs). -#### Open Questions - -- When does the mobile Account Holder derive - [`uid.{tld}` and `peopl.{tld}`](../../rust/crates/truapi-server/src/host_logic/product_account.rs) - per network instead of its hardcoded `uid.dot`, so paired hosts on non-dot - networks stop diverging from it? -- Can name validation and navigation learn the TLD from the registry `tld()` - view (now carried by `HostChainSet.tld`) instead of the compiled - [`DOTNS_TLDS`](../../rust/crates/truapi-platform/src/lib.rs) list, given - SCALE decode and the exported `parse_navigate` FFI have no configuration in - scope? - #### Use Cases | Use Case | Description | From ecf0198cd63c73e15e3661d481a40eb3a80df823 Mon Sep 17 00:00:00 2001 From: Tiago Tavares Date: Thu, 20 Aug 2026 18:48:49 +0100 Subject: [PATCH 08/10] docs: promote motivation to a top-level section --- docs/design/name-identifiers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/name-identifiers.md b/docs/design/name-identifiers.md index c4f0095f4..e5b205faa 100644 --- a/docs/design/name-identifiers.md +++ b/docs/design/name-identifiers.md @@ -22,7 +22,7 @@ namehash("example.dot") = keccak256(namehash("dot") ++ keccak256("example")) = 0x50cef3746492e11fe07821077c650ed11a908315a91b3a85b4a12afd21249605 ``` -#### Motivation +## Motivation Three motivations argue for a well defined design: From bea7b22a4eae38e3327cf1e6bc14346849a62c22 Mon Sep 17 00:00:00 2001 From: Tiago Tavares Date: Thu, 20 Aug 2026 18:49:27 +0100 Subject: [PATCH 09/10] docs: fold the individuality precedent into the motivation list --- docs/design/name-identifiers.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/design/name-identifiers.md b/docs/design/name-identifiers.md index e5b205faa..ffb65617c 100644 --- a/docs/design/name-identifiers.md +++ b/docs/design/name-identifiers.md @@ -37,12 +37,11 @@ Three motivations argue for a well defined design: - Distinct identities per TLD reduce confusion across Polkadot App versions and web domains, because what the user sees named differently is also keyed differently. - -The Individuality runtime takes this side for ring contexts: -[`build_product_context`](https://github.com/paritytech/individuality/blob/be61b7720e5345afff53f28b924f8bc129938e24/support/src/context.rs#L61-L80) -hashes the preimage `product/{name}.{tld}/{suffix}`, with the network suffix -an explicit argument. A host that derived ring contexts TLD-free would -disagree with the chain. +- The Individuality runtime takes this side for ring contexts: + [`build_product_context`](https://github.com/paritytech/individuality/blob/be61b7720e5345afff53f28b924f8bc129938e24/support/src/context.rs#L61-L80) + hashes the preimage `product/{name}.{tld}/{suffix}`, with the network suffix + an explicit argument. A host that derived ring contexts TLD-free would + disagree with the chain. ## Convention From 0a750e3d6a808ab07dd586373a406007ee182aa2 Mon Sep 17 00:00:00 2001 From: Tiago Tavares Date: Thu, 20 Aug 2026 18:50:13 +0100 Subject: [PATCH 10/10] docs: rename use cases to where name identifiers exist --- docs/design/name-identifiers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/name-identifiers.md b/docs/design/name-identifiers.md index ffb65617c..e5847cab4 100644 --- a/docs/design/name-identifiers.md +++ b/docs/design/name-identifiers.md @@ -77,7 +77,7 @@ drifts, so it MUST NOT be reimplemented outside and the [reserved-id table](../../rust/crates/truapi-server/src/host_logic/product_account.rs). -#### Use Cases +## Where Name Identifiers Exist | Use Case | Description | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |