Compare commits

..
13 Commits
Author SHA1 Message Date
grenadeandClaude Opus 5 0480bfcbc5 build: packageInfo at 14.0.3-quantus.3, as published
Lock Threads / lock (push) Has been cancelled
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-16 22:13:53 +03:00
grenadeandClaude Opus 5 a5157ef592 build: version the forks 14.0.3-quantus.3, on @quantus/crypto 0.3.0
The post-quantum-only change is a breaking one for every consumer. The keyring
and util-crypto surfaces both shrink, so it gets its own version rather than a
republish of quantus.2. @quantus/crypto moves to ^0.3.0, the version the
extension already resolves (wormhole addresses and nullifiers), so the two
share one copy of the WASM.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-16 21:10:31 +03:00
grenadeandClaude Opus 5 e1d88c6f63 feat!: post-quantum only: remove ed25519, sr25519, ecdsa and ethereum keys
KeypairType is now 'dilithium65' | 'dilithium87'. Upstream's four types are
gone with their primitives, not merely unoffered. Each falls to Shor's
algorithm, and a keyring that can hold such a key invites someone to keep funds
under it, inside a tool whose premise is that this is unsafe.

This was deferred until post-quantum signing was proven end to end (real
transfers on Heisenberg, a real mainnet wallet in the extension), so that
tearing out half of util-crypto could not muddy the diagnosis of a first
rejected extrinsic. That has happened.

util-crypto, removed:
- ed25519/, sr25519/, secp256k1/, and signature/ (signatureVerify, which only
  knew those three; dilithiumVerify is the verifier);
- hd/ethereum and hd/ledger;
- key/fromPath and keyHdkd{Ecdsa,Ed25519,Sr25519}, the junction derivation;
- address/derive (sr25519 soft derivation);
- mnemonic/toMiniSecret, Substrate's classical seeding.

util-crypto, kept because none of it holds a key:
- ethereumEncode, isEthereumAddress and isEthereumChecksum. @polkadot/types and
  the identicon renderer format 20-byte addresses with them, and a wallet has
  to be able to show an Ethereum address to recognise and refuse one.
  ethereumEncode now refuses a secp256k1 public key, saying why.
- evm ↔ substrate address conversion (its blake2/keccak hasher moved out of
  secp256k1/ into address/), derived and multi addresses, BIP39, and suri
  parsing.

keyring:
- Only ML-DSA arms remain. The default type is dilithium65.
- Every entry point (constructor, createFromUri, createFromPair, addFromAddress
  and, above all, createFromJson for a backup the user chose) refuses a
  quantum-unsafe type with "<type> keys are not quantum-safe and cannot be held
  here", not "unknown crypto type", which reads like a bug in this software.
- pair.verify handles ML-DSA, a bare signature or signature ‖ publicKey, under
  a context that defaults to the empty raw-bytes one. Derivation and VRF
  refuse.
- The test keyring is the Quantus dev accounts (crystal_alice, dilithium_bob,
  crystal_charlie: ML-DSA-87 from seeds of 0, 1 and 2) in place of sr25519
  Alice…Ferdie and ethereum Alith…Faith.

Fixed along the way: addFromAddress passed the decoded address as a public key.
That is the same bytes on Substrate. Here it is the account id, a hash of the
key, so every watch-only account reported the hash of its own address. It now
carries the address as an account id.

Specs:
- Upstream's per-scheme keyring specs (index, pair, encode, decode, toJson,
  vrf, suri, testingPairs) are replaced by keyring.spec.ts. It covers refusals
  at every entry point (including a polkadot{.js} ed25519 JSON backup), the
  dev-account addresses, watch-only addresses, JSON round trips for both
  schemes, verify in both signature forms and failing under the wrong context
  or signer, and the absence of derivation and VRF.
- The "classical paths unchanged" pins in the ML-DSA specs are gone with the
  paths.
- The BIP39 vectors toEntropy.spec used moved from sr25519/ to
  mnemonic/bip39Vectors.spec.ts.

yarn test: 2747 passing, 0 failing. yarn lint: clean.

hw-ledger and hw-ledger-transports remain. They are device transports holding
no primitives, and nothing consumes them.

Closes #6. Refs #1: signatureVerify is gone rather than made to take a
context; dilithiumVerify is the replacement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-16 21:05:52 +03:00
grenadeandClaude Opus 5 7f23d0a609 fix: gate quantum-safe chains in the wallet, not in the network list
Filtering `filterAvailable` by `isQuantumSafe` removed Polkadot and Kusama from
`selectableNetworks`, and `@polkadot/types-known` throws for a chain it holds
upgrade history for but cannot find:

    Error: Unable to find info for chain kusama
      at mapRaw (@polkadot/types-known/upgrades/index.js:13:15)

which made `@polkadot/api` fail at *import* — nothing to do with Quantus, and
not diagnosable from the message.

The mistake was gating a library primitive. These lists say what Substrate
knows about; what a wallet offers is the wallet's decision, and `isQuantumSafe`
stays exported so the extension can make it in its own chain list. Keeping them
complete also means a pasted Polkadot 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 say "invalid", which reads as a typo rather
than a warning.

`index.spec.ts` is restored from upstream with only the testnet-exposure and
shared-prefix deltas re-applied, since its network-ordering assertions had
started passing vacuously once Polkadot and Kusama left the list.

Also corrects Heisenberg's token symbol to HEI; `system_properties` on
wss://a1-heisenberg.quantus.cat says HEI, not QTC.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 14:14:58 +03:00
rob thijssenandClaude Opus 5 49f3442e5c build: version the forked packages 14.0.3-quantus.1, not upstream's 14.0.3
Publishing under upstream's exact version was not merely confusing in a lockfile
— it was unsafe.

@polkadot/util-crypto pins @polkadot/networks at an exact version, and
@polkadot/keyring pins util-crypto the same way. With every fork numbered
14.0.3, a consumer that forgot one resolution would have yarn quietly satisfy
that pin from npm: the *unforked* upstream package, installing cleanly, with no
Quantus network entry or no isDilithium, and nothing to indicate anything was
wrong until something failed much later for an unrelated-looking reason.

Renaming the forks makes that a resolution failure instead. Verified by deleting
the @polkadot/networks resolution in quantus/ui and running an install:

  YN0082: @polkadot/networks@npm:14.0.3-quantus.1: No candidates found

Unforked dependencies keep upstream's numbers — @polkadot/util, x-bigint,
x-randomvalues, wasm-* — because they come from npm and should.

Published with `--tag quantus`, which npm requires for a prerelease version and
which is the honest dist-tag anyway: these are not upstream's `latest`. The tag
is informational, since consumers pin by tarball URL. The old 14.0.3 artifacts
have been deleted from the registry so nothing can resolve to them.

Refs quantus/extension#2

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 11:27:59 +03:00
rob thijssenandClaude Opus 5 b39241c840 build: take @quantus/crypto from the Gitea registry, not a symlink
Replaces the `link:../wasm/packages/quantus-crypto/build` resolution with an
npmScopes entry routing @quantus to https://git.lair.cafe/api/packages/quantus/npm/.

Routing the whole scope is safe here because we own every name in it. The forked
@polkadot packages cannot be handled the same way — most of that scope is
unforked and lives on npm — which is why downstream repos pin those to tarball
URLs instead.

The symlink approach had run out of road. node resolves a portal or link to its
realpath, so a consumer's transitive dependencies then resolve inside the *linked
package's* tree, where workspace entries point at source directories with no
exports map. That cost one workaround here and four in quantus/ui.

Reads from the registry are anonymous; publishing uses
`pass gitea/package-publisher-token`. Note that a version cannot be overwritten
in place — npm refuses — so republishing is delete-then-publish, which that token
can also do.

Refs quantus/extension#2

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 11:08:27 +03:00
rob thijssenandClaude Opus 5 74e1e0cc6a feat(util-crypto): export quantus-cli wallet files
The counterpart to the importer, and the thing that stops it being a one-way
door. Without it an extension backup is readable only by polkadot-js-format
tooling, so a user who brought a wallet in could not take it back out.

The assumption that blocked this — that we cannot write the CLI format because we
do not store a mnemonic — turns out to be false. The format has a null-mnemonic
shape and the CLI writes one itself for every raw-seed wallet; crystal_alice,
which `quantus developer create-test-wallets` produces, has exactly that
alongside `derivation_path: "m/"`.

Verified against quantus-cli 2.2.2 rather than inferred. Exports of both schemes
were written into ~/.quantus/wallets and the CLI listed them with the correct
address, scheme and human checkphrase, showed them under `wallet view`, and
answered `export --format mnemonic` with "Mnemonic phrase is not available for
this wallet" — which is a correct statement about the file, and only reachable by
deriving the Argon2 key, opening the AES-GCM box and parsing the payload. Those
probes were removed; the committed test round-trips through our own importer,
since CI has no CLI.

What a null-mnemonic export costs the user is exactly one thing, and it is not
spending: the CLI cannot re-export a phrase from it or derive further accounts.
That is the honest consequence of never storing a phrase we showed once, and it
is the same position a raw-seed CLI wallet is already in.

Argon2 parameters match the CLI's rather than being raised. They travel in the
file and the CLI reads them, so in principle anything works — but "in principle"
is load-bearing there, and a wallet a slightly older CLI cannot open is worse
than a conservatively tuned KDF. Salt and nonce are fresh per export.

metadata.exported_by marks the file, so a CLI maintainer reading a bug report can
tell where it came from without inferring from shape.

Refs quantus/common#8

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 10:33:20 +03:00
rob thijssenandClaude Opus 5 e55352aa4e feat(util-crypto): import quantus-cli wallet files
A must-have per the UX decision in quantus/extension#8: the extension has to be
able to take a wallet a user already has. My earlier recommendation against this
was withdrawn — the reasoning was that a file importer would pull the mnemonic
into storage, which is a property of an implementation rather than of the format.
This one derives and discards.

Converts at the edge rather than teaching the keyring a second container.
Decrypt (Argon2id -> AES-256-GCM), take the key material, and hand back a plain
keypair for the caller to re-encrypt as the extension's own PKCS8 under a
password the user chooses. decodePair does not learn about Argon2 and the keyring
keeps exactly one on-disk format.

The CLI stores a mnemonic in every HD wallet. This returns `hadMnemonic` and not
the phrase — the posture is that a storage password unlocks signing and nothing
on disk regenerates the tree, and an import must leave us holding exactly what
importing the same mnemonic by hand would leave us holding. Matching upstream,
which stores key material only.

No new dependencies: @noble/hashes/argon2 was already a util-crypto dependency
and AES-GCM is WebCrypto. Async only because WebCrypto has no synchronous form,
which is a second reason this belongs at the edge — createFromJson is sync and
should stay so.

Three things are refused rather than worked around:

Argon2 parameters are read from the file, not assumed. They are stored because
they are expected to change, and a build that hardcoded m=19456,t=2,p=1 would
reject a correct password on an older or newer wallet — the least useful thing it
could say.

Non-empty kyber_ciphertext/kyber_public_key, and any encryption_version other
than 2, are refused with a message. Those fields are an ML-KEM envelope mode that
is planned or optional; decrypting by the path we know and ignoring them would
fail later and more confusingly, most likely as a bad-password error.

The address sits outside the encrypted blob, so it is the one field an attacker
can edit without the password. cliWalletAddressMatches is exposed separately so
a caller can say "this file's address does not match its key" rather than
"wrong password" — a user can act on the difference.

Fixtures are genuine CLI 2.2.2 output, not blobs this repo encrypted; testing a
decoder against its own encryption proves only self-consistency. They contain no
secret: the mnemonic inside each is the public Substrate dev phrase and the
password is empty, which is the only reason committing a decryptable wallet is
acceptable. The strongest test reaches the same account two ways — through the
CLI container and by deriving from that phrase ourselves.

Refs quantus/common#7

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 09:44:07 +03:00
rob thijssenandClaude Opus 5 f7dc72e32c feat(keyring,util-crypto): BIP44 derivation for ML-DSA, in place of junctions
createFromUri now derives ML-DSA pairs from the mnemonic along a hardened BIP44
path rather than from a seed along a junction chain. Lattice keys have no public
derivability, so there is no soft junction to emulate and the chain's own
Pair::derive refuses for the same reason.

The path is m/44'/189189'/<account>'/0'/<0 for 87 | 1 for 65>'. Two things about
it are unusual and both are deliberate: the account index sits at the third level
rather than the last, and the trailing index carries the scheme rather than an
address index. It is what quantus-cli and the mobile wallet already derive, so
anything else produces addresses no other Quantus tool can find.

The ML-DSA branch sits before the existing seeding, not after, because that
seeding is the wrong one for us. mnemonicToMiniSecret is Substrate's variant and
the default reach in this file; using it here yields a perfectly well-formed key
for an account nobody owns. We need the 64-byte BIP39 seed and the HMAC-SHA512
chain keyed with "Dilithium seed", which is what the hdwallet crate does.

