feat: default new accounts to ML-DSA-65, and stop conflating derivable with signable
New accounts are dilithium65. ML-DSA-87 stays importable — accounts predating the recorded scheme, and the dev-genesis ones — but is never offered as a choice. The important part of this change is not the default, it is splitting canDerive. Upstream used canDerive in two unrelated places: gating the derive-from-parent UI, and filtering which accounts are handed to dapps. That worked only because every type it knew about happened to be derivable, so the second use was really asking "is the type known". The coincidence ends with ML-DSA. Making canDerive honest — false for lattice keys, which are not derivable from one another — would have silently hidden **every Quantus account from every dapp**, leaving an extension that looked empty to the entire ecosystem while holding perfectly good accounts. It types and builds and tests clean either way, which is what makes it worth a comment and a test rather than a one-line edit. So: canInject for dapp injection, canDerive for actual derivation. Tabs.ts uses the former. canDerive returning false for ML-DSA removes the derive-from-parent flow on its own — `master` in the account context is found with it, and the menu item is gated on `master`. That is the right outcome and a permanent one: a pair holds no material a child could come from, and pair.derive() refuses outright (quantus/common#4). Adding a second account is therefore importing the same phrase with an index, which ImportSeed already supports — it builds `suri = seed + path` and the suri rules accept `//1`. Its advanced field is relabelled from "derivation path" to an account index, with a note saying each Quantus account derives from the phrase independently, because "derivation path" invites a junction chain that will be rejected. getSuri needs no change: it only special-cases ethereum, and returns the phrase untouched for everything else, which is what the Quantus path wants. Verified rather than assumed — the new spec checks the default type actually produces the address quantus-cli derives for the public dev phrase, that //1 gives a different account, and that ML-DSA is injectable but not derivable. 70 tests pass; typecheck, lint and build:chrome clean. Refs quantus/extension#3, quantus/extension#8 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
This commit is contained in:
co-authored by
Claude Opus 5
parent
6dfd109991
commit
ac08676ea7
@@ -20,7 +20,7 @@ import { accounts as accountsObservable } from '@polkadot/ui-keyring/observable/
|
||||
import { assert, isNumber } from '@polkadot/util';
|
||||
|
||||
import { PHISHING_PAGE_REDIRECT } from '../../defaults.js';
|
||||
import { canDerive } from '../../utils/index.js';
|
||||
import { canInject } from '../../utils/index.js';
|
||||
import RequestBytesSign from '../RequestBytesSign.js';
|
||||
import RequestExtrinsicSign from '../RequestExtrinsicSign.js';
|
||||
import { withErrorLog } from './helpers.js';
|
||||
@@ -35,7 +35,7 @@ function transformAccounts (accounts: SubjectInfo, anyType = false): InjectedAcc
|
||||
return Object
|
||||
.values(accounts)
|
||||
.filter(({ json: { meta: { isHidden } } }) => !isHidden)
|
||||
.filter(({ type }) => anyType ? true : canDerive(type))
|
||||
.filter(({ type }) => anyType ? true : canInject(type))
|
||||
.sort((a, b) => (a.json.meta.whenCreated || 0) - (b.json.meta.whenCreated || 0))
|
||||
.map(({ json: { address, meta: { genesisHash, name } }, type }): InjectedAccount => ({
|
||||
address,
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
|
||||
import type { KeypairType } from '@polkadot/util-crypto/types';
|
||||
|
||||
export function canDerive (type?: KeypairType): boolean {
|
||||
return !!type && ['ed25519', 'sr25519', 'ecdsa', 'ethereum'].includes(type);
|
||||
/** Every keypair type this extension can hold and sign with. */
|
||||
const SIGNABLE: KeypairType[] = ['dilithium65', 'dilithium87', 'ecdsa', 'ed25519', 'ethereum', 'sr25519'];
|
||||
|
||||
/** The types that support deriving a child account from a parent's key. */
|
||||
const DERIVABLE: KeypairType[] = ['ecdsa', 'ed25519', 'ethereum', 'sr25519'];
|
||||
|
||||
/**
|
||||
* Whether an account of this type should be offered to dapps.
|
||||
*
|
||||
* Upstream used `canDerive` for this, which worked only because every type it
|
||||
* knew about was derivable — so the check was really "is the type known". That
|
||||
* coincidence ends with ML-DSA: lattice keys are not derivable from one another,
|
||||
* and using `canDerive` here would have hidden **every Quantus account from every
|
||||
* dapp**, leaving an extension that looked empty to the whole ecosystem while
|
||||
* holding perfectly good accounts.
|
||||
*/
|
||||
export function canInject (type?: KeypairType): boolean {
|
||||
return !!type && SIGNABLE.includes(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a child account can be derived from a parent of this type.
|
||||
*
|
||||
* False for ML-DSA, and permanently so. Quantus derives every account from the
|
||||
* mnemonic independently along `m/44'/189189'/<account>'/0'/<scheme>'`, and a
|
||||
* pair holds no material a child could come from — `pair.derive()` refuses
|
||||
* outright (quantus/common#4). Adding another account therefore needs the
|
||||
* recovery phrase, not an unlocked parent, which is why the derive-from-parent
|
||||
* flow has no Quantus meaning.
|
||||
*/
|
||||
export function canDerive (type?: KeypairType): boolean {
|
||||
return !!type && DERIVABLE.includes(type);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright 2019-2026 @polkadot/extension-base authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export { canDerive } from './canDerive.js';
|
||||
export { canDerive, canInject } from './canDerive.js';
|
||||
export { isExtrinsicRequest } from './isExtrinsicRequest.js';
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2019-2026 @polkadot/extension-base authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// <reference types="@polkadot/dev-test/globals.d.ts" />
|
||||
import { Keyring } from '@polkadot/keyring';
|
||||
|
||||
import { canDerive, canInject } from './canDerive.js';
|
||||
|
||||
describe('quantus defaults', (): void => {
|
||||
it('the default type produces a Quantus account', (): void => {
|
||||
const keyring = new Keyring({ ss58Format: 189, type: 'dilithium65' });
|
||||
const pair = keyring.createFromUri(
|
||||
'bottom drive obey lake curtain smoke basket hold race lonely fit walk', {}, 'dilithium65');
|
||||
|
||||
expect(pair.address).toEqual('qzq29m9WvneDAeXbtgueKCREtNe1rVVs6bXSMLmjr6shqvwq6');
|
||||
});
|
||||
|
||||
it('//1 gives a different account from the same phrase', (): void => {
|
||||
const keyring = new Keyring({ ss58Format: 189, type: 'dilithium65' });
|
||||
const phrase = 'bottom drive obey lake curtain smoke basket hold race lonely fit walk';
|
||||
const zero = keyring.createFromUri(phrase, {}, 'dilithium65').address;
|
||||
const one = keyring.createFromUri(`${phrase}//1`, {}, 'dilithium65').address;
|
||||
|
||||
expect(zero).not.toEqual(one);
|
||||
});
|
||||
|
||||
// The trap: canDerive gated dapp injection upstream. If that had stayed, no
|
||||
// Quantus account would reach any dapp.
|
||||
it('ML-DSA accounts are injected but not derivable', (): void => {
|
||||
expect(canInject('dilithium65')).toEqual(true);
|
||||
expect(canInject('dilithium87')).toEqual(true);
|
||||
expect(canDerive('dilithium65')).toEqual(false);
|
||||
expect(canDerive('dilithium87')).toEqual(false);
|
||||
});
|
||||
|
||||
it('leaves the curve types alone', (): void => {
|
||||
expect(canInject('sr25519')).toEqual(true);
|
||||
expect(canDerive('sr25519')).toEqual(true);
|
||||
expect(canInject(undefined)).toEqual(false);
|
||||
});
|
||||
});
|
||||
@@ -101,13 +101,23 @@ function SeedAndPath ({ className, onAccountChange, onNextStep, type }: Props):
|
||||
<span>{t('advanced')}</span>
|
||||
</div>
|
||||
{ advanced && (
|
||||
<InputWithLabel
|
||||
className='derivationPath'
|
||||
isError={!!path && !!error}
|
||||
label={t('derivation path')}
|
||||
onChange={setPath}
|
||||
value={path || ''}
|
||||
/>
|
||||
<>
|
||||
<InputWithLabel
|
||||
className='derivationPath'
|
||||
isError={!!path && !!error}
|
||||
// Quantus derives each account from the phrase independently, so
|
||||
// this is an account index rather than a junction chain — see
|
||||
// quantus/common#4 for the accepted forms. Leaving it empty gives
|
||||
// account 0, which is what every other Quantus tool calls the
|
||||
// first account.
|
||||
label={t('account index, e.g. //1 (leave empty for the first account)')}
|
||||
onChange={setPath}
|
||||
value={path || ''}
|
||||
/>
|
||||
<Warning>
|
||||
{t('Each Quantus account derives from your recovery phrase on its own. To add a second account, enter the same phrase with //1, a third with //2, and so on.')}
|
||||
</Warning>
|
||||
</>
|
||||
)}
|
||||
{!!error && !!seed && (
|
||||
<Warning
|
||||
|
||||
@@ -3,4 +3,11 @@
|
||||
|
||||
import type { KeypairType } from '@polkadot/util-crypto/types';
|
||||
|
||||
export const DEFAULT_TYPE: KeypairType = 'sr25519';
|
||||
/**
|
||||
* The scheme new accounts use.
|
||||
*
|
||||
* ML-DSA-87 (`dilithium87`) is the legacy parameter set — accounts predating the
|
||||
* recorded scheme, and the dev-genesis accounts — so it must remain importable
|
||||
* but is never offered as a choice.
|
||||
*/
|
||||
export const DEFAULT_TYPE: KeypairType = 'dilithium65';
|
||||
|
||||
@@ -206,5 +206,7 @@
|
||||
"This is not a valid address": "",
|
||||
"Paste the address you want to track": "",
|
||||
"This account has no key in the extension, so it cannot sign. You will see its balance and can use it as a recipient.": "",
|
||||
"Add the address": ""
|
||||
"Add the address": "",
|
||||
"account index, e.g. //1 (leave empty for the first account)": "",
|
||||
"Each Quantus account derives from your recovery phrase on its own. To add a second account, enter the same phrase with //1, a third with //2, and so on.": ""
|
||||
}
|
||||
|
||||
@@ -100,7 +100,10 @@ cryptoWaitReady()
|
||||
console.log('crypto initialized');
|
||||
|
||||
// load all the keyring data
|
||||
keyring.loadAll({ store: new AccountsStore(), type: 'sr25519' });
|
||||
// ML-DSA-65 is the scheme new Quantus accounts use. ML-DSA-87 is legacy —
|
||||
// accounts created before the scheme was recorded, and the dev-genesis ones —
|
||||
// so it is importable but never chosen. See quantus/extension#1.
|
||||
keyring.loadAll({ store: new AccountsStore(), type: 'dilithium65' });
|
||||
|
||||
console.log('initialization completed');
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user