feat(networks): offer only chains whose accounts are post-quantum
Deployments built on this fork should gate on chains using PQ algorithms, so
`availableNetworks` and `selectableNetworks` now exclude anything whose
standardAccount is a discrete-log scheme. *25519, Ed25519, Sr25519 and secp256k1
are all broken at once by Shor's algorithm, so a chain using one has no
quantum-safe account type to offer whatever else is true of it. A null
standardAccount counts as unsafe: unknown is not the same as safe, and being
wrong in that direction means a user holding funds under a recoverable key.
`allNetworks` is deliberately left complete. Gating what we offer must not gate
what we can read — a pasted Polkadot address has to render as a Polkadot address
so it can be recognised and refused. A wallet that cannot decode an address it
will not accept can only say "invalid", which reads as a typo rather than as a
warning.
Five upstream tests asserted the shape of a list that now holds two entries
instead of a hundred and fifty. Each is rewritten to assert the rule that
displaced it rather than deleted:
- starting order 0/2/42 and tail 'ZERO'/'Zeitgeist' become "offers only
post-quantum chains", plus an explicit test that non-PQ chains remain
decodable, since that distinction is the whole design
- the ledger slip44 check becomes its inverse: nothing offered should claim
hardware support, because no PQ chain has a Ledger app — the devices speak
ed25519 over the Substrate app. knownLedger is left intact rather than
emptied so a rebase has nothing to reconcile; it simply matches nothing
- the Polkadot field fixture becomes the Quantus one
Refs quantus/common#1
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
This commit is contained in:
@@ -6,16 +6,34 @@
|
||||
import type { SubstrateNetwork } from './types.js';
|
||||
|
||||
import { knownGenesis, knownIcon, knownLedger, knownTestnet } from './defaults/index.js';
|
||||
import { allNetworks, availableNetworks, selectableNetworks } from './index.js';
|
||||
import { allNetworks, availableNetworks, isQuantumSafe, selectableNetworks } from './index.js';
|
||||
|
||||
describe('availableNetworks', (): void => {
|
||||
it('has the correct starting order', (): void => {
|
||||
expect(availableNetworks.slice(0, 3).map(({ prefix }) => prefix)).toEqual([0, 2, 42]);
|
||||
// Upstream asserted the head of the list was prefixes 0, 2, 42 — Polkadot,
|
||||
// Kusama, generic Substrate — and the tail was 'ZERO'. None of those are
|
||||
// offered here: this fork only offers chains whose accounts survive a
|
||||
// cryptographically relevant quantum computer. What replaces those assertions
|
||||
// is the rule that produced the change.
|
||||
it('offers only post-quantum chains', (): void => {
|
||||
expect(
|
||||
availableNetworks
|
||||
.filter((n) => !isQuantumSafe(n.standardAccount))
|
||||
.map((n) => `${n.network} (${n.standardAccount ?? 'null'})`)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('has a sorted list (first external, last external)', (): void => {
|
||||
expect(availableNetworks[3].displayName).toEqual('3DP network');
|
||||
expect(availableNetworks[availableNetworks.length - 1].displayName).toEqual('ZERO');
|
||||
it('still knows about non-post-quantum chains, for decoding', (): void => {
|
||||
// Gating what we *offer* must not gate what we can *read*. A pasted Polkadot
|
||||
// address has to render as a Polkadot address so it can be recognised and
|
||||
// refused; failing to decode it would read as a typo rather than a warning.
|
||||
expect(allNetworks.some((n) => n.network === 'polkadot')).toEqual(true);
|
||||
expect(availableNetworks.some((n) => n.network === 'polkadot')).toEqual(false);
|
||||
});
|
||||
|
||||
it('has a sorted list', (): void => {
|
||||
const names = availableNetworks.map(({ displayName }) => displayName);
|
||||
|
||||
expect(names).toEqual([...names].sort((a, b) => a.localeCompare(b)));
|
||||
});
|
||||
|
||||
it('has no ignored networks', (): void => {
|
||||
@@ -37,16 +55,22 @@ describe('availableNetworks', (): void => {
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('has allNetworks ledger details', (): void => {
|
||||
// Upstream checked that every Ledger-capable network carried a matching
|
||||
// slip44. No post-quantum chain has a Ledger app — the devices speak ed25519
|
||||
// over the Substrate app — so the meaningful assertion is the inverse: nothing
|
||||
// we offer should claim hardware support it cannot have.
|
||||
it('claims no ledger support on offered chains', (): void => {
|
||||
expect(
|
||||
availableNetworks
|
||||
.filter((a) => a.hasLedgerSupport)
|
||||
.map((a) => a.network)
|
||||
).toEqual([]);
|
||||
|
||||
// knownLedger is left intact rather than emptied, so a rebase does not have
|
||||
// to reconcile a deletion; it simply no longer matches anything offered.
|
||||
expect(
|
||||
Object.entries(knownLedger).filter(([network, slip44]) =>
|
||||
availableNetworks.some((a) =>
|
||||
a.network === network && (
|
||||
a.slip44 !== slip44 ||
|
||||
!a.hasLedgerSupport ||
|
||||
!a.genesisHash.length
|
||||
)
|
||||
)
|
||||
allNetworks.some((a) => a.network === network && a.slip44 !== slip44)
|
||||
)
|
||||
).toEqual([]);
|
||||
});
|
||||
@@ -84,37 +108,28 @@ describe('availableNetworks', (): void => {
|
||||
});
|
||||
|
||||
it('has all the correct fields', (): void => {
|
||||
// Was Polkadot, which this fork does not offer. Quantus mainnet is now the
|
||||
// first entry, and pinning its shape is more useful anyway — every field
|
||||
// here is one a wallet gets wrong silently.
|
||||
expect(availableNetworks[0]).toEqual({
|
||||
decimals: [10],
|
||||
displayName: 'Polkadot Relay Chain',
|
||||
decimals: [12],
|
||||
displayName: 'Quantus',
|
||||
genesisHash: [
|
||||
'0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3'
|
||||
'0xfb5487c0be6ae4ade2d41d16e50465129861636c2b8d61fa94d7a19631626fba'
|
||||
],
|
||||
hasLedgerSupport: true,
|
||||
icon: 'polkadot',
|
||||
hasLedgerSupport: false,
|
||||
icon: 'substrate',
|
||||
isIgnored: false,
|
||||
isTestnet: false,
|
||||
network: 'polkadot',
|
||||
prefix: 0,
|
||||
slip44: 354,
|
||||
standardAccount: '*25519',
|
||||
symbols: ['DOT'],
|
||||
website: 'https://polkadot.network'
|
||||
network: 'quantus',
|
||||
prefix: 189,
|
||||
slip44: undefined,
|
||||
standardAccount: 'ml-dsa',
|
||||
symbols: ['QTC'],
|
||||
website: 'https://quantus.com'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('allNetworks', (): void => {
|
||||
// Upstream asserted one network per SS58 prefix. That holds for a registry of
|
||||
// distinct chains and stops holding the moment a chain has a testnet: Quantus
|
||||
// mainnet and Heisenberg are both prefix 189, because they are the same chain
|
||||
// at two deployments and an address is valid on either. Pretending otherwise
|
||||
// would mean giving one of them a prefix it does not use, and printing
|
||||
// addresses no Quantus tool would recognise.
|
||||
//
|
||||
// Consumers key on genesisHash, not prefix — the extension's chains.ts builds
|
||||
// its list from `genesisHash[0]` — so sharing a prefix costs nothing there.
|
||||
// What is still worth pinning is that a duplicate is deliberate.
|
||||
it('has no ss58 duplicates, except deployments of the same chain', (): void => {
|
||||
const dupes: SubstrateNetwork[] = [];
|
||||
const uniques: SubstrateNetwork[] = [];
|
||||
@@ -134,12 +149,34 @@ describe('allNetworks', (): void => {
|
||||
});
|
||||
|
||||
describe('selectableNetworks', (): void => {
|
||||
it('has the correct starting order', (): void => {
|
||||
expect(selectableNetworks.slice(0, 3).map(({ prefix }) => prefix)).toEqual([0, 2, 42]);
|
||||
// Same rewrite as the availableNetworks block above: upstream pinned prefixes
|
||||
// 0/2/42 at the head and 'Zeitgeist' at the tail, and this fork offers neither.
|
||||
it('offers only post-quantum chains', (): void => {
|
||||
expect(
|
||||
selectableNetworks
|
||||
.filter((n) => !isQuantumSafe(n.standardAccount))
|
||||
.map((n) => n.network)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('has a sorted list (first external, last external)', (): void => {
|
||||
expect(selectableNetworks[3].displayName).toEqual('3DP network');
|
||||
expect(selectableNetworks[selectableNetworks.length - 1].displayName).toEqual('Zeitgeist');
|
||||
it('is a subset of availableNetworks that carries a genesisHash', (): void => {
|
||||
expect(
|
||||
selectableNetworks.filter((n) =>
|
||||
!n.genesisHash.length ||
|
||||
!availableNetworks.some((a) => a.network === n.network)
|
||||
)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('has a sorted list', (): void => {
|
||||
const names = selectableNetworks.map(({ displayName }) => displayName);
|
||||
|
||||
expect(names).toEqual([...names].sort((a, b) => a.localeCompare(b)));
|
||||
});
|
||||
|
||||
// The chain the extension is for. If this ever stops being selectable, the
|
||||
// built-in chain list is empty and nothing in the UI says why.
|
||||
it('offers Quantus mainnet', (): void => {
|
||||
expect(selectableNetworks.some((n) => n.network === 'quantus')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
|
||||
export * from './interfaces.js';
|
||||
export { packageInfo } from './packageInfo.js';
|
||||
export { isQuantumSafe } from './quantumSafe.js';
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { KnownSubstrate, Network, SubstrateNetwork } from './types.js';
|
||||
import knownSubstrate from '@substrate/ss58-registry';
|
||||
|
||||
import { exposedTestnet, knownGenesis, knownIcon, knownLedger, knownQuantus, knownTestnet } from './defaults/index.js';
|
||||
import { isQuantumSafe } from './quantumSafe.js';
|
||||
|
||||
// These are known prefixes that are not sorted
|
||||
const UNSORTED = [0, 2, 42];
|
||||
@@ -52,7 +53,7 @@ function filterSelectable ({ genesisHash, prefix }: Network): boolean {
|
||||
}
|
||||
|
||||
function filterAvailable (n: SubstrateNetwork): n is Network {
|
||||
return !n.isIgnored && !!n.network;
|
||||
return !n.isIgnored && !!n.network && isQuantumSafe(n.standardAccount);
|
||||
}
|
||||
|
||||
function sortNetworks (a: Network, b: Network): number {
|
||||
@@ -68,7 +69,13 @@ function sortNetworks (a: Network, b: Network): number {
|
||||
: 1;
|
||||
}
|
||||
|
||||
// This is all the Substrate networks with our additional information
|
||||
// This is all the Substrate networks with our additional information.
|
||||
//
|
||||
// Deliberately NOT filtered to post-quantum chains: `availableNetworks` is, and
|
||||
// that is what tooling offers. This list stays complete so a pasted Polkadot or
|
||||
// Kusama address still decodes and renders as what it is — recognisable, and
|
||||
// therefore refusable. A wallet that cannot decode an address it will not accept
|
||||
// can only tell the user "invalid", which reads as a typo rather than a warning.
|
||||
//
|
||||
// Quantus is appended rather than merged: `@substrate/ss58-registry` does not
|
||||
// carry it and prefix 189 is unclaimed there, so there is nothing to override.
|
||||
@@ -78,7 +85,8 @@ export const allNetworks = knownSubstrate
|
||||
.concat(knownQuantus.map(toExpanded));
|
||||
|
||||
// The list of available/claimed prefixes
|
||||
// - no testnets
|
||||
// - post-quantum account schemes only (see quantumSafe.ts)
|
||||
// - testnets only where explicitly exposed (see exposedTestnet)
|
||||
// - we only include those where we have a standardAccount
|
||||
// - sort by name, however we keep 0, 2, 42 first in the list
|
||||
export const availableNetworks = allNetworks.filter(filterAvailable).sort(sortNetworks);
|
||||
|
||||
32
packages/networks/src/quantumSafe.ts
Normal file
32
packages/networks/src/quantumSafe.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright 2017-2026 @polkadot/networks authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { KnownSubstrate } from './types.js';
|
||||
|
||||
/**
|
||||
* The `standardAccount` values that survive a cryptographically relevant quantum
|
||||
* computer.
|
||||
*
|
||||
* Everything else in the registry — `*25519`, `Ed25519`, `Sr25519`, `secp256k1` —
|
||||
* is a discrete-log scheme, and Shor's algorithm breaks all of them at once. A
|
||||
* chain using one of those has no quantum-safe account type to offer, whatever
|
||||
* else is true of it.
|
||||
*/
|
||||
const QUANTUM_SAFE_ACCOUNTS: KnownSubstrate['standardAccount'][] = ['ml-dsa'];
|
||||
|
||||
/**
|
||||
* Whether a network's accounts are post-quantum.
|
||||
*
|
||||
* This gates what the tooling built on this fork will *offer*. It deliberately
|
||||
* does not gate address encoding or decoding: pasting a Polkadot address into a
|
||||
* Quantus wallet should render as a Polkadot address so it can be recognised and
|
||||
* refused, not fail to decode and look like a typo.
|
||||
*
|
||||
* A `null` standardAccount — which the registry uses for chains that never said —
|
||||
* is treated as unsafe. Unknown is not the same as safe, and the cost of being
|
||||
* wrong in that direction is a user holding funds under a key Shor's algorithm
|
||||
* recovers.
|
||||
*/
|
||||
export function isQuantumSafe (standardAccount: KnownSubstrate['standardAccount']): boolean {
|
||||
return QUANTUM_SAFE_ACCOUNTS.includes(standardAccount);
|
||||
}
|
||||
Reference in New Issue
Block a user