Skip to content

fix(dot init): stop double-deriving the product account; key username on wallet root#188

Merged
UtkarshBhardwaj007 merged 1 commit into
mainfrom
fix/dot-init-account-derivation
May 19, 2026
Merged

fix(dot init): stop double-deriving the product account; key username on wallet root#188
UtkarshBhardwaj007 merged 1 commit into
mainfrom
fix/dot-init-account-derivation

Conversation

@UtkarshBhardwaj007
Copy link
Copy Markdown
Member

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 account log and to the mobile app's developer-tools debug screen.

1. Stop double-deriving the product account

IdentityLines called productAccountDisplay(loggedInAddress). loggedInAddress was already the product account (the output of createPlaygroundSessionSigner.publicKey). The helper ss58-decoded it and ran deriveProductAccountPublicKey a 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 in auth.ts::deriveSessionAddresses via a new shared helper derivePlaygroundProductPublicKey in sessionSigner.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:

Playground-app log CLI (after fix)
product address 5GGpUaN7XNaUp3nEVDPBSR4SQLxFxQsiPHbFwf69Apr3HgDZ matches
product derivedH160 0x47f68a0851a663dfacb4610d673ec708f05576b0 matches

Frozen-vector regression test in src/utils/auth.test.ts::deriveSessionAddresses locks 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

lookupUsername was being called with the product account. Usernames live at Resources.Consumers[<rootAccountId>] on the People parachain — mobile's Resources.register_person writes them keyed on the SSO rootAccountId. Polkadot-desktop's useSessionIdentity(session) reads the same root key. IdentityLines now calls lookupUsername(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-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 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

✓ logged in       5FZEMcMGTjSveipHTD35RsRtMqZf2wk41g2zAPL8j2UwWTrp
✓ username        alice.dot    (or "(no username set on chain)")
✓ product account 5GGpUaN7XNaUp3nEVDPBSR4SQLxFxQsiPHbFwf69Apr3HgDZ (0x47f68a0851a663dfacb4610d673ec708f05576b0)

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: export derivePlaygroundProductPublicKey(rootAccountId, ref) — single source of truth for product-account math.
  • src/utils/auth.ts: new SessionAddresses type + deriveSessionAddresses(session) helper. ConnectResult.existing, LoginStatus.success, and SessionHandle carry the bundle. SessionHandle.address kept as a back-compat alias.
  • src/commands/init/IdentityLines.tsx: take SessionAddresses, render three rows, look up username against root.
  • src/commands/init/InitScreen.tsx: track addresses instead of loggedInAddress. The "logged in" row now lives inside IdentityLines only.
  • src/commands/init/QrLogin.tsx: pass full SessionAddresses through onDone; suppress the duplicate "logged in" row on success.
  • src/commands/init/index.ts: rename existingAddressexistingAddresses.
  • 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 the deriveSessionAddresses block)
  • Live derivation match against the playground-app's published log for the test mnemonic
  • Manual dot init post-merge: confirm three rows render with full root + product SS58 + H160; confirm username lookup completes against the root (returns (no username set on chain) for accounts that haven't claimed one, otherwise the actual username)

… 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.
@UtkarshBhardwaj007 UtkarshBhardwaj007 merged commit 2211853 into main May 19, 2026
17 of 18 checks passed
@UtkarshBhardwaj007 UtkarshBhardwaj007 deleted the fix/dot-init-account-derivation branch May 19, 2026 19:22
@github-actions
Copy link
Copy Markdown
Contributor

E2E Test Pass · ✅ PASS

Tag: e2e-ci-pr · Branch: fix/dot-init-account-derivation · Commit: 7cd286f · Run logs

Cell Result Time
pr-deploy-cdm ✅ PASS 2m34s
pr-mod ✅ PASS 2m00s
pr-preflight ✅ PASS 2m54s
pr-install ✅ PASS 0m53s
pr-init-session ✅ PASS 1m51s
pr-deploy-frontend ✅ PASS 3m13s
pr-deploy-foundry ✅ PASS 1m09s
${{ matrix.cell }} ⏭️ SKIP 0m-1s
${{ matrix.cell }} ⏭️ SKIP 0m-1s

Sentry traces: view spans for this run

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant