[FEATURE BRANCH] Post-quantum accounts — unified signing + PQ algosdk fork (do not merge) - #1062
Draft
fmsouza wants to merge 29 commits into
Draft
[FEATURE BRANCH] Post-quantum accounts — unified signing + PQ algosdk fork (do not merge)#1062fmsouza wants to merge 29 commits into
fmsouza wants to merge 29 commits into
Conversation
…RA-4653] Alias `algosdk` to Joe Polny's fork (3.7.0-beta.1) via a pnpm catalog override, so `pqsig` becomes representable in the type the whole codebase already imports as `algosdk`. Drops the separate `@joe-p/algosdk` dependency, updates quantumAdapter.ts and its spec to import from `algosdk`, externalizes `algosdk` in the blockchain vite build, and narrows the PQ library firewall to the Falcon libraries only (the fork no longer needs its own forbidden-specifier seam).
…dk alias [PERA-4653] Task 1's commit (algosdk installed under the official name via a pnpm alias) invalidated several statements in this doc: "two swap seams" (only the kms Falcon seam is still firewalled), quantumAdapter.ts being "the only module importing @joe-p/algosdk" (it now imports plain algosdk), the swap-back instructions telling a future engineer to edit that import (the real swap-back is the one-line pnpm-workspace.yaml change), and the claim that both seam files carry a // SWAP: marker (quantumAdapter.ts's was removed). Corrected each in place; unrelated stale content (PQ-006, PQ-020, Seam A WASM-only claim) is left for the task that owns it.
…c [PERA-4653] Task 1 reduced the PQ library firewall to one seam directory and narrowed its forbidden-specifier pattern to @joe-p/react-native-falcon and falcon-1024 only (algosdk/@joe-p/algosdk is deliberately no longer matched). The Enforcement section still said "two seam directories" and "@joe-p/*", and described a now-deleted second positive-control test in the plural. Corrected all three against the spec as it currently stands.
…smFalconProvider [PERA-4653] Task 2 removed the runtime branch from getPQProvider (build-time selection via Metro's .native.ts resolution), which left these two doc comments describing a conditional that no longer exists. Comments only, no code change.
…age.json formatting, cover compactSignedResults [PERA-4653] - Doc previously said the fork hashes the signature preimage internally and callers pass a raw (un-digested) signature - backwards after this task's digest fix, and dangerous for whoever wires the KMS signer into this seam next. Documents pqSigningDigest/assemblePQSignedTransaction as the current Seam B surface and states the digest contract explicitly. - Removes stale references to the deleted QuantumSignedTransaction carrier from the PQ-006 section and the on-device verification checklist. - Restores packages/blockchain/package.json to its original 2-space formatting/key order (was inadvertently reformatted); net diff against the parent commit is now the single added @noble/hashes dependency line. - Adds a direct unit test for compactSignedResults (order + identity preservation, all-null input), which lost its only coverage when its signature was retyped.
…RA-4653] Deletes the parallel quantum signing path (createQuantumStrategy, quantumSignerActor, useQuantumTransactionSigner, the 'quantum' ResolvedSignerType) and folds quantum accounts into the same local-key path algo25/HD already share. useKMS().getPQSigningInfo(keyPairId) is the single place that resolves the scheme; useLocalKeyTransactionSigner uses it to decide, per call, whether to sign pqSigningDigest(txn) (the correct SHA-512/256 digest) or the plain encoded transaction, and to assemble the result via assemblePQSignedTransaction or a plain SignedTransaction. This closes the live bug where production signed the un-digested "TX"-prefixed encoding for Falcon transactions. Also swaps the deleted PeraSignedTxnResult/QuantumSignedTransaction byte carrier for the now-unified PeraSignedTransaction across the signing pipeline (submission, transports, models) and its fee-delegation/ transactions consumers.
….md [PERA-4653] The §PQ-006 "Dedicated strategy"/"Machine routing"/"Submission stays gated" bullets still described the deleted createQuantumStrategy / quantumSignerActor / 'quantum' ResolvedSignerType path. Updated to describe the unified local-key path this task landed, and fixed a matching stale "carrier-aware encodeSignedTransaction" comment in submitAndAutoRefresh.ts (the carrier is gone; pqsig is just a field).
…countType [PERA-4653] getPQSigningInfo decided the payload scheme from the child's type while signTransactionsWithKey decided the signer from the seed's scheme — two independent oracles that could disagree (e.g. keyPairId resolving to the quantum seed id itself, a legacy convenience resolveSeedKey still accepts), silently re-creating the un-digested-signing bug via a null fallthrough to the ed25519 path. getPQSigningInfo now derives from the same seed-scheme oracle the signer uses, and throws on a seed/child-type mismatch instead of returning null. Also deletes SignerInfo.accountType: it had exactly one writer (createLocalKeyStrategy) and zero readers since PQ-019 removed the submission-boundary gate its doc comment described, and this round's own review added a test pinning that dead write. Renamed a duplicated inner describe block in useLocalKeyTransactionSigner.spec.ts.
…bview signability [PERA-4653]
…nArbitraryData docstring [PERA-4653]
…ERA-4653] The carrier-based quantum guard in the swap approve callback checked for a representation that no longer exists. Retarget it onto the real reason quantum swap is blocked: the swap fee comes from the backend's prepare-transactions response and can't be raised on-device without invalidating pre-signed group members' `grp` hash (PQ-024 / PERA-4705). Also add the same account-level guard to requestSwapProposal, closing a hole where a quantum proposer's multisig signature extraction silently returns null.
…ning path [PERA-4653] The suite's comments and one assertion still referenced the deleted quantumSignerActor/createQuantumStrategy carrier. Quantum accounts now sign through the ordinary local-key path and the result is a plain SignedTransaction with `pqsig` populated instead of `sig` — updated the comments and the delivered-payload assertion to match, preserving the same two behaviors: the confirmation screen shows the raised PQ fee + explainer, and a local quantum payment reaches the callback transport's approve() without ever hitting algod broadcast.
…A-4653] isQuantumAccount is a raw account.type === 'quantum' tag check. Both swap quantum guards were checking the selected account directly, so a standard or multisig account rekeyed on-chain to a quantum auth account sailed past both guards (its own type stays e.g. 'algo25') and got Falcon-signed at a fee the backend computed without the PQ minimum. Resolve the effective signer via useSignerFor (one rekey hop) in useSwapExecution and pass that into both requestSwapSignatures and requestSwapProposal, matching the same resolve-then-check pattern useTransactionConfirmationScreen already uses for its isQuantumFee check.
Adds tools/localnet-quantum-check.ts: an end-to-end check that derives a Falcon-1024 address, funds it, builds a PQ-fee-raised payment, and asserts the assembled signed bytes match algosdk's own PQ signer byte-for-byte before attempting broadcast. Since no public algod accepts `pqsig` yet (verified against both LocalNet 4.7.4-stable and rel/nightly build 2680), the script reports PENDING (exit 0) when broadcast is rejected specifically for the unknown `pqsig` field, and FAILs (non-zero) on any other rejection reason, so it becomes a true pass/fail e2e test unchanged once a pqsig-capable node ships. Also adds a --new-quantum flag to tools/localnet-fund.ts to mint and fund a throwaway quantum account, wires the new script into package.json, and documents both in README.md. Drops the "not yet landed" hedge in docs/QUANTUM_PQ_INTEGRATION.md's PQ-023 section now that this has landed.
…A-4653] sendRawTransaction resolving only means algod accepted the bytes into its mempool, not that the transaction landed on chain. Splits submit and confirm into separate try/catch blocks: PQSIG_UNSUPPORTED is now only ever tested against the submit rejection, so a confirmation-wait failure can never be misread as PENDING. Confirmation uses algosdk's waitForConfirmation, the same helper submitAndAutoRefresh.ts already uses, wrapped in a wall-clock timeout so a node that accepts but never confirms produces a bounded FAIL instead of a hang. Also fixes a numeric-separators-style lint violation (1_000 -> 1000) and converts the fail() helper from an arrow-function const to a function declaration, since TypeScript's unreachable-code-after-never-call narrowing does not apply to arrow-function expressions, which the split submit/confirm logic now depends on.
…k everywhere [PERA-4653] Four importers (background, onramp, shared, transactions) depend on @algorandfoundation/algokit-utils but never declared algosdk directly. Without an algosdk instance already in their own dependency graph, pnpm's peer-dependency auto-install ignored the root algosdk override and resolved algokit-utils's peer against stock algosdk@3.6.0 instead of the @joe-p/algosdk fork used everywhere else — reintroducing the exact two-copies-of-algosdk problem the fork alias exists to prevent, and breaking the onramp build (AlgoAmount type mismatch across the two algokit-utils instances). Add an explicit "algosdk": "catalog:" dependency to each of the four packages, matching the pattern already used in packages/blockchain, so their own dependency graph contains the aliased algosdk and algokit-utils' peer resolves to it. Also pruned stale node_modules/.pnpm entries for the orphaned stock algosdk/algokit-utils/algokit-client-generator variants.
Per-importer algosdk pinning (previous commit) fixes today's dependency graph but doesn't guard against a fifth package silently reintroducing a second algosdk resolution on a future install — pnpm's peer-dependency auto-install can bypass the workspace-level algosdk override for any importer with no direct algosdk edge, exactly as happened mid-branch between Task 1 and Task 3 here. Add tools/check-single-algosdk.mjs: parses pnpm-lock.yaml's importers section directly (no `pnpm list` shell-out), asserts exactly one resolved algosdk identity across the whole workspace, and on failure names every importer referencing each distinct identity found. Fails loudly (not silently passes) if the lockfile's expected sections can't be located or no algosdk resolution is found at all, per "a guard that passes when it can't parse is worse than no guard." Wired in as `pnpm check:single-algosdk` and as its own step in tools/pre-push, alongside the existing lint/format/copyright/i18n/secret checks. Deliberately broke the invariant (removed the `algosdk` pin from packages/onramp added in the previous commit, reinstalled) and confirmed the guard fails and names `packages/onramp` specifically, then restored and confirmed it passes again — see task-9-report.md for both verbatim outputs.
…unds-freeze warning [PERA-4653] - No algod available today accepts `pqsig` — not mainnet, not testnet, not LocalNet (4.7.4-stable) — so a quantum-signed transaction cannot be confirmed anywhere. Three docs claims and one production comment said otherwise; a future engineer would have read them, run `pnpm localnet:quantum-check`, seen the designed PENDING and been tempted to "fix" the tier logic that makes the script meaningful. - Document `pnpm localnet:quantum-check` as shipped (it is, on this branch) rather than as follow-up tooling. - Restore the accurate rekey-to-quantum warning: it is a one-way door that strands funds, because the rekey-BACK transaction also needs a `pqsig` and is rejected at submit. The replaced text called the feature-flag gate "rollout control, not a functional limitation", which invites turning the flag on for testers. - Remove the redundant `algosdk` `minimumReleaseAgeExclude` entry (the `@joe-p/algosdk` entry covers the alias by resolved name) and amend the swap-back procedure, which is not purely a one-specifier change. - Give quantum its own `Quantum` webview type instead of aliasing it to `Algo25`, which states something untrue about the key type. - Restore the lost `assemblePQSignedTransaction` argument assertion, add a multi-transaction quantum case covering `signatures[idx]` pairing, and restore fork byte-equality for the rekeyed case.
Both strings reach the user verbatim — useSwapExecution displays `e.message` for any non-rejection failure — so English prose defeated the stated purpose of these guards, which is that a blocked quantum swap fails loudly AND correctly attributed. The guards now reject with a `QuantumSwapBlockedError` carrying an i18n key; `useSwapExecution` is the display site that renders it through `t()`. `Error.message` stays English for logs. Test assertions tightened from a lucky /fee/i substring match to the exact key.
…f scheme-genericity [PERA-4653] 7a: `getPQSigningInfo` returned a hardcoded `schemeId: 'falcon1024'` while `PQSignatureProvider.scheme` — the generic source that already knows the answer — was read only by a test. It now returns the provider's `scheme`, and the provider contract widens from the literal to `PQSchemeId` (type-only import, erased at runtime, so the pure-crypto seam stays uncoupled). 7b: the "no new types, just add a registry entry" claim was only true for the transaction assembly and signing path. schemes.ts and the integration doc now say that explicitly and name the two remaining single-scheme assumptions (feeCalculator's one `pqMultiplier` + boolean `isPQSigner` derived where the scheme isn't retrievable; `quantumSignKeyId`'s one child per seed) so a future implementer isn't misled. Neither is fixed here — out of scope. 8: signGroupsBySignerAccount's comment named the deleted quantum actor; check-single-algosdk claimed workspace-wide coverage while inspecting only the lockfile's `importers:` section; pqImportSideEffects now also guards `getPQProvider.native` (the on-device entry point, previously covered by neither this guard nor the source scanners — verified non-vacuous by making the require eager and watching it fail); the extension shim explains why its bare `exports` reference is legitimate under Metro's module wrapper; the firewall spec records `tools/` as an accepted blind spot and the doc's Enforcement section no longer reads as a proof.
# Conflicts: # package.json # pnpm-lock.yaml
# Conflicts: # package.json
5 tasks
Brings in 12 commits, notably #1071 (betanet + runtime-configurable custom network) and #1058 / PERA-4670 (migrated device-id telemetry). Three textual conflicts, plus one silent semantic break that auto-merged cleanly and would have shipped red: 1. `packages/device/src/hooks/useDevice.ts` — a genuine feature collision, not a textual one. Both sides rewrote the same file from the same base: feat/quantum migrated registration to Device API v3 (PERA-4705, #1078) and main added migrated-id origin tracking plus orphaning telemetry (PERA-4670, #1058). Kept v3's architecture and ported PERA-4670's telemetry onto its recreate path. Two deliberate adaptations, both commented in place: - PERA-4670 guarded the emit on an in-flight attempt id to be sure the write was still current. v3 does not need it: every registration is chained through `enqueueRegistration`, so one task runs per network and a recreate cannot be superseded mid-flight. - `reason` is now always `not_found`. v3's `shouldRecreateDevice` matches 404 only, because a 400 is a push-token race that retries with the SAME id rather than re-creating, so `device_already_exists` can no longer replace an id and is no longer a replacement reason. 2. `packages/device/src/hooks/__tests__/useDevice.test.ts` — kept v3's suite and ported PERA-4670's telemetry tests onto v3's mocks. The two tests asserting `device_already_exists` as a replacement reason were dropped as unreachable under v3 (see above), and one was added in their place asserting the inverse: a push-token race leaves a migrated id and its origin untouched. 3. `README.md` — main removed `localnet:use`/`localnet:unset`, `tools/use-localnet.sh` and `.env.localnet`, replacing the env-overlay flow with the runtime custom-network UI. Took main's snippet, kept the quantum funding line (`localnet:fund --new-quantum` still exists), and rewrote the quantum-verification section, which still told the reader to run the now-deleted `pnpm localnet:use`. 4. `packages/shared/src/api/__tests__/query-client.test.ts` — NOT flagged as a conflict; git combined both sides' edits into a file that no longer worked. #1071 made client construction lazy ("NEVER at module import time", see `ensureClientsBuilt`) while PERA-4705 had strengthened this test to read the real `setStandardHeaders` off `ky.create.mock.calls[0]`. Post-merge nothing was created at import, so `calls[0]` was undefined. Fixed by triggering a request first and locating the pera client by prefix instead of call order — keeping the strong header assertions rather than reverting to main's `expect(mockKy).toHaveBeenCalled()`. Verified on the merged tree: `turbo run build` 53/53 (incl. mobile `tsc --noEmit`), and `turbo run test:unit --force` 100/100 uncached with zero failures.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Addresses the PQ-023 review feedback: quantum accounts had an entirely separate signing route, when "falcon is just another key type". This collapses that route into the ordinary local-key path, models the PQ signature generically, and removes the remaining mocks.
txn.bytesToSign()un-digested, when the protocol requires the SHA-512/256 digest. The old adapter hid it behind a signer callback that ignored the bytes algosdk handed it, and because no algod verifiespqsigyet, nothing could catch it. Signing now goes throughpqSigningDigest(txn)=sha512_256(txn.bytesToSign()), pinned byte-for-byte against algosdk's own PQ signer by a differential test.Signing is unified — quantum is a
localKeysignerResolvedSignerTypeis back to'localKey' | 'hardware' | 'multisig'. Deleted: the machine'squantumstate,quantumSignerActor,createQuantumStrategy,useQuantumTransactionSigner, thesignQuantumTransactionsdependency, and the quantum branches indetermineSignerType/getSigningStrategy(including the ordering hazard that forced quantum to be tested beforehasSigningKeys).What remains is one branch, in
useLocalKeyTransactionSigner— same altitude as the existing algo25-vs-HD difference: which bytes to sign, and whether the signature lands insigorpqsig.Generic PQ signature model
PQSignature { schemeId, publicKey, signature }plus a scheme registry, replacing the Falcon-specificfalconSignaturefield. The opaqueQuantumSignedTransaction { txn, pqSignedBytes }byte carrier is gone — a PQ transaction is now a realSignedTransactionwith apqsig, soisQuantumSignedTransaction,PeraSignedTxnResult, theencodeSignedTransactioncarrier branch and thecompactSignedResultsnarrowing seam all disappear.Bugs fixed alongside
unsupported account type quantum.canSignViaParticipantsexcluded quantum;getLocalParticipantsandgetLocalUnsignedSigners(wired to the real Sign button) admitted it — so a Falcon signature could be produced for an Ed25519 slot.'Unsignable'. It now reports a newQuantumtype rather than being aliased onto'Algo25', since a dApp told "Ed25519" would be handed a ~1.2 KB Falcon signature it cannot verify.Other changes
Build-time Falcon provider selection (
getPQProvider.ts/.native.ts) replacing anif (navigator.product === 'ReactNative')runtime check. DeadFALCON_*_LENGTHconstants removed.pnpm localnet:quantum-checkadded. Acheck:single-algosdkpre-push guard added, because the alias silently drifted mid-development and only a full-tree build caught it.Why the alias can't be narrowed
The obvious question is: keep official
algosdkfor the app, use the fork only in quantum code. I measured this — it does not work. Two copies of algosdk means two class identities:Handing an official-built
Transactionto the fork's PQ signer fails outright:So only bytes can cross the boundary — which is precisely why the old code passed
unsignedTxnBytesand returnedUint8Array. That byte carrier is what forced the separate signing path this PR removes. And it now carries a worse hazard:Official algosdk silently drops the
pqsigfield — no error, just a transaction that has quietly lost its signature.decodeSignedTransactionis live in app code (the swap flow decodes backend pre-signed transactions with it).Conclusion: unified signing and the tree-wide alias are the same decision. You cannot have one without the other while official algosdk lacks
pqsig. Hence this branch rather than a narrower change.For the record, the measured production delta
Diffed the fork against official 3.6.0: 3 new PQ-only files; 13 shared files changed (~655 lines, predominantly additive);
transaction.tsis JSDoc-only; old APIs deprecated, not replaced; export surface is a superset (nothing removed —signBytes/verifyBytes/signTransactionjust movedmain.ts→signing.ts); runtime deps identical except@noble/curves, already in our tree.So the delta is small and additive — the objection isn't that the fork looks dangerous, it's that swapping the app's SDK for a flag-dark feature isn't a risk worth taking on
main. That reasoning stands regardless of the diff.Exit criteria — what makes this mergeable
algosdk@3.7.0ships withpqsig. Then the alias becomes^3.7.0— one specifier, plus dropping two workspace-config entries (documented inline and indocs/QUANTUM_PQ_INTEGRATION.md). A guard test fails loudly if anyone points it at a pre-3.7 official release. Official is currently 3.6.0; 3.7.0 does not exist yet.pqsig-capable algod exists, so quantum is verifiable end to end. Today none does — LocalNet (4.7.4-stable) rejectspqsig, and so doesalgorand/algod:nightlybuild 2680.pnpm localnet:quantum-checktherefore reports PENDING at exit 0 by design, and flips to a real PASS unedited when a node ships.grprecomputation that invalidates the backend's signatures.Maintaining this branch
mainregularly. It conflicts on nearly every Dependabot batch: ourfalcon-1024devDep line sits directly abovefallowinpackage.json, which two separate batches bumped in a single day (3.3.0 → 3.7.0 → 3.7.1). Each is a trivial union resolution.pnpm-lock.yaml. Takemain's and re-runpnpm installto replay this branch's spec delta.pnpm check:single-algosdkmust stay green. Two algosdk copies break cross-boundaryinstanceofand msgpack schema identity; the alias already drifted once via pnpm's peer auto-install, so everyalgokit-utilsdependent pinsalgosdkdirectly.Related Issues
Checklist
Additional Notes
Known limitations, documented not fixed
pqsigalgod ships — the rekey-back transaction also needs apqsig. Gated behindenable_quantum_accounts(default off); the warning inpackages/accounts/src/utils.tsis deliberately blunt.pqMultiplier) andquantumSignKeyId's one-child-per-seed id are genuine second-scheme blockers, named inschemes.tsand the integration doc.useProgramSigneradmits quantum for logicsig with the wrong PQ preimage (unreachable — card funding excludes quantum); fee delegation has no PQ fee adjustment;tools/sits outside lint and the PQ import firewall.Open follow-up not yet on this branch
A conformance suite asserting the fork is byte-identical to official 3.6.0 for every ed25519 transaction type the app builds. That would turn "is the fork safe for the whole app" from a judgement call into a gate, and is the strongest single de-risking step if this branch ever needs to merge before official 3.7.0 lands.
Verification
Full suite green on the merged tree: 103/103 turbo tasks, 607 test files, 4388 tests.
pnpm pre-push7/7.pnpm audit --prodclean. Integration suite includes 13 quantum flow tests.🤖 Generated with Claude Code