diff --git a/package.json b/package.json index 74777d68..84852cde 100644 --- a/package.json +++ b/package.json @@ -52,13 +52,13 @@ }, "resolutions": { "@polkadot/api": "^16.5.6", - "@polkadot/keyring": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.1/keyring-14.0.3-quantus.1.tgz", - "@polkadot/networks": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.1/networks-14.0.3-quantus.1.tgz", + "@polkadot/keyring": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.2/keyring-14.0.3-quantus.2.tgz", + "@polkadot/networks": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.2/networks-14.0.3-quantus.2.tgz", "@polkadot/rpc-provider": "^16.5.6", "@polkadot/types": "^16.5.6", - "@polkadot/ui-keyring": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.1/ui-keyring-3.16.7-quantus.1.tgz", + "@polkadot/ui-keyring": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.2/ui-keyring-3.16.7-quantus.2.tgz", "@polkadot/util": "^14.0.3", - "@polkadot/util-crypto": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.1/util-crypto-14.0.3-quantus.1.tgz", + "@polkadot/util-crypto": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.2/util-crypto-14.0.3-quantus.2.tgz", "@polkadot/x-fetch": "^14.0.3", "safe-buffer": "^5.2.1", "typescript": "^5.5.4" diff --git a/packages/extension-base/package.json b/packages/extension-base/package.json index db019c44..9cc61d9e 100644 --- a/packages/extension-base/package.json +++ b/packages/extension-base/package.json @@ -34,6 +34,7 @@ "@polkadot/ui-settings": "^3.16.7", "@polkadot/util": "^14.0.3", "@polkadot/util-crypto": "^14.0.3", + "@quantus/codec": "^0.1.0", "@quantus/crypto": "^0.1.1", "eventemitter3": "^5.0.1", "rxjs": "^7.8.1", diff --git a/packages/extension-ui/src/util/chains.ts b/packages/extension-ui/src/util/chains.ts index fd1556e1..19c86ee1 100644 --- a/packages/extension-ui/src/util/chains.ts +++ b/packages/extension-ui/src/util/chains.ts @@ -3,10 +3,24 @@ import type { MetadataDefBase } from '@polkadot/extension-inject/types'; -import { selectableNetworks } from '@polkadot/networks'; +import { isQuantumSafe, selectableNetworks } from '@polkadot/networks'; +// The chains this extension offers. +// +// Filtered here rather than in @polkadot/networks. An earlier attempt gated +// `selectableNetworks` itself, which broke @polkadot/api at import: +// @polkadot/types-known looks up every chain it holds upgrade history for and +// throws when one is missing. Those lists are a library's view of what Substrate +// networks exist; which of them a *wallet* offers is the wallet's decision, and +// this is the wallet. +// +// The filter is post-quantum accounts. Everything else in the registry uses a +// discrete-log scheme — *25519, Sr25519, Ed25519, secp256k1 — all of which Shor's +// algorithm breaks at once, so such a chain 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. const hashes: MetadataDefBase[] = selectableNetworks - .filter(({ genesisHash }) => !!genesisHash.length) + .filter(({ genesisHash, standardAccount }) => !!genesisHash.length && isQuantumSafe(standardAccount)) .map((network) => ({ chain: network.displayName, genesisHash: network.genesisHash[0], diff --git a/scripts/tier1/submit.mjs b/scripts/tier1/submit.mjs new file mode 100644 index 00000000..455a9007 --- /dev/null +++ b/scripts/tier1/submit.mjs @@ -0,0 +1,173 @@ +// Copyright 2019-2026 @polkadot/extension authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// quantus/extension#7 tier 1: sign a real extrinsic with the forked keyring and +// submit it to a real node. No browser, no extension, no dapp. +// +// Nothing here uses @polkadot/api. Its codec cannot read or write this chain — +// quantus/api#1 has the evidence — so every byte on the wire is produced by +// @quantus/codec against metadata the node generated by running +// `Metadata_metadata` against the runtime WASM. `WsProvider` appears only as a +// JSON-RPC transport; no type in this file is decoded by it. + +import { Runtime } from '@quantus/codec'; +import { contextForSpec } from '@quantus/crypto'; + +import { Keyring } from '@polkadot/keyring'; +import { WsProvider } from '@polkadot/rpc-provider'; +import { u8aToHex } from '@polkadot/util'; +import { blake2AsU8a, cryptoWaitReady, decodeAddress } from '@polkadot/util-crypto'; + +const ENDPOINT = process.env.QUANTUS_WS || 'wss://a1-heisenberg.quantus.cat'; +// crystal_alice: seed = 32 zero bytes, ML-DSA-87. Published in the chain's own +// source, funded at dev genesis, and assumed compromised by everyone. +const SEED = `0x${'00'.repeat(32)}`; +const TYPE = 'dilithium87'; +// Substrate's own rule, from `unchecked_extrinsic.rs`: a signing payload longer +// than 256 bytes is signed as its BLAKE2b-256 hash, otherwise as-is. +const HASH_ABOVE = 256; + +await cryptoWaitReady(); + +const keyring = new Keyring({ ss58Format: 189, type: TYPE }); +const pair = keyring.createFromUri(SEED, { name: 'crystal_alice' }, TYPE); + +console.log(`signer ${pair.address}`); + +const provider = new WsProvider(ENDPOINT); + +await provider.isReady; + +const [version, genesisHash, metadataHex] = await Promise.all([ + provider.send('state_getRuntimeVersion', []), + provider.send('chain_getBlockHash', ['0x0']), + provider.send('state_getMetadata', []) +]); + +const specVersion = version.specVersion; +const transactionVersion = version.transactionVersion; +const runtime = Runtime.fromMetadata(Uint8Array.from(Buffer.from(metadataHex.slice(2), 'hex'))); + +console.log(`chain ${version.specName} spec ${specVersion} tx ${transactionVersion}`); +console.log(`genesis ${genesisHash}`); +console.log(`extrinsic v${runtime.extrinsicVersion}`); + +const dest = process.env.QUANTUS_DEST || 'qzkYEQv8tQsmniZYdame3Cku18RL5g9bGK9Pdydq5TMPdpE3y'; // crystal_bob +const amount = 1_000_000_000n; // 0.001 +const destId = u8aToHex(decodeAddress(dest)); +const nonce = parseInt(await provider.send('system_accountNextIndex', [pair.address]), 10); + +console.log(`recipient ${dest}`); +console.log(`nonce ${nonce}`); + +const call = runtime.encodeCall('Balances', 'transfer_keep_alive', { + dest: { Id: destId }, + value: amount.toString() +}); + +console.log(`call ${u8aToHex(call)}`); + +// An immortal era, so `CheckMortality`'s implicit is the genesis hash and there +// is no birth block to agree with the node about. Mortality is a hardening step, +// not part of proving the fork can spend. +const values = runtime.standardExtensions({ + blockHash: genesisHash, + genesisHash, + nonce, + specVersion, + transactionVersion +}); + +// Every extension the runtime declares as non-empty and `standardExtensions` +// does not cover. Reported rather than skipped: skipping is the polkadot-js +// failure mode this whole package exists to avoid. +const unmet = runtime + .signedExtensions() + .filter((e) => (e.needsExtra || e.needsAdditional) && !values[e.identifier]); + +if (unmet.length) { + throw new Error(`runtime declares extensions this harness cannot fill: ${unmet.map((e) => e.identifier).join(', ')}`); +} + +const extra = runtime.encodeExtra(values); +const payload = runtime.signerPayload(call, values); +const toSign = payload.length > HASH_ABOVE ? blake2AsU8a(payload) : payload; +const context = contextForSpec(specVersion); + +console.log(`extra ${u8aToHex(extra)}`); +console.log(`payload ${payload.length} bytes -> signing ${toSign.length}`); +console.log(`context ${context.length ? new TextDecoder().decode(context) : '(empty)'}`); + +const signature = pair.sign(toSign, { context, withType: true }); + +console.log(`signature ${signature.length} bytes, variant 0x${signature[0].toString(16).padStart(2, '0')}`); + +const extrinsic = runtime.encodeExtrinsic({ Id: u8aToHex(pair.addressRaw) }, signature, extra, call); + +console.log(`extrinsic ${extrinsic.length} bytes`); + +// Decoding what we are about to send, with the same runtime that built it. A +// round trip that fails here is a bug in this package; one that succeeds and is +// still rejected is a disagreement with the chain, and the two are worth telling +// apart before the node is involved. +const readBack = runtime.decodeExtrinsic(extrinsic); + +console.log(`round trip v${readBack.version} signed=${readBack.signed} ${JSON.stringify(readBack.call)}`); + +const hash = await provider.send('author_submitExtrinsic', [u8aToHex(extrinsic)]); + +// Acceptance into the pool already means the signature verified: an extrinsic +// the runtime cannot check is rejected right here, as `1010: Invalid +// Transaction: Transaction has a bad signature`. +console.log(`accepted ${hash}`); + +// Inclusion, though, is the claim worth making, so wait for the chain to put it +// in a block and then read that block back with the same runtime — which also +// exercises the thing @polkadot/api cannot do at all: every Quantus block opens +// with a timestamp inherent whose preamble byte is `0x05`, bare and version 5, +// while this signed extrinsic is `0x84`, signed and version 4. +const DEADLINE = Date.now() + 10 * 60 * 1000; + +let height = parseInt((await provider.send('chain_getHeader', [])).number, 16); +let found = null; + +while (!found && Date.now() < DEADLINE) { + const head = parseInt((await provider.send('chain_getHeader', [])).number, 16); + + while (height <= head && !found) { + const blockHash = await provider.send('chain_getBlockHash', [`0x${height.toString(16)}`]); + const block = await provider.send('chain_getBlock', [blockHash]); + + for (const [index, raw] of block.block.extrinsics.entries()) { + const decoded = runtime.decodeExtrinsic(Uint8Array.from(Buffer.from(raw.slice(2), 'hex'))); + + if (raw === u8aToHex(extrinsic)) { + found = { blockHash, decoded, height, index }; + } + } + + height++; + } + + if (!found) { + await new Promise((resolve) => setTimeout(resolve, 10_000)); + } +} + +if (!found) { + throw new Error('accepted into the pool but not included within ten minutes'); +} + +console.log(`included block ${found.height} index ${found.index} (${found.blockHash})`); +console.log(`decoded ${JSON.stringify(found.decoded.call)}`); +console.log(`signature ${JSON.stringify(found.decoded.signature).slice(0, 60)}…`); + +const after = parseInt(await provider.send('system_accountNextIndex', [pair.address]), 10); + +console.log(`nonce ${nonce} -> ${after}`); + +if (after !== nonce + 1) { + throw new Error(`nonce did not advance: expected ${nonce + 1}, got ${after}`); +} + +await provider.disconnect(); diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 64ace445..ed014e31 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -10,7 +10,8 @@ ".prettierrc.cjs", "eslint.config.js", "i18next-scanner.config.cjs", - "rollup.config.js" + "rollup.config.js", + "scripts/**/*.mjs" ], "exclude": [ "**/node_modules/**/*" diff --git a/yarn.lock b/yarn.lock index 257dc7d8..0efbee8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -982,6 +982,7 @@ __metadata: "@polkadot/ui-settings": "npm:^3.16.7" "@polkadot/util": "npm:^14.0.3" "@polkadot/util-crypto": "npm:^14.0.3" + "@quantus/codec": "npm:^0.1.0" "@quantus/crypto": "npm:^0.1.1" eventemitter3: "npm:^5.0.1" rxjs: "npm:^7.8.1" @@ -1151,29 +1152,29 @@ __metadata: languageName: unknown linkType: soft -"@polkadot/keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.1/keyring-14.0.3-quantus.1.tgz": - version: 14.0.3-quantus.1 - resolution: "@polkadot/keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.1/keyring-14.0.3-quantus.1.tgz" +"@polkadot/keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.2/keyring-14.0.3-quantus.2.tgz": + version: 14.0.3-quantus.2 + resolution: "@polkadot/keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3-quantus.2/keyring-14.0.3-quantus.2.tgz" dependencies: "@polkadot/util": "npm:14.0.3" - "@polkadot/util-crypto": "npm:14.0.3-quantus.1" + "@polkadot/util-crypto": "npm:14.0.3-quantus.2" "@quantus/crypto": "npm:^0.1.0" tslib: "npm:^2.8.0" peerDependencies: "@polkadot/util": 14.0.3 "@polkadot/util-crypto": 14.0.3 - checksum: 10/513818828aebd066ec618e114441bbd2aa9cb83527fce4a78867d3b9f982e4bb17527090642d704a2a8b1fd047276b30ce8c22efcdc10c81d3e318f4b42c7059 + checksum: 10/e3885d6a75b4c006887d24dadbfc25fff562baae84d42fd2125878af60daecbf789deed12c975edfef0884a3caff4dd469dcdcd8b63eacbd1a5fbf8bbfe48815 languageName: node linkType: hard -"@polkadot/networks@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.1/networks-14.0.3-quantus.1.tgz": - version: 14.0.3-quantus.1 - resolution: "@polkadot/networks@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.1/networks-14.0.3-quantus.1.tgz" +"@polkadot/networks@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.2/networks-14.0.3-quantus.2.tgz": + version: 14.0.3-quantus.2 + resolution: "@polkadot/networks@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fnetworks/-/14.0.3-quantus.2/networks-14.0.3-quantus.2.tgz" dependencies: "@polkadot/util": "npm:14.0.3" "@substrate/ss58-registry": "npm:^1.51.0" tslib: "npm:^2.8.0" - checksum: 10/5d93c35908eec5b41f29ffd4a5a79234c7329264e61d6b4346398e5b495b80109c5ce35a2f2bb95e9f6fc4eb22b9da047b9ca4191ec73a668c43da73861d7122 + checksum: 10/3d2f0a08cde4be418d073f63587bed451a6ce4c91a8872a5e0c4191ff3e62d57a7942c1a1b6c183ed9c22742f5b95b7496fe4e32e3565baf2093d879ac51a55e languageName: node linkType: hard @@ -1362,14 +1363,14 @@ __metadata: languageName: node linkType: hard -"@polkadot/ui-keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.1/ui-keyring-3.16.7-quantus.1.tgz": - version: 3.16.7-quantus.1 - resolution: "@polkadot/ui-keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.1/ui-keyring-3.16.7-quantus.1.tgz" +"@polkadot/ui-keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.2/ui-keyring-3.16.7-quantus.2.tgz": + version: 3.16.7-quantus.2 + resolution: "@polkadot/ui-keyring@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fui-keyring/-/3.16.7-quantus.2/ui-keyring-3.16.7-quantus.2.tgz" dependencies: - "@polkadot/keyring": "npm:14.0.3-quantus.1" + "@polkadot/keyring": "npm:14.0.3-quantus.2" "@polkadot/ui-settings": "npm:3.16.7" "@polkadot/util": "npm:^14.0.3" - "@polkadot/util-crypto": "npm:14.0.3-quantus.1" + "@polkadot/util-crypto": "npm:14.0.3-quantus.2" mkdirp: "npm:^3.0.1" rxjs: "npm:^7.8.1" store: "npm:^2.0.12" @@ -1378,7 +1379,7 @@ __metadata: "@polkadot/keyring": "*" "@polkadot/ui-settings": "*" "@polkadot/util": "*" - checksum: 10/8f475e2fbbf795a82513e5195ec924f4c3077cb9cf6f09205db6507a814a527b0b99ba077e44ba506e3c8120366ae1f33371653858c7ed1f291bc1fd2917cb9f + checksum: 10/bd62eb7fb2ad152c8f6844938af34fa75fa6b359d5414cbaca0d77c80b808f8a4e51f62b223cbbb1cbe8feefab43b42db86dd978af92dcce3bab5236cdb3a885 languageName: node linkType: hard @@ -1411,13 +1412,13 @@ __metadata: languageName: node linkType: hard -"@polkadot/util-crypto@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.1/util-crypto-14.0.3-quantus.1.tgz": - version: 14.0.3-quantus.1 - resolution: "@polkadot/util-crypto@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.1/util-crypto-14.0.3-quantus.1.tgz" +"@polkadot/util-crypto@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.2/util-crypto-14.0.3-quantus.2.tgz": + version: 14.0.3-quantus.2 + resolution: "@polkadot/util-crypto@https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Futil-crypto/-/14.0.3-quantus.2/util-crypto-14.0.3-quantus.2.tgz" dependencies: "@noble/curves": "npm:^1.3.0" "@noble/hashes": "npm:^1.3.3" - "@polkadot/networks": "npm:14.0.3-quantus.1" + "@polkadot/networks": "npm:14.0.3-quantus.2" "@polkadot/util": "npm:14.0.3" "@polkadot/wasm-crypto": "npm:^7.5.3" "@polkadot/wasm-util": "npm:^7.5.3" @@ -1429,7 +1430,7 @@ __metadata: tslib: "npm:^2.8.0" peerDependencies: "@polkadot/util": 14.0.3 - checksum: 10/ae364a2c47ea6033222f34766f83f3c3740966f83f380978b716013d4ab3862f03355034afe6ef84e7f28c90bda1eb347114c342acd5f4844566580d13d34341 + checksum: 10/78f04147ddfe8ae0bca4cba2f235715615a140de9ab95f10d6e4bbabee4a2865e6f8a84f70668722ab688094a8519eeb019ed5122620b355a3ea40b0d8cfdeb8 languageName: node linkType: hard @@ -1602,6 +1603,16 @@ __metadata: languageName: node linkType: hard +"@quantus/codec@npm:^0.1.0": + version: 0.1.0 + resolution: "@quantus/codec@npm:0.1.0::__archiveUrl=https%3A%2F%2Fgit.lair.cafe%2Fapi%2Fpackages%2Fquantus%2Fnpm%2F%2540quantus%252Fcodec%2F-%2F0.1.0%2Fcodec-0.1.0.tgz" + dependencies: + fflate: "npm:^0.8.2" + tslib: "npm:^2.7.0" + checksum: 10/90d40e0909383f5f0fea3f662772702d78f1b6fbecb9e8cac901e8a20b5adafff97782a4c13faa7e2278abd97c3c12275695d2a21d19790a359df01267febe84 + languageName: node + linkType: hard + "@quantus/crypto@npm:^0.1.0, @quantus/crypto@npm:^0.1.1": version: 0.1.1 resolution: "@quantus/crypto@npm:0.1.1::__archiveUrl=https%3A%2F%2Fgit.lair.cafe%2Fapi%2Fpackages%2Fquantus%2Fnpm%2F%2540quantus%252Fcrypto%2F-%2F0.1.1%2Fcrypto-0.1.1.tgz"