Accepted suri forms are nothing (account 0, matching quantus-cli's default),
//<n> for an account index, and //m/44'/… for a full path. Everything else
throws. That matters more than it looks: the syntax was built for curve
junctions, where /foo is soft and //foo hard over arbitrary bytes, and quietly
reinterpreting //Alice as something would hand back an address no other tool
derives and no seed phrase obviously recovers.

A raw hex seed is taken undederived — that is how the dev-genesis accounts are
defined — and combining one with a derivation path is refused rather than
guessed, since whether such a seed is the master or already derived is
unanswerable.

pair.derive() refuses with its own message. Not "not implemented" but not
possible: a child would have to come from this pair's key material, and ML-DSA
keys are not derivable from one another at all. The caller needs the mnemonic, so
the error says so rather than being generically unhelpful.

Verified against quantus-cli for both schemes from the public Substrate dev
phrase, plus index separation, explicit paths agreeing with indices, passphrase
sensitivity, and every refusal.

Refs quantus/common#4

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 08:56:20 +03:00
rob thijssenandClaude Opus 5 506b77351c fix(keyring): make account backup and restore work for ML-DSA pairs
Before this, an exported Quantus account could not be imported by anything.
decodePair located its divider by trying the two secret lengths every curve
scheme uses, 64 then 32; an ML-DSA secret is 4032 or 4896 bytes, so neither
matched and restore threw "Invalid encoding divider found in body". That is a
data-durability bug rather than a convenience one, which is why it lands before
any build lets a user create an account.

decodePair now takes an optional secretLength. The caller passes it rather than
this function searching for the divider: searching would work almost always and
fail catastrophically when it did not, since PAIR_DIV is five bytes and a
4032-byte secret contains a false match about once in 270 million keys — and the
result would be a silently wrong key rather than an error. The public key is read
as the remainder, because its length also varies and the body ends there.

The harder half is ordering. createFromJson returns a *locked* pair and callers
read pair.address off it long before any password appears. Every other scheme
manages because the address is the public key; an ML-DSA account id is a one-way
Poseidon2 hash and the public key is inside the encrypted blob. So PairInfo gains
an optional accountId, carried as data and used for the address while locked,
which decodePkcs8 clears once the real key arrives.

It also checks them against each other, and that check is not paranoia. For the
curve schemes a JSON file with an edited `address` field cannot decode at all.
Here it decodes perfectly and yields a pair reporting an address its key does not
control — a user would see someone else's address in their own wallet and believe
they held it. Pinned by a test that tampers exactly that field.

One more silent-wrong-key path closed: decodePkcs8 decided "secret key or seed?"
by length, so a 4032-byte ML-DSA secret took the seed branch and was fed to keygen
as entropy, producing a valid and entirely wrong key. The type knows the answer,
so it is asked.

Refs quantus/common#3

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 08:50:56 +03:00
rob thijssenandClaude Opus 5 2bffbc62bc feat(keyring,util-crypto): ML-DSA arms for createPair, alongside the curve ones
Wires @quantus/crypto into the four TYPE_* maps, so a keyring pair of type
dilithium65/dilithium87 can be created, addressed, signed with and verified.
The curve arms are untouched: quantus/common#6 removes them, but only once
post-quantum signing is proven end to end, so that a rejected first extrinsic has
two suspects rather than three.

util-crypto gains thin dilithium/ wrappers so keyring composes primitives the way
it already does for ed25519, rather than reaching into @quantus/crypto directly.

TYPE_ADDRESS is the interesting arm. Every other entry is identity or a cheap
re-encoding because on Substrate the address *is* the public key; ML-DSA takes
1952 or 2592 bytes to 32 with no way back. dilithiumVerify therefore mirrors the
runtime's Verify::verify rather than inventing anything: split sig||pk, hash the
embedded public key, check it equals the account being verified against, and only
then check the signature. Skipping the account check would accept a valid
signature from *some* key rather than *the* key, which is the whole property.

TYPE_PREFIX gets 0 for dilithium87 and 1 for dilithium65. For the curve types
those index Substrate's MultiSignature; here they index the runtime's
DilithiumSignatureScheme — a different enum reached by the same withType
mechanism, so nothing upstream of createPair needs to know.

Signing takes a mandatory context, which is quantus/common#5 arriving early
because the two are inseparable: TYPE_SIGNATURE cannot have an ML-DSA arm without
one. There is deliberately no default. Extrinsics on spec >= 148 verify under
QUANTUS_EXTRINSIC and earlier ones under the empty context, and the wrong choice
is a valid signature the chain rejects, indistinguishable locally. Only the
caller knows the spec version.

The curve sign functions are wrapped rather than passed by reference: ed25519Sign
and sr25519Sign take a third argument of their own (onlyJs), and letting it line
up with the context slot would silently reinterpret a Uint8Array as a boolean.

VRF signing refuses for ML-DSA. The construction fakes a VRF from an ordinary
signature, which is sound only given properties ML-DSA's signature does not have;
producing something that looks like a VRF proof and cannot be verified as one is
worse than refusing.

Two runtime whitelists sat behind the KeypairType union and silently rejected the
new types — the Keyring constructor and createFromJson. The compiler cannot see
those, and the first failed at construction with a message listing four types.
Both now use one named KEYPAIR_TYPES so the next change to the union has a single
place to look and the message cannot drift from the check.

Verified against quantus-cli, not against ourselves: keyring.addFromSeed with 32
zero bytes yields crystal_alice's address, signs to the runtime's wire length,
verifies under the extrinsic context and fails under the spec-147 one, and
another account's signature is rejected for it.

Refs quantus/common#2, quantus/common#5

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-10 18:43:25 +03:00
rob thijssenandClaude Opus 5 c616a0d949 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
2026-09-10 18:28:13 +03:00
rob thijssenandClaude Opus 5 6ed8b1ea6d feat(util-crypto,networks): open KeypairType to ML-DSA, register Quantus
Adds dilithium65 and dilithium87 to KeypairType, and Quantus to the network
registry at SS58 prefix 189.

Two types rather than one because the parameter set is not an implementation
detail: it selects the signature enum variant byte the chain reads off the wire,
it selects the trailing hardened index of the derivation path, and it fixes the
key and signature lengths. New accounts use dilithium65; dilithium87 is legacy,
used by accounts predating the recorded scheme and by the dev-genesis accounts,
so it is supported but never chosen.

Opening the union turns every table keyed by KeypairType into a type error,
which is the point — it enumerates precisely the places needing a post-quantum
answer. Where that answer is not written yet, assertSubstrateType() expresses the
gap as a named, typed refusal at the call site rather than a silent undefined
from an object index. It throws rather than degrading because all of these paths
produce key material or an address, and a wrong-but-plausible value means funds
at an address nobody controls. Call sites disappear as quantus/common#2 and #4
land. keyFromPath throws for the same reason: silently ignoring a derivation
path would hand back the parent key under a child's name.

Quantus is appended to the registry rather than merged — @substrate/ss58-registry
does not carry it and nothing occupies prefixes 185..195, so there is nothing to
override, and appending means a registry bump cannot silently drop it.
standardAccount is 'ml-dsa', a value upstream's closed union does not allow,
hence the widened KnownSubstrate; it is load-bearing, since a missing
standardAccount makes toExpanded mark the network isIgnored.

Testnets are first-class in this fork. Upstream drops any testnet from
availableNetworks however complete its entry is, which suits a production chain
picker and not us: Quantus is under active development and a wallet that cannot
reach Heisenberg can only be tested against real funds. The exception is an
allowlist (exposedTestnet) rather than removing the rule — removing it exposed
all fifteen of upstream's testnets too, moving the sorted tail from 'ZERO' to
'ZERO Alphaville', which is a change we have no reason to make. isTestnet is
still reported honestly so consumers can label or filter; we just stop deciding
for them.

Genesis hashes come from blackbeard.observer's config, which records their
provenance: mainnet read from the node with chain_getBlockHash(0) on 2026-09-09,
its launch day, and explicitly not the staging chain that telemetry calls
"Quantus Staging Mainnet". Symbol and decimals (QTC, 12) agree between the
chain's chain_spec.rs and the Dart SDK's AppConstants.

Three upstream tests encoded rules this fork no longer follows. Each is rewritten
to assert the new rule rather than deleted:
  - genesis entries for testnets are now allowed, for ones we opted into
  - an exposed testnet must still be labelled a testnet, since a user cannot tell
    play money from real money by looking at an address
  - one network per SS58 prefix stops holding when a chain has a testnet;
    mainnet and Heisenberg are both 189 because an address is valid on either,
    and consumers key on genesisHash rather than prefix

Refs quantus/common#1

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-10 14:50:49 +03:00
135 changed files with 49821 additions and 4179 deletions
+9
View File
@@ -13,3 +13,12 @@ logFilters:
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.9.1.cjs
# @quantus packages come from the Gitea registry. Safe as a whole-scope route
# because we own every name in it — unlike @polkadot, where routing the scope
# would send yarn looking for dozens of unforked packages that are not there.
#
# Reads are anonymous; publishing needs `pass gitea/package-publisher-token`.
npmScopes:
quantus:
npmRegistryServer: "https://git.lair.cafe/api/packages/quantus/npm/"
+1 -2
View File
@@ -31,8 +31,7 @@
"deno": "yarn polkadot-dev-deno-map && yarn build && deno check --import-map=import_map.json mod.ts",
"lint": "polkadot-dev-run-lint",
"postinstall": "polkadot-dev-yarn-only",
"test": "polkadot-dev-run-test --env browser ^mnemonic/toMiniSecretCmp",
"test:mnemonicCmp": "polkadot-dev-run-test --env browser mnemonic/toMiniSecretCmp",
"test": "polkadot-dev-run-test --env browser",
"test:node": "polkadot-dev-run-test --env browser ^mnemonic/toMiniSecretCmp",
"test:one": "polkadot-dev-run-test --env browser"
},
+3 -2
View File
@@ -18,11 +18,12 @@
"./packageDetect.cjs"
],
"type": "module",
"version": "14.0.3",
"version": "14.0.3-quantus.3",
"main": "index.js",
"dependencies": {
"@polkadot/util": "14.0.3",
"@polkadot/util-crypto": "14.0.3",
"@polkadot/util-crypto": "14.0.3-quantus.3",
"@quantus/crypto": "^0.3.0",
"tslib": "^2.8.0"
},
"peerDependencies": {
-609
View File
@@ -1,609 +0,0 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import type { KeyringPair$Json } from './types.js';
import { hexToU8a, stringToU8a } from '@polkadot/util';
import { base64Decode, cryptoWaitReady, encodeAddress, mnemonicGenerate, randomAsU8a, setSS58Format } from '@polkadot/util-crypto';
import * as languages from '@polkadot/util-crypto/mnemonic/wordlists/index';
import { decodePair } from './pair/decode.js';
import Keyring from './index.js';
await cryptoWaitReady();
describe('keypair', (): void => {
describe('ed25519', (): void => {
const publicKeyOne = new Uint8Array([47, 140, 97, 41, 216, 22, 207, 81, 195, 116, 188, 127, 8, 195, 230, 62, 209, 86, 207, 120, 174, 251, 74, 101, 80, 217, 123, 135, 153, 121, 119, 238]);
const publicKeyTwo = new Uint8Array([215, 90, 152, 1, 130, 177, 10, 183, 213, 75, 254, 211, 201, 100, 7, 58, 14, 225, 114, 243, 218, 166, 35, 37, 175, 2, 26, 104, 247, 7, 81, 26]);
const seedOne = stringToU8a('12345678901234567890123456789012');
const seedTwo = hexToU8a('0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60');
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 42, type: 'ed25519' });
keyring.addFromSeed(seedOne, {});
});
it('adds the pair', (): void => {
expect(
keyring.addFromSeed(seedTwo, {}).publicKey
).toEqual(publicKeyTwo);
});
it('creates via a dev seed', (): void => {
expect(
keyring.addFromUri('//Alice').address
).toEqual('5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu');
});
it('creates a ed25519 pair via mnemonicToSeed', (): void => {
expect(
keyring.addFromUri(
'seed sock milk update focus rotate barely fade car face mechanic mercy'
).address
).toEqual('5DkQP32jP4DVJLWWBRBoZF2tpWjqFrcrTBo6H5NcSk7MxKCC');
});
it('adds from a mnemonic, with correct ss58', (): void => {
// eslint-disable-next-line deprecation/deprecation
setSS58Format(20); // this would not be used
keyring.setSS58Format(2); // this would be used
const pair = keyring.addFromMnemonic('moral movie very draw assault whisper awful rebuild speed purity repeat card', {});
expect(pair.address).toEqual('HSLu2eci2GCfWkRimjjdTXKoFSDL3rBv5Ey2JWCBj68cVZj');
expect(encodeAddress(pair.publicKey)).toEqual('35cDYtPsdG1HUa2n2MaARgJyRz1WKMBZK1DL6c5cX7nugQh1');
});
it('allows publicKeys retrieval', (): void => {
keyring.addFromSeed(seedTwo, {});
expect(
keyring.getPublicKeys()
).toEqual([publicKeyOne, publicKeyTwo]);
});
it('allows retrieval of a specific item', (): void => {
expect(
keyring.getPair(publicKeyOne).publicKey
).toEqual(publicKeyOne);
});
it('allows adding from JSON', (): void => {
expect(
keyring.addFromJson(
JSON.parse('{"address":"5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua","encoded":"0xb4a14995d25ab609f3686e9fa45f1fb237cd833f33f00d4b12c51858ca070d96972e47d73aae5eeb0fc06f923826cf0943fdb02c2c2ee30ef52a7912663053940d1da4da66b3a3f520ae07422c1c94b2d95690fca9d1f4a997623bb2923a8833280e19e7f72c3c5cfa343974e60e2b3dc53b404fdaf330756daad5e4e3","encoding":{"content":"pkcs8","type":"xsalsa20-poly1305","version":"0"},"meta":{"isTesting":true,"name":"alice"}}') as KeyringPair$Json
).publicKey
).toEqual(
new Uint8Array([209, 114, 167, 76, 218, 76, 134, 89, 18, 195, 43, 160, 168, 10, 87, 174, 105, 171, 174, 65, 14, 92, 203, 89, 222, 232, 78, 47, 68, 50, 219, 79])
);
});
it('signs and verifies', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(publicKeyOne);
const signature = pair.sign(MESSAGE);
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
it('signs and verifies (withType)', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(publicKeyOne);
const signature = pair.sign(MESSAGE, { withType: true });
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
});
describe('sr25519', (): void => {
const publicKeyOne = new Uint8Array([116, 28, 8, 160, 111, 65, 197, 150, 96, 143, 103, 116, 37, 155, 217, 4, 51, 4, 173, 250, 93, 62, 234, 98, 118, 11, 217, 190, 151, 99, 77, 99]);
const publicKeyTwo = hexToU8a('0x44a996beb1eef7bdcab976ab6d2ca26104834164ecf28fb375600576fcc6eb0f');
const seedOne = stringToU8a('12345678901234567890123456789012');
const seedTwo = hexToU8a('0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60');
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 42, type: 'sr25519' });
keyring.addFromSeed(seedOne, {});
});
it('creates with dev phrase when only path specified', (): void => {
expect(
keyring.createFromUri('//Alice').address
).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
});
it('creates with integer derivations', (): void => {
// MAX_SAFE_INTEGER
expect(
keyring.createFromUri('//9007199254740991').address
).toEqual('5CDsyNZyqxLpHnTvknr68anUcYoBFjZbFKiEJJf4prB75Uog');
// MAX_SAFE_INTEGER + extra digits
expect(
keyring.createFromUri('//900719925474099999').address
).toEqual('5GHj2D7RG2m2DXYwGSDpXwuuxn53G987i7p2EQVDqP4NYu4q');
});
it('creates via dev seed (2-byte encoding)', (): void => {
keyring.setSS58Format(252);
expect(
keyring.addFromUri('//Alice').address
).toEqual('xw8P6urbSAronL3zZFB7dg8p7LLSgKCUFDUgjohnf1iP434ic');
});
it('adds the pair', (): void => {
expect(
keyring.addFromSeed(seedTwo, {}).publicKey
).toEqual(publicKeyTwo);
});
it('adds from a mnemonic', (): void => {
keyring.setSS58Format(2);
expect(
keyring.addFromMnemonic('moral movie very draw assault whisper awful rebuild speed purity repeat card', {}).address
).toEqual('FSjXNRT2K1R5caeHLPD6WMrqYUpfGZB7ua8W89JFctZ1YqV');
});
it('allows publicKeys retrieval', (): void => {
keyring.addFromSeed(seedTwo, {});
expect(
keyring.getPublicKeys()
).toEqual([publicKeyOne, publicKeyTwo]);
});
it('allows retrieval of a specific item', (): void => {
expect(
keyring.getPair(publicKeyOne).publicKey
).toEqual(publicKeyOne);
});
it('allows adding from JSON', (): void => {
expect(
keyring.addFromJson(
JSON.parse('{"address":"5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua","encoded":"0xb4a14995d25ab609f3686e9fa45f1fb237cd833f33f00d4b12c51858ca070d96972e47d73aae5eeb0fc06f923826cf0943fdb02c2c2ee30ef52a7912663053940d1da4da66b3a3f520ae07422c1c94b2d95690fca9d1f4a997623bb2923a8833280e19e7f72c3c5cfa343974e60e2b3dc53b404fdaf330756daad5e4e3","encoding":{"content":"pkcs8","type":"xsalsa20-poly1305","version":"0"},"meta":{"isTesting":true,"name":"alice"}}') as KeyringPair$Json
).publicKey
).toEqual(
new Uint8Array([209, 114, 167, 76, 218, 76, 134, 89, 18, 195, 43, 160, 168, 10, 87, 174, 105, 171, 174, 65, 14, 92, 203, 89, 222, 232, 78, 47, 68, 50, 219, 79])
);
});
it('signs and verifies', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(publicKeyOne);
const signature = pair.sign(MESSAGE);
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
it('signs and verifies (withType)', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(publicKeyOne);
const signature = pair.sign(MESSAGE, { withType: true });
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
});
describe('ecdsa', (): void => {
const seedOne = 'potato act energy ahead stone taxi receive fame gossip equip chest round';
const seedTwo = hexToU8a('0x3c74be003bd9a876be439949ccf2b292bd966c94959a689173b295b326cd6da7');
const publicKeyOne = hexToU8a('0x02c6b6c664db5ef505477bba1cf2f1789c98796b9bb5fa21abd0ac4589bed980e7');
const publicKeyTwo = hexToU8a('0x021da683b913fb28c979ba3e5f1881415cef4b1f58a5d05ed3610a2995e7b4943c');
const addressKeyOne = hexToU8a('0x0cfd0dd2c59a9987b9848919163931b6a42283ffd3d91e92c98b522525a7038f');
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 42, type: 'ecdsa' });
keyring.addFromMnemonic(seedOne, {});
});
it('creates with dev phrase when only path specified', (): void => {
expect(
keyring.createFromUri('//Alice').address
).toEqual('5C7C2Z5sWbytvHpuLTvzKunnnRwQxft1jiqrLD5rhucQ5S9X');
});
it('adds the pair', (): void => {
expect(
keyring.addFromSeed(seedTwo, {}).publicKey
).toEqual(publicKeyTwo);
});
it('adds from a mnemonic', (): void => {
keyring.setSS58Format(2);
expect(
keyring.addFromMnemonic('moral movie very draw assault whisper awful rebuild speed purity repeat card').address
).toEqual('DrRE1KAcs4pCicX8yJPh7YxkLPQ2vXnCFSVRPQfx38KjEFe');
});
it('allows publicKeys retrieval', (): void => {
keyring.addFromSeed(seedTwo, {});
expect(
keyring.getPublicKeys()
).toEqual([publicKeyOne, publicKeyTwo]);
});
it('allows retrieval of a specific item', (): void => {
expect(
keyring.getPair(addressKeyOne).publicKey
).toEqual(publicKeyOne);
});
it('allows adding from JSON', (): void => {
expect(
keyring.addFromJson(
JSON.parse('{"address":"5DzMsaYFhmpRdErWrP6K6PD7UXzYoeETToSBUrZSvxasqWRz","encoded":"0xa192d39b42bc1601bf61df31039a554228593fadf870bc837b658a5114627aca199fff596260c95fe8994c66a47636cf0270aa08f402ba5541038753960d00e6c3af5e239ec58fb1eef3db7d6bc266f4853bdfe4ed17122d9092d879014d53980d2ee57f6f55a88c38836447d8645008e8815379626addc8f81f80cd49a2","encoding":{"content":"pkcs8","type":"xsalsa20-poly1305","version":"2"},"meta":{}}') as KeyringPair$Json
).address
).toEqual('5DzMsaYFhmpRdErWrP6K6PD7UXzYoeETToSBUrZSvxasqWRz');
});
it('allows creation from JSON', (): void => {
keyring.setSS58Format(2);
const pair = keyring.createFromJson(
JSON.parse('{"address":"0x02fde629668eb2bcc7d748f40a7e597f7c7b363498ff3db31f03ce4854937883ad","encoded":"qIhAhKqtf2iyEoWEr8nmBdksSI8EHHCpgJHToqd6Pl8AgAAAAQAAAAgAAADDZ//fj/BRRj+0+bl1KAlYgoPJp6nEUwiw0fVqO2BW4mjEgQ+iWwJEgDf1JUtecbzOlfhTXBzqX/dIYzLgUADrF4EFEPpboCWiU1iN7W/3DM1cOTRVvTGcbdIqW//z3axhz961qzeJVUIFgllwGe/euLUPIlKbIkiN/CsRYdQ=","encoding":{"content":["pkcs8","ecdsa"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"ecdsa","tags":[],"whenCreated":1600925898271}}') as KeyringPair$Json
);
expect(pair.address).toEqual('DHL8HKFuTTR55JzzLmkJRCAfPBbuevKaT9cXikxbEV97Ko8');
expect(pair.publicKey).toEqual(hexToU8a('0x02fde629668eb2bcc7d748f40a7e597f7c7b363498ff3db31f03ce4854937883ad'));
});
it('fails toJson() when password is incorrect', (): void => {
const pair = keyring.createFromJson(
JSON.parse('{"address":"0x02fde629668eb2bcc7d748f40a7e597f7c7b363498ff3db31f03ce4854937883ad","encoded":"qIhAhKqtf2iyEoWEr8nmBdksSI8EHHCpgJHToqd6Pl8AgAAAAQAAAAgAAADDZ//fj/BRRj+0+bl1KAlYgoPJp6nEUwiw0fVqO2BW4mjEgQ+iWwJEgDf1JUtecbzOlfhTXBzqX/dIYzLgUADrF4EFEPpboCWiU1iN7W/3DM1cOTRVvTGcbdIqW//z3axhz961qzeJVUIFgllwGe/euLUPIlKbIkiN/CsRYdQ=","encoding":{"content":["pkcs8","ecdsa"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"ecdsa","tags":[],"whenCreated":1600925898271}}') as KeyringPair$Json
);
expect(
() => pair.toJson('invalid')
).toThrow(/Unable to decode using the supplied passphrase/);
});
it('pass toJson() when password is correct', (): void => {
const pair = keyring.createFromJson(
JSON.parse('{"address":"0x02fde629668eb2bcc7d748f40a7e597f7c7b363498ff3db31f03ce4854937883ad","encoded":"qIhAhKqtf2iyEoWEr8nmBdksSI8EHHCpgJHToqd6Pl8AgAAAAQAAAAgAAADDZ//fj/BRRj+0+bl1KAlYgoPJp6nEUwiw0fVqO2BW4mjEgQ+iWwJEgDf1JUtecbzOlfhTXBzqX/dIYzLgUADrF4EFEPpboCWiU1iN7W/3DM1cOTRVvTGcbdIqW//z3axhz961qzeJVUIFgllwGe/euLUPIlKbIkiN/CsRYdQ=","encoding":{"content":["pkcs8","ecdsa"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"ecdsa","tags":[],"whenCreated":1600925898271}}') as KeyringPair$Json
);
expect(
() => pair.toJson('testing')
).not.toThrow();
});
it('encodes a pair toJSON (and decodes)', (): void => {
const pair = keyring.createFromUri('moral movie very draw assault whisper awful rebuild speed purity repeat card');
const json = pair.toJson('password');
expect(json.address).toEqual('0x03ddca309bd5fedd01f914d6fb76f23aa848a2a520802159215dba5085d7863619');
expect(json.encoding).toEqual({
content: ['pkcs8', 'ecdsa'],
type: ['scrypt', 'xsalsa20-poly1305'],
version: '3'
});
const newPair = keyring.createFromJson(json);
expect(newPair.publicKey).toEqual(pair.publicKey);
expect(
() => newPair.unlock('password')
).not.toThrow();
});
it('signs and verifies', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(addressKeyOne);
const signature = pair.sign(MESSAGE);
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
it('signs and verifies (withType)', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(addressKeyOne);
const signature = pair.sign(MESSAGE, { withType: true });
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
});
describe('ethereum', (): void => {
// combine mnemonic with derivation path
const PHRASE = 'seed sock milk update focus rotate barely fade car face mechanic mercy' + '/m/44\'/60\'/0\'/0/0';
const PRIV_KEY_ONE = '0x070dc3117300011918e26b02176945cc15c3d548cf49fd8418d97f93af699e46';
const ETH_ADDRESS_ONE = '0x31ea8795EE32D782C8ff41a5C68Dcbf0F5B27f6d';
const ETH_ADDRESS_TWO = '0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887';
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ type: 'ethereum' });
});
it('creates with dev phrase from the private key', (): void => {
const pair = keyring.addFromSeed(hexToU8a(PRIV_KEY_ONE));
expect(
pair.address
).toEqual(ETH_ADDRESS_ONE);
});
it('creates with dev phrase from the private key in createFromUri', (): void => {
const pair = keyring.createFromUri(PRIV_KEY_ONE);
expect(
pair.address
).toEqual(ETH_ADDRESS_ONE);
});
it('creates with dev phrase with derivation path specified', (): void => {
const pair = keyring.createFromUri(PHRASE);
expect(
pair.address
).toEqual(ETH_ADDRESS_ONE);
});
it('creates with dev phrase with derivation path specified - addFromUri', (): void => {
expect(
keyring.addFromUri(PHRASE).address
).toEqual(ETH_ADDRESS_ONE);
});
it('creates with dev phrase with derivation path specified - addFromUri with type', (): void => {
const keyringUntyped = new Keyring();
expect(
keyringUntyped.addFromUri(PHRASE, {}, 'ethereum').address
).toEqual(ETH_ADDRESS_ONE);
});
it('encodes a pair toJSON (and decodes)', (): void => {
const pair = keyring.createFromUri(PHRASE);
const json = pair.toJson('password');
expect(json.address).toEqual('0x0381351b1b46d2602b0992bb5d5531f9c1696b0812feb2534b6884adc47e2e1d8b'); // this is the public key (different from address for ethereum)
expect(json.encoding).toEqual({
content: ['pkcs8', 'ethereum'],
type: ['scrypt', 'xsalsa20-poly1305'],
version: '3'
});
const newPair = keyring.createFromJson(json);
expect(newPair.publicKey).toEqual(pair.publicKey);
expect(
() => newPair.unlock('password')
).not.toThrow();
});
it('encodes a pair toJSON and back', (): void => {
const pairOriginal = keyring.createFromUri(PHRASE);
const json = pairOriginal.toJson('password');
const pair = keyring.addFromJson(
json
);
expect(pair.address).toEqual(ETH_ADDRESS_ONE);
pair.decodePkcs8('password');
expect(pair.isLocked).toBe(false);
expect(pair.address).toBe(ETH_ADDRESS_ONE);
});
it('allows adding from JSON', (): void => {
const pair = keyring.addFromJson(
JSON.parse('{"address":"KWCv1L3QX9LDPwY4VzvLmarEmXjVJidUzZcinvVnmxAJJCBou","encoded":"U8qFEaghhmNV2PgFhjqzmhyUy37Ok7abfFU2MNsBd0sAgAAAAQAAAAgAAAA3+NniKogzNphiMNueB1X0sGA07B6CaXWfpXPx45iSXoTTprwzU5mOoSqUWO0GKHROI72LN+uJ8Yfv6Ll6JOOV3VPKfoVoFmYm+zDrrMPa0gk5E5kUuSijxADcE6zUrliPVr0Ix/qaghu5SJ7RtWDQLBf4Hp86SJ8Gg6gTSSk=","encoding":{"content":["pkcs8","ethereum"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{}}') as KeyringPair$Json
);
expect(pair.publicKey).toEqual(hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077'));
expect(pair.address).toEqual(ETH_ADDRESS_TWO);
pair.decodePkcs8('password');
expect(pair.isLocked).toBe(false);
expect(pair.publicKey).toEqual(hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077'));
expect(pair.address).toBe(ETH_ADDRESS_TWO);
});
it('allows for signing/verification', (): void => {
const MESSAGE = stringToU8a('just some test message');
const signer = keyring.createFromUri(PHRASE);
const verifier = keyring.addFromJson(
JSON.parse('{"address":"KWCv1L3QX9LDPwY4VzvLmarEmXjVJidUzZcinvVnmxAJJCBou","encoded":"U8qFEaghhmNV2PgFhjqzmhyUy37Ok7abfFU2MNsBd0sAgAAAAQAAAAgAAAA3+NniKogzNphiMNueB1X0sGA07B6CaXWfpXPx45iSXoTTprwzU5mOoSqUWO0GKHROI72LN+uJ8Yfv6Ll6JOOV3VPKfoVoFmYm+zDrrMPa0gk5E5kUuSijxADcE6zUrliPVr0Ix/qaghu5SJ7RtWDQLBf4Hp86SJ8Gg6gTSSk=","encoding":{"content":["pkcs8","ethereum"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{}}') as KeyringPair$Json
);
const signature = signer.sign(MESSAGE);
const dummyPublic = verifier.publicKey.slice();
dummyPublic[dummyPublic.length - 1] = 0;
expect(verifier.verify(MESSAGE, signature, signer.publicKey)).toBe(true);
expect(verifier.verify(MESSAGE, signature, dummyPublic)).toBe(false);
expect(verifier.verify(new Uint8Array(), signature, signer.publicKey)).toBe(false);
});
it('allows for signing/verification (withType)', (): void => {
const MESSAGE = stringToU8a('just some test message');
const signer = keyring.createFromUri(PHRASE);
const verifier = keyring.addFromJson(
JSON.parse('{"address":"KWCv1L3QX9LDPwY4VzvLmarEmXjVJidUzZcinvVnmxAJJCBou","encoded":"U8qFEaghhmNV2PgFhjqzmhyUy37Ok7abfFU2MNsBd0sAgAAAAQAAAAgAAAA3+NniKogzNphiMNueB1X0sGA07B6CaXWfpXPx45iSXoTTprwzU5mOoSqUWO0GKHROI72LN+uJ8Yfv6Ll6JOOV3VPKfoVoFmYm+zDrrMPa0gk5E5kUuSijxADcE6zUrliPVr0Ix/qaghu5SJ7RtWDQLBf4Hp86SJ8Gg6gTSSk=","encoding":{"content":["pkcs8","ethereum"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{}}') as KeyringPair$Json
);
const signature = signer.sign(MESSAGE, { withType: true });
const dummyPublic = verifier.publicKey.slice();
dummyPublic[dummyPublic.length - 1] = 0;
expect(verifier.verify(MESSAGE, signature, signer.publicKey)).toBe(true);
expect(verifier.verify(MESSAGE, signature, dummyPublic)).toBe(false);
expect(verifier.verify(new Uint8Array(), signature, signer.publicKey)).toBe(false);
});
});
describe('raw pair add/create', (): void => {
const json = JSON.parse('{"address":"5PjeoaQzCoYbSi42aQRKB3Sx18StCaEAzCbGEEbWbZyfKS3H","encoded":"JQUl8ZpoXv2OMkL9TPylLmcIye2cYhaS9INICbFgZTsAgAAAAQAAAAgAAAAr/0hJOOzokIdBG71TstigLABX9D5xGD7L37ySxtjDrVRg26LL90jLQ47quT9o3bq6ppXMVL6USk7Q4p3WU66bojTFuCDyhpYRhNbUqU6s0rD3S4bhv9lG+pG9vQ4eD5PVQUvxdANmJpYuDg45nrTmsMC5AHGdFGkHW/LHnkmbFid1cvPYkdiBoef5CIEdoly512pxMupVxnJWF1NT","encoding":{"content":["pkcs8","sr25519"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"name":"hello"}}') as KeyringPair$Json;
const decoded = decodePair('1', base64Decode(json.encoded), json.encoding.type);
const keyring = new Keyring({ ss58Format: 44 });
it('creates a pair from a private/public combo', (): void => {
const pair = keyring.createFromPair(decoded, json.meta, 'sr25519');
expect(pair.address).toEqual('5PjeoaQzCoYbSi42aQRKB3Sx18StCaEAzCbGEEbWbZyfKS3H');
expect(pair.isLocked).toEqual(false);
expect(pair.meta.name).toEqual('hello');
});
it('adds a pair from a private/public combo', (): void => {
keyring.addFromPair(decoded, json.meta, 'sr25519');
const pair = keyring.getPairs()[0];
expect(pair.address).toEqual('5PjeoaQzCoYbSi42aQRKB3Sx18StCaEAzCbGEEbWbZyfKS3H');
expect(pair.isLocked).toEqual(false);
expect(pair.meta.name).toEqual('hello');
});
});
describe('util', (): void => {
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 42 });
});
it('can re-encode an address to Polkadot live', (): void => {
expect(
keyring.encodeAddress('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 0)
).toEqual('15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5');
});
it('can re-encode an address to keyring default', (): void => {
expect(
keyring.encodeAddress('15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5')
).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
});
});
describe('version 2 JSON', (): void => {
const PAIR = '{"address":"5CczAE5AmGrZ93MeVhha3Ywam7j9dKB7cArnH7gtrXcMFJvu","encoded":"0xee8f236e2ac3217ce689692a4afc612220dc77fddaed0482f8f95136a7c3e034cccfbc495410a6e9b2439904974ed1d207abeca536ff6985ceb78edeeb3dc343e561c184c488101af8811d1331430b4ccf0e96ef507132e5132964e8564232e7100d973c5bee7b231dd0c8ad5273f3501515a422c8d7ed9d20a73c0ed17c98ee4588e54844bb73052dcad81f7a1094613d63c162fec7446c88b1fae70e","encoding":{"content":["pkcs8","sr25519"],"type":"xsalsa20-poly1305","version":"2"},"meta":{"genesisHash":"0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e","name":"json v2","tags":[],"whenCreated":1595243159596}}';
const PASS2 = 'versionTwo';
const PASS3 = 'versionThree';
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 42 });
});
it('can decode from a version 2 JSON file', (): void => {
const pair = keyring.addFromJson(JSON.parse(PAIR) as KeyringPair$Json);
pair.decodePkcs8(PASS2);
const json = pair.toJson(PASS3);
expect(pair.isLocked).toBe(false);
expect(pair.address).toBe('5CczAE5AmGrZ93MeVhha3Ywam7j9dKB7cArnH7gtrXcMFJvu');
expect(json.encoding).toEqual({
content: ['pkcs8', 'sr25519'],
type: ['scrypt', 'xsalsa20-poly1305'],
version: '3'
});
pair.decodePkcs8(PASS3);
expect(pair.address).toEqual('5CczAE5AmGrZ93MeVhha3Ywam7j9dKB7cArnH7gtrXcMFJvu');
});
});
describe('version 3 JSON (hex)', (): void => {
const PAIR = '{"address":"FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH","encoded":"0xcd238963070cc4d6806053ee1ac500c7add9c28732bb5d434a332f84a91d9be0008000000100000008000000cf630a1113941b350ddd06697e50399183162e5e9a0e893eafc7f5f4893a223dca5055706b9925b56fdb4304192143843da718e11717daf89cf4f4781f94fb443f61432f782d54280af9eec90bd3069c3cc2d957a42b7c18dc2e9497f623735518e0e49b58f8e4db2c09da3a45dbb935659d015fc94b946cba75b606a6ff7f4e823f6b049e2e6892026b49de02d6dbbd64646fe0933f537d9ea53a70be","encoding":{"content":["pkcs8","sr25519"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"version3","tags":[],"whenCreated":1595277797639}}';
const PASS3 = 'version3';
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 2 });
});
it('can decode from a version 3 JSON file', (): void => {
const pair = keyring.addFromJson(JSON.parse(PAIR) as KeyringPair$Json);
pair.decodePkcs8(PASS3);
expect(pair.isLocked).toBe(false);
expect(pair.address).toBe('FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH');
});
});
describe('version 3 JSON (base64)', (): void => {
const PAIR = '{"address":"FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH","encoded":"ILjSgYaGvq1zaCz/kx+aqfLaHBjLXz0Qsmr6RnkOVU4AgAAAAQAAAAgAAAB5R2hm5kgXyc0NQYFxvMU4zCdjB+ugs/ibEooqCvuudbaeKn3Ee47NkCqU1ecOJV+eeaVn4W4dRvIpj5kGmQOGsewR+MiQ/B0G9NFh7JXV0qcPlk2QMNW1/mbJrTO4miqL448BSkP7ZOhUV6HFUpMt3B9HwjiRLN8RORcFp0ID/Azs4Jl/xOpXNzbgQGIffWgCIKTxN9N1ku6tdlG4","encoding":{"content":["pkcs8","sr25519"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"version3","tags":[],"whenCreated":1595277797639,"whenEdited":1595278378596}}';
const PASS3 = 'version3';
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 2 });
});
it('can decode from a version 3 JSON file', (): void => {
const pair = keyring.addFromJson(JSON.parse(PAIR) as KeyringPair$Json);
pair.decodePkcs8(PASS3);
expect(pair.isLocked).toBe(false);
expect(pair.address).toBe('FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH');
});
});
describe('wordlist', (): void => {
it('creates keypair from different wordlists mnemonics', (): void => {
Object.keys(languages).forEach((language) => {
const mnemonic = mnemonicGenerate(12, languages[language as keyof typeof languages]);
const keyring = new Keyring({
type: 'ed25519'
});
expect(keyring.addFromMnemonic(
mnemonic,
{},
'ed25519',
languages[language as keyof typeof languages]
)).toBeDefined();
});
});
it('cannot create from invalid wordlist', (): void => {
const mnemonic = mnemonicGenerate(12, languages.japanese);
const keyring = new Keyring({
type: 'ed25519'
});
expect(() => keyring.addFromMnemonic(
mnemonic,
{},
'ed25519',
languages.english
)).toThrow('Invalid bip39 mnemonic specified');
});
});
});
+139
View File
@@ -0,0 +1,139 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import type { KeypairType } from '@polkadot/util-crypto/types';
import { contextForSpec } from '@quantus/crypto';
import { u8aConcat } from '@polkadot/util';
import { cryptoWaitReady, decodeAddress, dilithiumSizes } from '@polkadot/util-crypto';
import { Keyring } from './index.js';
import { createTestPairs } from './testingPairs.js';
await cryptoWaitReady();
// Addresses `quantus developer create-test-wallets` prints for the dev accounts.
const ALICE = 'qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7';
const BOB = 'qzkYEQv8tQsmniZYdame3Cku18RL5g9bGK9Pdydq5TMPdpE3y';
const CHARLIE = 'qzntBpmqHZF1jxC8KJKpuxcYuHST892jyXBqRctpAxd1WQ9BL';
const MESSAGE = new Uint8Array([1, 2, 3, 4]);
const PASSWORD = 'correct horse';
// Upstream's keyring specs exercised ed25519, sr25519, ecdsa and ethereum pairs.
// Those types, and their primitives, are gone (quantus/common#6). What replaces
// them is what a post-quantum-only keyring has to guarantee instead.
describe('Keyring (post-quantum only)', (): void => {
describe('refuses quantum-unsafe keys, and says why', (): void => {
const unsafe = ['ed25519', 'sr25519', 'ecdsa', 'ethereum'] as unknown as KeypairType[];
for (const type of unsafe) {
it(`at construction: ${type}`, (): void => {
expect(() => new Keyring({ type })).toThrow(/not quantum-safe and cannot be held here/);
});
it(`from a uri: ${type}`, (): void => {
expect(() => new Keyring().createFromUri('//Alice', {}, type)).toThrow(/not quantum-safe/);
});
}
// The entry point that matters most: a backup file the user chose. This is
// the shape of a real polkadot{.js} export of an ed25519 account.
it('from a polkadot{.js} JSON backup', (): void => {
const json = {
address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
encoded: '0x00',
encoding: { content: ['pkcs8', 'ed25519'], type: ['scrypt', 'xsalsa20-poly1305'], version: '3' },
meta: { name: 'polkadot account' }
};
expect(() => new Keyring().createFromJson(json as never)).toThrow(/ed25519 keys are not quantum-safe and cannot be held here/);
});
it('and still refuses an unknown type as unknown', (): void => {
expect(() => new Keyring({ type: 'dilithium44' as KeypairType })).toThrow(/found 'dilithium44'/);
});
});
it('defaults to ML-DSA-65', (): void => {
expect(new Keyring().type).toEqual('dilithium65');
});
describe('test pairs are the Quantus dev accounts', (): void => {
const pairs = createTestPairs({ ss58Format: 189 });
it('with the addresses the chain endows', (): void => {
expect(pairs.crystal_alice.address).toEqual(ALICE);
expect(pairs.dilithium_bob.address).toEqual(BOB);
expect(pairs.crystal_charlie.address).toEqual(CHARLIE);
expect(pairs.crystal_alice.type).toEqual('dilithium87');
});
it('and a nobody pair of a type the keyring can hold', (): void => {
expect(pairs.nobody.type).toEqual('dilithium65');
});
});
// Upstream decoded the address and passed it as the public key, which for
// Substrate is the same bytes. Here the address is a hash of the key, so every
// watch-only account reported the hash of its own address.
it('adds a watch-only account at the address it was given', (): void => {
const keyring = new Keyring({ ss58Format: 189 });
const watched = keyring.addFromAddress(ALICE, { name: 'watched' });
expect(watched.address).toEqual(ALICE);
expect(watched.addressRaw).toEqual(decodeAddress(ALICE));
expect(keyring.getPair(ALICE).meta.name).toEqual('watched');
});
describe('JSON round trip', (): void => {
for (const type of ['dilithium65', 'dilithium87'] as const) {
it(`${type}: encrypts, restores locked at the right address, and unlocks`, (): void => {
const keyring = new Keyring({ ss58Format: 189 });
const pair = keyring.addFromSeed(new Uint8Array(32).fill(7), { name: 'roundtrip' }, type);
const restored = keyring.createFromJson(pair.toJson(PASSWORD));
expect(restored.isLocked).toBe(true);
expect(restored.address).toEqual(pair.address);
expect(() => restored.decodePkcs8('wrong')).toThrow();
restored.decodePkcs8(PASSWORD);
expect(restored.publicKey).toEqual(pair.publicKey);
expect(restored.sign(MESSAGE, { context: contextForSpec(148) })).toEqual(pair.sign(MESSAGE, { context: contextForSpec(148) }));
});
}
});
describe('verify', (): void => {
const keyring = new Keyring({ ss58Format: 189 });
const alice = keyring.addFromSeed(new Uint8Array(32), {}, 'dilithium87');
const bob = keyring.addFromSeed(new Uint8Array(32).fill(1), {}, 'dilithium87');
const context = contextForSpec(148);
// what pair.sign returns: the chain's wire form, signature ‖ publicKey
const signature = alice.sign(MESSAGE, { context });
const bare = signature.slice(0, dilithiumSizes('dilithium87').signature);
it('accepts the signature ‖ publicKey form and a bare signature', (): void => {
expect(signature).toEqual(u8aConcat(bare, alice.publicKey));
expect(alice.verify(MESSAGE, signature, alice.publicKey, context)).toBe(true);
expect(alice.verify(MESSAGE, bare, alice.publicKey, context)).toBe(true);
});
it('fails under the wrong context, for the wrong signer, or for another message', (): void => {
expect(alice.verify(MESSAGE, signature, alice.publicKey)).toBe(false);
expect(alice.verify(MESSAGE, signature, bob.publicKey, context)).toBe(false);
expect(alice.verify(new Uint8Array([9]), signature, alice.publicKey, context)).toBe(false);
});
});
it('has no derivation and no VRF', (): void => {
const pair = new Keyring().addFromSeed(new Uint8Array(32));
expect(() => pair.derive('//1')).toThrow(/derive from the mnemonic/);
expect(() => pair.vrfSign(MESSAGE)).toThrow(/VRF signing is not available/);
expect(() => pair.vrfVerify(MESSAGE, new Uint8Array(96), pair.publicKey)).toThrow(/VRF verification is not available/);
});
});
+80 -56
View File
@@ -4,19 +4,40 @@
import type { EncryptedJsonEncoding, Keypair, KeypairType } from '@polkadot/util-crypto/types';
import type { KeyringInstance, KeyringOptions, KeyringPair, KeyringPair$Json, KeyringPair$Meta } from './types.js';
import { hexToU8a, isHex, stringToU8a } from '@polkadot/util';
import { base64Decode, decodeAddress, ed25519PairFromSeed as ed25519FromSeed, encodeAddress, ethereumEncode, hdEthereum, keyExtractSuri, keyFromPath, mnemonicToLegacySeed, mnemonicToMiniSecret, secp256k1PairFromSeed as secp256k1FromSeed, sr25519PairFromSeed as sr25519FromSeed } from '@polkadot/util-crypto';
import { hexToU8a, isHex } from '@polkadot/util';
import { base64Decode, decodeAddress, dilithiumPairFromMnemonic, dilithiumPairFromSeed, dilithiumPathFromSuri, encodeAddress, keyExtractSuri } from '@polkadot/util-crypto';
import { createPair } from './pair/index.js';
import { DEV_PHRASE } from './defaults.js';
import { Pairs } from './pairs.js';
const PairFromSeed = {
ecdsa: (seed: Uint8Array): Keypair => secp256k1FromSeed(seed),
ed25519: (seed: Uint8Array): Keypair => ed25519FromSeed(seed),
ethereum: (seed: Uint8Array): Keypair => secp256k1FromSeed(seed),
sr25519: (seed: Uint8Array): Keypair => sr25519FromSeed(seed)
};
/**
* Every keypair type this keyring will construct.
*
* Was two separate inline arrays, both of which a widened `KeypairType` silently
* walked past — the union is compile-time and these checks are not. Named once so
* the next change to the union has one place to look, and so the error message
* cannot drift from the check.
*/
const KEYPAIR_TYPES: KeypairType[] = ['dilithium65', 'dilithium87'];
/** Upstream's types, refused by name so the refusal can say why. */
const QUANTUM_UNSAFE = ['ecdsa', 'ed25519', 'ethereum', 'sr25519'];
/**
* Refuse anything that is not a post-quantum keypair type.
*
* A key of upstream's types arriving here (a JSON backup from polkadot{.js}, a
* caller asking for sr25519) is not a bug in this software, and "unknown crypto
* type" would read as one. Say what it is instead. quantus/common#6
*/
function assertKeypairType (type: string | undefined): asserts type is KeypairType {
if (type && QUANTUM_UNSAFE.includes(type)) {
throw new Error(`${type} keys are not quantum-safe and cannot be held here; only ML-DSA (dilithium65, dilithium87) keys are supported`);
} else if (!KEYPAIR_TYPES.includes(type as KeypairType)) {
throw new Error(`Expected a keypair type of one of ${KEYPAIR_TYPES.map((t) => `'${t}'`).join(', ')}, found '${type || 'unknown'}'`);
}
}
function pairToPublic ({ publicKey }: KeyringPair): Uint8Array {
return publicKey;
@@ -48,11 +69,12 @@ export class Keyring implements KeyringInstance {
public decodeAddress = decodeAddress;
constructor (options: KeyringOptions = {}) {
options.type = options.type || 'ed25519';
// ML-DSA-65 is the scheme new Quantus accounts use.
options.type = options.type || 'dilithium65';
if (!['ecdsa', 'ethereum', 'ed25519', 'sr25519'].includes(options.type || 'undefined')) {
throw new Error(`Expected a keyring type of either 'ed25519', 'sr25519', 'ethereum' or 'ecdsa', found '${options.type || 'unknown'}`);
}
// Behind the union type: a JavaScript caller, or one casting, can still pass
// anything, and this fails at construction rather than at use.
assertKeypairType(options.type);
this.#pairs = new Pairs();
this.#ss58 = options.ss58Format;
@@ -74,7 +96,7 @@ export class Keyring implements KeyringInstance {
}
/**
* @description Returns the type of the keyring, ed25519, sr25519 or ecdsa
* @description Returns the type of the keyring: dilithium65 or dilithium87
*/
public get type (): KeypairType {
return this.#type;
@@ -97,9 +119,14 @@ export class Keyring implements KeyringInstance {
* `addPair` to stores in a keyring pair dictionary the public key of the generated pair as a key and the pair as the associated value.
*/
public addFromAddress (address: string | Uint8Array, meta: KeyringPair$Meta = {}, encoded: Uint8Array | null = null, type: KeypairType = this.type, ignoreChecksum?: boolean, encType?: EncryptedJsonEncoding[]): KeyringPair {
const publicKey = this.decodeAddress(address, ignoreChecksum);
assertKeypairType(type);
return this.addPair(createPair({ toSS58: this.encodeAddress, type }, { publicKey, secretKey: new Uint8Array() }, meta, encoded, encType));
// An address is a Poseidon2 hash of an ML-DSA public key, not the key, so it
// is carried as an account id. Upstream passed it as `publicKey`, which for
// these types made every watch-only account report the hash of its address.
const accountId = this.decodeAddress(address, ignoreChecksum);
return this.addPair(createPair({ toSS58: this.encodeAddress, type }, { accountId, publicKey: new Uint8Array(), secretKey: new Uint8Array() }, meta, encoded, encType));
}
/**
@@ -144,7 +171,7 @@ export class Keyring implements KeyringInstance {
*/
public addFromSeed (seed: Uint8Array, meta: KeyringPair$Meta = {}, type: KeypairType = this.type): KeyringPair {
return this.addPair(
createPair({ toSS58: this.encodeAddress, type }, PairFromSeed[type](seed), meta, null)
this.createFromPair(dilithiumPairFromSeed(seed, type), meta, type)
);
}
@@ -175,19 +202,24 @@ export class Keyring implements KeyringInstance {
? [type]
: type;
if (!['ed25519', 'sr25519', 'ecdsa', 'ethereum'].includes(cryptoType)) {
throw new Error(`Unknown crypto type ${cryptoType}`);
}
// The guard that matters most: JSON arrives from a file the user chose, and a
// polkadot{.js} backup of an sr25519 account must be refused with a reason,
// not constructed and not reported as an unknown type.
assertKeypairType(cryptoType);
// Here the address and publicKey are 32 bytes and isomorphic. This is why the address field needs to be the public key for ethereum type pairs
const publicKey = isHex(address)
const raw = isHex(address)
? hexToU8a(address)
: this.decodeAddress(address, ignoreChecksum);
const decoded = isHex(encoded)
? hexToU8a(encoded)
: base64Decode(encoded);
return createPair({ toSS58: this.encodeAddress, type: cryptoType as KeypairType }, { publicKey, secretKey: new Uint8Array() }, meta, decoded, encType);
// The account id is a one-way Poseidon2 hash, and the 1952/2592-byte public
// key is inside `decoded`, encrypted until someone supplies a password. So
// carry the account id as an account id: createPair uses it for the address
// while locked, and checks it against the real public key the moment
// `decodePkcs8` produces one.
return createPair({ toSS58: this.encodeAddress, type: cryptoType }, { accountId: raw, publicKey: new Uint8Array(), secretKey: new Uint8Array() }, meta, decoded, encType);
}
/**
@@ -195,6 +227,8 @@ export class Keyring implements KeyringInstance {
* @summary Creates a pair from an explicit publicKey/secreteKey combination
*/
public createFromPair (pair: Keypair, meta: KeyringPair$Meta = {}, type: KeypairType = this.type): KeyringPair {
assertKeypairType(type);
return createPair({ toSS58: this.encodeAddress, type }, pair, meta, null);
}
@@ -203,50 +237,40 @@ export class Keyring implements KeyringInstance {
* @summary Creates a Keypair from an suri
* @description This creates a pair from the suri, but does not add it to the keyring
*/
public createFromUri (_suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, wordlist?: string[]): KeyringPair {
// here we only aut-add the dev phrase if we have a hard-derived path
public createFromUri (_suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, _wordlist?: string[]): KeyringPair {
assertKeypairType(type);
// here we only auto-add the dev phrase if we have a hard-derived path
const suri = _suri.startsWith('//')
? `${DEV_PHRASE}${_suri}`
: _suri;
const { derivePath, password, path, phrase } = keyExtractSuri(suri);
let seed: Uint8Array;
const isPhraseHex = isHex(phrase, 256);
const { derivePath, password, phrase } = keyExtractSuri(suri);
if (isPhraseHex) {
seed = hexToU8a(phrase);
} else {
const parts = phrase.split(' ');
if ([12, 15, 18, 21, 24].includes(parts.length)) {
seed = type === 'ethereum'
? mnemonicToLegacySeed(phrase, '', false, 64)
: mnemonicToMiniSecret(phrase, password, wordlist);
} else {
if (phrase.length > 32) {
throw new Error('specified phrase is not a valid mnemonic and is invalid as a raw seed at > 32 bytes');
}
seed = stringToU8a(phrase.padEnd(32));
// ML-DSA derives from the mnemonic itself along a hardened BIP44 path, not
// from a seed along a junction chain: lattice keys have no public
// derivability, so there is no soft junction to emulate, and the chain's own
// Pair::derive refuses for the same reason.
if (isHex(phrase, 256)) {
// A raw 32-byte seed goes straight into keygen, which is how the
// dev-genesis accounts are defined. Combining one with a derivation path
// is ambiguous (is the seed the master, or already derived?), so refuse
// rather than pick.
if (derivePath) {
throw new Error('A derivation path cannot be combined with a raw seed for post-quantum pairs');
}
return this.createFromPair(dilithiumPairFromSeed(hexToU8a(phrase), type), meta, type);
}
const derived = type === 'ethereum'
? isPhraseHex
? PairFromSeed[type](seed) // for eth, if the private key is provided as suri, it must be derived only once
: hdEthereum(seed, derivePath.substring(1))
: keyFromPath(PairFromSeed[type](seed), path, type);
return createPair({ toSS58: this.encodeAddress, type }, derived, meta, null);
return this.createFromPair(
dilithiumPairFromMnemonic(phrase, password || '', dilithiumPathFromSuri(type, derivePath), type),
meta,
type
);
}
/**
* @name encodeAddress
* @description Encodes the input into an ss58 representation
*/
public encodeAddress = (address: Uint8Array | string, ss58Format?: number): string => {
return this.type === 'ethereum'
? ethereumEncode(address)
: encodeAddress(address, ss58Format ?? this.#ss58);
return encodeAddress(address, ss58Format ?? this.#ss58);
};
/**
+1 -1
View File
@@ -3,4 +3,4 @@
// Do not edit, auto-generated by @polkadot/dev
export const packageInfo = { name: '@polkadot/keyring', path: 'auto', type: 'auto', version: '14.0.3' };
export const packageInfo = { name: '@polkadot/keyring', path: 'auto', type: 'auto', version: '14.0.3-quantus.3' };
-26
View File
@@ -1,26 +0,0 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { createTestPairs } from '../testingPairs.js';
const keyring = createTestPairs({ type: 'ed25519' }, false);
describe('decode', (): void => {
it('fails when no data provided', (): void => {
expect(
(): void => keyring.alice.decodePkcs8()
).toThrow(/No encrypted data available/);
});
it('returns correct publicKey from encoded', (): void => {
const PASS = 'testing';
expect(
(): void => keyring.alice.decodePkcs8(
PASS, keyring.alice.encodePkcs8(PASS)
)
).not.toThrow();
});
});
+30 -1
View File
@@ -16,8 +16,20 @@ const SEED_OFFSET = PAIR_HDR.length;
* For divisor/headers, don't rely on the magic being static. These will
* change between generations, aka with the long-awaited 4th generation
* of the format. The external decode interface is the only way to use and decode these.
*
* `secretLength` exists for keys that are not 32 or 64 bytes. The body is
* `HDR ‖ secretKey ‖ DIV ‖ publicKey`, and upstream located the divider by
* trying the two lengths every curve scheme uses. An ML-DSA secret is 4032 or
* 4896 bytes, so neither offset matches and the decode throws "Invalid encoding
* divider found in body" — a stored account that cannot be read back.
*
* The caller passes the length rather than this function searching for the
* divider. Searching would work almost always and fail catastrophically when it
* did not: PAIR_DIV is five bytes, so a 4032-byte secret contains a false match
* about once in 270 million keys, and the result would be a silently wrong key
* rather than an error. The caller knows the type, so it knows the length.
**/
export function decodePair (passphrase?: string, encrypted?: Uint8Array | null, _encType?: EncryptedJsonEncoding | EncryptedJsonEncoding[]): { publicKey: Uint8Array; secretKey: Uint8Array } {
export function decodePair (passphrase?: string, encrypted?: Uint8Array | null, _encType?: EncryptedJsonEncoding | EncryptedJsonEncoding[], secretLength?: number): { publicKey: Uint8Array; secretKey: Uint8Array } {
const encType = Array.isArray(_encType) || _encType === undefined
? _encType
: [_encType];
@@ -29,6 +41,23 @@ export function decodePair (passphrase?: string, encrypted?: Uint8Array | null,
throw new Error('Invalid encoding header found in body');
}
// an explicitly-sized secret (ML-DSA); there is one offset and it either
// matches or the blob is not what the caller said it was
if (secretLength) {
const divOffset = SEED_OFFSET + secretLength;
if (!u8aEq(decrypted.subarray(divOffset, divOffset + PAIR_DIV.length), PAIR_DIV)) {
throw new Error('Invalid encoding divider found in body');
}
return {
// the public key is the remainder: its length varies by scheme and the
// body ends here, so there is nothing to read past it
publicKey: decrypted.subarray(divOffset + PAIR_DIV.length),
secretKey: decrypted.subarray(SEED_OFFSET, divOffset)
};
}
// setup for generation 3 format
let secretKey = decrypted.subarray(SEED_OFFSET, SEED_OFFSET + SEC_LENGTH);
let divOffset = SEED_OFFSET + SEC_LENGTH;
@@ -0,0 +1,91 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { contextForSpec, Scheme, sizes } from '@quantus/crypto';
import { dilithiumVerify } from '@polkadot/util-crypto';
import { Keyring } from '../keyring.js';
// crystal_alice, from the chain's dev genesis: a raw 32-byte seed straight into
// ML-DSA-87 keygen, no HD derivation. The expected address is what
// `quantus developer create-test-wallets` prints — an independent implementation,
// not this one.
const ALICE_ADDRESS = 'qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7';
const MESSAGE = new Uint8Array([0x74, 0x68, 0x65, 0x20, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64]);
describe('dilithium pairs', (): void => {
const keyring = new Keyring({ ss58Format: 189, type: 'dilithium87' });
const alice = keyring.addFromSeed(new Uint8Array(32), { name: 'crystal_alice' }, 'dilithium87');
const context = contextForSpec(148);
const s87 = sizes(Scheme.MlDsa87);
const s65 = sizes(Scheme.MlDsa65);
it('derives the address quantus-cli derives', (): void => {
expect(alice.address).toEqual(ALICE_ADDRESS);
});
// The assumption this whole fork exists to break. Everywhere else in this
// package `addressRaw` and `publicKey` are the same 32 bytes.
it('has an addressRaw that is not the publicKey', (): void => {
expect(alice.addressRaw.length).toEqual(32);
expect(alice.publicKey.length).toEqual(s87.publicKey);
});
it('signs to the wire form the runtime reads', (): void => {
const signature = alice.sign(MESSAGE, { context });
expect(signature.length).toEqual(s87.signatureWithPublicKey);
});
// `withType` prepends the signature enum's variant index. For the curve types
// that enum is Substrate's MultiSignature; here it is the runtime's
// DilithiumSignatureScheme, reached by the same mechanism.
it('prepends the chain variant byte with withType', (): void => {
const signed87 = alice.sign(MESSAGE, { context, withType: true });
const k65 = keyring.addFromSeed(new Uint8Array(32).fill(9), {}, 'dilithium65');
const signed65 = k65.sign(MESSAGE, { context, withType: true });
expect(signed87[0]).toEqual(0);
expect(signed87.length).toEqual(s87.signatureWithPublicKey + 1);
expect(signed65[0]).toEqual(1);
expect(signed65.length).toEqual(s65.signatureWithPublicKey + 1);
});
it('verifies against the account id', (): void => {
const signature = alice.sign(MESSAGE, { context });
expect(dilithiumVerify(MESSAGE, signature, alice.addressRaw, 'dilithium87', context)).toEqual(true);
});
// The spec-148 boundary. A signature made under the wrong context is
// cryptographically valid and rejected by the chain, so this is the only place
// it can be caught locally.
it('does not verify under the wrong context', (): void => {
const signature = alice.sign(MESSAGE, { context });
expect(dilithiumVerify(MESSAGE, signature, alice.addressRaw, 'dilithium87', contextForSpec(147))).toEqual(false);
});
// Verification checks the embedded public key hashes to the account being
// verified against, not merely that the signature is valid for *some* key.
it('does not verify another account signature', (): void => {
const bob = keyring.addFromSeed(new Uint8Array(32).fill(1), {}, 'dilithium87');
const signature = bob.sign(MESSAGE, { context });
expect(dilithiumVerify(MESSAGE, signature, alice.addressRaw, 'dilithium87', context)).toEqual(false);
});
it('refuses to sign without a context', (): void => {
expect(() => alice.sign(MESSAGE)).toThrow(/signing context is required/);
});
// There is no ML-DSA VRF, and faking one from an ordinary signature would
// produce output that looks like a VRF proof and cannot be verified as one.
it('refuses to VRF sign', (): void => {
expect(() => alice.vrfSign(MESSAGE)).toThrow(/not available for dilithium87/);
});
});
@@ -0,0 +1,97 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { dilithiumPath } from '@polkadot/util-crypto';
import { Keyring } from '../keyring.js';
// The well-known Substrate development phrase — public by design, so pinning it
// commits no secret. Any account it derives is assumed compromised.
const DEV_PHRASE = 'bottom drive obey lake curtain smoke basket hold race lonely fit walk';
// What `quantus wallet import --mnemonic-file <DEV_PHRASE> --scheme <s>` prints.
// An independent implementation, not this one.
const EXPECT_65 = 'qzq29m9WvneDAeXbtgueKCREtNe1rVVs6bXSMLmjr6shqvwq6';
const EXPECT_87 = 'qzjrYTUnnE5NduTZKxe9dESCMTZg7nTueKM3bwhnkRdD1iYV4';
// crystal_alice — a raw 32-byte seed straight into keygen, no derivation.
const ALICE = 'qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7';
describe('dilithium derivation', (): void => {
const keyring = new Keyring({ ss58Format: 189, type: 'dilithium65' });
it('builds the paths quantus-cli uses', (): void => {
expect(dilithiumPath('dilithium65')).toEqual("m/44'/189189'/0'/0'/1'");
expect(dilithiumPath('dilithium87')).toEqual("m/44'/189189'/0'/0'/0'");
expect(dilithiumPath('dilithium65', 3)).toEqual("m/44'/189189'/3'/0'/1'");
});
// The whole derivation chain at once: BIP39 to a 64-byte seed (not Substrate's
// mnemonicToMiniSecret), the HMAC-SHA512 walk keyed with "Dilithium seed", the
// trailing hardened index carrying the scheme, and the Poseidon2 account hash.
it('derives what quantus-cli derives, both schemes', (): void => {
expect(keyring.createFromUri(DEV_PHRASE, {}, 'dilithium65').address).toEqual(EXPECT_65);
expect(keyring.createFromUri(DEV_PHRASE, {}, 'dilithium87').address).toEqual(EXPECT_87);
});
it('defaults to account index 0', (): void => {
expect(keyring.createFromUri(`${DEV_PHRASE}//0`, {}, 'dilithium65').address).toEqual(EXPECT_65);
});
it('derives distinct accounts per index', (): void => {
const zero = keyring.createFromUri(`${DEV_PHRASE}//0`, {}, 'dilithium65').address;
const one = keyring.createFromUri(`${DEV_PHRASE}//1`, {}, 'dilithium65').address;
expect(zero).not.toEqual(one);
});
it('accepts an explicit hardened path', (): void => {
const byIndex = keyring.createFromUri(`${DEV_PHRASE}//2`, {}, 'dilithium65').address;
const byPath = keyring.createFromUri(`${DEV_PHRASE}//m/44'/189189'/2'/0'/1'`, {}, 'dilithium65').address;
expect(byPath).toEqual(byIndex);
});
// The suri syntax was built for curve junctions, where `/foo` is a soft
// derivation over arbitrary bytes. Neither form exists for ML-DSA, and quietly
// reinterpreting one would hand back an address no other tool derives.
it('refuses soft derivation', (): void => {
expect(() => keyring.createFromUri(`${DEV_PHRASE}/0`, {}, 'dilithium65')).toThrow(/Soft derivation is not possible/);
});
it('refuses a named junction', (): void => {
expect(() => keyring.createFromUri(`${DEV_PHRASE}//Alice`, {}, 'dilithium65')).toThrow(/Unsupported derivation path/);
});
it('refuses an unhardened level in an explicit path', (): void => {
expect(() => keyring.createFromUri(`${DEV_PHRASE}//m/44'/189189'/0'/0'/1`, {}, 'dilithium65')).toThrow(/Unhardened derivation/);
});
// A raw seed is already key material. Whether it is the master or something
// already derived is unanswerable, so combining it with a path is refused
// rather than guessed.
it('takes a raw hex seed underived, and refuses to derive from one', (): void => {
const seed = `0x${'00'.repeat(32)}`;
expect(keyring.createFromUri(seed, {}, 'dilithium87').address).toEqual(ALICE);
expect(() => keyring.createFromUri(`${seed}//1`, {}, 'dilithium87')).toThrow(/cannot be combined with a raw seed/);
});
it('honours a BIP39 passphrase', (): void => {
const plain = keyring.createFromUri(DEV_PHRASE, {}, 'dilithium65').address;
const withPass = keyring.createFromUri(`${DEV_PHRASE}///hunter2`, {}, 'dilithium65').address;
expect(plain).not.toEqual(withPass);
});
// Deriving a child *from a pair* is not merely unimplemented here, it is
// impossible: ML-DSA keys are not derivable from one another, and the Quantus
// tree derives each account from the mnemonic independently.
it('refuses to derive from an existing pair', (): void => {
const pair = keyring.createFromUri(DEV_PHRASE, {}, 'dilithium65');
expect(() => pair.derive('//1')).toThrow(/derive from the mnemonic with createFromUri/);
});
});
@@ -0,0 +1,103 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { contextForSpec } from '@quantus/crypto';
import { dilithiumVerify } from '@polkadot/util-crypto';
import { Keyring } from '../keyring.js';
// The address `quantus developer create-test-wallets` prints for crystal_alice.
const ALICE_ADDRESS = 'qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7';
const PASSWORD = 'not a good password';
const MESSAGE = new Uint8Array([0x6a, 0x73, 0x6f, 0x6e]);
describe('dilithium account JSON', (): void => {
const keyring = new Keyring({ ss58Format: 189, type: 'dilithium87' });
const context = contextForSpec(148);
const backup = (type: 'dilithium65' | 'dilithium87', fill = 0) => {
const pair = keyring.addFromSeed(new Uint8Array(32).fill(fill), { name: 'backed up' }, type);
return { json: pair.toJson(PASSWORD), original: pair };
};
// The whole point. Before this worked, an exported Quantus account could not be
// imported by anything — decodePair looked for its divider at one of two fixed
// offsets and an ML-DSA secret is at neither, so restore threw "Invalid
// encoding divider found in body". That is a data-durability bug, not a
// convenience one: it had to land before any build let a user create an account.
it('round-trips an ML-DSA-87 account', (): void => {
const { json, original } = backup('dilithium87');
const restored = keyring.createFromJson(json);
restored.decodePkcs8(PASSWORD);
expect(restored.address).toEqual(original.address);
expect(restored.publicKey).toEqual(original.publicKey);
expect(restored.isLocked).toEqual(false);
});
it('round-trips an ML-DSA-65 account', (): void => {
const { json, original } = backup('dilithium65', 5);
const restored = keyring.createFromJson(json);
restored.decodePkcs8(PASSWORD);
expect(restored.address).toEqual(original.address);
expect(restored.publicKey).toEqual(original.publicKey);
});
// A restored pair is locked, and callers read `pair.address` off it long before
// any password appears. For every other scheme that works because the address
// *is* the public key; here the key is still encrypted, so the account id has
// to be carried as data until decodePkcs8 supplies the real one.
it('reports the same address before and after unlock', (): void => {
const { json } = backup('dilithium87');
const restored = keyring.createFromJson(json);
expect(restored.isLocked).toEqual(true);
expect(restored.address).toEqual(ALICE_ADDRESS);
restored.decodePkcs8(PASSWORD);
expect(restored.address).toEqual(ALICE_ADDRESS);
});
it('restores a pair that can still sign', (): void => {
const { json, original } = backup('dilithium87');
const restored = keyring.createFromJson(json);
restored.decodePkcs8(PASSWORD);
const signature = restored.sign(MESSAGE, { context });
expect(dilithiumVerify(MESSAGE, signature, original.addressRaw, 'dilithium87', context)).toEqual(true);
});
it('fails cleanly on a wrong password', (): void => {
const { json } = backup('dilithium87');
const restored = keyring.createFromJson(json);
expect(() => restored.decodePkcs8('wrong')).toThrow();
// and must not have half-applied anything
expect(restored.isLocked).toEqual(true);
expect(restored.address).toEqual(ALICE_ADDRESS);
});
// For every other scheme a tampered `address` field cannot decode at all,
// because the address is the public key. Here it decodes perfectly and yields a
// pair reporting an address its key does not control — a user would see someone
// else's address in their own wallet and believe they held it.
it('rejects JSON whose address does not match its key', (): void => {
const { json } = backup('dilithium87');
const other = keyring.addFromSeed(new Uint8Array(32).fill(2), {}, 'dilithium87');
const tampered = { ...json, address: other.address };
const restored = keyring.createFromJson(tampered);
expect(restored.address).toEqual(other.address);
expect(() => restored.decodePkcs8(PASSWORD)).toThrow(/does not match the address/);
});
});
-28
View File
@@ -1,28 +0,0 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { NONCE_LENGTH, SCRYPT_LENGTH } from '@polkadot/util-crypto/json/constants';
import { createTestPairs } from '../testingPairs.js';
import { PAIR_DIV, PAIR_HDR, PUB_LENGTH, SEC_LENGTH } from './defaults.js';
const DECODED_LENGTH = PAIR_DIV.length + PAIR_HDR.length + PUB_LENGTH + SEC_LENGTH;
const ENCODED_LENGTH = 16 + DECODED_LENGTH + NONCE_LENGTH + SCRYPT_LENGTH;
const keyring = createTestPairs({ type: 'ed25519' }, false);
describe('encode', (): void => {
it('returns PKCS8 when no passphrase supplied', (): void => {
expect(
keyring.alice.encodePkcs8()
).toHaveLength(DECODED_LENGTH);
});
it('returns encoded PKCS8 when passphrase supplied', (): void => {
expect(
keyring.alice.encodePkcs8('testing')
).toHaveLength(ENCODED_LENGTH);
});
});
-189
View File
@@ -1,189 +0,0 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { hexToU8a, u8aToHex } from '@polkadot/util';
import { cryptoWaitReady, encodeAddress as toSS58, setSS58Format } from '@polkadot/util-crypto';
import { PAIRSSR25519 } from '../testing.js';
import { createTestPairs } from '../testingPairs.js';
import { createPair } from './index.js';
const keyring = createTestPairs({ type: 'ed25519' }, false);
const TEST_ADDRESS = '0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887';
await cryptoWaitReady();
describe('pair', (): void => {
const SIGNATURE = new Uint8Array([80, 191, 198, 147, 225, 207, 75, 88, 126, 39, 129, 109, 191, 38, 72, 181, 75, 254, 81, 143, 244, 79, 237, 38, 236, 141, 28, 252, 134, 26, 169, 234, 79, 33, 153, 158, 151, 34, 175, 188, 235, 20, 35, 135, 83, 120, 139, 211, 233, 130, 1, 208, 201, 215, 73, 80, 56, 98, 185, 196, 11, 8, 193, 14]);
it('has a publicKey', (): void => {
expect(
keyring.alice.publicKey
).toEqual(
new Uint8Array([209, 114, 167, 76, 218, 76, 134, 89, 18, 195, 43, 160, 168, 10, 87, 174, 105, 171, 174, 65, 14, 92, 203, 89, 222, 232, 78, 47, 68, 50, 219, 79])
);
expect(
keyring.alice.addressRaw
).toEqual(
new Uint8Array([209, 114, 167, 76, 218, 76, 134, 89, 18, 195, 43, 160, 168, 10, 87, 174, 105, 171, 174, 65, 14, 92, 203, 89, 222, 232, 78, 47, 68, 50, 219, 79])
);
});
it('allows signing', (): void => {
expect(
keyring.alice.sign(
new Uint8Array([0x61, 0x62, 0x63, 0x64])
)
).toEqual(SIGNATURE);
});
it('validates a correctly signed message', (): void => {
expect(
keyring.alice.verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64]),
SIGNATURE,
keyring.alice.publicKey
)
).toEqual(true);
});
it('fails a correctly signed message (signer changed)', (): void => {
expect(
keyring.alice.verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64]),
SIGNATURE,
keyring.bob.publicKey
)
).toEqual(false);
});
it('fails a correctly signed message (message changed)', (): void => {
expect(
keyring.alice.verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]),
SIGNATURE,
keyring.alice.publicKey
)
).toEqual(false);
});
it('allows vrf sign and verify', (): void => {
const message = new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]);
expect(
keyring.alice.vrfVerify(
message,
keyring.alice.vrfSign(message),
keyring.alice.publicKey
)
).toBe(true);
});
it('fails vrf sign and verify (publicKey changed)', (): void => {
const message = new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]);
expect(
keyring.alice.vrfVerify(
message,
keyring.alice.vrfSign(message),
keyring.bob.publicKey
)
).toBe(false);
});
it('allows setting/getting of meta', (): void => {
keyring.bob.setMeta({ foo: 'bar', something: 'else' });
expect(keyring.bob.meta).toMatchObject({ foo: 'bar', something: 'else' });
keyring.bob.setMeta({ something: 'thing' });
expect(keyring.bob.meta).toMatchObject({ foo: 'bar', something: 'thing' });
});
it('allows encoding of address with different prefixes', (): void => {
expect(keyring.alice.address).toEqual('5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua');
// eslint-disable-next-line deprecation/deprecation
setSS58Format(255);
expect(keyring.alice.address).toEqual('yGHU8YKprxHbHdEv7oUK4rzMZXtsdhcXVG2CAMyC9WhzhjH2k');
// eslint-disable-next-line deprecation/deprecation
setSS58Format(42);
});
it('allows getting public key after decoding', (): void => {
const PASS = 'testing';
const encoded = keyring.alice.encodePkcs8(PASS);
const pair = createPair({ toSS58, type: 'sr25519' }, { publicKey: keyring.alice.publicKey });
pair.decodePkcs8(PASS, encoded);
expect(pair.isLocked).toEqual(false);
});
it('allows derivation on the pair', (): void => {
const alice = createPair({ toSS58, type: 'sr25519' }, { publicKey: hexToU8a(PAIRSSR25519[0].p), secretKey: hexToU8a(PAIRSSR25519[0].s) }, {});
const stash = alice.derive('//stash');
const soft = alice.derive('//funding/0');
expect(stash.publicKey).toEqual(hexToU8a(PAIRSSR25519[1].p));
expect(soft.address).toEqual('5ECQNn7UueWHPFda5qUi4fTmTtyCnPvGnuoyVVSj5CboJh9J');
});
it('fails to sign when locked', (): void => {
const pair = createPair({ toSS58, type: 'sr25519' }, { publicKey: keyring.alice.publicKey });
expect(pair.isLocked).toEqual(true);
expect((): Uint8Array =>
pair.sign(new Uint8Array([0]))
).toThrow('Cannot sign with a locked key pair');
});
describe('ethereum', (): void => {
const PUBLICDERIVED = new Uint8Array([
3, 129, 53, 27, 27, 70, 210, 96,
43, 9, 146, 187, 93, 85, 49, 249,
193, 105, 107, 8, 18, 254, 178, 83,
75, 104, 132, 173, 196, 126, 46, 29,
139
]);
const SECRETDERIVED = new Uint8Array([
7, 13, 195, 17, 115, 0, 1, 25,
24, 226, 107, 2, 23, 105, 69, 204,
21, 195, 213, 72, 207, 73, 253, 132,
24, 217, 127, 147, 175, 105, 158, 70
]);
it('has a valid address from a known public', (): void => {
const pair = createPair({ toSS58, type: 'ethereum' }, { publicKey: hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077') });
expect(pair.address).toEqual(TEST_ADDRESS);
expect(pair.addressRaw).toEqual(hexToU8a(TEST_ADDRESS));
});
it('has a valid address from a known ethereum address (20 length)', (): void => {
const pair = createPair({ toSS58, type: 'ethereum' }, { publicKey: new Uint8Array([75, 32, 205, 127, 248, 119, 52, 31, 46, 171, 170, 23, 158, 23, 46, 108, 95, 180, 186, 168]), secretKey: new Uint8Array([]) });
expect(pair.address.toLowerCase()).toEqual('0x4b20cd7ff877341f2eabaa179e172e6c5fb4baa8');
expect(pair.addressRaw).toEqual(hexToU8a('0x4b20cd7ff877341f2eabaa179e172e6c5fb4baa8'));
});
it('converts to json', (): void => {
const pair = createPair({ toSS58, type: 'ethereum' }, { publicKey: PUBLICDERIVED, secretKey: SECRETDERIVED });
const json = pair.toJson('password');
expect(json.encoding).toEqual({
content: ['pkcs8', 'ethereum'],
type: ['scrypt', 'xsalsa20-poly1305'],
version: '3'
});
expect(json.address).toEqual(u8aToHex(PUBLICDERIVED));
});
});
});
+83 -97
View File
@@ -1,12 +1,12 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { EncryptedJsonEncoding, Keypair, KeypairType } from '@polkadot/util-crypto/types';
import type { EncryptedJsonEncoding, KeypairType } from '@polkadot/util-crypto/types';
import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta, SignOptions } from '../types.js';
import type { PairInfo } from './types.js';
import { objectSpread, u8aConcat, u8aEmpty, u8aEq, u8aToHex, u8aToU8a } from '@polkadot/util';
import { blake2AsU8a, ed25519PairFromSeed as ed25519FromSeed, ed25519Sign, ethereumEncode, keccakAsU8a, keyExtractPath, keyFromPath, secp256k1Compress, secp256k1Expand, secp256k1PairFromSeed as secp256k1FromSeed, secp256k1Sign, signatureVerify, sr25519PairFromSeed as sr25519FromSeed, sr25519Sign, sr25519VrfSign, sr25519VrfVerify } from '@polkadot/util-crypto';
import { objectSpread, u8aConcat, u8aEmpty, u8aEq, u8aToU8a } from '@polkadot/util';
import { dilithiumAccountFromPublic, dilithiumSign, dilithiumSizes, dilithiumVerify } from '@polkadot/util-crypto';
import { decodePair } from './decode.js';
import { encodePair } from './encode.js';
@@ -19,42 +19,38 @@ interface Setup {
const SIG_TYPE_NONE = new Uint8Array();
const TYPE_FROM_SEED = {
ecdsa: secp256k1FromSeed,
ed25519: ed25519FromSeed,
ethereum: secp256k1FromSeed,
sr25519: sr25519FromSeed
// The Quantus runtime's `DilithiumSignatureScheme`: `Dilithium87` is variant 0
// and `Dilithium65` variant 1. Reached through the same `withType` mechanism
// upstream used for Substrate's `MultiSignature`, so nothing above this file
// needs to know which enum it is.
//
// There are no other arms. Upstream's ed25519, sr25519, ecdsa and ethereum were
// removed with their primitives: each falls to Shor's algorithm, and this keyring
// exists to hold keys that do not. quantus/common#6
const TYPE_PREFIX: Record<KeypairType, Uint8Array> = {
dilithium65: new Uint8Array([1]),
dilithium87: new Uint8Array([0])
};
const TYPE_PREFIX = {
ecdsa: new Uint8Array([2]),
ed25519: new Uint8Array([0]),
ethereum: new Uint8Array([2]),
sr25519: new Uint8Array([1])
};
/**
* A post-quantum pair will not sign without being told which context to use.
*
* See `SignOptions.context`. The alternative — defaulting to the extrinsic
* context — produces a valid signature that a spec-147 chain rejects, with
* nothing local able to explain why.
*/
function requireContext (context?: Uint8Array): Uint8Array {
if (!context) {
throw new Error('A signing context is required for post-quantum keypairs; pass options.context (see contextForSpec)');
}
const TYPE_SIGNATURE = {
ecdsa: (m: Uint8Array, p: Partial<Keypair>) => secp256k1Sign(m, p, 'blake2'),
ed25519: ed25519Sign,
ethereum: (m: Uint8Array, p: Partial<Keypair>) => secp256k1Sign(m, p, 'keccak'),
sr25519: sr25519Sign
};
const TYPE_ADDRESS = {
ecdsa: (p: Uint8Array) => p.length > 32 ? blake2AsU8a(p) : p,
ed25519: (p: Uint8Array) => p,
ethereum: (p: Uint8Array) => p.length === 20 ? p : keccakAsU8a(secp256k1Expand(p)),
sr25519: (p: Uint8Array) => p
};
return context;
}
function isLocked (secretKey?: Uint8Array): secretKey is undefined {
return !secretKey || u8aEmpty(secretKey);
}
function vrfHash (proof: Uint8Array, context?: string | Uint8Array, extra?: string | Uint8Array): Uint8Array {
return blake2AsU8a(u8aConcat(context || '', extra || '', proof));
}
/**
* @name createPair
* @summary Creates a keyring pair object
@@ -86,18 +82,29 @@ function vrfHash (proof: Uint8Array, context?: string | Uint8Array, extra?: stri
* an `encoded` property that is assigned with the encoded public key in hex format, and an `encoding`
* property that indicates whether the public key value of the `encoded` property is encoded or not.
*/
export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: PairInfo, meta: KeyringPair$Meta = {}, encoded: Uint8Array | null = null, encTypes?: EncryptedJsonEncoding[]): KeyringPair {
export function createPair ({ toSS58, type }: Setup, { accountId, publicKey, secretKey }: PairInfo, meta: KeyringPair$Meta = {}, encoded: Uint8Array | null = null, encTypes?: EncryptedJsonEncoding[]): KeyringPair {
const decodePkcs8 = (passphrase?: string, userEncoded?: Uint8Array | null): void => {
const decoded = decodePair(passphrase, userEncoded || encoded, encTypes);
// ML-DSA secrets are 4032/4896 bytes, which is neither of the two lengths
// decodePair would otherwise try, so it has to be told.
const decoded = decodePair(passphrase, userEncoded || encoded, encTypes, dilithiumSizes(type).secretKey);
if (decoded.secretKey.length === 64) {
publicKey = decoded.publicKey;
secretKey = decoded.secretKey;
} else {
const pair = TYPE_FROM_SEED[type](decoded.secretKey);
publicKey = decoded.publicKey;
secretKey = decoded.secretKey;
publicKey = pair.publicKey;
secretKey = pair.secretKey;
// The public key has only now arrived. If the pair was constructed from JSON
// it has been reporting an address carried as data since then, so check the
// two agree.
//
// This is not a paranoid check. A JSON file whose `address` field has been
// edited decodes perfectly and would yield a pair reporting an address its
// key does not control: a user would see an attacker's address in their own
// wallet and believe they held it.
if (accountId) {
if (!u8aEq(dilithiumAccountFromPublic(publicKey), accountId)) {
throw new Error('Decoded public key does not match the address in the account JSON');
}
accountId = undefined;
}
};
@@ -110,24 +117,19 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P
return encoded;
};
const encodeAddress = (): string => {
const raw = TYPE_ADDRESS[type](publicKey);
return type === 'ethereum'
? ethereumEncode(raw)
: toSS58(raw);
};
// While a pair restored from JSON, or added by address, has no public key,
// its account id cannot be computed (it is a one-way Poseidon2 hash of that
// key), so use the one carried alongside. `decodePkcs8` clears it once the
// real key arrives and has been checked against it.
const addressRawOf = (): Uint8Array =>
accountId ?? dilithiumAccountFromPublic(publicKey);
return {
get address (): string {
return encodeAddress();
return toSS58(addressRawOf());
},
get addressRaw (): Uint8Array {
const raw = TYPE_ADDRESS[type](publicKey);
return type === 'ethereum'
? raw.slice(-20)
: raw;
return addressRawOf();
},
get isLocked (): boolean {
return isLocked(secretKey);
@@ -143,17 +145,12 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P
},
// eslint-disable-next-line sort-keys
decodePkcs8,
derive: (suri: string, meta?: KeyringPair$Meta): KeyringPair => {
if (type === 'ethereum') {
throw new Error('Unable to derive on this keypair');
} else if (isLocked(secretKey)) {
throw new Error('Cannot derive on a locked keypair');
}
const { path } = keyExtractPath(suri);
const derived = keyFromPath({ publicKey, secretKey }, path, type);
return createPair({ toSS58, type }, derived, meta, null);
derive: (_suri: string, _meta?: KeyringPair$Meta): KeyringPair => {
// Not "not implemented" — not possible. A child here would have to come
// from this pair's key material, and ML-DSA keys are not derivable from
// one another at all; the Quantus tree derives every account from the
// mnemonic independently. So the caller needs the mnemonic, not this pair.
throw new Error(`Unable to derive from an existing ${type} pair; derive from the mnemonic with createFromUri instead`);
},
encodePkcs8: (passphrase?: string): Uint8Array => {
return recode(passphrase);
@@ -173,48 +170,37 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P
options.withType
? TYPE_PREFIX[type]
: SIG_TYPE_NONE,
TYPE_SIGNATURE[type](u8aToU8a(message), { publicKey, secretKey })
dilithiumSign(u8aToU8a(message), { publicKey, secretKey }, type, requireContext(options.context))
);
},
toJson: (passphrase?: string): KeyringPair$Json => {
// NOTE: For ecdsa and ethereum, the publicKey cannot be extracted from the address. For these
// pass the hex-encoded publicKey through to the address portion of the JSON (before decoding)
// unless the publicKey is already an address
const address = ['ecdsa', 'ethereum'].includes(type)
? publicKey.length === 20
? u8aToHex(publicKey)
: u8aToHex(secp256k1Compress(publicKey))
: encodeAddress();
return pairToJson(type, { address, meta }, recode(passphrase), !!passphrase);
return pairToJson(type, { address: toSS58(addressRawOf()), meta }, recode(passphrase), !!passphrase);
},
unlock: (passphrase?: string): void => {
return decodePkcs8(passphrase);
},
verify: (message: string | Uint8Array, signature: string | Uint8Array, signerPublic: string | Uint8Array): boolean => {
return signatureVerify(message, signature, TYPE_ADDRESS[type](u8aToU8a(signerPublic))).isValid;
verify: (message: string | Uint8Array, signature: string | Uint8Array, signerPublic: string | Uint8Array, context: Uint8Array = new Uint8Array()): boolean => {
const pk = u8aToU8a(signerPublic);
const sig = u8aToU8a(signature);
const { signature: sigLength } = dilithiumSizes(type);
// Accept a bare signature or the chain's `signature ‖ publicKey` form; the
// verifier needs the latter, and the public key is at hand either way.
const sigWithPublic = sig.length === sigLength
? u8aConcat(sig, pk)
: sig;
// Unlike signing, a wrong context here cannot produce anything harmful —
// only `false` — so the empty context used for raw bytes is the default.
return dilithiumVerify(u8aToU8a(message), sigWithPublic, dilithiumAccountFromPublic(pk), type, context);
},
vrfSign: (message: string | Uint8Array, context?: string | Uint8Array, extra?: string | Uint8Array): Uint8Array => {
if (isLocked(secretKey)) {
throw new Error('Cannot sign with a locked key pair');
}
if (type === 'sr25519') {
return sr25519VrfSign(message, { secretKey }, context, extra);
}
const proof = TYPE_SIGNATURE[type](u8aToU8a(message), { publicKey, secretKey });
return u8aConcat(vrfHash(proof, context, extra), proof);
vrfSign: (): Uint8Array => {
// There is no ML-DSA VRF, and faking one out of an ordinary signature needs
// uniqueness ML-DSA does not have in the form required.
throw new Error(`VRF signing is not available for ${type}`);
},
vrfVerify: (message: string | Uint8Array, vrfResult: Uint8Array, signerPublic: Uint8Array | string, context?: string | Uint8Array, extra?: string | Uint8Array): boolean => {
if (type === 'sr25519') {
return sr25519VrfVerify(message, vrfResult, publicKey, context, extra);
}
const result = signatureVerify(message, u8aConcat(TYPE_PREFIX[type], vrfResult.subarray(32)), TYPE_ADDRESS[type](u8aToU8a(signerPublic)));
return result.isValid && u8aEq(vrfResult.subarray(0, 32), vrfHash(vrfResult.subarray(32), context, extra));
vrfVerify: (): boolean => {
throw new Error(`VRF verification is not available for ${type}`);
}
};
}
+2 -2
View File
@@ -18,7 +18,7 @@ const json: KeyringPair$Json = {
address,
encoded: '',
encoding: {
content: ['pkcs8', 'ed25519'],
content: ['pkcs8', 'dilithium65'],
type: 'none',
version: '0'
},
@@ -46,7 +46,7 @@ const pair: KeyringPair = {
new Uint8Array(64),
toJson: (_passphrase?: string): KeyringPair$Json =>
json,
type: 'ed25519',
type: 'dilithium65',
unlock: (_passphrase?: string): void =>
undefined,
verify: (_message: Uint8Array, _signature: Uint8Array): boolean =>
-42
View File
@@ -1,42 +0,0 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { createTestPairs } from '../testingPairs.js';
const keyring = createTestPairs({ type: 'ed25519' }, false);
describe('toJson', (): void => {
it('creates an unencoded output with no passphrase', (): void => {
expect(
keyring.alice.toJson()
).toMatchObject({
address: '5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua',
encoded: 'MFMCAQEwBQYDK2VwBCIEIEFsaWNlICAgICAgICAgICAgICAgICAgICAgICAgICAg0XKnTNpMhlkSwyugqApXrmmrrkEOXMtZ3uhOL0Qy20+hIwMhANFyp0zaTIZZEsMroKgKV65pq65BDlzLWd7oTi9EMttP',
encoding: {
content: ['pkcs8', 'ed25519'],
type: ['none'],
version: '3'
},
meta: {
isTesting: true,
name: 'alice'
}
});
});
it('creates an encoded output with passphrase', (): void => {
const json = keyring.alice.toJson('testing');
expect(json.encoded).toHaveLength(268);
expect(json).toMatchObject({
address: '5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua',
encoding: {
content: ['pkcs8', 'ed25519'],
type: ['scrypt', 'xsalsa20-poly1305'],
version: '3'
}
});
});
});
+15
View File
@@ -2,6 +2,21 @@
// SPDX-License-Identifier: Apache-2.0
export interface PairInfo {
/**
* The 32-byte account id, when it cannot be derived from `publicKey`.
*
* Only ML-DSA pairs need this, and only while locked. Everywhere else the
* address is a cheap function of the public key, so a pair restored from JSON
* can report its address before anyone supplies a password. A Quantus account
* id is a one-way Poseidon2 hash, and the public key lives inside the
* encrypted blob — so between `createFromJson` and `decodePkcs8` there is
* nothing to compute it from, and callers read `pair.address` in exactly that
* window.
*
* Carried as data rather than derived, and checked against the real public key
* once that arrives.
*/
accountId?: Uint8Array | undefined;
publicKey: Uint8Array;
secretKey?: Uint8Array | undefined;
seed?: Uint8Array | null;
-47
View File
@@ -1,47 +0,0 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { u8aToHex } from '@polkadot/util';
import { cryptoWaitReady, ed25519PairFromSeed, encodeAddress as toSS58, randomAsU8a, secp256k1PairFromSeed, sr25519PairFromSeed } from '@polkadot/util-crypto';
import { createPair } from './index.js';
const MESSAGE = 'this is a test message';
const CONTEXT = 'some context';
await cryptoWaitReady();
const ecdsa = createPair({ toSS58, type: 'ecdsa' }, secp256k1PairFromSeed(randomAsU8a()));
const ed25519 = createPair({ toSS58, type: 'ed25519' }, ed25519PairFromSeed(randomAsU8a()));
const sr25519 = createPair({ toSS58, type: 'sr25519' }, sr25519PairFromSeed(randomAsU8a()));
describe('vrf', (): void => {
it('has deterministic signature values for ecdsa', (): void => {
const sig1 = ecdsa.vrfSign(MESSAGE, CONTEXT);
const sig2 = ecdsa.vrfSign(MESSAGE, CONTEXT);
expect(u8aToHex(sig1)).toEqual(u8aToHex(sig2));
expect(ecdsa.vrfVerify(MESSAGE, sig1, ecdsa.publicKey, CONTEXT)).toEqual(true);
expect(ecdsa.vrfVerify(MESSAGE, sig2, ecdsa.publicKey, CONTEXT)).toEqual(true);
});
it('has deterministic signature values for ed25519', (): void => {
const sig1 = ed25519.vrfSign(MESSAGE, CONTEXT);
const sig2 = ed25519.vrfSign(MESSAGE, CONTEXT);
expect(u8aToHex(sig1)).toEqual(u8aToHex(sig2));
expect(ed25519.vrfVerify(MESSAGE, sig1, ed25519.publicKey, CONTEXT)).toEqual(true);
expect(ed25519.vrfVerify(MESSAGE, sig2, ed25519.publicKey, CONTEXT)).toEqual(true);
});
it('has deterministic signature values for sr25519', (): void => {
const sig1 = sr25519.vrfSign(MESSAGE, CONTEXT);
const sig2 = sr25519.vrfSign(MESSAGE, CONTEXT);
expect(u8aToHex(sig1.slice(0, 32))).toEqual(u8aToHex(sig2.slice(0, 32)));
expect(sr25519.vrfVerify(MESSAGE, sig1, sr25519.publicKey, CONTEXT)).toEqual(true);
expect(sr25519.vrfVerify(MESSAGE, sig2, sr25519.publicKey, CONTEXT)).toEqual(true);
});
});
-109
View File
@@ -1,109 +0,0 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
// From https://github.com/paritytech/substrate/wiki/Secret-URI-Test-Vectors
import type { KeypairType } from '@polkadot/util-crypto/types';
import { u8aToHex } from '@polkadot/util';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import Keyring from './index.js';
const PHRASE = 'bottom drive obey lake curtain smoke basket hold race lonely fit walk';
const ETHEREUM_PHRASE = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
const TESTS = {
ecdsa: [
{
pk: '0x020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1',
ss: '5C7C2Z5sWbytvHpuLTvzKunnnRwQxft1jiqrLD5rhucQ5S9X',
uri: `${PHRASE}//Alice`
}
],
ethereum: [
{
pk: '0x0381351b1b46d2602b0992bb5d5531f9c1696b0812feb2534b6884adc47e2e1d8b',
ss: '0x31ea8795EE32D782C8ff41a5C68Dcbf0F5B27f6d',
uri: `${ETHEREUM_PHRASE}/m/44'/60'/0'/0/0`
},
{
pk: '0x02509540919faacf9ab52146c9aa40db68172d83777250b28e4679176e49ccdd9f',
ss: '0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac',
uri: `${PHRASE}/m/44'/60'/0'/0/0`
},
{
pk: '0x033bc19e36ff1673910575b6727a974a9abd80c9a875d41ab3e2648dbfb9e4b518',
ss: '0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0',
uri: `${PHRASE}/m/44'/60'/0'/0/1`
}
],
sr25519: [
{
pk: '0x46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a',
ss: '5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV',
uri: PHRASE
},
{
pk: '0xb69355deefa7a8f33e9297f5af22e680f03597a99d4f4b1c44be47e7a2275802',
ss: '5GC6LfpV352HtJPySfAecb5JdePtf4R9Vq49NUU8RhzgBqgq',
uri: `${PHRASE}///password`
},
{
pk: '0x40b9675df90efa6069ff623b0fdfcf706cd47ca7452a5056c7ad58194d23440a',
ss: '5DXZzrDxHbkQov4QBAY4TjpwnHCMrKXkomTnKSw8UArBEY5v',
uri: `${PHRASE}/foo`
},
{
pk: '0x547d4a55642ec7ebadc0bd29b6e570b8c926059b3c0655d4948075e9a7e6f31e',
ss: '5DyV6fZuvPemWrUqBgWwTSgoV86w6xms3KhkFU6cQcWxU8eP',
uri: `${PHRASE}//foo`
},
{
pk: '0x3841947ffcde6f5fef26fb68b59bb8665637e30e32ec2051f99cf6b9c674fe09',
ss: '5DLU27is5iViNopQb2KxsTyPx6j4vCu8X3sk3j3NNLkPCqKM',
uri: `${PHRASE}//foo/bar`
},
{
pk: '0xdc142f7476a7b0aa262aeccf207f1d18daa90762db393006741e8a31f39dbc53',
ss: '5H3GPTqDSpjkfDwbHy12PD6BWm8jvGSX4xYC8UMprHpTPcRg',
uri: `${PHRASE}/foo//bar`
},
{
pk: '0xa2e56b06407a6d1e819d2fc33fa0ec604b29c2e868b70b3696bb049b8725934b',
ss: '5FkHmNgbg64MwStgCyDi2Uw3ufFu11mqQgmWT9uwK4Lghvpv',
uri: `${PHRASE}//foo/bar//42/69`
},
{
pk: '0x0e0d24e3e1ff2c07f269c99e2e0df8681fda1851ac42fc846ca2daaa90cd8f14',
ss: '5CP8S23JBNXYNpJsL7ESPJBNnUZE6itcfM4EnDxEhaVEU6dT',
uri: `${PHRASE}//foo/bar//42/69///password`
},
{
pk: '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d',
ss: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
uri: `${PHRASE}//Alice`
}
]
};
await cryptoWaitReady();
describe('keyring.addFromUri', (): void => {
for (const [type, tests] of Object.entries(TESTS)) {
const keyring = new Keyring({ type: type as KeypairType });
describe(`${type}`, (): void => {
tests.forEach(({ pk, ss, uri }): void => {
it(`creates ${uri}`, (): void => {
const pair = keyring.addFromUri(uri, {}, type as KeypairType);
expect(u8aToHex(pair.publicKey)).toEqual(pk);
expect(pair.address).toEqual(ss);
});
});
});
}
});
+33 -136
View File
@@ -1,151 +1,48 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@polkadot/util/types';
import type { KeypairType } from '@polkadot/util-crypto/types';
import type { KeyringInstance, KeyringOptions } from './types.js';
import { hexToU8a } from '@polkadot/util';
import { createPair } from './pair/index.js';
import { Keyring } from './keyring.js';
interface PairDef {
name?: string;
p: HexString;
s: HexString;
seed?: string;
type: KeypairType
}
// NOTE This is not great since we have the secretKey here explicitly, but a testing
// keyring is for testing - what happens is that in most cases the keyring is initialises
// before anything else. Since the sr25519 crypto is async, this creates problems with
// adding the keys when only the keyring is used.
export const PAIRSSR25519: PairDef[] = [
{
p: '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d',
s: '0x98319d4ff8a9508c4bb0cf0b5a78d760a0b2082c02775e6e82370816fedfff48925a225d97aa00682d6a59b95b18780c10d7032336e88f3442b42361f4a66011', // nosemgrep
seed: 'Alice',
type: 'sr25519'
},
{
p: '0xbe5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f',
s: '0xe8da6c9d810e020f5e3c7f5af2dea314cbeaa0d72bc6421e92c0808a0c584a6046ab28e97c3ffc77fe12b5a4d37e8cd4afbfebbf2391ffc7cb07c0f38c023efd', // nosemgrep
seed: 'Alice//stash',
type: 'sr25519'
},
{
p: '0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48',
s: '0x081ff694633e255136bdb456c20a5fc8fed21f8b964c11bb17ff534ce80ebd5941ae88f85d0c1bfc37be41c904e1dfc01de8c8067b0d6d5df25dd1ac0894a325', // nosemgrep
seed: 'Bob',
type: 'sr25519'
},
{
p: '0xfe65717dad0447d715f660a0a58411de509b42e6efb8375f562f58a554d5860e',
s: '0xc006507cdfc267a21532394c49ca9b754ca71de21e15a1cdf807c7ceab6d0b6c3ed408d9d35311540dcd54931933e67cf1ea10d46f75408f82b789d9bd212fde', // nosemgrep
seed: 'Bob//stash',
type: 'sr25519'
},
{
p: '0x90b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe22',
s: '0xa8f2d83016052e5d6d77b2f6fd5d59418922a09024cda701b3c34369ec43a7668faf12ff39cd4e5d92bb773972f41a7a5279ebc2ed92264bed8f47d344f8f18c', // nosemgrep
seed: 'Charlie',
type: 'sr25519'
},
{
p: '0x306721211d5404bd9da88e0204360a1a9ab8b87c66c1bc2fcdd37f3c2222cc20',
s: '0x20e05482ca4677e0edbc58ae9a3a59f6ed3b1a9484ba17e64d6fe8688b2b7b5d108c4487b9323b98b11fe36cb301b084e920f7b7895536809a6d62a451b25568', // nosemgrep
seed: 'Dave',
type: 'sr25519'
},
{
p: '0xe659a7a1628cdd93febc04a4e0646ea20e9f5f0ce097d9a05290d4a9e054df4e',
s: '0x683576abfd5dc35273e4264c23095a1bf21c14517bece57c7f0cc5c0ed4ce06a3dbf386b7828f348abe15d76973a72009e6ef86a5c91db2990cb36bb657c6587', // nosemgrep
seed: 'Eve',
type: 'sr25519'
},
{
p: '0x1cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c',
s: '0xb835c20f450079cf4f513900ae9faf8df06ad86c681884122c752a4b2bf74d4303e4f21bc6cc62bb4eeed5a9cce642c25e2d2ac1464093b50f6196d78e3a7426', // nosemgrep
seed: 'Ferdie',
type: 'sr25519'
}
];
export const PAIRSETHEREUM: PairDef[] = [
{
name: 'Alith',
p: '0x02509540919faacf9ab52146c9aa40db68172d83777250b28e4679176e49ccdd9f',
s: '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133', // nosemgrep
type: 'ethereum'
},
{
name: 'Baltathar',
p: '0x033bc19e36ff1673910575b6727a974a9abd80c9a875d41ab3e2648dbfb9e4b518',
s: '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b', // nosemgrep
type: 'ethereum'
},
{
name: 'Charleth',
p: '0x0234637bdc0e89b5d46543bcbf8edff329d2702bc995e27e9af4b1ba009a3c2a5e',
s: '0x0b6e18cafb6ed99687ec547bd28139cafdd2bffe70e6b688025de6b445aa5c5b', // nosemgrep
type: 'ethereum'
},
{
name: 'Dorothy',
p: '0x02a00d60b2b408c2a14c5d70cdd2c205db8985ef737a7e55ad20ea32cc9e7c417c',
s: '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68', // nosemgrep
type: 'ethereum'
},
{
name: 'Ethan',
p: '0x025cdc005b752651cd3f728fb9192182acb3a9c89e19072cbd5b03f3ee1f1b3ffa',
s: '0x7dce9bc8babb68fec1409be38c8e1a52650206a7ed90ff956ae8a6d15eeaaef4', // nosemgrep
type: 'ethereum'
},
{
name: 'Faith',
p: '0x037964b6c9d546da4646ada28a99e34acaa1d14e7aba861a9055f9bd200c8abf74',
s: '0xb9d2ea9a615f3165812e8d44de0d24da9bbd164b65c4f0573e1ce2c8dbd9c8df', // nosemgrep
type: 'ethereum'
}
];
function createMeta (name?: string, seed?: string) {
if (!name && !seed) {
throw new Error('Testing pair should have either a name or a seed');
}
return {
isTesting: true,
name: name || seed?.replace('//', '_').toLowerCase()
};
interface DevAccount {
name: string;
/** The byte the 32-byte seed is filled with. */
seed: number;
}
/**
* @name testKeyring
* @summary Create an instance of Keyring pre-populated with locked test accounts
* @description The test accounts (i.e. alice, bob, dave, eve, ferdie)
* are available on the dev chain and each test account is initialized with DOT funds.
* The accounts a Quantus dev chain endows at genesis.
*
* From `chain:primitives/dilithium-crypto/src/pair.rs`: ML-DSA-87 keys generated
* directly from a 32-byte seed filled with one byte, with no HD derivation. Their
* account ids are pinned against `quantus-cli` in `@quantus/crypto`'s tests
* (`dev_account_ids_match_the_cli`), e.g. crystal_alice is
* `qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7`.
*
* They replace upstream's sr25519 Alice…Ferdie and Ethereum Alith…Faith, which
* this keyring can no longer hold. quantus/common#6
*/
export function createTestKeyring (options: KeyringOptions = {}, isDerived = true): KeyringInstance {
const keyring = new Keyring(options);
const pairs = options.type === 'ethereum'
? PAIRSETHEREUM
: PAIRSSR25519;
export const DEV_ACCOUNTS: DevAccount[] = [
{ name: 'crystal_alice', seed: 0 },
{ name: 'dilithium_bob', seed: 1 },
{ name: 'crystal_charlie', seed: 2 }
];
for (const { name, p, s, seed, type } of pairs) {
const meta = createMeta(name, seed);
const pair = !isDerived && !name && seed
? keyring.addFromUri(seed, meta, options.type)
: keyring.addPair(
createPair(
{ toSS58: keyring.encodeAddress, type },
{ publicKey: hexToU8a(p), secretKey: hexToU8a(s) },
meta
)
);
/**
* @name testKeyring
* @summary Create an instance of Keyring pre-populated with the dev accounts
* @description crystal_alice, dilithium_bob and crystal_charlie, as a Quantus
* dev chain endows them. They are always ML-DSA-87, whatever `options.type` says,
* because that is what the chain made them; `options` sets the keyring's default
* type and SS58 format for anything added afterwards. `_isDerived` is accepted for
* upstream's signature and ignored: these accounts have no derivation.
*/
export function createTestKeyring (options: KeyringOptions = {}, _isDerived = true): KeyringInstance {
const keyring = new Keyring(options);
for (const { name, seed } of DEV_ACCOUNTS) {
const pair = keyring.addFromSeed(new Uint8Array(32).fill(seed), { isTesting: true, name }, 'dilithium87');
pair.lock = (): void => {
// we don't have lock/unlock functionality here
-79
View File
@@ -1,79 +0,0 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { u8aToHex } from '@polkadot/util';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import Keyring from './index.js';
import { createTestPairs } from './testingPairs.js';
const TEST_ADD = '0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac';
await cryptoWaitReady();
describe('testingPairs', (): void => {
it('creates without failing', (): void => {
expect(
Object.keys(createTestPairs())
).toHaveLength(2 + 0 + 7); // stash, session, pairs
});
it('has the correct address for Alice (non-HDKD)', (): void => {
expect(
createTestPairs({ type: 'ed25519' }, false).alice.address
).toEqual('5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua');
});
it('has the correct address for Alice (HDKD)', (): void => {
expect(
createTestPairs({ type: 'ed25519' }).alice.address
).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
});
it('has the correct address for Alith (Eth)', (): void => {
expect(
createTestPairs({ type: 'ethereum' }).Alith.address
).toEqual(TEST_ADD);
});
it('has the correct address for Alith (Eth), same as obtained by createFromUri', (): void => {
const keyring = new Keyring({ type: 'ethereum' });
const pair = keyring.createFromUri('0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133');
expect(pair?.address).toEqual(TEST_ADD);
});
describe('checks eth test addresses', (): void => {
const ring = createTestPairs({ type: 'ethereum' });
const keyring = new Keyring({ type: 'ethereum' });
// priv keys generated by ganache-cli --mnemonic "bottom drive obey lake curtain smoke basket hold race lonely fit walk"
const privKeys: string[] = ['0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133',
'0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b',
'0x0b6e18cafb6ed99687ec547bd28139cafdd2bffe70e6b688025de6b445aa5c5b',
'0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68',
'0x7dce9bc8babb68fec1409be38c8e1a52650206a7ed90ff956ae8a6d15eeaaef4',
'0xb9d2ea9a615f3165812e8d44de0d24da9bbd164b65c4f0573e1ce2c8dbd9c8df',
'0x96b8a38e12e1a31dee1eab2fffdf9d9990045f5b37e44d8cc27766ef294acf18',
'0x0d6dcaaef49272a5411896be8ad16c01c35d6f8c18873387b71fbc734759b0ab',
'0x4c42532034540267bf568198ccec4cb822a025da542861fcb146a5fab6433ff8',
'0x94c49300a58d576011096bcb006aa06f5a91b34b4383891e8029c21dc39fbb8b'];
// @ts-expect-error We should not delete from the maps, however this is a test
delete ring.nobody;
Object
.keys(ring)
.filter((_, i) => i < 6)
.forEach((testKeyring, i) => {
it(`checks #${i}`, (): void => {
expect(
u8aToHex(ring[testKeyring].publicKey)
).toEqual(
u8aToHex(keyring.createFromUri(privKeys[i]).publicKey)
);
});
});
});
});
+6 -29
View File
@@ -1,7 +1,6 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeypairType } from '@polkadot/util-crypto/types';
import type { KeyringOptions, KeyringPair } from './types.js';
import { nobody } from './pair/nobody.js';
@@ -9,39 +8,17 @@ import { createTestKeyring } from './testing.js';
export interface TestKeyringMap {
nobody: KeyringPair;
[index: string]: KeyringPair;
}
export interface TestKeyringMapSubstrate extends TestKeyringMap {
alice: KeyringPair;
bob: KeyringPair;
charlie: KeyringPair;
dave: KeyringPair;
eve: KeyringPair;
ferdie: KeyringPair;
/** The Quantus dev accounts, by name. See `DEV_ACCOUNTS`. */
export interface TestKeyringMapQuantus extends TestKeyringMap {
crystal_alice: KeyringPair;
crystal_charlie: KeyringPair;
dilithium_bob: KeyringPair;
}
export interface TestKeyringMapEthereum extends TestKeyringMap {
Alith: KeyringPair;
Baltathar: KeyringPair;
Charleth: KeyringPair;
Dorothy: KeyringPair;
Ethan: KeyringPair;
Faith: KeyringPair;
}
export type DetectMap<O extends KeyringOptions | undefined> = DetectPairType<O> extends 'ethereum'
? TestKeyringMapEthereum
: TestKeyringMapSubstrate;
export type DetectPairType<O extends KeyringOptions | undefined> = O extends KeyringOptions
? O['type'] extends KeypairType
? O['type']
: 'sr25519'
: 'sr25519';
export function createTestPairs <O extends KeyringOptions, M = DetectMap<O>> (options?: O, isDerived = true): M {
export function createTestPairs <O extends KeyringOptions, M = TestKeyringMapQuantus> (options?: O, isDerived = true): M {
const keyring = createTestKeyring(options, isDerived);
const pairs = keyring.getPairs();
const map: TestKeyringMap = { nobody: nobody() };
+17 -2
View File
@@ -7,7 +7,7 @@ import type { EncryptedJson, Keypair, KeypairType, Prefix } from '@polkadot/util
export interface KeyringOptions {
/** The ss58Format to use for address encoding (defaults to 42) */
ss58Format?: Prefix;
/** The type of keyring to create (defaults to ed25519) */
/** The type of keyring to create (defaults to dilithium65, ML-DSA-65) */
type?: KeypairType;
}
@@ -72,6 +72,17 @@ export interface KeyringPair$Json extends EncryptedJson {
}
export interface SignOptions {
/**
* The FIPS 204 domain-separation context, for post-quantum pairs.
*
* Mandatory for `dilithium65`/`dilithium87` and ignored by every other type.
* There is deliberately no default: Quantus extrinsics on spec >= 148 are
* verified under `QUANTUS_EXTRINSIC` and earlier ones under the empty context,
* and a signature made under the wrong one is cryptographically valid, rejected
* by the chain, and locally indistinguishable from a correct one. Only the
* caller knows the spec version, so only the caller can answer.
*/
context?: Uint8Array;
/** Create a MultiSignature-compatible output with an indicator type */
withType?: boolean;
}
@@ -92,7 +103,11 @@ export interface KeyringPair {
sign (message: string | Uint8Array, options?: SignOptions): Uint8Array;
toJson (passphrase?: string): KeyringPair$Json;
unlock (passphrase?: string): void;
verify (message: string | Uint8Array, signature: Uint8Array, signerPublic: string | Uint8Array): boolean;
/**
* Verify an ML-DSA signature, bare or as the chain's `signature ‖ publicKey`,
* under `context` (default: the empty context used for raw bytes).
*/
verify (message: string | Uint8Array, signature: Uint8Array, signerPublic: string | Uint8Array, context?: Uint8Array): boolean;
vrfSign (message: string | Uint8Array, context?: string | Uint8Array, extra?: string | Uint8Array): Uint8Array;
vrfVerify (message: string | Uint8Array, vrfResult: Uint8Array, signerPublic: string | Uint8Array, context?: string | Uint8Array, extra?: string | Uint8Array): boolean;
}
+1 -1
View File
@@ -15,7 +15,7 @@
},
"sideEffects": false,
"type": "module",
"version": "14.0.3",
"version": "14.0.3-quantus.3",
"main": "index.js",
"dependencies": {
"@polkadot/util": "14.0.3",
+7 -2
View File
@@ -22,10 +22,15 @@ describe('defaults', (): void => {
).toEqual([]);
});
it('has no entries for testnets', (): void => {
// Upstream asserted the opposite: that no testnet appears in knownGenesis.
// This fork carries Quantus testnets deliberately — a wallet for a chain
// under development has to reach its testnet — so the assertion becomes that
// any testnet listed here is one we meant to list. See defaults/genesis.ts.
it('has genesis entries only for testnets we deliberately support', (): void => {
expect(
genesisKeys.filter((network) =>
knownTestnet[network]
knownTestnet[network] &&
!network.startsWith('quantus')
)
).toEqual([]);
});
+26 -3
View File
@@ -8,9 +8,20 @@ import type { KnownGenesis } from '../types.js';
// oldest goes last. This make lookups for the current a simple genesisHash[0]
// where the latest ios always the first entry (See Kusama as an example)
//
// IMPORTANT: Apart from the test relays, this list is limited to live parachains
// and live production networks. It does not and should not contain any testnets,
// either stand-alone or connected to test relays such as Westend/Rococo
// UPSTREAM SAID: "Apart from the test relays, this list is limited to live
// parachains and live production networks. It does not and should not contain
// any testnets, either stand-alone or connected to test relays such as
// Westend/Rococo"
//
// THIS FORK DOES NOT. That rule keeps testnets out of polkadot-js/apps' chain
// picker, which is a sensible product decision for a production explorer and the
// wrong one for us. Quantus is a chain under active development, and a wallet
// that cannot talk to its testnet is a wallet that cannot be tested against
// anything but real funds. Testnets are first-class here.
//
// `isTestnet` is still set honestly, so a consumer that wants to hide them can;
// what changed is that we no longer make that choice on the consumer's behalf.
// See `toExpanded` in ../interfaces.ts.
export const knownGenesis: KnownGenesis = {
acala: [
'0xfc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c'
@@ -153,6 +164,18 @@ export const knownGenesis: KnownGenesis = {
polymesh: [
'0x6fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063'
],
// Quantus mainnet, read from the node with `chain_getBlockHash(0)` on
// 2026-09-09, the day it launched. Not the staging chain, which is
// `0xb0e90ed9f291a9194f79…` and which telemetry confusingly calls
// "Quantus Staging Mainnet".
quantus: [
'0xfb5487c0be6ae4ade2d41d16e50465129861636c2b8d61fa94d7a19631626fba'
],
// Heisenberg, the public Quantus testnet. A testnet carrying a genesis hash is
// a deliberate departure from upstream — see the note above.
'quantus-heisenberg': [
'0xa5aa9e5c84d4a3722c152295e7973c9af522f2fb1ef7db5afaa3d5f4dc8d3b4f'
],
quartz: [
'0xcd4d732201ebe5d6b014edda071c4203e16867305332301dc8d092044b28e554'
],
+2 -1
View File
@@ -4,4 +4,5 @@
export { knownGenesis } from './genesis.js';
export { knownIcon } from './icons.js';
export { knownLedger } from './ledger.js';
export { knownTestnet } from './testnets.js';
export { knownQuantus } from './quantus.js';
export { exposedTestnet, knownTestnet } from './testnets.js';
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2017-2026 @polkadot/networks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KnownSubstrate } from '../types.js';
// Quantus networks, which `@substrate/ss58-registry` does not carry.
//
// Prefix 189 is unclaimed in that registry — nothing occupies 185..195 at all —
// so this is an addition rather than an override. It lives in this fork because
// the registry is Parity-curated and post-quantum support has not landed
// upstream; see the standing convention in quantus/extension#1.
//
// `standardAccount: 'ml-dsa'` is a value upstream's type does not allow, which
// is why `KnownSubstrate` is widened in ../types.ts. It is load-bearing: a null
// or missing `standardAccount` makes `toExpanded` mark the network `isIgnored`
// and it disappears from `availableNetworks`.
export const knownQuantus: KnownSubstrate[] = [
{
decimals: [12],
displayName: 'Quantus',
network: 'quantus',
prefix: 189,
standardAccount: 'ml-dsa',
symbols: ['QTC'],
website: 'https://quantus.com'
},
{
decimals: [12],
displayName: 'Quantus Heisenberg',
network: 'quantus-heisenberg',
prefix: 189,
standardAccount: 'ml-dsa',
// HEI, not QTC — confirmed from the chain itself via system_properties on
// a1-heisenberg.quantus.cat. A testnet token that displayed as the mainnet
// symbol would be a good way to confuse play money for real.
symbols: ['HEI'],
website: 'https://quantus.com'
}
];
+20 -1
View File
@@ -3,7 +3,8 @@
import type { KnownTestnet } from '../types.js';
// testnets should not allow selection
// Marks a network as a testnet. Purely descriptive — see `exposedTestnet` below
// for which of them this fork actually offers.
export const knownTestnet: KnownTestnet = {
'': true, // this is the default non-network entry
'cess-testnet': true,
@@ -11,6 +12,24 @@ export const knownTestnet: KnownTestnet = {
jupiter: true,
'mathchain-testnet': true,
p3dt: true,
'quantus-heisenberg': true,
subspace_testnet: true,
'zero-alphaville': true
};
// Testnets this fork exposes anyway, despite upstream's rule that a testnet is
// never offered.
//
// That rule keeps testnets out of polkadot-js/apps' chain picker, which is right
// for a production explorer and wrong for us: Quantus is under active
// development, and a wallet that cannot reach its testnet can only be tested
// against real funds.
//
// An explicit allowlist rather than dropping the rule wholesale. Dropping it
// exposed all fifteen of upstream's testnets too, which is a change we have no
// reason to make and no way to have thought about — it moved the tail of the
// sorted list from 'ZERO' to 'ZERO Alphaville' and would have surprised anyone
// reading `availableNetworks`. Opting in one network at a time says what we mean.
export const exposedTestnet: KnownTestnet = {
'quantus-heisenberg': true
};
+38 -8
View File
@@ -6,7 +6,7 @@
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 => {
@@ -51,16 +51,40 @@ describe('availableNetworks', (): void => {
).toEqual([]);
});
it('has no testnets exposed', (): void => {
// Upstream asserted that no testnet reaches availableNetworks at all. This
// fork exposes the Quantus ones deliberately — see exposedTestnet — so what is
// worth pinning is that an exposed testnet is still *labelled* one. Losing the
// label is the failure that matters: a user cannot tell play money from real
// money by looking at an address.
it('labels every exposed testnet as a testnet', (): void => {
expect(
Object.keys(knownTestnet).filter((network) =>
availableNetworks.some((a) =>
a.network === network
)
availableNetworks.some((a) => a.network === network && !a.isTestnet)
)
).toEqual([]);
});
it('exposes no testnets except the ones we opted into', (): void => {
expect(
availableNetworks
.filter((a) => a.isTestnet && !a.network.startsWith('quantus'))
.map((a) => a.network)
).toEqual([]);
});
// These lists stay complete on purpose. An earlier version of this fork
// filtered them to post-quantum chains, which broke @polkadot/types-known — it
// looks up every chain it holds upgrade history for and throws when one is
// missing, so @polkadot/api failed at import. Gating what a wallet offers is
// the wallet's job; isQuantumSafe is exported for it and the extension applies
// it in its own chain list.
it('identifies which chains are post-quantum', (): void => {
expect(isQuantumSafe('ml-dsa')).toEqual(true);
expect(isQuantumSafe('Sr25519')).toEqual(false);
expect(isQuantumSafe(null)).toEqual(false);
expect(availableNetworks.filter((n) => isQuantumSafe(n.standardAccount)).map((n) => n.network)).toEqual(['quantus', 'quantus-heisenberg']);
});
it('has allNetworks icons, except for overrides', (): void => {
expect(
availableNetworks.filter(({ icon, network }) =>
@@ -92,14 +116,20 @@ describe('availableNetworks', (): void => {
});
describe('allNetworks', (): void => {
it('has no ss58 duplicates', (): void => {
// Upstream asserted one network per SS58 prefix. That stops holding the moment
// a chain has a testnet: Quantus mainnet and Heisenberg are both 189, because
// they are the same chain at two deployments and an address is valid on
// either. Consumers key on genesisHash, so sharing a prefix costs nothing.
it('has no ss58 duplicates, except deployments of the same chain', (): void => {
const dupes: SubstrateNetwork[] = [];
const uniques: SubstrateNetwork[] = [];
allNetworks.forEach((a): void => {
if (uniques.some((u) => u.prefix === a.prefix)) {
const clash = uniques.find((u) => u.prefix === a.prefix);
if (clash && clash.network.split('-')[0] !== a.network.split('-')[0]) {
dupes.push(a);
} else {
} else if (!clash) {
uniques.push(a);
}
});
+1
View File
@@ -7,3 +7,4 @@
export * from './interfaces.js';
export { packageInfo } from './packageInfo.js';
export { isQuantumSafe } from './quantumSafe.js';
+34 -5
View File
@@ -5,7 +5,7 @@ import type { KnownSubstrate, Network, SubstrateNetwork } from './types.js';
import knownSubstrate from '@substrate/ss58-registry';
import { knownGenesis, knownIcon, knownLedger, knownTestnet } from './defaults/index.js';
import { exposedTestnet, knownGenesis, knownIcon, knownLedger, knownQuantus, knownTestnet } from './defaults/index.js';
// These are known prefixes that are not sorted
const UNSORTED = [0, 2, 42];
@@ -26,7 +26,16 @@ function toExpanded (o: KnownSubstrate): SubstrateNetwork {
// filtering
n.isTestnet = !!knownTestnet[network] || TESTNETS.includes(nameParts[nameParts.length - 1]);
n.isIgnored = n.isTestnet || (
// Upstream had a bare `isTestnet ||` here, so a testnet was dropped no matter
// how complete its entry was. That suits a production chain picker and does
// not suit us — Quantus development happens on Heisenberg, and tools that
// cannot reach a testnet can only be tested against real funds.
//
// The exception is an allowlist rather than a blanket removal: see
// `exposedTestnet`. `isTestnet` is still reported honestly either way, so a
// consumer can label or filter — we simply no longer make that call for them.
n.isIgnored = (n.isTestnet && !exposedTestnet[network]) || (
!(
o.standardAccount &&
o.decimals?.length &&
@@ -59,11 +68,31 @@ function sortNetworks (a: Network, b: Network): number {
: 1;
}
// This is all the Substrate networks with our additional information
export const allNetworks = knownSubstrate.map(toExpanded);
// This is all the Substrate networks with our additional information.
//
// None of these three lists is filtered to post-quantum chains, and an earlier
// version of this fork filtering `availableNetworks` was a mistake worth
// recording. These are *library* lists, not a product's offer: other packages
// consume them expecting the chains Substrate knows about to be present.
// `@polkadot/types-known` looks up every chain it has upgrade history for and
// **throws** when one is missing, so removing Polkadot and Kusama here made
// `@polkadot/api` fail at import — nothing to do with Quantus, and impossible to
// diagnose from the error.
//
// Gating what a wallet *offers* is the wallet's decision. `isQuantumSafe` is
// exported for exactly that, and the extension applies it in its own chain list.
// Keeping these complete also means a pasted Polkadot address still decodes and
// renders as what it is — recognisable, and therefore refusable.
//
// 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.
// Appending also means a registry bump cannot silently drop it.
export const allNetworks = knownSubstrate
.map((o) => toExpanded(o as KnownSubstrate))
.concat(knownQuantus.map(toExpanded));
// The list of available/claimed prefixes
// - no testnets
// - 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);
+1 -1
View File
@@ -3,4 +3,4 @@
// Do not edit, auto-generated by @polkadot/dev
export const packageInfo = { name: '@polkadot/networks', path: 'auto', type: 'auto', version: '14.0.3' };
export const packageInfo = { name: '@polkadot/networks', path: 'auto', type: 'auto', version: '14.0.3-quantus.3' };
+32
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);
}
+13 -1
View File
@@ -12,7 +12,19 @@ export type KnownLedger = Record<string, number>;
export type KnownGenesis = Record<string, HexString[]>;
export type KnownSubstrate = RegistryEntry;
/**
* A registry entry, widened for schemes the upstream registry does not know about.
*
* `@substrate/ss58-registry` types `standardAccount` as a closed union of the
* curves Substrate ships with. Quantus signs with ML-DSA, which is not one of
* them and — given the registry is a Parity-curated list and post-quantum
* support has not landed upstream — is not going to become one. Widening here
* keeps the Quantus entry in `defaults/quantus.ts` type-checked rather than
* cast, and leaves every upstream entry exactly as the registry declares it.
*/
export type KnownSubstrate = Omit<RegistryEntry, 'standardAccount'> & {
standardAccount: RegistryEntry['standardAccount'] | 'ml-dsa';
};
export type KnownTestnet = Record<string, true>;
+3 -2
View File
@@ -20,7 +20,7 @@
"./packageDetect.cjs"
],
"type": "module",
"version": "14.0.3",
"version": "14.0.3-quantus.3",
"browser": {
"crypto": false,
"stream": false
@@ -29,12 +29,13 @@
"dependencies": {
"@noble/curves": "^1.3.0",
"@noble/hashes": "^1.3.3",
"@polkadot/networks": "14.0.3",
"@polkadot/networks": "14.0.3-quantus.3",
"@polkadot/util": "14.0.3",
"@polkadot/wasm-crypto": "^7.5.3",
"@polkadot/wasm-util": "^7.5.3",
"@polkadot/x-bigint": "14.0.3",
"@polkadot/x-randomvalues": "14.0.3",
"@quantus/crypto": "^0.3.0",
"@scure/base": "^1.1.7",
"@scure/sr25519": "^0.2.0",
"tslib": "^2.8.0"
@@ -1,26 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { waitReady } from '@polkadot/wasm-crypto';
import { deriveAddress } from './index.js';
describe('deriveAddress', (): void => {
beforeEach(async (): Promise<void> => {
await waitReady();
});
it('derives a known path', (): void => {
expect(
deriveAddress('5CZtJLXtVzrBJq1fMWfywDa6XuRwXekGdShPR4b8i9GWSbzB', '/joe/polkadot/0')
).toEqual('5GZ4srnepXvdsuNVoxCGyVZd8ScDm4gkGLTKuaGARy9akjTa');
});
it('fails on hard paths', (): void => {
expect(
() => deriveAddress('5CZtJLXtVzrBJq1fMWfywDa6XuRwXekGdShPR4b8i9GWSbzB', '//bob')
).toThrow(/Expected suri to contain a combination of non-hard paths/);
});
});
@@ -1,36 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveJunction } from '../key/DeriveJunction.js';
import type { Prefix } from './types.js';
import { keyExtractPath } from '../key/index.js';
import { sr25519DerivePublic } from '../sr25519/index.js';
import { decodeAddress } from './decode.js';
import { encodeAddress } from './encode.js';
function filterHard ({ isHard }: DeriveJunction): boolean {
return isHard;
}
/**
* @name deriveAddress
* @summary Creates a sr25519 derived address from the supplied and path.
* @description
* Creates a sr25519 derived address based on the input address/publicKey and the uri supplied.
*/
export function deriveAddress (who: string | Uint8Array, suri: string, ss58Format?: Prefix): string {
const { path } = keyExtractPath(suri);
if (!path.length || path.every(filterHard)) {
throw new Error('Expected suri to contain a combination of non-hard paths');
}
let publicKey = decodeAddress(who);
for (const { chainCode } of path) {
publicKey = sr25519DerivePublic(publicKey, chainCode);
}
return encodeAddress(publicKey, ss58Format);
}
@@ -1,13 +1,13 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HashType } from '../secp256k1/types.js';
import type { HashType } from './hasher.js';
import type { Prefix } from './types.js';
import { u8aConcat } from '@polkadot/util';
import { hasher } from '../secp256k1/hasher.js';
import { encodeAddress } from './encode.js';
import { hasher } from './hasher.js';
/**
* @name evmToAddress
@@ -1,11 +1,12 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HashType } from './types.js';
/** Which hash an EVM ↔ Substrate address conversion uses. */
import { blake2AsU8a } from '../blake2/index.js';
import { keccakAsU8a } from '../keccak/index.js';
export type HashType = 'blake2' | 'keccak';
export function hasher (hashType: HashType, data: Uint8Array | string, onlyJs?: boolean): Uint8Array {
return hashType === 'keccak'
? keccakAsU8a(data, undefined, onlyJs)
@@ -5,7 +5,6 @@ export { addressToEvm } from './addressToEvm.js';
export { checkAddress } from './check.js';
export { checkAddressChecksum } from './checksum.js';
export { decodeAddress } from './decode.js';
export { deriveAddress } from './derive.js';
export { encodeAddress } from './encode.js';
export { encodeDerivedAddress } from './encodeDerived.js';
export { encodeMultiAddress } from './encodeMulti.js';
+2 -4
View File
@@ -13,7 +13,8 @@ export * from './base58/index.js';
export * from './base64/index.js';
export * from './blake2/index.js';
export * from './crypto.js';
export * from './ed25519/index.js';
export * from './dilithium.js';
export * from './dilithium/index.js';
export * from './ethereum/index.js';
export * from './hd/index.js';
export * from './hmac/index.js';
@@ -26,8 +27,5 @@ export * from './networks.js';
export * from './pbkdf2/index.js';
export * from './random/index.js';
export * from './scrypt/index.js';
export * from './secp256k1/index.js';
export * from './sha/index.js';
export * from './signature/index.js';
export * from './sr25519/index.js';
export * from './xxhash/index.js';
+43
View File
@@ -0,0 +1,43 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeypairType } from './types.js';
/** The Quantus post-quantum keypair types. */
export const DILITHIUM_TYPES = ['dilithium65', 'dilithium87'] as const;
/** A Quantus post-quantum keypair type. */
export type DilithiumType = (typeof DILITHIUM_TYPES)[number];
/** The keypair types that predate Quantus, i.e. everything upstream supports. */
export type SubstrateKeypairType = Exclude<KeypairType, DilithiumType>;
/** Whether `type` is one of the Quantus post-quantum schemes. */
export function isDilithium (type: KeypairType): type is DilithiumType {
return type === 'dilithium65' || type === 'dilithium87';
}
/**
* Narrow a `KeypairType` to the ones the curve-based code paths understand,
* throwing for the Quantus schemes.
*
* Opening up `KeypairType` turns every lookup table keyed by it into a type
* error — which is the point, since it enumerates precisely the places that need
* a post-quantum answer. Where that answer is not written yet, this is how the
* gap is expressed: a named, typed refusal at the call site rather than a silent
* `undefined` from an object index.
*
* It throws rather than degrading because every one of these paths produces key
* material or an address. Returning a wrong-but-plausible value would mean funds
* at an address nobody controls, discovered later and unrecoverably.
*
* Call sites disappear as the arms are implemented — see quantus/common#2 for
* the pair maps and quantus/common#4 for derivation.
*/
export function assertSubstrateType (type: KeypairType, what: string): SubstrateKeypairType {
if (isDilithium(type)) {
throw new Error(`${what} is not implemented for ${type}`);
}
return type;
}
@@ -0,0 +1,17 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { accountFromPublicKey } from '@quantus/crypto';
/**
* The 32-byte account id for an ML-DSA public key — Poseidon2 over Goldilocks.
*
* This is the step with no Substrate equivalent. There an `AccountId32` *is* the
* public key; here it is a one-way hash of one, which is why an ML-DSA signature
* has to carry its public key inside itself and why nothing can recover a key
* from an address. Every "the address and the publicKey are isomorphic"
* assumption in this codebase is false for these types.
*/
export function dilithiumAccountFromPublic (publicKey: Uint8Array): Uint8Array {
return accountFromPublicKey(publicKey);
}
@@ -0,0 +1,203 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import type { QuantusCliWallet } from './cliWallet.js';
import fs from 'node:fs';
import path from 'node:path';
import { u8aToHex } from '@polkadot/util';
import { encodeAddress } from '../address/encode.js';
import { cliWalletAddressMatches, dilithiumFromCliWallet, dilithiumToCliWallet } from './cliWallet.js';
import { dilithiumPairFromMnemonic } from './pair.js';
const FIXTURES = path.join(process.cwd(), 'packages/util-crypto/src/dilithium/fixtures');
// The public Substrate dev phrase — what both fixtures were created from, and
// published everywhere. No secret is committed.
const DEV_PHRASE = 'bottom drive obey lake curtain smoke basket hold race lonely fit walk';
function load (name: string): QuantusCliWallet {
return JSON.parse(fs.readFileSync(path.join(FIXTURES, name), 'utf-8')) as QuantusCliWallet;
}
interface Case {
file: string;
path: string;
type: 'dilithium65' | 'dilithium87';
}
const CASE_65: Case = { file: 'cli-dev_ml_dsa_65.json', path: "m/44'/189189'/0'/0'/1'", type: 'dilithium65' };
const CASE_87: Case = { file: 'cli-dev_ml_dsa_87.json', path: "m/44'/189189'/0'/0'/0'", type: 'dilithium87' };
// Both parameter sets get the identical battery. Written as a function called
// twice rather than a loop over a `describe`, because a computed describe title
// is invisible to static analysis and the linter rightly objects.
function testScheme (c: Case): void {
const wallet = load(c.file);
it('imports and reproduces the address the file claims', async (): Promise<void> => {
const imported = await dilithiumFromCliWallet(wallet);
expect(imported.type).toEqual(c.type);
expect(encodeAddress(imported.accountId, 189)).toEqual(wallet.address);
});
// The same account reached two entirely different ways: through the CLI's
// encrypted container, and by deriving from the mnemonic ourselves. If either
// side were wrong these would not meet.
it('yields the key our own derivation yields', async (): Promise<void> => {
const imported = await dilithiumFromCliWallet(wallet);
const derived = dilithiumPairFromMnemonic(DEV_PHRASE, '', c.path, c.type);
expect(u8aToHex(imported.keypair.publicKey)).toEqual(u8aToHex(derived.publicKey));
expect(u8aToHex(imported.keypair.secretKey)).toEqual(u8aToHex(derived.secretKey));
});
it('reports the derivation path the CLI recorded', async (): Promise<void> => {
const imported = await dilithiumFromCliWallet(wallet);
expect(imported.derivationPath).toEqual(c.path);
});
// The CLI keeps a mnemonic in every HD wallet. We do not return it — the
// extension's posture is that a storage password unlocks signing and nothing on
// disk regenerates the tree. The flag exists so a caller can tell the user
// where their backup lives without the phrase passing through.
it('reports that a mnemonic was present without returning it', async (): Promise<void> => {
const imported = await dilithiumFromCliWallet(wallet);
expect(imported.hadMnemonic).toEqual(true);
expect(Object.keys(imported).includes('mnemonic')).toEqual(false);
});
}
describe('quantus-cli wallet import', (): void => {
describe('dilithium65', (): void => {
testScheme(CASE_65);
});
describe('dilithium87', (): void => {
testScheme(CASE_87);
});
it('fails clearly on a wrong password', async (): Promise<void> => {
await expect(dilithiumFromCliWallet(load(CASE_65.file), 'wrong')).rejects.toThrow(/password may be incorrect/);
});
// An ML-KEM envelope mode that is planned or optional. Ignoring these fields
// and decrypting by the path we know would fail later and more confusingly,
// most likely as a bad-password error on a password that was correct.
it('refuses a wallet using ML-KEM envelope encryption', async (): Promise<void> => {
const wallet = { ...load(CASE_65.file), kyber_ciphertext: [1, 2, 3] };
await expect(dilithiumFromCliWallet(wallet)).rejects.toThrow(/ML-KEM envelope encryption/);
});
it('refuses an unknown container version', async (): Promise<void> => {
const wallet = { ...load(CASE_65.file), encryption_version: 3 };
await expect(dilithiumFromCliWallet(wallet)).rejects.toThrow(/encryption version 3/);
});
// The Argon2 parameters live in the file because they are expected to change.
// A build that assumed today's values would reject a valid password on an
// older or newer wallet — the least useful thing it could say.
it('reads the Argon2 parameters rather than assuming them', async (): Promise<void> => {
const wallet = { ...load(CASE_65.file), argon2_params: '$argon2i$v=19$m=19456,t=2,p=1$x' };
await expect(dilithiumFromCliWallet(wallet)).rejects.toThrow(/Unsupported Argon2 parameters/);
});
// The address sits outside the encrypted blob, so it is the one field an
// attacker can edit without the password. Unchecked, a tampered file imports
// cleanly and produces an account displaying an address it cannot sign for.
it('detects an address that disagrees with the key', async (): Promise<void> => {
const a = await dilithiumFromCliWallet(load(CASE_65.file));
const b = await dilithiumFromCliWallet(load(CASE_87.file));
expect(cliWalletAddressMatches(a.accountId, a.accountId)).toEqual(true);
expect(cliWalletAddressMatches(a.accountId, b.accountId)).toEqual(false);
});
// Export is what stops import being a one-way door. The mnemonic is always
// null, which is a shape the CLI writes itself for raw-seed wallets and opens
// normally — verified against quantus-cli 2.2.2 by hand, since CI has no CLI.
describe('export', (): void => {
it('round-trips a keypair back through import', async (): Promise<void> => {
const source = await dilithiumFromCliWallet(load(CASE_65.file));
const exported = await dilithiumToCliWallet({
derivationPath: CASE_65.path,
keypair: source.keypair,
name: 'round tripped',
type: 'dilithium65'
}, 'a password');
const reimported = await dilithiumFromCliWallet(exported, 'a password');
expect(u8aToHex(reimported.keypair.publicKey)).toEqual(u8aToHex(source.keypair.publicKey));
expect(u8aToHex(reimported.keypair.secretKey)).toEqual(u8aToHex(source.keypair.secretKey));
expect(reimported.type).toEqual('dilithium65');
expect(reimported.derivationPath).toEqual(CASE_65.path);
});
it('writes the address the key actually hashes to', async (): Promise<void> => {
const source = await dilithiumFromCliWallet(load(CASE_87.file));
const exported = await dilithiumToCliWallet({
keypair: source.keypair, name: 'addressed', type: 'dilithium87'
});
expect(exported.address).toEqual(load(CASE_87.file).address);
});
it('carries no mnemonic', async (): Promise<void> => {
const source = await dilithiumFromCliWallet(load(CASE_65.file));
const exported = await dilithiumToCliWallet({ keypair: source.keypair, name: 'x', type: 'dilithium65' });
const reimported = await dilithiumFromCliWallet(exported);
expect(source.hadMnemonic).toEqual(true);
expect(reimported.hadMnemonic).toEqual(false);
});
// A raw-seed account has no path, and the CLI records `m/` for that rather
// than omitting the field.
it('records m/ for an account with no derivation path', async (): Promise<void> => {
const source = await dilithiumFromCliWallet(load(CASE_65.file));
const exported = await dilithiumToCliWallet({ keypair: source.keypair, name: 'raw', type: 'dilithium65' });
const reimported = await dilithiumFromCliWallet(exported);
expect(reimported.derivationPath).toEqual('m/');
});
it('uses a fresh salt and nonce each time', async (): Promise<void> => {
const source = await dilithiumFromCliWallet(load(CASE_65.file));
const input = { keypair: source.keypair, name: 'x', type: 'dilithium65' as const };
const a = await dilithiumToCliWallet(input);
const b = await dilithiumToCliWallet(input);
expect(a.argon2_salt).not.toEqual(b.argon2_salt);
expect(a.aes_nonce).not.toEqual(b.aes_nonce);
expect(a.encrypted_data).not.toEqual(b.encrypted_data);
});
it('writes a container this build declares it supports', async (): Promise<void> => {
const source = await dilithiumFromCliWallet(load(CASE_65.file));
const exported = await dilithiumToCliWallet({ keypair: source.keypair, name: 'x', type: 'dilithium65' });
expect(exported.encryption_version).toEqual(2);
expect(exported.kyber_ciphertext).toEqual([]);
expect(exported.kyber_public_key).toEqual([]);
expect(exported.argon2_params.startsWith('$argon2id$v=19$m=')).toEqual(true);
expect(exported.argon2_salt.length).toEqual(16);
expect(exported.aes_nonce.length).toEqual(12);
});
it('refuses key material that does not match the scheme', async (): Promise<void> => {
const source = await dilithiumFromCliWallet(load(CASE_65.file));
await expect(dilithiumToCliWallet({ keypair: source.keypair, name: 'x', type: 'dilithium87' })).rejects.toThrow(/Key lengths do not match/);
});
});
});
@@ -0,0 +1,304 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DilithiumType } from '../dilithium.js';
import type { Keypair } from '../types.js';
import { argon2id } from '@noble/hashes/argon2';
import { u8aEq } from '@polkadot/util';
import { encodeAddress } from '../address/encode.js';
import { base64Encode } from '../base64/index.js';
import { dilithiumAccountFromPublic } from './account.js';
import { dilithiumSizes } from './scheme.js';
/** The wallet file `quantus-cli` writes to ~/.quantus/wallets/<name>.json */
export interface QuantusCliWallet {
address: string;
aes_nonce: number[];
argon2_params: string;
argon2_salt: number[];
created_at?: string;
encrypted_data: number[];
encryption_version: number;
kyber_ciphertext?: number[];
kyber_public_key?: number[];
name?: string;
wallet_type?: string;
}
export interface QuantusCliImport {
/** The 32-byte account id the file claims, verified against the key. */
accountId: Uint8Array;
/** The derivation path the CLI recorded, for display. `m/` for a raw-seed wallet. */
derivationPath: string | null;
keypair: Keypair;
/** Whether the file carried a mnemonic. It is never returned — see below. */
hadMnemonic: boolean;
name: string | null;
type: DilithiumType;
}
/** The only container version this understands. */
const SUPPORTED_VERSION = 2;
const SCHEME_TO_TYPE: Record<string, DilithiumType> = {
'ml-dsa-65': 'dilithium65',
'ml-dsa-87': 'dilithium87'
};
const TYPE_TO_SCHEME: Record<DilithiumType, string> = {
dilithium65: 'ml-dsa-65',
dilithium87: 'ml-dsa-87'
};
/**
* Argon2 parameters written on export.
*
* Deliberately the same values `quantus-cli` 2.2.2 uses rather than something
* stronger. The parameters travel in the file and the CLI reads them, so in
* principle anything works — but "in principle" is doing load-bearing work
* there, and a wallet that a slightly older CLI cannot open is a worse outcome
* than one whose KDF is tuned conservatively. Raise them when there is a reason
* to believe every CLI in circulation honours the PHC string.
*/
const EXPORT_ARGON2: Argon2Params = { m: 19456, p: 1, t: 2 };
/**
* SS58 prefix for the `address` field of an exported wallet.
*
* Fixed rather than a parameter: this is the Quantus CLI's format, the CLI only
* ever deals with Quantus, and an address at any other prefix would be one it
* could not match to the key inside.
*/
const QUANTUS_PREFIX = 189;
/** What the CLI records for a wallet with no derivation — a raw seed. */
const NO_DERIVATION = 'm/';
interface Argon2Params {
m: number;
p: number;
t: number;
}
/**
* Parse the PHC string the CLI stores, e.g.
* `$argon2id$v=19$m=19456,t=2,p=1$<salt>`.
*
* Read rather than hardcoded. The parameters are in the file because they are
* expected to change, and a build that assumed today's values would fail to open
* an older or newer wallet with a *wrong password* error — which is the least
* useful thing it could say, since the password would be right.
*/
function parseArgon2Params (phc: string): Argon2Params {
const match = /^\$argon2id\$v=19\$m=(\d+),t=(\d+),p=(\d+)/.exec(phc);
if (!match) {
throw new Error(`Unsupported Argon2 parameters in wallet: ${phc}`);
}
return {
m: parseInt(match[1], 10),
p: parseInt(match[3], 10),
t: parseInt(match[2], 10)
};
}
function assertSupported (wallet: QuantusCliWallet): void {
if (wallet.encryption_version !== SUPPORTED_VERSION) {
throw new Error(`Unsupported quantus-cli wallet encryption version ${wallet.encryption_version}; this build understands version ${SUPPORTED_VERSION}`);
}
// An ML-KEM envelope mode, either planned or optional — the fields exist and
// are empty in every wallet seen so far. Refuse rather than ignore them and
// decrypt by the path we do know: a wallet encrypted to an ML-KEM key is not
// one we can open, and treating it as if it were would fail later and more
// confusingly, most likely as a bad-password error.
if (wallet.kyber_ciphertext?.length || wallet.kyber_public_key?.length) {
throw new Error('This wallet uses ML-KEM envelope encryption, which is not supported');
}
}
/**
* Decrypt and validate a `quantus-cli` wallet file.
*
* The result is exactly what importing the same account by mnemonic would
* produce: a keypair, and nothing that can regenerate the rest of the tree. The
* CLI stores a mnemonic inside every HD wallet and this **deliberately does not
* return it** — the extension's posture is that a storage password unlocks
* signing and nothing on disk regenerates the wallet. `hadMnemonic` says one was
* present, so a caller can tell the user where their backup lives, without the
* phrase itself passing through.
*
* Async because AES-GCM is WebCrypto, which has no synchronous form. That is
* also why this is an edge conversion rather than something `createFromJson`
* learns: that path is synchronous and should stay so, and the keyring should
* keep exactly one on-disk format.
*/
export async function dilithiumFromCliWallet (wallet: QuantusCliWallet, password = ''): Promise<QuantusCliImport> {
assertSupported(wallet);
const { m, p, t } = parseArgon2Params(wallet.argon2_params);
const key = argon2id(
new TextEncoder().encode(password),
new Uint8Array(wallet.argon2_salt),
{ dkLen: 32, m, p, t }
);
let plaintext: Uint8Array;
try {
const aesKey = await crypto.subtle.importKey('raw', key, 'AES-GCM', false, ['decrypt']);
plaintext = new Uint8Array(
await crypto.subtle.decrypt(
{ iv: new Uint8Array(wallet.aes_nonce), name: 'AES-GCM' },
aesKey,
new Uint8Array(wallet.encrypted_data)
)
);
} catch {
// AES-GCM authenticates, so this is a wrong password or a corrupted file and
// there is no way to tell which from here.
throw new Error('Unable to decrypt the wallet; the password may be incorrect');
}
const decoded = JSON.parse(new TextDecoder().decode(plaintext)) as {
keypair?: { private_key?: number[]; public_key?: number[]; scheme?: string };
derivation_path?: string;
mnemonic?: string | null;
name?: string;
};
const scheme = decoded.keypair?.scheme;
const type = scheme ? SCHEME_TO_TYPE[scheme] : undefined;
if (!type) {
throw new Error(`Unsupported signature scheme in wallet: ${scheme ?? 'none'}`);
}
if (!decoded.keypair?.public_key || !decoded.keypair.private_key) {
throw new Error('Wallet contains no keypair');
}
const publicKey = new Uint8Array(decoded.keypair.public_key);
const secretKey = new Uint8Array(decoded.keypair.private_key);
const sizes = dilithiumSizes(type);
if (publicKey.length !== sizes.publicKey || secretKey.length !== sizes.secretKey) {
throw new Error(`Wallet key lengths do not match ${scheme}: expected ${sizes.publicKey}/${sizes.secretKey}, found ${publicKey.length}/${secretKey.length}`);
}
// The address travels outside the encrypted blob, so it is the one field an
// attacker can edit without knowing the password. Left unchecked, a tampered
// file would import cleanly and produce an account displaying an address it
// cannot sign for — the same failure the account-JSON check in the keyring
// exists to prevent.
const accountId = dilithiumAccountFromPublic(publicKey);
return {
accountId,
derivationPath: decoded.derivation_path ?? null,
hadMnemonic: !!decoded.mnemonic,
keypair: { publicKey, secretKey },
name: decoded.name ?? wallet.name ?? null,
type
};
}
/**
* Whether a decrypted wallet's key matches the address its file claims.
*
* Separate from the decrypt so the caller can render the mismatch rather than
* only catch it — "this file's address does not match its key" is a different
* message from "wrong password", and a user can act on the difference.
*/
export function cliWalletAddressMatches (accountId: Uint8Array, decodedAddress: Uint8Array): boolean {
return u8aEq(accountId, decodedAddress);
}
export interface QuantusCliExportInput {
/** The derivation path, where known. Omit for a raw-seed account. */
derivationPath?: string | null;
keypair: Keypair;
name: string;
type: DilithiumType;
}
/**
* Write a `quantus-cli` wallet file.
*
* The mnemonic is **always null**. That is not a limitation worked around: the
* CLI's format has a null-mnemonic shape and writes one itself for every
* raw-seed wallet, and a wallet in that shape lists, opens and spends normally.
* What it cannot do is `export --format mnemonic` or derive further accounts —
* the honest consequence of the extension never storing a phrase it showed once.
*
* Without this, importing a CLI wallet (see `dilithiumFromCliWallet`) is a
* one-way door, and an extension backup is readable only by polkadot-js-format
* tooling.
*/
export async function dilithiumToCliWallet ({ derivationPath, keypair, name, type }: QuantusCliExportInput, password = ''): Promise<QuantusCliWallet> {
const sizes = dilithiumSizes(type);
if (keypair.publicKey.length !== sizes.publicKey || keypair.secretKey.length !== sizes.secretKey) {
throw new Error(`Key lengths do not match ${type}: expected ${sizes.publicKey}/${sizes.secretKey}, found ${keypair.publicKey.length}/${keypair.secretKey.length}`);
}
const payload = JSON.stringify({
derivation_path: derivationPath || NO_DERIVATION,
keypair: {
private_key: Array.from(keypair.secretKey),
public_key: Array.from(keypair.publicKey),
scheme: TYPE_TO_SCHEME[type]
},
metadata: {
algorithm: TYPE_TO_SCHEME[type].toUpperCase(),
// So a CLI maintainer reading a bug report can tell where the file came
// from without having to guess from its shape.
exported_by: 'quantus-extension',
version: '1.0.0'
},
mnemonic: null,
name
});
const salt = crypto.getRandomValues(new Uint8Array(16));
const nonce = crypto.getRandomValues(new Uint8Array(12));
const key = argon2id(
new TextEncoder().encode(password),
salt,
{ dkLen: 32, m: EXPORT_ARGON2.m, p: EXPORT_ARGON2.p, t: EXPORT_ARGON2.t }
);
const aesKey = await crypto.subtle.importKey('raw', key, 'AES-GCM', false, ['encrypt']);
const encrypted = new Uint8Array(
await crypto.subtle.encrypt(
{ iv: nonce, name: 'AES-GCM' },
aesKey,
new TextEncoder().encode(payload)
)
);
return {
address: encodeAddress(dilithiumAccountFromPublic(keypair.publicKey), QUANTUS_PREFIX),
aes_nonce: Array.from(nonce),
// The salt goes into the PHC string as well as its own field, because that
// is what the CLI writes and a reader may take it from either.
argon2_params: `$argon2id$v=19$m=${EXPORT_ARGON2.m},t=${EXPORT_ARGON2.t},p=${EXPORT_ARGON2.p}$${base64Unpadded(salt)}`,
argon2_salt: Array.from(salt),
created_at: new Date().toISOString(),
encrypted_data: Array.from(encrypted),
encryption_version: SUPPORTED_VERSION,
kyber_ciphertext: [],
kyber_public_key: [],
name,
wallet_type: 'hot'
};
}
/** Unpadded standard base64, as the PHC string format requires. */
function base64Unpadded (data: Uint8Array): string {
return base64Encode(data).replace(/=+$/, '');
}
@@ -0,0 +1,22 @@
# quantus-cli wallet fixtures
Genuine output of `quantus-cli` 2.2.2, not files this repo constructed. Testing a
decoder against a blob the same code encrypted would prove only that it is
self-consistent; these prove it agrees with the tool whose wallets users actually
have.
Created with:
```sh
quantus wallet import --name dev_ml_dsa_65 --scheme ml-dsa-65 \
--mnemonic-file <phrase> --allow-empty-password
```
**They contain no secret.** The mnemonic inside each is the well-known Substrate
development phrase — `bottom drive obey lake curtain smoke basket hold race
lonely fit walk` — which is published in polkadot-sdk, in polkadot-js and in every
tutorial. The password is empty. Any account they derive is assumed compromised
and must never hold value.
That is the only reason it is acceptable to commit a decryptable wallet file. Do
not add a fixture created from a generated mnemonic, however throwaway it seems.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { dilithiumAccountFromPublic } from './account.js';
export type { QuantusCliExportInput, QuantusCliImport, QuantusCliWallet } from './cliWallet.js';
export { cliWalletAddressMatches, dilithiumFromCliWallet, dilithiumToCliWallet } from './cliWallet.js';
export { dilithiumPairFromMnemonic, dilithiumPairFromSeed } from './pair.js';
export { dilithiumPath, dilithiumPathFromSuri } from './path.js';
export { dilithiumSchemeFor, dilithiumSizes } from './scheme.js';
export { dilithiumSign } from './sign.js';
export { dilithiumVerify } from './verify.js';
@@ -0,0 +1,35 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DilithiumType } from '../dilithium.js';
import type { Keypair } from '../types.js';
import { keypairFromMnemonic, keypairFromSeed } from '@quantus/crypto';
import { dilithiumSchemeFor } from './scheme.js';
/**
* Create an ML-DSA keypair from 32 bytes of entropy.
*
* FIPS 204 `ML-DSA.KeyGen_internal` with no Quantus-specific step. The parameter
* set is absorbed into the seed expansion, so the same 32 bytes give independent
* keys per scheme which is why the dev-genesis accounts (`[0u8; 32]` and
* friends) and HD-derived accounts share this one entry point.
*/
export function dilithiumPairFromSeed (seed: Uint8Array, type: DilithiumType): Keypair {
return keypairFromSeed(seed, dilithiumSchemeFor(type));
}
/**
* Create an ML-DSA keypair from a BIP39 mnemonic at a hardened path.
*
* The seeding is the part that silently goes wrong. This runs mnemonic the
* 64-byte BIP39 seed an HMAC-SHA512 chain keyed with the literal string
* "Dilithium seed". Substrate's own `mnemonicToMiniSecret` is a different
* derivation entirely and is what the rest of this package reaches for by
* default; using it here yields a perfectly well-formed key for an account
* nobody owns.
*/
export function dilithiumPairFromMnemonic (mnemonic: string, password: string, path: string, type: DilithiumType): Keypair {
return keypairFromMnemonic(mnemonic, password, path, dilithiumSchemeFor(type));
}
@@ -0,0 +1,73 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DilithiumType } from '../dilithium.js';
/** Quantus BIP44 coin type. */
const COIN_TYPE = 189189;
/**
* The derivation path for an account index.
*
* ```text
* m/44'/189189'/<account>'/0'/<0 for ML-DSA-87 | 1 for ML-DSA-65>'
* ```
*
* Two things about this are unusual and both are deliberate: the account index
* sits at the third level rather than the last, and the trailing index carries
* the *scheme* rather than an address index. It is what `quantus-cli` and the
* mobile wallet already derive, so anything else produces addresses no other
* Quantus tool can find. Do not improve it.
*
* Every level is hardened. Lattice keys are not publicly derivable, so an
* unhardened level cannot mean what BIP-32 implies, and the hdwallet crate
* rejects one outright.
*/
export function dilithiumPath (type: DilithiumType, account = 0): string {
if (!Number.isInteger(account) || account < 0) {
throw new Error(`Invalid account index: ${account}`);
}
return `m/44'/${COIN_TYPE}'/${account}'/0'/${type === 'dilithium65' ? 1 : 0}'`;
}
/**
* Resolve the derivation part of a suri to a Quantus path.
*
* Accepted:
* - nothing account 0, the default `quantus-cli` uses
* - `//<n>` account index n
* - `//m/44'/189189'/…` that path verbatim, for anything the index form
* cannot express (the wormhole tree, say)
*
* Everything else throws, and that matters more than it looks. The suri syntax
* these strings come from was built for curve junctions, where `/foo` is a soft
* derivation and `//foo` a hard one over arbitrary bytes. Neither exists for
* ML-DSA. Quietly reinterpreting `//Alice` as *something* would hand the user an
* address no other tool derives and no seed phrase obviously recovers.
*/
export function dilithiumPathFromSuri (type: DilithiumType, derivePath: string): string {
if (!derivePath) {
return dilithiumPath(type);
}
if (!derivePath.startsWith('//')) {
throw new Error(`Soft derivation is not possible for ${type}; use //<index> or a full //m/44'/${COIN_TYPE}'/… path`);
}
const rest = derivePath.substring(2);
if (/^\d+$/.test(rest)) {
return dilithiumPath(type, parseInt(rest, 10));
}
if (rest.startsWith('m/')) {
if (/\d(?!')(?:\/|$)/.test(rest)) {
throw new Error(`Unhardened derivation is not possible for ${type}: ${rest}`);
}
return rest;
}
throw new Error(`Unsupported derivation path for ${type}: ${derivePath}`);
}
@@ -0,0 +1,32 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Sizes } from '@quantus/crypto';
import type { DilithiumType } from '../dilithium.js';
import { Scheme, sizes } from '@quantus/crypto';
/**
* The `@quantus/crypto` scheme selector for a keyring type.
*
* The mapping is the identity on the chain's signature-enum variant index:
* `Dilithium87` is variant 0 and `Dilithium65` is variant 1, and that byte is
* what a signed extrinsic carries. Nothing here invents a numbering.
*/
export function dilithiumSchemeFor (type: DilithiumType): Scheme {
return type === 'dilithium65'
? Scheme.MlDsa65
: Scheme.MlDsa87;
}
/**
* Key and signature lengths for a keyring type, read from the crate.
*
* Not written down here on purpose. These are consensus-critical the runtime
* decodes a fixed-size array with no compact length prefix so a constant that
* drifted from the crate would re-frame every byte after the signature while
* looking entirely healthy.
*/
export function dilithiumSizes (type: DilithiumType): Sizes {
return sizes(dilithiumSchemeFor(type));
}
@@ -0,0 +1,38 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DilithiumType } from '../dilithium.js';
import type { Keypair } from '../types.js';
import { sign, signatureWithPublicKey } from '@quantus/crypto';
import { u8aToU8a } from '@polkadot/util';
import { dilithiumSchemeFor } from './scheme.js';
/**
* Sign a message with an ML-DSA keypair under a FIPS 204 context.
*
* Returns `signature ‖ publicKey`, which is what the runtime's
* `DilithiumSignatureScheme` carries as a fixed-size array with **no** compact
* length prefix. The public key travels inside the signature because the account
* id is a one-way hash of it, so a verifier holding only an address cannot
* recover the key to check anything.
*
* `context` is mandatory and has no default. Extrinsics on spec >= 148 are
* verified under `QUANTUS_EXTRINSIC` and earlier ones under the empty context;
* a signature made under the wrong one is cryptographically valid, rejected by
* the chain, and indistinguishable from a correct one without asking a node.
* Defaulting would turn that into a silent failure at the worst possible moment,
* so the caller which is the only party that knows the spec version states it.
*/
export function dilithiumSign (message: string | Uint8Array, { publicKey, secretKey }: Partial<Keypair>, type: DilithiumType, context: Uint8Array): Uint8Array {
if (!publicKey || !secretKey) {
throw new Error('Expected a valid keypair to sign with');
}
const scheme = dilithiumSchemeFor(type);
const signature = sign(u8aToU8a(message), { publicKey, secretKey }, context, scheme);
return signatureWithPublicKey(signature, publicKey);
}
@@ -0,0 +1,48 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DilithiumType } from '../dilithium.js';
import { sizes, verify } from '@quantus/crypto';
import { u8aToU8a } from '@polkadot/util';
import { dilithiumAccountFromPublic } from './account.js';
import { dilithiumSchemeFor } from './scheme.js';
/**
* Verify a `signature ‖ publicKey` blob against an account id.
*
* This mirrors the runtime's `Verify::verify` rather than inventing a scheme:
* split the blob, hash the embedded public key with Poseidon2, check it equals
* the account being verified against, and only then check the signature. Both
* halves matter. Skipping the account check would accept a valid signature from
* *some* key rather than *the* key, which is the whole of the security property.
*
* `account` is the 32-byte account id not a public key. For every other scheme
* in this package those are the same bytes; here they are not.
*/
export function dilithiumVerify (message: string | Uint8Array, sigWithPublic: Uint8Array, account: Uint8Array, type: DilithiumType, context: Uint8Array): boolean {
const scheme = dilithiumSchemeFor(type);
const { publicKey: publicKeyLen, signature: signatureLen } = sizes(scheme);
if (sigWithPublic.length !== signatureLen + publicKeyLen) {
return false;
}
const signature = sigWithPublic.subarray(0, signatureLen);
const publicKey = sigWithPublic.subarray(signatureLen);
const derived = dilithiumAccountFromPublic(publicKey);
if (derived.length !== account.length) {
return false;
}
for (let i = 0; i < derived.length; i++) {
if (derived[i] !== account[i]) {
return false;
}
}
return verify(u8aToU8a(message), signature, publicKey, context, scheme);
}
@@ -1,18 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { compactAddLength, isU8a, stringToU8a, u8aConcat } from '@polkadot/util';
import { blake2AsU8a } from '../blake2/asU8a.js';
const HDKD = compactAddLength(stringToU8a('Ed25519HDKD'));
export function ed25519DeriveHard (seed: Uint8Array, chainCode: Uint8Array): Uint8Array {
if (!isU8a(chainCode) || chainCode.length !== 32) {
throw new Error('Invalid chainCode passed to derive');
}
return blake2AsU8a(
u8aConcat(HDKD, seed, chainCode)
);
}
-13
View File
@@ -1,13 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/**
* @summary Implements ed25519 operations
*/
export { ed25519DeriveHard } from './deriveHard.js';
export { ed25519PairFromRandom } from './pair/fromRandom.js';
export { ed25519PairFromSecret } from './pair/fromSecret.js';
export { ed25519PairFromSeed } from './pair/fromSeed.js';
export { ed25519PairFromString } from './pair/fromString.js';
export { ed25519Sign } from './sign.js';
export { ed25519Verify } from './verify.js';
@@ -1,28 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import type { Keypair } from '../../types.js';
import { ed25519PairFromRandom } from '../index.js';
describe('ed25519PairFromRandom', (): void => {
let keypair: Keypair;
beforeEach((): void => {
keypair = ed25519PairFromRandom();
});
it('generates a valid publicKey', (): void => {
expect(
keypair.publicKey
).toHaveLength(32);
});
it('generates a valid secretKey', (): void => {
expect(
keypair.secretKey
).toHaveLength(64);
});
});
@@ -1,25 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../../types.js';
import { randomAsU8a } from '../../random/index.js';
import { ed25519PairFromSeed } from './fromSeed.js';
/**
* @name ed25519PairFromRandom
* @summary Creates a new public/secret keypair.
* @description
* Returns a new generate object containing a `publicKey` & `secretKey`.
* @example
* <BR>
*
* ```javascript
* import { ed25519PairFromRandom } from '@polkadot/util-crypto';
*
* ed25519PairFromRandom(); // => { secretKey: [...], publicKey: [...] }
* ```
*/
export function ed25519PairFromRandom (): Keypair {
return ed25519PairFromSeed(randomAsU8a());
}
@@ -1,33 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { ed25519PairFromSecret } from '../index.js';
describe('ed25519PairFromSecret', (): void => {
const secretKey = new Uint8Array([
18, 52, 86, 120, 144, 18, 52, 86,
120, 144, 18, 52, 86, 120, 144, 18,
18, 52, 86, 120, 144, 18, 52, 86,
120, 144, 18, 52, 86, 120, 144, 18,
180, 114, 93, 155, 165, 255, 217, 82,
16, 250, 209, 11, 193, 10, 88, 218,
190, 190, 41, 193, 236, 252, 1, 152,
216, 214, 0, 41, 45, 138, 13, 53
]);
it('generates a valid publicKey/secretKey pair', (): void => {
expect(
ed25519PairFromSecret(secretKey)
).toEqual({
publicKey: new Uint8Array([
180, 114, 93, 155, 165, 255, 217, 82,
16, 250, 209, 11, 193, 10, 88, 218,
190, 190, 41, 193, 236, 252, 1, 152,
216, 214, 0, 41, 45, 138, 13, 53
]),
secretKey
});
});
});
@@ -1,29 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../../types.js';
/**
* @name ed25519PairFromSecret
* @summary Creates a new public/secret keypair from a secret.
* @description
* Returns a object containing a `publicKey` & `secretKey` generated from the supplied secret.
* @example
* <BR>
*
* ```javascript
* import { ed25519PairFromSecret } from '@polkadot/util-crypto';
*
* ed25519PairFromSecret(...); // => { secretKey: [...], publicKey: [...] }
* ```
*/
export function ed25519PairFromSecret (secretKey: Uint8Array): Keypair {
if (secretKey.length !== 64) {
throw new Error('Invalid secretKey provided');
}
return {
publicKey: secretKey.slice(32),
secretKey
};
}
@@ -1,42 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { stringToU8a } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { ed25519PairFromSeed } from '../index.js';
describe('ed25519PairFromSeed', (): void => {
// NOTE: Aligned with Rust test, b"12345678901234567890123456789012"
const TEST = stringToU8a('12345678901234567890123456789012');
const RESULT = {
publicKey: new Uint8Array([
0x2f, 0x8c, 0x61, 0x29, 0xd8, 0x16, 0xcf, 0x51,
0xc3, 0x74, 0xbc, 0x7f, 0x08, 0xc3, 0xe6, 0x3e,
0xd1, 0x56, 0xcf, 0x78, 0xae, 0xfb, 0x4a, 0x65,
0x50, 0xd9, 0x7b, 0x87, 0x99, 0x79, 0x77, 0xee
]),
secretKey: new Uint8Array([
49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50,
// public part
0x2f, 0x8c, 0x61, 0x29, 0xd8, 0x16, 0xcf, 0x51,
0xc3, 0x74, 0xbc, 0x7f, 0x08, 0xc3, 0xe6, 0x3e,
0xd1, 0x56, 0xcf, 0x78, 0xae, 0xfb, 0x4a, 0x65,
0x50, 0xd9, 0x7b, 0x87, 0x99, 0x79, 0x77, 0xee
])
};
beforeEach(async (): Promise<void> => {
await waitReady();
});
it('generates a valid publicKey/secretKey pair (u8a)', (): void => {
[true, false].forEach((onlyJs): void => {
expect(
ed25519PairFromSeed(TEST, onlyJs)
).toEqual(RESULT);
});
});
});
@@ -1,41 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../../types.js';
import { ed25519 } from '@noble/curves/ed25519';
import { hasBigInt, u8aConcatStrict } from '@polkadot/util';
import { ed25519KeypairFromSeed, isReady } from '@polkadot/wasm-crypto';
/**
* @name ed25519PairFromSeed
* @summary Creates a new public/secret keypair from a seed.
* @description
* Returns a object containing a `publicKey` & `secretKey` generated from the supplied seed.
* @example
* <BR>
*
* ```javascript
* import { ed25519PairFromSeed } from '@polkadot/util-crypto';
*
* ed25519PairFromSeed(...); // => { secretKey: [...], publicKey: [...] }
* ```
*/
export function ed25519PairFromSeed (seed: Uint8Array, onlyJs?: boolean): Keypair {
if (!hasBigInt || (!onlyJs && isReady())) {
const full = ed25519KeypairFromSeed(seed);
return {
publicKey: full.slice(32),
secretKey: full.slice(0, 64)
};
}
const publicKey = ed25519.getPublicKey(seed);
return {
publicKey,
secretKey: u8aConcatStrict([seed, publicKey])
};
}
@@ -1,17 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { ed25519PairFromString } from '../index.js';
describe('ed25519PairFromSeed', (): void => {
it('generates a valid publicKey/secretKey pair', (): void => {
expect(
ed25519PairFromString('test')
).toEqual({
publicKey: new Uint8Array([188, 108, 179, 142, 36, 142, 76, 87, 77, 193, 147, 139, 254, 110, 196, 217, 117, 233, 167, 165, 250, 150, 247, 237, 198, 68, 129, 4, 211, 209, 136, 48]),
secretKey: new Uint8Array([146, 139, 32, 54, 105, 67, 226, 175, 209, 30, 188, 14, 174, 46, 83, 169, 59, 241, 119, 164, 252, 243, 91, 204, 100, 213, 3, 112, 78, 101, 226, 2, 188, 108, 179, 142, 36, 142, 76, 87, 77, 193, 147, 139, 254, 110, 196, 217, 117, 233, 167, 165, 250, 150, 247, 237, 198, 68, 129, 4, 211, 209, 136, 48])
});
});
});
@@ -1,31 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../../types.js';
import { stringToU8a } from '@polkadot/util';
import { blake2AsU8a } from '../../blake2/asU8a.js';
import { ed25519PairFromSeed } from './fromSeed.js';
/**
* @name ed25519PairFromString
* @summary Creates a new public/secret keypair from a string.
* @description
* Returns a object containing a `publicKey` & `secretKey` generated from the supplied string. The string is hashed and the value used as the input seed.
* @example
* <BR>
*
* ```javascript
* import { ed25519PairFromString } from '@polkadot/util-crypto';
*
* ed25519PairFromString('test'); // => { secretKey: [...], publicKey: [...] }
* ```
*/
export function ed25519PairFromString (value: string): Keypair {
return ed25519PairFromSeed(
blake2AsU8a(
stringToU8a(value)
)
);
}
@@ -1,40 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { stringToU8a } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { perfWasm } from '../test/index.js';
import { ed25519PairFromSeed, ed25519Sign } from './index.js';
const PAIR = ed25519PairFromSeed(
stringToU8a('12345678901234567890123456789012')
);
describe('ed25519Sign', (): void => {
beforeEach(async (): Promise<void> => {
await waitReady();
});
for (const onlyJs of [false, true]) {
describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => {
it('returns a valid signature for the message', (): void => {
expect(
ed25519Sign(
new Uint8Array([0x61, 0x62, 0x63, 0x64]),
PAIR,
onlyJs
)
).toEqual(
new Uint8Array([28, 58, 206, 239, 249, 70, 59, 191, 166, 40, 219, 218, 235, 170, 25, 79, 10, 94, 9, 197, 34, 126, 1, 150, 246, 68, 28, 238, 36, 26, 172, 163, 168, 90, 202, 211, 126, 246, 57, 212, 43, 24, 88, 197, 240, 113, 118, 76, 37, 81, 91, 110, 236, 50, 144, 134, 100, 223, 220, 238, 34, 185, 211, 7])
);
});
});
}
perfWasm('ed25519Sign', 250, (input, onlyJs) =>
ed25519Sign(input, PAIR, onlyJs)
);
});
-38
View File
@@ -1,38 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../types.js';
import { ed25519 } from '@noble/curves/ed25519';
import { hasBigInt, u8aToU8a } from '@polkadot/util';
import { ed25519Sign as wasmSign, isReady } from '@polkadot/wasm-crypto';
/**
* @name ed25519Sign
* @summary Signs a message using the supplied secretKey
* @description
* Returns message signature of `message`, using the `secretKey`.
* @example
* <BR>
*
* ```javascript
* import { ed25519Sign } from '@polkadot/util-crypto';
*
* ed25519Sign([...], [...]); // => [...]
* ```
*/
export function ed25519Sign (message: string | Uint8Array, { publicKey, secretKey }: Partial<Keypair>, onlyJs?: boolean): Uint8Array {
if (!secretKey) {
throw new Error('Expected a valid secretKey');
} else if (!publicKey) {
throw new Error('Expected a valid publicKey');
}
const messageU8a = u8aToU8a(message);
const privateU8a = secretKey.subarray(0, 32);
return !hasBigInt || (!onlyJs && isReady())
? wasmSign(publicKey, privateU8a, messageU8a)
: ed25519.sign(messageU8a, privateU8a);
}
@@ -1,84 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { stringToU8a } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { ed25519PairFromSeed, ed25519Verify } from './index.js';
describe('ed25519Verify', (): void => {
let publicKey: Uint8Array;
let signature: Uint8Array;
beforeEach(async (): Promise<void> => {
await waitReady();
publicKey = ed25519PairFromSeed(
stringToU8a('12345678901234567890123456789012')
).publicKey;
signature = new Uint8Array([28, 58, 206, 239, 249, 70, 59, 191, 166, 40, 219, 218, 235, 170, 25, 79, 10, 94, 9, 197, 34, 126, 1, 150, 246, 68, 28, 238, 36, 26, 172, 163, 168, 90, 202, 211, 126, 246, 57, 212, 43, 24, 88, 197, 240, 113, 118, 76, 37, 81, 91, 110, 236, 50, 144, 134, 100, 223, 220, 238, 34, 185, 211, 7]);
});
for (const onlyJs of [false, true]) {
describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => {
it('validates a correctly signed message', (): void => {
expect(
ed25519Verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64]),
signature,
publicKey,
onlyJs
)
).toEqual(true);
});
it('fails a correctly signed message (message changed)', (): void => {
expect(
ed25519Verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]),
signature,
publicKey,
onlyJs
)
).toEqual(false);
});
it('fails a correctly signed message (signature changed)', (): void => {
signature[0] = 0xff;
expect(
ed25519Verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64]),
signature,
publicKey,
onlyJs
)
).toEqual(false);
});
it('throws error when publicKey lengths do not match', (): void => {
expect(
() => ed25519Verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64]),
signature,
new Uint8Array([1, 2]),
onlyJs
)
).toThrow(/Invalid publicKey/);
});
it('throws error when signature lengths do not match', (): void => {
expect(
() => ed25519Verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64]),
new Uint8Array([1, 2]),
publicKey,
onlyJs
)
).toThrow(/Invalid signature/);
});
});
}
});
@@ -1,41 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ed25519 } from '@noble/curves/ed25519';
import { hasBigInt, u8aToU8a } from '@polkadot/util';
import { ed25519Verify as wasmVerify, isReady } from '@polkadot/wasm-crypto';
/**
* @name ed25519Sign
* @summary Verifies the signature on the supplied message.
* @description
* Verifies the `signature` on `message` with the supplied `publicKey`. Returns `true` on sucess, `false` otherwise.
* @example
* <BR>
*
* ```javascript
* import { ed25519Verify } from '@polkadot/util-crypto';
*
* ed25519Verify([...], [...], [...]); // => true/false
* ```
*/
export function ed25519Verify (message: string | Uint8Array, signature: string | Uint8Array, publicKey: string | Uint8Array, onlyJs?: boolean): boolean {
const messageU8a = u8aToU8a(message);
const publicKeyU8a = u8aToU8a(publicKey);
const signatureU8a = u8aToU8a(signature);
if (publicKeyU8a.length !== 32) {
throw new Error(`Invalid publicKey, received ${publicKeyU8a.length}, expected 32`);
} else if (signatureU8a.length !== 64) {
throw new Error(`Invalid signature, received ${signatureU8a.length} bytes, expected 64`);
}
try {
return !hasBigInt || (!onlyJs && isReady())
? wasmVerify(signatureU8a, messageU8a, publicKeyU8a)
: ed25519.verify(signatureU8a, messageU8a, publicKeyU8a);
} catch {
return false;
}
}
@@ -17,7 +17,7 @@ describe('formatAddress', () => {
it('returns fails on invalid address', () => {
expect(
() => ethereumEncode('0xnotaddress')
).toThrow(/Invalid address or publicKey provided/);
).toThrow(/Invalid address provided/);
});
it('converts lowercase to the checksummed address', () => {
@@ -36,16 +36,15 @@ describe('formatAddress', () => {
describe('from publicKey', (): void => {
const ADDRESS = '0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887';
it('encodes a compressed publicKey', (): void => {
// Deriving an address from a secp256k1 key needs the curve, which is gone
// (quantus/common#6). Refused with the reason, not reported as a bad length.
it('refuses a compressed or expanded secp256k1 publicKey', (): void => {
expect(
ethereumEncode('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')
).toEqual(ADDRESS);
});
it('encodes an expanded publicKey', (): void => {
() => ethereumEncode('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')
).toThrow(/secp256k1 is quantum-unsafe and has been removed/);
expect(
ethereumEncode('0x04b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
).toEqual(ADDRESS);
() => ethereumEncode('0x04b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
).toThrow(/secp256k1 is quantum-unsafe and has been removed/);
});
it('encodes a pre-hashed key', (): void => {
+13 -7
View File
@@ -6,13 +6,17 @@ import type { HexString } from '@polkadot/util/types';
import { u8aToHex, u8aToU8a } from '@polkadot/util';
import { keccakAsU8a } from '../keccak/index.js';
import { secp256k1Expand } from '../secp256k1/index.js';
/**
* Formats an Ethereum address for display: EIP-55 checksum casing.
*
* Kept, though secp256k1 is gone, because it holds no key: `@polkadot/types`
* and the identicon renderer format 20-byte addresses with it, and a wallet has
* to be able to *show* an Ethereum address to recognise and refuse one. What is
* gone is deriving an address from a secp256k1 public key, which needed the
* curve. quantus/common#6
*/
function getH160 (u8a: Uint8Array): Uint8Array {
if ([33, 65].includes(u8a.length)) {
u8a = keccakAsU8a(secp256k1Expand(u8a));
}
return u8a.slice(-20);
}
@@ -23,8 +27,10 @@ export function ethereumEncode (addressOrPublic?: string | Uint8Array): HexStrin
const u8aAddress = u8aToU8a(addressOrPublic);
if (![20, 32, 33, 65].includes(u8aAddress.length)) {
throw new Error(`Invalid address or publicKey provided, received ${u8aAddress.length} bytes input`);
if ([33, 65].includes(u8aAddress.length)) {
throw new Error('Deriving an Ethereum address from a secp256k1 public key is not supported: secp256k1 is quantum-unsafe and has been removed');
} else if (![20, 32].includes(u8aAddress.length)) {
throw new Error(`Invalid address provided, received ${u8aAddress.length} bytes input`);
}
const address = u8aToHex(getH160(u8aAddress), -1, false);
@@ -1,54 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { mnemonicToLegacySeed } from '@polkadot/util-crypto';
import { hdEthereum } from './index.js';
describe('hdEthereum', (): void => {
const PHRASE = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
const derivationPath = 'm/44\'/60\'/0\'/0/0';
const PUBLIC = new Uint8Array([
3, 118, 64, 77, 247, 27, 4, 157,
236, 206, 251, 221, 230, 244, 154, 147,
189, 131, 249, 169, 102, 78, 3, 185,
153, 19, 89, 40, 24, 25, 139, 131,
93
]);
const SECRET = new Uint8Array([
166, 162, 203, 17, 2, 206, 110, 176,
18, 102, 230, 144, 90, 158, 25, 232,
43, 180, 176, 49, 189, 149, 3, 71,
243, 228, 223, 104, 125, 132, 58, 228
]
);
const PUBLICDERIVED = new Uint8Array([
3, 129, 53, 27, 27, 70, 210, 96,
43, 9, 146, 187, 93, 85, 49, 249,
193, 105, 107, 8, 18, 254, 178, 83,
75, 104, 132, 173, 196, 126, 46, 29,
139
]);
const SECRETDERIVED = new Uint8Array([
7, 13, 195, 17, 115, 0, 1, 25,
24, 226, 107, 2, 23, 105, 69, 204,
21, 195, 213, 72, 207, 73, 253, 132,
24, 217, 127, 147, 175, 105, 158, 70
]);
it('derives the right key pair from a mnemonic', (): void => {
const key = hdEthereum(mnemonicToLegacySeed(PHRASE, '', false, 64));
expect(key.publicKey).toEqual(PUBLIC);
expect(key.secretKey).toEqual(SECRET);
});
it('derives the right key pair from a mnemonic and a derivation path', (): void => {
const key = hdEthereum(mnemonicToLegacySeed(PHRASE, '', false, 64), derivationPath);
expect(key.publicKey).toEqual(PUBLICDERIVED);
expect(key.secretKey).toEqual(SECRETDERIVED);
});
});
@@ -1,69 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../../types.js';
import { bnToU8a, stringToU8a, u8aConcat } from '@polkadot/util';
import { BN_BE_32_OPTS } from '../../bn.js';
import { hmacShaAsU8a } from '../../hmac/index.js';
import { secp256k1PairFromSeed, secp256k1PrivateKeyTweakAdd } from '../../secp256k1/index.js';
import { HARDENED, hdValidatePath } from '../validatePath.js';
interface CodedKeypair extends Keypair {
chainCode: Uint8Array;
}
const MASTER_SECRET = stringToU8a('Bitcoin seed');
function createCoded (secretKey: Uint8Array, chainCode: Uint8Array): CodedKeypair {
return {
chainCode,
publicKey: secp256k1PairFromSeed(secretKey).publicKey,
secretKey
};
}
function deriveChild (hd: CodedKeypair, index: number): CodedKeypair {
const indexBuffer = bnToU8a(index, BN_BE_32_OPTS);
const data = index >= HARDENED
? u8aConcat(new Uint8Array(1), hd.secretKey, indexBuffer)
: u8aConcat(hd.publicKey, indexBuffer);
try {
const I = hmacShaAsU8a(hd.chainCode, data, 512);
return createCoded(
secp256k1PrivateKeyTweakAdd(hd.secretKey, I.slice(0, 32)),
I.slice(32)
);
} catch {
// In case parse256(IL) >= n or ki == 0, proceed with the next value for i
return deriveChild(hd, index + 1);
}
}
export function hdEthereum (seed: Uint8Array, path = ''): Keypair {
const I = hmacShaAsU8a(MASTER_SECRET, seed, 512);
let hd = createCoded(I.slice(0, 32), I.slice(32));
if (!path || path === 'm' || path === 'M' || path === "m'" || path === "M'") {
return hd;
}
if (!hdValidatePath(path)) {
throw new Error('Invalid derivation path');
}
const parts = path.split('/').slice(1);
for (const p of parts) {
hd = deriveChild(hd, parseInt(p, 10) + (
(p.length > 1) && p.endsWith("'")
? HARDENED
: 0
));
}
return hd;
}
-2
View File
@@ -1,6 +1,4 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { hdEthereum } from './ethereum/index.js';
export { hdLedger } from './ledger/index.js';
export { hdValidatePath } from './validatePath.js';
@@ -1,34 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { BN_EIGHT, bnToU8a, u8aConcat, u8aToBn } from '@polkadot/util';
import { BN_LE_32_OPTS, BN_LE_512_OPTS, BN_LE_OPTS } from '../../bn.js';
import { hmacShaAsU8a } from '../../hmac/index.js';
// performs hard-only derivation on the xprv
export function ledgerDerivePrivate (xprv: Uint8Array, index: number): Uint8Array {
const kl = xprv.subarray(0, 32);
const kr = xprv.subarray(32, 64);
const cc = xprv.subarray(64, 96);
const data = u8aConcat([0], kl, kr, bnToU8a(index, BN_LE_32_OPTS));
const z = hmacShaAsU8a(cc, data, 512);
data[0] = 0x01;
return u8aConcat(
bnToU8a(
u8aToBn(kl, BN_LE_OPTS).iadd(
u8aToBn(z.subarray(0, 28), BN_LE_OPTS).imul(BN_EIGHT)
),
BN_LE_512_OPTS
).subarray(0, 32),
bnToU8a(
u8aToBn(kr, BN_LE_OPTS).iadd(
u8aToBn(z.subarray(32, 64), BN_LE_OPTS)
),
BN_LE_512_OPTS
).subarray(0, 32),
hmacShaAsU8a(cc, data, 512).subarray(32, 64)
);
}
@@ -1,64 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { u8aToHex } from '@polkadot/util';
import { hdLedger } from '../index.js';
const MNE_0 = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const MNE_1 = 'open jelly jeans corn ketchup supreme brief element armed lens vault weather original scissors rug priority vicious lesson raven spot gossip powder person volcano';
const MNE_P = `${MNE_1} testing`;
const TESTS = {
Kusama: {
slip44: 0x01b2,
tests: [
{
ed25519: '0x98cb4e14e0e08ea876f88d728545ea7572dc07dbbe69f1731c418fb827e69d41',
index: [0, 0],
mnemonic: MNE_0
},
{
ed25519: '0x70e9010e84c81095aaa5f63b1c5a6a66a1dcbec017a23c2f3b7a1b08fe5ea65a',
index: [0, 0],
mnemonic: MNE_1
},
{
ed25519: '0xf06730efb1e6ea59ac752a7c3620fade3909062fb88597856cc3af72045fa65a',
index: [5, 7],
mnemonic: MNE_1
}
]
},
Polkadot: {
slip44: 0x0162,
tests: [
{
ed25519: '0xe8c68348586d53e4e8d1a864b0e4e17c75e4eb06e0c63c1432bef2ba29e69d41',
index: [0, 0],
mnemonic: MNE_0
},
{
ed25519: '0x3890e8db837eba3f8f25215c753e1091062298ce671a51441e7ef89a7adc4f48',
index: [0, 0],
mnemonic: MNE_P
}
]
}
};
describe('ledgerDerive', (): void => {
Object.entries(TESTS).forEach(([network, { slip44, tests }]): void => {
tests.forEach(({ ed25519, index: [account, address], mnemonic }, index): void => {
it(`derives a known ed25519 seed for ${network} (${index})`, (): void => {
expect(u8aToHex(
hdLedger(mnemonic, `m/44'/${slip44}'/${account}'/0'/${address}'`)
.secretKey
.slice(0, 32)
)).toEqual(ed25519);
});
});
});
});
@@ -1,42 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../../types.js';
import { ed25519PairFromSeed } from '../../ed25519/index.js';
import { mnemonicValidate } from '../../mnemonic/index.js';
import { HARDENED, hdValidatePath } from '../validatePath.js';
import { ledgerDerivePrivate } from './derivePrivate.js';
import { ledgerMaster } from './master.js';
export function hdLedger (_mnemonic: string, path: string): Keypair {
const words = _mnemonic
.split(' ')
.map((s) => s.trim())
.filter((s) => s);
if (![12, 24, 25].includes(words.length)) {
throw new Error('Expected a mnemonic with 24 words (or 25 including a password)');
}
const [mnemonic, password] = words.length === 25
? [words.slice(0, 24).join(' '), words[24]]
: [words.join(' '), ''];
if (!mnemonicValidate(mnemonic)) {
throw new Error('Invalid mnemonic passed to ledger derivation');
} else if (!hdValidatePath(path)) {
throw new Error('Invalid derivation path');
}
const parts = path.split('/').slice(1);
let seed = ledgerMaster(mnemonic, password);
for (const p of parts) {
const n = parseInt(p.replace(/'$/, ''), 10);
seed = ledgerDerivePrivate(seed, (n < HARDENED) ? (n + HARDENED) : n);
}
return ed25519PairFromSeed(seed.slice(0, 32));
}
@@ -1,19 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { u8aToHex } from '@polkadot/util';
import { ledgerMaster } from './master.js';
const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const XPRV = '0x402b03cd9c8bed9ba9f9bd6cd9c315ce9fcc59c7c25d37c85a36096617e69d418e35cb4a3b737afd007f0688618f21a8831643c0e6c77fc33c06026d2a0fc93832596435e70647d7d98ef102a32ea40319ca8fb6c851d7346d3bd8f9d1492658';
describe('ledgerDerive', (): void => {
it('derives a known master xprv', (): void => {
expect(u8aToHex(
ledgerMaster(MNEMONIC)
)).toEqual(XPRV);
});
});
@@ -1,26 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { u8aConcat } from '@polkadot/util';
import { hmacShaAsU8a } from '../../hmac/index.js';
import { mnemonicToSeedSync } from '../../mnemonic/bip39.js';
const ED25519_CRYPTO = 'ed25519 seed';
// gets an xprv from a mnemonic
export function ledgerMaster (mnemonic: string, password?: string): Uint8Array {
const seed = mnemonicToSeedSync(mnemonic, password);
const chainCode = hmacShaAsU8a(ED25519_CRYPTO, new Uint8Array([1, ...seed]), 256);
let priv;
while (!priv || (priv[31] & 0b0010_0000)) {
priv = hmacShaAsU8a(ED25519_CRYPTO, priv || seed, 512);
}
priv[0] &= 0b1111_1000;
priv[31] &= 0b0111_1111;
priv[31] |= 0b0100_0000;
return u8aConcat(priv, chainCode);
}
-28
View File
@@ -1,28 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair, KeypairType } from '../types.js';
import type { DeriveJunction } from './DeriveJunction.js';
import { keyHdkdEcdsa } from './hdkdEcdsa.js';
import { keyHdkdEd25519 } from './hdkdEd25519.js';
import { keyHdkdSr25519 } from './hdkdSr25519.js';
const generators = {
ecdsa: keyHdkdEcdsa,
ed25519: keyHdkdEd25519,
// FIXME This is Substrate-compatible, not Ethereum-compatible
ethereum: keyHdkdEcdsa,
sr25519: keyHdkdSr25519
};
export function keyFromPath (pair: Keypair, path: DeriveJunction[], type: KeypairType): Keypair {
const keyHdkd = generators[type];
let result = pair;
for (const junction of path) {
result = keyHdkd(result, junction);
}
return result;
}
@@ -1,17 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../types.js';
import type { DeriveJunction } from './DeriveJunction.js';
export function createSeedDeriveFn (fromSeed: (seed: Uint8Array) => Keypair, derive: (seed: Uint8Array, chainCode: Uint8Array) => Uint8Array): (keypair: Keypair, junction: DeriveJunction) => Keypair {
return (keypair: Keypair, { chainCode, isHard }: DeriveJunction): Keypair => {
if (!isHard) {
throw new Error('A soft key was found in the path and is not supported');
}
return fromSeed(
derive(keypair.secretKey.subarray(0, 32), chainCode)
);
};
}
@@ -1,8 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { secp256k1DeriveHard } from '../secp256k1/deriveHard.js';
import { secp256k1PairFromSeed } from '../secp256k1/pair/fromSeed.js';
import { createSeedDeriveFn } from './hdkdDerive.js';
export const keyHdkdEcdsa = /*#__PURE__*/ createSeedDeriveFn(secp256k1PairFromSeed, secp256k1DeriveHard);
@@ -1,7 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ed25519DeriveHard, ed25519PairFromSeed } from '../ed25519/index.js';
import { createSeedDeriveFn } from './hdkdDerive.js';
export const keyHdkdEd25519 = /*#__PURE__*/ createSeedDeriveFn(ed25519PairFromSeed, ed25519DeriveHard);
@@ -1,14 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Keypair } from '../types.js';
import type { DeriveJunction } from './DeriveJunction.js';
import { sr25519DeriveHard } from '../sr25519/deriveHard.js';
import { sr25519DeriveSoft } from '../sr25519/deriveSoft.js';
export function keyHdkdSr25519 (keypair: Keypair, { chainCode, isSoft }: DeriveJunction): Keypair {
return isSoft
? sr25519DeriveSoft(keypair, chainCode)
: sr25519DeriveHard(keypair, chainCode);
}
-4
View File
@@ -6,7 +6,3 @@
*/
export { keyExtractPath } from './extractPath.js';
export { keyExtractSuri } from './extractSuri.js';
export { keyFromPath } from './fromPath.js';
export { keyHdkdEcdsa } from './hdkdEcdsa.js';
export { keyHdkdEd25519 } from './hdkdEd25519.js';
export { keyHdkdSr25519 } from './hdkdSr25519.js';
@@ -3,6 +3,10 @@
/// <reference types="@polkadot/dev-test/globals.d.ts" />
// The standard BIP39 test vectors (Trezor): mnemonic, entropy, seed, and
// upstream's sr25519 expanded secret. Moved here from sr25519/ when that
// directory was removed (quantus/common#6); the mnemonic and entropy columns are
// what is still used.
// mnemonic, entropy, seed, secret (expanded)
type Test = [string, string, string, string];
@@ -7,5 +7,4 @@
export { mnemonicGenerate } from './generate.js';
export { mnemonicToEntropy } from './toEntropy.js';
export { mnemonicToLegacySeed } from './toLegacySeed.js';
export { mnemonicToMiniSecret } from './toMiniSecret.js';
export { mnemonicValidate } from './validate.js';
@@ -6,8 +6,8 @@
import { u8aToHex } from '@polkadot/util';
import { cryptoWaitReady } from '../index.js';
import tests from '../sr25519/pair/testing.spec.js';
import { french as frenchWords } from './wordlists/index.js';
import tests from './bip39Vectors.spec.js';
import { mnemonicToEntropy } from './toEntropy.js';
await cryptoWaitReady();
@@ -1,67 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { u8aEq, u8aToHex } from '@polkadot/util';
import { cryptoWaitReady } from '../index.js';
import tests from '../sr25519/pair/testing.spec.js';
import { korean as koreanWords } from './wordlists/index.js';
import { mnemonicToMiniSecret } from './toMiniSecret.js';
const MNEMONIC = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
const SEED = '0x4d1ab2a57929edfd018aaa974e62ed557e3f54b4104acabedf73c8f5a1dbb029';
await cryptoWaitReady();
describe('mnemonicToMiniSecret', (): void => {
for (const password of [undefined, 'foo', 'bar']) {
it(`generates Wasm & Js equivalents for password=${password || 'undefined'}`, (): void => {
expect(
u8aEq(
mnemonicToMiniSecret(MNEMONIC, password, undefined, true),
mnemonicToMiniSecret(MNEMONIC, password, undefined, false)
)
).toEqual(true);
});
}
it('creates a known minisecret from a non-english mnemonic', (): void => {
const mnemonic = '엉덩이 능동적 숫자 팩시밀리 비난 서적 파출소 도움 독창적 인생 상류 먼지 답변 음반 수박 사업 노란색 공사 우체국 특급 도대체 금지 굉장히 고무신';
expect(
() => mnemonicToMiniSecret(mnemonic, 'testing')
).toThrow();
expect(
u8aToHex(mnemonicToMiniSecret(mnemonic, 'testing', koreanWords))
).toEqual('0xefa278a62535581767a2f49cb542ed91b65fb911e1b05e7a09c702b257f10c13');
});
for (const onlyJs of [false, true]) {
describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => {
it('generates a valid seed', (): void => {
expect(
u8aToHex(mnemonicToMiniSecret(MNEMONIC, undefined, undefined, onlyJs))
).toEqual(SEED);
});
it('fails with non-mnemonics', (): void => {
expect(
() => mnemonicToMiniSecret('foo bar baz', undefined, undefined, onlyJs)
).toThrow(/mnemonic specified/);
});
tests.forEach(([mnemonic, , seed], index): void => {
it(`Created correct seed for ${index}`, (): void => {
expect(
u8aToHex(mnemonicToMiniSecret(mnemonic, 'Substrate', undefined, onlyJs))
).toEqual(
// mini returned here, only check first 32-bytes (64 hex + 2 prefix)
seed.substring(0, 66)
);
});
});
});
}
});
@@ -1,23 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { stringToU8a } from '@polkadot/util';
import { bip39ToMiniSecret, isReady } from '@polkadot/wasm-crypto';
import { pbkdf2Encode } from '../pbkdf2/index.js';
import { mnemonicToEntropy } from './toEntropy.js';
import { mnemonicValidate } from './validate.js';
export function mnemonicToMiniSecret (mnemonic: string, password = '', wordlist?: string[], onlyJs?: boolean): Uint8Array {
if (!mnemonicValidate(mnemonic, wordlist, onlyJs)) {
throw new Error('Invalid bip39 mnemonic specified');
} else if (!wordlist && !onlyJs && isReady()) {
return bip39ToMiniSecret(mnemonic, password);
}
const entropy = mnemonicToEntropy(mnemonic, wordlist);
const salt = stringToU8a(`mnemonic${password}`);
// return the first 32 bytes as the seed
return pbkdf2Encode(entropy, salt).password.slice(0, 32);
}
@@ -1,64 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { arrayRange, u8aEq } from '@polkadot/util';
import { cryptoWaitReady, ed25519PairFromSeed, mnemonicGenerate, mnemonicToMiniSecret, sr25519PairFromSeed } from '../index.js';
// NOTE: This basically controls how long stuff runs for, YMMV
//
// - 100 runs with 5 checks, takes 2mins on _my_ machine
// - 10_000 runs with 5 checks should be ~3hrs
const NUM_RUNS = 100;
const NUM_CHECKS = 5;
await cryptoWaitReady();
// generate either a JS or WASM mnemonic
for (const onlyJsMnemonic of [false, true]) {
describe(`mnemonicToMiniSecret (conpare), onlyJs${(onlyJsMnemonic && 'true') || 'false'}`, (): void => {
for (const i of arrayRange(NUM_RUNS)) {
// loop through lots of mnemonics
describe(`run=${i + 1}`, (): void => {
// compare both JS and WASM outputs against original
for (const onlyJsMini of [false, true]) {
describe(`onlyJsMini=${(onlyJsMini && 'true') || 'false'}`, (): void => {
// NOTE we cannot actually use the onlyJsMnemonic flag here
const mnemonic = mnemonicGenerate(12);
describe(`${mnemonic}`, (): void => {
// do iterations to check and re-check that all matches
for (const count of arrayRange(NUM_CHECKS)) {
it(`check=${count + 1}`, (): void => {
const minisecret = mnemonicToMiniSecret(mnemonic, count ? `${count}` : '', undefined, onlyJsMnemonic);
const edpub = ed25519PairFromSeed(minisecret).publicKey;
const srpub = sr25519PairFromSeed(minisecret).publicKey;
const testmini = mnemonicToMiniSecret(mnemonic, count ? `${count}` : '', undefined, onlyJsMini);
// explicit minisecret compare
expect(
u8aEq(minisecret, testmini)
).toEqual(true);
// compare the sr25519 keypair generated
expect(
u8aEq(srpub, sr25519PairFromSeed(testmini).publicKey)
).toEqual(true);
// compare ed both in WASM and JS
[true, false].forEach((onlyJsEd): void => {
expect(
u8aEq(edpub, ed25519PairFromSeed(testmini, onlyJsEd).publicKey)
).toEqual(true);
});
});
}
});
});
}
});
}
});
}
+1 -1
View File
@@ -3,4 +3,4 @@
// Do not edit, auto-generated by @polkadot/dev
export const packageInfo = { name: '@polkadot/util-crypto', path: 'auto', type: 'auto', version: '14.0.3' };
export const packageInfo = { name: '@polkadot/util-crypto', path: 'auto', type: 'auto', version: '14.0.3-quantus.3' };
@@ -1,47 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { hexToU8a } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { perfWasm } from '../test/index.js';
import { secp256k1Compress } from './index.js';
describe('secp256k1Compress', (): void => {
beforeEach(async (): Promise<void> => {
await waitReady();
});
for (const onlyJs of [false, true]) {
describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => {
it('returns a compressed key as-is', (): void => {
expect(
secp256k1Compress(
hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077'),
onlyJs
)
).toEqual(
hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')
);
});
it('compresses a known key', (): void => {
expect(
secp256k1Compress(
hexToU8a('0x04b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af'),
onlyJs
)
).toEqual(
hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')
);
});
});
}
perfWasm('secp256k1Compress', 100000, (input, onlyJs) => secp256k1Compress(input, onlyJs), [[
hexToU8a('0x04b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
]]
);
});
@@ -1,21 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { secp256k1 } from '@noble/curves/secp256k1';
import { hasBigInt } from '@polkadot/util';
import { isReady, secp256k1Compress as wasm } from '@polkadot/wasm-crypto';
export function secp256k1Compress (publicKey: Uint8Array, onlyJs?: boolean): Uint8Array {
if (![33, 65].includes(publicKey.length)) {
throw new Error(`Invalid publicKey provided, received ${publicKey.length} bytes input`);
}
if (publicKey.length === 33) {
return publicKey;
}
return !hasBigInt || (!onlyJs && isReady())
? wasm(publicKey)
: secp256k1.ProjectivePoint.fromHex(publicKey).toRawBytes(true);
}
@@ -1,17 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { compactAddLength, isU8a, stringToU8a, u8aConcat } from '@polkadot/util';
import { blake2AsU8a } from '../blake2/asU8a.js';
const HDKD = compactAddLength(stringToU8a('Secp256k1HDKD'));
export function secp256k1DeriveHard (seed: Uint8Array, chainCode: Uint8Array): Uint8Array {
if (!isU8a(chainCode) || chainCode.length !== 32) {
throw new Error('Invalid chainCode passed to derive');
}
// NOTE This is specific to the Substrate HDD derivation, so always use the blake2 hasher
return blake2AsU8a(u8aConcat(HDKD, seed, chainCode), 256);
}
@@ -1,47 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { hexToU8a } from '@polkadot/util';
import { waitReady } from '@polkadot/wasm-crypto';
import { perfWasm } from '../test/index.js';
import { secp256k1Expand } from './index.js';
describe('secp256k1Expand', (): void => {
beforeEach(async (): Promise<void> => {
await waitReady();
});
for (const onlyJs of [false, true]) {
describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => {
it('expands a known key', (): void => {
expect(
secp256k1Expand(
hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077'),
onlyJs
)
).toEqual(
hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
);
});
it('expands a known full key', (): void => {
expect(
secp256k1Expand(
hexToU8a('0x04b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af'),
onlyJs
)
).toEqual(
hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
);
});
});
}
perfWasm('secp256k1Expand', 2000, (input, onlyJs) => secp256k1Expand(input, onlyJs), [[
hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')
]]
);
});
@@ -1,30 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { secp256k1 } from '@noble/curves/secp256k1';
import { bnToU8a, hasBigInt, u8aConcat } from '@polkadot/util';
import { isReady, secp256k1Expand as wasm } from '@polkadot/wasm-crypto';
import { BN_BE_256_OPTS } from '../bn.js';
export function secp256k1Expand (publicKey: Uint8Array, onlyJs?: boolean): Uint8Array {
if (![33, 65].includes(publicKey.length)) {
throw new Error(`Invalid publicKey provided, received ${publicKey.length} bytes input`);
}
if (publicKey.length === 65) {
return publicKey.subarray(1);
}
if (!hasBigInt || (!onlyJs && isReady())) {
return wasm(publicKey).subarray(1);
}
const { x, y } = secp256k1.ProjectivePoint.fromHex(publicKey);
return u8aConcat(
bnToU8a(x, BN_BE_256_OPTS),
bnToU8a(y, BN_BE_256_OPTS)
);
}
@@ -1,24 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { hasher } from './hasher.js';
describe('hasher', (): void => {
it('creates a blake2 hash', (): void => {
expect(
hasher('blake2', 'abc')
).toEqual(
new Uint8Array([189, 221, 129, 60, 99, 66, 57, 114, 49, 113, 239, 63, 238, 152, 87, 155, 148, 150, 78, 59, 177, 203, 62, 66, 114, 98, 200, 192, 104, 213, 35, 25])
);
});
it('creates a keccak hash', (): void => {
expect(
hasher('keccak', 'abc')
).toEqual(
new Uint8Array([78, 3, 101, 122, 234, 69, 169, 79, 199, 212, 123, 168, 38, 200, 214, 103, 192, 209, 230, 227, 58, 100, 160, 54, 236, 68, 245, 143, 161, 45, 108, 69])
);
});
});
@@ -1,10 +0,0 @@
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { secp256k1Compress } from './compress.js';
export { secp256k1Expand } from './expand.js';
export { secp256k1PairFromSeed } from './pair/fromSeed.js';
export { secp256k1Recover } from './recover.js';
export { secp256k1Sign } from './sign.js';
export { secp256k1PrivateKeyTweakAdd } from './tweakAdd.js';
export { secp256k1Verify } from './verify.js';

Some files were not shown because too many files have changed in this diff Show More