fractal-pqc 0.7.1 → 0.8.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/bin/cli.mjs +33 -5
- package/examples/README.md +21 -0
- package/examples/anchor-bundle.json +30 -0
- package/letter/galaxy-2026-08-31.md +140 -0
- package/package.json +6 -3
- package/src/letter-claims.mjs +403 -0
- package/src/verify-letter.mjs +111 -0
- package/test/claims.mjs +6 -2
- package/test/letter-claims.mjs +108 -0
package/bin/cli.mjs
CHANGED
|
@@ -237,20 +237,48 @@ 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"]) {
|
|
241
|
-
const t = spawnSync(process.execPath, [join(__dirname, "..", "test", f)],
|
|
240
|
+
for (const f of ["transparency.mjs", "primacy.mjs", "anchoring.mjs", "conformance.mjs", "m2-policy.mjs", "bip341-scriptpath.mjs", "claims.mjs", "letter-claims.mjs"]) {
|
|
241
|
+
const t = spawnSync(process.execPath, [join(__dirname, "..", "test", f)],
|
|
242
|
+
{ stdio: "inherit", env: { ...process.env, FRACTAL_SELFTEST_DEPTH: "1" } });
|
|
242
243
|
if ((t.status ?? 1) !== 0) process.exit(t.status ?? 1);
|
|
243
244
|
}
|
|
244
245
|
process.exit(0);
|
|
245
246
|
}
|
|
246
247
|
|
|
248
|
+
// Run every checkable assertion in the letter against this package. The letter cannot be
|
|
249
|
+
// sent if this is not green — which is the entire point of building it.
|
|
250
|
+
case "verify-letter": {
|
|
251
|
+
const { verifyLetter } = await import("../src/verify-letter.mjs");
|
|
252
|
+
const { SHIPPED_LETTER } = await import("../src/letter-claims.mjs");
|
|
253
|
+
const p = rest.find((a) => !a.startsWith("--")) || SHIPPED_LETTER;
|
|
254
|
+
process.exit(await verifyLetter({ path: p, json: rest.includes("--json") }));
|
|
255
|
+
}
|
|
256
|
+
|
|
247
257
|
// Verify a transparency bundle WITHOUT trusting the log that served it.
|
|
248
258
|
// Everything is recomputed locally: the head signature, the Merkle root from the
|
|
249
259
|
// audit path, and (when a previous head is present) the append-only property.
|
|
250
260
|
case "verify-anchor": {
|
|
251
261
|
const path = rest[0];
|
|
252
|
-
if (!path) die("usage: fractal-pqc verify-anchor <bundle.json>
|
|
253
|
-
|
|
262
|
+
if (!path) die("usage: fractal-pqc verify-anchor <bundle.json> --log-id <hex> --cutoff <height> [--block-merkle-root=<h>=<root>]");
|
|
263
|
+
// Round 8, caught by running our own README against a clean install: an unparseable
|
|
264
|
+
// file threw a raw SyntaxError with a stack trace. The verb a reviewer is told to run
|
|
265
|
+
// first must never answer with a crash — it teaches them nothing and it looks like the
|
|
266
|
+
// tool is broken rather than the input.
|
|
267
|
+
let raw;
|
|
268
|
+
try { raw = readFileSync(path, "utf8"); }
|
|
269
|
+
catch (e) { die(`cannot read ${path}: ${e.message}`); }
|
|
270
|
+
let b;
|
|
271
|
+
try { b = JSON.parse(raw); }
|
|
272
|
+
catch {
|
|
273
|
+
die(`${path} is not a JSON transparency bundle.\n` +
|
|
274
|
+
` A bundle is JSON with { sth, entries, otsHex } — the signed head, the COMPLETE\n` +
|
|
275
|
+
` ordered entry list for that head, and the OpenTimestamps proof.\n` +
|
|
276
|
+
` If you passed a .anchor or .ots file, those are the raw anchor blobs that go\n` +
|
|
277
|
+
` INSIDE a bundle, not the bundle itself.\n` +
|
|
278
|
+
` A ready-to-run example ships with this package:\n` +
|
|
279
|
+
` node_modules/fractal-pqc/examples/anchor-bundle.json\n` +
|
|
280
|
+
` and examples/README.md has the exact command, flags included.`);
|
|
281
|
+
}
|
|
254
282
|
// PROVENANCE, applied completely this time. Round 3 fixed the headers and left the
|
|
255
283
|
// OTHER two halves of the predicate readable from the claimant's own JSON: the log
|
|
256
284
|
// identity to pin against, and the cutoff height. Both must come from the operator.
|
|
@@ -424,7 +452,7 @@ Usage:
|
|
|
424
452
|
code it names is broken. A claim no mutation can
|
|
425
453
|
kill is vacuous, and is reported as such.
|
|
426
454
|
fractal-pqc verify-vector Check the official BIP-340 test vector
|
|
427
|
-
fractal-pqc selftest Run everything:
|
|
455
|
+
fractal-pqc selftest Run everything: 315 real checks, no mocks
|
|
428
456
|
|
|
429
457
|
Docs: integrations/pqc-migration-kit/README.md`);
|
|
430
458
|
process.exit(cmd ? 1 : 0);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Ejecutar \`verify-anchor\` — comando completo
|
|
2
|
+
|
|
3
|
+
npx fractal-pqc verify-anchor node_modules/fractal-pqc/examples/anchor-bundle.json \\
|
|
4
|
+
--log-id a6d188b06e777d2a10f10320f9a3f54114383a560ce413671aeffaff1daa11dc \\
|
|
5
|
+
--cutoff 850000 \\
|
|
6
|
+
--block-merkle-root=800000=828dff2ed9ef1e054f10a53d722d8de73632a55798e249b1fdf76f21e7846b1a
|
|
7
|
+
|
|
8
|
+
**Por qué hacen falta esos tres flags, y por qué el bundle no los trae dentro.**
|
|
9
|
+
|
|
10
|
+
| Flag | Por qué NO puede salir del bundle |
|
|
11
|
+
|---|---|
|
|
12
|
+
| \`--log-id\` | Fijar la identidad contra un valor que el propio bundle suministra no prueba nada. Tiene que llegarle por otro canal. |
|
|
13
|
+
| \`--cutoff\` | No existe un Día-Q objetivo. Nos negamos a imprimir uno: la altura la elige quien verifica. |
|
|
14
|
+
| \`--block-merkle-root\` | Un \`.ots\` es dato inerte que *dice* "bitcoin, altura N". Solo una cabecera real lo convierte en un hecho. La ronda 3 de nuestro propio asedio forjó la frontera temporal entera con ~100 bytes porque esto era opcional. Léala de **su propio nodo**: \`bitcoin-cli getblockheader <hash>\`.
|
|
15
|
+
|
|
16
|
+
Para probar que rechaza lo que debe rechazar, cambie un carácter del \`--log-id\` o baje el
|
|
17
|
+
\`--cutoff\` por debajo de 800000: en ambos casos debe negarse, no advertir.
|
|
18
|
+
|
|
19
|
+
> **Estos son datos de DEMOSTRACIÓN.** La atestación de Bitcoin es sintética para que el flujo
|
|
20
|
+
> corra sin red. Contra un ancla real, la raíz de Merkle la pone usted desde su propio nodo — y
|
|
21
|
+
> ese es exactamente el punto: nosotros no estamos en ese camino.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_what": "A PQ-ANCHOR-v1 transparency bundle. Everything here is recomputed by verify-anchor; nothing is trusted because the file says so.",
|
|
3
|
+
"_note": "DEMONSTRATION DATA. The Bitcoin height below is a synthetic attestation so the flow can be run offline. Against a real anchor you would pass a Merkle root read from YOUR OWN node's block header.",
|
|
4
|
+
"subjectClassicalPub": "035e6ea9db0fe35a8c23d3759765720e8f6130e5d7094d3a3efc5ef1b83333219f",
|
|
5
|
+
"sth": {
|
|
6
|
+
"version": 1,
|
|
7
|
+
"logId": "a6d188b06e777d2a10f10320f9a3f54114383a560ce413671aeffaff1daa11dc",
|
|
8
|
+
"treeSize": 2,
|
|
9
|
+
"rootHash": "8816ac848537484210a1e69aa4849e56339ab4b9651f4b660b0ec031833428c9",
|
|
10
|
+
"timestampMs": 1756000000000,
|
|
11
|
+
"signature": "50a222c060ca26951386b643b250a8adc6711933020af3d5d4203ffcad170a9124e3014b93969120a025837ad0a49c5d4dd5903c8365d9cdd9aa7a292aa19146b64de4b47b19ed5b05013031272140e0692bba2a58983a568ede1686210cdad8c9e87ea6f3f7415cf31b51ac9ea291d6fa206454bb99804713963479de95659d843dcc8b906608d11ba8a7c4013e86d351c534b4ccd0569974cdbe2054d7cc53d4e04bbc52b335cb76347fcaa3ba5988b7941e34e6db6875ab90bb5bd4ecc92dedf1cf50494009f212c9835e2f816ac11c06c416d76188c0098e04e738f3366f4ae85a8aaa81890894660a411abc1e09f74560c19ab3ed178a676fa4d17ff5e1aa3ff0ab08320b8599a922e227bc4beaf091b2be54a49f1e7659f981c7f8776190272d5a369d40b0c3491312ff66034952507d87ae133624ce4f1c7b1fae7e351de186f1435638bae6b9748918c61a85705377ed0372f2798abf0057224df60596308970452769a38c472d953ceb61b293770e2828f6c5c24302f1deb816ab529c1dfaad4bb2b85ae8d69849102c9d3c875208334d61c14627724238cbde93a4be62a369ff8f62375865a13122cd2b2458a54e061521c867bf2e1a9637ee98bf406b9831bf46ed9e81decac9aa574ef7d0bded217281a523275fd997f4e1276af763784b10129472338b644bb9ae91901eb1c8d4afd46b7140818eb1c91e2a603e09007171df3c8d9ee3bf9139034826224eff1721921fc3cb843ff8145a260f6a386b830b783d2a8249071d5996d2250a870e004ca699904dc03a20693b6436cf32e8b1736649596f7c919e00694d67148b55b6ff0ef3e02807194a6472ed9a7b014cb357b7494c04eeb779c69af130a77a4c6c2e0b01ab4740d062dcf711b270adde39a0c2457898ca75f5c3769d87c99e159762ca9d20ae6bd6653269130ada4fd2f5242c76bc87538410594a6211ab2f86de47f48cac8bd200e151d72008ccc6a3eced21c0dae63d7b9844f2dae93a791c996dfde67cb9041e46ff5866c6ceb7aff90ed27907760649610f5598001dd441186f69003bdc7ab3b33043c885b7a3883ae14286300cfd0124eb222f033c3d433e755b1e390f70fef9d371a2a9364a398fc903d150a7851247c5e419c68b3fca6ad1e2f4cf76ffbb8c8a95792dc3dc6a7621247402446cae07c1896b1f90d78e8f2b9972e6c089fe58d6fabd2f6b8366a6819316437aa1275cd79234fb7c05df87f9c41fa213187e8b76bc819b35d26b4d8ce1ae876b0c29595c86c04f486d652e555dc52a2abb8d6a55ed74e56838dfadfba6cd8893758034abf8fa8ab3f6a52e764b346f0ecf2ee27ef980a00ab56ec8de5b3c776a766220a3cb72a5aa845f3efc500b77b443897208551a373c0011eb90157cd51f3df91e227c5570f163591033a12a4b46a49a91beccfb685a217f2446081030450e2528887d52193bb3bb466c0f2db0d94a6c900bb5c70cfac2112348e1d7ca6257ea20143b26a807c738eaf23bf1671aeab1a24ae769864bf19689f7b2822deb95b8b1eb6419f22d0437a69c64536702ebc5c373c762cd275fad7be147cdba7425b22d508488e0a3f6a9f557688c4b317bc79bd776d18227ae4291ce23862279372a4ef608f84535c74d6590b61ee5c99a49d63a32142d016224f0d2efeac910aaf38958b32984da7d2ea788d0b1624f49bb36527a18b0018b55cd686adab1e1af743645bda8befd4dd6aa51701213452950bfb4d7de85d261cee6e7f864cdd43ef9c81423fffa0de7f3cc856f83279fd0187f2c8488993aad0f8f90d609ca516ebea8637227f3378c0fdce0414933f37455dc1c1e8c14117e4c7d60f4e5ce23a640fd78b8eeae3f476a961d64d0e54ca982982bba738bf4507f83171fcf7652111ff411dbc92c457291186af22a83cbbe1b724a44a0527c424a8538dd27af6d040ef0bbcfb88722c936f327cb6711614db60013056d35fd53c7e9122346d5280abc1c1133837259656d236e9a50fc917690447ef245d67e557b87b0757ee59beebd190faff5e9809b08803b6f6d3e814ffa3549b31f3e25ff92b7d1ac4ac770681dbe28a43b4ec665f5fd857d62e4755c4246a12c13e68a47b659277965818af8170170c1972c02a9363892ed4f73d5a447a581142937373591eebd90dbefdcc8a9bd9052eb296344847a929eab9d126a7fc6481e687e0adca5524280a4d1641c7b82159617deb233a87af439d26c088ad1d3c15f7a0f4bd64e2f06c65e696088722a059272b3044febbd6e6fb0f2015a5f6fb696cf0329623d583ed33fb49afa8ec1fb5902f1ecff91ddc9d6b38829e3fc556d058d47fa241f44c9d49d3331ffc0110e8d5d3096ab2a776f6ae5eb67908eaa26da0c06d74efa037f21941e403b11adce25798a5604445e1a4d6500d6abaa9454ff193dec0138a0e2190748a6f307731222ecc0e48a89088d0cc2247b0dc072e5e720540a6b229af7553782b32f0ff28a63aedc12f9f97d1270f730ddf009cf9ac11280fe55b072ceac8130473639659456d7d2dd78d6415e4d8c2a64b1cd990d3491e871b68f903f2eb8a4327bf0e8059e878e74a473c12c9fe2776e13b6cf93627fac958690e23b8a0bc7729a454efd593ce3dab03781c72ac78fb8e84c14e899cc64c761f8cdfaf6d6fc830f4ee8dd3810a71e832a385106de49b023bab838ed45cc16d41dc2196c6f17a9c957cf644b257e1e7250aa760be7a7ccb5aeeda62b12f07b25d66e6558931561719764ece4e1aed90dfd42d9422d26268d46d45b805990bf84dda0f2080e67fccc8566a4c6c3ec28d292c3a81f661f7df898e3deec722c59a009800edb922839da6ff8bc0bd5f99ced09d54402d706ace0c791d193b9b60a384e07bb7fc4e747e288044a33a33aff62799c83a09e0b561467445ca189c62bf106ee3e8adb7ecf6c4cbfa1f527db9bb3f553f194d6fd421732222112ccc0f67a00323d05f96947d1a5822d089e9ac69bd47f99edd640d8b241b2273fa3aa6fcec61d67164648341eab3273ee128c30de8ce9cca09516c4345bec500849a3db348d05920765537a2414519b95a4db74ec8569343eb5be989bf6c5f5820dc9842ebe68cde52e00a809241a39f605f1730646c16f377927c90d08135cba121c68a464a7ef7722e94a07221ca876ec615fa086539cd6d1e42bd89a77bd34b22e52214caed4b588f9c29a13d2b26ad833663fba75125a1e48c305a834898e176588844c3c350cf97a553c8833a99a2a1d2553e731609b3115a2a8087d7512bedb88dcdec9aa5d7e515492812217c28580ac96636b78cb8a78697d7165770178cdf79b83d3e39e93cd64269148ae952db6310ec9d7a86202f8e11a603c798581b26688cb42692fb22184e26f735c27d67bf76e856c374b951788b5f8d2bab79eb1d8c4efbca76b6cd85f6d47272258e3bd5f0b6743b858ea523eb39def407771c0935ca6b146674ebcd361d9af8e5ab47b01f71d79fae311009a2ec755401536262ede34a942569d36e4df5cd683618de1fd51a8b27f59924aaff699f7ba0c0ba6dfc03ddac3e00cf58f7bd54f6db3a21b1e0ee47c7cfc481fb44f6776071df87878846d374ea4cbf0f62fa04476c8980defd7ea0da9873748707a24e8156ad616a10dd7e2ced42d331ed4da897e04cfa449cfe461065627f6531b4d98a0f01639a6f7bd025b9a43b00c93ac169b44fdfce769d5037a9ab86db2b356922f3894ac0d8f232e72c5a59ff51c4c33b70973d6737fb752908878d300dcd5c1c1c21d0c05c00aa264475eae27230d6fea55cd3d466254df7e3d2f1e6f55d6528f4cffe0d106aa0664eba0545da0d3232a5968671e714fb5436a88e583063664a6d67323f4b22038c42c7180aaf3453645b38b89fdfdb1d5e3b256a6f6dceab1dd01166282ecf858fad71c4a8cd7d6a3d77d38068d254e55122a37bf6cd4c2a8ae44483332d79a80bb99d7ec92e4f7cf349dbe0f4f3413240f6ada96c52018b8e2fc850fc63a5a2fcb1aac90855d30f23b07b81374f3c14c0de45fb2430bb6ab563d3356eeb51180758cb7fa4c7f239fc587b922ef92a7c347b9b5065bec823be19446b52e9d900f6c6eff75612fb8b6080b9d50285215fb70571d196415986cb03e94bba716ed437e2db3d2b0befbd2e185be70338a78d73001dcc8fdb47af99f70fc2d3e8a051de076ff7deef641c346d4247c3a3f352a91b69450a03285a47f73ea84b8dfce84a0b8563d09a896ce8271421db10f89468565e9bf14bb00d391d6209e9b85970508c9ae21dd85afa33556f71be81f795e82cbc4ee4b9d875bb8535ec10a44b0e68d6cd4f9e12c00d1583538c8d2f7d792f0c657cd1ad32430f0e468b267f9dbacc763c9d556e71af5b21a4328e9c536f98093ebd97f68a993b6649de063e9dc3708202f478b0224fee36a66eeb1b04a69547a8c0c34d07da8a80ff32849fdf25c1573e7d5d9d137584fc07ff52350437ffa5a403cc1f50a2b1a6675ee3f652eb7c4620a79c4d9fe457bb7df06ad1fbf8ea697307ffd0030aa6ea1eb5bdd708fbb543ee92310d6fc8c6ac589de313398a8acaeaf12334b656a6d99b8f437487fbbbed7141c3b3c3d5281afddf01f5b7aa2c1d11840565b78a1adf900000000000000000007101620262e",
|
|
12
|
+
"publicKey": "680819d95404e02e46625a6acf21410dd4d1ac4a7f1f09f172c097f82896315f7f3f217f0f0fd511710e5cbdf3bcb7d2dd87ce61d88ac3ae2015be84820233b6411f973019cf66fb7546a2a1d89894c4ecf7ef4d7dfc90ac15705d192baa62fe60328b77b32cc74ece0e84ea1d2ed679cbd008466f81d3c57142402e2c90990bc33ebbb02b334603cbaf02d59904048deb3baf51c23de32f6a54edaa4dfe47f759b61af57bb8ebb4e0a09f72532c88c23a2e3ffe86a9bd26962eac72e0aed500068bebe231926dd05bae152bca64e1c6c4dd458eeb676c38a3c049e2de25581ad3d674828d99d147c20db96c2a694cec78ae4c251e466afc892e7aada526ae8ea6dd8db3c81535727d9123d6671bd3d94d0ef15601eb0ab38ee28c19ae5b8aef9d2aba9c462102fe24e198c792aa321a13e8a1bec6fbe515cc39a7dd1bf3c3bc549da5d7c10fa01fb2fe6265784e0528cfb55f04d32d5f91d0c2f5f2cb67943c3dbdcca248b3e1fa151f7c0f0281fd6de42fd0aabc64427783355ce9bb9bff3c98e96f743cf0ce78ddd26a9bf4e7bd5204abca9a21959d525093b5f0c4a971ab47a8c2f6029fe442dff353b184aaba09a655acd3cdc03eef0be12ac52862043e5535792312cad3836bb2b6450f2669010db1f790f1984f1887a398e3843970c166197d7e2ab85159aa0cd1ef79f140b23e6876126e85bc5f931ab20b78befac39c201359c77238cacbb083392c775d2cb3ac56e08bbe79806e05476ae64c3ce07911b7b03e505df729eb94e340e686003648b77c8b96577de41738fb10bb3686be6016398ec8b9ab1ec5e82feef7b94c587b9a832b6fc76737ae2cde5630c79f52939705801d6a3f984f5c8445b431d9a927e0aa62fad7efaacc1dd79db6506dcefca57e67d59247eab2d92c0af0c0ecee0161f401515d7e274f25a4add9d91a1673780e6723153de4345ab1f1d3ca1e3bd3261c6fde6a92f5924077de341bdcea4d5845194ec3db494e9eb2c05eaf2ca821a29ecc65abd12c3a8f6779afe67016025158c708deede29a24d35946717557912a8e0b119fbe1fd74bc7ccaafc17b554eea511e99cf74420dd5dd03103a4be2928bf0353f5ee7409bfa174960798b9882e3dd8af38f61e6d9422f5eefc4b0d30cf291e6826475d14f75e58ba43ab841ccae9a5dbf3b164cc1ac9f1eade92483b3b1a11a5676504bf66dae070e7ddb88f58d9d02bc86d05f14e67a52bcae1470d66aa9b42c9403d1c45c2fcc343686cec3d27af0d7578b391298fefd752b4ddb14b97f03b9ad3c2f5fc756bb43c55efffc0b8821638228519cd78dc8566ea7e56be6b907adad910ddfc08dcbb50ec22b65b04001a1e7e95f9d324e2fba09a427c5ca8d1d5704d9ed658b0dd499be1c4d349b49742b74368b222a476e5d153b51b3cf81c82e38875b2f8b59e298aaa284f908ec2b96f584573426c66568cc8df90068a0d5196e189d1ff147aad0de2a0b75f9dbaec4face23d8dcf98ac5a7cedf37308d2621484f8fdf9806d6c38a669bb95bc84bce9d17e984328425c56bc1fcb6d531dfe5203501bac343f0f73a78f0a60b2431c163bd52e91412e01b9eedd667618ff0b68b35b86f7672239589f8e442d272461356beee79af2bafe1e1542fbf62795dcd20b87f1c7d3299ef0384bcad6aa90765e254e51b2c053f435b5c9f7ab0fe38b0be7abf081a60c0af5282c54f2cbb198eae1a8767d45397ed740dc4994939271e121ba1816561645b848c36141e87794438a25fc6fc162363ec6b7d1ee7875b1e8bde88c644b531ad289210c939b4d9a5f2c126b50fc7c39496050d7e01749f70adfe5dcd322e8430cbdc659786bdd79d701ee03d10ec67efcf2a3ed860269621641857e72205f5d2a2fc4fd7c2269bce2914be4803230feff6ebe87cf87858a239267d6a3b4678864e5073dde996dad4e88e35327818fae5104b5e3b92bf6dddc2f3750e72806bed204789681fe094fd005a6b8257273dacf480cd56276b191f6f00a5495794dddc6d1561ec18cdefb03ac91e45fe5cdec28a8facd12817c529c671393ed77ae22048198e8237bdcb0effa556219826ae66620068dc5d0da3cff17232e9f0b7c37d5a7f0dd0d23b9a0ac0520c0aa9b047a439e3c6dd56424d5cd823a9467b99685c0b6555ef87565fbd344693a9a3d8b9bc4ae3e7c960340aeec456dd8d24e56204156ee6fd3426ba07e46c16d23d60d77cf06fe94831e12effcfd29ff389989a6b57b1dae6ebd90b56a184aa6dd7626aa039c9d7d94cd884d0ee82ad468116ea00003156742e4966b78fe79f810f12098f3c3c37a0a6274154dd2cbac40ae8b456ad48e187b21dba6ab2259952de6842a8874ebc7d694a985a740086e6563d46882694d7c4ddbd0f71def37f5de97132a403148a0f7f4303d7bd88c232e25b0c21043f855c2dbfe0eea9b224ab1b71fe9f3475fad6caac40feeb86d99979e521efc4e1a29f8b16f711bdb582503ce6f5c2aac67dcc829d834442b3cc314b428f709a094beb8912ee0b255e0c933f786747776c0714aa899839d94d6f8d815e6d5e8af7779b4d49725bf97d649f9365ef5db97a4d859a9eaf9a71e7bb8b8b0388825d65c91e7f52f6eb6b84507027131f0a61c028896fb9b33ca9ec88fd2a6b19ad5714ed140322f31aa54fa963c99a3a4a14c08af2f454d3de766bcefc26cab0c0637c97419af24f5870e765cb7f121c31529c0f2915fc48262a7cdfe02f376169a9c",
|
|
13
|
+
"signingMode": "derived"
|
|
14
|
+
},
|
|
15
|
+
"entries": [
|
|
16
|
+
{
|
|
17
|
+
"classicalPub": "02eb5a0db09ab52d4d0274efd39e9e47c55ebf78cdf5441700d04964f0617e5b16",
|
|
18
|
+
"pqPublic": "90c7e862cca92a419a9006e4c32457d7827c568e69e25f28e81177407fde383697e8f98e276b1bd988705c7bf9a47b19be65ff599209d10ee318e17059d8982d14810f95088194324bd6b24e7372b170440816a22598dc1d1f7afc5a47e97724ed1ecd6c01d2bd82e5f3f1749c3e3fac2dd71f74be348736b59eddbf72c00b0f81d3095891af5d21fb97e6ff51513da84fc897fd703fab47f68519a3fef2cd1d1c6ca11c5f99f71ff57a5e0e220b3d9279ab0b66a1e8aece9068eb0bb755034b59f0e93f7d3b3822e2ca49054a1a9abe0fedd414362ffe6100a62e500c6c4bc2c8f27835cad84d7cedebbda9cf842ee0ae8512146a99b065ab5579d1a0b8529ff4ebafb2ebc486cae6dd7ed6317f2aa8527e15c6bb7f4b44d4b1bdacafc23d3e618610b5584beebdac90be695bd43be5265d8fa2c0a855d659a3849697d535e016c45400ab69cf71a1fa98066b338b89a5b0aef4fef0340208e6a48d1f477342a1e55269702f582f5d365bc173907e8733af1a084496d286dd6c1dd0ce76080798308b141321dffb60a4b68b6608202a852d78efbb3376d66473e3b23b259937e04fbe3343c92a7e11cccde32be3b218b9d892bd5b1e4d566db9656743092d55b226624091fa32e320b9bd944b939bb43fb75db5047dffdc2926ff24c6656307aa9e8734c423d645852b01c22d36f8a3dafaf9e6e8d400531e278ca89b73786812b1c23b44d4e2821498896754de78939bb66ac5e8c67cbbd6d382939970c01bfd775c6b0408ef55c1b8fd1afebc7e9582e0a5fe69b153420361cc6f61f4df148a0a43468ecb5ee623bd68f396028a49497c9be8f54d936e55cd25e7e7e92cc16627024e1af741b0da3a4c9c2983138c10eb9a163ff052b657d8f9599423c455bf256b653d58917e35a6412fd8d73e9edb146d0c27f9ef3100fe1e857b35c48f95ae66e4f350480c475a0b384f05fe17ecd9d87492294317601a9b585d82b910df1a9f0465a0a43673ae52d8da29ce2b566d9e63f213042bc1f00d456e68e4f0ec6fdfe434f7865d659f7b60fac5658182527c276662d4ba7dcbe1dd64c9422c14f6c18f36c17c83c6eaa24790175d157deea4cdb0f356864ad39a9edb70a645c305ea4a8433be03ff41b6949c136247061bff4b6fb70ad29e3581471f30112db2c7a5b7e3a968da4e9c0b6f642eb339e082a38237f5e8f38998bf5bf9cb493eaced4f2edd5b89d47eb09da8cf64c1c705f7d4ecaba1130967ca82f5e76e596b9939ed5c038391ca379ec3b0261186a55e260084df049f98452ac2d9bf04ee3d0c965f0077d5e38232cd0d1c885ffe2a0142383d6822defb67f11be349d23bd8fc01f197679b06d47ae076b53af9fd4394a68bece54b671ae791ab1e4974ba5c6e102123f2be342bbc5da6780102cb4fc0b9fd78bb8b7aed2db110c0a0a06cf7eb0125903d80741c4e32c6fb459ac4cce6398f3c65b5edea2e3e561df6a31897bde40403a9c0bfcfe9d0de5ef8058c84c86eec8ddb600c632464e491cf9140dca840c6ed2935a87c6e3984eaceac675735a5547491b69de61dccde9c2da65b343cd0adbdd5f00c67a07afd9ea4e8c4322361e3c0a4b0446592063ab565304f16fa72bc1f96ea70e4ec7d61a8c252ffa9bafc51d9133fae026be1f85ac5cfa1583eee794e54de7f5a7a828120d666577d7da9e3c6854c362286bd4219676742bd77094a949fb14b3eea57093f0255deecb94af46b642621a5f0c9722495c5f377b925ffc4c37a1eebc22bb0a1bad226e0b220a507ab8f5ac9174ac63057eabda4c3b7d0404b44365b1da7c19b9aa56ad62327847816828dca0a4edd1ede715e848d54820fbdef470b268f80555576dbd7b1d31101959885b6444dd6d41eb656cd555f0f9750c5b354f94203981edb430511b42e43363064718abd79c75c0f4d20ac6e78f5e0bd0a29209e32d823cf53fd6875bd781dc8d899685801d71890ebec39d818434c518341881b615bd81510f3a52639b40520380d15b5b83cce20cc48fd67d17dc946ded9488648bddbb1f6be50d4e5e7fcf201897a9b6d2509839b4bfb46c08a680a33f6a60cdbc6362b220475e0657abb07bc84249c05be362b43b61526d10625ea6b683b651e729bda84526c91e8367b48731a565a2109b4acac242dcbb66deae412248c42e9c0ee20b15496e40e0fd4457dafff25d1b6eaa21a2e638681a8a40699e7d93ba06761caad33ec41e16dd7bb3513c8077b4336ed6aebf4841cbc619c9779d8b7c8ec4357e6811b894ea0eb81e1d2503ddd5a43020877f6653344b8e3fe937f2078be957fff9d31e1a3a0b96a9e4b40fc9ad777c32a4c69095860054388b165b5b2e9846a7ce4da76650760dcb4d049299ee04528c8ca22fe8dcf25756ce6465c13c889190c0d5564f91047f43a0e9374a7d96945090931ba4c9b24d53ecf436e098078762fb5f413fcd2539ea12533b7dbc928144b5d5eaab13b4c290433952af851e219d5f0154d154c0bceabc9a9cb8572881a0eef9be30982bbf1ea7b1ed13d1d3f3f285075712f83ccdc65c8948e2fb1f4d8a6d3636f1497c664dc740be32eb53cda7c56621df002deb8162cce7b0cb3a3e8f76cc0514c907b7c910fe941843d1234b2996d7df438413aa7cf45318da17b7bbf3eb4dbd015803ccfb0d75314f9b4fc92c1e28e8efb767072c3eac80194a0e2d3ab4f7040bb882e9795748db074430d5f9bf59112395be56ae7258317fd2e8a5394",
|
|
19
|
+
"classicalSig": "db37066b5d6ad310054381917becf2869431dc7aa3bb64940f934c8bcedb7cb17bcc821dc923ad1391f44a63c386505ee9ef3c3bd006b2639d400e03b959603d",
|
|
20
|
+
"pqSig": "3581df49324128170b5af682c3ee327f8e14974ad4d968adf8f0ce7b18da6f022575c8f6adbe3083cc9d4afa72461fd702a172bf6507e6fedec28cb7fd31bcf8cd6529c3bef72c4f9ab960c1166bdeee0d0430ec34536ac25229c27dce4ff7b2237939663da11fbd305e9d878db7ff7adb5bb49f012a492205fd250685c4de8a40dbf7914098d00b1e663436dc31333d63709ce2a5e9d1fb668039e2d02f7053227b2b7457d5d48391356f4aae33913cf6caff65fd8d10375c4986f1eba25d2f12da9dff5ab9aeb25590c2fc3b85309b116bc33f9da154c8b0f6572780be5198174c1895c904f3020c041e68ac84626a8348ece3c40921cf33dbd6acac12f60604bb788bd0605e634b622680ed9b987cf09f54507b524801fee26f9a5c2db667d0ad2993b901349a14428de4ffc9c7ee4ecfa873e6ffba686fe648a271af824ee0a007f9ff2af100a069295ec5a813ba2d12d4acae93c8243035e3f45bcf6e39c2a03f6b0deacb778824236d697924ee9f516f764c99a327f9b381b447217ac28240a32addc0c13b6a5d6ccbb9abcd32f50a75af6b0a4f1929a281cc3346b29d68447fc0681b296d9c0cacdf467af7f9f2df76be4aca0283a3751b19c73062fcf29e1e09bbd367210c3bf36242ed205a141c6b077c8a418233193baf9cb00cacd2f08698926b8008d2d7819405efb69adbd096f334ca6d8fb92acd640ad5e4782f49f6f153e6866e6d4e63ca28739795e164b1c34fe7bc4f1090e300d264b082700e11bbd74099b4e2b65468bf7301e5d9d8cf7e3046fa2e763b6d59b6240c967ea7a9f80e7a586c4c5f0debebe70c8a0cef2bd56f57cc00cf663b2646c1d195f220e536514897d3b6632bb5e65b173184ad73fe3dac911825f929f53a05cf10498536e272ee418d85b658adbad8fd06e5e52de3b28d8e9bc8cae96a3dd3fb1313528ce1c1266f8a52c11c03d34a9e7ac7e6650d71aa67a455531bf010dfb1408ebd1185d71dcacd03bb25d197c9b3df0f6d508d1d3fc1b77786ba5823c11b9768a6a52d49550c1d70b713ee0881158d5a97d7b1853d10cef49053fea3fe58e244fc033a74b8ef33719abe5753b58658519104f6ef1f8cb34c59fb7028b17e50df8424377c93ad2c2b0e5134cd4c55618f1525804b8adf82242e5c9c5c16721a740416301ab702894daf47db61e9c55005a10cbe87a8277da0bd01bb21a184d3c64699f6edcfa82ca60a8fc85566231ec2f0a3152e5317b86a299d3d66d208041604f230268494de58795a15c86bcc36f9c0680ec5ac1fa083eb3ba07cffcfd317edfed5f194d112ca5562f9cfb63e3b63adf6b35921f2c48c870bb0ae5552642fd187db042bca9ab7e486922d08e92c2558751e11aad43d73859f4c7bffb5923f20db6c9af070e47e3be4196daf2e55f9145e4ad5eed3428c2888348e9964ffe58a712522f86ddb0380f66a70ca44c733fc4dabea96e18ecb0cecf5226e7a38da9496d265991008d6f489d58c711fd5404dcd6531821d8402bc535af2d0bb6ccc7e569d1faa8b4f5daa78535d5ff2d1a80475daa5b3bd05be8cff9c82f8e066d5eb71ca63795a221e6b17429b80081b140033ccce91450b49c68f317f4f58b67de17e94dc02204bd25730295ff2a3aa4f94065f8e253c8f95e326fb7b6ece1966c4d50e1b0e8e8e69c2bc94f0d80ce65eda75734611940aa669ecc3835236f346d00204ab14a1473f0907e4a24e2acc04c32d71a75f6bff68247b619c21abcc32e6b2e2ab87f0b319ce70f16d1aaf4db3f9e1ed3430d28786a622855261a160516afe5435fe4c525b23a4ef7ae2aa6df651e31e217dfacc7aae35e81772bc2b5cad20f7fcee14d8dd6fb381002ff545abda8e3f64f40bb4974b3ee8e6a92f189aa653d239a7a27112d532c00e43162b216a2504335a88fc6967e474ded0c75de9c8cdc3a2da68ed3816e86ae58f20cebb98bd32003717df6d0eae3ffb369fcd716c07677ddeb3a1b2486a921f5af05671496b955cf7aee5be73bad7f8e955423fca8e41fbecd2f372e669f692e04305d2dc464862cbed058aa4a86d1b1693b1ea979bef6f1165e0815ccff3e3296121c09a88ec7dd5478f2b62afc01da8f20cd000ce874f2cc051f64e6ed0321c521086d2fd179518dc70c0be94e8759051a7a20c90cfdbead001f35687f03823fb730e16314603061c545593512448acf27608eada9ccc66826b53f5542d92726bc53374f0b3b4771459e8ee7e84a573e352434bffbac1f4cb588f23ef411bd72eb26a497f33d186dfe7415c3034f7c1c83f81d40f5540546abe97511e1f733f12eb00d95da8178aca7814a493b2b1867e35c8dc148fa1ec042365879a706bd782c266f31a8cc2df0c1e620f3a4b8acbb13e5b738f260aa18e53fa29403f68aeb1b222b7f9532ea423fc880d4579e54860fa893c9b5d28a30c4b3e7e4d7e748b793752cd04732ac75e2fb4d5e03ccc02932b851eb83f1deb15461c204c613bba1724c74d586382ac70ce9c8b365d17ea88ceccc386da44747e280b4740b0b2a037ce084e87f7f5af030a4230ae984e909be84e9acbcf3aa5b982d16be793505b8ce2dde7e6255a1b514e9f869ff08262cdb3cc9a1d688dc27e39e3ce44844ccfcec46573e82a814a379f663e5c16f5dc5495229fd2e63df760bef6a83704a3a1a4c0182465033e693b3e62ce2ecd91dafb0427862f85fdb73c4dad72d2b3b678684048b5507d2c56e91cefe52e9c80cf8cb2d957cd09d41155a1fc9db98ad2c7c52d43f509d752973d7b43f882824e5beda53a7a29795f8afd57d84750bb80b24dd3767306389e79be75f61e9d5129843f2cb5257da1fe724fa6bf382d7d830a70652da51047a52688a4024a15f511cc883a67d59767ef272a3231aac1734a35e7b2cf04b091917b413eb71cb0e70a2bcbdec223d6fffd54655b9eb81fd9d0e00c1fa8d0c591d89e77eb0a16b015c5f177a1585032dcdb9526632ec6ddda64e2781155192e41bdff87f8f09d768237f79b359fac6a80a70226f16dcd63691e9fcaa2dea500267f2511efdfe2756f4a028f139be832ac9cf5b10993eabd31b8c28b091326897196e635c8a0131b97d4611e973ed947309691816e5d4224a769a176c7fe8c485eb4c42825026f7a74e5a0b1270362771b312faa4dd9f26c1dcee6564d9acd93c8d7f92e94e0a1ea3558a03ded3598ebfa69bb660d93e83f9eecb3a89cf780b06973e8e52325f5482323e1808219d7820c0d5a9813dae812778b7d4a394de37068e4d253b972ebccb914b112fe400412a021f5633c10d7267f851319cc235720a26fbd3fd8de29cae950a2f54e7792924ab65ad97c4c7c70920d05110c52a04dbece125046b29786efb000cafda144a0a82e88c222a766ba6d6725bd4c9ebd688f20bcea2cd30992ca526b653c412197277136375a6e473dd0d3e8165eb175a093acccf3136a65461feb1cb008ae6ea1ef6ad02df9c144d05f6da845fcfd2e79794e3d6bab2e9eec140544c4997986827c43c6ea9c1d13e3383e3bdeabc6f80d9811a678109badda8f440f170905ebcc26dbcc4c25c0376532fc3e1e21ef17152c8af4625c566a0fa0ba51357a7c73577769fe707bb9ac04766130f15e7d6851e2e3c38f768542de0cc272e147ccf9f8755452c1ababb44d16cb0ee1b6e5a1fc5a813739c4a4f412d67b4e0a070fd16bdbcbbdb98da6492071cf9fa165a4ff7ecc51e39f73d8e2a1f550af9203fca625bd7d815ea5eeb3f73b5fd86a62dc5b20de6cc0a3d0ad7ee37ef6f8ee86fdbcc0f145e9b1fc1339703d6f86a25dd85c833f32e80b78b66d252144d75a54c4a73b55f05682ac402b97ed1f3d174399e029ebe15ad4f7931b50f532403adfcc9851c2a3f26034f63338f3384358a5e29ff8010e4cb031a01243a61e0096b7018147587f1aeb1423d28c15d368582805e6ac2df88d96a797865d5397689bbf35385e295d81a425a2e2166dd47ede663de0815ac7fdb0ede43fa6de9ef5d7b2d1325b0c576e6f2da78c8fe60cbdae1fc22179c98d45c7faa60b8d613943554eee3c09368a2e2d8586ac5e57d68affa80d64551865aca2810d2362dc4b90e8e761692d07e931dfd9598131e83974ed3a3ffd57ff7e6806ec1f05f278045d9a86895385ca4b463dd720f9a12896a33c0a415bc737141eeb42794875149751b4dbb3c0e0c2cf7815e3de3e957e7a77f53219e9207a83cc04d2bfd3dcdd450e6325824626d6e6b6179dd379d5f93f2579bc6119e3d83a4e450e8a6c8ff6484f5b662123b8515d732f15a1959cd728744427efc7b24da8dbb015ba2e943a07585434bb2b8758f3bd737867bfdff3cb2e572ea17038a52f9fdbf193cfe50103ae73b4b88b6dff6a2598780fc0fa7243ee7928768006547d697bcd2d9d5a7a2758f869679210b7ece3d1f3a44f654a71c7bc7b2e1ac25df94c2a988dd87f3a20804f1db5ab406995f488b62ff39c59c59be052abbb9bd8c9a95da0d7842ebe8a8c815523e22bc326e67c7cad879196aaa89fb0e33cfe9e8b3bdcd932d374a6c18035629956236599b2d79ed39813ab39621ed31f223077d2f0021421242f52658991c8f1364e8f9fbec3db0334565d8ad3dce0f700243f85c40000000000000000000000000000000000060711182126"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"classicalPub": "035e6ea9db0fe35a8c23d3759765720e8f6130e5d7094d3a3efc5ef1b83333219f",
|
|
24
|
+
"pqPublic": "4998545ea479b3baf5c863524ded6d749f056f1054df6e5d1a8012f1afd97dbaaf7de18581c88dc2352cf81328be8fee6db02565e75432661dc5b5ec9e7503e2ee04075cc51fb444759092e20dce1626486d6182e9e1554aed8e5a6738efa52ed46add5d9809067c2b83d82433d7da5b24c0666994086d580d8d962259b10b0b62c3cb64d1e63a80c2933be9a9ef09aab25f1a4302af474c3c1019e7ec2e982ab55fa75a06fc054e775a76815e4fdec6925e6ef75a5e0ff726d69ebd995a0a969a83289f7df12aa625937ef42fc4a3ed9d9bf3b4227cb05858374c2d8b6c89b122d93c23250999bc9a12b82bb109cc6f0ebae7dc6e0ca0339302668e1da1a0c0281c13ed4ab5aea65945a2dc966fc0a46c6cc762366dc56695768b4b05173d4f86452551fc858434c7604c9e4f4740b70f6015354b074f64153b6a54ce23e5699ec288c4436b2776098109152bdaa2fd715c4ca545c97c135127dc45da3dcf780ddaa3c831fb5d8fedc713cf18341b508bbf78b9511237273802d62e90135e10dd17a8d82a587ae21392bd698518d36c6026558c843d54991a509d0ac5f1d4a2049e18c5f772f5f3ec3e8c9c3d4edf77d3157e1c11eeb858c8db81ba3903dfe9df609e99a392a74949d66d9c5f20ac1d462e9bcc08b802a8f6d81df57ea5843f05e7963c5710867823c86b28a8665445e772368fc044fdc35d96e60e3311c1a291d197da801674e6b2c6b3ae510a0c5d24766f56f25a0311c0d6ceeb6aeeefa2a9ce102902ff830025968259332f8112a400ea4e36ef8a39679dd9498447d123a2532112403fc3cd218fe785a88430165d8a603fc808f4268d39c202d49bd51493935f92be25a334af2baed21737a29d96997ee436a0ab713c61dc96c28be3b38ed62a77b81fa192803ad259ac055c4a9429eca8e00939eefe0ede4032b25b263f8ca44fb152f6edd7a37b8e72f3d1c9fa875403836324d6bf5f13d4a163df66c1bc4905e48579fcd854adcb0001b192cdc17608820b98cc2cb4406b6249070838d015bff6ca8fad1061d037ba480bdff01deb8e06670673a0ea4e22ca6e1920b157492a09abcac018ce2854ffd266635673c93cd44e0e8d3101a6d70b51a8572b4912619f71d0a5c1245a2ec9f2b828342c21f3de4c2312a836391ac450be133bb5c42e038a9c95a9c045bc5ef3c7887fbada3c6ca810a712170f82788c9ad9296c41aabea86736d39e2d0849e0b7578b59a4e0c526d2ace8fb041c4c9cab78eef6b5f86d7ee76b2a0b73c1c884a45fa59f94bd45f5f1ea0527dd9f0c5f3a0446b5ce6c06ade61bb7e4b16f7e1271207613e543aa044a29d3a082dd2643df4b52daa0e8174d15a37d647d420391ecb714703cdbf8d30d26071ac619190d2407ca95bb2fc597b2bdba4afc7f92caddc0013d4910002d1a961b07430e12820b94d7b02d05dbaca6c27642901027845f3c9ae59960386480461cdc6ca8bb5de7dc0849db6d33cbdd92af3c76362b10e18dab911bc0fb5732e23cfb1801ebe66bd71ede367c24fc479e9ffbc6fe968e2f134701fbddba02883c879161c6ab188036e4acc1a438263da551f106e8143d5cf05d48e922aeaf50d6e1c8b5e6d644e72f27405dfd2827a15092d0657a9b32e153feaffa51bd3abc3aec58cf5607aa8e81f396a732b6db0f9f078d2ff80d23cccbb8e6b2c3582e04249987959afd98194e43be1332b314e5e3bdb45a0def55f929a67ab0ed2fc7ebff764e348427250c8b201e2c84bb3a38b09ea9f8baa6e749d738e410633aeb21db78d4b95c52a01ebb4c657d846f79f18322d2160e950d24bc65e0a59795b12579eab249c749c85b00b539d6b02c73f4a4bd950cee2c4de9db2addeeba77fb9e6cbae2c98d2402aa5b13edc6ae1ee22f2db4f6ec05a99e9d5d3121b06a61b875ea26cc1881c1710cdb83decb201d19d99c72c409a6b9d9643a13e75eb86131f91490ff24af89dfcbc263e52d7d4736ccfd2f8d48de04c2be13176abbb76d0860f8d7362f9944ecf1bc85b5e4d7badf7e2c01b77beaf63891b200f12861861aff1face5169d19bc5f471c0aed6fce1faea76a647997ef22e9cef025b81ea7e5a1b2c697fc6f0057ff36367b37708f9447b32a8d740e8aad38eb8db405315b53453ca6ae26e05751c6c781e63341e3a2e61e0997e51e5b899e267ba73b2aa276438516c5a73d806911b57ce276b731306c207489735f43fe8ce10e488d808b4a606050563c49bae23c7db1fc76685c66ab738fceaa760463fe6984756f136d02ea554b1c0ec716a0743835f259c95a71f387db819661f8f3c4c51f56692f83013282b0bd6558c0ae7c49b55996632cf2dedf4b3b935dab80862e9411bc72b9ff2fa4b744ba25b20ff5b45146dd692281d74138a7239ecdd16f61013265786067e89649355d178ca4c5698ffe9ae4f5f2f220b7a6298b3188e192eb48068ad813d72148a1bffa69b1ea63052421798fa8b081d69f16aa5e9df04d95b3575f693d35fe42d747aeea0fb8fd171312528c14f78a57970105a0b6db568c39ce437b815f8d6e80197f2b1b9199f3f86245cbacac24e565742f7c6c60672197cc7b3a99155f8daf6f897568538cb458083c53442c4f145e7f86235bb1ab6a27d3720afae1ee86d53b85cb82a26a6606de424e863b0aa1ecc0c7f443024595ce1c723f8d17a55ec9ab27217463eb853b47ebead79f7b0ee1a71514ca94d0f781056a5556e109ebeefdf1c2ad92f8",
|
|
25
|
+
"classicalSig": "9ab62a2db49c55683006d2506e9aadd61558f0eea0c460f9b8241bff781bbfae4213c87a617cbd7e1a4cce82bdffe51e5bc1a3b249171696f4849b8a4d9b0cbe",
|
|
26
|
+
"pqSig": "5b520f2657fcdf322b8ef38ab0baeb439a3fbc64aca050a7d09d3af23643d9da48ff35531ec2eb9290de9c407e1762f41303e3a08c33bb5040377e0a56b53190a8257c48fb612b3cb53cb7670e88d49602ff09ccdb0c64e0a5bbe3c893fd29559114b0c9a6942915130fed0551e3d97b74bd53ba624d73874d0b8728f76920ed13f1c075e5ddb09c74d0517b3ef9fd9c85093a8323088924f96d6cba4157eb432e8bc336c811e0e2aa255a668c6a990b5f764bbf1913dc2792c1f0292aea59160d620cdd687c6b1cc716f1e73cabde3f335533b4e0375fa215b2007f05202a05e9581940a30dd7271907f62cc12306bce75b902590f161748bca47c1fa4904e8d5571cf93fc0a6034c5a0fbe85e713d32662d916765b5d2807e2456f9759014d0593ab1f1d2257e5d143a0b61141abb7b3d62a482f5b72fa69ee7f2060391a7ef139d4889d69ad906de2b59157990d730f97544a892fe057307c324d45b5f01b36e88bf62c46b6f10c465a698444c66819ab837b40ad3f89d721d222ea479eef22f5fc6abf92ace2436f0376e3ef0a23aa95b03ba2611c169c457215f63d9dda63a851e7d9ff4a207e831812e17a8b988755a1668f6be41e23e95a414f6a1940db1f4698b37edf9e14140dd084abdc1ccb03eb46df2c0a12aecb11709614695046145cb99d3d099a75d935c1615e691c9f3391b7cfe1ba56f4058c8aff9eee2118e5f3ef01511beef51310927f07f2a2a83331140ee08fe77ad25bcc89373675be26d1dadb46cd8a4ef1d248bdd87e16f02c386adb82b9eaa21a07280ee17c3277083e3430c06d4547d024a075b40dbc5e0a79c67f6f1c7e0a4b1b5b07fe70509073a291d26c1b6932d4a0bf9336886153f987e7deb903357b893af05e6d928bdbe23d8c6f7fb282df7c4ed19f08197f4693fcae731e7a465b66374eb0b957e97fed063145c2c12a13618156b969979a84d5db64b4337fba33ccccd68c68bb71a96b79279b5f71f97118c16faf437a001fc468865763d8887da74243d48cf24cee63f13be8ad4b4d0d7009176c961769408db0b6479d66f1dbbe5186aad46d6cab45dc2bc4cd0c16f1ba73e54f082b00856e8e3fb33539245d0b8e89b480a99308fd5355f6633be77a3e3873265cd1fad830f4eda70af8621738908ff3899ce1715dedabba3e8bad5bb7344107807cffc10acba5ccf98710a0080e0817f04307f60009d01d7cf39677f287b2ed392dd2746ce0e0387eec2b865e5e9c55721651c4c6b32bdc89dd68dc51fe13d1af9e1e97bde5445071cf098671c95f90317a17b025b9ea41e38268815c13b1e04802005d8805b4bd553271ec0edabf2c302f60185c8f9ef79c4ea1f4b6eaadd08c90592f97a40f8e1f8c14dbf42c996b01c0932b073f14f0264b34fbd50fa89ee1c8503cd09f3b6708954c18e958cf69c052741f067b5c13d860fc41036ea9977d9f3f611255ecc1b2689a2d3ca7e70e10d5ebbd34dbd9ca9f3a694cd9042817fb4e3a1d65f098aa6cbc64189c57271ed93a44af98d58762749814ecd9858d32b49a8fd77054452b454d96d893afa8a0d87b6bfc5ac98772a50c9067260f795dd792e3b791d79023eafd7659b63d647ca1eb32fddb468b9259519a5bd7cc9ff4f546db5b2b6cca5ccfa75f985ebcb379c0483a274ad765e1c5f998db1aca9d2c719dc859636540c91e57a87846cfda0afcf69e3b26d97ceef60b26e8f1f0ac43934cf090e766ece3e4eeda4e6f11c70219bf620ef0eb3326a6fe5bbd72f3c5bfb204200f3bc10c2d9a911a21cffcc62cb4fda96a0a896f241be1aee018b359f11ae7d6b11877414982eb10d4176e76b5cca68e2091506227e6d4948f4b1e0e138f8af56a969db3ca039f11525b7e9f224fd623fb45ed3ea3c80ffa04fa078e73a102bbd1078187df8c986d88c4d1f7ed9cd7ee9f4e322adae48b807d331a4315ac500098c4073dbc53562cc2b8a21adf0373e164979324f691e77c22f268bcade20d5a970a0d632f5d4133969fd4f09e61560f77622f0e4ae331eb808094ab5eac60b9b8c3542c3a569450cb8019166d9f0ee3940fce2eecaec39cbd05b9b25498dc258fa77aaa36388f8e64ef27d1710ac11356909ceabfa82834e4b2ef6bf85559be331cb6f663a32274eff06adc32d1411bbe1b8c50b6edd968868c33cd5fda51617cc4f4f7e1b58584c6b7c10873ee7c000708c4445def3c8b5c34532c44eb9627fbdd4b5cb3afea04809b5ed777c19a3a95babb5031505e972d0142462894d24ade571f921ed175ca227aa749ff8132ed3d73589d27dc173d701a92c9c2ab6dedfabc42d38fd9dc059aa0a7cf81a6c9559a152402ff9c9e985b77f9e29a4a34ab48452437f3442d538cb50747222fdeaeacd6b47d74d7ea16a49e54dd4218ac6bcf760d82ff329593ee32638c8c708c095d59933832025b0d12ec242a5d595cdb77a20b75faeae779e34575c8fdc032bf8df55d95b7c463e6b61ebc8ff8dd0743e742bccb21718f7b720653b6297dc86b489ba74b376356a670c4d9a0eb19c5f2f9931d2c482c15193fc2557ec4bdd16a56bb1426b7bc5d2b078cd652f243439b677fe4f3900445698c9aa2e0489de20da72218782d95afd4be7edf9dfeb74c9830f85730a2ca3d8d672fae4832ed4a6d6f1aed1a7ae5a6e2d6ddc2c2a14fccddf6f4f869d26db522eb6af7fa95e557be4b2e4cbe059b8a8ef8a1b3fd1a36e8a24dc0d314eec25ed40389e9aba7efa5283319826289afabea5c16c17cc5bba60a9cd7f729c1038f36104f08a26a381b4ae033e03b768261b91f1ec2bc252f0a3e2d1f220ff039ed31cd74f85864cfc7ba4bffc369bad356ac4e4ae4abf3a69d16841ebc789420ed6bbc36bdf7b7e553d4c5f41c4fa49a632b0ad498ad551c578d249353feb9163f1d170141f777d79c74ec368647f526bc8692d3fe2125808f8a044df058f771e5d59e53e3347a0bc5f80f4b69b15221e93289d0db2660a8c45a428e4749a35e24cd3b7a9f9b9219624a677baf1993143b06bc80a0eb906d4454d0d0d2d204e6ad317447834cebcde12fbcce0d1a7a40dfb8260497407148e87443e2c0b64de522cc41d13e7d2c56a734cbd4d7aee95f58755865c90dc8911946bfccd2e56d543e37d1eef36e21dca2fce018eb4a1f3a366229a5b378f3da02d51cee1dd512e05ffbf82cad5d93af32359cf851309626478034470b9e44ebf5ef019152af39dc51f90f24103f68a5b3732d4e3ee2a78a8b0be9db4b5030f493e474a2e412594eff6acf17ee89962a227e30d11ac0c8fd1109cdfa7a35622b21fd677b7748d8f6bdc87da975d5bd0f1c308c65b40cacdd24c750e9021623b3e7b86066388907d2c98b378ab075c9191a2fa94e6f799d41acda933554fe288d6f047bbf9e62004a2669d82c1eaa8ba9b64c0be6ceefdbbc8aaf978b24508abace1980ef6566fb7d02f1435d68451937f0f15fd189a530c200341a8343cccf02c9531efc4e493a44f1f083152e981cdff333a967659ec0e0fe18d1aad778643993f2b36e14c45d35abf686e0eabefa33431636d8ffe54d903bbeb7d31944ecb309b0c1be852c0ad8d9d6a93f3b1549fd05555e5fc710a473a7aa94b263aece43d62df521fbec960d5f296db50022d3040ab9cc2e36820a2d3fb47fc116728a874c5d24a8fe4a554d8dbdfba5668f53f97a07b71b3a619d25b9206709f2b305ce54c42c8708acd8c60663382f8905acf5515827474c6dd4bec8f7fd90e5030d3c119d0c066471bc018864badb12bead3908320a71d3783b84dff675715b08def2b1fe4219a0b040079237dd4a0ef907f43cbca11158e8831fb98d138314f33a2282ffa7a916e90ec7d7e1a4856ec2874a4bca38253f9702d99dcea2e428d3dd92e74b27929a370e79544dc0612dadacae4896eeed6623cd5b03b9f8d6419f562a3c2d06b957823fc8c6a177ce426b7bc8f536ea1c6df4ba0e894c83e95dc655e02dc6fd62556019a33819c583e6e1787466bcd4b1e30fa951c45e2672cfdcefa0baa000fa095298726df6d90cf068705a8e38cc08266f8fb6a6f93b86d2746d332022c685c7a04a3a25a1a86163a43a794dc63215d9632c127ddaac59c54600626cb048ce0488caedfbf2ddd7797469e17358a7e1566fc2406a16cadce531db6fc9fe07c2a01a2cb08aff6580c539ac976d8c921472ba0c88ae5009f9891bb6c5c587bbe129a3d38f97bca764fcefa7019f0d024b407f8cce1772e5c198e8f51e1e7915e4db01a0f2fa77a80b084e3c3067dcfc1a0d6ca7becfe9e999c3646616287ed05d52d48ab209137ab5f3ce75440baa0aecfd4f6196fa769db756375188598bd4e6639c4f6fda5b31badfa773c118524ea3fdbe99f6f3e6bda3330e4b02259dd33ea2cd33dba7735cea131d3de3227aa17562c6e338ba2e687793dbc0adc41c17c8d719de8ada44f1feee0c1dd888930e850d5c488a2865b05df5c0b6844bcd7a8c2f6040931c54fd1c4f0f3e215cd3fb5c82402deafe3d7087c94f3bc7bf97c77f3f6d64eae67099a86b8573dc4fdb6e942d80a7174f7cb235ede2cf7e69d409131d360c24686979bb212d3499b2d3197d819aa9c3da66949af51728868caab8d6eff7fa000000000000000000000000000000000000040a10171b25"
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"otsHex": "004f70656e54696d657374616d7073000050726f6f6600bf89e2e884e8929401081a6b84e7216ff7fdb149e29857a53236e78d2d723da5104f051eefd92eff8d82000588960d73d719010380ea30"
|
|
30
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Subject: Re: FractalAI — what enforces the PQ requirement (it's on npm; run it yourself)
|
|
2
|
+
|
|
3
|
+
Hi,
|
|
4
|
+
|
|
5
|
+
Three questions, and the third is the one that matters, so I'll start there — including a boundary I want to state before anything else.
|
|
6
|
+
|
|
7
|
+
**1) What enforces the post-quantum signature requirement**
|
|
8
|
+
|
|
9
|
+
Three layers. Two exist and are published. The third does not exist, and I won't pretend otherwise.
|
|
10
|
+
|
|
11
|
+
**Layer 1 — Binding (exists).** A holder's classical key (secp256k1 / Taproot output key) is bound to an ML-DSA-65 (FIPS-204, NIST Level 3) public key inside a domain-separated commitment.
|
|
12
|
+
|
|
13
|
+
**Layer 2 — Spend policy, fail-closed (exists — the actual enforcement point).** The rule a custodian runs is: **no ML-DSA-65 signature over the spend digest, bound to the holder's first-seen anchored commitment ⇒ no signature is released.** Every guard refuses by default; the signing call is the last statement in the function, and an unexpected input refuses rather than escaping.
|
|
14
|
+
|
|
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
|
+
|
|
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.8.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
|
+
|
|
19
|
+
**2) You can run all of it, right now, without believing any of this**
|
|
20
|
+
|
|
21
|
+
Including this email. It makes fourteen factual assertions — every count, every block
|
|
22
|
+
height, every version, and five sentences about what the engine does. Each one is registered
|
|
23
|
+
with an executable check that reads the asserted value **out of this letter** and compares it
|
|
24
|
+
to a value measured from the package. Change a number in the letter and the check reads the
|
|
25
|
+
new number and fails; change the code and the measurement moves and it fails. Neither can
|
|
26
|
+
drift from the other.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
npx fractal-pqc verify-letter # this email, checked against the code, on your machine
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The letter you are reading ships inside the package, so you can also confirm nothing was
|
|
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 four things
|
|
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.
|
|
38
|
+
|
|
39
|
+
I built that tool because of the disease in section 5, and I built it for this letter
|
|
40
|
+
specifically. It found three false numbers in my own draft before you saw it.
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
npm i fractal-pqc@0.8.0
|
|
44
|
+
npx fractal-pqc claims # every security claim we make, each with an attack
|
|
45
|
+
npx fractal-pqc claims --gaps # and what the green does NOT cover
|
|
46
|
+
npx fractal-pqc claims --mutate # break the code, watch the sentences die
|
|
47
|
+
npx fractal-pqc selftest # 315 checks, no mocks
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
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
|
+
|
|
52
|
+
A bundle ships in the package so you can run that path end to end;
|
|
53
|
+
`node_modules/fractal-pqc/examples/README.md` has the command with its flags filled in:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
npx fractal-pqc verify-anchor node_modules/fractal-pqc/examples/anchor-bundle.json \
|
|
57
|
+
--log-id <the id, obtained independently of the bundle> \
|
|
58
|
+
--cutoff <the height YOU choose> \
|
|
59
|
+
--block-merkle-root=<height>=<root read from YOUR OWN node>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Those three flags are mandatory and **none of them may come from the bundle**. Pinning against an identity the bundle supplies proves nothing; there is no objective Q-day, so the cutoff is yours; and a `.ots` is inert data that merely *claims* a Bitcoin height until a real header confirms it — round 3 of our own siege forged the entire temporal frontier with about a hundred bytes precisely because that last one was optional. Change one character of the log id and it refuses. Lower the cutoff below the anchor and it reports the frontier as NOT established and says, in those words, *do not authorise a spend on this.*
|
|
63
|
+
|
|
64
|
+
To prove our OTS codec is right rather than merely self-consistent, the suite verifies the OpenTimestamps project's **own** example, produced by their reference client and confirmed in Bitcoin block **358391**. If we ever disagree with the reference implementation, our tests fail. Script-path (tapscript) is asserted against the **official BIP-341 wallet test vectors**: 7/7 scriptPubKey cases and 12/12 control blocks, byte for byte.
|
|
65
|
+
|
|
66
|
+
**3) On the $320k — the ask stands, and I've already absorbed the first slice of it**
|
|
67
|
+
|
|
68
|
+
You were right to push back: our application named a number without a line-item scope. Here is the scope, and one change I'd like you to notice.
|
|
69
|
+
|
|
70
|
+
**Milestone 1 is delivered, and it is not on this invoice.** The anchor registry — the exact gap your question exposed — is built, published, and runnable by you today, for $0. I'd rather show you a milestone than describe one.
|
|
71
|
+
|
|
72
|
+
What remains is, deliberately, **precisely what we cannot do alone**:
|
|
73
|
+
|
|
74
|
+
**Tranche 1 — $120,000 / 90 days** (M1 is delivered and unbilled; this is the first paid decision)
|
|
75
|
+
|
|
76
|
+
| # | What remains | Why money is required for it | Amount |
|
|
77
|
+
|---|---|---|---|
|
|
78
|
+
| ~~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.8.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
|
+
|
|
81
|
+
**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
|
+
**Tranche 3 — $120,000** — public specification, wallet interoperability, long-term maintenance of the vectors. Success measured by independent adopters, not by our own claims.
|
|
83
|
+
|
|
84
|
+
You commit to a tranche only after the previous one is publicly verified by you. If a milestone doesn't verify, the engagement stops and you owe nothing further.
|
|
85
|
+
|
|
86
|
+
**3b) M2, in detail — because it is the part I think you will actually care about**
|
|
87
|
+
|
|
88
|
+
**Exposure-relative primacy.** The cutoff frontier in M1 has a weakness we state in our own README: the guarantee is "anchored before the cryptography broke", and **nobody knows when it broke**. Every verifier is guessing a date, and a private break earlier than the guess produces forged entries indistinguishable from real ones.
|
|
89
|
+
|
|
90
|
+
For one important class of holders, that guess is now unnecessary.
|
|
91
|
+
|
|
92
|
+
For a secp256k1 key whose hash is all the chain has ever shown — **an unspent P2PKH/P2WPKH output** — the kit proves a fact checkable in Bitcoin rather than an estimate: that the holder's post-quantum commitment was anchored in a Bitcoin block **strictly before** the block in which their public key first appeared **on chain**. We do not supply the exposure height and deliberately ship **no oracle** for it — the verifier supplies it from their own node, and the anchor is confirmed against the verifier's own block header.
|
|
93
|
+
|
|
94
|
+
Where that inequality holds, the proof depends on **no Q-day date**: at the moment of anchoring the chain had not yet revealed the public key, so the ability to break secp256k1 was not sufficient to have produced that commitment.
|
|
95
|
+
|
|
96
|
+
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
|
+
|
|
98
|
+
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
|
+
|
|
100
|
+
The claim is bounded, and we say so in the code itself:
|
|
101
|
+
- it assumes the holder published the key through **no other channel** — we measure on-chain appearance only, and a key leaked via an unconfirmed mempool broadcast or a shared xpub was derivable earlier than this proof suggests;
|
|
102
|
+
- it **does not save keys already exposed by an earlier spend**. You cannot anchor into the past. Those coins must be moved, and the engine says exactly that when it refuses.
|
|
103
|
+
|
|
104
|
+
We put this through a full adversarial round before writing this paragraph. It found two real defects in the reporting surface — an absent parameter yielding the strongest claim, and refusals that still returned `valid: true` — both now closed, both now covered by mutations that turn the ledger red if they regress. **That round is why this paragraph says "on chain" rather than "public", and why the two bounds above are in the package and not only in this email.**
|
|
105
|
+
|
|
106
|
+
**4) The four limits no design of this shape can close — and we ship them in the package**
|
|
107
|
+
|
|
108
|
+
1. **The last signature is classical.** Covered above. No soft fork, no immunity.
|
|
109
|
+
2. **The cutoff is a date nobody can verify.** The guarantee is "anchored before the cryptography broke", and nobody knows when it broke. A private break earlier than the chosen cutoff produces forged entries indistinguishable from real ones. **We refuse to print a Q-day**; the verifier supplies the height, and without one the engine refuses to authorise at all.
|
|
110
|
+
3. **A timestamp proves "no later than", never "no earlier".** So primacy is relative to the log you pinned. Coverage is bounded by prior *adoption*, not by cryptography.
|
|
111
|
+
4. **Pinning a log's identity does not pin its history.** Two same-size heads carry the same logId. Split view is detectable, never preventable; pass known heads and the engine refuses on contradiction.
|
|
112
|
+
|
|
113
|
+
**5) What I actually think we are, since you'll ask**
|
|
114
|
+
|
|
115
|
+
We are not the standard, and anyone claiming to be one at this stage is selling. Here is the specific thing we are, and you can falsify it in thirty seconds:
|
|
116
|
+
|
|
117
|
+
**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
|
+
|
|
119
|
+
We built that because we needed it. Seven adversarial siege rounds against our own code — roughly 900 executed exploits — 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. 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
|
+
|
|
121
|
+
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
|
+
|
|
123
|
+
That is also why the retraction in section 1 is a *test*, not a footnote: the old immunity claim is a mutation, and it must kill the claim that replaced it. In a field that has watched a lot of projects overclaim quantum immunity, being the team that turned its own retraction into an executable assertion is the only credential I'd ask you to weigh.
|
|
124
|
+
|
|
125
|
+
**Four commitments that go with the money**
|
|
126
|
+
|
|
127
|
+
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 seven times, the most recent one in the days before this email; that's not a hypothetical policy.
|
|
129
|
+
3. **Vectors stay CC0.** 33 of them are already published. Whoever runs them owns their own verification.
|
|
130
|
+
4. **Non-dilutive, open source, no exclusivity.**
|
|
131
|
+
|
|
132
|
+
**One more thing, possibly worth more than the money.** Galaxy Research publishes. I'd rather this end as a citable artefact than a vendor deliverable: publish the binding spec and the conformance vectors **jointly**, with your team as reviewers, and every red-team finding — ours included — disclosed. Whoever owns the vectors that wallets run shapes how this migration gets done.
|
|
133
|
+
|
|
134
|
+
**Where we stand, plainly.** FRACTAL AI S.A.S. (Colombia). We run a post-quantum L1 in production whose consensus signs every block with ML-DSA-65 — a single-operator chain, which we state publicly rather than calling it a network. We are pre-revenue. We are not a Bitcoin protocol team, which is exactly why the kit is validated against official BIP vectors rather than our own assumptions.
|
|
135
|
+
|
|
136
|
+
Could we do 20 minutes this week? Agenda: (1) the enforcement path line by line, including the attack that broke v1; (2) the four limits, and whether you agree they are irreducible; (3) whether this tranche structure fits how you fund.
|
|
137
|
+
|
|
138
|
+
Best,
|
|
139
|
+
John Edward Romo Sánchez
|
|
140
|
+
CEO, FRACTAL AI S.A.S. (NIT 902025722-1) · helloinvestor@fractalai.net.co
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fractal-pqc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -36,19 +36,22 @@
|
|
|
36
36
|
"LICENSE",
|
|
37
37
|
"README.md",
|
|
38
38
|
"bin",
|
|
39
|
+
"examples",
|
|
40
|
+
"letter",
|
|
39
41
|
"src",
|
|
40
42
|
"test",
|
|
41
43
|
"tools",
|
|
42
44
|
"vectors"
|
|
43
45
|
],
|
|
44
46
|
"scripts": {
|
|
45
|
-
"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",
|
|
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
48
|
"selftest": "node bin/cli.mjs selftest",
|
|
47
49
|
"conformance": "node test/conformance.mjs",
|
|
48
50
|
"claims": "node bin/cli.mjs claims",
|
|
49
51
|
"mutate": "node bin/cli.mjs claims --mutate",
|
|
50
52
|
"attribution": "node bin/cli.mjs claims --attribution",
|
|
51
|
-
"gaps": "node bin/cli.mjs claims --gaps"
|
|
53
|
+
"gaps": "node bin/cli.mjs claims --gaps",
|
|
54
|
+
"verify-letter": "node bin/cli.mjs verify-letter"
|
|
52
55
|
},
|
|
53
56
|
"dependencies": {
|
|
54
57
|
"@noble/curves": "^2.2.0",
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* letter-claims.mjs — the letter verifies itself.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS
|
|
5
|
+
* ---------------
|
|
6
|
+
* Seven adversarial siege rounds against this package found the same defect every time and
|
|
7
|
+
* never once in the mathematics: an English sentence and a code path written separately,
|
|
8
|
+
* with a green test suite in between hiding the gap. `claims-registry.mjs` closed HALF of
|
|
9
|
+
* that problem — it governs CODE -> CLAIM, so killing the code kills the claim. Nothing
|
|
10
|
+
* governed PROSE -> CODE, and every one of round 7's findings landed in exactly that hole,
|
|
11
|
+
* including a frozen scope string that told auditors a gate did not exist while the gate
|
|
12
|
+
* was refusing signatures.
|
|
13
|
+
*
|
|
14
|
+
* This is the other half, applied to the highest-stakes prose we have: the letter that asks
|
|
15
|
+
* an investor for money.
|
|
16
|
+
*
|
|
17
|
+
* THE RULE THAT MAKES IT REAL, NOT DECORATIVE
|
|
18
|
+
* -------------------------------------------
|
|
19
|
+
* A claim here NEVER hardcodes the number it is checking. It EXTRACTS the asserted value
|
|
20
|
+
* from the letter text and compares it against a value measured from the live package.
|
|
21
|
+
*
|
|
22
|
+
* - Change the letter -> the extractor reads the new value -> the check fails.
|
|
23
|
+
* - Change the code -> the measurement changes -> the check fails.
|
|
24
|
+
*
|
|
25
|
+
* Neither can drift from the other without the tool going red. That is the whole point, and
|
|
26
|
+
* it is why the extractor must never be allowed to "not find" a value quietly: an assertion
|
|
27
|
+
* that vanishes from the letter is reported as MISSING, never as passing.
|
|
28
|
+
*
|
|
29
|
+
* AND THE VACUITY RULE, INHERITED FROM THE CLAIMS LEDGER
|
|
30
|
+
* -----------------------------------------------------
|
|
31
|
+
* A check that cannot fail is measuring nothing. Every claim therefore carries `falsify`: a
|
|
32
|
+
* perturbation of the ASSERTED value under which the check MUST fail. A claim that survives
|
|
33
|
+
* its own falsification is reported as VACUOUS and the run is not green.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { execFileSync } from "node:child_process";
|
|
37
|
+
import { readFileSync } from "node:fs";
|
|
38
|
+
import { fileURLToPath } from "node:url";
|
|
39
|
+
import { dirname, join } from "node:path";
|
|
40
|
+
import { ml_dsa65 } from "@noble/post-quantum/ml-dsa.js";
|
|
41
|
+
|
|
42
|
+
const PKG = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
43
|
+
export const SHIPPED_LETTER = join(PKG, "letter", "galaxy-2026-08-31.md");
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Pull one asserted value out of the letter.
|
|
47
|
+
*
|
|
48
|
+
* Absence is a FAILURE, never a pass — an assertion that quietly disappears must not be
|
|
49
|
+
* mistaken for one that holds.
|
|
50
|
+
*
|
|
51
|
+
* And AMBIGUITY is a failure too. The first version took `re.exec`, which returns the FIRST
|
|
52
|
+
* match and silently ignores the rest, so a letter reading "block **358391** (and also block
|
|
53
|
+
* **999999**)" verified green: the checker read the true value and never saw the false one
|
|
54
|
+
* sitting beside it. Our own adversarial pass found that. A claim stated twice with two
|
|
55
|
+
* different values is not a claim we can verify — it is two claims, one of which we are not
|
|
56
|
+
* checking. Refuse instead of picking one.
|
|
57
|
+
*/
|
|
58
|
+
function extract(text, re, label) {
|
|
59
|
+
const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : re.flags + "g");
|
|
60
|
+
const values = [...text.matchAll(g)].map((m) => m[1]);
|
|
61
|
+
if (values.length === 0) return { missing: true, label };
|
|
62
|
+
const distinct = [...new Set(values.map(String))];
|
|
63
|
+
if (distinct.length > 1) return { ambiguous: true, label, values: distinct };
|
|
64
|
+
return { value: values[0], label, occurrences: values.length };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const num = (s) => Number(String(s).replace(/[, ]/g, ""));
|
|
68
|
+
|
|
69
|
+
// ── measurements, each against the LIVE package ────────────────────────────
|
|
70
|
+
let _selftest = null;
|
|
71
|
+
export const RECURSION_GUARD = "letter-claim measurement would re-enter selftest";
|
|
72
|
+
function selftestTotal() {
|
|
73
|
+
// A letter claim that MEASURES the suite cannot run INSIDE the suite: selftest would
|
|
74
|
+
// spawn letter-claims, which would spawn selftest, forever. Refuse loudly instead of
|
|
75
|
+
// hanging, and let the caller report the skip rather than pretending it passed.
|
|
76
|
+
if (process.env.FRACTAL_SELFTEST_DEPTH) { const e = new Error(RECURSION_GUARD); e.recursion = true; throw e; }
|
|
77
|
+
if (_selftest !== null) return _selftest;
|
|
78
|
+
const out = execFileSync(process.execPath, [join(PKG, "bin", "cli.mjs"), "selftest"],
|
|
79
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 600000 });
|
|
80
|
+
let total = 0, failed = 0;
|
|
81
|
+
for (const m of out.matchAll(/^(\d+) passed, (\d+) failed$/gm)) { total += +m[1]; failed += +m[2]; }
|
|
82
|
+
_selftest = { total, failed };
|
|
83
|
+
return _selftest;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function bip341Counts() {
|
|
87
|
+
const out = execFileSync(process.execPath, [join(PKG, "test", "bip341-scriptpath.mjs")],
|
|
88
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 300000 });
|
|
89
|
+
const spk = /all (\d+) official scriptPubKey cases parsed/.exec(out);
|
|
90
|
+
const cb = /all (\d+) control blocks reproduce the official bytes/.exec(out);
|
|
91
|
+
return { scriptPubKey: spk ? +spk[1] : -1, controlBlocks: cb ? +cb[1] : -1 };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function vectorCount() {
|
|
95
|
+
const v = JSON.parse(readFileSync(join(PKG, "vectors", "pq-anchor-v1.json"), "utf8"));
|
|
96
|
+
return Object.values(v.vectors || {}).reduce((n, a) => n + (Array.isArray(a) ? a.length : 0), 0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function otsReferenceHeight() {
|
|
100
|
+
const out = execFileSync(process.execPath, [join(PKG, "test", "anchoring.mjs")],
|
|
101
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 300000 });
|
|
102
|
+
const m = /\b(358391)\b/.exec(out);
|
|
103
|
+
return m ? +m[1] : -1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function siegeRounds() {
|
|
107
|
+
// Every mutation is tagged with the round that caught the bug it reintroduces.
|
|
108
|
+
const ids = execFileSync(process.execPath, ["-e",
|
|
109
|
+
`import("${join(PKG, "src", "mutations.mjs")}").then(m => console.log(m.MUTATIONS.map(x => x.id).join("\\n")))`],
|
|
110
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 120000 });
|
|
111
|
+
const rounds = new Set();
|
|
112
|
+
for (const m of ids.matchAll(/M-R(\d+)-/g)) rounds.add(+m[1]);
|
|
113
|
+
return Math.max(...rounds);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A LIVE MOUNT THAT AUTHORISES.
|
|
118
|
+
*
|
|
119
|
+
* Round 5 taught this the expensive way: an attack whose input dies on validation before it
|
|
120
|
+
* reaches the guard it claims to test passes for the wrong reason and proves nothing. The
|
|
121
|
+
* first version of L10 did exactly that — it refused with `anchor-not-provable`, never
|
|
122
|
+
* reaching the output-binding check it was written to prove. So every behavioural claim
|
|
123
|
+
* below runs against a world that AUTHORISES in the control, and each attack perturbs
|
|
124
|
+
* exactly one thing.
|
|
125
|
+
*/
|
|
126
|
+
let _mount = null;
|
|
127
|
+
async function mount() {
|
|
128
|
+
if (_mount) return _mount;
|
|
129
|
+
const T = await import(join(PKG, "src", "transparency.mjs"));
|
|
130
|
+
const P = await import(join(PKG, "src", "primacy.mjs"));
|
|
131
|
+
const OTS = await import(join(PKG, "src", "ots.mjs"));
|
|
132
|
+
const ME = await import(join(PKG, "src", "migration-envelope.mjs"));
|
|
133
|
+
const POL = await import(join(PKG, "src", "policy.mjs"));
|
|
134
|
+
const { schnorr } = await import("@noble/curves/secp256k1.js");
|
|
135
|
+
const { sha256 } = await import("@noble/hashes/sha2.js");
|
|
136
|
+
|
|
137
|
+
const HEIGHT = 800_000, EXPOSURE = 850_000, CUTOFF = 900_000;
|
|
138
|
+
const logkp = ml_dsa65.keygen(new Uint8Array(32).fill(0x9c));
|
|
139
|
+
const victim = ME.generateMigrationIdentity();
|
|
140
|
+
const vcert = ME.createMigrationCommitment(victim);
|
|
141
|
+
const noise = ME.createMigrationCommitment(ME.generateMigrationIdentity());
|
|
142
|
+
const E = (c) => ({ classicalPub: c.classicalPub, pqPublic: c.pqPublic, classicalSig: c.classicalSig, pqSig: c.pqSig });
|
|
143
|
+
const entries = [E(noise), E(vcert)];
|
|
144
|
+
const sth = T.signTreeHead({ secretKey: logkp.secretKey, publicKey: logkp.publicKey,
|
|
145
|
+
treeSize: entries.length, rootHash: T.merkleRoot(entries.map(P.entryLeaf)), timestampMs: 1_756_000_000_000 });
|
|
146
|
+
const hb = T.sthSignedBytes({ version: sth.version, logId: sth.logId, treeSize: sth.treeSize,
|
|
147
|
+
rootHash: T.fromHex(sth.rootHash), timestampMs: sth.timestampMs });
|
|
148
|
+
const d = OTS.otsSha256(hb);
|
|
149
|
+
const otsHex = T.toHex(OTS.encodeOtsFile({ majorVersion: OTS.OTS_MAJOR_VERSION, fileHashOp: "sha256",
|
|
150
|
+
fileDigest: d, timestamp: OTS.timestampWithAttestation(d, { kind: "bitcoin", height: HEIGHT }) }));
|
|
151
|
+
const att = OTS.evaluateOts(OTS.decodeOtsFile(T.fromHex(otsHex))).blockAttestations[0];
|
|
152
|
+
const headers = { [HEIGHT]: att.merkleRootInternalHex.match(/../g).reverse().join("") };
|
|
153
|
+
const policyKey = new Uint8Array(32).fill(0x2b);
|
|
154
|
+
const digest = sha256(new TextEncoder().encode("mount: victim spend"));
|
|
155
|
+
const base = () => ({ cert: vcert, spendDigest: digest, pqSignatureHex: ME.authorizeSpend(victim, digest),
|
|
156
|
+
anchorEvidence: { sth, entries, otsHex }, expectedLogId: sth.logId, policyKey,
|
|
157
|
+
cutoffBlockHeight: CUTOFF, blockMerkleRoots: headers });
|
|
158
|
+
_mount = { POL, P, base, EXPOSURE, HEIGHT, CUTOFF, policyKey, schnorr, vcert };
|
|
159
|
+
return _mount;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const WORD_NUM = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10 };
|
|
163
|
+
|
|
164
|
+
// ── the registry ───────────────────────────────────────────────────────────
|
|
165
|
+
export const LETTER_CLAIMS = [
|
|
166
|
+
{
|
|
167
|
+
id: "L1-selftest-count",
|
|
168
|
+
what: "the number of checks the letter says the suite runs",
|
|
169
|
+
extract: (t) => extract(t, /npx fractal-pqc selftest\s+# (\d+) checks, no mocks/, "checks"),
|
|
170
|
+
measure: () => selftestTotal().total,
|
|
171
|
+
compare: (a, m) => num(a) === m,
|
|
172
|
+
falsify: (a) => String(num(a) + 1),
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "L2-selftest-zero-failures",
|
|
176
|
+
what: "'no mocks' is worthless if the suite does not actually pass",
|
|
177
|
+
extract: (t) => extract(t, /npx fractal-pqc selftest\s+# \d+ checks, (no mocks)/, "no mocks"),
|
|
178
|
+
measure: () => selftestTotal().failed,
|
|
179
|
+
compare: (_a, m) => m === 0,
|
|
180
|
+
falsify: null, // structural: cannot be perturbed from the letter
|
|
181
|
+
structuralNote: "measured directly; the letter cannot assert a failure count",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "L3-package-version",
|
|
185
|
+
what: "the exact version the letter tells the reader to install",
|
|
186
|
+
extract: (t) => extract(t, /npm i fractal-pqc@([0-9]+\.[0-9]+\.[0-9]+)/, "version"),
|
|
187
|
+
measure: () => JSON.parse(readFileSync(join(PKG, "package.json"), "utf8")).version,
|
|
188
|
+
compare: (a, m) => a === m,
|
|
189
|
+
falsify: (a) => a.replace(/\d+$/, (n) => String(+n + 1)),
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: "L4-ots-reference-block",
|
|
193
|
+
what: "the Bitcoin block in which the OpenTimestamps reference proof is confirmed",
|
|
194
|
+
extract: (t) => extract(t, /confirmed in Bitcoin block \*\*(\d+)\*\*/, "height"),
|
|
195
|
+
measure: () => otsReferenceHeight(),
|
|
196
|
+
compare: (a, m) => num(a) === m,
|
|
197
|
+
falsify: (a) => String(num(a) + 1),
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: "L5-bip341-scriptpubkey",
|
|
201
|
+
what: "official BIP-341 scriptPubKey cases asserted byte for byte",
|
|
202
|
+
extract: (t) => extract(t, /(\d+)\/\d+ scriptPubKey cases/, "cases"),
|
|
203
|
+
measure: () => bip341Counts().scriptPubKey,
|
|
204
|
+
compare: (a, m) => num(a) === m,
|
|
205
|
+
falsify: (a) => String(num(a) + 1),
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: "L6-bip341-control-blocks",
|
|
209
|
+
what: "official BIP-341 control blocks reproduced byte for byte",
|
|
210
|
+
extract: (t) => extract(t, /(\d+)\/\d+ control blocks/, "blocks"),
|
|
211
|
+
measure: () => bip341Counts().controlBlocks,
|
|
212
|
+
compare: (a, m) => num(a) === m,
|
|
213
|
+
falsify: (a) => String(num(a) + 1),
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: "L7-cc0-vector-count",
|
|
217
|
+
what: "the number of CC0 conformance vectors the letter says are published",
|
|
218
|
+
extract: (t) => extract(t, /\*\*Vectors stay CC0\.\*\* (\d+) of them are already published/, "vectors"),
|
|
219
|
+
measure: () => vectorCount(),
|
|
220
|
+
compare: (a, m) => num(a) === m,
|
|
221
|
+
falsify: (a) => String(num(a) + 1),
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: "L8-siege-rounds",
|
|
225
|
+
what: "how many adversarial rounds the letter claims, vs how many the mutation matrix records",
|
|
226
|
+
extract: (t) => extract(t, /(Seven|Six|Eight|Five|Nine) adversarial siege rounds/, "rounds"),
|
|
227
|
+
measure: () => siegeRounds(),
|
|
228
|
+
compare: (a, m) => WORD_NUM[String(a).toLowerCase()] === m,
|
|
229
|
+
falsify: (a) => (String(a).toLowerCase() === "seven" ? "Eight" : "Seven"),
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: "L9-tranche-arithmetic",
|
|
233
|
+
what: "the three tranches must add to the headline number",
|
|
234
|
+
extract: (t) => extract(t, /On the \$(\d{3})k/, "total-k"),
|
|
235
|
+
// `measure` receives the text OF THE LETTER BEING VERIFIED. The first version read
|
|
236
|
+
// SHIPPED_LETTER here — so when you pointed the tool at any other file it measured the
|
|
237
|
+
// packaged letter instead, and a tampered tranche sailed through green. Our own
|
|
238
|
+
// adversarial pass changed Tranche 3 from $120,000 to $220,000 and this returned OK.
|
|
239
|
+
// It is the same disease one layer up: the checker was reading a different document
|
|
240
|
+
// from the one it claimed to be checking. And the figure it was getting wrong is the
|
|
241
|
+
// ask itself.
|
|
242
|
+
measure: (t) => {
|
|
243
|
+
// ONLY the tranche headings. An earlier version also added the figure inside the
|
|
244
|
+
// milestone table, which is the SAME money stated twice — it measured 440,000 and
|
|
245
|
+
// called honest prose a lie.
|
|
246
|
+
const parts = [...t.matchAll(/\*\*Tranche \d+ — \$([\d,]+)/g)].map((m) => num(m[1]));
|
|
247
|
+
return parts.length >= 3 ? parts.reduce((a, b) => a + b, 0) : -1;
|
|
248
|
+
},
|
|
249
|
+
compare: (a, m) => num(a) * 1000 === m,
|
|
250
|
+
falsify: (a) => String(num(a) + 1),
|
|
251
|
+
},
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
// ── behavioural claims: sentences that assert the ENGINE does something ─────
|
|
255
|
+
export const BEHAVIOURAL_CLAIMS = [
|
|
256
|
+
{
|
|
257
|
+
id: "L10-refuses-unbound-spend",
|
|
258
|
+
quote: "a request that does not say which coin it is for is refused",
|
|
259
|
+
run: async () => {
|
|
260
|
+
const { POL, base } = await mount();
|
|
261
|
+
const control = POL.authorizeAndSign({ ...base(), singlePolicyKeyPerHolder: true });
|
|
262
|
+
if (control.authorized !== true) return { pass: false, detail: `el CONTROL no autoriza (${control.reason}) — el ataque seria vacuo` };
|
|
263
|
+
const { singlePolicyKeyPerHolder: _d, ...silent } = { ...base(), singlePolicyKeyPerHolder: true };
|
|
264
|
+
const r = POL.authorizeAndSign(silent);
|
|
265
|
+
const pass = r.authorized === false && r.policySignatureHex === null &&
|
|
266
|
+
r.reason === "spend-not-bound-to-this-holder-output";
|
|
267
|
+
return { pass, detail: `control autoriza ✓ · silencio → ${r.reason}` };
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: "L11-rejects-injected-conclusion",
|
|
272
|
+
quote: "Supplied means enforced — a parameter that can be passed and silently dropped is worse than one that does not exist",
|
|
273
|
+
run: async () => {
|
|
274
|
+
const { POL, base, EXPOSURE } = await mount();
|
|
275
|
+
const control = POL.authorizeAndSign({ ...base(), singlePolicyKeyPerHolder: true, exposureHeight: EXPOSURE });
|
|
276
|
+
if (control.authorized !== true) return { pass: false, detail: `el CONTROL no autoriza (${control.reason})` };
|
|
277
|
+
if (control.evidence?.exposureRelative !== true) return { pass: false, detail: "el gate no emitio la prueba de exposicion" };
|
|
278
|
+
const late = POL.authorizeAndSign({ ...base(), singlePolicyKeyPerHolder: true, exposureHeight: 1 });
|
|
279
|
+
const inj = POL.authorizeAndSign({ ...base(), singlePolicyKeyPerHolder: true,
|
|
280
|
+
_exposureProof: { exposureRelative: true, anchoredAtHeight: 1, exposureHeight: 9_999_999 } });
|
|
281
|
+
const pass = late.authorized === false && inj.authorized === false && inj.reason === "request-malformed";
|
|
282
|
+
return { pass, detail: `control autoriza ✓ · anclado-tras-exposicion → ${late.reason} · conclusion inyectada → ${inj.reason}` };
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: "L12-mldsa65-parameters",
|
|
287
|
+
quote: "ML-DSA-65 (FIPS-204, NIST Level 3)",
|
|
288
|
+
run: async () => {
|
|
289
|
+
const k = ml_dsa65.keygen(new Uint8Array(32).fill(1));
|
|
290
|
+
const ok = k.publicKey.length === 1952 && k.secretKey.length === 4032;
|
|
291
|
+
return { pass: ok, detail: `pk=${k.publicKey.length} sk=${k.secretKey.length} (FIPS-204 ML-DSA-65 = 1952/4032)` };
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: "L13-four-limits-are-in-the-package",
|
|
296
|
+
quote: "The four limits no design of this shape can close — and we ship them in the package",
|
|
297
|
+
run: async () => {
|
|
298
|
+
const pol = await import(join(PKG, "src", "policy.mjs"));
|
|
299
|
+
const pri = await import(join(PKG, "src", "primacy.mjs"));
|
|
300
|
+
const blob = JSON.stringify(pol.POLICY_SCOPE) + JSON.stringify(pri.EXPOSURE_SCOPE) + JSON.stringify(pri.PRIMACY_SCOPE || {});
|
|
301
|
+
const found = {
|
|
302
|
+
"1 last signature is classical": /Consensus checks a classical Schnorr|not immunity/i.test(blob),
|
|
303
|
+
"2 the cutoff is unverifiable": /no objective Q-day|We will not pick it/i.test(blob),
|
|
304
|
+
"3 no later than, never no earlier": /no earlier|adoption/i.test(blob),
|
|
305
|
+
"4 identity does not pin history": /does not pin its HISTORY|limitFour/i.test(blob),
|
|
306
|
+
};
|
|
307
|
+
const missing = Object.entries(found).filter(([, v]) => !v).map(([k]) => k);
|
|
308
|
+
return { pass: missing.length === 0, detail: missing.length ? `AUSENTES: ${missing.join(", ")}` : "los cuatro presentes" };
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: "L14-gate-is-conjunction-not-substitution",
|
|
313
|
+
quote: "anchored at 800,000 with exposure at 850,000",
|
|
314
|
+
run: async () => {
|
|
315
|
+
const pri = await import(join(PKG, "src", "primacy.mjs"));
|
|
316
|
+
const s = JSON.stringify(pri.EXPOSURE_SCOPE);
|
|
317
|
+
const declared = /narrowsNeverRescues/.test(s) && /800000/.test(s) && /850000/.test(s) && /790000/.test(s);
|
|
318
|
+
return { pass: declared, detail: declared
|
|
319
|
+
? "el paquete declara la MISMA conjunción con las mismas alturas"
|
|
320
|
+
: "el paquete no declara la conjunción con esas cifras" };
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* COVERAGE: which numbers in the letter is nothing checking?
|
|
327
|
+
*
|
|
328
|
+
* Our own adversarial pass wrote "confirmed in Bitcoin block **358391** (and also block
|
|
329
|
+
* **999999**)" and the tool went green — not because it read the wrong value, but because
|
|
330
|
+
* the false number matched no registered pattern, so NOTHING LOOKED AT IT. That is the same
|
|
331
|
+
* hole `claims --gaps` exists for one layer down: the ledger governs what it names, and what
|
|
332
|
+
* it does not name is invisible.
|
|
333
|
+
*
|
|
334
|
+
* A tool that silently checks 14 of 40 numbers and prints "14/14 verified" is worse than no
|
|
335
|
+
* tool, because the green is read as coverage. So we enumerate every number-bearing
|
|
336
|
+
* assertion in the letter, subtract the ones a registered claim consumes, and PRINT THE
|
|
337
|
+
* REMAINDER. It does not make them true. It makes them visible.
|
|
338
|
+
*/
|
|
339
|
+
export function uncoveredFigures(text) {
|
|
340
|
+
const covered = new Set();
|
|
341
|
+
for (const c of LETTER_CLAIMS) {
|
|
342
|
+
const ex = c.extract(text);
|
|
343
|
+
if (!ex.missing && !ex.ambiguous) covered.add(String(ex.value).replace(/[, ]/g, ""));
|
|
344
|
+
}
|
|
345
|
+
const out = [];
|
|
346
|
+
const lines = text.split("\n");
|
|
347
|
+
for (let i = 0; i < lines.length; i++) {
|
|
348
|
+
const ln = lines[i];
|
|
349
|
+
if (/^\s*(npm i|npx |--|\||```)/.test(ln)) continue; // comandos y tablas: ruido
|
|
350
|
+
for (const m of ln.matchAll(/\b(\d[\d,]{2,})\b/g)) {
|
|
351
|
+
const raw = m[1].replace(/[, ]/g, "");
|
|
352
|
+
if (covered.has(raw)) continue;
|
|
353
|
+
if (/^(2026|2025|902025722|1)$/.test(raw)) continue; // fechas y el NIT
|
|
354
|
+
// Nombres de estandares: FIPS-204, RFC 6962, BIP-341/340. Son identificadores, no
|
|
355
|
+
// afirmaciones cuantitativas. Filtrarlos NO es esconderlos: una lista con ruido se
|
|
356
|
+
// deja de leer, y una lista que se deja de leer es una lista que no existe.
|
|
357
|
+
const before = ln.slice(Math.max(0, m.index - 6), m.index);
|
|
358
|
+
if (/(FIPS-|RFC |BIP-|SHA-|SHA3-|P2|secp)$/i.test(before)) continue;
|
|
359
|
+
const ctx = ln.trim().slice(Math.max(0, m.index - 46), m.index + 54).replace(/\s+/g, " ");
|
|
360
|
+
// Algunas cifras las consume un claim de forma INDIRECTA (la aritmetica de tramos,
|
|
361
|
+
// o una frase conductual). Se anota, en vez de callarlo o de contarlo como cubierto.
|
|
362
|
+
const indirect =
|
|
363
|
+
/Tranche \d+ — \$/.test(ln) ? "sumada por L9 (aritmetica de tramos)"
|
|
364
|
+
: /anchored at 800,000|exposure at 850,000|cutoff of 790,000/.test(ln) ? "afirmada por L14 (la conjuncion)"
|
|
365
|
+
: null;
|
|
366
|
+
out.push({ value: m[1], line: i + 1, context: "…" + ctx + "…", indirect });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export const LETTER_SCOPE = Object.freeze({
|
|
373
|
+
rule: "no factual assertion in the letter may be stated without an executable check that " +
|
|
374
|
+
"reads the ASSERTED value from the letter itself and compares it to a value measured " +
|
|
375
|
+
"from this package.",
|
|
376
|
+
whyExtractionMatters:
|
|
377
|
+
"hardcoding the number here would let the letter and the checker drift apart in exactly " +
|
|
378
|
+
"the way seven sieges found the prose and the code drifting. The value is read from the " +
|
|
379
|
+
"letter, so editing the letter changes what is checked.",
|
|
380
|
+
vacuity:
|
|
381
|
+
"every extracted claim carries `falsify`: a perturbation of the asserted value under " +
|
|
382
|
+
"which the check MUST fail. A claim that survives its own falsification is measuring " +
|
|
383
|
+
"nothing and is reported as VACUOUS.",
|
|
384
|
+
doesNotProve:
|
|
385
|
+
"that the letter is TRUE. It proves that every assertion we knew how to make checkable " +
|
|
386
|
+
"IS checked, and it lists what is not covered. Judgements — 'we are not the standard', " +
|
|
387
|
+
"'this is the only package we know of' — are opinions and are marked as such, never " +
|
|
388
|
+
"reported as verified.",
|
|
389
|
+
coverageIsNotTruth:
|
|
390
|
+
"14/14 means every REGISTERED assertion matched a measurement. It does not mean the " +
|
|
391
|
+
"letter contains no other number. `uncoveredFigures` lists the figures no claim " +
|
|
392
|
+
"consumes, because a green that is read as coverage is how a checker starts lying for " +
|
|
393
|
+
"you. Our own adversarial pass put a false block height next to a true one and this " +
|
|
394
|
+
"tool went green — not by reading the wrong value, but by never looking at it.",
|
|
395
|
+
notCovered: [
|
|
396
|
+
"'Ours is the only package I know of…' — a claim about the world, unfalsifiable by us",
|
|
397
|
+
"'We are not the standard' — a judgement",
|
|
398
|
+
"'roughly 900 executed exploits' — an aggregate we count by hand across seven rounds",
|
|
399
|
+
"'263 assertions passed' in round 5 — a historical fact about a state of the code that no " +
|
|
400
|
+
"longer exists. It cannot be re-measured today, so it is declared rather than checked.",
|
|
401
|
+
"every forward-looking commitment (tranches, timelines, the failure clause)",
|
|
402
|
+
],
|
|
403
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify-letter.mjs — run every checkable assertion in the letter against this package.
|
|
3
|
+
*
|
|
4
|
+
* Exit code is the product: 0 only when every extracted assertion matched a measurement,
|
|
5
|
+
* every behavioural sentence held, and no claim survived its own falsification.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { LETTER_CLAIMS, BEHAVIOURAL_CLAIMS, LETTER_SCOPE, SHIPPED_LETTER, uncoveredFigures } from "./letter-claims.mjs";
|
|
9
|
+
|
|
10
|
+
export async function verifyLetter({ path = SHIPPED_LETTER, json = false, quiet = false } = {}) {
|
|
11
|
+
const text = readFileSync(path, "utf8");
|
|
12
|
+
const rows = [];
|
|
13
|
+
|
|
14
|
+
for (const c of LETTER_CLAIMS) {
|
|
15
|
+
const ex = c.extract(text);
|
|
16
|
+
if (ex.missing) {
|
|
17
|
+
rows.push({ id: c.id, verdict: "MISSING", what: c.what,
|
|
18
|
+
detail: `the letter no longer contains an assertion for "${ex.label}" — an assertion that ` +
|
|
19
|
+
`disappears is a failure, never a pass` });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (ex.ambiguous) {
|
|
23
|
+
rows.push({ id: c.id, verdict: "AMBIGUOUS", what: c.what,
|
|
24
|
+
detail: `the letter states "${ex.label}" more than once with different values ` +
|
|
25
|
+
`(${ex.values.join(", ")}). Verifying the first and ignoring the rest would ` +
|
|
26
|
+
`green-light the false one sitting next to the true one.` });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
let measured, err = null;
|
|
30
|
+
try { measured = c.measure(text); } catch (e) { err = e.message; if (e.recursion) err = e.message; }
|
|
31
|
+
if (err) {
|
|
32
|
+
// A claim that CANNOT run here (it measures the suite, and we are inside the suite)
|
|
33
|
+
// is SKIPPED, never failed and never quietly passed. Same rule as everywhere else:
|
|
34
|
+
// the gap is reported, not absorbed.
|
|
35
|
+
const recursion = /re-enter selftest/.test(err);
|
|
36
|
+
rows.push({ id: c.id, verdict: recursion ? "SKIPPED" : "ERROR", what: c.what,
|
|
37
|
+
detail: recursion ? `${err} — run \`verify-letter\` outside selftest to check this one` : err });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ok = c.compare(ex.value, measured);
|
|
42
|
+
// VACUITY: perturb the ASSERTED value; the check must then fail.
|
|
43
|
+
let vacuous = false;
|
|
44
|
+
if (ok && typeof c.falsify === "function") {
|
|
45
|
+
vacuous = c.compare(c.falsify(ex.value), measured) === true;
|
|
46
|
+
}
|
|
47
|
+
rows.push({
|
|
48
|
+
id: c.id, what: c.what,
|
|
49
|
+
asserted: String(ex.value), measured: String(measured),
|
|
50
|
+
verdict: !ok ? "FAILED" : vacuous ? "VACUOUS" : "VERIFIED",
|
|
51
|
+
...(c.structuralNote ? { note: c.structuralNote } : {}),
|
|
52
|
+
...(vacuous ? { detail: "changing the asserted value still passes — this check measures nothing" } : {}),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const b of BEHAVIOURAL_CLAIMS) {
|
|
57
|
+
let r;
|
|
58
|
+
try { r = await b.run(); } catch (e) { r = { pass: false, detail: `threw: ${e.message}` }; }
|
|
59
|
+
// A behavioural sentence must also be present in the letter, or we are testing prose
|
|
60
|
+
// that nobody is reading. Match on NORMALISED text: the letter is markdown, so
|
|
61
|
+
// "Supplied means **enforced**" and "Supplied means enforced" are the same sentence,
|
|
62
|
+
// and a checker that cannot see that reports a live sentence as missing — which is a
|
|
63
|
+
// false accusation against honest prose, the mirror image of the bug it is hunting.
|
|
64
|
+
const norm = (x) => x.replace(/[*_`]/g, "").replace(/[…]/g, "").replace(/\s+/g, " ").trim();
|
|
65
|
+
const needle = norm(b.quote).split(" ").slice(0, 8).join(" ");
|
|
66
|
+
const present = norm(text).includes(needle);
|
|
67
|
+
rows.push({ id: b.id, what: b.quote,
|
|
68
|
+
verdict: !present ? "MISSING" : r.pass ? "VERIFIED" : "FAILED",
|
|
69
|
+
detail: !present ? "this sentence is no longer in the letter" : r.detail });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const bad = rows.filter((r) => r.verdict !== "VERIFIED" && r.verdict !== "SKIPPED");
|
|
73
|
+
const skipped = rows.filter((r) => r.verdict === "SKIPPED").length;
|
|
74
|
+
const out = {
|
|
75
|
+
letter: path,
|
|
76
|
+
lettersha256: null,
|
|
77
|
+
total: rows.length,
|
|
78
|
+
verified: rows.length - bad.length - skipped,
|
|
79
|
+
skipped,
|
|
80
|
+
rows,
|
|
81
|
+
notCovered: LETTER_SCOPE.notCovered,
|
|
82
|
+
uncoveredFigures: uncoveredFigures(text),
|
|
83
|
+
verdict: bad.length === 0
|
|
84
|
+
? "Every checkable assertion in this letter matched a measurement taken from this package."
|
|
85
|
+
: `${bad.length} assertion(s) did NOT verify. The letter is wrong, the code is wrong, or both.`,
|
|
86
|
+
};
|
|
87
|
+
if (json) { if (!quiet) console.log(JSON.stringify(out, null, 2)); return bad.length === 0 ? 0 : 1; }
|
|
88
|
+
|
|
89
|
+
const W = 34;
|
|
90
|
+
console.log(`\nEvery factual assertion in ${path.split("/").slice(-1)[0]}, checked against this package.\n`);
|
|
91
|
+
for (const r of rows) {
|
|
92
|
+
const mark = { VERIFIED: "✓", FAILED: "✗", VACUOUS: "∅", MISSING: "?", AMBIGUOUS: "≠", SKIPPED: "–", ERROR: "!" }[r.verdict];
|
|
93
|
+
console.log(` ${mark} [${r.id}] ${r.what}`);
|
|
94
|
+
if (r.asserted !== undefined)
|
|
95
|
+
console.log(` letter says: ${String(r.asserted).padEnd(W - 12)} measured here: ${r.measured}`);
|
|
96
|
+
if (r.detail) console.log(` ${r.detail}`);
|
|
97
|
+
if (r.note) console.log(` (${r.note})`);
|
|
98
|
+
}
|
|
99
|
+
console.log(`\n ${out.verified}/${out.total - skipped} verified${skipped ? `, ${skipped} skipped inside selftest` : ""}.\n ${out.verdict}\n`);
|
|
100
|
+
console.log(" NOT covered by any check above, and said out loud rather than implied:");
|
|
101
|
+
for (const n of LETTER_SCOPE.notCovered) console.log(` · ${n}`);
|
|
102
|
+
const un = uncoveredFigures(text);
|
|
103
|
+
console.log(`\n And ${un.length} figure(s) in this letter that NO claim consumes — listed, not hidden,`);
|
|
104
|
+
console.log(" because a green read as coverage is how a checker starts lying for you:");
|
|
105
|
+
for (const u of un.slice(0, 12))
|
|
106
|
+
console.log(` · ${String(u.value).padEnd(9)} line ${String(u.line).padEnd(4)}` +
|
|
107
|
+
(u.indirect ? ` [${u.indirect}]` : " [UNCHECKED]") + ` ${u.context}`);
|
|
108
|
+
if (un.length > 12) console.log(` · …and ${un.length - 12} more`);
|
|
109
|
+
console.log();
|
|
110
|
+
return bad.length === 0 ? 0 : 1;
|
|
111
|
+
}
|
package/test/claims.mjs
CHANGED
|
@@ -25,8 +25,12 @@ check("a proof that throws does not pass silently",
|
|
|
25
25
|
runClaims([{ id: "x", statement: "s", module: "m", proof: () => { throw new Error("boom"); }, attack: () => true }]).ok === false);
|
|
26
26
|
check("HONESTY: the method states it does NOT prove completeness",
|
|
27
27
|
CLAIMS_METHOD.whatItDoesNotProve.includes("completeness"));
|
|
28
|
+
// R[1-6] hardcoded a fact about the world — "there are at most six rounds" — into a test.
|
|
29
|
+
// Round 7 arrived and the test declared its claims invalid. A regex is not the place to
|
|
30
|
+
// freeze how many times you have been wrong. Caught by `verify-letter` on its first run,
|
|
31
|
+
// which is the entire reason that tool exists.
|
|
28
32
|
check("every claim names the siege round that taught us it needed guarding",
|
|
29
|
-
CLAIMS.every((c) => /^R
|
|
33
|
+
CLAIMS.every((c) => /^R\d+-/.test(c.id)));
|
|
30
34
|
|
|
31
35
|
/* ── the third admissibility condition ─────────────────────────────────── */
|
|
32
36
|
import { MUTATIONS, MUTATION_METHOD } from "../src/mutations.mjs";
|
|
@@ -44,7 +48,7 @@ console.log("\nMutation coverage (cheap structural check; run `npm run mutate` f
|
|
|
44
48
|
check("every mutation names a real claim",
|
|
45
49
|
MUTATIONS.every((m) => m.mustKill.every((id) => CLAIMS.some((c) => c.id === id))));
|
|
46
50
|
check("every mutation reintroduces a bug from a named siege round",
|
|
47
|
-
MUTATIONS.every((m) => /^M-R
|
|
51
|
+
MUTATIONS.every((m) => /^M-R\d+-/.test(m.id) && typeof m.describes === "string"));
|
|
48
52
|
check("HONESTY: the method states a surviving mutation can mean defence in depth, not a defect",
|
|
49
53
|
MUTATION_METHOD.whatItDoesNotProve.includes("defence in depth"));
|
|
50
54
|
check("HONESTY: and that it does not prove completeness either",
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The letter's own claims, held to the same admissibility rule as the code's.
|
|
3
|
+
* A checker nobody checks is the disease with an extra layer.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { LETTER_CLAIMS, BEHAVIOURAL_CLAIMS, LETTER_SCOPE, SHIPPED_LETTER } from "../src/letter-claims.mjs";
|
|
7
|
+
import { verifyLetter } from "../src/verify-letter.mjs";
|
|
8
|
+
|
|
9
|
+
let pass = 0, fail = 0;
|
|
10
|
+
const check = (n, c) => { if (c) { pass++; console.log(" ✓ " + n); } else { fail++; console.log(" ✗ " + n); } };
|
|
11
|
+
|
|
12
|
+
console.log("\nThe letter verifies itself:");
|
|
13
|
+
const text = readFileSync(SHIPPED_LETTER, "utf8");
|
|
14
|
+
|
|
15
|
+
check("the letter we sent ships inside the package", text.length > 5000 && /^Subject: Re: FractalAI/.test(text));
|
|
16
|
+
|
|
17
|
+
check("every extracted claim FINDS its assertion in the letter",
|
|
18
|
+
LETTER_CLAIMS.every((c) => !c.extract(text).missing));
|
|
19
|
+
|
|
20
|
+
check("★ every extracted claim can be FALSIFIED — a check that cannot fail measures nothing",
|
|
21
|
+
LETTER_CLAIMS.every((c) => typeof c.falsify === "function" || typeof c.structuralNote === "string"));
|
|
22
|
+
|
|
23
|
+
const skipped = [];
|
|
24
|
+
check("no claim survives its own falsification (vacuity gate)",
|
|
25
|
+
LETTER_CLAIMS.every((c) => {
|
|
26
|
+
if (typeof c.falsify !== "function") return true;
|
|
27
|
+
const ex = c.extract(text);
|
|
28
|
+
if (ex.missing) return false;
|
|
29
|
+
let m;
|
|
30
|
+
try { m = c.measure(text); }
|
|
31
|
+
catch (e) {
|
|
32
|
+
// A skip is REPORTED, never counted as a pass. Inside selftest, the claims that
|
|
33
|
+
// measure selftest itself cannot run; `npx fractal-pqc verify-letter` runs them all.
|
|
34
|
+
if (e.recursion) { skipped.push(c.id); return true; }
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return c.compare(ex.value, m) === true && c.compare(c.falsify(ex.value), m) === false;
|
|
38
|
+
}));
|
|
39
|
+
if (skipped.length) console.log(` (skipped inside selftest, they measure selftest: ${skipped.join(", ")} — run \`verify-letter\` for those)`);
|
|
40
|
+
|
|
41
|
+
check("every behavioural sentence checked is actually PRESENT in the letter",
|
|
42
|
+
BEHAVIOURAL_CLAIMS.every((b) => {
|
|
43
|
+
const norm = (x) => x.replace(/[*_`]/g, "").replace(/[…]/g, "").replace(/\s+/g, " ").trim();
|
|
44
|
+
return norm(text).includes(norm(b.quote).split(" ").slice(0, 7).join(" "));
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
check("HONESTY: the scope names what it does NOT prove",
|
|
48
|
+
LETTER_SCOPE.doesNotProve.includes("TRUE") && LETTER_SCOPE.notCovered.length >= 3);
|
|
49
|
+
|
|
50
|
+
check("HONESTY: it explains why the value is EXTRACTED and never hardcoded",
|
|
51
|
+
LETTER_SCOPE.whyExtractionMatters.includes("drift"));
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
/* ── ADVERSARIAL: the checker must REFUSE a tampered letter ────────────────
|
|
55
|
+
* A verifier nobody attacks is a verifier that will pass a lie the first time
|
|
56
|
+
* one arrives. Each perturbation below changed exactly one thing in the letter
|
|
57
|
+
* and MUST be refused. Two of them were accepted when first written, and both
|
|
58
|
+
* were the same disease: L9 read the SHIPPED letter while claiming to check the
|
|
59
|
+
* file it was given (a tampered tranche total sailed through — the ask itself),
|
|
60
|
+
* and the extractor took the FIRST regex match, so a false value sitting beside
|
|
61
|
+
* a true one was never looked at.
|
|
62
|
+
*/
|
|
63
|
+
import { writeFileSync, mkdtempSync } from "node:fs";
|
|
64
|
+
import { tmpdir } from "node:os";
|
|
65
|
+
import { join as pjoin } from "node:path";
|
|
66
|
+
|
|
67
|
+
console.log("\nAdversarial: a tampered letter must be refused");
|
|
68
|
+
{
|
|
69
|
+
const dir = mkdtempSync(pjoin(tmpdir(), "letter-adv-"));
|
|
70
|
+
const TAMPERS = [
|
|
71
|
+
["inflated test count", (t) => t.replace(/(\d+) checks, no mocks/, "999 checks, no mocks")],
|
|
72
|
+
["deleted assertion", (t) => t.replace(/npx fractal-pqc selftest\s+# \d+ checks, no mocks/, "")],
|
|
73
|
+
["inflated CC0 vectors", (t) => t.replace(/(\d+) of them are already published/, "999 of them are already published")],
|
|
74
|
+
["unicode homoglyph digits", (t) => t.replace(/(\d+) checks, no mocks/, "٣١٣ checks, no mocks")],
|
|
75
|
+
["false value beside a true", (t) => t.replace(/confirmed in Bitcoin block \*\*(\d+)\*\*/,
|
|
76
|
+
"confirmed in Bitcoin block **$1**, and confirmed in Bitcoin block **999999**")],
|
|
77
|
+
["inflated siege rounds", (t) => t.replace(/Seven adversarial/, "Twelve adversarial")],
|
|
78
|
+
["wrong version", (t) => t.replace(/fractal-pqc@[\d.]+/g, "fractal-pqc@9.9.9")],
|
|
79
|
+
["inflated BIP-341 cases", (t) => t.replace(/7\/7 scriptPubKey/, "9/9 scriptPubKey")],
|
|
80
|
+
["inflated control blocks", (t) => t.replace(/12\/12 control blocks/, "20/20 control blocks")],
|
|
81
|
+
["★ tranche total broken", (t) => t.replace(/\*\*Tranche 3 — \$120,000/, "**Tranche 3 — $220,000")],
|
|
82
|
+
["empty file", () => ""],
|
|
83
|
+
];
|
|
84
|
+
// Inside selftest, the claim that MEASURES selftest is skipped, so the tamper that only
|
|
85
|
+
// that claim would catch is not detectable here. Say so and count it separately — a
|
|
86
|
+
// tamper we cannot test is not a tamper we refused.
|
|
87
|
+
const insideSelftest = Boolean(process.env.FRACTAL_SELFTEST_DEPTH);
|
|
88
|
+
const undetectableHere = insideSelftest ? ["inflated test count", "unicode homoglyph digits"] : [];
|
|
89
|
+
let refused = 0, skippedTampers = 0;
|
|
90
|
+
for (const [name, f] of TAMPERS) {
|
|
91
|
+
if (undetectableHere.includes(name)) { skippedTampers++; continue; }
|
|
92
|
+
const p = pjoin(dir, name.replace(/\W+/g, "_") + ".md");
|
|
93
|
+
writeFileSync(p, f(text));
|
|
94
|
+
const code = await verifyLetter({ path: p, json: true, quiet: true }).catch(() => 1);
|
|
95
|
+
if (code !== 0) refused++; else console.log(` ✗ ACCEPTED a tampered letter: ${name}`);
|
|
96
|
+
}
|
|
97
|
+
check(`★ all ${TAMPERS.length - skippedTampers} testable tampered letters refused`,
|
|
98
|
+
refused === TAMPERS.length - skippedTampers);
|
|
99
|
+
if (skippedTampers)
|
|
100
|
+
console.log(` (${skippedTampers} tamper(s) not testable inside selftest — they are only ` +
|
|
101
|
+
`caught by the claim that measures selftest. \`npx fractal-pqc verify-letter\` ` +
|
|
102
|
+
`catches all ${TAMPERS.length}.)`);
|
|
103
|
+
const good = await verifyLetter({ path: SHIPPED_LETTER, json: true, quiet: true });
|
|
104
|
+
check("and the untouched letter is still ACCEPTED (the control is not vacuous)", good === 0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(`\n${pass} passed, ${fail} failed\n`);
|
|
108
|
+
process.exit(fail === 0 ? 0 : 1);
|