@mikeargento/bitgraph 1.2.0 → 1.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/dist/__tests__/audit-memory-ingest.test.js +15 -0
- package/dist/__tests__/audit-memory-ingest.test.js.map +1 -1
- package/dist/__tests__/fuse-placement.test.d.ts +2 -0
- package/dist/__tests__/fuse-placement.test.d.ts.map +1 -0
- package/dist/__tests__/fuse-placement.test.js +31 -0
- package/dist/__tests__/fuse-placement.test.js.map +1 -0
- package/dist/__tests__/fuse-set-sdk.test.d.ts +2 -0
- package/dist/__tests__/fuse-set-sdk.test.d.ts.map +1 -0
- package/dist/__tests__/fuse-set-sdk.test.js +513 -0
- package/dist/__tests__/fuse-set-sdk.test.js.map +1 -0
- package/dist/__tests__/fuse-set.test.d.ts +2 -0
- package/dist/__tests__/fuse-set.test.d.ts.map +1 -0
- package/dist/__tests__/fuse-set.test.js +863 -0
- package/dist/__tests__/fuse-set.test.js.map +1 -0
- package/dist/canonical.d.ts +54 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +119 -0
- package/dist/canonical.js.map +1 -0
- package/dist/fuse-cli.js +111 -32
- package/dist/fuse-cli.js.map +1 -1
- package/dist/fuse.d.ts +94 -3
- package/dist/fuse.d.ts.map +1 -1
- package/dist/fuse.js +275 -48
- package/dist/fuse.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/proof-hash.d.ts +9 -0
- package/dist/proof-hash.d.ts.map +1 -0
- package/dist/proof-hash.js +75 -0
- package/dist/proof-hash.js.map +1 -0
- package/dist/types.d.ts +705 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/verifier.d.ts +27 -0
- package/dist/verifier.d.ts.map +1 -0
- package/dist/verifier.js +844 -0
- package/dist/verifier.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/audit-memory-ingest.test.ts +16 -0
- package/src/__tests__/fuse-placement.test.ts +32 -0
- package/src/__tests__/fuse-set-sdk.test.ts +638 -0
- package/src/__tests__/fuse-set.test.ts +1000 -0
- package/src/fuse-cli.ts +97 -27
- package/src/fuse.ts +356 -46
- package/src/index.ts +4 -1
package/src/fuse-cli.ts
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* bitgraph-fuse: the internal harness (spec 9.2), as a command rather than a
|
|
6
6
|
* page. Exercises Forms A, B and C end to end through fuse(), writes the
|
|
7
7
|
* Frame (and the fused bytes when they must be kept), and prints the bounded
|
|
8
|
-
* copy of spec 9.3. `
|
|
9
|
-
*
|
|
8
|
+
* copy of spec 9.3. `set` runs fuseSet() over N files under one slot and
|
|
9
|
+
* writes the proof beside the manifest bytes. `check` runs verifyFuse over a
|
|
10
|
+
* Frame (or bare proof) and a file, or verifyFuseMember when the proof is a
|
|
11
|
+
* set proof and the file is not its manifest, so every verification path can
|
|
12
|
+
* be exercised from a shell.
|
|
10
13
|
*
|
|
11
14
|
* Not a product surface. The site has no /fuse page: a page would make the
|
|
12
15
|
* website build depend on packages that are not published, which is a deploy
|
|
@@ -18,9 +21,9 @@
|
|
|
18
21
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
19
22
|
import { basename, extname, join, resolve } from "node:path";
|
|
20
23
|
import { sha256 } from "@noble/hashes/sha256";
|
|
21
|
-
import { parseFrame, verifyFuse, bytesToBase64 } from "@mikeargento/bitgraph-verify";
|
|
22
|
-
import type { BitGraphProof, PlacementId } from "@mikeargento/bitgraph-verify";
|
|
23
|
-
import { fuse, builderFor, FuseError } from "./fuse.js";
|
|
24
|
+
import { parseFrame, verifyFuse, verifyFuseMember, bytesToBase64 } from "@mikeargento/bitgraph-verify";
|
|
25
|
+
import type { BitGraphProof, PlacementId, FuseVerifyResult, FuseMemberResult } from "@mikeargento/bitgraph-verify";
|
|
26
|
+
import { fuse, fuseSet, builderFor, placementForBytes, fusedNamesFor, FuseError } from "./fuse.js";
|
|
24
27
|
import type { FuseTransport } from "./fuse.js";
|
|
25
28
|
|
|
26
29
|
const USAGE = `bitgraph-fuse: BitGraph producer harness (profile bitgraph-fuse/1, working name)
|
|
@@ -29,12 +32,14 @@ const USAGE = `bitgraph-fuse: BitGraph producer harness (profile bitgraph-fuse/1
|
|
|
29
32
|
Form A or B over an existing file. The file is never modified.
|
|
30
33
|
bitgraph-fuse produce [--origin <file>] [options]
|
|
31
34
|
Form C: a canonical payload naming an optional source.
|
|
32
|
-
bitgraph-fuse
|
|
33
|
-
|
|
35
|
+
bitgraph-fuse set <file>... [options]
|
|
36
|
+
N files under one slot; the committed artifact is the set manifest. The files are never modified.
|
|
37
|
+
bitgraph-fuse check <frame-or-proof.json> <file> [--manifest <set.manifest.json>] [--max-positions N]
|
|
38
|
+
Verify a Frame (or bare proof) against the fused bytes or the original; a set proof against a member or its original.
|
|
34
39
|
|
|
35
|
-
Options for fuse and
|
|
36
|
-
--out <dir> where to write the Frame and fused bytes (default: .)
|
|
37
|
-
--keep also write the fused bytes for byte-exact placements
|
|
40
|
+
Options for fuse, produce and set:
|
|
41
|
+
--out <dir> where to write the Frame (for set: set.proof.json and set.manifest.json) and fused bytes (default: .)
|
|
42
|
+
--keep also write the fused bytes for byte-exact placements (set: inputs need distinct names)
|
|
38
43
|
--base-url <url> commit surface (default https://bitgraph.ing)
|
|
39
44
|
--allocate-path <p> default /api/fuse/allocate (parent-direct: /allocate-slot)
|
|
40
45
|
--commit-path <p> default /api/fuse/commit (parent-direct: /commit)
|
|
@@ -78,6 +83,10 @@ function transportFrom(flags: Map<string, string | true>): FuseTransport {
|
|
|
78
83
|
return t;
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
function outDirOf(args: Args): string {
|
|
87
|
+
return resolve(typeof args.flags.get("out") === "string" ? (args.flags.get("out") as string) : ".");
|
|
88
|
+
}
|
|
89
|
+
|
|
81
90
|
async function writeOutputs(outDir: string, label: string, frame: unknown, fusedName: string | null, fused: Uint8Array | undefined): Promise<string[]> {
|
|
82
91
|
await mkdir(outDir, { recursive: true });
|
|
83
92
|
const written: string[] = [];
|
|
@@ -102,8 +111,7 @@ async function runFuse(args: Args): Promise<number> {
|
|
|
102
111
|
const fusedName = `${label.slice(0, label.length - extname(label).length)}.fused${ext}`;
|
|
103
112
|
const keep = args.flags.get("keep") === true;
|
|
104
113
|
const r = await fuse(builderFor(placement as PlacementId, original), { placement: placement as PlacementId, original, fusedFile: fusedName, keepFused: keep, transport: transportFrom(args.flags) });
|
|
105
|
-
const
|
|
106
|
-
const written = await writeOutputs(outDir, label, r.frame, keep ? fusedName : null, r.fusedBytes);
|
|
114
|
+
const written = await writeOutputs(outDirOf(args), label, r.frame, keep ? fusedName : null, r.fusedBytes);
|
|
107
115
|
report(r.proof, r.verification.category, r.recovered, true);
|
|
108
116
|
process.stdout.write(`\nwrote:\n${written.map((w) => " " + w).join("\n")}\n`);
|
|
109
117
|
if (!keep) process.stdout.write(`\nThe fused bytes were not kept (${placement} is byte-exact: any verifier rebuilds them from the original and the proof). Pass --keep to write them.\n`);
|
|
@@ -114,13 +122,56 @@ async function runProduce(args: Args): Promise<number> {
|
|
|
114
122
|
const originPath = args.flags.get("origin");
|
|
115
123
|
const originDigest = typeof originPath === "string" ? sha256(new Uint8Array(await readFile(resolve(originPath)))) : undefined;
|
|
116
124
|
const r = await fuse(builderFor("produced/1"), { placement: "produced/1", ...(originDigest !== undefined ? { originDigest } : {}), fusedFile: "produced.json", transport: transportFrom(args.flags) });
|
|
117
|
-
const
|
|
118
|
-
const written = await writeOutputs(outDir, "produced", r.frame, "produced.json", r.fusedBytes);
|
|
125
|
+
const written = await writeOutputs(outDirOf(args), "produced", r.frame, "produced.json", r.fusedBytes);
|
|
119
126
|
report(r.proof, r.verification.category, r.recovered, originDigest !== undefined);
|
|
120
127
|
process.stdout.write(`\nwrote:\n${written.map((w) => " " + w).join("\n")}\n`);
|
|
121
128
|
return 0;
|
|
122
129
|
}
|
|
123
130
|
|
|
131
|
+
/** N files under one slot. Writes the proof and the manifest bytes exactly (they are the committed artifact, so `check` can hash them as it). */
|
|
132
|
+
async function runSet(args: Args): Promise<number> {
|
|
133
|
+
if (args.positional.length === 0) { process.stderr.write(USAGE); return 64; }
|
|
134
|
+
const keep = args.flags.get("keep") === true;
|
|
135
|
+
const members = [];
|
|
136
|
+
for (const file of args.positional) {
|
|
137
|
+
const original = new Uint8Array(await readFile(resolve(file)));
|
|
138
|
+
members.push({ original, placement: placementForBytes(original), name: sanitize(basename(file)) });
|
|
139
|
+
}
|
|
140
|
+
// Under --keep every member's fused bytes go to a file named from its own
|
|
141
|
+
// name and placement; two inputs that would share that name are refused
|
|
142
|
+
// here, before any allocation, rather than one silently overwriting the other.
|
|
143
|
+
if (keep) {
|
|
144
|
+
const seen = new Map<string, number>();
|
|
145
|
+
for (const [i, m] of members.entries()) {
|
|
146
|
+
const { fusedName } = fusedNamesFor(m.name, m.placement);
|
|
147
|
+
const j = seen.get(fusedName);
|
|
148
|
+
if (j !== undefined) { process.stderr.write(`set --keep: members ${j} and ${i} would both be written as ${fusedName}; give the files distinct names\n`); return 64; }
|
|
149
|
+
seen.set(fusedName, i);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const r = await fuseSet(members, { keepFused: keep, transport: transportFrom(args.flags) });
|
|
153
|
+
const outDir = outDirOf(args);
|
|
154
|
+
await mkdir(outDir, { recursive: true });
|
|
155
|
+
const written: string[] = [];
|
|
156
|
+
const proofPath = join(outDir, "set.proof.json");
|
|
157
|
+
await writeFile(proofPath, JSON.stringify(r.proof, null, 2) + "\n");
|
|
158
|
+
written.push(proofPath);
|
|
159
|
+
const manifestPath = join(outDir, "set.manifest.json");
|
|
160
|
+
await writeFile(manifestPath, r.manifestBytes);
|
|
161
|
+
written.push(manifestPath);
|
|
162
|
+
for (const m of r.members) {
|
|
163
|
+
if (m.fusedBytes === undefined || m.fusedName === null) continue;
|
|
164
|
+
const p = join(outDir, m.fusedName);
|
|
165
|
+
await writeFile(p, m.fusedBytes);
|
|
166
|
+
written.push(p);
|
|
167
|
+
}
|
|
168
|
+
report(r.proof, r.verification.category, r.recovered, false);
|
|
169
|
+
for (const m of r.members) process.stdout.write(`member ${m.index} row ${m.manifestIndex} ${m.placement} ${m.verification.category} ${m.fusedName ?? ""}\n`);
|
|
170
|
+
if (!r.manifestEchoed) process.stdout.write(`note the proof does not carry the manifest; keep set.manifest.json beside it\n`);
|
|
171
|
+
process.stdout.write(`\nwrote:\n${written.map((w) => " " + w).join("\n")}\n`);
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
124
175
|
function report(proof: BitGraphProof, category: string, recovered: boolean, hasOrigin: boolean): void {
|
|
125
176
|
const c = proof.commit;
|
|
126
177
|
if (hasOrigin) process.stdout.write(COPY_ORIGINAL + "\n\n");
|
|
@@ -134,6 +185,22 @@ function report(proof: BitGraphProof, category: string, recovered: boolean, hasO
|
|
|
134
185
|
if (recovered) process.stdout.write(`note the commit response was lost; the proof was read back by digest and matched on the held slot\n`);
|
|
135
186
|
}
|
|
136
187
|
|
|
188
|
+
/** The verdict lines shared by both verifiers; the `set` line appears only for a member verdict. */
|
|
189
|
+
function printVerdict(r: FuseVerifyResult | FuseMemberResult): void {
|
|
190
|
+
process.stdout.write(`category ${r.category}\n`);
|
|
191
|
+
process.stdout.write(`proof ${r.proof.valid ? "valid" : `invalid: ${r.proof.reason ?? ""}`}\n`);
|
|
192
|
+
process.stdout.write(`file digest ${r.fileDigestB64}\n`);
|
|
193
|
+
process.stdout.write(`artifact ${r.artifactDigestB64}\n`);
|
|
194
|
+
if (r.originDigestB64) process.stdout.write(`origin ${r.originDigestB64}\n`);
|
|
195
|
+
if (r.placement) process.stdout.write(`placement ${r.placement}\n`);
|
|
196
|
+
if (r.span) process.stdout.write(`span slot ${r.span.slotCounter} to commit ${r.span.commitCounter} (${r.span.positions} positions)\n`);
|
|
197
|
+
if (r.policy.maxPositions !== null) process.stdout.write(`span policy ${r.policy.spanExceeded ? "EXCEEDED" : "within"} ${r.policy.maxPositions} positions\n`);
|
|
198
|
+
if ("set" in r && r.set?.member) process.stdout.write(`set member ${r.set.member.index + 1} of ${r.set.memberCount}\n`);
|
|
199
|
+
if (r.reason) process.stdout.write(`reason ${r.reason}\n`);
|
|
200
|
+
for (const s of r.statements) process.stdout.write(`\n${s}\n`);
|
|
201
|
+
process.stdout.write(`\nfloor computed by the Player from anchors in a bundle (bitgraph-play check); not available here\n`);
|
|
202
|
+
}
|
|
203
|
+
|
|
137
204
|
async function runCheck(args: Args): Promise<number> {
|
|
138
205
|
const [frameArg, fileArg] = args.positional;
|
|
139
206
|
if (frameArg === undefined || fileArg === undefined) { process.stderr.write(USAGE); return 64; }
|
|
@@ -148,18 +215,20 @@ async function runCheck(args: Args): Promise<number> {
|
|
|
148
215
|
}
|
|
149
216
|
const bytes = new Uint8Array(await readFile(resolve(fileArg)));
|
|
150
217
|
const max = args.flags.get("max-positions");
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
218
|
+
const policy = typeof max === "string" ? { maxPositions: BigInt(max) } : {};
|
|
219
|
+
// A set proof and a file that is not its manifest: the member verifier answers.
|
|
220
|
+
const a = proof.attribution;
|
|
221
|
+
if (a?.name === "bitgraph-fuse/1" && a.title === "set/1" && bytesToBase64(sha256(bytes)) !== proof.artifact.digestB64) {
|
|
222
|
+
const manifestArg = args.flags.get("manifest");
|
|
223
|
+
const manifest = typeof manifestArg === "string" ? new Uint8Array(await readFile(resolve(manifestArg))) : undefined;
|
|
224
|
+
const r = await verifyFuseMember({ proof, bytes, ...(manifest !== undefined ? { manifest } : {}), ...policy });
|
|
225
|
+
printVerdict(r);
|
|
226
|
+
if (r.category === "SET_MEMBER_DIRECT" || r.category === "SET_MEMBER_FROM_ORIGIN") return r.policy.spanExceeded ? 1 : 0;
|
|
227
|
+
if (r.category === "UNDETERMINED_PLACEMENT" || r.category === "NO_MATCH") return 2;
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
const r = await verifyFuse({ proof, bytes, frame, ...policy });
|
|
231
|
+
printVerdict(r);
|
|
163
232
|
if (r.category === "RECORDED" || r.category === "FUSED_DIRECT" || r.category === "FUSED_FROM_ORIGIN") return r.policy.spanExceeded ? 1 : 0;
|
|
164
233
|
if (r.category === "UNDETERMINED_PLACEMENT" || r.category === "NO_MATCH") return 2;
|
|
165
234
|
return 1;
|
|
@@ -171,12 +240,13 @@ async function main(): Promise<number> {
|
|
|
171
240
|
switch (args.command) {
|
|
172
241
|
case "fuse": return await runFuse(args);
|
|
173
242
|
case "produce": return await runProduce(args);
|
|
243
|
+
case "set": return await runSet(args);
|
|
174
244
|
case "check": return await runCheck(args);
|
|
175
245
|
default: process.stderr.write(USAGE); return 64;
|
|
176
246
|
}
|
|
177
247
|
} catch (err) {
|
|
178
248
|
if (err instanceof FuseError) {
|
|
179
|
-
process.stderr.write(`no fused proof was completed (${err.code}${err.status !== null ? `, ${err.status}` : ""}): ${err.message}\n`);
|
|
249
|
+
process.stderr.write(`no fused proof was completed (${err.code}${err.status !== null ? `, ${err.status}` : ""}${err.member !== null ? `, member ${err.member}` : ""}): ${err.message}\n`);
|
|
180
250
|
return err.code === "tee-restarting" ? 2 : 1;
|
|
181
251
|
}
|
|
182
252
|
process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}\n`);
|