fix(dot init): stop double-deriving the product account; key username on wallet root#188
Merged
Merged
Conversation
… on wallet root
Three coupled bugs in `dot init`'s identity block.
1. Double-derivation. `IdentityLines` called `productAccountDisplay`
on `loggedInAddress`, which was already the product account (the
output of `createPlaygroundSessionSigner.publicKey`). The helper
then ran `deriveProductAccountPublicKey` again on the SS58-decoded
pubkey, producing a doubly-derived ghost (`5DHk4grp...`). The H160
we printed didn't match the playground-app's `derivedH160` for
the same account.
Fix: a `SessionAddresses = { rootAddress, productAddress,
productH160 }` triple is computed once in `auth.ts` via the new
`derivePlaygroundProductPublicKey` helper in `sessionSigner.ts`,
then threaded through `ConnectResult`, `LoginStatus.success`, and
`SessionHandle`. The signer and the display now share one
derivation function, so a future change to product-account params
can't desync the two.
Verified end-to-end against the playground-app's published log
for the test mnemonic `train snow there sponsor artwork zebra
gossip depth narrow blame change private`:
address = 5GGpUaN7XNaUp3nEVDPBSR4SQLxFxQsiPHbFwf69Apr3HgDZ
derivedH160 = 0x47f68a0851a663dfacb4610d673ec708f05576b0
These now match what `dot init` prints. A frozen-vector
regression test in `src/utils/auth.test.ts::deriveSessionAddresses`
locks the contract.
2. Username key. `lookupUsername` was being called with the product
account; usernames live at `Resources.Consumers[<rootAccountId>]`
on the People parachain. Storage is written by mobile's
`Resources.register_person` keyed on the SSO `rootAccountId`, not
any product derivation. Polkadot-desktop's `useSessionIdentity`
reads the same root key. `IdentityLines` now calls
`lookupUsername(addresses.rootAddress)`.
3. "logged in" label. The row labelled "logged in" used to show the
product account, which a user reasonably expects to be their
wallet identity. It now shows `addresses.rootAddress` (the
SSO-handshake `rootUserAccountId`). The product account is on its
own row underneath with the full SS58 + H160.
This still won't match what the mobile app shows as "Wallet
account address" on its debug screen (hard `//wallet` junction
the host can't reproduce from a public key) — but it does match
what the SSO handshake actually transmits and is the key the
username lookup is keyed on.
CLAUDE.md gets a new "Accounts: root, product, and what the mobile
app shows" section under "Non-obvious invariants" so the next
maintainer doesn't have to re-derive this from host-papp's codec.
Mechanically:
- `src/utils/sessionSigner.ts`: export
`derivePlaygroundProductPublicKey(rootAccountId, ref)`. Single
source of truth for product-account math; called by the signer
constructor and by `auth.ts::deriveSessionAddresses`.
- `src/utils/auth.ts`: new `SessionAddresses` type and
`deriveSessionAddresses(session)` helper. `ConnectResult.existing`,
`LoginStatus.success`, and `SessionHandle` carry the bundle.
`SessionHandle.address` is kept as a back-compat alias for
`addresses.productAddress` because `signer.ts::resolveSigner`
spreads the handle into `ResolvedSigner` and downstream deploy /
registry code reads `.address` for the signing key.
- `src/commands/init/IdentityLines.tsx`: take `SessionAddresses`,
render three rows, look up username against the root.
- `src/commands/init/InitScreen.tsx`: track `addresses` instead of
`loggedInAddress`. The "logged in" row is now rendered inside
`IdentityLines` only.
- `src/commands/init/QrLogin.tsx`: pass the full `SessionAddresses`
through `onDone`; suppress the duplicate "logged in" row on
success.
- `src/commands/init/index.ts`: rename `existingAddress` to
`existingAddresses`.
- `src/commands/init/identityLine.ts` + `identityLine.test.ts`:
deleted. The helpers encoded the exact wrong shape and have no
remaining callers.
- `src/utils/auth.test.ts`: three regression tests for
`deriveSessionAddresses` — frozen vector match, product != root,
H160-tracks-SS58.
Contributor
E2E Test Pass · ✅ PASSTag:
Sentry traces: view spans for this run |
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #185. Three coupled bugs in
dot init's identity block, surfaced when comparing the CLI's output to the playground-app's[playground.dot] selected accountlog and to the mobile app's developer-tools debug screen.1. Stop double-deriving the product account
IdentityLinescalledproductAccountDisplay(loggedInAddress).loggedInAddresswas already the product account (the output ofcreatePlaygroundSessionSigner.publicKey). The helper ss58-decoded it and randeriveProductAccountPublicKeya second time, producing a doubly-derived ghost address whose H160 didn't match what the playground-app uses.Fix: a
SessionAddresses = { rootAddress, productAddress, productH160 }triple is computed once inauth.ts::deriveSessionAddressesvia a new shared helperderivePlaygroundProductPublicKeyinsessionSigner.ts. Both the signer constructor and the display now go through the same function, so they can't drift.Verified end-to-end against the playground-app's log for the test mnemonic
train snow there sponsor artwork zebra gossip depth narrow blame change private:address5GGpUaN7XNaUp3nEVDPBSR4SQLxFxQsiPHbFwf69Apr3HgDZderivedH1600x47f68a0851a663dfacb4610d673ec708f05576b0Frozen-vector regression test in
src/utils/auth.test.ts::deriveSessionAddresseslocks the contract — three tests: known-vector match,product != root, and H160 moves in lock-step with the product SS58 when the root changes.2. Username lookup keyed on the wallet root
lookupUsernamewas being called with the product account. Usernames live atResources.Consumers[<rootAccountId>]on the People parachain — mobile'sResources.register_personwrites them keyed on the SSOrootAccountId. Polkadot-desktop'suseSessionIdentity(session)reads the same root key.IdentityLinesnow callslookupUsername(addresses.rootAddress).3. "logged in" label
The row used to show the product account, which the user reasonably expects to be their wallet identity. It now shows
addresses.rootAddress(the SSO-handshakerootUserAccountId). The product account is on its own row underneath with the full SS58 + H160.This still won't match what the mobile app shows as "Wallet account address" on its debug screen (hard
//walletjunction; the host can't reach it from a public key alone) — but it does match what the SSO handshake actually transmits and is the storage key the username lookup uses.What the user sees now
Documentation
CLAUDE.md gets a new "Accounts: root, product, and what the mobile app shows" section under "Non-obvious invariants" so the next maintainer can re-verify the host-vs-mobile derivation map without re-reading host-papp's handshake codec.
Mechanical changes
src/utils/sessionSigner.ts: exportderivePlaygroundProductPublicKey(rootAccountId, ref)— single source of truth for product-account math.src/utils/auth.ts: newSessionAddressestype +deriveSessionAddresses(session)helper.ConnectResult.existing,LoginStatus.success, andSessionHandlecarry the bundle.SessionHandle.addresskept as a back-compat alias.src/commands/init/IdentityLines.tsx: takeSessionAddresses, render three rows, look up username against root.src/commands/init/InitScreen.tsx: trackaddressesinstead ofloggedInAddress. The "logged in" row now lives insideIdentityLinesonly.src/commands/init/QrLogin.tsx: pass fullSessionAddressesthroughonDone; suppress the duplicate "logged in" row on success.src/commands/init/index.ts: renameexistingAddress→existingAddresses.src/commands/init/identityLine.ts+identityLine.test.ts: deleted. The helpers encoded the exact wrong shape and have no remaining callers.Test plan
pnpm format:check(145 files clean)pnpm lint:license(172/172 files)pnpm test— 536 passed, 1 skipped (added 3 new regression tests in thederiveSessionAddressesblock)dot initpost-merge: confirm three rows render with full root + product SS58 + H160; confirmusernamelookup completes against the root (returns(no username set on chain)for accounts that haven't claimed one, otherwise the actual username)