fractal-pqc 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -8
- package/bin/cli.mjs +213 -2
- package/package.json +23 -7
- package/src/address.mjs +22 -0
- package/src/claims-registry.mjs +437 -0
- package/src/claims.mjs +143 -0
- package/src/index.mjs +9 -1
- package/src/mutations.mjs +405 -0
- package/src/ots.mjs +642 -0
- package/src/policy.mjs +289 -0
- package/src/primacy.mjs +502 -0
- package/src/tapscript.mjs +243 -0
- package/src/transparency.mjs +658 -0
- package/test/anchoring.mjs +83 -0
- package/test/bip341-scriptpath.mjs +126 -0
- package/test/bip341-wallet-vectors.json +452 -0
- package/test/claims.mjs +57 -0
- package/test/conformance.mjs +122 -0
- package/test/m2-policy.mjs +168 -0
- package/test/primacy.mjs +335 -0
- package/test/transparency.mjs +328 -0
- package/test/vectors-ots/bitcoin-block-358391.json +9 -0
- package/test/vectors-ots/fractalai-sth-anchor-demo.anchor +0 -0
- package/test/vectors-ots/fractalai-sth-anchor-demo.anchor.ots +0 -0
- package/test/vectors-ots/hello-world.txt +1 -0
- package/test/vectors-ots/hello-world.txt.ots +0 -0
- package/tools/generate-vectors.mjs +119 -0
- package/vectors/pq-anchor-v1.json +405 -0
package/README.md
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
# fractal-pqc
|
|
2
2
|
|
|
3
|
-
> ⚠️ **Reference toolkit — NOT for production custody of real funds (yet).** It is
|
|
4
|
-
> **unaudited**, **key-path-only**, proven on **signet (not mainnet)**, and its quantum
|
|
5
|
-
> guarantee depends on a first-seen anchor registry that is still on the roadmap. Use it
|
|
6
|
-
> for **evaluation, testnet, education, and building on** — do **not** secure real Bitcoin
|
|
7
|
-
> with it until it is audited and 1.0. We say precisely what it is.
|
|
8
|
-
|
|
9
3
|
A small, **runnable, test-vector-verified** reference for quantum-safe migration of a
|
|
10
4
|
Bitcoin-style key. It binds a classical **secp256k1 / Taproot** key to a post-quantum
|
|
11
5
|
**ML-DSA-65 (NIST FIPS-204)** key and requires a post-quantum signature to authorize —
|
|
@@ -25,7 +19,19 @@ without invalidating already-signed history. This kit is a concrete, honest firs
|
|
|
25
19
|
|
|
26
20
|
## What is REAL here (verified, not aspirational)
|
|
27
21
|
|
|
28
|
-
|
|
22
|
+
> **The security layer (v0.4.0):** `transparency.mjs` (PQ-ANCHOR-v1 append-only log:
|
|
23
|
+
> inclusion + consistency proofs, ML-DSA-65 signed heads, equivocation detection),
|
|
24
|
+
> `primacy.mjs` (first-seen proven by complete enumeration + a Bitcoin-anchored temporal
|
|
25
|
+
> frontier), `policy.mjs` (fail-closed custodian gate), `tapscript.mjs` (BIP-341
|
|
26
|
+
> script-path, asserted against the official wallet vectors) and `ots.mjs` (independent
|
|
27
|
+
> OpenTimestamps codec). All are exported: `fractal-pqc/primacy`, `/policy`,
|
|
28
|
+
> `/transparency`, `/tapscript`, `/ots`.
|
|
29
|
+
>
|
|
30
|
+
> **The limit we have not closed, stated up front:** a holder who never registered a
|
|
31
|
+
> dual-signed commitment before the verifier's cutoff height cannot be rescued by any of
|
|
32
|
+
> this. `test/primacy.mjs` asserts that out loud rather than leaving it to be discovered.
|
|
33
|
+
|
|
34
|
+
Everything below is exercised by `npm test` with real keys — **281 checks pass**:
|
|
29
35
|
|
|
30
36
|
- **secp256k1** commitment + spend authorization (`@noble/curves`).
|
|
31
37
|
- **Taproot BIP-340 Schnorr** sign/verify, asserted against the **official BIP-340 test
|
|
@@ -134,7 +140,7 @@ so the property is never silently over-claimed.
|
|
|
134
140
|
- **Key-path-only (no script tree / merkle root).** `taprootTweakPrivateKey`/`signTaprootKeyPath`
|
|
135
141
|
handle key-path-only outputs — exactly what this kit's `p2trAddress` derives. Pointing the
|
|
136
142
|
signer at a foreign Taproot output that commits to a script tree would produce an invalid
|
|
137
|
-
signature. Script-path (tapscript) spends
|
|
143
|
+
signature. Script-path (tapscript) spends ARE implemented and asserted against the official BIP-341 wallet test vectors (7/7 cases, 12/12 control blocks, byte for byte).
|
|
138
144
|
- **Secrets are not zeroized.** Private-key `Uint8Array`s are not wiped after use (best-effort
|
|
139
145
|
only in JS); `keygen` prints secrets by design (testnet/experimental).
|
|
140
146
|
- **Not a BIP and not consensus.** The "recovery commitment" is an application-layer
|
package/bin/cli.mjs
CHANGED
|
@@ -33,6 +33,9 @@ import {
|
|
|
33
33
|
sendP2trKeyPath,
|
|
34
34
|
explorerAddrUrl,
|
|
35
35
|
} from "../src/index.mjs";
|
|
36
|
+
import * as T from "../src/transparency.mjs";
|
|
37
|
+
import * as O from "../src/ots.mjs";
|
|
38
|
+
import * as P from "../src/primacy.mjs";
|
|
36
39
|
|
|
37
40
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38
41
|
const hex = (b) => Buffer.from(b).toString("hex");
|
|
@@ -188,6 +191,43 @@ switch (cmd) {
|
|
|
188
191
|
break;
|
|
189
192
|
}
|
|
190
193
|
|
|
194
|
+
// The honesty ledger: every security sentence this package makes, re-derived on every
|
|
195
|
+
// run, each with the attack written to break it. Four sieges found the same disease
|
|
196
|
+
// four times — an English sentence and a code path drifting apart with a green suite in
|
|
197
|
+
// between — so a claim here is inadmissible unless it ships with both.
|
|
198
|
+
case "claims": {
|
|
199
|
+
const C = await import("../src/claims.mjs");
|
|
200
|
+
const R = await import("../src/claims-registry.mjs");
|
|
201
|
+
const report = C.runClaims(R.CLAIMS);
|
|
202
|
+
if (rest.includes("--gaps")) {
|
|
203
|
+
// What the green does NOT buy. Publishing N/N without this is how an honest tool
|
|
204
|
+
// starts looking like an over-claiming one.
|
|
205
|
+
const R2 = await import("../src/claims-registry.mjs");
|
|
206
|
+
out({ claimed: report.results.map((r) => ({ id: r.id, module: r.module, holds: r.holds })),
|
|
207
|
+
unclaimedGuards: R2.UNCLAIMED_GUARDS,
|
|
208
|
+
note: "A guard on this list is not covered by any claim. Round 5 of our own siege " +
|
|
209
|
+
"deleted an unclaimed guard and the ledger still printed all-green." });
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
if (rest.includes("--attribution")) {
|
|
213
|
+
const M = await import("../src/mutations.mjs");
|
|
214
|
+
const a = M.runAttackAttribution();
|
|
215
|
+
out(a);
|
|
216
|
+
process.exit(a.ok ? 0 : 1);
|
|
217
|
+
}
|
|
218
|
+
if (rest.includes("--mutate")) {
|
|
219
|
+
// The third admissibility condition: a claim must DIE when its code dies.
|
|
220
|
+
const M = await import("../src/mutations.mjs");
|
|
221
|
+
const mut = M.runMutations({ claimIds: report.results.filter((r) => r.holds).map((r) => r.id) });
|
|
222
|
+
if (rest.includes("--json")) out({ claims: report, mutations: mut });
|
|
223
|
+
else { console.log(C.formatClaims(report)); console.log(""); console.log(M.formatMutations(mut)); }
|
|
224
|
+
process.exit(report.ok && mut.ok ? 0 : 1);
|
|
225
|
+
}
|
|
226
|
+
if (rest.includes("--json")) out(report);
|
|
227
|
+
else console.log(C.formatClaims(report));
|
|
228
|
+
process.exit(report.ok ? 0 : 1);
|
|
229
|
+
}
|
|
230
|
+
|
|
191
231
|
case "verify-vector": {
|
|
192
232
|
const ok = verifiesBip340OfficialVector();
|
|
193
233
|
out({ bip340_official_vector: ok ? "VERIFIED (Bitcoin-consensus-correct)" : "FAILED" });
|
|
@@ -196,7 +236,164 @@ switch (cmd) {
|
|
|
196
236
|
|
|
197
237
|
case "selftest": {
|
|
198
238
|
const r = spawnSync(process.execPath, [join(__dirname, "..", "test", "vectors.mjs")], { stdio: "inherit" });
|
|
199
|
-
process.exit(r.status ?? 1);
|
|
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)], { stdio: "inherit" });
|
|
242
|
+
if ((t.status ?? 1) !== 0) process.exit(t.status ?? 1);
|
|
243
|
+
}
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Verify a transparency bundle WITHOUT trusting the log that served it.
|
|
248
|
+
// Everything is recomputed locally: the head signature, the Merkle root from the
|
|
249
|
+
// audit path, and (when a previous head is present) the append-only property.
|
|
250
|
+
case "verify-anchor": {
|
|
251
|
+
const path = rest[0];
|
|
252
|
+
if (!path) die("usage: fractal-pqc verify-anchor <bundle.json> [--log-id <hex>]");
|
|
253
|
+
const b = JSON.parse(readFileSync(path, "utf8"));
|
|
254
|
+
// PROVENANCE, applied completely this time. Round 3 fixed the headers and left the
|
|
255
|
+
// OTHER two halves of the predicate readable from the claimant's own JSON: the log
|
|
256
|
+
// identity to pin against, and the cutoff height. Both must come from the operator.
|
|
257
|
+
const pin = flag("log-id");
|
|
258
|
+
if (!pin) die("verify-anchor requires --log-id <hex>, obtained independently of this " +
|
|
259
|
+
"bundle. Pinning against an identity the bundle supplies proves nothing.");
|
|
260
|
+
const cutoffRaw = flag("cutoff");
|
|
261
|
+
if (cutoffRaw === undefined) {
|
|
262
|
+
die("verify-anchor requires --cutoff <height>: the block height below which an anchor " +
|
|
263
|
+
"counts as 'before'. There is no objective Q-day and the bundle does not get to choose it.");
|
|
264
|
+
}
|
|
265
|
+
const cutoff = Number(cutoffRaw);
|
|
266
|
+
if (!Number.isSafeInteger(cutoff) || cutoff < 0) die(`--cutoff must be a non-negative integer, got ${cutoffRaw}`);
|
|
267
|
+
// PROVENANCE. The block headers are the entire trust boundary, so they must arrive on a
|
|
268
|
+
// channel the claimant does not control. Reading them out of the same JSON that carries
|
|
269
|
+
// the .ots makes the confirmation self-fulfilling — round 3 of our own siege did exactly
|
|
270
|
+
// that and printed `quantum_property_holds: true` over a forged anchor.
|
|
271
|
+
const roots = {};
|
|
272
|
+
for (const a of rest) {
|
|
273
|
+
const m = /^--block-merkle-root=(\d+)=([0-9a-fA-F]{64})$/.exec(a);
|
|
274
|
+
if (m) roots[Number(m[1])] = m[2].toLowerCase();
|
|
275
|
+
}
|
|
276
|
+
const haveRoots = Object.keys(roots).length > 0;
|
|
277
|
+
// Round 2 of our own siege found this verb still called the ROUND-0 membership-only
|
|
278
|
+
// gate, so a package consumer got exactly the trust model the fixes replaced. It now
|
|
279
|
+
// calls primacy, and reports the temporal frontier rather than implying it.
|
|
280
|
+
const r = b.entries
|
|
281
|
+
? P.proveFirstSeen({
|
|
282
|
+
subjectClassicalPub: b.subjectClassicalPub || b.classicalPub,
|
|
283
|
+
entries: b.entries, sth: b.sth, expectedLogId: pin,
|
|
284
|
+
cutoffBlockHeight: cutoff,
|
|
285
|
+
otsHex: b.otsHex,
|
|
286
|
+
blockMerkleRoots: haveRoots ? roots : undefined, // NEVER b.blockMerkleRoots
|
|
287
|
+
})
|
|
288
|
+
: { valid: false, reason: "no-enumeration",
|
|
289
|
+
detail: "This bundle carries no `entries` list. Inclusion alone proves membership, " +
|
|
290
|
+
"never primacy, and primacy is the whole quantum property. Supply the full " +
|
|
291
|
+
"ordered entry list for the signed head." };
|
|
292
|
+
const result = {
|
|
293
|
+
head_signature: T.verifyTreeHead(b.sth, pin ? { expectedLogId: pin } : {}),
|
|
294
|
+
primacy: r.valid
|
|
295
|
+
? `VERIFIED — earliest DUAL-SIGNED binding at index ${r.firstSeenIndex}, enumeration ` +
|
|
296
|
+
`checked against the signed root`
|
|
297
|
+
: `FAILED (${r.reason})${r.detail ? " — " + r.detail : ""}`,
|
|
298
|
+
first_seen_fact_hash: r.firstSeenFactHash || null,
|
|
299
|
+
temporal_frontier: r.temporalFrontier || "NOT ESTABLISHED (no cutoff/anchor supplied)",
|
|
300
|
+
// Never print an affirmative boolean the evidence does not support. This field used to
|
|
301
|
+
// read `true` while the nested note admitted the header had not been checked.
|
|
302
|
+
quantum_property_holds: r.quantumPropertyHolds === true,
|
|
303
|
+
block_headers_provenance: haveRoots
|
|
304
|
+
? "supplied on the command line, independent of the bundle"
|
|
305
|
+
: "ABSENT — the bundle's own .ots is unauthenticated data. Pass " +
|
|
306
|
+
"--block-merkle-root=<height>=<root> from your own node.",
|
|
307
|
+
ignored_unsigned_entries: r.ignoredUnsignedEntries || [],
|
|
308
|
+
log_identity_pinned: Boolean(pin),
|
|
309
|
+
};
|
|
310
|
+
if (b.previousSth && b.consistency) {
|
|
311
|
+
// The previous head was NEVER authenticated here: only .treeSize and .rootHash were
|
|
312
|
+
// read, and its logId was never compared, so the pin did not reach it. An operator
|
|
313
|
+
// who rewrote history could attach a FABRICATED previous head and this tool printed
|
|
314
|
+
// the product's strongest guarantee. Found by our own siege. Authenticate first.
|
|
315
|
+
const prev = T.verifyTreeHead(b.previousSth, pin ? { expectedLogId: pin } : {});
|
|
316
|
+
if (!prev.valid) {
|
|
317
|
+
result.append_only = `NOT CHECKED — the previous head does not verify (${prev.reason}). ` +
|
|
318
|
+
`An unauthenticated previous head proves nothing, so no append-only claim is made.`;
|
|
319
|
+
out(result);
|
|
320
|
+
process.exit(r.valid ? 0 : 1);
|
|
321
|
+
}
|
|
322
|
+
if (b.previousSth.logId !== b.sth.logId) {
|
|
323
|
+
result.append_only = "NOT CHECKED — the two heads are from DIFFERENT logs, so " +
|
|
324
|
+
"consistency between them is meaningless.";
|
|
325
|
+
out(result);
|
|
326
|
+
process.exit(r.valid ? 0 : 1);
|
|
327
|
+
}
|
|
328
|
+
if (b.previousSth.treeSize > b.sth.treeSize) {
|
|
329
|
+
result.append_only = "NOT CHECKED — the 'previous' head is LARGER than the current one.";
|
|
330
|
+
out(result);
|
|
331
|
+
process.exit(r.valid ? 0 : 1);
|
|
332
|
+
}
|
|
333
|
+
const c = T.verifyConsistency(
|
|
334
|
+
b.previousSth.treeSize, b.sth.treeSize,
|
|
335
|
+
T.fromHex(b.previousSth.rootHash), T.fromHex(b.sth.rootHash),
|
|
336
|
+
b.consistency.map(T.fromHex)
|
|
337
|
+
);
|
|
338
|
+
result.append_only = c
|
|
339
|
+
? "VERIFIED (both heads authenticated under the pinned log; this head extends the " +
|
|
340
|
+
"previous one, so no history was rewritten between them)"
|
|
341
|
+
: "UNRESOLVED — the supplied consistency proof does not verify. The proof is an " +
|
|
342
|
+
"UNSIGNED field, so this is consistent with the log rewriting history AND with " +
|
|
343
|
+
"whoever handed you this bundle altering the proof. Ask the log for the proof " +
|
|
344
|
+
"between these exact heads before concluding anything.";
|
|
345
|
+
if (!c) result.allegation = T.detectEquivocation(b.previousSth, b.sth, { consistencyProof: b.consistency });
|
|
346
|
+
} else {
|
|
347
|
+
result.append_only = "NOT CHECKED (bundle carried no previous head + consistency proof)";
|
|
348
|
+
}
|
|
349
|
+
// Bitcoin anchoring: from the .ots alone, derive the block height and the Merkle
|
|
350
|
+
// root that block must contain. You then check that root against a header you get
|
|
351
|
+
// from your own node. We are not in that path.
|
|
352
|
+
if (b.otsHex && b.anchorFileHex) {
|
|
353
|
+
try {
|
|
354
|
+
const anchorFile = T.fromHex(b.anchorFileHex);
|
|
355
|
+
const ev = O.evaluateOts(O.decodeOtsFile(T.fromHex(b.otsHex)));
|
|
356
|
+
const commits = ev.fileDigestHex === T.toHex(O.otsSha256(anchorFile));
|
|
357
|
+
if (!commits) {
|
|
358
|
+
result.bitcoin_anchor = "FAILED — the .ots does not commit to the anchor file supplied";
|
|
359
|
+
} else if ((ev.blockAttestations || []).length > 0) {
|
|
360
|
+
const att = ev.blockAttestations[0];
|
|
361
|
+
const rootDisplay = (att.merkleRootInternalHex.match(/../g) || []).reverse().join("");
|
|
362
|
+
result.bitcoin_anchor = {
|
|
363
|
+
status: "BITCOIN-ATTESTED",
|
|
364
|
+
chain: att.chain,
|
|
365
|
+
block_height: att.height,
|
|
366
|
+
block_merkle_root: rootDisplay,
|
|
367
|
+
check_it_yourself: `bitcoin-cli getblockheader $(bitcoin-cli getblockhash ${att.height}) | grep merkleroot`,
|
|
368
|
+
means: "the tree head existed no later than this block. Anteriority, not uniqueness.",
|
|
369
|
+
};
|
|
370
|
+
} else {
|
|
371
|
+
result.bitcoin_anchor = {
|
|
372
|
+
status: "PENDING",
|
|
373
|
+
calendars: (ev.pending || []).map((p) => p.uri),
|
|
374
|
+
means: "the calendars accepted it but no Bitcoin block confirms it YET. " +
|
|
375
|
+
"A pending anchor is NOT a Bitcoin timestamp. Upgrade the proof and re-check.",
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
} catch (e) {
|
|
379
|
+
result.bitcoin_anchor = `FAILED to parse the .ots: ${e.message}`;
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
result.bitcoin_anchor = "ABSENT (bundle carried no .ots). The head is our assertion only.";
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (!pin) {
|
|
386
|
+
result.caveat = "No log identity was pinned, so this proves nothing about THE log. " +
|
|
387
|
+
"Pin it with --log-id from an independently obtained source.";
|
|
388
|
+
}
|
|
389
|
+
if (r.valid && r.quantumPropertyHolds !== true) {
|
|
390
|
+
result.warning = "Primacy holds WITHIN the log, but the temporal frontier is not " +
|
|
391
|
+
"established. Being first in a log is not being first in TIME, and only time " +
|
|
392
|
+
"separates the holder from a post-quantum attacker. Do not authorise a spend on this.";
|
|
393
|
+
}
|
|
394
|
+
result.scope = T.HONEST_SCOPE.doesNotProve;
|
|
395
|
+
out(result);
|
|
396
|
+
process.exit(r.valid ? 0 : 1);
|
|
200
397
|
}
|
|
201
398
|
|
|
202
399
|
default:
|
|
@@ -212,8 +409,22 @@ Usage:
|
|
|
212
409
|
fractal-pqc receive --key <taproot> [--network tb] Show the funding (P2TR) address
|
|
213
410
|
fractal-pqc send-testnet --key <taproot> --to <addr> --amount <sats> [--fee-rate N] [--network tb|signet] [--broadcast]
|
|
214
411
|
Build+sign a P2TR spend; dry-run unless --broadcast
|
|
412
|
+
fractal-pqc verify-anchor <bundle.json> --log-id <hex> --cutoff <height>
|
|
413
|
+
--block-merkle-root=<height>=<root> (from YOUR node)
|
|
414
|
+
Verify a transparency bundle offline: head
|
|
415
|
+
signature, inclusion (root recomputed) and
|
|
416
|
+
append-only consistency. Trusts nothing.
|
|
417
|
+
fractal-pqc claims [--mutate|--gaps|--attribution] [--json]
|
|
418
|
+
THE HONESTY LEDGER: every security claim this
|
|
419
|
+
package makes, with the attack written to break
|
|
420
|
+
it. Exits non-zero if any claim is false or was
|
|
421
|
+
stated without a proof AND an attack.
|
|
422
|
+
--mutate also reintroduces bugs that really
|
|
423
|
+
shipped here and proves each claim DIES when the
|
|
424
|
+
code it names is broken. A claim no mutation can
|
|
425
|
+
kill is vacuous, and is reported as such.
|
|
215
426
|
fractal-pqc verify-vector Check the official BIP-340 test vector
|
|
216
|
-
fractal-pqc selftest Run
|
|
427
|
+
fractal-pqc selftest Run everything: 281 real checks, no mocks
|
|
217
428
|
|
|
218
429
|
Docs: integrations/pqc-migration-kit/README.md`);
|
|
219
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.4.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",
|
|
@@ -18,21 +18,37 @@
|
|
|
18
18
|
"exports": {
|
|
19
19
|
".": "./src/index.mjs",
|
|
20
20
|
"./migration-envelope": "./src/migration-envelope.mjs",
|
|
21
|
-
"./bitcoin": "./src/bitcoin.mjs"
|
|
21
|
+
"./bitcoin": "./src/bitcoin.mjs",
|
|
22
|
+
"./transparency": "./src/transparency.mjs",
|
|
23
|
+
"./primacy": "./src/primacy.mjs",
|
|
24
|
+
"./policy": "./src/policy.mjs",
|
|
25
|
+
"./tapscript": "./src/tapscript.mjs",
|
|
26
|
+
"./ots": "./src/ots.mjs",
|
|
27
|
+
"./address": "./src/address.mjs",
|
|
28
|
+
"./claims": "./src/claims.mjs",
|
|
29
|
+
"./claims-registry": "./src/claims-registry.mjs",
|
|
30
|
+
"./mutations": "./src/mutations.mjs"
|
|
22
31
|
},
|
|
23
32
|
"bin": {
|
|
24
33
|
"fractal-pqc": "./bin/cli.mjs"
|
|
25
34
|
},
|
|
26
35
|
"files": [
|
|
27
|
-
"
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"README.md",
|
|
28
38
|
"bin",
|
|
39
|
+
"src",
|
|
29
40
|
"test",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
41
|
+
"tools",
|
|
42
|
+
"vectors"
|
|
32
43
|
],
|
|
33
44
|
"scripts": {
|
|
34
|
-
"test": "node test/vectors.mjs",
|
|
35
|
-
"selftest": "node bin/cli.mjs selftest"
|
|
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",
|
|
46
|
+
"selftest": "node bin/cli.mjs selftest",
|
|
47
|
+
"conformance": "node test/conformance.mjs",
|
|
48
|
+
"claims": "node bin/cli.mjs claims",
|
|
49
|
+
"mutate": "node bin/cli.mjs claims --mutate",
|
|
50
|
+
"attribution": "node bin/cli.mjs claims --attribution",
|
|
51
|
+
"gaps": "node bin/cli.mjs claims --gaps"
|
|
36
52
|
},
|
|
37
53
|
"dependencies": {
|
|
38
54
|
"@noble/curves": "^2.2.0",
|
package/src/address.mjs
CHANGED
|
@@ -59,6 +59,28 @@ export function p2trAddress(internalXOnly, hrp = "bc") {
|
|
|
59
59
|
return addr;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Encode a P2TR address from an ALREADY-TWEAKED output key (witness v1, bech32m).
|
|
64
|
+
*
|
|
65
|
+
* `p2trAddress` takes an INTERNAL key and applies the key-path tweak itself, which is
|
|
66
|
+
* right for key-path-only outputs and wrong for anything with a script tree — tweaking
|
|
67
|
+
* an output key a second time yields a valid-looking address for coins nobody can spend.
|
|
68
|
+
* Script-path callers derive the output key with `taprootOutputKeyWithTree` and encode it
|
|
69
|
+
* here. Asserted against the official BIP-341 `bip350Address` vectors.
|
|
70
|
+
*
|
|
71
|
+
* @param {Uint8Array} outputKeyXOnly 32-byte x-only TWEAKED output key
|
|
72
|
+
* @param {"bc"|"tb"|"bcrt"} [hrp="bc"] network prefix
|
|
73
|
+
*/
|
|
74
|
+
export function p2trAddressFromOutputKey(outputKeyXOnly, hrp = "bc") {
|
|
75
|
+
assertHrp(hrp);
|
|
76
|
+
if (!(outputKeyXOnly instanceof Uint8Array) || outputKeyXOnly.length !== 32) {
|
|
77
|
+
throw new Error("p2trAddressFromOutputKey: expected a 32-byte x-only output key");
|
|
78
|
+
}
|
|
79
|
+
const addr = segwitEncode(hrp, 1, outputKeyXOnly);
|
|
80
|
+
if (!addr) throw new Error("failed to encode P2TR address");
|
|
81
|
+
return addr;
|
|
82
|
+
}
|
|
83
|
+
|
|
62
84
|
/**
|
|
63
85
|
* Decode a bech32/bech32m SegWit address into its scriptPubKey bytes, so we can pay it.
|
|
64
86
|
* witver 0 → OP_0 PUSH<20|32> program (P2WPKH / P2WSH)
|