@mikeargento/bitgraph 1.4.0 → 1.5.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/dist/__tests__/fuse-set-sdk.test.js +77 -15
- package/dist/__tests__/fuse-set-sdk.test.js.map +1 -1
- package/dist/fuse-cli.js +3 -2
- package/dist/fuse-cli.js.map +1 -1
- package/dist/fuse.d.ts +32 -2
- package/dist/fuse.d.ts.map +1 -1
- package/dist/fuse.js +84 -26
- package/dist/fuse.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/fuse-set-sdk.test.ts +88 -26
- package/src/fuse-cli.ts +3 -2
- package/src/fuse.ts +107 -28
- package/src/index.ts +1 -1
package/src/fuse.ts
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
computeSlotRecordHash,
|
|
47
47
|
fuseAttribution,
|
|
48
48
|
getPlacement,
|
|
49
|
+
parseSetManifest,
|
|
49
50
|
readSetMetadata,
|
|
50
51
|
SET_METADATA_KEY,
|
|
51
52
|
verifyFuse,
|
|
@@ -65,6 +66,25 @@ import type {
|
|
|
65
66
|
SlotAllocation,
|
|
66
67
|
} from "@mikeargento/bitgraph-verify";
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* SHA-256 over bytes: the platform's native hasher when one is present
|
|
71
|
+
* (WebCrypto, in browsers and in Node), else the JavaScript library. The
|
|
72
|
+
* native path runs about ten times faster over large files and both give
|
|
73
|
+
* the same digest; a test pins that. A platform that refuses the input (a
|
|
74
|
+
* shared or detached buffer) falls back to the library.
|
|
75
|
+
*/
|
|
76
|
+
export async function digest(bytes: Uint8Array): Promise<Uint8Array> {
|
|
77
|
+
const subtle = (globalThis as { crypto?: { subtle?: { digest?: (alg: string, data: Uint8Array) => Promise<ArrayBuffer> } } }).crypto?.subtle;
|
|
78
|
+
if (subtle !== undefined && typeof subtle.digest === "function") {
|
|
79
|
+
try {
|
|
80
|
+
return new Uint8Array(await subtle.digest("SHA-256", bytes));
|
|
81
|
+
} catch {
|
|
82
|
+
// fall through to the library
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return sha256(bytes);
|
|
86
|
+
}
|
|
87
|
+
|
|
68
88
|
export type { FuseFrame, PlacementId, SlotAllocation, BitGraphProof, SetManifest, FuseMemberResult, FuseVerifyResult } from "@mikeargento/bitgraph-verify";
|
|
69
89
|
|
|
70
90
|
/** What the builder receives. The raw nonce is deliberately absent. */
|
|
@@ -381,7 +401,7 @@ export async function fuse(builder: FuseBuilder, options: FuseOptions): Promise<
|
|
|
381
401
|
if (options.originDigest !== undefined && options.originDigest.length !== 32) throw new FuseError("bad-input", "originDigest must be 32 bytes");
|
|
382
402
|
|
|
383
403
|
const t: BoundTransport = { ...DEFAULTS, ...(options.transport ?? {}) };
|
|
384
|
-
const originDigest = options.original !== undefined ?
|
|
404
|
+
const originDigest = options.original !== undefined ? await digest(options.original) : options.originDigest;
|
|
385
405
|
const originDigestB64 = originDigest !== undefined ? bytesToBase64(originDigest) : null;
|
|
386
406
|
|
|
387
407
|
// 1. nonce
|
|
@@ -399,7 +419,7 @@ export async function fuse(builder: FuseBuilder, options: FuseOptions): Promise<
|
|
|
399
419
|
requireCommitment(placement, fused, commitment);
|
|
400
420
|
|
|
401
421
|
// 3. hash
|
|
402
|
-
const artifactDigest =
|
|
422
|
+
const artifactDigest = await digest(fused);
|
|
403
423
|
const artifactDigestB64 = bytesToBase64(artifactDigest);
|
|
404
424
|
|
|
405
425
|
// 4. fill
|
|
@@ -469,9 +489,32 @@ export interface FuseSetMember {
|
|
|
469
489
|
builder?: FuseBuilder;
|
|
470
490
|
}
|
|
471
491
|
|
|
492
|
+
export interface FuseSetProgress {
|
|
493
|
+
/**
|
|
494
|
+
* "hash": origin digests, one per member, before any request.
|
|
495
|
+
* "fuse": each member built and its fused digest taken, after the slot is held.
|
|
496
|
+
* "commit": 0 of 1 before the request, 1 of 1 when the proof is back.
|
|
497
|
+
* "verify": only with verifyMembers, one per member.
|
|
498
|
+
*/
|
|
499
|
+
phase: "hash" | "fuse" | "commit" | "verify";
|
|
500
|
+
done: number;
|
|
501
|
+
total: number;
|
|
502
|
+
}
|
|
503
|
+
|
|
472
504
|
export interface FuseSetOptions {
|
|
473
505
|
/** Return each member's fused bytes. Default false: they are virtual, rebuilt from the original and the proof. */
|
|
474
506
|
keepFused?: boolean;
|
|
507
|
+
/**
|
|
508
|
+
* Run the full verifier (verifyFuseMember) over every member's fused bytes
|
|
509
|
+
* after the commit and return each verdict under `verification`. Default
|
|
510
|
+
* false: every member is bound to the returned proof by digest, its row in
|
|
511
|
+
* the committed manifest, which is itself verified FUSED_DIRECT; that is
|
|
512
|
+
* linear and reads no bytes. The full pass re-hashes every member with the
|
|
513
|
+
* verifier's own hasher and grows with the square of the member count.
|
|
514
|
+
*/
|
|
515
|
+
verifyMembers?: boolean;
|
|
516
|
+
/** Called as the set advances. A throw inside it is ignored: a progress hook never changes the outcome. */
|
|
517
|
+
onProgress?: (progress: FuseSetProgress) => void;
|
|
475
518
|
/** Actor-bound commits: an agency envelope passed through untouched. */
|
|
476
519
|
agency?: unknown;
|
|
477
520
|
transport?: FuseTransport;
|
|
@@ -492,8 +535,8 @@ export interface FuseSetMemberResult {
|
|
|
492
535
|
frameName: string | null;
|
|
493
536
|
/** Present only when keepFused is true. */
|
|
494
537
|
fusedBytes?: Uint8Array;
|
|
495
|
-
/**
|
|
496
|
-
verification
|
|
538
|
+
/** Present only with verifyMembers: the verifier's own verdict against this member's fused bytes. Always SET_MEMBER_DIRECT on success, with set.manifestSource "argument". */
|
|
539
|
+
verification?: FuseMemberResult;
|
|
497
540
|
}
|
|
498
541
|
|
|
499
542
|
export interface FuseSetResult {
|
|
@@ -528,6 +571,14 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
528
571
|
interface Checked { placement: Placement; id: SetMemberPlacement; original: Uint8Array; originDigest: Uint8Array; name: string | null; builder: FuseBuilder }
|
|
529
572
|
const checked: Checked[] = [];
|
|
530
573
|
const seen = new Map<string, number>();
|
|
574
|
+
const report = (phase: FuseSetProgress["phase"], done: number, total: number) => {
|
|
575
|
+
if (options.onProgress === undefined) return;
|
|
576
|
+
try {
|
|
577
|
+
options.onProgress({ phase, done, total });
|
|
578
|
+
} catch {
|
|
579
|
+
// a progress hook never changes the outcome
|
|
580
|
+
}
|
|
581
|
+
};
|
|
531
582
|
for (let i = 0; i < members.length; i++) {
|
|
532
583
|
const m = members[i];
|
|
533
584
|
// A null, undefined or missing element is refused like any other member without original bytes.
|
|
@@ -538,7 +589,7 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
538
589
|
if (placement.form === "C") throw new FuseError("bad-input", `member ${i}: ${id} takes no original; a set holds trailer/1 and container/1 members only`, null, i);
|
|
539
590
|
if (m.name !== undefined && typeof m.name !== "string") throw new FuseError("bad-input", `member ${i}: name must be a string`, null, i);
|
|
540
591
|
if (m.builder !== undefined && typeof m.builder !== "function") throw new FuseError("bad-input", `member ${i}: builder must be a function`, null, i);
|
|
541
|
-
const originDigest =
|
|
592
|
+
const originDigest = await digest(m.original);
|
|
542
593
|
// The same original under the same placement fuses to the same bytes, which one manifest lists once.
|
|
543
594
|
const key = `${id}:${bytesToHex(originDigest)}`;
|
|
544
595
|
const j = seen.get(key);
|
|
@@ -547,6 +598,7 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
547
598
|
}
|
|
548
599
|
seen.set(key, i);
|
|
549
600
|
checked.push({ placement, id, original: m.original, originDigest, name: m.name ?? null, builder: m.builder ?? builderFor(id, m.original) });
|
|
601
|
+
report("hash", i + 1, members.length);
|
|
550
602
|
}
|
|
551
603
|
const t: BoundTransport = { ...DEFAULTS, ...(options.transport ?? {}) };
|
|
552
604
|
|
|
@@ -557,7 +609,12 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
557
609
|
// is held and its TTL is running; a throw here burns it but commits nothing.
|
|
558
610
|
const commitment = computeSlotCommitment(slot);
|
|
559
611
|
const commitmentHex = bytesToHex(commitment);
|
|
560
|
-
const
|
|
612
|
+
const keep = options.keepFused === true;
|
|
613
|
+
const verifyMembers = options.verifyMembers === true;
|
|
614
|
+
// A member's fused bytes are virtual: each is built, hashed and released in
|
|
615
|
+
// turn, so memory holds the originals and one fused copy. They are held only
|
|
616
|
+
// for a caller who keeps them or asks the full verifier to read them.
|
|
617
|
+
const fusedBytes: (Uint8Array | null)[] = [];
|
|
561
618
|
const rows: SetMember[] = [];
|
|
562
619
|
for (let i = 0; i < checked.length; i++) {
|
|
563
620
|
const c = checked[i]!;
|
|
@@ -572,16 +629,18 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
572
629
|
// The row's origin must be the origin the bytes embed, else the member
|
|
573
630
|
// would verify INVALID_ORIGIN_ATTRIBUTION after the slot is spent. Both
|
|
574
631
|
// facts are checked when both are present: the digest the bytes declare
|
|
575
|
-
// (container/1's payload) and the bytes they carry,
|
|
576
|
-
//
|
|
632
|
+
// (container/1's payload) and the bytes they carry, compared byte for
|
|
633
|
+
// byte with the member's original rather than hashed again, so a builder
|
|
634
|
+
// cannot pack other bytes under the member's digest and leave a member no
|
|
577
635
|
// original rebuilds.
|
|
578
636
|
const declared = located.originDigest;
|
|
579
|
-
const carried = located.originalBytes
|
|
580
|
-
if ((declared !== undefined && !bytesEqual(declared, c.originDigest)) || (carried !== undefined && !bytesEqual(carried, c.
|
|
637
|
+
const carried = located.originalBytes;
|
|
638
|
+
if ((declared !== undefined && !bytesEqual(declared, c.originDigest)) || (carried !== undefined && !bytesEqual(carried, c.original))) {
|
|
581
639
|
throw new FuseError("builder-failed", `member ${i}: the fused bytes embed an origin that is not the member's original; nothing was committed and the slot will expire`, null, i);
|
|
582
640
|
}
|
|
583
|
-
|
|
584
|
-
|
|
641
|
+
rows.push({ artifact: await digest(fused), origin: c.originDigest, placement: c.id });
|
|
642
|
+
fusedBytes.push(keep || verifyMembers ? fused : null);
|
|
643
|
+
report("fuse", i + 1, checked.length);
|
|
585
644
|
}
|
|
586
645
|
|
|
587
646
|
// 3. hash: the canonical manifest is the artifact
|
|
@@ -591,7 +650,7 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
591
650
|
} catch (err) {
|
|
592
651
|
throw new FuseError("bad-input", `the set manifest could not be built: ${err instanceof Error ? err.message : String(err)}; nothing was committed and the slot will expire`);
|
|
593
652
|
}
|
|
594
|
-
const artifactDigestB64 = bytesToBase64(
|
|
653
|
+
const artifactDigestB64 = bytesToBase64(await digest(manifestBytes));
|
|
595
654
|
const manifest = JSON.parse(new TextDecoder().decode(manifestBytes)) as SetManifest;
|
|
596
655
|
|
|
597
656
|
// 4. fill: one commit, the parsed manifest riding along as unsigned metadata
|
|
@@ -604,45 +663,65 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
604
663
|
metadata: { [SET_METADATA_KEY]: manifest },
|
|
605
664
|
};
|
|
606
665
|
if (options.agency !== undefined) body.agency = options.agency;
|
|
666
|
+
report("commit", 0, 1);
|
|
607
667
|
const { proof, recovered } = await commitUnderSlot(t, body, artifactDigestB64, slot);
|
|
668
|
+
report("commit", 1, 1);
|
|
608
669
|
|
|
609
670
|
// The manifest is verified by a reader before the proof is called a set proof.
|
|
610
671
|
const verification = await verifyFuse({ proof, bytes: manifestBytes });
|
|
611
672
|
if (verification.category !== "FUSED_DIRECT" || verification.placement !== "set/1") {
|
|
612
673
|
throw new FuseError("verification-failed", `the returned proof does not verify as a set: ${verification.category}${verification.reason ? ` (${verification.reason})` : ""}`);
|
|
613
674
|
}
|
|
614
|
-
// The echo is unsigned and advisory. Absent is normal
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
675
|
+
// The echo is unsigned and advisory. Absent is normal for a boundary that
|
|
676
|
+
// drops metadata on a held-slot commit (enclaves before v6, and a proxy
|
|
677
|
+
// that does not forward it); differing means a boundary rewrote the
|
|
678
|
+
// response.
|
|
618
679
|
const echoed = readSetMetadata(proof);
|
|
619
680
|
if (echoed !== null && !bytesEqual(echoed, manifestBytes)) {
|
|
620
681
|
throw new FuseError("verification-failed", `the returned proof echoes a set manifest under metadata["${SET_METADATA_KEY}"] that differs from the committed one`);
|
|
621
682
|
}
|
|
622
683
|
const manifestEchoed = echoed !== null;
|
|
623
|
-
// Every member
|
|
624
|
-
|
|
684
|
+
// Every member is bound to the returned proof by its row: the manifest the
|
|
685
|
+
// proof commits (verified FUSED_DIRECT above) is parsed strictly, and each
|
|
686
|
+
// member's computed fused digest, origin and placement must sit in it. No
|
|
687
|
+
// member's bytes are read again. With verifyMembers the full verifier runs
|
|
688
|
+
// over each member's fused bytes as well, against the explicit manifest
|
|
689
|
+
// bytes so no verdict depends on the echo, and its verdict is returned.
|
|
690
|
+
const parsed = parseSetManifest(manifestBytes);
|
|
691
|
+
if (parsed === null) throw new FuseError("verification-failed", "the committed manifest does not parse as a set manifest");
|
|
692
|
+
const rowIndex = new Map<string, number>();
|
|
693
|
+
parsed.members.forEach((row, k) => rowIndex.set(bytesToHex(row.artifact), k));
|
|
625
694
|
const results: FuseSetMemberResult[] = [];
|
|
626
695
|
for (let i = 0; i < checked.length; i++) {
|
|
627
696
|
const c = checked[i]!;
|
|
628
|
-
const
|
|
629
|
-
const
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
697
|
+
const row = rows[i]!;
|
|
698
|
+
const k = rowIndex.get(bytesToHex(row.artifact));
|
|
699
|
+
const listed = k !== undefined ? parsed.members[k] : undefined;
|
|
700
|
+
if (k === undefined || listed === undefined || !bytesEqual(listed.origin, row.origin) || listed.placement !== row.placement) {
|
|
701
|
+
throw new FuseError("verification-failed", `member ${i}: the committed manifest does not list this member's fused digest with its origin and placement`, null, i);
|
|
702
|
+
}
|
|
703
|
+
const memberArtifactB64 = bytesToBase64(row.artifact);
|
|
704
|
+
let verification: FuseMemberResult | undefined;
|
|
705
|
+
if (verifyMembers) {
|
|
706
|
+
const v = await verifyFuseMember({ proof, bytes: fusedBytes[i]!, manifest: manifestBytes });
|
|
707
|
+
const member = v.set?.member ?? null;
|
|
708
|
+
if (v.category !== "SET_MEMBER_DIRECT" || member === null || member.fusedDigestB64 !== memberArtifactB64 || member.index !== k) {
|
|
709
|
+
throw new FuseError("verification-failed", `member ${i}: the returned proof does not verify this member: ${v.category}${v.reason ? ` (${v.reason})` : ""}`, null, i);
|
|
710
|
+
}
|
|
711
|
+
verification = v;
|
|
712
|
+
report("verify", i + 1, checked.length);
|
|
634
713
|
}
|
|
635
714
|
const names = c.name !== null ? fusedNamesFor(c.name, c.id) : null;
|
|
636
715
|
results.push({
|
|
637
716
|
index: i,
|
|
638
|
-
manifestIndex:
|
|
717
|
+
manifestIndex: k,
|
|
639
718
|
placement: c.id,
|
|
640
719
|
originDigestB64: bytesToBase64(c.originDigest),
|
|
641
720
|
artifactDigestB64: memberArtifactB64,
|
|
642
721
|
fusedName: names?.fusedName ?? null,
|
|
643
722
|
frameName: names?.frameName ?? null,
|
|
644
|
-
...(keep ? { fusedBytes:
|
|
645
|
-
verification:
|
|
723
|
+
...(keep ? { fusedBytes: fusedBytes[i]! } : {}),
|
|
724
|
+
...(verification !== undefined ? { verification } : {}),
|
|
646
725
|
});
|
|
647
726
|
}
|
|
648
727
|
return {
|
package/src/index.ts
CHANGED
|
@@ -42,7 +42,7 @@ export { Constructor } from "./constructor.js";
|
|
|
42
42
|
// same slot. The resulting proof is ordinary bitgraph/1.
|
|
43
43
|
export { fuse, fuseSet, MAX_SET_MEMBERS, builderFor, FuseError, digestFromBase64, placementForBytes, fusedNamesFor } from "./fuse.js";
|
|
44
44
|
export type { FuseBuilder, BuilderInput, FuseOptions, FuseResult, FuseTransport, FuseErrorCode } from "./fuse.js";
|
|
45
|
-
export type { FuseSetMember, SetMemberPlacement, FuseSetOptions, FuseSetMemberResult, FuseSetResult } from "./fuse.js";
|
|
45
|
+
export type { FuseSetMember, SetMemberPlacement, FuseSetOptions, FuseSetProgress, FuseSetMemberResult, FuseSetResult } from "./fuse.js";
|
|
46
46
|
// The verify-package types those results are made of, so the core entry names everything it returns.
|
|
47
47
|
export type { FuseFrame, PlacementId, SetManifest, FuseMemberResult, FuseVerifyResult } from "@mikeargento/bitgraph-verify";
|
|
48
48
|
|