fractal-pqc 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -36
- package/bin/cli.mjs +18 -2
- package/letter/galaxy-2026-08-31.md +29 -11
- package/package.json +7 -7
- package/src/claims-registry.mjs +305 -46
- package/src/demo.mjs +200 -0
- package/src/letter-claims.mjs +184 -70
- package/src/m2-broadcast.mjs +111 -0
- package/src/mutations.mjs +87 -6
- package/src/policy.mjs +254 -97
- package/src/tx.mjs +38 -3
- package/src/verify-letter.mjs +69 -14
- package/test/demo.mjs +33 -0
- package/test/letter-claims.mjs +38 -5
- package/test/m2-broadcast.mjs +185 -0
- package/test/m2-policy.mjs +16 -5
- package/test/primacy.mjs +27 -9
package/README.md
CHANGED
|
@@ -124,34 +124,64 @@ The SDK equivalent is `sendP2trKeyPath({ internalPriv, to, amountSats, network,
|
|
|
124
124
|
|
|
125
125
|
The quantum protection does **not** come from a cert verifying in isolation (a cert only
|
|
126
126
|
proves whoever built it held both secret keys). It comes from **anchoring**: the holder
|
|
127
|
-
publishes their commitment first-seen and immutably
|
|
128
|
-
|
|
129
|
-
take `anchoredFactHash` and reject any
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
publishes their commitment first-seen and immutably in an append-only transparency log
|
|
128
|
+
(RFC 6962) anchored into Bitcoin (OpenTimestamps), and every verifier **pins that anchored
|
|
129
|
+
factHash**. `verifySpend`/`verifyMigrationCommitment` take `anchoredFactHash` and reject any
|
|
130
|
+
cert that doesn't match it. Skip the anchor and the kit provides no quantum protection —
|
|
131
|
+
`verifySpend` fails closed rather than pretend otherwise. The anchor registry itself
|
|
132
|
+
(first-seen, no-duplicate, immutable — `primacy.mjs` + `ots.mjs` + `verify-anchor`) is
|
|
133
|
+
**delivered and published** (see the Roadmap below); this reference makes the anchor a
|
|
134
|
+
**required verifier input** so the property is never silently over-claimed. One honest
|
|
135
|
+
residual: this package can *verify* a `.ots` you already obtained — it has no code to
|
|
136
|
+
*submit* a new digest to an OpenTimestamps calendar server or poll it to Bitcoin
|
|
137
|
+
confirmation. Creating a brand-new anchor today requires an external tool (the reference
|
|
138
|
+
`ots` CLI or the `python-opentimestamps` library); we deliberately don't vendor one, per
|
|
139
|
+
`ots.mjs`'s own "no network" scope.
|
|
134
140
|
|
|
135
141
|
## What this is NOT
|
|
136
142
|
|
|
137
|
-
> **
|
|
138
|
-
> `spendDigest` chosen by the requester
|
|
139
|
-
> *legitimate* client of a custodian obtain a policy-key signature over **another client's
|
|
140
|
-
> sighash
|
|
141
|
-
>
|
|
142
|
-
>
|
|
143
|
-
>
|
|
144
|
-
>
|
|
145
|
-
>
|
|
143
|
+
> **NEVER SIGN A DIGEST YOU DID NOT COMPUTE YOURSELF.** Until 0.9.0 `authorizeAndSign` read a
|
|
144
|
+
> 32-byte `spendDigest` chosen by the requester, and round 7 of our own siege used that to have
|
|
145
|
+
> one *legitimate* client of a custodian obtain a policy-key signature over **another client's
|
|
146
|
+
> sighash**. The round-7 fix compared the presented scriptPubKey against one recomputed from the
|
|
147
|
+
> requester's fact hash — and round 8 walked through it, because **both sides of that comparison
|
|
148
|
+
> were the requester's**.
|
|
149
|
+
>
|
|
150
|
+
> An engine that signs a digest it was handed is not a policy engine, it is a signing **oracle**,
|
|
151
|
+
> and no guard around it helps: every check then compares values the caller controls. So the
|
|
152
|
+
> caller no longer supplies a digest. It supplies the **transaction**:
|
|
153
|
+
>
|
|
154
|
+
> ```js
|
|
155
|
+
> authorizeAndSign({ cert, pqSignatureHex, anchorEvidence, expectedLogId, policyKey,
|
|
156
|
+
> cutoffBlockHeight, blockMerkleRoots,
|
|
157
|
+
> spendContext: { tx, inputIndex, prevoutValues, prevoutScriptPubKeys } })
|
|
158
|
+
> ```
|
|
159
|
+
>
|
|
160
|
+
> `spendContext` is **mandatory** and a request carrying `spendDigest` is refused at the door.
|
|
161
|
+
> The engine recomputes the Taproot output key from the holder's **anchored** first-seen fact
|
|
162
|
+
> hash, requires the prevout at `inputIndex` to be that coin, and then derives the BIP-341
|
|
163
|
+
> sighash **itself**. Use `buildSpendContext()` — the same helper a custodian uses — so the
|
|
164
|
+
> holder signs exactly the bytes the policy key will sign.
|
|
165
|
+
>
|
|
166
|
+
> **THE TWO RESIDUALS, stated rather than hidden.** (1) The engine imposes **nothing on the
|
|
167
|
+
> outputs**: it proves *whose* coin is spent and *who* authorised it, never *where* the value
|
|
168
|
+
> goes. (2) `buildSpendContext` builds a **single-input** transaction while the engine accepts
|
|
169
|
+
> multi-input ones, so for those the holder cannot re-derive the digest with the shipped helper
|
|
170
|
+
> and would be signing a digest the custodian computed — the very mistake this section is about.
|
|
171
|
+
> **The holder must inspect the transaction, not merely sign the digest they were handed.**
|
|
146
172
|
(honest scope — do not overstate)
|
|
147
173
|
|
|
148
174
|
- **Not yet confirmed in a live mempool.** The sighash is proven consensus-correct against
|
|
149
|
-
the official BIP-341 vector, and the
|
|
150
|
-
funded **testnet** broadcast (which needs tBTC from
|
|
151
|
-
|
|
175
|
+
the official BIP-341 vector, and both the key-path and script-path (M2) broadcasters are
|
|
176
|
+
built + dry-run-tested — but an actual funded **testnet** broadcast (which needs tBTC from
|
|
177
|
+
a faucet) is the operator's final step for either path.
|
|
178
|
+
- **`p2trAddress`'s signer is key-path-only.** `taprootTweakPrivateKey`/`signTaprootKeyPath`
|
|
152
179
|
handle key-path-only outputs — exactly what this kit's `p2trAddress` derives. Pointing the
|
|
153
180
|
signer at a foreign Taproot output that commits to a script tree would produce an invalid
|
|
154
|
-
signature. Script-path (tapscript)
|
|
181
|
+
signature. Script-path (tapscript) *construction* — the policy leaf, control blocks, and the
|
|
182
|
+
BIP-341 script-path sighash the M2 policy engine signs — IS implemented and asserted against
|
|
183
|
+
the official BIP-341 wallet test vectors (7/7 cases, 12/12 control blocks, byte for byte);
|
|
184
|
+
what's missing is the step after the signature, per the point above.
|
|
155
185
|
- **Secrets are not zeroized.** Private-key `Uint8Array`s are not wiped after use (best-effort
|
|
156
186
|
only in JS); `keygen` prints secrets by design (testnet/experimental).
|
|
157
187
|
- **Not a BIP and not consensus.** The "recovery commitment" is an application-layer
|
|
@@ -162,24 +192,64 @@ so the property is never silently over-claimed.
|
|
|
162
192
|
|
|
163
193
|
## Roadmap — from reference to real Bitcoin custody tooling (the grant-funded work)
|
|
164
194
|
|
|
165
|
-
|
|
195
|
+
Milestones below match the tranche structure in our funding ask (`letter/galaxy-2026-08-31.md`)
|
|
196
|
+
exactly, so this table and that letter can never quietly drift apart the way this section once
|
|
197
|
+
did. Each milestone is independently verifiable, open-source, and shippable on its own.
|
|
166
198
|
|
|
167
199
|
1. **Bitcoin address + tx layer** — ✅ *done + vector-verified*: bech32/bech32m (BIP-173/350),
|
|
168
|
-
P2TR `bc1p…` derivation (BIP-341), segwit tx + txid (genesis-checked),
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
200
|
+
P2TR `bc1p…` derivation (BIP-341), segwit tx + txid (genesis-checked), key-path AND
|
|
201
|
+
script-path BIP-341 sighashes **matching the official vectors byte-for-byte** (7/7
|
|
202
|
+
scriptPubKey cases, 12/12 control blocks), **fees + coin selection**, **BIP-174 PSBT**
|
|
203
|
+
create→sign→finalize (key-path), and a **testnet broadcaster** (fetch UTXOs/fees →
|
|
204
|
+
build+sign → publish; dry-run tested offline, never yet run against a live network) — all
|
|
205
|
+
permanent tests.
|
|
206
|
+
|
|
207
|
+
2. **M1 — Bitcoin-anchored first-seen registry** — ✅ **DELIVERED, published, $0** (see the
|
|
208
|
+
letter). `primacy.mjs` + `ots.mjs` + `verify-anchor`: an append-only transparency log
|
|
209
|
+
(RFC 6962), ML-DSA-65-signed heads, first-seen resolution by complete enumeration, and a
|
|
210
|
+
temporal frontier confirmed against a real Bitcoin block header the verifier supplies —
|
|
211
|
+
run it yourself, offline, with `npx fractal-pqc verify-anchor`. **Honest residual:** this
|
|
212
|
+
delivers *verification* of an anchor; it does not deliver *creation* of one. Submitting a
|
|
213
|
+
new digest to an OpenTimestamps calendar and polling it to Bitcoin confirmation needs an
|
|
214
|
+
external tool today (the reference `ots` CLI or `python-opentimestamps`) — not vendored
|
|
215
|
+
here, and not yet documented anywhere outside this README.
|
|
216
|
+
|
|
217
|
+
3. **M2 — Exposure-relative primacy, in a real custodian's signing flow** (Tranche 1,
|
|
218
|
+
$120,000/90 days — see the letter for the full scope). The primitive is shipped and
|
|
219
|
+
enforced in `fractal-pqc@0.9.0`: `policy.mjs`'s `authorizeAndSign` computes the BIP-341
|
|
220
|
+
sighash itself, requires the prevout to be the coin the holder's anchor governs, and (opt-in
|
|
221
|
+
via `exposureHeight`) additionally requires the commitment predate the chain first revealing
|
|
222
|
+
the classical key. **What remains, concretely, is engineering a custodian around that
|
|
223
|
+
primitive — not the primitive itself:** (a) ✅ *done*, `m2-broadcast.mjs` — rebuilds the
|
|
224
|
+
witness an `authorizeAndSign` decision actually authorised (never a caller-supplied leaf,
|
|
225
|
+
which would let someone splice in a different script) and serializes/dry-run-broadcasts it;
|
|
226
|
+
the Schnorr signature is re-verified independently against a freshly recomputed BIP-342
|
|
227
|
+
sighash, not against this module's own bookkeeping; (b) ✅ *already done* (round 9,
|
|
228
|
+
before this roadmap section was last reconciled) — `buildSpendContext` takes `otherInputs`,
|
|
229
|
+
so a custodian consolidating more than one deposit per holder does not recreate the round-8
|
|
230
|
+
signing-oracle bug; the holder's independently-derived digest for a real 2-input spend
|
|
231
|
+
matches the one the engine signs, proven by `R9-holder-can-reproduce-any-digest`; (c) a thin
|
|
232
|
+
service wrapper (HTTP/RPC, structured audit logging of every authorize/refuse decision,
|
|
233
|
+
idempotent UTXO handling) around the pure library; (d) a real answer for `policyKey`
|
|
234
|
+
custody (today a single Schnorr key per custodian — losing or leaking it is custodian-wide,
|
|
235
|
+
not just holder-wide, and nothing here enforces the `singlePolicyKeyPerHolder` convention
|
|
236
|
+
callers are trusted to uphold); (e) the funded testnet broadcast itself.
|
|
237
|
+
|
|
238
|
+
4. **Tranche 2, $80,000 — independent external review, multisig PSBT, key rotation.**
|
|
239
|
+
External review: scope not yet defined here, tracked separately from the two engineering
|
|
240
|
+
items. **Multisig PSBT: 0% built.** `psbt.mjs` is single-signer P2TR key-path only; no
|
|
241
|
+
BIP-174 multisig fields, no k-of-N Tapscript threshold primitive, no multi-leaf
|
|
242
|
+
`policyLeafScript` — not even listed in this package's own `UNCLAIMED_GUARDS` honesty
|
|
243
|
+
ledger yet. **Key rotation: confirmed NOT IMPLEMENTED** (`npx fractal-pqc claims --gaps`
|
|
244
|
+
says so verbatim) — a holder who loses their ML-DSA-65 secret is locked out permanently,
|
|
245
|
+
by the same first-seen-primacy logic that makes the rest of the design sound. A real fix
|
|
246
|
+
needs a pre-registered guardian quorum, a distinct anchored entry type for a rotation
|
|
247
|
+
event, a challenge window measured in confirmed blocks, and a new "current active key"
|
|
248
|
+
resolver in `policy.mjs` — comparable in scope to the original primacy/policy engine.
|
|
249
|
+
|
|
250
|
+
5. **Tranche 3, $120,000 — public specification, wallet interoperability, long-term
|
|
251
|
+
maintenance of the vectors.** Not started; success is measured by independent adopters
|
|
252
|
+
running the CC0 vectors, not by our own claims.
|
|
183
253
|
|
|
184
254
|
## License
|
|
185
255
|
|
package/bin/cli.mjs
CHANGED
|
@@ -237,7 +237,7 @@ switch (cmd) {
|
|
|
237
237
|
case "selftest": {
|
|
238
238
|
const r = spawnSync(process.execPath, [join(__dirname, "..", "test", "vectors.mjs")], { stdio: "inherit" });
|
|
239
239
|
if ((r.status ?? 1) !== 0) process.exit(r.status ?? 1);
|
|
240
|
-
for (const f of ["transparency.mjs", "primacy.mjs", "anchoring.mjs", "conformance.mjs", "m2-policy.mjs", "bip341-scriptpath.mjs", "claims.mjs", "letter-claims.mjs"]) {
|
|
240
|
+
for (const f of ["transparency.mjs", "primacy.mjs", "anchoring.mjs", "conformance.mjs", "m2-policy.mjs", "m2-broadcast.mjs", "bip341-scriptpath.mjs", "claims.mjs", "demo.mjs", "letter-claims.mjs"]) {
|
|
241
241
|
const t = spawnSync(process.execPath, [join(__dirname, "..", "test", f)],
|
|
242
242
|
{ stdio: "inherit", env: { ...process.env, FRACTAL_SELFTEST_DEPTH: "1" } });
|
|
243
243
|
if ((t.status ?? 1) !== 0) process.exit(t.status ?? 1);
|
|
@@ -247,6 +247,14 @@ switch (cmd) {
|
|
|
247
247
|
|
|
248
248
|
// Run every checkable assertion in the letter against this package. The letter cannot be
|
|
249
249
|
// sent if this is not green — which is the entire point of building it.
|
|
250
|
+
// The whole story in one run: identity -> commitment -> log -> Bitcoin anchor ->
|
|
251
|
+
// offline verification -> the custodian decision, and the six refusals. The enforcement
|
|
252
|
+
// point had no CLI verb at all, so a reviewer could read about it and never run it.
|
|
253
|
+
case "demo": {
|
|
254
|
+
const { runDemo } = await import("../src/demo.mjs");
|
|
255
|
+
process.exit(await runDemo());
|
|
256
|
+
}
|
|
257
|
+
|
|
250
258
|
case "verify-letter": {
|
|
251
259
|
const { verifyLetter } = await import("../src/verify-letter.mjs");
|
|
252
260
|
const { SHIPPED_LETTER } = await import("../src/letter-claims.mjs");
|
|
@@ -451,8 +459,16 @@ Usage:
|
|
|
451
459
|
shipped here and proves each claim DIES when the
|
|
452
460
|
code it names is broken. A claim no mutation can
|
|
453
461
|
kill is vacuous, and is reported as such.
|
|
462
|
+
fractal-pqc demo THE WHOLE STORY, offline, in one run: identity,
|
|
463
|
+
dual-signed commitment, transparency log, Bitcoin
|
|
464
|
+
anchor, offline verification, and the custodian
|
|
465
|
+
policy engine authorising the rightful holder and
|
|
466
|
+
REFUSING six attacks — including a post-quantum
|
|
467
|
+
adversary holding the victim's classical key.
|
|
468
|
+
fractal-pqc verify-letter [file] Check every factual assertion in our letter
|
|
469
|
+
against this package. Exits non-zero if any fails.
|
|
454
470
|
fractal-pqc verify-vector Check the official BIP-340 test vector
|
|
455
|
-
fractal-pqc selftest Run everything:
|
|
471
|
+
fractal-pqc selftest Run everything: 357 real checks, no mocks
|
|
456
472
|
|
|
457
473
|
Docs: integrations/pqc-migration-kit/README.md`);
|
|
458
474
|
process.exit(cmd ? 1 : 0);
|
|
@@ -14,11 +14,11 @@ Three layers. Two exist and are published. The third does not exist, and I won't
|
|
|
14
14
|
|
|
15
15
|
**Layer 3 — Consensus (does NOT exist).** Bitcoin does not reject a spend for lacking a PQ signature, and nothing we ship changes that. I'll be blunter than the pitch requires: the only thing consensus ever checks is a classical Schnorr signature on the policy key. A quantum adversary who reaches that key spends anyway. What this construction actually buys is **concentration** — N exposed classical keys become one hash-hidden, rotatable, custodian-controlled key — plus a post-quantum-signed record of who was authorised to make that key sign, giving the custodian a cryptographic reason to refuse everyone else. That is a real reduction in attack surface and a governance mechanism. **It is not immunity, and we removed the sentence that said it was.**
|
|
16
16
|
|
|
17
|
-
One correction I owe you inside this same paragraph, because it is the finding of our seventh siege and it lands squarely on the sentence above. That sentence used to read "a record of who *should have been allowed to spend*", and until days before this email that was an overclaim: the engine received the spend digest as 32 opaque bytes and never learned **which output** it was signing for. Our own siege stood up a second, entirely legitimate client of the same custodian and had it obtain a policy-key signature over the **first client's** sighash. Every check passed. The concentration this paragraph describes — one policy key across many leaves — is precisely the deployment in which that is theft. It is fixed in `0.
|
|
17
|
+
One correction I owe you inside this same paragraph, because it is the finding of our seventh siege and it lands squarely on the sentence above. That sentence used to read "a record of who *should have been allowed to spend*", and until days before this email that was an overclaim: the engine received the spend digest as 32 opaque bytes and never learned **which output** it was signing for. Our own siege stood up a second, entirely legitimate client of the same custodian and had it obtain a policy-key signature over the **first client's** sighash. Every check passed. The concentration this paragraph describes — one policy key across many leaves — is precisely the deployment in which that is theft. It is fixed in `0.9.0`: the leaf commits to the holder's own anchored fact hash, the engine recomputes the output key from it, and **a request that does not say which coin it is for is refused** rather than defaulted. A footgun you step on by omission is a fail-open default with a paragraph next to it.
|
|
18
18
|
|
|
19
19
|
**2) You can run all of it, right now, without believing any of this**
|
|
20
20
|
|
|
21
|
-
Including this email. It makes
|
|
21
|
+
Including this email. It makes eighteen factual assertions — every count, every block
|
|
22
22
|
height, every version, and five sentences about what the engine does. Each one is registered
|
|
23
23
|
with an executable check that reads the asserted value **out of this letter** and compares it
|
|
24
24
|
to a value measured from the package. Change a number in the letter and the check reads the
|
|
@@ -31,22 +31,38 @@ npx fractal-pqc verify-letter # this email, checked against the code, on your
|
|
|
31
31
|
|
|
32
32
|
The letter you are reading ships inside the package, so you can also confirm nothing was
|
|
33
33
|
altered between my sending it and your reading it. **If any assertion fails, this email is
|
|
34
|
-
wrong — and you will know before I do.** It also prints, in the same output, the
|
|
35
|
-
in here that no check covers: two judgements, one hand-counted aggregate, and every
|
|
36
|
-
forward-looking commitment. Those are opinions and promises, and we refuse to report them as
|
|
37
|
-
verified.
|
|
34
|
+
wrong — and you will know before I do.** It also prints, in the same output, the five things in here that no check covers: two judgements, one hand-counted aggregate, one historical figure that can no longer be re-measured, and every forward-looking commitment. Those are opinions, memories and promises, and we refuse to report them as verified.
|
|
38
35
|
|
|
39
36
|
I built that tool because of the disease in section 5, and I built it for this letter
|
|
40
37
|
specifically. It found three false numbers in my own draft before you saw it.
|
|
41
38
|
|
|
42
39
|
```
|
|
43
|
-
npm i fractal-pqc@0.
|
|
40
|
+
npm i fractal-pqc@0.10.0
|
|
41
|
+
npx fractal-pqc demo # the whole path, offline, in 20 seconds — see below
|
|
44
42
|
npx fractal-pqc claims # every security claim we make, each with an attack
|
|
45
43
|
npx fractal-pqc claims --gaps # and what the green does NOT cover
|
|
46
44
|
npx fractal-pqc claims --mutate # break the code, watch the sentences die
|
|
47
|
-
npx fractal-pqc selftest #
|
|
45
|
+
npx fractal-pqc selftest # 357 checks, no mocks
|
|
48
46
|
```
|
|
49
47
|
|
|
48
|
+
**Start with `demo`.** It is the answer to your question, executed rather than described.
|
|
49
|
+
In one offline run it generates a holder identity, dual-signs a migration commitment, appends
|
|
50
|
+
it to an RFC 6962 log, signs the head with ML-DSA-65, anchors it, verifies the anchor by
|
|
51
|
+
recomputing everything — and then runs the custodian policy engine. That last part is the
|
|
52
|
+
point, and it is why most of what you will watch are **refusals**: the engine authorises the
|
|
53
|
+
rightful holder once, and then refuses six times. Including this one, which is the attack the
|
|
54
|
+
whole design exists for:
|
|
55
|
+
|
|
56
|
+
> *a post-quantum adversary who already HOLDS the victim's classical key, produces a genuinely
|
|
57
|
+
> valid dual-signed rebinding, and an honest append-only log accepts it — and the engine still
|
|
58
|
+
> refuses, because the attacker loses on PRIMACY, not on the signature.*
|
|
59
|
+
|
|
60
|
+
The demo names the synthetic parts (there is no network call, so the Bitcoin attestation is
|
|
61
|
+
constructed locally and it says so on the line where it matters), and it ends by naming the
|
|
62
|
+
ceiling: consensus only ever checks a classical Schnorr signature, so this is not immunity.
|
|
63
|
+
It is also a test — `test/demo.mjs` fails the build if any of those six refusals ever starts
|
|
64
|
+
authorising, which is the only reason I am willing to put it in this email.
|
|
65
|
+
|
|
50
66
|
Published today. `verify-anchor` verifies a bundle offline: the tree head's ML-DSA-65 signature, the inclusion proof with the Merkle root **recomputed** rather than compared to one we hand you, an RFC 6962 consistency proof that no history was rewritten, and a Bitcoin anchor derived from the OpenTimestamps proof alone — height and Merkle root, which you then check against a header from **your own node**. We are not in that path.
|
|
51
67
|
|
|
52
68
|
A bundle ships in the package so you can run that path end to end;
|
|
@@ -76,7 +92,7 @@ What remains is, deliberately, **precisely what we cannot do alone**:
|
|
|
76
92
|
| # | What remains | Why money is required for it | Amount |
|
|
77
93
|
|---|---|---|---|
|
|
78
94
|
| ~~M1~~ | ~~Bitcoin-anchored first-seen registry~~ | **DELIVERED — `npx fractal-pqc verify-anchor`** | **$0** |
|
|
79
|
-
| **M2** | **Exposure-relative primacy, in a real custodian's signing flow** — the design is below, and the primitive is already shipped and enforced in `fractal-pqc@0.
|
|
95
|
+
| **M2** | **Exposure-relative primacy, in a real custodian's signing flow** — the design is below, and the primitive is already shipped and enforced in `fractal-pqc@0.9.0`. What remains is a *custodian* running it against funded testnet spends | The code, the BIP-341 conformance and the gate exist. What is missing is a custodian, and custodian engineering time is what money buys | $120,000 |
|
|
80
96
|
|
|
81
97
|
**Tranche 2 — $80,000** — **independent external review**, published in full including negative findings, plus multisig PSBT and key rotation (today a lost ML-DSA secret is a permanent lockout, and the package says so). We cannot review ourselves; this is the one deliverable whose entire value comes from it not being us.
|
|
82
98
|
**Tranche 3 — $120,000** — public specification, wallet interoperability, long-term maintenance of the vectors. Success measured by independent adopters, not by our own claims.
|
|
@@ -95,6 +111,8 @@ Where that inequality holds, the proof depends on **no Q-day date**: at the mome
|
|
|
95
111
|
|
|
96
112
|
It is wired as a **gate**, not a report: `authorizeAndSign({ …, exposureHeight })` refuses and releases no signature when the commitment was anchored after exposure. Supplied means **enforced** — a parameter that can be passed and silently dropped is worse than one that does not exist.
|
|
97
113
|
|
|
114
|
+
And one structural change our eighth siege forced, which I would rather you heard from me: **the engine computes the digest it signs and refuses any request that carries one.** Until last week it accepted a 32-byte digest from the caller, which makes it a signing *oracle* — every check it performs then compares two values the caller controls. Our own siege used that to have one legitimate client of a custodian obtain a policy-key signature over another client's coin. The caller now supplies the transaction; the engine derives the BIP-341 sighash itself, over the prevout it verified belongs to that holder.
|
|
115
|
+
|
|
98
116
|
And the precise shape of it, which our seventh siege made us state properly: **this gate is a conjunction, never a substitution.** The cutoff you choose stays mandatory and is still applied afterwards, so a commitment anchored at 800,000 with exposure at 850,000 — the strongest position this scheme can give anyone — is still refused under a cutoff of 790,000. Supplying an exposure height *narrows* what authorises; it never rescues what the cutoff rejects. What is true, and is the whole point, is that the second condition depends on no Q-day estimate. The receipt now says exactly that, in those words, instead of the flatter sentence it used to print.
|
|
99
117
|
|
|
100
118
|
The claim is bounded, and we say so in the code itself:
|
|
@@ -116,7 +134,7 @@ We are not the standard, and anyone claiming to be one at this stage is selling.
|
|
|
116
134
|
|
|
117
135
|
**Ours is the only package I know of that will tell you, on your machine, which of its own security claims it cannot back.** A claim is admissible only with three things: an executable proof, an executable *attack* that must fail, and a **mutation** of the code it names under which the claim must fail. A sentence no mutation can kill is reported as vacuous and the build breaks.
|
|
118
136
|
|
|
119
|
-
We built that because we needed it.
|
|
137
|
+
We built that because we needed it. Eleven adversarial siege rounds against our own code — ten of them found a real defect, roughly 1,150 executed exploits between those ten — found the same disease every time and never once in the mathematics: an English sentence and a code path written separately, with a fully green test suite in between hiding the gap. Round 5 deleted a single line binding an anchor to the head it timestamps; the ledger printed all-green and 263 assertions passed, because no claim named that guard. Round 7 — run against the very paragraph in section 3b, in the days before this email — found the cross-client signature described in section 1, and found a frozen scope string still telling auditors that the M2 gate did not exist while the gate was refusing signatures. Round 10, run against this exact letter before it was sent: `cutoffBlockHeight`, `blockMerkleRoots` and `knownHeads` were each read straight off the request at every point they were needed, instead of being snapshotted once the way `anchorEvidence` already was — and for `knownHeads` that was not a theoretical gap. An accessor that shows a genuinely conflicting log head to the length check and an empty array to the loop that actually runs equivocation detection produced a real, verifiable ML-DSA-gated Schnorr signature in exactly the case section 4's limit #4 promises a refusal for. All three fields now get the same snapshot-once discipline. The `knownHeads` and `cutoffBlockHeight` exploits are each closed and covered by a mutation that turns the ledger red if either regresses. `blockMerkleRoots` got the identical fix on the identical reasoning, but we could not construct an attack that flips authorisation through it alone — every consumer looks the anchor height up by key, so a divergent read fails closed rather than open — and we are not shipping a mutation we cannot honestly make fail; that field's hardening is disclosed as defense-in-depth, not as the closure of a demonstrated bypass. Round 11 is the odd one out and we say so rather than pad the count: it found no defect in existing code, because the code it tests did not exist yet. `authorizeAndSign` returned a raw signature and nothing else in the package ever turned it into a broadcastable transaction — the custodian integration this letter's M2 section describes as remaining. `m2-broadcast.mjs` is that missing step, admitted to the same matrix: it rebuilds the exact witness a decision authorised, never a caller-supplied one, and the signature is re-verified independently against a freshly recomputed sighash before anything is finalized. Every mutation in the matrix reintroduces a bug that really shipped in this package, tagged with the round that caught it. **The matrix is simultaneously our test harness and the public record of our own failures.**
|
|
120
138
|
|
|
121
139
|
I am telling you about round 7 in the letter that asks you for money, before you could possibly have found it yourself, because the alternative is a commitment on the next page that would be worth nothing.
|
|
122
140
|
|
|
@@ -125,7 +143,7 @@ That is also why the retraction in section 1 is a *test*, not a footnote: the ol
|
|
|
125
143
|
**Four commitments that go with the money**
|
|
126
144
|
|
|
127
145
|
1. **Every deliverable arrives with its own post-quantum receipt, Bitcoin-anchored.** You verify our work using the mechanism you're funding.
|
|
128
|
-
2. **Public failure clause.** If anyone — including us — breaks the binding scheme during the grant, we publish it in full within 72 hours and stop invoicing until it's fixed. Our own siege has broken it
|
|
146
|
+
2. **Public failure clause.** If anyone — including us — breaks the binding scheme during the grant, we publish it in full within 72 hours and stop invoicing until it's fixed. Our own siege has broken it ten times, the most recent one in the days before this email; that's not a hypothetical policy.
|
|
129
147
|
3. **Vectors stay CC0.** 33 of them are already published. Whoever runs them owns their own verification.
|
|
130
148
|
4. **Non-dilutive, open source, no exclusivity.**
|
|
131
149
|
|
package/package.json
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fractal-pqc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "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 scope.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "FractalAI",
|
|
8
8
|
"homepage": "https://fractalai.net.co",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"url": "https://github.com/fractalai/fractal-ai",
|
|
12
|
-
"directory": "integrations/pqc-migration-kit"
|
|
9
|
+
"bugs": {
|
|
10
|
+
"email": "helloinvestor@fractalai.net.co"
|
|
13
11
|
},
|
|
14
12
|
"engines": {
|
|
15
13
|
"node": ">=18"
|
|
@@ -22,6 +20,7 @@
|
|
|
22
20
|
"./transparency": "./src/transparency.mjs",
|
|
23
21
|
"./primacy": "./src/primacy.mjs",
|
|
24
22
|
"./policy": "./src/policy.mjs",
|
|
23
|
+
"./m2-broadcast": "./src/m2-broadcast.mjs",
|
|
25
24
|
"./tapscript": "./src/tapscript.mjs",
|
|
26
25
|
"./ots": "./src/ots.mjs",
|
|
27
26
|
"./address": "./src/address.mjs",
|
|
@@ -44,14 +43,15 @@
|
|
|
44
43
|
"vectors"
|
|
45
44
|
],
|
|
46
45
|
"scripts": {
|
|
47
|
-
"test": "node test/vectors.mjs && node test/transparency.mjs && node test/primacy.mjs && node test/anchoring.mjs && node test/conformance.mjs && node test/m2-policy.mjs && node test/bip341-scriptpath.mjs && node test/claims.mjs && node test/letter-claims.mjs",
|
|
46
|
+
"test": "node test/vectors.mjs && node test/transparency.mjs && node test/primacy.mjs && node test/anchoring.mjs && node test/conformance.mjs && node test/m2-policy.mjs && node test/m2-broadcast.mjs && node test/bip341-scriptpath.mjs && node test/claims.mjs && node test/demo.mjs && node test/letter-claims.mjs",
|
|
48
47
|
"selftest": "node bin/cli.mjs selftest",
|
|
49
48
|
"conformance": "node test/conformance.mjs",
|
|
50
49
|
"claims": "node bin/cli.mjs claims",
|
|
51
50
|
"mutate": "node bin/cli.mjs claims --mutate",
|
|
52
51
|
"attribution": "node bin/cli.mjs claims --attribution",
|
|
53
52
|
"gaps": "node bin/cli.mjs claims --gaps",
|
|
54
|
-
"verify-letter": "node bin/cli.mjs verify-letter"
|
|
53
|
+
"verify-letter": "node bin/cli.mjs verify-letter",
|
|
54
|
+
"demo": "node bin/cli.mjs demo"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@noble/curves": "^2.2.0",
|