Nothing is injectable, which is the whole reason this repo is a fork rather than a wrapper. Add arms; do not restructure the maps (see the rebasability convention in quantus/extension#1).
TYPE_PREFIX is already the right shape
A happy accident worth not breaking: pair.sign(msg, { withType: true }) prepends TYPE_PREFIX[type], and the chain's DilithiumSignatureScheme variant byte is exactly that — dilithium87: [0], dilithium65: [1]. With TYPE_SIGNATURE returning sig ‖ pk, the bytes @polkadot/types hands to a dapp are already the correct SCALE encoding of the runtime's signature enum, with no special-casing anywhere upstream of here.
TYPE_ADDRESS is the interesting one
Every existing arm is either identity or a cheap re-encoding, because on Substrate the address is the public key. For Quantus it is Poseidon2(publicKey) — 1952 or 2592 bytes in, 32 bytes out, one-way.
That breaks an invariant createPair currently relies on without stating it: a locked pair can still report its address, because the public key is in memory even when the secret is not. For Quantus that still holds — the public key is enough to compute the address — but the moment the public key isn't available, it does not. Which is exactly the situation #3 is about, and the two issues should be designed together: createPair will likely need to accept a precomputed account id for pairs restored from JSON.
Also
decodePkcs8 branches on decoded.secretKey.length === 64 to decide "secret key" vs "seed". A 4032- or 4896-byte ML-DSA secret takes the wrong branch and gets fed to TYPE_FROM_SEED as if it were entropy. Make the check type-aware rather than length-magic.
a pair created from a known mnemonic reports the SS58 address that quantus-cli reports for it
pair.sign(payload, { withType: true }) produces bytes that the runtime's Verify::verify accepts
the sr25519/ed25519/ecdsa/ethereum paths are byte-identical to upstream — pin this with a test, it is the thing a careless rebase will quietly break
Depends on #1. Context in quantus/extension#1.
## There is no plugin point
`packages/keyring/src/pair/index.ts` holds four module-private const maps and a `createPair` that closes over them:
```js
const TYPE_FROM_SEED = { ecdsa, ed25519, ethereum, sr25519 };
const TYPE_PREFIX = { ed25519: [0], sr25519: [1], ecdsa: [2], ethereum: [2] };
const TYPE_SIGNATURE = { … };
const TYPE_ADDRESS = { ed25519: (p) => p, sr25519: (p) => p, … };
```
Nothing is injectable, which is the whole reason this repo is a fork rather than a wrapper. Add arms; do not restructure the maps (see the rebasability convention in quantus/extension#1).
## `TYPE_PREFIX` is already the right shape
A happy accident worth not breaking: `pair.sign(msg, { withType: true })` prepends `TYPE_PREFIX[type]`, and the chain's `DilithiumSignatureScheme` variant byte is exactly that — `dilithium87: [0]`, `dilithium65: [1]`. With `TYPE_SIGNATURE` returning `sig ‖ pk`, the bytes `@polkadot/types` hands to a dapp are already the correct SCALE encoding of the runtime's signature enum, with no special-casing anywhere upstream of here.
## `TYPE_ADDRESS` is the interesting one
Every existing arm is either identity or a cheap re-encoding, because on Substrate the address *is* the public key. For Quantus it is `Poseidon2(publicKey)` — 1952 or 2592 bytes in, 32 bytes out, one-way.
That breaks an invariant `createPair` currently relies on without stating it: **a locked pair can still report its address**, because the public key is in memory even when the secret is not. For Quantus that still holds — the public key is enough to compute the address — but the moment the public key *isn't* available, it does not. Which is exactly the situation #3 is about, and the two issues should be designed together: `createPair` will likely need to accept a precomputed account id for pairs restored from JSON.
## Also
- `decodePkcs8` branches on `decoded.secretKey.length === 64` to decide "secret key" vs "seed". A 4032- or 4896-byte ML-DSA secret takes the wrong branch and gets fed to `TYPE_FROM_SEED` as if it were entropy. Make the check type-aware rather than length-magic.
- `derive()` is #4.
- `pair.sign()` needs a signing context — #5.
## Acceptance
- [ ] a pair created from a known mnemonic reports the SS58 address that `quantus-cli` reports for it
- [ ] `pair.sign(payload, { withType: true })` produces bytes that the runtime's `Verify::verify` accepts
- [ ] the sr25519/ed25519/ecdsa/ethereum paths are byte-identical to upstream — pin this with a test, it is the thing a careless rebase will quietly break
ML-DSA in createPair: landed in 2bffbc62b and 506b77351.
Addresses match quantus-cli: for a known mnemonic, both schemes (dilithiumDerive.spec.ts); the raw-seed crystal_alice is checked in dilithium.spec.ts.
Signature layout:sign(…, { withType: true }) carries the chain's variant byte and length, and verifies with dilithiumVerify. Chain acceptance was then shown through quantus/extension#7 tier 1.
Curve schemes unchanged: upstream's pair/index.spec.ts is untouched, the curve sign functions are wrapped so a context can't reach them, and sr25519 is pinned.
Also fixed:
decodePkcs8 now decides by key type rather than by key length.
Pairs restored from JSON carry an account id, checked against the real key on unlock.
Verified against `main` (`7f23d0a60`), closing.
- **ML-DSA in `createPair`:** landed in `2bffbc62b` and `506b77351`.
- **Addresses match `quantus-cli`:** for a known mnemonic, both schemes (`dilithiumDerive.spec.ts`); the raw-seed crystal_alice is checked in `dilithium.spec.ts`.
- **Signature layout:** `sign(…, { withType: true })` carries the chain's variant byte and length, and verifies with `dilithiumVerify`. Chain acceptance was then shown through quantus/extension#7 tier 1.
- **Curve schemes unchanged:** upstream's `pair/index.spec.ts` is untouched, the curve sign functions are wrapped so a context can't reach them, and sr25519 is pinned.
- **Also fixed:**
- `decodePkcs8` now decides by key type rather than by key length.
- Pairs restored from JSON carry an account id, checked against the real key on unlock.
The classical arms themselves go in quantus/common#6.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Depends on #1. Context in quantus/extension#1.
There is no plugin point
packages/keyring/src/pair/index.tsholds four module-private const maps and acreatePairthat closes over them:Nothing is injectable, which is the whole reason this repo is a fork rather than a wrapper. Add arms; do not restructure the maps (see the rebasability convention in quantus/extension#1).
TYPE_PREFIXis already the right shapeA happy accident worth not breaking:
pair.sign(msg, { withType: true })prependsTYPE_PREFIX[type], and the chain'sDilithiumSignatureSchemevariant byte is exactly that —dilithium87: [0],dilithium65: [1]. WithTYPE_SIGNATUREreturningsig ‖ pk, the bytes@polkadot/typeshands to a dapp are already the correct SCALE encoding of the runtime's signature enum, with no special-casing anywhere upstream of here.TYPE_ADDRESSis the interesting oneEvery existing arm is either identity or a cheap re-encoding, because on Substrate the address is the public key. For Quantus it is
Poseidon2(publicKey)— 1952 or 2592 bytes in, 32 bytes out, one-way.That breaks an invariant
createPaircurrently relies on without stating it: a locked pair can still report its address, because the public key is in memory even when the secret is not. For Quantus that still holds — the public key is enough to compute the address — but the moment the public key isn't available, it does not. Which is exactly the situation #3 is about, and the two issues should be designed together:createPairwill likely need to accept a precomputed account id for pairs restored from JSON.Also
decodePkcs8branches ondecoded.secretKey.length === 64to decide "secret key" vs "seed". A 4032- or 4896-byte ML-DSA secret takes the wrong branch and gets fed toTYPE_FROM_SEEDas if it were entropy. Make the check type-aware rather than length-magic.derive()is #4.pair.sign()needs a signing context — #5.Acceptance
quantus-clireports for itpair.sign(payload, { withType: true })produces bytes that the runtime'sVerify::verifyacceptsVerified against
main(7f23d0a60), closing.createPair: landed in2bffbc62band506b77351.quantus-cli: for a known mnemonic, both schemes (dilithiumDerive.spec.ts); the raw-seed crystal_alice is checked indilithium.spec.ts.sign(…, { withType: true })carries the chain's variant byte and length, and verifies withdilithiumVerify. Chain acceptance was then shown through quantus/extension#7 tier 1.pair/index.spec.tsis untouched, the curve sign functions are wrapped so a context can't reach them, and sr25519 is pinned.decodePkcs8now decides by key type rather than by key length.The classical arms themselves go in quantus/common#6.