Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions docs/design/name-identifiers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
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
```

## 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
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 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 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)
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).

## Where Name Identifiers Exist

| 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. |
| 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). |
12 changes: 12 additions & 0 deletions ios/truapi-host/Sources/TrUAPIHost/truapi_platform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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
}

Expand All @@ -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)
}
}
Expand Down
64 changes: 52 additions & 12 deletions ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3893,6 +3893,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.
Expand Down Expand Up @@ -3923,7 +3928,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
Expand All @@ -3933,6 +3942,7 @@ public struct NativeHostRuntimeConfig: Equatable, Hashable {
self.bulletinChainGenesisHash = bulletinChainGenesisHash
self.localSessionSecret = localSessionSecret
self.localSessionLiteUsername = localSessionLiteUsername
self.dotnsTld = dotnsTld
}


Expand All @@ -3959,7 +3969,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)
)
}

Expand All @@ -3973,6 +3984,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)
}
}

Expand Down Expand Up @@ -4113,6 +4125,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.
Expand Down Expand Up @@ -4152,7 +4169,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
Expand All @@ -4165,6 +4186,7 @@ public struct NativeRuntimeConfig: Equatable, Hashable {
self.localSessionSecret = localSessionSecret
self.localSessionLiteUsername = localSessionLiteUsername
self.pairingDeeplinkScheme = pairingDeeplinkScheme
self.dotnsTld = dotnsTld
}


Expand Down Expand Up @@ -4194,7 +4216,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)
)
}

Expand All @@ -4211,6 +4234,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)
}
}

Expand Down Expand Up @@ -4977,6 +5001,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.
*/
Expand Down Expand Up @@ -5050,16 +5082,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)
)

Expand Down Expand Up @@ -5094,23 +5129,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)

}
Expand Down
5 changes: 5 additions & 0 deletions js/packages/truapi-host/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions rust/crates/truapi-codegen/tests/golden/host-callbacks.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions rust/crates/truapi-host-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

Expand Down Expand Up @@ -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
Expand Down
Loading
Loading