fractal-pqc 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/cli.mjs +1 -1
- package/package.json +1 -1
- package/src/claims-registry.mjs +52 -0
- package/src/index.mjs +1 -1
- package/src/mutations.mjs +28 -0
- package/src/primacy.mjs +176 -0
- package/test/claims.mjs +2 -2
- package/test/primacy.mjs +40 -2
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ without invalidating already-signed history. This kit is a concrete, honest firs
|
|
|
31
31
|
> dual-signed commitment before the verifier's cutoff height cannot be rescued by any of
|
|
32
32
|
> this. `test/primacy.mjs` asserts that out loud rather than leaving it to be discovered.
|
|
33
33
|
|
|
34
|
-
Everything below is exercised by `npm test` with real keys — **
|
|
34
|
+
Everything below is exercised by `npm test` with real keys — **293 checks pass**:
|
|
35
35
|
|
|
36
36
|
- **secp256k1** commitment + spend authorization (`@noble/curves`).
|
|
37
37
|
- **Taproot BIP-340 Schnorr** sign/verify, asserted against the **official BIP-340 test
|
package/bin/cli.mjs
CHANGED
|
@@ -424,7 +424,7 @@ Usage:
|
|
|
424
424
|
code it names is broken. A claim no mutation can
|
|
425
425
|
kill is vacuous, and is reported as such.
|
|
426
426
|
fractal-pqc verify-vector Check the official BIP-340 test vector
|
|
427
|
-
fractal-pqc selftest Run everything:
|
|
427
|
+
fractal-pqc selftest Run everything: 293 real checks, no mocks
|
|
428
428
|
|
|
429
429
|
Docs: integrations/pqc-migration-kit/README.md`);
|
|
430
430
|
process.exit(cmd ? 1 : 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fractal-pqc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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",
|
package/src/claims-registry.mjs
CHANGED
|
@@ -178,6 +178,52 @@ export const CLAIMS = [
|
|
|
178
178
|
},
|
|
179
179
|
note: "R5: the most consequential guard in the module, and nothing named it.",
|
|
180
180
|
},
|
|
181
|
+
{
|
|
182
|
+
id: "R6-exposure-relative-primacy",
|
|
183
|
+
module: "primacy.mjs",
|
|
184
|
+
statement: "A commitment anchored in Bitcoin strictly BEFORE the block in which the " +
|
|
185
|
+
"CHAIN first revealed the holder's public key needs no Q-day estimate: at " +
|
|
186
|
+
"that moment breaking secp256k1 was not sufficient to produce it. Anchored " +
|
|
187
|
+
"after exposure — or with the exposure height merely ABSENT — the strong " +
|
|
188
|
+
"claim is refused, and the refusal says so in `valid`, not only in a new field.",
|
|
189
|
+
proof: () => {
|
|
190
|
+
const r = P.proveExposureRelativePrimacy({
|
|
191
|
+
subjectClassicalPub: cert.classicalPub, entries: ENTRIES, sth: STH,
|
|
192
|
+
expectedLogId: STH.logId, otsHex: OTS_HEX, blockMerkleRoots: HEADERS,
|
|
193
|
+
exposureHeight: ANCHOR_HEIGHT + 50_000,
|
|
194
|
+
});
|
|
195
|
+
return r.exposureRelative === true && r.strength === "anchored-before-exposure" &&
|
|
196
|
+
r.blocksBefore === 50_000;
|
|
197
|
+
},
|
|
198
|
+
attack: () => {
|
|
199
|
+
const base = { subjectClassicalPub: cert.classicalPub, entries: ENTRIES, sth: STH,
|
|
200
|
+
expectedLogId: STH.logId, otsHex: OTS_HEX, blockMerkleRoots: HEADERS };
|
|
201
|
+
// Exposed BEFORE the anchor: the strong claim must be refused, and it must say the
|
|
202
|
+
// coins should be moved rather than quietly degrading to the guessed cutoff.
|
|
203
|
+
const late = P.proveExposureRelativePrimacy({ ...base, exposureHeight: ANCHOR_HEIGHT - 1 });
|
|
204
|
+
if (late.exposureRelative !== false || late.reason !== "anchored-after-exposure") return false;
|
|
205
|
+
// ROUND 6: the refusal must be visible to a consumer that checks `.valid` — which is
|
|
206
|
+
// what every other gate in this package does. It used to come back green.
|
|
207
|
+
if (late.valid !== false || late.quantumPropertyHolds !== false) return false;
|
|
208
|
+
// ROUND 6: an ABSENT field is nobody's assertion. It used to yield "never-exposed",
|
|
209
|
+
// the strongest claim the module makes.
|
|
210
|
+
const omitted = P.proveExposureRelativePrimacy(base);
|
|
211
|
+
if (omitted.exposureRelative !== false || omitted.reason !== "exposure-height-not-supplied") return false;
|
|
212
|
+
// ROUND 6: the internal sentinel must never be accepted as a real exposure height.
|
|
213
|
+
if (P.proveExposureRelativePrimacy({ ...base, exposureHeight: Number.MAX_SAFE_INTEGER })
|
|
214
|
+
.exposureRelative !== false) return false;
|
|
215
|
+
// Exposed at EXACTLY the anchor height is not "before" — the boundary must be strict.
|
|
216
|
+
const same = P.proveExposureRelativePrimacy({ ...base, exposureHeight: ANCHOR_HEIGHT });
|
|
217
|
+
if (same.exposureRelative !== false) return false;
|
|
218
|
+
// A malformed height must refuse, never coerce.
|
|
219
|
+
if (P.proveExposureRelativePrimacy({ ...base, exposureHeight: NaN }).exposureRelative !== false) return false;
|
|
220
|
+
// And with no header-confirmed anchor there is no height to compare at all.
|
|
221
|
+
return P.proveExposureRelativePrimacy({ ...base, blockMerkleRoots: undefined,
|
|
222
|
+
exposureHeight: ANCHOR_HEIGHT + 50_000 }).exposureRelative === false;
|
|
223
|
+
},
|
|
224
|
+
note: "The founder's insight: what is at risk is the PAST. A global Q-day is a guess; " +
|
|
225
|
+
"a key's exposure block is a fact in Bitcoin.",
|
|
226
|
+
},
|
|
181
227
|
{
|
|
182
228
|
id: "R4-cutoff-never-coerced",
|
|
183
229
|
module: "primacy.mjs",
|
|
@@ -431,6 +477,12 @@ export const UNCLAIMED_GUARDS = Object.freeze([
|
|
|
431
477
|
"blocks) — again stronger than a claim, again not in the ledger" },
|
|
432
478
|
{ where: "policy.mjs", guard: "the ORDER of the refusal guards",
|
|
433
479
|
why: "each guard is claimed individually; nothing claims that reordering them is safe" },
|
|
480
|
+
{ where: "primacy.mjs", guard: "exposure-relative primacy is not an authorisation gate",
|
|
481
|
+
why: "policy.mjs does not consume it. It is a verifiable proof primitive; nothing in the " +
|
|
482
|
+
"signing path requires it, and we do not imply otherwise" },
|
|
483
|
+
{ where: "primacy.mjs", guard: "'never exposed' is the CALLER's assertion",
|
|
484
|
+
why: "the kit cannot verify non-exposure. It can only refuse when the field is absent, " +
|
|
485
|
+
"which it now does. Off-chain publication of a key is outside what we measure" },
|
|
434
486
|
{ where: "primacy.mjs", guard: "key rotation / revocation",
|
|
435
487
|
why: "NOT IMPLEMENTED. A holder who loses their ML-DSA secret is locked out permanently " +
|
|
436
488
|
"and the old certificate keeps authorising. This is a known gap, not a covered one." },
|
package/src/index.mjs
CHANGED
|
@@ -56,7 +56,7 @@ export {
|
|
|
56
56
|
parsePsbt, signPsbtTaprootKeyPath, finalizePsbt,
|
|
57
57
|
} from "./psbt.mjs";
|
|
58
58
|
|
|
59
|
-
export const VERSION = "0.
|
|
59
|
+
export const VERSION = "0.5.1";
|
|
60
60
|
|
|
61
61
|
// The security layer, actually exported. Round 2 of our own siege found that primacy,
|
|
62
62
|
// policy and transparency existed in the repo and were unreachable from the published
|
package/src/mutations.mjs
CHANGED
|
@@ -101,6 +101,34 @@ export const MUTATIONS = [
|
|
|
101
101
|
to: ` "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"`,
|
|
102
102
|
mustKill: ["R3-nums-required-for-enforcement"],
|
|
103
103
|
},
|
|
104
|
+
{
|
|
105
|
+
id: "M-R6-exposure-boundary-not-strict",
|
|
106
|
+
describes: "R6: anchoring AT or AFTER exposure is accepted as if it were before",
|
|
107
|
+
file: "src/primacy.mjs",
|
|
108
|
+
from: ` if (anchoredAt >= exposureHeight) {`,
|
|
109
|
+
to: ` if (false) {`,
|
|
110
|
+
mustKill: ["R6-exposure-relative-primacy"],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "M-R6-absent-height-is-assertion",
|
|
114
|
+
describes: "R6: an ABSENT exposure height is treated as the 'never exposed' assertion again",
|
|
115
|
+
file: "src/primacy.mjs",
|
|
116
|
+
from: ` if (!args || !Object.hasOwn(args, "exposureHeight")) {`,
|
|
117
|
+
to: ` if (false) {`,
|
|
118
|
+
mustKill: ["R6-exposure-relative-primacy"],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "M-R6-refusal-stays-green",
|
|
122
|
+
describes: "R6: a refused exposure result comes back valid:true, as it did before",
|
|
123
|
+
file: "src/primacy.mjs",
|
|
124
|
+
from: ` ...base, valid: false, quantumPropertyHolds: false,
|
|
125
|
+
exposureRelative: false, anchoredAtHeight: anchoredAt, exposureHeight,
|
|
126
|
+
reason: "anchored-after-exposure",`,
|
|
127
|
+
to: ` ...base,
|
|
128
|
+
exposureRelative: false, anchoredAtHeight: anchoredAt, exposureHeight,
|
|
129
|
+
reason: "anchored-after-exposure",`,
|
|
130
|
+
mustKill: ["R6-exposure-relative-primacy"],
|
|
131
|
+
},
|
|
104
132
|
{
|
|
105
133
|
id: "M-R4-cutoff-unvalidated",
|
|
106
134
|
describes: "R4: a malformed cutoff is coerced again — `height > NaN` passes silently",
|
package/src/primacy.mjs
CHANGED
|
@@ -500,3 +500,179 @@ export const PRIMACY_SCOPE = Object.freeze({
|
|
|
500
500
|
"a subject-indexed side tree with non-membership proofs, committed in the head, would " +
|
|
501
501
|
"make the enumeration O(log n). It is not implemented and is not claimed.",
|
|
502
502
|
});
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
/* ── EXPOSURE-RELATIVE PRIMACY ──────────────────────────────────────────── */
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Prove a commitment was anchored BEFORE the holder's public key was ever exposed.
|
|
509
|
+
*
|
|
510
|
+
* WHY THIS IS STRICTLY STRONGER THAN A Q-DAY CUTOFF
|
|
511
|
+
* -------------------------------------------------
|
|
512
|
+
* The cutoff frontier has an admitted weakness, and it is limit #2 in our README: the
|
|
513
|
+
* guarantee is "anchored before the cryptography broke", and NOBODY KNOWS WHEN IT BROKE.
|
|
514
|
+
* Every verifier is guessing a date, and a private break earlier than the guess produces
|
|
515
|
+
* forged entries indistinguishable from real ones.
|
|
516
|
+
*
|
|
517
|
+
* This removes the guess for a large and important class of holders.
|
|
518
|
+
*
|
|
519
|
+
* A secp256k1 public key has an exact, immutable, publicly verifiable moment at which it
|
|
520
|
+
* became known to the world: the block in which it first appeared on chain — the first
|
|
521
|
+
* spend from that address. Before that block the key existed only in its owner's wallet.
|
|
522
|
+
*
|
|
523
|
+
* So: if a commitment's tree head was anchored in Bitcoin STRICTLY BEFORE the block that
|
|
524
|
+
* first exposed the public key, then at the moment of anchoring NOBODY — with or without a
|
|
525
|
+
* quantum computer — could have derived the private key, because the PUBLIC key was not
|
|
526
|
+
* public yet. There was nothing to break.
|
|
527
|
+
*
|
|
528
|
+
* That is a per-key, chain-verifiable fact. It does not depend on anyone's estimate of
|
|
529
|
+
* Q-day, and it does not weaken if that estimate turns out to be wrong.
|
|
530
|
+
*
|
|
531
|
+
* WHO THIS ACTUALLY SAVES — AND WHO IT CANNOT
|
|
532
|
+
* -------------------------------------------
|
|
533
|
+
* In an UNSPENT P2PKH or P2WPKH output the chain holds only the HASH of the public key.
|
|
534
|
+
* The key itself is still private. Those holders can anchor today and obtain this stronger
|
|
535
|
+
* proof. That is the set of coins still worth protecting, and it is large.
|
|
536
|
+
*
|
|
537
|
+
* For a key already exposed by a previous spend, this gives NOTHING retroactively. You
|
|
538
|
+
* cannot anchor into the past. Those coins need to be moved, and no evidence scheme
|
|
539
|
+
* changes that. We say so rather than implying coverage we do not have.
|
|
540
|
+
*
|
|
541
|
+
* WHERE THE EXPOSURE HEIGHT COMES FROM — NOT FROM US
|
|
542
|
+
* --------------------------------------------------
|
|
543
|
+
* `exposureHeight` is supplied by the VERIFIER, derived from their own node or index. We
|
|
544
|
+
* deliberately do not ship an oracle for it: an exposure height we asserted would be one
|
|
545
|
+
* more thing to trust us about, and the entire point is that this fact is independently
|
|
546
|
+
* checkable in Bitcoin itself. Pass `null` to assert the key has never been exposed —
|
|
547
|
+
* which the verifier must also confirm themselves.
|
|
548
|
+
*/
|
|
549
|
+
export function proveExposureRelativePrimacy(args) {
|
|
550
|
+
const {
|
|
551
|
+
subjectClassicalPub, entries, sth, expectedLogId,
|
|
552
|
+
exposureHeight, otsHex, blockMerkleRoots,
|
|
553
|
+
} = args || {};
|
|
554
|
+
|
|
555
|
+
// FAIL-OPEN BY OMISSION (round 6 of our own siege). The guard below used `== null`, so
|
|
556
|
+
// an ABSENT field, an `undefined`, or a typo produced `strength: "never-exposed"` — the
|
|
557
|
+
// strongest claim this module makes — byte-identical to the deliberate `null` the
|
|
558
|
+
// docstring documents as a verifier ASSERTION. Absence is nobody's assertion. Worse, the
|
|
559
|
+
// sibling function in this same file uses the identical sentinel with the OPPOSITE
|
|
560
|
+
// polarity: it refuses. Presence is now required, and the comparison is strict.
|
|
561
|
+
if (!args || !Object.hasOwn(args, "exposureHeight")) {
|
|
562
|
+
return {
|
|
563
|
+
valid: false, exposureRelative: false, quantumPropertyHolds: false,
|
|
564
|
+
reason: "exposure-height-not-supplied",
|
|
565
|
+
note: "Non-exposure is an ASSERTION the verifier makes from their own node. A field " +
|
|
566
|
+
"that is simply absent does not make it. Pass an explicit height, or an " +
|
|
567
|
+
"explicit null to assert the key has never appeared on chain.",
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
// Establish the anchor first, WITHOUT letting the exposure height act as the cutoff.
|
|
571
|
+
// Passing the exposure as `cutoffBlockHeight` would make the base check fire first and
|
|
572
|
+
// report a generic "anchored-after-cutoff", losing the specific — and far more useful —
|
|
573
|
+
// distinction between "you anchored too late" and "you never anchored at all". The
|
|
574
|
+
// permissive cutoff here only obtains a header-CONFIRMED height; the real comparison is
|
|
575
|
+
// the one below, and it is the whole point of this function.
|
|
576
|
+
const base = proveFirstSeen({
|
|
577
|
+
subjectClassicalPub, entries, sth, expectedLogId,
|
|
578
|
+
cutoffBlockHeight: Number.MAX_SAFE_INTEGER,
|
|
579
|
+
otsHex, blockMerkleRoots,
|
|
580
|
+
});
|
|
581
|
+
if (!base.valid) return { ...base, exposureRelative: false };
|
|
582
|
+
|
|
583
|
+
if (!base.quantumPropertyHolds) {
|
|
584
|
+
return {
|
|
585
|
+
// A refusal must say NO in the field consumers read. This kept `valid: true` from
|
|
586
|
+
// the spread, so the case this function exists to reject came back green to every
|
|
587
|
+
// caller that checks `.valid` — which is what the rest of this package does.
|
|
588
|
+
...base, valid: false, quantumPropertyHolds: false, exposureRelative: false,
|
|
589
|
+
reason: "no-confirmed-anchor",
|
|
590
|
+
note: "Without a header-confirmed Bitcoin anchor there is no height to compare " +
|
|
591
|
+
"against the exposure, so the stronger claim cannot be made.",
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const anchoredAt = base.temporalFrontier.height;
|
|
596
|
+
// The permissive cutoff passed internally is a SENTINEL, not a verifier's choice. It
|
|
597
|
+
// must never travel in the result as `cutoffBlockHeight`, where a reader could cite it
|
|
598
|
+
// as an established frontier.
|
|
599
|
+
base.temporalFrontier = {
|
|
600
|
+
...base.temporalFrontier,
|
|
601
|
+
cutoffBlockHeight: undefined,
|
|
602
|
+
cutoffNotApplied: "none — exposure-relative mode; the comparison is against the key's exposure block",
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
if (exposureHeight === null) {
|
|
606
|
+
return {
|
|
607
|
+
...base, exposureRelative: true, anchoredAtHeight: anchoredAt, exposureHeight: null,
|
|
608
|
+
strength: "never-exposed",
|
|
609
|
+
note: `Anchored at block ${anchoredAt}, and you assert this key has NEVER appeared ` +
|
|
610
|
+
`on chain. If that holds, the key was never derivable by anyone at any point, ` +
|
|
611
|
+
`so no Q-day estimate is involved. Confirm the non-exposure yourself: this is ` +
|
|
612
|
+
`the one fact we cannot check for you.`,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// An implausible height is not a height. MAX_SAFE_INTEGER especially: it is the internal
|
|
617
|
+
// sentinel, and accepting it as a real exposure would make every anchor look early.
|
|
618
|
+
if (!Number.isSafeInteger(exposureHeight) || exposureHeight < 0 || exposureHeight > 10_000_000) {
|
|
619
|
+
return {
|
|
620
|
+
...base, valid: false, quantumPropertyHolds: false, exposureRelative: false,
|
|
621
|
+
reason: "malformed-exposure-height",
|
|
622
|
+
note: "Expected a plausible Bitcoin block height (0..10,000,000). Refused rather than " +
|
|
623
|
+
"coerced.",
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (anchoredAt >= exposureHeight) {
|
|
628
|
+
return {
|
|
629
|
+
...base, valid: false, quantumPropertyHolds: false,
|
|
630
|
+
exposureRelative: false, anchoredAtHeight: anchoredAt, exposureHeight,
|
|
631
|
+
reason: "anchored-after-exposure",
|
|
632
|
+
strength: "cutoff-only",
|
|
633
|
+
note: `Anchored at block ${anchoredAt}, but the key was already public at block ` +
|
|
634
|
+
`${exposureHeight}. From that block on, anyone able to break secp256k1 could ` +
|
|
635
|
+
`have produced this commitment too. The proof falls back to whatever your ` +
|
|
636
|
+
`Q-day cutoff is worth — and that is a guess. These coins should be moved.`,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
...base, exposureRelative: true, anchoredAtHeight: anchoredAt, exposureHeight,
|
|
642
|
+
strength: "anchored-before-exposure",
|
|
643
|
+
blocksBefore: exposureHeight - anchoredAt,
|
|
644
|
+
note: `Anchored at block ${anchoredAt}, ${exposureHeight - anchoredAt} blocks BEFORE ` +
|
|
645
|
+
`the key was first exposed at ${exposureHeight}. At the moment of anchoring the ` +
|
|
646
|
+
`chain had NOT yet revealed the public key, so breaking secp256k1 was not sufficient ` +
|
|
647
|
+
`to produce this commitment. This claim does not depend on any Q-day estimate.`,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export const EXPOSURE_SCOPE = Object.freeze({
|
|
652
|
+
proves:
|
|
653
|
+
"that a commitment was anchored in Bitcoin strictly before the block that first made " +
|
|
654
|
+
"the holder's public key public — a per-key fact, checkable in Bitcoin, that needs no " +
|
|
655
|
+
"Q-day estimate",
|
|
656
|
+
whyStronger:
|
|
657
|
+
"the cutoff frontier asks the verifier to guess when the cryptography broke. This asks " +
|
|
658
|
+
"nothing: before exposure there was no public key to break.",
|
|
659
|
+
savesWho:
|
|
660
|
+
"holders of UNSPENT P2PKH/P2WPKH outputs, where the chain holds only the key's hash. " +
|
|
661
|
+
"They can still anchor before exposing anything.",
|
|
662
|
+
cannotSave:
|
|
663
|
+
"keys already exposed by an earlier spend. You cannot anchor into the past, and no " +
|
|
664
|
+
"evidence scheme changes that — those coins must be moved.",
|
|
665
|
+
alsoRequires:
|
|
666
|
+
"that the holder published the key through NO OTHER CHANNEL. We measure on-chain " +
|
|
667
|
+
"appearance only. A key revealed in a mempool broadcast that never confirmed, in a " +
|
|
668
|
+
"shared xpub, or anywhere off-chain was derivable earlier than this proof suggests. " +
|
|
669
|
+
"The adversary's window opens at min(any publication of the key, on-chain exposure); " +
|
|
670
|
+
"this kit measures the second.",
|
|
671
|
+
notAnAuthorisationGate:
|
|
672
|
+
"policy.mjs does not consume this. It is an independently verifiable proof primitive, " +
|
|
673
|
+
"not yet a mandatory gate on the signing path. Saying otherwise would be the sixth " +
|
|
674
|
+
"incarnation of the mistake this package documents.",
|
|
675
|
+
exposureHeightIsNotOurs:
|
|
676
|
+
"the verifier supplies it from their own node. An exposure height we asserted would be " +
|
|
677
|
+
"one more thing to trust us about, and the point is that it is independently checkable.",
|
|
678
|
+
});
|
package/test/claims.mjs
CHANGED
|
@@ -26,7 +26,7 @@ check("a proof that throws does not pass silently",
|
|
|
26
26
|
check("HONESTY: the method states it does NOT prove completeness",
|
|
27
27
|
CLAIMS_METHOD.whatItDoesNotProve.includes("completeness"));
|
|
28
28
|
check("every claim names the siege round that taught us it needed guarding",
|
|
29
|
-
CLAIMS.every((c) => /^R[1-
|
|
29
|
+
CLAIMS.every((c) => /^R[1-6]-/.test(c.id)));
|
|
30
30
|
|
|
31
31
|
/* ── the third admissibility condition ─────────────────────────────────── */
|
|
32
32
|
import { MUTATIONS, MUTATION_METHOD } from "../src/mutations.mjs";
|
|
@@ -44,7 +44,7 @@ console.log("\nMutation coverage (cheap structural check; run `npm run mutate` f
|
|
|
44
44
|
check("every mutation names a real claim",
|
|
45
45
|
MUTATIONS.every((m) => m.mustKill.every((id) => CLAIMS.some((c) => c.id === id))));
|
|
46
46
|
check("every mutation reintroduces a bug from a named siege round",
|
|
47
|
-
MUTATIONS.every((m) => /^M-R[1-
|
|
47
|
+
MUTATIONS.every((m) => /^M-R[1-6]-/.test(m.id) && typeof m.describes === "string"));
|
|
48
48
|
check("HONESTY: the method states a surviving mutation can mean defence in depth, not a defect",
|
|
49
49
|
MUTATION_METHOD.whatItDoesNotProve.includes("defence in depth"));
|
|
50
50
|
check("HONESTY: and that it does not prove completeness either",
|
package/test/primacy.mjs
CHANGED
|
@@ -25,7 +25,7 @@ function check(name, cond) {
|
|
|
25
25
|
}
|
|
26
26
|
const te = new TextEncoder();
|
|
27
27
|
const CUTOFF = 900_000;
|
|
28
|
-
let OTS, HB, HDRS; // the verifier's declared Q-day boundary
|
|
28
|
+
let OTS, HB, HDRS, STH_A; // the verifier's declared Q-day boundary
|
|
29
29
|
|
|
30
30
|
console.log("PRIMACY — 'first' only counts if it means 'before the cryptography broke'\n");
|
|
31
31
|
|
|
@@ -179,7 +179,7 @@ console.log("\n★★★ ROUND 3 — what Bitcoin freezes is the PROOF OF CONTRO
|
|
|
179
179
|
majorVersion: O.OTS_MAJOR_VERSION, fileHashOp: "sha256", fileDigest: digest, timestamp: ts,
|
|
180
180
|
}));
|
|
181
181
|
};
|
|
182
|
-
const otsHex = mkOts(800_000); OTS = otsHex; HB = headBytes;
|
|
182
|
+
const otsHex = mkOts(800_000); OTS = otsHex; HB = headBytes; STH_A = head.sth;
|
|
183
183
|
const attRoot = (hex) => {
|
|
184
184
|
const ev = O.evaluateOts(O.decodeOtsFile(T.fromHex(hex)));
|
|
185
185
|
const a = ev.blockAttestations.find((x) => x.chain === "bitcoin");
|
|
@@ -331,5 +331,43 @@ console.log("\nRound-4 structural fixes:");
|
|
|
331
331
|
})());
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
console.log("\n★★★ PRIMACÍA RELATIVA A LA EXPOSICIÓN — no depende de adivinar el Día-Q:");
|
|
335
|
+
{
|
|
336
|
+
const { proveExposureRelativePrimacy, EXPOSURE_SCOPE } = await import("../src/primacy.mjs");
|
|
337
|
+
const common = { subjectClassicalPub: victimCert.classicalPub, entries, sth: STH_A,
|
|
338
|
+
expectedLogId: LOG_ID, otsHex: OTS, blockMerkleRoots: HDRS };
|
|
339
|
+
|
|
340
|
+
// El ancla real está en el bloque 800000 (ver el montaje de arriba).
|
|
341
|
+
const before = proveExposureRelativePrimacy({ ...common, exposureHeight: 850_000 });
|
|
342
|
+
check("★ anclado ANTES de la exposición → la afirmación fuerte se sostiene",
|
|
343
|
+
before.exposureRelative === true && before.strength === "anchored-before-exposure");
|
|
344
|
+
check("...y dice cuántos bloques de margen hay", before.blocksBefore === 50_000);
|
|
345
|
+
check("...y declara que NO depende de ninguna estimación de Día-Q",
|
|
346
|
+
before.note.includes("does not depend on any Q-day estimate"));
|
|
347
|
+
|
|
348
|
+
const after = proveExposureRelativePrimacy({ ...common, exposureHeight: 700_000 });
|
|
349
|
+
check("★ anclado DESPUÉS de la exposición → NO se hace la afirmación fuerte",
|
|
350
|
+
after.exposureRelative === false && after.reason === "anchored-after-exposure");
|
|
351
|
+
check("...y dice sin rodeos que esas monedas hay que moverlas",
|
|
352
|
+
after.note.includes("should be moved"));
|
|
353
|
+
|
|
354
|
+
const never = proveExposureRelativePrimacy({ ...common, exposureHeight: null });
|
|
355
|
+
check("clave nunca expuesta → la afirmación más fuerte posible",
|
|
356
|
+
never.exposureRelative === true && never.strength === "never-exposed");
|
|
357
|
+
check("HONESTIDAD: pide al verificador confirmar la no-exposición él mismo",
|
|
358
|
+
never.note.includes("we cannot check for you"));
|
|
359
|
+
|
|
360
|
+
check("una altura de exposición malformada se rehúsa, no se coerciona",
|
|
361
|
+
proveExposureRelativePrimacy({ ...common, exposureHeight: NaN }).exposureRelative === false);
|
|
362
|
+
check("sin ancla confirmada por cabecera no se hace ninguna afirmación fuerte",
|
|
363
|
+
proveExposureRelativePrimacy({ ...common, blockMerkleRoots: undefined, exposureHeight: 850_000 })
|
|
364
|
+
.exposureRelative === false);
|
|
365
|
+
|
|
366
|
+
check("HONESTIDAD: declara a quién NO puede salvar (claves ya expuestas)",
|
|
367
|
+
EXPOSURE_SCOPE.cannotSave.includes("must be moved"));
|
|
368
|
+
check("HONESTIDAD: la altura de exposición NO la ponemos nosotros",
|
|
369
|
+
EXPOSURE_SCOPE.exposureHeightIsNotOurs.includes("independently checkable"));
|
|
370
|
+
}
|
|
371
|
+
|
|
334
372
|
console.log(`\n${pass} passed, ${fail} failed`);
|
|
335
373
|
process.exit(fail === 0 ? 0 : 1);
|