npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fractal-pqc

v0.12.0

Published

Runnable reference for quantum-safe migration of a Bitcoin-style key: bind secp256k1/Taproot to ML-DSA-65 (FIPS-204), derive P2TR addresses, build+sign BIP-341 key-path spends (official-vector-verified), and broadcast on testnet. Real primitives, honest s

Readme

fractal-pqc

A small, runnable, test-vector-verified reference for quantum-safe migration of a Bitcoin-style key. It binds a classical secp256k1 / Taproot key to a post-quantum ML-DSA-65 (NIST FIPS-204) key and requires a post-quantum signature to authorize — the core mechanism of any Bitcoin quantum-readiness transition.

Built from the same crypto-agility discipline FractalAI already runs in production on its own post-quantum L1 (verify-both-during-migration, no-downgrade), distilled to the secp256k1 → ML-DSA-65 case relevant to Bitcoin.

Why this exists

A large quantum computer can recover a secp256k1 private key from an exposed public key (Shor's algorithm), so any Bitcoin output whose pubkey is on-chain becomes spendable by an attacker. The migration problem: let a holder bind their existing key to a post-quantum successor key and, from then on, require a PQC signature to spend — without invalidating already-signed history. This kit is a concrete, honest first step.

What is REAL here (verified, not aspirational)

The security layer (v0.7.0): transparency.mjs (PQ-ANCHOR-v1 append-only log: inclusion + consistency proofs, ML-DSA-65 signed heads, equivocation detection), primacy.mjs (first-seen proven by complete enumeration + a Bitcoin-anchored temporal frontier), policy.mjs (fail-closed custodian gate), tapscript.mjs (BIP-341 script-path, asserted against the official wallet vectors) and ots.mjs (independent OpenTimestamps codec). All are exported: fractal-pqc/primacy, /policy, /transparency, /tapscript, /ots.

The limit we have not closed, stated up front: a holder who never registered a dual-signed commitment before the verifier's cutoff height cannot be rescued by any of this. test/primacy.mjs asserts that out loud rather than leaving it to be discovered.

Everything below is exercised by npm test with real keys — 304 checks pass:

  • secp256k1 commitment + spend authorization (@noble/curves).
  • Taproot BIP-340 Schnorr sign/verify, asserted against the official BIP-340 test vector — i.e. byte-for-byte consensus-correct with Bitcoin, not an approximation.
  • ML-DSA-65 (FIPS-204, Cat-3) signing/verification (@noble/post-quantum): 1952-byte public key, 4032-byte secret, 3309-byte signature.
  • Dual-signature migration commitment: classical + PQC both sign the same canonical, domain-separated binding; a verifier reconstructs identical bytes.
  • Taproot → PQC recovery commitment (P2QRH-style): binds a 32-byte x-only Taproot output key to an ML-DSA-65 key, dual-signed.
  • Real Bitcoin address derivation: bech32/bech32m (official BIP-173/350 vectors), P2TR bc1p…/tb1p… via the BIP-341 taproot tweak (official BIP-341 vector → bc1p2wsldez…), and P2WPKH bc1q….
  • Real Bitcoin transactions: segwit/legacy serialization + txid (validated against the Bitcoin genesis coinbase txid), and a BIP-341 key-path sighash that matches the OFFICIAL bip-0341/wallet-test-vectors.json byte-for-byte (a permanent test in the suite) — i.e. a key-path SIGHASH_DEFAULT spend signs the Bitcoin-consensus message and will not be sighash-rejected by the network. Key-path signing produces a Schnorr sig that also verifies against the Taproot output key.
  • Fees + coin selection: exact segwit vsize (1-in/1-out P2TR = 111 vB), accumulative coin selection with change/dust handling and a fee-rate (sat/vB) → conserves value.
  • BIP-174 PSBT (P2TR key-path): createPsbtsignPsbtTaprootKeyPathfinalizePsbt into a broadcastable tx; serialize↔parse round-trips, base64 carries the psbt\xff magic, and the finalized witness signature verifies against the Taproot output key. The signer refuses any input the key doesn't control (custodian-safe).
  • Testnet broadcaster: fetches your address's UTXOs + the live fee rate (mempool.space Esplora API), builds+signs the spend, and publishes it — dry-run by default, --broadcast to send. addressToScriptPubKey lets you pay any SegWit address.
  • The quantum property under test (correctly modeled): verifySpend requires the verifier to pin the holder's anchored commitment (anchoredFactHash). Given that pin, an attacker who recovered the classical key post-Shor — and who can forge a well-formed cert re-binding the victim's classical key to the attacker's OWN PQC key — is still rejected (anchor-mismatch), because the forged cert's factHash differs from the anchored one. Without the anchor, verifySpend fails closed (anchor-required).

Run it:

cd integrations/pqc-migration-kit
npm install        # or reuse the workspace's @noble packages
npm test

SDK

import {
  generateMigrationIdentity, createMigrationCommitment, verifyMigrationCommitment,
  authorizeSpend, verifySpend,
  generateTaprootKey, taprootSign, taprootVerify, verifiesBip340OfficialVector,
  createTaprootPqcCommitment, verifyTaprootPqcCommitment,
} from "fractal-pqc";

const id = generateMigrationIdentity();          // secp256k1 + ML-DSA-65
const cert = createMigrationCommitment(id);      // dual-signed, anchorable
verifyMigrationCommitment(cert).valid;           // true

CLI

npx fractal-pqc verify-vector          # assert the official BIP-340 test vector
npx fractal-pqc selftest               # run the full real test suite (304 checks)
npx fractal-pqc keygen > key.json      # generate a migration identity (testnet/experimental)
npx fractal-pqc keygen --taproot > tk.json           # a Taproot key
npx fractal-pqc address --key tk.json --network tb   # derive a real tb1p… Taproot address
npx fractal-pqc commit --key key.json > commit.json
npx fractal-pqc verify commit.json     # exit 0 = valid, 1 = invalid/tampered
npx fractal-pqc taproot-commit --key <taproot+pq key.json>   # Taproot -> PQC recovery commitment

verify/taproot-verify/verify-vector exit non-zero on failure, so they compose into CI and custody scripts.

Send a real spend on testnet

npx fractal-pqc keygen --taproot > tk.json          # your Taproot key
npx fractal-pqc receive --key tk.json --network tb  # → tb1p… funding address + explorer link
# fund that address from a testnet faucet, then:
npx fractal-pqc send-testnet --key tk.json --to <dest tb1…> --amount 20000 --network tb
#   ↑ DRY-RUN: fetches your UTXOs + live fee rate, builds+signs, prints hex + txid (does NOT send)
npx fractal-pqc send-testnet --key tk.json --to <dest tb1…> --amount 20000 --network tb --broadcast
#   ↑ actually publishes via mempool.space and returns the txid + explorer URL

The SDK equivalent is sendP2trKeyPath({ internalPriv, to, amountSats, network, broadcast }).

Security model — the anchor is load-bearing

The quantum protection does not come from a cert verifying in isolation (a cert only proves whoever built it held both secret keys). It comes from anchoring: the holder publishes their commitment first-seen and immutably in an append-only transparency log (RFC 6962) anchored into Bitcoin (OpenTimestamps), and every verifier pins that anchored factHash. verifySpend/verifyMigrationCommitment take anchoredFactHash and reject any cert that doesn't match it. Skip the anchor and the kit provides no quantum protection — verifySpend fails closed rather than pretend otherwise. The anchor registry itself (first-seen, no-duplicate, immutable — primacy.mjs + ots.mjs + verify-anchor) is delivered and published (see the Roadmap below); this reference makes the anchor a required verifier input so the property is never silently over-claimed. One honest residual: this package can verify a .ots you already obtained — it has no code to submit a new digest to an OpenTimestamps calendar server or poll it to Bitcoin confirmation. Creating a brand-new anchor today requires an external tool (the reference ots CLI or the python-opentimestamps library); we deliberately don't vendor one, per ots.mjs's own "no network" scope.

What this is NOT

NEVER SIGN A DIGEST YOU DID NOT COMPUTE YOURSELF. Until 0.9.0 authorizeAndSign read a 32-byte spendDigest chosen by the requester, and round 7 of our own siege used that to have one legitimate client of a custodian obtain a policy-key signature over another client's sighash. The round-7 fix compared the presented scriptPubKey against one recomputed from the requester's fact hash — and round 8 walked through it, because both sides of that comparison were the requester's.

An engine that signs a digest it was handed is not a policy engine, it is a signing oracle, and no guard around it helps: every check then compares values the caller controls. So the caller no longer supplies a digest. It supplies the transaction:

authorizeAndSign({ cert, pqSignatureHex, anchorEvidence, expectedLogId, policyKey,
                   cutoffBlockHeight, blockMerkleRoots,
                   spendContext: { tx, inputIndex, prevoutValues, prevoutScriptPubKeys } })

spendContext is mandatory and a request carrying spendDigest is refused at the door. The engine recomputes the Taproot output key from the holder's anchored first-seen fact hash, requires the prevout at inputIndex to be that coin, and then derives the BIP-341 sighash itself. Use buildSpendContext() — the same helper a custodian uses — so the holder signs exactly the bytes the policy key will sign.

THE TWO RESIDUALS, stated rather than hidden. (1) The engine imposes nothing on the outputs: it proves whose coin is spent and who authorised it, never where the value goes. (2) buildSpendContext builds a single-input transaction while the engine accepts multi-input ones, so for those the holder cannot re-derive the digest with the shipped helper and would be signing a digest the custodian computed — the very mistake this section is about. The holder must inspect the transaction, not merely sign the digest they were handed. (honest scope — do not overstate)

  • Not yet confirmed in a live mempool. The sighash is proven consensus-correct against the official BIP-341 vector, and both the key-path and script-path (M2) broadcasters are built + dry-run-tested — but an actual funded testnet broadcast (which needs tBTC from a faucet) is the operator's final step for either path.
  • p2trAddress's signer is key-path-only. taprootTweakPrivateKey/signTaprootKeyPath handle key-path-only outputs — exactly what this kit's p2trAddress derives. Pointing the signer at a foreign Taproot output that commits to a script tree would produce an invalid signature. Script-path (tapscript) construction — the policy leaf, control blocks, and the BIP-341 script-path sighash the M2 policy engine signs — IS implemented and asserted against the official BIP-341 wallet test vectors (7/7 cases, 12/12 control blocks, byte for byte); what's missing is the step after the signature, per the point above.
  • Secrets are not zeroized. Private-key Uint8Arrays are not wiped after use (best-effort only in JS); keygen prints secrets by design (testnet/experimental).
  • Not a BIP and not consensus. The "recovery commitment" is an application-layer construction, not a Bitcoin output type. A real quantum-resistant output requires a soft fork (e.g. a P2QRH / BIP-360-style commitment) — out of scope for this reference.
  • Not audited. Reference code; the underlying @noble libraries are audited, this composition is not.

Roadmap — from reference to real Bitcoin custody tooling (the grant-funded work)

Milestones below match the tranche structure in our funding ask (letter/galaxy-2026-08-31.md) exactly, so this table and that letter can never quietly drift apart the way this section once did. Each milestone is independently verifiable, open-source, and shippable on its own.

  1. Bitcoin address + tx layer — ✅ done + vector-verified: bech32/bech32m (BIP-173/350), P2TR bc1p… derivation (BIP-341), segwit tx + txid (genesis-checked), key-path AND script-path BIP-341 sighashes matching the official vectors byte-for-byte (7/7 scriptPubKey cases, 12/12 control blocks), fees + coin selection, BIP-174 PSBT create→sign→finalize (key-path), and a testnet broadcaster (fetch UTXOs/fees → build+sign → publish; dry-run tested offline, never yet run against a live network) — all permanent tests.

  2. M1 — Bitcoin-anchored first-seen registry — ✅ DELIVERED, published, $0 (see the letter). primacy.mjs + ots.mjs + verify-anchor: an append-only transparency log (RFC 6962), ML-DSA-65-signed heads, first-seen resolution by complete enumeration, and a temporal frontier confirmed against a real Bitcoin block header the verifier supplies — run it yourself, offline, with npx fractal-pqc verify-anchor. Honest residual: this delivers verification of an anchor; it does not deliver creation of one. Submitting a new digest to an OpenTimestamps calendar and polling it to Bitcoin confirmation needs an external tool today (the reference ots CLI or python-opentimestamps) — not vendored here, and not yet documented anywhere outside this README.

  3. M2 — Exposure-relative primacy, in a real custodian's signing flow (Tranche 1, $120,000/90 days — see the letter for the full scope). The primitive is shipped and enforced in [email protected]: policy.mjs's authorizeAndSign computes the BIP-341 sighash itself, requires the prevout to be the coin the holder's anchor governs, and (opt-in via exposureHeight) additionally requires the commitment predate the chain first revealing the classical key. What remains, concretely, is engineering a custodian around that primitive — not the primitive itself: (a) ✅ done, m2-broadcast.mjs — rebuilds the witness an authorizeAndSign decision actually authorised (never a caller-supplied leaf, which would let someone splice in a different script) and serializes/dry-run-broadcasts it; the Schnorr signature is re-verified independently against a freshly recomputed BIP-342 sighash, not against this module's own bookkeeping; (b) ✅ already done (round 9, before this roadmap section was last reconciled) — buildSpendContext takes otherInputs, so a custodian consolidating more than one deposit per holder does not recreate the round-8 signing-oracle bug; the holder's independently-derived digest for a real 2-input spend matches the one the engine signs, proven by R9-holder-can-reproduce-any-digest; (c) ✅ partially donecustodian-log.mjs gives structured audit logging (hash-chained, so an edited/reordered/deleted past decision is detectable — including refusals, not just successes) and idempotent request handling (a retried request under the same key never re-signs, proven by R12-custodian-log-is-idempotent-and-tamper-evident), with an explicit design note on why the raw policyKey is never persisted. Not done: an actual HTTP/RPC layer — this is a library a custodian's service calls, not a service itself; (d) ✅ partially done (round 13) — policy-key-registry.mjs enforces the singlePolicyKeyPerHolder convention instead of trusting callers to uphold it: a policy key stays bound to whichever classical key first authorised under it, and a different holder is refused before a signature is ever computed, proven by R13-policy-key-registry-refuses-cross-holder-reuse against a genuine two-holder attack. Not done: the wrapper is opt-in (a caller can still call authorizeAndSign directly and skip it), and HSM/multisig custody of the raw Schnorr secret itself — losing or leaking that secret is still custodian-wide, not just holder-wide; (e) the funded testnet broadcast itself.

    A custodian using (c) and (d) together should compose them with the audit log OUTERMOST and the registry INNERMOST — custodianAuthorize(log, idempotencyKey, req, ts, (r) => registry.authorize(r)) — round 15 found that the other order (registry outermost) silently drops the audit trail for a refused cross-holder attempt, which is exactly the attempt most worth recording. Proven by R15-log-and-registry-compose-without-losing-either-guarantee and test/composed-custody.mjs, the only place either wrapper is exercised alongside the other rather than in isolation.

  4. Tranche 2, $80,000 — independent external review, multisig PSBT, key rotation. External review: scope not yet defined here, tracked separately from the two engineering items. Multisig PSBT: 0% built. psbt.mjs is single-signer P2TR key-path only; no BIP-174 multisig fields, no k-of-N Tapscript threshold primitive, no multi-leaf policyLeafScript — not even listed in this package's own UNCLAIMED_GUARDS honesty ledger yet. Key rotation: confirmed NOT IMPLEMENTED (npx fractal-pqc claims --gaps says so verbatim) — a holder who loses their ML-DSA-65 secret is locked out permanently, by the same first-seen-primacy logic that makes the rest of the design sound. A real fix needs a pre-registered guardian quorum, a distinct anchored entry type for a rotation event, a challenge window measured in confirmed blocks, and a new "current active key" resolver in policy.mjs — comparable in scope to the original primacy/policy engine.

  5. Tranche 3, $120,000 — public specification, wallet interoperability, long-term maintenance of the vectors. Not started; success is measured by independent adopters running the CC0 vectors, not by our own claims.

License

MIT. Uses @noble/curves, @noble/hashes, @noble/post-quantum (audited, MIT).