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:
rob thijssen
2026-09-10 18:28:13 +03:00
parent 6ed8b1ea6d
commit c616a0d949
4 changed files with 123 additions and 45 deletions

View File

@@ -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);
});
});

View File

@@ -7,3 +7,4 @@
export * from './interfaces.js';
export { packageInfo } from './packageInfo.js';
export { isQuantumSafe } from './quantumSafe.js';

View File

@@ -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);

View 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);
}