mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-28 02:38:44 +00:00
names: derive one key per name at m/44'/60'/i'/0/k
This commit is contained in:
@@ -23,6 +23,75 @@ The client keeps no local names record.
|
||||
chain, payment, stealth/gifting, GUIs, min-commitment-age enforcement,
|
||||
tx-hash inclusion verification, anti-grief deposits.
|
||||
|
||||
## One key per name
|
||||
|
||||
A name is owned by a key derived at `m/44'/60'/i'/0/k` — the profile's BIP-44
|
||||
account `i`, and the name at address index `k`. `k` is taken when the name is
|
||||
registered, so a profile's second name lands on a different address.
|
||||
|
||||
```
|
||||
seed (BIP-39)
|
||||
└── profile account i
|
||||
└── m/44'/60'/i'/0/k one key per name; k = 0 is the profile's first
|
||||
```
|
||||
|
||||
Worked through, for a profile that buys two names and later imports a second
|
||||
seed it had used in a dapp:
|
||||
|
||||
```
|
||||
seed1 generated by the CLI
|
||||
└── profile Alice = account 0
|
||||
├── m/44'/60'/0'/0/0 0x69A6…2d32 owns alice.simplex
|
||||
└── m/44'/60'/0'/0/1 0x4C1f…9Ab7 owns lizzy.simplex
|
||||
|
||||
seed2 imported later
|
||||
└── m 0x1D07…4bE9 owns lucy.simplex (root, no derivation)
|
||||
```
|
||||
|
||||
Nothing here is a custom layout: `account` and `address_index` are what BIP-44
|
||||
has those levels for, and the addresses line up with wallets users already have.
|
||||
Pinned in the tests against the standard `abandon … about` mnemonic:
|
||||
|
||||
```
|
||||
m/44'/60'/0'/0/0 0x9858EfFD232B4033E47d90003D41EC34EcaEda94 MetaMask account 1
|
||||
m/44'/60'/0'/0/1 0x6Fac4D18c912343BF86fa7049364Dd4E424Ab9C0 MetaMask account 2
|
||||
m/44'/60'/1'/0/0 0x78839F6054d7ed13918bAe0473BA31b1Ca9D7265 Ledger Live account 2
|
||||
```
|
||||
|
||||
So **profile 0's names are exactly MetaMask's account list, in order**, and each
|
||||
profile's first name is the matching Ledger Live account. Moving a single name
|
||||
between this wallet and another one is therefore a derivation question already
|
||||
answered — but the commands to do it (recovery-phrase import, single-name key
|
||||
export) are follow-up work, not in this PR.
|
||||
|
||||
**Why not one key per profile.** Exporting it would hand over every name that
|
||||
profile owns, and `SimplexResolver` keeps one nonce per signer shared across
|
||||
every node it owns — so a shared key would serialise every name's record edits
|
||||
behind one counter. Both go away with an index per name.
|
||||
|
||||
**Which key owns which name is not on chain and not derivable**, so it is
|
||||
recorded locally (`wallet_name_keys`). The path is stored literally rather than
|
||||
as indices, because a name found on an imported seed may sit on a layout that is
|
||||
not ours — `lucy.simplex` above stays at the root and is re-derived from `"m"`.
|
||||
|
||||
### Where stealth addresses will attach
|
||||
|
||||
Not in this PR, but the layout has to leave room. A profile publishes **one
|
||||
meta-address**: a spend key and a viewing key, both hardened under purpose
|
||||
`5564'` at the profile level, `m/5564'/60'/i'/0'/0` and `m/5564'/60'/i'/1'/0`.
|
||||
|
||||
A sender derives a fresh destination from it without any handshake — shared
|
||||
secret `s = keccak256(r · P_view)` for a random ephemeral `r`, destination
|
||||
`addr(P_spend + s·G)` — and the recipient recomputes `s` from the sender's
|
||||
ephemeral public key `R` as `keccak256(p_view · R)`, holding the name with
|
||||
`p_spend + s`.
|
||||
|
||||
So a received name's key is **not at a derivation path**: it is the spend key
|
||||
plus a scalar, recoverable from `R` rather than from an index. One meta-address
|
||||
per profile therefore serves any number of received names, which is why the
|
||||
meta-address sits at the profile level while owned names sit at the address
|
||||
level.
|
||||
|
||||
## Why two RPCs for one call
|
||||
|
||||
`purchaseName` splits into **commit** then **reveal** so the registrar cannot
|
||||
@@ -146,8 +215,8 @@ a user registering the same name again.
|
||||
|
||||
| Component | This PR | Extends to |
|
||||
|---|---|---|
|
||||
| **Wallet** | `Wallet.newSeed` + `deriveAccount` + `accountAddress`, already built. No signing. | `signIntent` (also built) unlocks edits/transfers with no shape change. |
|
||||
| **Wallet storage** | the prototype's `wallet_seeds` migration and `Store.Wallets` verbatim: one seed per DB, one account index per profile, allocated from a stored high-water mark. | recovery import, `backed_up` reminder and the one-time-address table are already in the schema; they need code only. |
|
||||
| **Wallet** | `Wallet.newSeed` + `deriveNameKey` / `deriveAtPath` + `accountAddress`. No signing. | `signIntent` (also built) unlocks edits/transfers with no shape change. |
|
||||
| **Wallet storage** | `wallet_seeds` plus `wallet_name_keys`: one account index per profile, one address index per name, both from stored high-water marks. | recovery import, `backed_up` reminder and the one-time-address table are already in the schema; they need code only. |
|
||||
| **RPC transport** | badges' `APISendServiceRequest` / `APISendServiceResponse`, unchanged. | shared. |
|
||||
| **Service** | extend `BadgeService/Service.hs` `handleServiceRequest` to dispatch `NRCommit`/`NRReveal`; add an in-memory chain mock (a `TVar (Map name entry)`, like `Names.Service.Mock`). | swap the mock for a relayer to a deployed SNRC. |
|
||||
| **Resolution** | the mock chain is the record: the name resolves there to owner and SimpleX link. No client-side names store. | a resolver read against a deployed SNRC; add a local cache only if a listing UX needs it. |
|
||||
|
||||
Reference in New Issue
Block a user