Skip to content

On test networks the personhood product cannot use its own person keys: product ids carry the network TLD, the keys are pinned to peopl.dot #619

Description

@peetzweg

What

A product session is identified by the dotNS name it was opened under, so on previewnet the personhood product is peopl.testnet and on paseo-next-v2 it is peopl.paseo. The two person keys are derived under the literal peopl.dot on every network. The ring-VRF ownership rule compares the two strings. Result: on any network other than .dot, the personhood product does not own the keys that make its users people, and no product does.

session product id registerRingVrfKey(Index(0), People) listRingVrfKeys(owner: "peopl.dot") ringVrfSign / createAccountProof with the peopl.dot handle
peopl.dot the key the People ring holds own keys, no prompt signs
peopl.testnet / peopl.paseo a valid key that is in no ring after the account-access consent, and only entries a peopl.dot session registered earlier NotAllowlisted

Measured with truapi-host 0.13.1 on previewnet, one wallet session, and read against main at 696fd735. The CLI is the only host where a peopl.dot session on a test network exists at all, because --product-id is free-form. A host that opens the product by name (the iOS shell passes <name>.<tld> into openProductExecution) never produces it.

Why it happens

Three parts of the core agree that a product's identity is its label and the TLD is the network, one part does not, and the keys sit on the wrong side of that line.

flowchart LR
    subgraph id["Product id = dotNS name, per network"]
        nav["navigate peopl.testnet"] --> pid["session product id<br/>peopl.testnet"]
        trusted["REMOTE_PERMISSION_TRUSTED_LABELS<br/>peopl · dim2 · stash<br/>(no TLD, every network)"]
        manifest["manifest trustedProducts<br/>keys carry no TLD"]
        accounts["product accounts<br/>//product//peopl.testnet/i"]
        contexts["proof contexts<br/>network_suffix = testnet"]
    end

    subgraph keys["Person keys = peopl.dot, every network"]
        const["PERSONHOOD_PRODUCT_ID = &quot;peopl.dot&quot;"] --> full["//peopl.dot//index_bytes(0)<br/>full person key"]
        const --> lite["//peopl.dot//index_bytes(1)<br/>light person key"]
        full --> ring["People ring"]
        lite --> lring["PeopleLite ring"]
    end

    pid -- "register_ring_vrf_key<br/>derives under the caller" --> wrong["//peopl.testnet//0<br/>in no ring"]
    pid -- "require_owned_ring_vrf_key<br/>peopl.testnet ≠ peopl.dot" --> refused["NotAllowlisted"]
    refused -.-> full
Loading

Concretely:

  • normalize_product_identifier accepts any name ending in DOTNS_TLDS = ["dot", "paseo", "testnet"] and keeps the TLD. classify_dotns shares the list "so navigation and derivation accept the same per-network names". truapi-host: derive the product id from the product manifest #521 states the intent: a product is peopl.<tld> for the network being targeted.
  • REMOTE_PERMISSION_TRUSTED_LABELS is documented as "Entries carry no TLD, so one entry covers the product on every network"; peopl.dot and peopl.paseo are named as the same trusted product. The manifest's trustedProducts follows the same rule: "Keys carry no TLD … Append the TLD of the network you resolve against before matching."
  • Proof contexts already take a network_suffix (derive_slot_context, derive_pgas_context), matching the runtime, which scopes every People context by the live network suffix.
  • derive_full_person_ring_vrf_entropy and derive_lite_person_ring_vrf_entropy take only the root entropy. The product id is hash input, blake2b(blake2b(root, "ring-vrf"), chaincode("peopl.dot")), then keyed by the index. Nothing on chain ties a member key to a product id; Members.Members and People.Keys hold bare Bandersnatch keys.
  • register_ring_vrf_key builds the handle from the calling product id and derives from that. require_owned_ring_vrf_key in signing_host.rs and pairing_host.rs compares the caller's full id with the handle's dot_ns_identifier. The registry's owner_entries matches the same way. There is no allowlist source (Cross-product ring VRF: hosts diverge (Android prompts, Rust core refuses) and the allowlist gate has no source #373).
  • The registry is written only by register_ring_vrf_key. The wallet derives both reserved keys for its own allowance proofs (reserved_person_collection_candidates) but never registers them, so a fresh wallet on a test network returns [] to every product until a peopl.dot session registers them.

