@mikeargento/bitgraph 1.6.0 → 1.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/dist/__tests__/fuse-placement.test.js +10 -10
- package/dist/__tests__/fuse-roundtrip.test.js +71 -2
- package/dist/__tests__/fuse-roundtrip.test.js.map +1 -1
- package/dist/__tests__/fuse-set-sdk.test.js +44 -1
- package/dist/__tests__/fuse-set-sdk.test.js.map +1 -1
- package/dist/__tests__/fuse-set.test.js +7 -2
- package/dist/__tests__/fuse-set.test.js.map +1 -1
- package/dist/__tests__/fuse-set2.test.d.ts +2 -0
- package/dist/__tests__/fuse-set2.test.d.ts.map +1 -0
- package/dist/__tests__/fuse-set2.test.js +294 -0
- package/dist/__tests__/fuse-set2.test.js.map +1 -0
- package/dist/fuse-cli.js +2 -2
- package/dist/fuse-cli.js.map +1 -1
- package/dist/fuse.d.ts +30 -10
- package/dist/fuse.d.ts.map +1 -1
- package/dist/fuse.js +68 -23
- package/dist/fuse.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/fuse-placement.test.ts +10 -10
- package/src/__tests__/fuse-roundtrip.test.ts +71 -2
- package/src/__tests__/fuse-set-sdk.test.ts +47 -3
- package/src/__tests__/fuse-set.test.ts +7 -2
- package/src/__tests__/fuse-set2.test.ts +343 -0
- package/src/fuse-cli.ts +2 -2
- package/src/fuse.ts +93 -39
- package/src/index.ts +1 -0
package/src/fuse.ts
CHANGED
|
@@ -39,6 +39,14 @@ import { sha256 } from "@noble/hashes/sha256";
|
|
|
39
39
|
import {
|
|
40
40
|
buildFrame,
|
|
41
41
|
buildSetManifest,
|
|
42
|
+
buildSetMemberProof,
|
|
43
|
+
buildSetRoot,
|
|
44
|
+
buildSetTree,
|
|
45
|
+
parseSetRoot,
|
|
46
|
+
SET2_PLACEMENT_ID,
|
|
47
|
+
SET_PLACEMENT_ID as SET_PLACEMENT_ID_LOCAL,
|
|
48
|
+
setRootFromMember,
|
|
49
|
+
MAX_SET2_MEMBERS,
|
|
42
50
|
bytesEqual,
|
|
43
51
|
bytesToBase64,
|
|
44
52
|
bytesToHex,
|
|
@@ -55,18 +63,7 @@ import {
|
|
|
55
63
|
verifyFuseMember,
|
|
56
64
|
base64ToBytes,
|
|
57
65
|
} from "@mikeargento/bitgraph-verify";
|
|
58
|
-
import type {
|
|
59
|
-
BitGraphProof,
|
|
60
|
-
FuseFrame,
|
|
61
|
-
FuseMemberResult,
|
|
62
|
-
FuseVerifyResult,
|
|
63
|
-
Located,
|
|
64
|
-
Placement,
|
|
65
|
-
PlacementId,
|
|
66
|
-
SetManifest,
|
|
67
|
-
SetMember,
|
|
68
|
-
SlotAllocation,
|
|
69
|
-
} from "@mikeargento/bitgraph-verify";
|
|
66
|
+
import type { BitGraphProof, FuseFrame, FuseMemberResult, FuseVerifyResult, Located, Placement, PlacementId, SetManifest, SetMember, SetMemberProof, SetRoot, SlotAllocation } from "@mikeargento/bitgraph-verify";
|
|
70
67
|
|
|
71
68
|
/**
|
|
72
69
|
* SHA-256 over bytes: the platform's native hasher when one is present
|
|
@@ -186,12 +183,14 @@ export class FuseError extends Error {
|
|
|
186
183
|
* safe only where decoders stop at an end marker or read by declared sizes:
|
|
187
184
|
* JPEG (EOI), PNG (IEND), GIF (0x3B), TIFF and the TIFF-based raws such as
|
|
188
185
|
* DNG, CR2, NEF, ARW (offset tables), BMP and RIFF containers such as WebP,
|
|
189
|
-
* WAV, AVI (declared sizes). Everything else goes into `container/
|
|
190
|
-
* that carries the original untouched
|
|
186
|
+
* WAV, AVI (declared sizes). Everything else goes into `container/2`, a tar
|
|
187
|
+
* that carries the original untouched and FIRST, so a scanner can hash it
|
|
188
|
+
* once and finish the fused digest later: PDF, ZIP-based documents, ISO base
|
|
191
189
|
* media video and images, Matroska, MP3, structured and plain text, and any
|
|
192
|
-
* format not recognised here.
|
|
190
|
+
* format not recognised here. Artifacts made under `container/1` (the
|
|
191
|
+
* manifest first) stay readable; nothing new is made under it.
|
|
193
192
|
*/
|
|
194
|
-
export function placementForBytes(bytes: Uint8Array): "trailer/1" | "container/
|
|
193
|
+
export function placementForBytes(bytes: Uint8Array): "trailer/1" | "container/2" {
|
|
195
194
|
const at = (sig: number[], offset = 0): boolean => bytes.length >= offset + sig.length && sig.every((v, i) => bytes[offset + i] === v);
|
|
196
195
|
const trailerSafe =
|
|
197
196
|
at([0xff, 0xd8, 0xff]) || // JPEG
|
|
@@ -200,7 +199,7 @@ export function placementForBytes(bytes: Uint8Array): "trailer/1" | "container/1
|
|
|
200
199
|
at([0x49, 0x49, 0x2a, 0x00]) || at([0x4d, 0x4d, 0x00, 0x2a]) || // TIFF, DNG, CR2, NEF, ARW
|
|
201
200
|
(at([0x42, 0x4d]) && bytes.length >= 14) || // BMP
|
|
202
201
|
(at([0x52, 0x49, 0x46, 0x46]) && bytes.length >= 12); // RIFF: WebP, WAV, AVI
|
|
203
|
-
return trailerSafe ? "trailer/1" : "container/
|
|
202
|
+
return trailerSafe ? "trailer/1" : "container/2";
|
|
204
203
|
}
|
|
205
204
|
|
|
206
205
|
/** Names for a fused artifact and its Frame, from the original's name. */
|
|
@@ -209,7 +208,7 @@ export function fusedNamesFor(originalName: string, placement: PlacementId): { f
|
|
|
209
208
|
const stem = dot > 0 ? originalName.slice(0, dot) : originalName;
|
|
210
209
|
const ext = dot > 0 ? originalName.slice(dot) : "";
|
|
211
210
|
return {
|
|
212
|
-
fusedName: placement === "trailer/1" ? `${stem}.fused${ext}` : placement
|
|
211
|
+
fusedName: placement === "trailer/1" ? `${stem}.fused${ext}` : placement.startsWith("container/") ? `${stem}.fused.tar` : `${stem}.produced.json`,
|
|
213
212
|
frameName: `${originalName}.bitgraph-fuse.json`,
|
|
214
213
|
};
|
|
215
214
|
}
|
|
@@ -479,7 +478,7 @@ export async function fuse(builder: FuseBuilder, options: FuseOptions): Promise<
|
|
|
479
478
|
export const MAX_SET_MEMBERS = 2000;
|
|
480
479
|
|
|
481
480
|
/** The placements a set member takes: Forms A and B, one original per member. */
|
|
482
|
-
export type SetMemberPlacement = "trailer/1" | "container/1";
|
|
481
|
+
export type SetMemberPlacement = "trailer/1" | "container/1" | "container/2";
|
|
483
482
|
|
|
484
483
|
/** What a hashed member's fused digest is computed for: the held slot and its commitment. */
|
|
485
484
|
export interface FusedDigestInput {
|
|
@@ -559,15 +558,25 @@ export interface FuseSetProgress {
|
|
|
559
558
|
/**
|
|
560
559
|
* "hash": each member checked before any request (a bytes member's origin digest is taken here).
|
|
561
560
|
* "fuse": each member's fused digest taken, after the slot is held.
|
|
561
|
+
* "tree": set/2 only, 0 of 1 before the tree is built, 1 of 1 after.
|
|
562
562
|
* "commit": 0 of 1 before the request, 1 of 1 when the proof is back.
|
|
563
563
|
* "verify": only with verifyMembers, one per member.
|
|
564
564
|
*/
|
|
565
|
-
phase: "hash" | "fuse" | "commit" | "verify";
|
|
565
|
+
phase: "hash" | "fuse" | "tree" | "commit" | "verify";
|
|
566
566
|
done: number;
|
|
567
567
|
total: number;
|
|
568
568
|
}
|
|
569
569
|
|
|
570
570
|
export interface FuseSetOptions {
|
|
571
|
+
/**
|
|
572
|
+
* "set/1" (default): the committed artifact is the canonical manifest of
|
|
573
|
+
* every member, which rides in the commit and in the proof; at most
|
|
574
|
+
* MAX_SET_MEMBERS. "set/2": the committed artifact is the root of a Merkle
|
|
575
|
+
* tree over the same rows, a few hundred bytes whatever N is; each member
|
|
576
|
+
* gets its leaf index and path (see FuseSetMemberResult.path), which a
|
|
577
|
+
* reader needs beside the proof; at most MAX_SET2_MEMBERS.
|
|
578
|
+
*/
|
|
579
|
+
set?: "set/1" | "set/2";
|
|
571
580
|
/** Return each member's fused bytes. Default false: they are virtual, rebuilt from the original and the proof. A hashed member has none to return. */
|
|
572
581
|
keepFused?: boolean;
|
|
573
582
|
/**
|
|
@@ -604,14 +613,22 @@ export interface FuseSetMemberResult {
|
|
|
604
613
|
fusedBytes?: Uint8Array;
|
|
605
614
|
/** 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". */
|
|
606
615
|
verification?: FuseMemberResult;
|
|
616
|
+
/** set/2 only: the member's inclusion path, siblings from the leaf up; with manifestIndex and the set's count it is the member's evidence (memberProof). */
|
|
617
|
+
path?: Uint8Array[];
|
|
618
|
+
/** set/2 only: the member's evidence as its JSON object, ready to ride beside the proof or under proof.metadata[SET_MEMBER_METADATA_KEY]. */
|
|
619
|
+
memberProof?: SetMemberProof;
|
|
607
620
|
}
|
|
608
621
|
|
|
609
622
|
export interface FuseSetResult {
|
|
623
|
+
/** Which set kind was made. */
|
|
624
|
+
set: "set/1" | "set/2";
|
|
610
625
|
proof: BitGraphProof;
|
|
611
|
-
/** The committed artifact. Keep it beside the proof. */
|
|
626
|
+
/** The committed artifact: the set/1 manifest, or the set/2 root document. Keep it beside the proof. */
|
|
612
627
|
manifestBytes: Uint8Array;
|
|
613
|
-
/** JSON.parse of manifestBytes; the exact object sent under metadata. */
|
|
614
|
-
manifest: SetManifest;
|
|
628
|
+
/** JSON.parse of manifestBytes; the exact object sent under metadata. For set/2 a SetRoot. */
|
|
629
|
+
manifest: SetManifest | SetRoot;
|
|
630
|
+
/** set/2 only: the tree root, standard base64. */
|
|
631
|
+
treeRootB64?: string;
|
|
615
632
|
/** SHA-256 of manifestBytes; equals proof.artifact.digestB64. */
|
|
616
633
|
artifactDigestB64: string;
|
|
617
634
|
slotCommitmentB64: string;
|
|
@@ -636,7 +653,10 @@ export interface FuseSetResult {
|
|
|
636
653
|
export async function fuseSet(members: readonly FuseSetMember[], options: FuseSetOptions = {}): Promise<FuseSetResult> {
|
|
637
654
|
// 0. validate, before any request. A refusal here burns nothing.
|
|
638
655
|
if (!Array.isArray(members) || members.length === 0) throw new FuseError("bad-input", "a set lists at least one member");
|
|
639
|
-
|
|
656
|
+
const setKind = options.set ?? "set/1";
|
|
657
|
+
if (setKind !== "set/1" && setKind !== "set/2") throw new FuseError("bad-input", `set must be "set/1" or "set/2" (got ${String(setKind)})`);
|
|
658
|
+
const cap = setKind === "set/1" ? MAX_SET_MEMBERS : MAX_SET2_MEMBERS;
|
|
659
|
+
if (members.length > cap) throw new FuseError("bad-input", `a ${setKind} set lists at most ${cap} members (got ${members.length})`);
|
|
640
660
|
const keep = options.keepFused === true;
|
|
641
661
|
const verifyMembers = options.verifyMembers === true;
|
|
642
662
|
type Kind = "bytes" | "loaded" | "hashed";
|
|
@@ -672,7 +692,7 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
672
692
|
const id = m.placement ?? placementForBytes(m.original as Uint8Array);
|
|
673
693
|
const placement = getPlacement(id);
|
|
674
694
|
if (placement === undefined) throw new FuseError("bad-placement", `member ${i}: placement "${id}" is not registered`, null, i);
|
|
675
|
-
if (placement.form === "C") throw bad(i, `${id} takes no original; a set holds trailer/1 and container/
|
|
695
|
+
if (placement.form === "C") throw bad(i, `${id} takes no original; a set holds trailer/1, container/1 and container/2 members only`);
|
|
676
696
|
if (m.name !== undefined && typeof m.name !== "string") throw bad(i, "name must be a string");
|
|
677
697
|
if (m.builder !== undefined && typeof m.builder !== "function") throw bad(i, "builder must be a function");
|
|
678
698
|
if (kind !== "bytes" && !(m.originDigest instanceof Uint8Array && m.originDigest.length === 32)) throw bad(i, `a ${kind} member names its originDigest, 32 bytes`);
|
|
@@ -772,23 +792,32 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
772
792
|
report("fuse", i + 1, checked.length);
|
|
773
793
|
}
|
|
774
794
|
|
|
775
|
-
// 3. hash: the canonical manifest
|
|
795
|
+
// 3. hash: the committed artifact. set/1: the canonical manifest of every
|
|
796
|
+
// row. set/2: the root document over the Merkle tree of the same rows.
|
|
776
797
|
let manifestBytes: Uint8Array;
|
|
798
|
+
let tree: ReturnType<typeof buildSetTree> | null = null;
|
|
777
799
|
try {
|
|
778
|
-
|
|
800
|
+
if (setKind === "set/1") {
|
|
801
|
+
manifestBytes = buildSetManifest(commitment, rows);
|
|
802
|
+
} else {
|
|
803
|
+
report("tree", 0, 1);
|
|
804
|
+
tree = buildSetTree(rows);
|
|
805
|
+
manifestBytes = buildSetRoot(commitment, tree.sorted.length, tree.root);
|
|
806
|
+
report("tree", 1, 1);
|
|
807
|
+
}
|
|
779
808
|
} catch (err) {
|
|
780
|
-
throw new FuseError("bad-input", `the set manifest could not be built: ${err instanceof Error ? err.message : String(err)}; ${expiring}`);
|
|
809
|
+
throw new FuseError("bad-input", `the set ${setKind === "set/1" ? "manifest" : "root document"} could not be built: ${err instanceof Error ? err.message : String(err)}; ${expiring}`);
|
|
781
810
|
}
|
|
782
811
|
const artifactDigestB64 = bytesToBase64(await digest(manifestBytes));
|
|
783
|
-
const manifest = JSON.parse(new TextDecoder().decode(manifestBytes)) as SetManifest;
|
|
812
|
+
const manifest = JSON.parse(new TextDecoder().decode(manifestBytes)) as SetManifest | SetRoot;
|
|
784
813
|
|
|
785
|
-
// 4. fill: one commit, the parsed
|
|
814
|
+
// 4. fill: one commit, the parsed artifact riding along as unsigned metadata
|
|
786
815
|
const body: Record<string, unknown> = {
|
|
787
816
|
digests: [{ digestB64: artifactDigestB64, hashAlg: "sha256" }],
|
|
788
817
|
slotId: slot.nonceB64,
|
|
789
818
|
slot,
|
|
790
819
|
chainId: "bitgraph:main",
|
|
791
|
-
attribution: fuseAttribution("set/1"),
|
|
820
|
+
attribution: fuseAttribution(setKind === "set/1" ? SET_PLACEMENT_ID_LOCAL : SET2_PLACEMENT_ID),
|
|
792
821
|
metadata: { [SET_METADATA_KEY]: manifest },
|
|
793
822
|
};
|
|
794
823
|
if (options.agency !== undefined) body.agency = options.agency;
|
|
@@ -796,10 +825,10 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
796
825
|
const { proof, recovered } = await commitUnderSlot(t, body, artifactDigestB64, slot);
|
|
797
826
|
report("commit", 1, 1);
|
|
798
827
|
|
|
799
|
-
// The
|
|
828
|
+
// The committed artifact is verified by a reader before the proof is called a set proof.
|
|
800
829
|
const verification = await verifyFuse({ proof, bytes: manifestBytes });
|
|
801
|
-
if (verification.category !== "FUSED_DIRECT" || verification.placement !==
|
|
802
|
-
throw new FuseError("verification-failed", `the returned proof does not verify as a set: ${verification.category}${verification.reason ? ` (${verification.reason})` : ""}`);
|
|
830
|
+
if (verification.category !== "FUSED_DIRECT" || verification.placement !== setKind) {
|
|
831
|
+
throw new FuseError("verification-failed", `the returned proof does not verify as a ${setKind} set: ${verification.category}${verification.reason ? ` (${verification.reason})` : ""}`);
|
|
803
832
|
}
|
|
804
833
|
// The echo is unsigned and advisory. Absent is normal for a boundary that
|
|
805
834
|
// drops metadata on a held-slot commit (enclaves before v6, and a proxy
|
|
@@ -816,18 +845,39 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
816
845
|
// member's bytes are read again. With verifyMembers the full verifier runs
|
|
817
846
|
// over each member's fused bytes as well, against the explicit manifest
|
|
818
847
|
// bytes so no verdict depends on the echo, and its verdict is returned.
|
|
819
|
-
|
|
820
|
-
|
|
848
|
+
// set/1: the strictly parsed manifest lists each computed row. set/2: the
|
|
849
|
+
// strictly parsed root document states the count and the root the tree
|
|
850
|
+
// over these rows computed, and each member's path recomputes that root
|
|
851
|
+
// (the verifier's own check, run here once per member).
|
|
852
|
+
let listedRows: readonly SetMember[];
|
|
853
|
+
if (setKind === "set/1") {
|
|
854
|
+
const parsed = parseSetManifest(manifestBytes);
|
|
855
|
+
if (parsed === null) throw new FuseError("verification-failed", "the committed manifest does not parse as a set manifest");
|
|
856
|
+
listedRows = parsed.members;
|
|
857
|
+
} else {
|
|
858
|
+
const doc = parseSetRoot(manifestBytes);
|
|
859
|
+
if (doc === null || tree === null) throw new FuseError("verification-failed", "the committed root document does not parse as a set root");
|
|
860
|
+
if (doc.count !== tree.sorted.length || !bytesEqual(doc.root, tree.root)) throw new FuseError("verification-failed", "the committed root document does not state this tree's count and root");
|
|
861
|
+
listedRows = tree.sorted;
|
|
862
|
+
}
|
|
821
863
|
const rowIndex = new Map<string, number>();
|
|
822
|
-
|
|
864
|
+
listedRows.forEach((row, k) => rowIndex.set(bytesToHex(row.artifact), k));
|
|
823
865
|
const results: FuseSetMemberResult[] = [];
|
|
824
866
|
for (let i = 0; i < checked.length; i++) {
|
|
825
867
|
const c = checked[i]!;
|
|
826
868
|
const row = rows[i]!;
|
|
827
869
|
const k = rowIndex.get(bytesToHex(row.artifact));
|
|
828
|
-
const listed = k !== undefined ?
|
|
870
|
+
const listed = k !== undefined ? listedRows[k] : undefined;
|
|
829
871
|
if (k === undefined || listed === undefined || !bytesEqual(listed.origin, row.origin) || listed.placement !== row.placement) {
|
|
830
|
-
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);
|
|
872
|
+
throw new FuseError("verification-failed", `member ${i}: the committed ${setKind === "set/1" ? "manifest" : "tree"} does not list this member's fused digest with its origin and placement`, null, i);
|
|
873
|
+
}
|
|
874
|
+
let path: Uint8Array[] | undefined;
|
|
875
|
+
let memberProof: SetMemberProof | undefined;
|
|
876
|
+
if (tree !== null) {
|
|
877
|
+
path = tree.tree.path(k);
|
|
878
|
+
const reached = setRootFromMember(row, k, tree.sorted.length, path);
|
|
879
|
+
if (reached === null || !bytesEqual(reached, tree.root)) throw new FuseError("verification-failed", `member ${i}: its path does not recompute the committed root`, null, i);
|
|
880
|
+
memberProof = buildSetMemberProof(row, k, tree.sorted.length, path);
|
|
831
881
|
}
|
|
832
882
|
const memberArtifactB64 = bytesToBase64(row.artifact);
|
|
833
883
|
let verification: FuseMemberResult | undefined;
|
|
@@ -852,12 +902,16 @@ export async function fuseSet(members: readonly FuseSetMember[], options: FuseSe
|
|
|
852
902
|
frameName: names?.frameName ?? null,
|
|
853
903
|
...(keep && held !== null ? { fusedBytes: held } : {}),
|
|
854
904
|
...(verification !== undefined ? { verification } : {}),
|
|
905
|
+
...(path !== undefined ? { path } : {}),
|
|
906
|
+
...(memberProof !== undefined ? { memberProof } : {}),
|
|
855
907
|
});
|
|
856
908
|
}
|
|
857
909
|
return {
|
|
910
|
+
set: setKind,
|
|
858
911
|
proof,
|
|
859
912
|
manifestBytes,
|
|
860
913
|
manifest,
|
|
914
|
+
...(tree !== null ? { treeRootB64: bytesToBase64(tree.root) } : {}),
|
|
861
915
|
artifactDigestB64,
|
|
862
916
|
slotCommitmentB64: bytesToBase64(commitment),
|
|
863
917
|
members: results,
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ export { Constructor } from "./constructor.js";
|
|
|
41
41
|
// slot, write a commitment to it into the artifact, hash, commit under the
|
|
42
42
|
// same slot. The resulting proof is ordinary bitgraph/1.
|
|
43
43
|
export { fuse, fuseSet, MAX_SET_MEMBERS, trailerBytesFor, builderFor, FuseError, digestFromBase64, placementForBytes, fusedNamesFor } from "./fuse.js";
|
|
44
|
+
export { MAX_SET2_MEMBERS, SET2_PLACEMENT_ID, SET_MEMBER_METADATA_KEY } from "@mikeargento/bitgraph-verify";
|
|
44
45
|
export type { FuseBuilder, BuilderInput, FuseOptions, FuseResult, FuseTransport, FuseErrorCode } from "./fuse.js";
|
|
45
46
|
export type { FuseSetMember, FuseSetBytesMember, FuseSetLoadedMember, FuseSetHashedMember, FusedDigestInput, SetMemberPlacement, FuseSetOptions, FuseSetProgress, FuseSetMemberResult, FuseSetResult } from "./fuse.js";
|
|
46
47
|
// The verify-package types those results are made of, so the core entry names everything it returns.
|