restoreAccount(json,password){constcryptoType=Array.isArray(json.encoding.content)?json.encoding.content[1]:'ed25519';constencType=Array.isArray(json.encoding.type)?json.encoding.type:[json.encoding.type];constpair=createPair({toSS58:this.encodeAddress,type:cryptoType},{publicKey:this.decodeAddress(json.address,true)},// ← here
json.meta,…);pair.decodePkcs8(password);…}
decodeAddress turns the SS58 string back into 32 bytes and hands them over as the public key. On Substrate that is correct — the address is the public key. On Quantus the address is Poseidon2(publicKey) and the real public key is 1952 or 2592 bytes, so this constructs a pair whose public key is a hash of the public key.
Note the ordering: decodePkcs8(password) runs on the next line and does have the real public key available inside the encrypted blob. So the fix is not to find the key from somewhere new — it is to stop pretending to know it before the blob is open, and to let the pair carry the account id as data until it is.
Do not solve this twice
quantus/common#3 has to answer the identical question for keyring.createFromJson, and it is the one that decides what createPair accepts. Settle the shape there, then apply it here — two independent answers to "how does a locked ML-DSA pair know its address" is how the two paths drift and one of them starts returning a wrong-but-plausible address.
While in this file
loadAddress guards with json.address.length !== 66 to spot Ethereum-style addresses among hex. A Quantus account id is 32 bytes and hits the normal path, so this is fine as it stands — but it is exactly the kind of length-sniffing that will need revisiting if raw public keys ever end up in this store. Worth a comment rather than a change.
Acceptance
a JSON file exported by the extension restores to an account with the same address
the address reported before unlock matches the address after unlock
Mirror of quantus/common#3 — the same mistake, in a second place. Context in quantus/extension#1.
## The line
`packages/ui-keyring/src/Keyring.ts`:
```js
restoreAccount (json, password) {
const cryptoType = Array.isArray(json.encoding.content) ? json.encoding.content[1] : 'ed25519';
const encType = Array.isArray(json.encoding.type) ? json.encoding.type : [json.encoding.type];
const pair = createPair(
{ toSS58: this.encodeAddress, type: cryptoType },
{ publicKey: this.decodeAddress(json.address, true) }, // ← here
json.meta,
…
);
pair.decodePkcs8(password);
…
}
```
`decodeAddress` turns the SS58 string back into 32 bytes and hands them over as the public key. On Substrate that is correct — the address *is* the public key. On Quantus the address is `Poseidon2(publicKey)` and the real public key is 1952 or 2592 bytes, so this constructs a pair whose public key is a hash of the public key.
Note the ordering: `decodePkcs8(password)` runs on the *next* line and does have the real public key available inside the encrypted blob. So the fix is not to find the key from somewhere new — it is to stop pretending to know it before the blob is open, and to let the pair carry the account id as data until it is.
## Do not solve this twice
quantus/common#3 has to answer the identical question for `keyring.createFromJson`, and it is the one that decides what `createPair` accepts. **Settle the shape there, then apply it here** — two independent answers to "how does a locked ML-DSA pair know its address" is how the two paths drift and one of them starts returning a wrong-but-plausible address.
## While in this file
`loadAddress` guards with `json.address.length !== 66` to spot Ethereum-style addresses among hex. A Quantus account id is 32 bytes and hits the normal path, so this is fine as it stands — but it is exactly the kind of length-sniffing that will need revisiting if raw public keys ever end up in this store. Worth a comment rather than a change.
## Acceptance
- [ ] a JSON file exported by the extension restores to an account with the same address
- [ ] the address reported before unlock matches the address after unlock
- [ ] upstream-format ed25519/sr25519 files restore unchanged
Exactly as this issue framed it — solved in quantus/common#3 first, then mirrored. restoreAccount now passes an accountId rather than a public key it cannot have:
createPair uses it for the address while locked and clears it once decodePkcs8 supplies the real key — checking the two agree on the way. That check earns its place here for the same reason as in the keyring: the address sits outside the encrypted blob, so a tampered one decodes cleanly and yields an account displaying an address its key cannot sign for. Pinned by a test.
The 'ed25519' fallback for version-0 JSON is left as-is and commented. Those files predate the crypto type being recorded and are by definition not Quantus, so the guess is safe — but quantus/common#6 removes ed25519 from KeypairType, at which point it has to become a refusal.
Acceptance
a JSON file exported by the extension restores to an account with the same address
the address reported before unlock matches the address after unlock
upstream-format sr25519 files restore unchanged
the restored account can sign, and the signature verifies against its address
The cross-repo linking has run out of road
Getting this repo to see the forked common took four separate fixes, each for the same underlying cause: node resolves a portal/link symlink to its realpath, after which @polkadot/util-crypto resolves inside common's tree — where it is the workspace source directory, with no exports map.
resolutions pointing the four @polkadot packages at common's build output
tsconfig.base.json path mappings, with the *.d.ts substitution first, because NodeNext will not infer an extension for a bare path and looks for a typesdirectory instead of types.d.ts
tsconfig.json overriding those with the runtime .js view, because @polkadot/dev-ts reads that file and would otherwise import a declaration file as a module — "Debug Failure. Output generation failed"
--preserve-symlinks in the test script, which yarn itself warns is required for portals
All four are dev-time scaffolding for a problem that does not exist once these packages come from a registry. This is the third repo to hit it (common → @quantus/crypto, now ui → common), and the extension will be the fourth with more dependencies than any of them.
quantus/extension#2 is now the blocking item, not a tidy-up. Publishing to the Gitea npm registry needs a token, which I cannot create — so it needs a hand. Until then every downstream repo pays this tax again.
One incidental result worth recording: yarn lint errors dropped from 25 to 8. The tsconfig mappings resolved types that were previously any, removing a pile of no-unsafe-assignment. The remaining 8 are upstream's own, in loadContract and allowGenesis, and are left alone per the rebasability convention — note that yarn lint therefore still exits non-zero on this repo, as it does on pristine upstream.
Done, on `quantus-restore-account` (`ec1399f3`). 5 specs; 46 tests pass repo-wide, build clean.
## The fix
Exactly as this issue framed it — solved in quantus/common#3 first, then mirrored. `restoreAccount` now passes an **accountId** rather than a public key it cannot have:
```ts
const info = isDilithium(cryptoType)
? { accountId: raw, publicKey: new Uint8Array(), secretKey: new Uint8Array() }
: { publicKey: raw };
```
`createPair` uses it for the address while locked and clears it once `decodePkcs8` supplies the real key — checking the two agree on the way. That check earns its place here for the same reason as in the keyring: the address sits outside the encrypted blob, so a tampered one decodes cleanly and yields an account displaying an address its key cannot sign for. Pinned by a test.
The `'ed25519'` fallback for version-0 JSON is left as-is and commented. Those files predate the crypto type being recorded and are by definition not Quantus, so the guess is safe — but quantus/common#6 removes `ed25519` from `KeypairType`, at which point it has to become a refusal.
## Acceptance
- [x] a JSON file exported by the extension restores to an account with the same address
- [x] the address reported before unlock matches the address after unlock
- [x] upstream-format sr25519 files restore unchanged
- [x] the restored account can sign, and the signature verifies against its address
## The cross-repo linking has run out of road
Getting this repo to see the forked `common` took four separate fixes, each for the same underlying cause: **node resolves a portal/link symlink to its realpath**, after which `@polkadot/util-crypto` resolves inside *common's* tree — where it is the workspace source directory, with no `exports` map.
1. `resolutions` pointing the four `@polkadot` packages at `common`'s build output
2. `tsconfig.base.json` path mappings, with the `*.d.ts` substitution **first**, because NodeNext will not infer an extension for a bare path and looks for a `types` *directory* instead of `types.d.ts`
3. `tsconfig.json` overriding those with the runtime `.js` view, because `@polkadot/dev-ts` reads that file and would otherwise import a declaration file as a module — "Debug Failure. Output generation failed"
4. `--preserve-symlinks` in the test script, which yarn itself warns is required for portals
All four are dev-time scaffolding for a problem that does not exist once these packages come from a registry. This is the third repo to hit it (`common` → `@quantus/crypto`, now `ui` → `common`), and the extension will be the fourth with more dependencies than any of them.
**quantus/extension#2 is now the blocking item**, not a tidy-up. Publishing to the Gitea npm registry needs a token, which I cannot create — so it needs a hand. Until then every downstream repo pays this tax again.
One incidental result worth recording: `yarn lint` errors dropped from **25 to 8**. The tsconfig mappings resolved types that were previously `any`, removing a pile of `no-unsafe-assignment`. The remaining 8 are upstream's own, in `loadContract` and `allowGenesis`, and are left alone per the rebasability convention — note that `yarn lint` therefore still exits non-zero on this repo, as it does on pristine upstream.
Restored address is right: fixed in ec1399f3. restoreAccount passes the stored account id rather than inventing a public key, the same approach as common's createFromJson. An extension-exported ML-DSA JSON restores to the same address and can sign (Keyring.dilithium.spec.ts).
Tampering is caught: a JSON whose address does not match its key is rejected.
Address stable across unlock: checked in common's dilithiumJson.spec.ts, since restoreAccount unlocks immediately.
Upstream files unchanged: sr25519 restore is pinned, and ed25519 still takes the unchanged branch until quantus/common#6 removes it.
Verified against `main` (`2d6a9ad1`), closing.
- **Restored address is right:** fixed in `ec1399f3`. `restoreAccount` passes the stored account id rather than inventing a public key, the same approach as common's `createFromJson`. An extension-exported ML-DSA JSON restores to the same address and can sign (`Keyring.dilithium.spec.ts`).
- **Tampering is caught:** a JSON whose address does not match its key is rejected.
- **Address stable across unlock:** checked in common's `dilithiumJson.spec.ts`, since `restoreAccount` unlocks immediately.
- **Upstream files unchanged:** sr25519 restore is pinned, and ed25519 still takes the unchanged branch until quantus/common#6 removes it.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Mirror of quantus/common#3 — the same mistake, in a second place. Context in quantus/extension#1.
The line
packages/ui-keyring/src/Keyring.ts:decodeAddressturns the SS58 string back into 32 bytes and hands them over as the public key. On Substrate that is correct — the address is the public key. On Quantus the address isPoseidon2(publicKey)and the real public key is 1952 or 2592 bytes, so this constructs a pair whose public key is a hash of the public key.Note the ordering:
decodePkcs8(password)runs on the next line and does have the real public key available inside the encrypted blob. So the fix is not to find the key from somewhere new — it is to stop pretending to know it before the blob is open, and to let the pair carry the account id as data until it is.Do not solve this twice
quantus/common#3 has to answer the identical question for
keyring.createFromJson, and it is the one that decides whatcreatePairaccepts. Settle the shape there, then apply it here — two independent answers to "how does a locked ML-DSA pair know its address" is how the two paths drift and one of them starts returning a wrong-but-plausible address.While in this file
loadAddressguards withjson.address.length !== 66to spot Ethereum-style addresses among hex. A Quantus account id is 32 bytes and hits the normal path, so this is fine as it stands — but it is exactly the kind of length-sniffing that will need revisiting if raw public keys ever end up in this store. Worth a comment rather than a change.Acceptance
Done, on
quantus-restore-account(ec1399f3). 5 specs; 46 tests pass repo-wide, build clean.The fix
Exactly as this issue framed it — solved in quantus/common#3 first, then mirrored.
restoreAccountnow passes an accountId rather than a public key it cannot have:createPairuses it for the address while locked and clears it oncedecodePkcs8supplies the real key — checking the two agree on the way. That check earns its place here for the same reason as in the keyring: the address sits outside the encrypted blob, so a tampered one decodes cleanly and yields an account displaying an address its key cannot sign for. Pinned by a test.The
'ed25519'fallback for version-0 JSON is left as-is and commented. Those files predate the crypto type being recorded and are by definition not Quantus, so the guess is safe — but quantus/common#6 removesed25519fromKeypairType, at which point it has to become a refusal.Acceptance
The cross-repo linking has run out of road
Getting this repo to see the forked
commontook four separate fixes, each for the same underlying cause: node resolves a portal/link symlink to its realpath, after which@polkadot/util-cryptoresolves inside common's tree — where it is the workspace source directory, with noexportsmap.resolutionspointing the four@polkadotpackages atcommon's build outputtsconfig.base.jsonpath mappings, with the*.d.tssubstitution first, because NodeNext will not infer an extension for a bare path and looks for atypesdirectory instead oftypes.d.tstsconfig.jsonoverriding those with the runtime.jsview, because@polkadot/dev-tsreads that file and would otherwise import a declaration file as a module — "Debug Failure. Output generation failed"--preserve-symlinksin the test script, which yarn itself warns is required for portalsAll four are dev-time scaffolding for a problem that does not exist once these packages come from a registry. This is the third repo to hit it (
common→@quantus/crypto, nowui→common), and the extension will be the fourth with more dependencies than any of them.quantus/extension#2 is now the blocking item, not a tidy-up. Publishing to the Gitea npm registry needs a token, which I cannot create — so it needs a hand. Until then every downstream repo pays this tax again.
One incidental result worth recording:
yarn linterrors dropped from 25 to 8. The tsconfig mappings resolved types that were previouslyany, removing a pile ofno-unsafe-assignment. The remaining 8 are upstream's own, inloadContractandallowGenesis, and are left alone per the rebasability convention — note thatyarn linttherefore still exits non-zero on this repo, as it does on pristine upstream.Verified against
main(2d6a9ad1), closing.ec1399f3.restoreAccountpasses the stored account id rather than inventing a public key, the same approach as common'screateFromJson. An extension-exported ML-DSA JSON restores to the same address and can sign (Keyring.dilithium.spec.ts).dilithiumJson.spec.ts, sincerestoreAccountunlocks immediately.