RFC-0024 says the personhood product "must own the full and light keys — under RFC-0022, the peopl.dot domain of the ring-VRF tree". On a test network that sentence cannot be true under the current rule.

Why it matters

Membership is one member key in the People ring, and Score.register needs a signature by the index-0 key. From a peopl.testnet session the product mints a key nobody holds, can list its real keys only if something else registered them, and can never sign with them. So on every test network the personhood product cannot take a user from "score reached" to "member", and cannot even show membership on a fresh wallet. On .dot the name and the constant coincide and everything works, which is why this has not surfaced there.

The keys themselves are aligned across hosts today: the Rust core, the iOS native path and Android all derive //peopl.dot//index_bytes(0|1). That is worth keeping true whatever is decided below; a per-host difference forks personhood by device.

To reproduce

truapi-host signing-host --network previewnet --base-path <path> \
  --product-id peopl.testnet --auto-accept --script register.ts

with register.ts calling truapi.account.registerRingVrfKey({ index: { tag: "Index", value: 0 }, ring: <People ring location> }), then the same run with --product-id peopl.testnet calling ringVrfSign with handle { dotNsIdentifier: "peopl.dot", derivationIndex: Index(0) }. The first returns a public key Members.Members does not know; the second returns NotAllowlisted. Re-run the first with --product-id peopl.dot and the key is the one the ring holds.

Two ways out

flowchart TB
    q{{"A product is peopl.&lt;tld&gt;.<br/>Which side moves?"}}
    q -- "the ownership rule" --> A
    q -- "the key derivation" --> B

    subgraph A["A · keys stay peopl.dot, peopl.&lt;tld&gt; owns them"]
        a1["register_ring_vrf_key from peopl.&lt;tld&gt;<br/>derives the reserved peopl.dot key"]
        a2["require_owned_ring_vrf_key and owner_entries<br/>match the reserved id by label"]
        a3["no migration, no other host changes"]
        a4["enshrines: product id = label,<br/>except keys, which are .dot"]
    end

    subgraph B["B · person keys become peopl.&lt;tld&gt;"]
        b1["derive_*_person_ring_vrf_entropy<br/>takes the network suffix the host already has"]
        b2["attestation, CLI person creation,<br/>allowance proofs follow"]
        b3["no ownership special-case;<br/>consistent with accounts, contexts, #521"]
        b4["RFC-0022 amended; iOS and Android in step;<br/>people on paseo-next-v2 re-onboard"]
    end
Loading

A. Keep the keys at peopl.dot; let the personhood product on any network own them. A peopl.<tld> session's register_ring_vrf_key produces the reserved keys, list_ring_vrf_keys answers for them as its own, and ring_vrf_sign / create_account_proof accept them. This is the rule REMOTE_PERMISSION_TRUSTED_LABELS already applies, extended to key ownership. Small, no migration, and every existing ring member stays a member. It also fixes into the protocol that keys alone ignore the TLD.

B. Derive the person keys under peopl.<network tld>. derive_full_person_ring_vrf_entropy and derive_lite_person_ring_vrf_entropy take the same network suffix the proof contexts take, and every consumer (attestation.rs, register_name.rs, accounts.rs, reserved_person_collection_candidates) follows. Then the personhood product's own register_ring_vrf_key mints the right key with no ownership change at all, and one identity scheme covers accounts, contexts and keys. The cost is real: RFC-0022 writes //peopl.dot//index_bytes(0) literally; the iOS native path and Android hardcode the same and must change together (iOS PR 1382 attempted exactly this and was closed); and everyone already in a ring on paseo-next-v2 holds a peopl.dot key and would need re-recognition. Previewnet is wiped regularly and does not carry that cost.

We lean to B as the end state, because A leaves two id schemes side by side and this issue is what that produces. But B is a decision for the RFC owner and the other hosts, not a bug fix.

Ask

Decide which side moves, and record it in RFC-0022 / RFC-0024 so the hosts stop diverging on it. Until then, a peopl.<tld> session on a test network has no way to register, list or sign with the person keys.

Related: #373 (ownership gate has no allowlist source), #521 (product id follows the network TLD), #515.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions