@mikeargento/bitgraph 1.9.0 → 1.9.2
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/LICENSE +1 -1
- package/README.md +58 -35
- package/dist/constructor.js +1 -1
- package/dist/constructor.js.map +1 -1
- package/dist/fuse-cli.js +1 -1
- package/dist/fuse-cli.js.map +1 -1
- package/dist/fuse.js +1 -1
- package/dist/fuse.js.map +1 -1
- package/dist/host.js +1 -1
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/policy.js +1 -1
- package/dist/policy.js.map +1 -1
- package/package.json +23 -8
- package/src/constructor.ts +1 -1
- package/src/fuse-cli.ts +1 -1
- package/src/fuse.ts +1 -1
- package/src/host.ts +1 -1
- package/src/index.ts +3 -2
- package/src/policy.ts +1 -1
- package/dist/__tests__/audit-anchors.test.d.ts +0 -2
- package/dist/__tests__/audit-anchors.test.d.ts.map +0 -1
- package/dist/__tests__/audit-anchors.test.js +0 -152
- package/dist/__tests__/audit-anchors.test.js.map +0 -1
- package/dist/__tests__/audit-anomalies.test.d.ts +0 -2
- package/dist/__tests__/audit-anomalies.test.d.ts.map +0 -1
- package/dist/__tests__/audit-anomalies.test.js +0 -663
- package/dist/__tests__/audit-anomalies.test.js.map +0 -1
- package/dist/__tests__/audit-attestation.test.d.ts +0 -2
- package/dist/__tests__/audit-attestation.test.d.ts.map +0 -1
- package/dist/__tests__/audit-attestation.test.js +0 -403
- package/dist/__tests__/audit-attestation.test.js.map +0 -1
- package/dist/__tests__/audit-bench-smoke.test.d.ts +0 -2
- package/dist/__tests__/audit-bench-smoke.test.d.ts.map +0 -1
- package/dist/__tests__/audit-bench-smoke.test.js +0 -87
- package/dist/__tests__/audit-bench-smoke.test.js.map +0 -1
- package/dist/__tests__/audit-boundary.test.d.ts +0 -20
- package/dist/__tests__/audit-boundary.test.d.ts.map +0 -1
- package/dist/__tests__/audit-boundary.test.js +0 -84
- package/dist/__tests__/audit-boundary.test.js.map +0 -1
- package/dist/__tests__/audit-cli.test.d.ts +0 -2
- package/dist/__tests__/audit-cli.test.d.ts.map +0 -1
- package/dist/__tests__/audit-cli.test.js +0 -239
- package/dist/__tests__/audit-cli.test.js.map +0 -1
- package/dist/__tests__/audit-dependencies.test.d.ts +0 -2
- package/dist/__tests__/audit-dependencies.test.d.ts.map +0 -1
- package/dist/__tests__/audit-dependencies.test.js +0 -116
- package/dist/__tests__/audit-dependencies.test.js.map +0 -1
- package/dist/__tests__/audit-fixtures.d.ts +0 -265
- package/dist/__tests__/audit-fixtures.d.ts.map +0 -1
- package/dist/__tests__/audit-fixtures.js +0 -511
- package/dist/__tests__/audit-fixtures.js.map +0 -1
- package/dist/__tests__/audit-ingest.test.d.ts +0 -2
- package/dist/__tests__/audit-ingest.test.d.ts.map +0 -1
- package/dist/__tests__/audit-ingest.test.js +0 -473
- package/dist/__tests__/audit-ingest.test.js.map +0 -1
- package/dist/__tests__/audit-memory-ingest.test.d.ts +0 -12
- package/dist/__tests__/audit-memory-ingest.test.d.ts.map +0 -1
- package/dist/__tests__/audit-memory-ingest.test.js +0 -135
- package/dist/__tests__/audit-memory-ingest.test.js.map +0 -1
- package/dist/__tests__/audit-property.test.d.ts +0 -2
- package/dist/__tests__/audit-property.test.d.ts.map +0 -1
- package/dist/__tests__/audit-property.test.js +0 -771
- package/dist/__tests__/audit-property.test.js.map +0 -1
- package/dist/__tests__/audit-real-fixtures.test.d.ts +0 -2
- package/dist/__tests__/audit-real-fixtures.test.d.ts.map +0 -1
- package/dist/__tests__/audit-real-fixtures.test.js +0 -132
- package/dist/__tests__/audit-real-fixtures.test.js.map +0 -1
- package/dist/__tests__/audit-real-live.test.d.ts +0 -2
- package/dist/__tests__/audit-real-live.test.d.ts.map +0 -1
- package/dist/__tests__/audit-real-live.test.js +0 -107
- package/dist/__tests__/audit-real-live.test.js.map +0 -1
- package/dist/__tests__/audit-reconstruct.test.d.ts +0 -2
- package/dist/__tests__/audit-reconstruct.test.d.ts.map +0 -1
- package/dist/__tests__/audit-reconstruct.test.js +0 -287
- package/dist/__tests__/audit-reconstruct.test.js.map +0 -1
- package/dist/__tests__/audit-report.test.d.ts +0 -2
- package/dist/__tests__/audit-report.test.d.ts.map +0 -1
- package/dist/__tests__/audit-report.test.js +0 -186
- package/dist/__tests__/audit-report.test.js.map +0 -1
- package/dist/__tests__/audit-roundtrip.test.d.ts +0 -2
- package/dist/__tests__/audit-roundtrip.test.d.ts.map +0 -1
- package/dist/__tests__/audit-roundtrip.test.js +0 -368
- package/dist/__tests__/audit-roundtrip.test.js.map +0 -1
- package/dist/__tests__/audit-temporal.test.d.ts +0 -2
- package/dist/__tests__/audit-temporal.test.d.ts.map +0 -1
- package/dist/__tests__/audit-temporal.test.js +0 -364
- package/dist/__tests__/audit-temporal.test.js.map +0 -1
- package/dist/__tests__/audit-tiers.test.d.ts +0 -2
- package/dist/__tests__/audit-tiers.test.d.ts.map +0 -1
- package/dist/__tests__/audit-tiers.test.js +0 -282
- package/dist/__tests__/audit-tiers.test.js.map +0 -1
- package/dist/__tests__/audit-witness-real.test.d.ts +0 -19
- package/dist/__tests__/audit-witness-real.test.d.ts.map +0 -1
- package/dist/__tests__/audit-witness-real.test.js +0 -82
- package/dist/__tests__/audit-witness-real.test.js.map +0 -1
- package/dist/__tests__/audit-witness.test.d.ts +0 -2
- package/dist/__tests__/audit-witness.test.d.ts.map +0 -1
- package/dist/__tests__/audit-witness.test.js +0 -308
- package/dist/__tests__/audit-witness.test.js.map +0 -1
- package/dist/__tests__/canonical.test.d.ts +0 -2
- package/dist/__tests__/canonical.test.d.ts.map +0 -1
- package/dist/__tests__/canonical.test.js +0 -162
- package/dist/__tests__/canonical.test.js.map +0 -1
- package/dist/__tests__/chain-hash-anchor.test.d.ts +0 -22
- package/dist/__tests__/chain-hash-anchor.test.d.ts.map +0 -1
- package/dist/__tests__/chain-hash-anchor.test.js +0 -55
- package/dist/__tests__/chain-hash-anchor.test.js.map +0 -1
- package/dist/__tests__/commit-service.integration.test.d.ts +0 -2
- package/dist/__tests__/commit-service.integration.test.d.ts.map +0 -1
- package/dist/__tests__/commit-service.integration.test.js +0 -95
- package/dist/__tests__/commit-service.integration.test.js.map +0 -1
- package/dist/__tests__/constructor.test.d.ts +0 -2
- package/dist/__tests__/constructor.test.d.ts.map +0 -1
- package/dist/__tests__/constructor.test.js +0 -267
- package/dist/__tests__/constructor.test.js.map +0 -1
- package/dist/__tests__/declared-proof-fixture.d.ts +0 -66
- package/dist/__tests__/declared-proof-fixture.d.ts.map +0 -1
- package/dist/__tests__/declared-proof-fixture.js +0 -66
- package/dist/__tests__/declared-proof-fixture.js.map +0 -1
- package/dist/__tests__/fuse-placement.test.d.ts +0 -2
- package/dist/__tests__/fuse-placement.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-placement.test.js +0 -31
- package/dist/__tests__/fuse-placement.test.js.map +0 -1
- package/dist/__tests__/fuse-roundtrip.test.d.ts +0 -2
- package/dist/__tests__/fuse-roundtrip.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-roundtrip.test.js +0 -241
- package/dist/__tests__/fuse-roundtrip.test.js.map +0 -1
- package/dist/__tests__/fuse-sdk.test.d.ts +0 -2
- package/dist/__tests__/fuse-sdk.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-sdk.test.js +0 -117
- package/dist/__tests__/fuse-sdk.test.js.map +0 -1
- package/dist/__tests__/fuse-set-sdk.test.d.ts +0 -2
- package/dist/__tests__/fuse-set-sdk.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-set-sdk.test.js +0 -721
- package/dist/__tests__/fuse-set-sdk.test.js.map +0 -1
- package/dist/__tests__/fuse-set.test.d.ts +0 -2
- package/dist/__tests__/fuse-set.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-set.test.js +0 -868
- package/dist/__tests__/fuse-set.test.js.map +0 -1
- package/dist/__tests__/fuse-set2.test.d.ts +0 -2
- package/dist/__tests__/fuse-set2.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-set2.test.js +0 -294
- package/dist/__tests__/fuse-set2.test.js.map +0 -1
- package/dist/__tests__/fuse-vectors.test.d.ts +0 -2
- package/dist/__tests__/fuse-vectors.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-vectors.test.js +0 -175
- package/dist/__tests__/fuse-vectors.test.js.map +0 -1
- package/dist/__tests__/fuse-verify.test.d.ts +0 -2
- package/dist/__tests__/fuse-verify.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-verify.test.js +0 -250
- package/dist/__tests__/fuse-verify.test.js.map +0 -1
- package/dist/__tests__/hello-world.test.d.ts +0 -2
- package/dist/__tests__/hello-world.test.d.ts.map +0 -1
- package/dist/__tests__/hello-world.test.js +0 -9
- package/dist/__tests__/hello-world.test.js.map +0 -1
- package/dist/__tests__/mock-proof-fixture.d.ts +0 -15
- package/dist/__tests__/mock-proof-fixture.d.ts.map +0 -1
- package/dist/__tests__/mock-proof-fixture.js +0 -34
- package/dist/__tests__/mock-proof-fixture.js.map +0 -1
- package/dist/__tests__/policy-enforcement.test.d.ts +0 -2
- package/dist/__tests__/policy-enforcement.test.d.ts.map +0 -1
- package/dist/__tests__/policy-enforcement.test.js +0 -188
- package/dist/__tests__/policy-enforcement.test.js.map +0 -1
- package/dist/__tests__/proof-hash-regression.test.d.ts +0 -2
- package/dist/__tests__/proof-hash-regression.test.d.ts.map +0 -1
- package/dist/__tests__/proof-hash-regression.test.js +0 -95
- package/dist/__tests__/proof-hash-regression.test.js.map +0 -1
- package/dist/__tests__/proof-hash.test.d.ts +0 -2
- package/dist/__tests__/proof-hash.test.d.ts.map +0 -1
- package/dist/__tests__/proof-hash.test.js +0 -116
- package/dist/__tests__/proof-hash.test.js.map +0 -1
- package/dist/__tests__/proof-integrity.test.d.ts +0 -2
- package/dist/__tests__/proof-integrity.test.d.ts.map +0 -1
- package/dist/__tests__/proof-integrity.test.js +0 -426
- package/dist/__tests__/proof-integrity.test.js.map +0 -1
- package/dist/__tests__/realistic-proof-fixture.d.ts +0 -56
- package/dist/__tests__/realistic-proof-fixture.d.ts.map +0 -1
- package/dist/__tests__/realistic-proof-fixture.js +0 -57
- package/dist/__tests__/realistic-proof-fixture.js.map +0 -1
- package/dist/__tests__/verifier.test.d.ts +0 -2
- package/dist/__tests__/verifier.test.d.ts.map +0 -1
- package/dist/__tests__/verifier.test.js +0 -531
- package/dist/__tests__/verifier.test.js.map +0 -1
- package/dist/canonical.d.ts +0 -54
- package/dist/canonical.d.ts.map +0 -1
- package/dist/canonical.js +0 -119
- package/dist/canonical.js.map +0 -1
- package/dist/proof-hash.d.ts +0 -9
- package/dist/proof-hash.d.ts.map +0 -1
- package/dist/proof-hash.js +0 -75
- package/dist/proof-hash.js.map +0 -1
- package/dist/types.d.ts +0 -705
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/dist/verifier.d.ts +0 -27
- package/dist/verifier.d.ts.map +0 -1
- package/dist/verifier.js +0 -844
- package/dist/verifier.js.map +0 -1
- package/src/__tests__/audit-anchors.test.ts +0 -188
- package/src/__tests__/audit-anomalies.test.ts +0 -815
- package/src/__tests__/audit-attestation.test.ts +0 -529
- package/src/__tests__/audit-bench-smoke.test.ts +0 -108
- package/src/__tests__/audit-boundary.test.ts +0 -96
- package/src/__tests__/audit-cli.test.ts +0 -288
- package/src/__tests__/audit-dependencies.test.ts +0 -141
- package/src/__tests__/audit-fixtures.ts +0 -736
- package/src/__tests__/audit-ingest.test.ts +0 -575
- package/src/__tests__/audit-memory-ingest.test.ts +0 -156
- package/src/__tests__/audit-property.test.ts +0 -892
- package/src/__tests__/audit-real-fixtures.test.ts +0 -153
- package/src/__tests__/audit-real-live.test.ts +0 -128
- package/src/__tests__/audit-reconstruct.test.ts +0 -353
- package/src/__tests__/audit-report.test.ts +0 -222
- package/src/__tests__/audit-roundtrip.test.ts +0 -464
- package/src/__tests__/audit-temporal.test.ts +0 -446
- package/src/__tests__/audit-tiers.test.ts +0 -343
- package/src/__tests__/audit-witness-real.test.ts +0 -92
- package/src/__tests__/audit-witness.test.ts +0 -345
- package/src/__tests__/canonical.test.ts +0 -253
- package/src/__tests__/chain-hash-anchor.test.ts +0 -67
- package/src/__tests__/commit-service.integration.test.ts +0 -117
- package/src/__tests__/constructor.test.ts +0 -340
- package/src/__tests__/declared-proof-fixture.ts +0 -65
- package/src/__tests__/fuse-placement.test.ts +0 -32
- package/src/__tests__/fuse-roundtrip.test.ts +0 -243
- package/src/__tests__/fuse-sdk.test.ts +0 -142
- package/src/__tests__/fuse-set-sdk.test.ts +0 -852
- package/src/__tests__/fuse-set.test.ts +0 -1005
- package/src/__tests__/fuse-set2.test.ts +0 -343
- package/src/__tests__/fuse-vectors.test.ts +0 -218
- package/src/__tests__/fuse-verify.test.ts +0 -278
- package/src/__tests__/hello-world.test.ts +0 -10
- package/src/__tests__/mock-proof-fixture.ts +0 -36
- package/src/__tests__/policy-enforcement.test.ts +0 -226
- package/src/__tests__/proof-hash-regression.test.ts +0 -107
- package/src/__tests__/proof-hash.test.ts +0 -129
- package/src/__tests__/proof-integrity.test.ts +0 -541
- package/src/__tests__/real-fixtures/anchor-after-112762.json +0 -47
- package/src/__tests__/real-fixtures/anchor-with-ethereum-112758.json +0 -47
- package/src/__tests__/real-fixtures/live-proof-0.json +0 -42
- package/src/__tests__/real-fixtures/live-proof-1.json +0 -43
- package/src/__tests__/real-fixtures/live-proof-2.json +0 -43
- package/src/__tests__/real-fixtures/successor-112760.json +0 -38
- package/src/__tests__/real-fixtures/witness-25525146.json +0 -6
- package/src/__tests__/real-fixtures/witness-25525147.json +0 -6
- package/src/__tests__/realistic-proof-fixture.ts +0 -57
- package/src/__tests__/verifier.test.ts +0 -624
|
@@ -1,852 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Mike Argento. All rights reserved. See LICENSE.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* fuseSet() against a fake transport whose commit answer MINTS the proof from
|
|
5
|
-
* the body it received, under the slot it handed out, with a local signer:
|
|
6
|
-
* the returned proof therefore matches what the SDK sent and verifies for
|
|
7
|
-
* real. Every refusal path runs, every happy path is compared against an
|
|
8
|
-
* independent oracle, and every negative case runs against the real
|
|
9
|
-
* verifier. Nothing here touches a ledger or a network.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { describe, test, before } from "node:test";
|
|
13
|
-
import * as assert from "node:assert/strict";
|
|
14
|
-
import { readFileSync } from "node:fs";
|
|
15
|
-
import { fileURLToPath } from "node:url";
|
|
16
|
-
import { signAsync } from "@noble/ed25519";
|
|
17
|
-
import { sha256 } from "@noble/hashes/sha256";
|
|
18
|
-
import {
|
|
19
|
-
getPlacement,
|
|
20
|
-
canonicalize,
|
|
21
|
-
canonicalSlotBody,
|
|
22
|
-
computeSlotCommitment,
|
|
23
|
-
buildSetManifest,
|
|
24
|
-
parseSetManifest,
|
|
25
|
-
fuseAttribution,
|
|
26
|
-
verifyFuse,
|
|
27
|
-
verifyFuseMember,
|
|
28
|
-
bytesToBase64,
|
|
29
|
-
bytesToHex,
|
|
30
|
-
SET_METADATA_KEY,
|
|
31
|
-
} from "@mikeargento/bitgraph-verify";
|
|
32
|
-
import type { BitGraphProof, SlotAllocation, Attribution, SetMember, SetManifest } from "@mikeargento/bitgraph-verify";
|
|
33
|
-
import { makeKey, signBody, b64, utf8 } from "./audit-fixtures.js";
|
|
34
|
-
import type { ManualKey } from "./audit-fixtures.js";
|
|
35
|
-
import { fuse, fuseSet, builderFor, placementForBytes, fusedNamesFor, FuseError, MAX_SET_MEMBERS, digest, trailerBytesFor } from "../fuse.js";
|
|
36
|
-
import { parseSetRoot } from "@mikeargento/bitgraph-verify";
|
|
37
|
-
import type { FuseSetProgress, FuseSetBytesMember, FuseSetLoadedMember, FuseSetHashedMember, FusedDigestInput } from "../fuse.js";
|
|
38
|
-
import type { FuseSetMember } from "../fuse.js";
|
|
39
|
-
|
|
40
|
-
const FIX = fileURLToPath(new URL("../../src/__tests__/fuse-fixtures/", import.meta.url));
|
|
41
|
-
const bytes = (name: string) => new Uint8Array(readFileSync(FIX + name));
|
|
42
|
-
const dec = (b: Uint8Array) => new TextDecoder().decode(b);
|
|
43
|
-
|
|
44
|
-
const original = bytes("original.txt");
|
|
45
|
-
const png = bytes("image.png");
|
|
46
|
-
const note = utf8("a third member, plain text made for the set\n");
|
|
47
|
-
const unrelated = utf8("a file nobody recorded\n");
|
|
48
|
-
|
|
49
|
-
// The phase-1 fixtures fuse original.txt as trailer/1 and image.png as
|
|
50
|
-
// container/1, so these members declare those placements; the by-bytes
|
|
51
|
-
// default (placementForBytes) is asserted on its own in test 3.
|
|
52
|
-
const two = (): FuseSetBytesMember[] => [{ original, placement: "trailer/1" }, { original: png, placement: "container/1" }];
|
|
53
|
-
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
// Local signer: slots and proofs minted from a received commit body
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
|
|
58
|
-
const EPOCH = bytesToBase64(new Uint8Array(32).fill(0x5e));
|
|
59
|
-
|
|
60
|
-
/** A slot record signed by `key`, with a 32-byte nonce and the anchored chain. */
|
|
61
|
-
async function allocateSlot(key: ManualKey, counter: string): Promise<SlotAllocation> {
|
|
62
|
-
const body = {
|
|
63
|
-
version: "bitgraph/slot/1" as const,
|
|
64
|
-
nonceB64: b64(crypto.getRandomValues(new Uint8Array(32))),
|
|
65
|
-
counter,
|
|
66
|
-
epochId: EPOCH,
|
|
67
|
-
publicKeyB64: key.publicKeyB64,
|
|
68
|
-
chainId: "bitgraph:main",
|
|
69
|
-
};
|
|
70
|
-
return { ...body, signatureB64: b64(await signAsync(canonicalize(body), key.privateKey)) };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** The commit body the SDK sends, as the fake boundary receives it. */
|
|
74
|
-
interface CommitBody {
|
|
75
|
-
digests: Array<{ digestB64: string; hashAlg: string }>;
|
|
76
|
-
slotId: string;
|
|
77
|
-
slot: SlotAllocation;
|
|
78
|
-
chainId: string;
|
|
79
|
-
attribution: Attribution;
|
|
80
|
-
metadata?: Record<string, unknown>;
|
|
81
|
-
agency?: unknown;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
interface MintOptions {
|
|
85
|
-
key: ManualKey;
|
|
86
|
-
slot: SlotAllocation;
|
|
87
|
-
/** The received body: its digest, attribution and metadata become the proof's. */
|
|
88
|
-
body: CommitBody;
|
|
89
|
-
commitCounter?: string;
|
|
90
|
-
/** Attach the body's metadata under proof.metadata (default true). */
|
|
91
|
-
withMetadata?: boolean;
|
|
92
|
-
/** Override the signed attribution; null for none at all. */
|
|
93
|
-
attribution?: Attribution | null;
|
|
94
|
-
/** Override the artifact digest; the proof stays validly signed. */
|
|
95
|
-
artifactDigestB64?: string;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** A proof minted from a received body under `slot`: the digest is the artifact, the attribution is signed, the slot is consumed. */
|
|
99
|
-
async function mintFromBody(o: MintOptions): Promise<BitGraphProof> {
|
|
100
|
-
const commit: BitGraphProof["commit"] = {
|
|
101
|
-
nonceB64: o.slot.nonceB64,
|
|
102
|
-
counter: o.commitCounter ?? (BigInt(o.slot.counter) + 4n).toString(),
|
|
103
|
-
epochId: o.slot.epochId,
|
|
104
|
-
slotCounter: o.slot.counter,
|
|
105
|
-
slotHashB64: b64(sha256(canonicalize(canonicalSlotBody(o.slot)))),
|
|
106
|
-
};
|
|
107
|
-
(commit as unknown as Record<string, unknown>)["chainId"] = "bitgraph:main";
|
|
108
|
-
const attribution = o.attribution === undefined ? o.body.attribution : o.attribution;
|
|
109
|
-
const proof = await signBody(
|
|
110
|
-
o.key,
|
|
111
|
-
{ hashAlg: "sha256", digestB64: o.artifactDigestB64 ?? o.body.digests[0]!.digestB64 },
|
|
112
|
-
commit,
|
|
113
|
-
"test-measurement-set",
|
|
114
|
-
attribution === null ? undefined : { attribution },
|
|
115
|
-
);
|
|
116
|
-
proof.slotAllocation = o.slot;
|
|
117
|
-
if (o.withMetadata !== false && o.body.metadata !== undefined) proof.metadata = structuredClone(o.body.metadata);
|
|
118
|
-
return proof;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
|
-
// Fake transport, in the fuse-sdk style
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
|
|
125
|
-
interface Call { path: string; body: unknown }
|
|
126
|
-
interface Answer { status: number; json: unknown }
|
|
127
|
-
type CommitAnswer = (calls: Call[], body: CommitBody) => Answer | Promise<Answer>;
|
|
128
|
-
type LookupAnswer = (calls: Call[]) => Answer | Promise<Answer>;
|
|
129
|
-
type AllocateAnswer = (calls: Call[]) => Answer;
|
|
130
|
-
|
|
131
|
-
/** A transport that hands out `slot` and answers the commit with `commit`; a lookup and an allocate answer are optional. */
|
|
132
|
-
function fakeTransport(slot: SlotAllocation, commit: CommitAnswer, lookup?: LookupAnswer, allocate?: AllocateAnswer) {
|
|
133
|
-
const calls: Call[] = [];
|
|
134
|
-
const f: typeof fetch = async (input, init) => {
|
|
135
|
-
const url = String(input);
|
|
136
|
-
const path = url.replace(/^https?:\/\/[^/]+/, "");
|
|
137
|
-
const body = init?.body ? JSON.parse(String(init.body)) : undefined;
|
|
138
|
-
calls.push({ path, body });
|
|
139
|
-
const reply = (r: Answer) => new Response(JSON.stringify(r.json), { status: r.status, headers: { "Content-Type": "application/json" } });
|
|
140
|
-
if (path === "/api/fuse/allocate") return reply(allocate ? allocate(calls) : { status: 200, json: { slotId: slot.nonceB64, slot, chainId: "bitgraph:main" } });
|
|
141
|
-
if (path === "/api/fuse/commit") return reply(await commit(calls, body as CommitBody));
|
|
142
|
-
if (path.startsWith("/api/proofs/")) return reply(lookup ? await lookup(calls) : { status: 200, json: { proofs: [] } });
|
|
143
|
-
return reply({ status: 404, json: { error: "no route" } });
|
|
144
|
-
};
|
|
145
|
-
return { calls, transport: { baseUrl: "https://example.test", fetch: f, recoveryAttempts: 2, recoveryDelayMs: 1 } };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** The honest boundary: mints the proof from the received body under the slot it handed out. */
|
|
149
|
-
function honest(key: ManualKey, slot: SlotAllocation, mint: Omit<MintOptions, "key" | "slot" | "body"> = {}) {
|
|
150
|
-
return fakeTransport(slot, async (_calls, body) => ({ status: 200, json: { proof: await mintFromBody({ key, slot, body, ...mint }) } }));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const allocates = (calls: Call[]) => calls.filter((c) => c.path === "/api/fuse/allocate");
|
|
154
|
-
const commits = (calls: Call[]) => calls.filter((c) => c.path === "/api/fuse/commit");
|
|
155
|
-
const toUrlSafe = (s: string) => s.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
156
|
-
|
|
157
|
-
// ---------------------------------------------------------------------------
|
|
158
|
-
// The oracle: what the SDK must send, computed independently
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
|
|
161
|
-
interface Oracle { commitment: Uint8Array; fused: Uint8Array[]; rows: SetMember[]; manifest: Uint8Array; digestB64: string; sorted: SetMember[] }
|
|
162
|
-
|
|
163
|
-
/** The commitment from the slot, each member built by its placement, the canonical manifest and its digest. */
|
|
164
|
-
function oracle(slot: SlotAllocation, members: readonly FuseSetBytesMember[]): Oracle {
|
|
165
|
-
const commitment = computeSlotCommitment(slot);
|
|
166
|
-
const placementOf = (m: FuseSetBytesMember) => m.placement ?? placementForBytes(m.original);
|
|
167
|
-
const fused = members.map((m) => getPlacement(placementOf(m))!.build({ original: m.original, commitment }));
|
|
168
|
-
const rows = members.map((m, i) => ({ artifact: sha256(fused[i]!), origin: sha256(m.original), placement: placementOf(m) }));
|
|
169
|
-
const manifest = buildSetManifest(commitment, rows);
|
|
170
|
-
return { commitment, fused, rows, manifest, digestB64: bytesToBase64(sha256(manifest)), sorted: parseSetManifest(manifest)!.members };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const expectedBody = (slot: SlotAllocation, o: Oracle): CommitBody => ({
|
|
174
|
-
digests: [{ digestB64: o.digestB64, hashAlg: "sha256" }],
|
|
175
|
-
slotId: slot.nonceB64,
|
|
176
|
-
slot,
|
|
177
|
-
chainId: "bitgraph:main",
|
|
178
|
-
attribution: { name: "bitgraph-fuse/1", title: "set/1" },
|
|
179
|
-
metadata: { [SET_METADATA_KEY]: JSON.parse(dec(o.manifest)) as SetManifest },
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
let key!: ManualKey;
|
|
183
|
-
let slot!: SlotAllocation;
|
|
184
|
-
let other!: SlotAllocation;
|
|
185
|
-
|
|
186
|
-
before(async () => {
|
|
187
|
-
key = await makeKey();
|
|
188
|
-
slot = await allocateSlot(key, "10");
|
|
189
|
-
other = await allocateSlot(key, "20");
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// ---------------------------------------------------------------------------
|
|
193
|
-
// The beats
|
|
194
|
-
// ---------------------------------------------------------------------------
|
|
195
|
-
|
|
196
|
-
describe("fuseSet(): one slot, N files, the manifest as the artifact", () => {
|
|
197
|
-
test("1. a trailer/1 and a container/1 member: one allocate, one commit, the whole body byte for byte against the oracle", async () => {
|
|
198
|
-
const members = two();
|
|
199
|
-
const o = oracle(slot, members);
|
|
200
|
-
const { calls, transport } = honest(key, slot);
|
|
201
|
-
await fuseSet(members, { transport });
|
|
202
|
-
assert.equal(allocates(calls).length, 1);
|
|
203
|
-
assert.equal(commits(calls).length, 1);
|
|
204
|
-
const body = commits(calls)[0]!.body as Record<string, unknown>;
|
|
205
|
-
assert.deepEqual(body, expectedBody(slot, o));
|
|
206
|
-
assert.deepEqual(Object.keys(body), ["digests", "slotId", "slot", "chainId", "attribution", "metadata"]);
|
|
207
|
-
assert.deepEqual(body.attribution, { name: "bitgraph-fuse/1", title: "set/1" });
|
|
208
|
-
assert.ok(!("message" in (body.attribution as object)), "a set has no single origin");
|
|
209
|
-
assert.ok(!("agency" in body));
|
|
210
|
-
assert.deepEqual(body.metadata, { [SET_METADATA_KEY]: JSON.parse(dec(o.manifest)) });
|
|
211
|
-
// The raw nonce rides only where the protocol needs it: the slot record and the slotId that names it.
|
|
212
|
-
const { slot: _slot, slotId: _slotId, ...rest } = body;
|
|
213
|
-
assert.ok(!JSON.stringify(rest).includes(slot.nonceB64));
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test("2. the result: manifest bytes, digests, commitment, flags, and the manifest's own verification", async () => {
|
|
217
|
-
const members = two();
|
|
218
|
-
const o = oracle(slot, members);
|
|
219
|
-
const r = await fuseSet(members, { transport: honest(key, slot).transport });
|
|
220
|
-
assert.deepEqual(r.manifestBytes, o.manifest);
|
|
221
|
-
assert.equal(r.artifactDigestB64, bytesToBase64(sha256(r.manifestBytes)));
|
|
222
|
-
assert.equal(r.artifactDigestB64, r.proof.artifact.digestB64);
|
|
223
|
-
assert.equal(r.artifactDigestB64, o.digestB64);
|
|
224
|
-
assert.deepEqual(r.manifest, JSON.parse(dec(r.manifestBytes)));
|
|
225
|
-
assert.equal(r.slotCommitmentB64, bytesToBase64(o.commitment));
|
|
226
|
-
assert.equal(r.recovered, false);
|
|
227
|
-
assert.equal(r.manifestEchoed, true);
|
|
228
|
-
assert.equal(r.verification.category, "FUSED_DIRECT", r.verification.reason ?? "");
|
|
229
|
-
assert.equal(r.verification.placement, "set/1");
|
|
230
|
-
assert.equal(r.verification.originDigestB64, null);
|
|
231
|
-
assert.equal(r.proof.attribution?.title, "set/1");
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
test("3. members come back in the caller's order, each with its manifest row and its names; with verifyMembers each is SET_MEMBER_DIRECT against explicit bytes", async () => {
|
|
235
|
-
const members = two();
|
|
236
|
-
members[0]!.name = "photo.jpg";
|
|
237
|
-
const o = oracle(slot, members);
|
|
238
|
-
const r = await fuseSet(members, { transport: honest(key, slot).transport, verifyMembers: true });
|
|
239
|
-
assert.deepEqual(r.members.map((m) => m.index), [0, 1]);
|
|
240
|
-
for (const m of r.members) {
|
|
241
|
-
const row = o.rows[m.index]!;
|
|
242
|
-
assert.equal(m.placement, row.placement);
|
|
243
|
-
assert.equal(m.originDigestB64, bytesToBase64(row.origin));
|
|
244
|
-
assert.equal(m.artifactDigestB64, bytesToBase64(row.artifact));
|
|
245
|
-
assert.equal(m.manifestIndex, o.sorted.findIndex((s) => bytesToHex(s.artifact) === bytesToHex(row.artifact)));
|
|
246
|
-
assert.equal(m.manifestIndex, m.verification!.set!.member!.index);
|
|
247
|
-
assert.ok(!("fusedBytes" in m), "not kept by default");
|
|
248
|
-
assert.equal(m.verification!.category, "SET_MEMBER_DIRECT", m.verification!.reason ?? "");
|
|
249
|
-
assert.equal(m.verification!.set!.manifestSource, "argument");
|
|
250
|
-
assert.equal(m.verification!.set!.member!.fusedDigestB64, m.artifactDigestB64);
|
|
251
|
-
assert.equal(m.verification!.set!.memberCount, 2);
|
|
252
|
-
}
|
|
253
|
-
// Without verifyMembers the same rows come back bound by digest, and no verdict is invented.
|
|
254
|
-
const plain = await fuseSet(members, { transport: honest(key, slot).transport });
|
|
255
|
-
assert.deepEqual(plain.members.map((m) => [m.index, m.manifestIndex, m.placement, m.originDigestB64, m.artifactDigestB64]), r.members.map((m) => [m.index, m.manifestIndex, m.placement, m.originDigestB64, m.artifactDigestB64]));
|
|
256
|
-
for (const m of plain.members) assert.ok(!("verification" in m), "no verifier verdict without verifyMembers");
|
|
257
|
-
assert.deepEqual({ fusedName: r.members[0]!.fusedName, frameName: r.members[0]!.frameName }, fusedNamesFor("photo.jpg", "trailer/1"));
|
|
258
|
-
assert.equal(r.members[1]!.fusedName, null);
|
|
259
|
-
assert.equal(r.members[1]!.frameName, null);
|
|
260
|
-
// The by-bytes default: PNG is trailer-safe, plain text goes into a container.
|
|
261
|
-
const d = await fuseSet([{ original: png }, { original }], { transport: honest(key, slot).transport });
|
|
262
|
-
assert.deepEqual(d.members.map((m) => m.placement), [placementForBytes(png), placementForBytes(original)]);
|
|
263
|
-
assert.deepEqual(d.members.map((m) => m.placement), ["trailer/1", "container/2"]);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
test("4. each original rebuilds its member (SET_MEMBER_FROM_ORIGIN); with the echo stripped and no explicit bytes nothing is bound", async () => {
|
|
267
|
-
const members = two();
|
|
268
|
-
const r = await fuseSet(members, { transport: honest(key, slot).transport });
|
|
269
|
-
for (const m of r.members) {
|
|
270
|
-
const v = await verifyFuseMember({ proof: r.proof, bytes: members[m.index]!.original, manifest: r.manifestBytes });
|
|
271
|
-
assert.equal(v.category, "SET_MEMBER_FROM_ORIGIN", v.reason ?? "");
|
|
272
|
-
assert.equal(v.set!.member!.index, m.manifestIndex);
|
|
273
|
-
assert.equal(v.placement, m.placement);
|
|
274
|
-
}
|
|
275
|
-
const stripped = structuredClone(r.proof);
|
|
276
|
-
delete stripped.metadata;
|
|
277
|
-
const fused = getPlacement("trailer/1")!.build({ original, commitment: computeSlotCommitment(slot) });
|
|
278
|
-
const unbound = await verifyFuseMember({ proof: stripped, bytes: fused });
|
|
279
|
-
assert.equal(unbound.category, "INVALID_SET_MANIFEST");
|
|
280
|
-
assert.equal(unbound.set, null);
|
|
281
|
-
// Which is why manifestBytes is kept beside the proof.
|
|
282
|
-
assert.equal((await verifyFuseMember({ proof: stripped, bytes: fused, manifest: r.manifestBytes })).category, "SET_MEMBER_DIRECT");
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test("5. verifyFuse answers for the manifest and knows nothing about a member", async () => {
|
|
286
|
-
const r = await fuseSet(two(), { transport: honest(key, slot).transport });
|
|
287
|
-
const m = await verifyFuse({ proof: r.proof, bytes: r.manifestBytes });
|
|
288
|
-
assert.equal(m.category, "FUSED_DIRECT", m.reason ?? "");
|
|
289
|
-
assert.equal(m.placement, "set/1");
|
|
290
|
-
const fused = getPlacement("trailer/1")!.build({ original, commitment: computeSlotCommitment(slot) });
|
|
291
|
-
assert.equal((await verifyFuse({ proof: r.proof, bytes: fused })).category, "NO_MATCH");
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test("6. an agency envelope passes through untouched, and is absent otherwise", async () => {
|
|
295
|
-
const a = honest(key, slot);
|
|
296
|
-
await fuseSet(two(), { transport: a.transport, agency: { x: 1 } });
|
|
297
|
-
assert.deepEqual((commits(a.calls)[0]!.body as { agency?: unknown }).agency, { x: 1 });
|
|
298
|
-
const b = honest(key, slot);
|
|
299
|
-
await fuseSet(two(), { transport: b.transport });
|
|
300
|
-
assert.ok(!("agency" in (commits(b.calls)[0]!.body as object)));
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
// ---------------------------------------------------------------------------
|
|
305
|
-
// Refusals after the slot is held: nothing is committed
|
|
306
|
-
// ---------------------------------------------------------------------------
|
|
307
|
-
|
|
308
|
-
describe("fuseSet(): a bad member burns the slot and commits nothing", () => {
|
|
309
|
-
test("7. a member whose builder drops the commitment is commitment-missing, naming the member; no commit, one allocate", async () => {
|
|
310
|
-
const { calls, transport } = honest(key, slot);
|
|
311
|
-
const members = two();
|
|
312
|
-
members[1]!.builder = () => original;
|
|
313
|
-
await assert.rejects(fuseSet(members, { transport }), (e: FuseError) => {
|
|
314
|
-
assert.equal(e.code, "commitment-missing");
|
|
315
|
-
assert.match(e.message, /^member 1:/);
|
|
316
|
-
assert.equal(e.member, 1);
|
|
317
|
-
assert.ok(!e.message.includes(slot.nonceB64));
|
|
318
|
-
return true;
|
|
319
|
-
});
|
|
320
|
-
assert.equal(commits(calls).length, 0, "no commit was attempted");
|
|
321
|
-
assert.equal(allocates(calls).length, 1);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
test("8. a builder that throws, or returns something other than bytes, is builder-failed naming the member", async () => {
|
|
325
|
-
const a = honest(key, slot);
|
|
326
|
-
const throwing = two();
|
|
327
|
-
throwing[0]!.builder = () => { throw new Error("disk full"); };
|
|
328
|
-
await assert.rejects(fuseSet(throwing, { transport: a.transport }), (e: FuseError) => e.code === "builder-failed" && /^member 0: the builder threw: disk full/.test(e.message) && e.member === 0);
|
|
329
|
-
assert.equal(commits(a.calls).length, 0);
|
|
330
|
-
const b = honest(key, slot);
|
|
331
|
-
const stringy = two();
|
|
332
|
-
stringy[1]!.builder = (() => "not bytes") as never;
|
|
333
|
-
await assert.rejects(fuseSet(stringy, { transport: b.transport }), (e: FuseError) => e.code === "builder-failed" && /^member 1: the builder must return a Uint8Array/.test(e.message) && e.member === 1);
|
|
334
|
-
assert.equal(commits(b.calls).length, 0);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
test("9. bytes that carry the commitment but embed another member's origin are builder-failed naming the member and the origin", async () => {
|
|
338
|
-
const { calls, transport } = honest(key, slot);
|
|
339
|
-
// Two trailer/1 members; member 1's builder hands back member 0's fused bytes, which carry c but embed member 0's original.
|
|
340
|
-
const members: FuseSetBytesMember[] = [{ original, placement: "trailer/1" }, { original: note, placement: "trailer/1" }];
|
|
341
|
-
members[1]!.builder = ({ commitment }) => getPlacement("trailer/1")!.build({ original, commitment });
|
|
342
|
-
await assert.rejects(fuseSet(members, { transport }), (e: FuseError) => e.code === "builder-failed" && /member 1/.test(e.message) && /origin/.test(e.message) && e.member === 1);
|
|
343
|
-
assert.equal(commits(calls).length, 0);
|
|
344
|
-
assert.equal(allocates(calls).length, 1);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
test("9b. container/1: bytes that declare the member's origin digest but carry other bytes are builder-failed naming the member; no commit", async () => {
|
|
348
|
-
const { calls, transport } = honest(key, slot);
|
|
349
|
-
const members: FuseSetBytesMember[] = [{ original, placement: "trailer/1" }, { original: png, placement: "container/1" }];
|
|
350
|
-
// The payload names png's digest (so the declared origin matches) while the tar carries other bytes.
|
|
351
|
-
members[1]!.builder = ({ commitment, originDigest }) => getPlacement("container/1")!.build({ original: utf8("not the member's original\n"), originDigest: originDigest!, commitment });
|
|
352
|
-
await assert.rejects(fuseSet(members, { transport }), (e: FuseError) => e.code === "builder-failed" && /^member 1/.test(e.message) && /origin/.test(e.message) && e.member === 1);
|
|
353
|
-
assert.equal(commits(calls).length, 0, "nothing was committed");
|
|
354
|
-
assert.equal(allocates(calls).length, 1);
|
|
355
|
-
// The mirror: a payload declaring another digest over the member's own bytes is refused the same way.
|
|
356
|
-
const declared: FuseSetBytesMember[] = [{ original, placement: "trailer/1" }, { original: png, placement: "container/1" }];
|
|
357
|
-
declared[1]!.builder = ({ commitment }) => getPlacement("container/1")!.build({ original: png, originDigest: sha256(unrelated), commitment });
|
|
358
|
-
const b = honest(key, slot);
|
|
359
|
-
await assert.rejects(fuseSet(declared, { transport: b.transport }), (e: FuseError) => e.code === "builder-failed" && e.member === 1);
|
|
360
|
-
assert.equal(commits(b.calls).length, 0);
|
|
361
|
-
// And the honest container/1 builder still passes both checks.
|
|
362
|
-
const ok = await fuseSet([{ original, placement: "trailer/1" }, { original: png, placement: "container/1" }], { transport: honest(key, slot).transport, verifyMembers: true });
|
|
363
|
-
assert.equal(ok.members[1]!.verification!.category, "SET_MEMBER_DIRECT");
|
|
364
|
-
});
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
// ---------------------------------------------------------------------------
|
|
368
|
-
// Refusals before any request: no slot is burned
|
|
369
|
-
// ---------------------------------------------------------------------------
|
|
370
|
-
|
|
371
|
-
describe("fuseSet(): bad input is refused before any request", () => {
|
|
372
|
-
test("10. the same original under the same placement twice names both members; under two placements it is two rows", async () => {
|
|
373
|
-
const { calls, transport } = honest(key, slot);
|
|
374
|
-
await assert.rejects(
|
|
375
|
-
fuseSet([{ original, placement: "trailer/1" }, { original: png, placement: "container/1" }, { original, placement: "trailer/1" }], { transport }),
|
|
376
|
-
(e: FuseError) => e.code === "bad-input" && /members 0 and 2/.test(e.message) && e.member === 2,
|
|
377
|
-
);
|
|
378
|
-
assert.equal(calls.length, 0, "no slot was burned");
|
|
379
|
-
const r = await fuseSet([{ original, placement: "trailer/1" }, { original, placement: "container/1" }], { transport, verifyMembers: true });
|
|
380
|
-
assert.equal((r.manifest as SetManifest).members.length, 2);
|
|
381
|
-
assert.deepEqual(r.members.map((m) => m.placement), ["trailer/1", "container/1"]);
|
|
382
|
-
assert.equal(r.members[0]!.originDigestB64, r.members[1]!.originDigestB64);
|
|
383
|
-
assert.notEqual(r.members[0]!.artifactDigestB64, r.members[1]!.artifactDigestB64);
|
|
384
|
-
for (const m of r.members) assert.equal(m.verification!.category, "SET_MEMBER_DIRECT");
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
test("11. an empty set and a set over the cap are refused; the cap runs before the duplicate check", async () => {
|
|
388
|
-
const { calls, transport } = honest(key, slot);
|
|
389
|
-
await assert.rejects(fuseSet([], { transport }), (e: FuseError) => e.code === "bad-input" && /at least one/.test(e.message));
|
|
390
|
-
const same: FuseSetMember = { original, placement: "trailer/1" };
|
|
391
|
-
await assert.rejects(
|
|
392
|
-
fuseSet(new Array<FuseSetMember>(MAX_SET_MEMBERS + 1).fill(same), { transport }),
|
|
393
|
-
(e: FuseError) => e.code === "bad-input" && /at most 2000 members \(got 2001\)/.test(e.message) && !/members 0 and 1/.test(e.message),
|
|
394
|
-
);
|
|
395
|
-
assert.equal(MAX_SET_MEMBERS, 2000);
|
|
396
|
-
assert.equal(calls.length, 0);
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
test("12. a produced/1, set/1, unregistered, or non-bytes member is refused naming the member", async () => {
|
|
400
|
-
const { calls, transport } = honest(key, slot);
|
|
401
|
-
for (const id of ["produced/1", "set/1"]) {
|
|
402
|
-
await assert.rejects(
|
|
403
|
-
fuseSet([{ original, placement: "trailer/1" }, { original: png, placement: id as never }], { transport }),
|
|
404
|
-
(e: FuseError) => e.code === "bad-input" && /^member 1: /.test(e.message) && e.message.includes(id) && e.member === 1,
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
await assert.rejects(
|
|
408
|
-
fuseSet([{ original, placement: "xmp/9" as never }], { transport }),
|
|
409
|
-
(e: FuseError) => e.code === "bad-placement" && /^member 0: placement "xmp\/9" is not registered/.test(e.message) && e.member === 0,
|
|
410
|
-
);
|
|
411
|
-
await assert.rejects(
|
|
412
|
-
fuseSet([{ original: "text" as never }], { transport }),
|
|
413
|
-
(e: FuseError) => e.code === "bad-input" && /^member 0: original must be a Uint8Array/.test(e.message) && e.member === 0,
|
|
414
|
-
);
|
|
415
|
-
assert.equal(calls.length, 0);
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
test("12b. a null, undefined or missing member element is bad-input naming the member, never a bare TypeError", async () => {
|
|
419
|
-
const { calls, transport } = honest(key, slot);
|
|
420
|
-
const first: FuseSetMember = { original, placement: "trailer/1" };
|
|
421
|
-
const sparse: FuseSetMember[] = [first];
|
|
422
|
-
sparse.length = 2;
|
|
423
|
-
for (const members of [[first, null], [first, undefined], sparse, [null]] as unknown as FuseSetMember[][]) {
|
|
424
|
-
const expected = members.length === 1 ? 0 : 1;
|
|
425
|
-
await assert.rejects(fuseSet(members, { transport }), (e: unknown) => {
|
|
426
|
-
assert.ok(e instanceof FuseError, `not a FuseError: ${String(e)}`);
|
|
427
|
-
assert.equal(e.code, "bad-input");
|
|
428
|
-
assert.match(e.message, new RegExp(`^member ${expected}: original must be a Uint8Array`));
|
|
429
|
-
assert.equal(e.member, expected);
|
|
430
|
-
return true;
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
assert.equal(calls.length, 0, "no slot was burned");
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
test("13. the member cap keeps a full manifest under half the parent's 1 MB body cap", () => {
|
|
437
|
-
const rows: SetMember[] = Array.from({ length: MAX_SET_MEMBERS }, (_, i) => ({ artifact: sha256(utf8(`artifact ${i}`)), origin: sha256(utf8(`origin ${i}`)), placement: "container/1" }));
|
|
438
|
-
const manifest = buildSetManifest(computeSlotCommitment(slot), rows);
|
|
439
|
-
assert.ok(manifest.length + 4096 < 512 * 1024, `${manifest.length} bytes`);
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
// ---------------------------------------------------------------------------
|
|
444
|
-
// The boundary: other slots, lost responses, refusals, restarts
|
|
445
|
-
// ---------------------------------------------------------------------------
|
|
446
|
-
|
|
447
|
-
describe("fuseSet(): the boundary", () => {
|
|
448
|
-
test("14. a proof under a different slot is never labelled a set", async () => {
|
|
449
|
-
const { transport } = fakeTransport(slot, async (_calls, body) => ({ status: 200, json: { proof: await mintFromBody({ key, slot: other, body }) } }));
|
|
450
|
-
await assert.rejects(fuseSet(two(), { transport }), (e: FuseError) => e.code === "slot-mismatch");
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
test("15. a lost commit response is read back by the MANIFEST digest and matched on the held slot; nothing is re-allocated", async () => {
|
|
454
|
-
const o = oracle(slot, two());
|
|
455
|
-
const real = await mintFromBody({ key, slot, body: expectedBody(slot, o) });
|
|
456
|
-
// The decoy commits the same digest under another slot: recovery must match the slot, not the digest alone.
|
|
457
|
-
const decoy = await mintFromBody({ key, slot: other, body: expectedBody(slot, o) });
|
|
458
|
-
const { calls, transport } = fakeTransport(
|
|
459
|
-
slot,
|
|
460
|
-
() => { throw new Error("socket hang up"); },
|
|
461
|
-
() => ({ status: 200, json: { proofs: [{ proof: decoy }, { proof: real }] } }),
|
|
462
|
-
);
|
|
463
|
-
const r = await fuseSet(two(), { transport, verifyMembers: true });
|
|
464
|
-
assert.equal(r.recovered, true);
|
|
465
|
-
assert.deepEqual(r.proof, real);
|
|
466
|
-
assert.equal(allocates(calls).length, 1);
|
|
467
|
-
const lookup = calls.find((c) => c.path.startsWith("/api/proofs/"))!;
|
|
468
|
-
assert.ok(lookup.path.includes(toUrlSafe(o.digestB64)), lookup.path);
|
|
469
|
-
assert.equal(r.manifestEchoed, true);
|
|
470
|
-
for (const m of r.members) assert.equal(m.verification!.category, "SET_MEMBER_DIRECT");
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
test("16. 409 slot-unavailable: read back when the ledger has the proof, else reported as slot-unavailable and never retried into a new slot", async () => {
|
|
474
|
-
const o = oracle(slot, two());
|
|
475
|
-
const real = await mintFromBody({ key, slot, body: expectedBody(slot, o) });
|
|
476
|
-
const gone = () => ({ status: 409, json: { error: "gone", code: "slot-unavailable" } });
|
|
477
|
-
const a = fakeTransport(slot, gone, () => ({ status: 200, json: { proofs: [{ proof: real }] } }));
|
|
478
|
-
const r = await fuseSet(two(), { transport: a.transport });
|
|
479
|
-
assert.equal(r.recovered, true);
|
|
480
|
-
assert.deepEqual(r.proof, real);
|
|
481
|
-
const b = fakeTransport(slot, gone);
|
|
482
|
-
await assert.rejects(fuseSet(two(), { transport: b.transport }), (e: FuseError) => e.code === "slot-unavailable" && e.status === 409 && !e.message.includes(slot.nonceB64));
|
|
483
|
-
assert.equal(allocates(b.calls).length, 1);
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
test("17. tee-restarting: on allocate nothing is committed; on commit the ledger is read back first, then reported", async () => {
|
|
487
|
-
const restarting = () => ({ status: 503, json: { error: "restarting", code: "tee-restarting" } });
|
|
488
|
-
const a = fakeTransport(slot, () => ({ status: 200, json: {} }), undefined, restarting);
|
|
489
|
-
await assert.rejects(fuseSet(two(), { transport: a.transport }), (e: FuseError) => e.code === "tee-restarting" && e.status === 503);
|
|
490
|
-
assert.equal(commits(a.calls).length, 0);
|
|
491
|
-
const o = oracle(slot, two());
|
|
492
|
-
const real = await mintFromBody({ key, slot, body: expectedBody(slot, o) });
|
|
493
|
-
const b = fakeTransport(slot, restarting, () => ({ status: 200, json: { proofs: [{ proof: real }] } }));
|
|
494
|
-
assert.equal((await fuseSet(two(), { transport: b.transport })).recovered, true);
|
|
495
|
-
const c = fakeTransport(slot, restarting);
|
|
496
|
-
await assert.rejects(fuseSet(two(), { transport: c.transport }), (e: FuseError) => e.code === "tee-restarting");
|
|
497
|
-
assert.equal(allocates(c.calls).length, 1);
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
test("18. a slot off the anchored chain is refused; nothing is built or committed", async () => {
|
|
501
|
-
const { calls, transport } = honest(key, { ...slot, chainId: "global" });
|
|
502
|
-
await assert.rejects(fuseSet(two(), { transport }), (e: FuseError) => e.code === "allocate-failed" && /anchored chain/.test(e.message));
|
|
503
|
-
assert.equal(commits(calls).length, 0);
|
|
504
|
-
});
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
// ---------------------------------------------------------------------------
|
|
508
|
-
// The result and the verification at return
|
|
509
|
-
// ---------------------------------------------------------------------------
|
|
510
|
-
|
|
511
|
-
describe("fuseSet(): the result and the checks before it is returned", () => {
|
|
512
|
-
test("19. keepFused returns each member's fused bytes; false and the default do not; the verifier's verdict only with verifyMembers", async () => {
|
|
513
|
-
const members = two();
|
|
514
|
-
const commitment = computeSlotCommitment(slot);
|
|
515
|
-
const kept = await fuseSet(members, { transport: honest(key, slot).transport, keepFused: true, verifyMembers: true });
|
|
516
|
-
for (const m of kept.members) {
|
|
517
|
-
assert.deepEqual(m.fusedBytes, getPlacement(m.placement)!.build({ original: members[m.index]!.original, commitment }));
|
|
518
|
-
assert.equal(m.verification!.category, "SET_MEMBER_DIRECT");
|
|
519
|
-
}
|
|
520
|
-
const keptOnly = await fuseSet(members, { transport: honest(key, slot).transport, keepFused: true });
|
|
521
|
-
for (const m of keptOnly.members) {
|
|
522
|
-
assert.deepEqual(m.fusedBytes, getPlacement(m.placement)!.build({ original: members[m.index]!.original, commitment }));
|
|
523
|
-
assert.ok(!("verification" in m));
|
|
524
|
-
}
|
|
525
|
-
for (const opts of [{ keepFused: false }, {}]) {
|
|
526
|
-
const r = await fuseSet(members, { transport: honest(key, slot).transport, ...opts });
|
|
527
|
-
for (const m of r.members) {
|
|
528
|
-
assert.ok(!("fusedBytes" in m));
|
|
529
|
-
assert.ok(!("verification" in m));
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
test("20. a boundary that does not echo the manifest: success, manifestEchoed false, and with verifyMembers every member verified against the explicit bytes", async () => {
|
|
535
|
-
const r = await fuseSet(two(), { transport: honest(key, slot, { withMetadata: false }).transport, verifyMembers: true });
|
|
536
|
-
assert.equal(r.manifestEchoed, false);
|
|
537
|
-
assert.equal(r.proof.metadata, undefined);
|
|
538
|
-
assert.equal(r.verification.category, "FUSED_DIRECT");
|
|
539
|
-
for (const m of r.members) {
|
|
540
|
-
assert.equal(m.verification!.category, "SET_MEMBER_DIRECT", m.verification!.reason ?? "");
|
|
541
|
-
assert.equal(m.verification!.set!.manifestSource, "argument");
|
|
542
|
-
}
|
|
543
|
-
const plain = await fuseSet(two(), { transport: honest(key, slot, { withMetadata: false }).transport });
|
|
544
|
-
assert.equal(plain.manifestEchoed, false);
|
|
545
|
-
assert.equal(plain.members.length, 2);
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
test("21. an echoed manifest that differs from the committed one is verification-failed; no result", async () => {
|
|
549
|
-
const { transport } = fakeTransport(slot, async (_calls, body) => {
|
|
550
|
-
const proof = await mintFromBody({ key, slot, body });
|
|
551
|
-
const m = proof.metadata![SET_METADATA_KEY] as SetManifest;
|
|
552
|
-
m.members[0]!.placement = m.members[0]!.placement === "trailer/1" ? "container/1" : "trailer/1";
|
|
553
|
-
return { status: 200, json: { proof } };
|
|
554
|
-
});
|
|
555
|
-
await assert.rejects(fuseSet(two(), { transport }), (e: FuseError) => e.code === "verification-failed" && /echoes/.test(e.message));
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
test("22. a proof committing another digest, or signed under another title, is verification-failed", async () => {
|
|
559
|
-
const a = honest(key, slot, { artifactDigestB64: bytesToBase64(sha256(unrelated)) });
|
|
560
|
-
await assert.rejects(fuseSet(two(), { transport: a.transport }), (e: FuseError) => e.code === "verification-failed" && /NO_MATCH/.test(e.message) && /as a set/.test(e.message));
|
|
561
|
-
const b = honest(key, slot, { attribution: fuseAttribution("trailer/1") });
|
|
562
|
-
await assert.rejects(fuseSet(two(), { transport: b.transport }), (e: FuseError) => e.code === "verification-failed");
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
test("23. the 51st file against the real verifier: bytes carrying the commitment but listed nowhere are SET_NOT_MEMBER; unrelated bytes NO_MATCH", async () => {
|
|
566
|
-
const r = await fuseSet(two(), { transport: honest(key, slot).transport });
|
|
567
|
-
const stray = getPlacement("trailer/1")!.build({ original: unrelated, commitment: computeSlotCommitment(slot) });
|
|
568
|
-
const s = await verifyFuseMember({ proof: r.proof, bytes: stray, manifest: r.manifestBytes });
|
|
569
|
-
assert.equal(s.category, "SET_NOT_MEMBER", s.reason ?? "");
|
|
570
|
-
assert.equal(s.set!.member, null);
|
|
571
|
-
assert.match(s.reason!, /not among the 2 members/);
|
|
572
|
-
const n = await verifyFuseMember({ proof: r.proof, bytes: unrelated, manifest: r.manifestBytes });
|
|
573
|
-
assert.equal(n.category, "NO_MATCH");
|
|
574
|
-
});
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
// ---------------------------------------------------------------------------
|
|
578
|
-
// fuse() regression and wiring
|
|
579
|
-
// ---------------------------------------------------------------------------
|
|
580
|
-
|
|
581
|
-
describe("wiring", () => {
|
|
582
|
-
test("24. fuse() through the same transport still sends the placement and origin in the attribution and no metadata", async () => {
|
|
583
|
-
const { calls, transport } = honest(key, slot);
|
|
584
|
-
const r = await fuse(builderFor("trailer/1", original), { placement: "trailer/1", original, transport });
|
|
585
|
-
assert.equal(r.verification.category, "FUSED_DIRECT", r.verification.reason ?? "");
|
|
586
|
-
assert.equal(r.frame.type, "bitgraph-fuse/1");
|
|
587
|
-
const body = commits(calls)[0]!.body as Record<string, unknown>;
|
|
588
|
-
assert.deepEqual(body.attribution, { name: "bitgraph-fuse/1", title: "trailer/1", message: bytesToBase64(sha256(original)) });
|
|
589
|
-
assert.ok(!("metadata" in body));
|
|
590
|
-
assert.deepEqual(Object.keys(body), ["digests", "slotId", "slot", "chainId", "attribution"]);
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
test("25. test:core lists this file and the core index exports fuseSet and MAX_SET_MEMBERS", () => {
|
|
594
|
-
const pkg = JSON.parse(readFileSync(fileURLToPath(new URL("../../package.json", import.meta.url)), "utf8")) as { scripts: Record<string, string> };
|
|
595
|
-
assert.ok(pkg.scripts["test:core"]!.split(" ").includes("dist/__tests__/fuse-set-sdk.test.js"));
|
|
596
|
-
const index = readFileSync(fileURLToPath(new URL("../../src/index.ts", import.meta.url)), "utf8");
|
|
597
|
-
assert.match(index, /export \{[^}]*\bfuseSet\b[^}]*\} from "\.\/fuse\.js"/);
|
|
598
|
-
assert.match(index, /export \{[^}]*\bMAX_SET_MEMBERS\b[^}]*\} from "\.\/fuse\.js"/);
|
|
599
|
-
assert.equal(typeof fuseSet, "function");
|
|
600
|
-
assert.equal(MAX_SET_MEMBERS, 2000);
|
|
601
|
-
// The verify-package types the set results are made of are reachable from the core entry.
|
|
602
|
-
const typeExports = [...index.matchAll(/export type \{([^}]*)\} from "@mikeargento\/bitgraph-verify"/g)].flatMap((m) => m[1]!.split(",").map((s) => s.trim()));
|
|
603
|
-
for (const name of ["SetManifest", "FuseMemberResult", "FuseVerifyResult", "FuseFrame", "PlacementId"]) assert.ok(typeExports.includes(name), `index.ts re-exports type ${name}`);
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
test("26. FUSE.md: Sets is its own subsection, the echo is stated per boundary, and the return-time binding and its costs are stated", () => {
|
|
607
|
-
const doc = readFileSync(fileURLToPath(new URL("../../docs/fuse/FUSE.md", import.meta.url)), "utf8");
|
|
608
|
-
const headings = [...doc.matchAll(/^(##+) (.+)$/gm)].map((m) => `${m[1]} ${m[2]}`);
|
|
609
|
-
const sets = headings.indexOf("### Sets");
|
|
610
|
-
assert.ok(sets >= 0, "a Sets subsection");
|
|
611
|
-
assert.equal(headings[sets + 1], "### Harness", "the single-fuse harness copy sits under its own heading, not under Sets");
|
|
612
|
-
assert.equal(headings[sets + 2], "## Ledger");
|
|
613
|
-
const setsText = doc.slice(doc.indexOf("### Sets"), doc.indexOf("### Harness"));
|
|
614
|
-
assert.ok(!setsText.includes("Bounded copy, verbatim"), "no bounded copy inside Sets");
|
|
615
|
-
assert.ok(!/the parent and the enclave do echo/.test(doc), "the parent path does not echo the manifest");
|
|
616
|
-
assert.match(setsText, /Enclave v6 .*keeps metadata on a held-slot commit/s);
|
|
617
|
-
assert.match(setsText, /returns\s+`manifestEchoed:\s+true`/);
|
|
618
|
-
assert.match(setsText, /drops\s+metadata\s+returns\s+`manifestEchoed:\s+false`/);
|
|
619
|
-
assert.match(setsText, /bound to it by digest/);
|
|
620
|
-
assert.match(setsText, /`verifyMembers: true`/);
|
|
621
|
-
assert.match(setsText, /grows\s+with\s+the\s+square\s+of\s+the\s+member\s+count/);
|
|
622
|
-
assert.match(setsText, /native SHA-256/);
|
|
623
|
-
assert.match(setsText, /one fused copy/);
|
|
624
|
-
assert.match(setsText, /`onProgress`/);
|
|
625
|
-
assert.match(setsText, /after the commit, so the slot TTL is not\s+at risk/);
|
|
626
|
-
assert.ok(!doc.includes("\u2014"), "no em dashes");
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
test("27. bitgraph-fuse set --keep refuses two inputs that would share a fused name before any request; without --keep they proceed", async () => {
|
|
630
|
-
const { mkdtemp, mkdir, writeFile: write, rm } = await import("node:fs/promises");
|
|
631
|
-
const { tmpdir } = await import("node:os");
|
|
632
|
-
const { join } = await import("node:path");
|
|
633
|
-
const { execFile } = await import("node:child_process");
|
|
634
|
-
const { promisify } = await import("node:util");
|
|
635
|
-
const run = promisify(execFile);
|
|
636
|
-
const cli = fileURLToPath(new URL("../fuse-cli.js", import.meta.url));
|
|
637
|
-
const dir = await mkdtemp(join(tmpdir(), "bitgraph-set-cli-"));
|
|
638
|
-
try {
|
|
639
|
-
await mkdir(join(dir, "a"));
|
|
640
|
-
await mkdir(join(dir, "b"));
|
|
641
|
-
await write(join(dir, "a", "x.txt"), "one\n");
|
|
642
|
-
await write(join(dir, "b", "x.txt"), "two\n");
|
|
643
|
-
// Nothing listens on port 1: a request would fail as a network error, so exit 64 with the collision message proves no request was made.
|
|
644
|
-
const flags = ["--out", join(dir, "out"), "--base-url", "http://127.0.0.1:1"];
|
|
645
|
-
type Run = { code: number; stdout: string; stderr: string };
|
|
646
|
-
const attempt = (args: string[]): Promise<Run> => run(process.execPath, [cli, ...args]).then((r) => ({ code: 0, ...r }), (e: Run) => e);
|
|
647
|
-
const kept = await attempt(["set", join(dir, "a", "x.txt"), join(dir, "b", "x.txt"), "--keep", ...flags]);
|
|
648
|
-
assert.equal(kept.code, 64, kept.stderr);
|
|
649
|
-
assert.match(kept.stderr, /members 0 and 1 would both be written as x\.fused\.tar/);
|
|
650
|
-
assert.ok(!/no fused proof was completed/.test(kept.stderr), "refused before fuseSet ran");
|
|
651
|
-
const loose = await attempt(["set", join(dir, "a", "x.txt"), join(dir, "b", "x.txt"), ...flags]);
|
|
652
|
-
assert.equal(loose.code, 1, loose.stderr);
|
|
653
|
-
assert.match(loose.stderr, /no fused proof was completed \(network\)/, "without --keep the set reached the allocate request");
|
|
654
|
-
} finally {
|
|
655
|
-
await rm(dir, { recursive: true, force: true });
|
|
656
|
-
}
|
|
657
|
-
});
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
describe("fuseSet(): hashing and progress", () => {
|
|
661
|
-
test("28. digest(): the native hasher and the library agree at every size, and the library answers when the platform has no hasher or refuses the input", async () => {
|
|
662
|
-
for (const n of [0, 1, 55, 56, 63, 64, 65, 1000, 70_000, 1_000_003]) {
|
|
663
|
-
const b = new Uint8Array(n);
|
|
664
|
-
for (let i = 0; i < n; i++) b[i] = (i * 7 + n) & 0xff;
|
|
665
|
-
assert.equal(bytesToHex(await digest(b)), bytesToHex(sha256(b)), `size ${n}`);
|
|
666
|
-
}
|
|
667
|
-
const sample = utf8("a member hashed without WebCrypto\n");
|
|
668
|
-
const want = bytesToHex(sha256(sample));
|
|
669
|
-
const desc = Object.getOwnPropertyDescriptor(globalThis, "crypto")!;
|
|
670
|
-
try {
|
|
671
|
-
Object.defineProperty(globalThis, "crypto", { value: undefined, configurable: true, writable: true });
|
|
672
|
-
assert.equal(bytesToHex(await digest(sample)), want, "no platform hasher: the library");
|
|
673
|
-
Object.defineProperty(globalThis, "crypto", { value: { subtle: { digest: () => Promise.reject(new TypeError("refused")) } }, configurable: true, writable: true });
|
|
674
|
-
assert.equal(bytesToHex(await digest(sample)), want, "a refusing platform hasher: the library");
|
|
675
|
-
} finally {
|
|
676
|
-
Object.defineProperty(globalThis, "crypto", desc);
|
|
677
|
-
}
|
|
678
|
-
assert.equal(bytesToHex(await digest(sample)), want, "restored");
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
test("29. onProgress: hash and fuse per member before and after the slot, commit 0 of 1 then 1 of 1, verify only with verifyMembers; a throwing hook changes nothing", async () => {
|
|
682
|
-
const members: FuseSetMember[] = [...two(), { original: note, placement: "container/1" }];
|
|
683
|
-
const seen: FuseSetProgress[] = [];
|
|
684
|
-
const r = await fuseSet(members, { transport: honest(key, slot).transport, onProgress: (p) => seen.push({ ...p }) });
|
|
685
|
-
assert.equal(r.members.length, 3);
|
|
686
|
-
const of = (phase: FuseSetProgress["phase"]) => seen.filter((p) => p.phase === phase).map((p) => `${p.done}/${p.total}`);
|
|
687
|
-
assert.deepEqual(of("hash"), ["1/3", "2/3", "3/3"]);
|
|
688
|
-
assert.deepEqual(of("fuse"), ["1/3", "2/3", "3/3"]);
|
|
689
|
-
assert.deepEqual(of("commit"), ["0/1", "1/1"]);
|
|
690
|
-
assert.deepEqual(of("verify"), []);
|
|
691
|
-
assert.deepEqual(seen.map((p) => p.phase), ["hash", "hash", "hash", "fuse", "fuse", "fuse", "commit", "commit"], "hash before the slot, fuse after it, then the commit");
|
|
692
|
-
const verified: FuseSetProgress[] = [];
|
|
693
|
-
await fuseSet(members, { transport: honest(key, slot).transport, verifyMembers: true, onProgress: (p) => verified.push({ ...p }) });
|
|
694
|
-
assert.deepEqual(verified.filter((p) => p.phase === "verify").map((p) => `${p.done}/${p.total}`), ["1/3", "2/3", "3/3"]);
|
|
695
|
-
assert.equal(verified.findIndex((p) => p.phase === "verify") > verified.findIndex((p) => p.phase === "commit" && p.done === 1), true, "verify after the commit");
|
|
696
|
-
let calls = 0;
|
|
697
|
-
const thrown = await fuseSet(members, { transport: honest(key, slot).transport, onProgress: () => { calls++; throw new Error("a hook that throws"); } });
|
|
698
|
-
assert.equal(thrown.members.length, 3);
|
|
699
|
-
assert.equal(calls, 8, "every report was attempted");
|
|
700
|
-
});
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
describe("fuseSet(): loaded and hashed members", () => {
|
|
704
|
-
/** The trailer/1 member finished from a hasher state, as a scanner does: hash the original once, keep the state, add the trailer later. */
|
|
705
|
-
const savedState = (bytes: Uint8Array) => { const h = sha256.create(); h.update(bytes); return h; };
|
|
706
|
-
const finish = (h: ReturnType<typeof sha256.create>, trailer: Uint8Array) => { const c = h.clone(); c.update(trailer); return c.digest(); };
|
|
707
|
-
|
|
708
|
-
test("30. trailerBytesFor is the placement's own suffix: original followed by it hashes to the trailer/1 build, and a saved state finished with it agrees", () => {
|
|
709
|
-
const commitment = computeSlotCommitment(slot);
|
|
710
|
-
const t = trailerBytesFor(commitment);
|
|
711
|
-
assert.equal(t.length, 48);
|
|
712
|
-
assert.deepEqual(t.subarray(0, 8), utf8("BGFUSE01"));
|
|
713
|
-
assert.deepEqual(t.subarray(8, 16), new Uint8Array(8));
|
|
714
|
-
assert.deepEqual(t.subarray(16), commitment);
|
|
715
|
-
for (const o of [original, png, note, new Uint8Array(0), new Uint8Array(55).fill(1), new Uint8Array(64).fill(2), new Uint8Array(1_000_003).fill(3)]) {
|
|
716
|
-
const built = getPlacement("trailer/1")!.build({ original: o, commitment });
|
|
717
|
-
assert.deepEqual(built.subarray(o.length), t, `suffix for ${o.length} bytes`);
|
|
718
|
-
assert.deepEqual(finish(savedState(o), t), sha256(built), `finished state for ${o.length} bytes`);
|
|
719
|
-
}
|
|
720
|
-
assert.throws(() => trailerBytesFor(new Uint8Array(31)), (e: FuseError) => e.code === "bad-input");
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
test("31. a loaded member is read once, after the slot is held, checked against its named digest, and released unless kept", async () => {
|
|
724
|
-
const { calls, transport } = honest(key, slot);
|
|
725
|
-
const order: string[] = [];
|
|
726
|
-
const wrapped = { ...transport, fetch: async (url: string, init?: RequestInit) => { order.push(new URL(url).pathname); return (transport.fetch as (u: string, i?: RequestInit) => Promise<Response>)(url, init); } } as unknown as typeof transport;
|
|
727
|
-
let loads = 0;
|
|
728
|
-
const loaded: FuseSetLoadedMember = { load: async () => { loads++; order.push("load"); return original; }, originDigest: sha256(original), placement: "trailer/1", name: "photo.jpg" };
|
|
729
|
-
const r = await fuseSet([loaded, { original: png, placement: "container/1" }], { transport: wrapped });
|
|
730
|
-
assert.equal(loads, 1);
|
|
731
|
-
assert.ok(order.indexOf("load") > order.indexOf("/api/fuse/allocate"), "read after the slot is held");
|
|
732
|
-
assert.ok(order.indexOf("load") < order.indexOf("/api/fuse/commit"), "read before the commit");
|
|
733
|
-
const o = oracle(slot, [{ original, placement: "trailer/1" }, { original: png, placement: "container/1" }]);
|
|
734
|
-
assert.equal(r.members[0]!.artifactDigestB64, bytesToBase64(o.rows[0]!.artifact));
|
|
735
|
-
assert.equal(r.members[0]!.originDigestB64, bytesToBase64(sha256(original)));
|
|
736
|
-
assert.equal(r.members[0]!.fusedName, fusedNamesFor("photo.jpg", "trailer/1").fusedName);
|
|
737
|
-
assert.ok(!("fusedBytes" in r.members[0]!));
|
|
738
|
-
assert.equal(commits(calls).length, 1);
|
|
739
|
-
const kept = await fuseSet([{ ...loaded }], { transport: honest(key, slot).transport, keepFused: true });
|
|
740
|
-
assert.deepEqual(kept.members[0]!.fusedBytes, getPlacement("trailer/1")!.build({ original, commitment: computeSlotCommitment(slot) }));
|
|
741
|
-
// A wrong named digest burns the slot and commits nothing.
|
|
742
|
-
const bad = honest(key, slot);
|
|
743
|
-
await assert.rejects(fuseSet([{ load: () => original, originDigest: sha256(unrelated), placement: "trailer/1" }], { transport: bad.transport }), (e: FuseError) => e.code === "bad-input" && /^member 0: originDigest is not the SHA-256 of the loaded bytes/.test(e.message) && e.member === 0);
|
|
744
|
-
assert.equal(commits(bad.calls).length, 0);
|
|
745
|
-
assert.equal(allocates(bad.calls).length, 1);
|
|
746
|
-
// A loader that throws, or returns something other than bytes, is load-failed naming the member.
|
|
747
|
-
const thrown = honest(key, slot);
|
|
748
|
-
await assert.rejects(fuseSet([{ load: () => { throw new Error("gone"); }, originDigest: sha256(original), placement: "trailer/1" }], { transport: thrown.transport }), (e: FuseError) => e.code === "load-failed" && /^member 0: load threw: gone/.test(e.message) && e.member === 0);
|
|
749
|
-
assert.equal(commits(thrown.calls).length, 0);
|
|
750
|
-
await assert.rejects(fuseSet([{ original, placement: "trailer/1" }, { load: (() => "text") as never, originDigest: sha256(original), placement: "container/1" }], { transport: honest(key, slot).transport }), (e: FuseError) => e.code === "load-failed" && /^member 1: load must return a Uint8Array/.test(e.message) && e.member === 1);
|
|
751
|
-
// Without a placement or a digest nothing is requested.
|
|
752
|
-
const quiet = honest(key, slot);
|
|
753
|
-
await assert.rejects(fuseSet([{ load: () => original, originDigest: sha256(original) } as never], { transport: quiet.transport }), (e: FuseError) => e.code === "bad-input" && /^member 0: a loaded member names its placement/.test(e.message));
|
|
754
|
-
await assert.rejects(fuseSet([{ load: () => original, placement: "trailer/1" } as never], { transport: quiet.transport }), (e: FuseError) => e.code === "bad-input" && /^member 0: a loaded member names its originDigest/.test(e.message));
|
|
755
|
-
assert.equal(quiet.calls.length, 0);
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
test("32. a hashed member answers its fused digest for the held commitment and never shows its bytes; the row is what the real verifier finds", async () => {
|
|
759
|
-
const { calls, transport } = honest(key, slot);
|
|
760
|
-
const inputs: FusedDigestInput[] = [];
|
|
761
|
-
const state = savedState(original);
|
|
762
|
-
const hashed: FuseSetHashedMember = {
|
|
763
|
-
originDigest: sha256(original),
|
|
764
|
-
placement: "trailer/1",
|
|
765
|
-
name: "photo.jpg",
|
|
766
|
-
fusedDigest: (input) => { inputs.push(input); return finish(state, trailerBytesFor(input.commitment)); },
|
|
767
|
-
};
|
|
768
|
-
const r = await fuseSet([hashed, { original: png, placement: "container/1" }, { load: () => note, originDigest: sha256(note), placement: "container/1" }], { transport, keepFused: true });
|
|
769
|
-
assert.equal(inputs.length, 1);
|
|
770
|
-
const commitment = computeSlotCommitment(slot);
|
|
771
|
-
assert.deepEqual(inputs[0]!.commitment, commitment);
|
|
772
|
-
assert.equal(inputs[0]!.commitmentHex, bytesToHex(commitment));
|
|
773
|
-
assert.equal(inputs[0]!.slot.nonceB64, slot.nonceB64);
|
|
774
|
-
const built = getPlacement("trailer/1")!.build({ original, commitment });
|
|
775
|
-
assert.equal(r.members[0]!.artifactDigestB64, bytesToBase64(sha256(built)), "the row is the hash of the placement's build");
|
|
776
|
-
assert.ok(!("fusedBytes" in r.members[0]!), "nothing to keep for a hashed member");
|
|
777
|
-
assert.ok("fusedBytes" in r.members[1]! && "fusedBytes" in r.members[2]!, "bytes and loaded members are kept");
|
|
778
|
-
assert.equal(r.members.length, 3);
|
|
779
|
-
assert.equal((r.manifest as SetManifest).members.length, 3);
|
|
780
|
-
assert.equal(commits(calls).length, 1);
|
|
781
|
-
// The real verifier reads the member from the bytes a reader would build, and from its original.
|
|
782
|
-
const direct = await verifyFuseMember({ proof: r.proof, bytes: built, manifest: r.manifestBytes });
|
|
783
|
-
assert.equal(direct.category, "SET_MEMBER_DIRECT", direct.reason ?? "");
|
|
784
|
-
assert.equal(direct.set!.member!.index, r.members[0]!.manifestIndex);
|
|
785
|
-
const fromOrigin = await verifyFuseMember({ proof: r.proof, bytes: original, manifest: r.manifestBytes });
|
|
786
|
-
assert.equal(fromOrigin.category, "SET_MEMBER_FROM_ORIGIN", fromOrigin.reason ?? "");
|
|
787
|
-
// verifyMembers refuses a hashed member before any request; a throwing or short answer is builder-failed naming the member.
|
|
788
|
-
const quiet = honest(key, slot);
|
|
789
|
-
await assert.rejects(fuseSet([hashed], { transport: quiet.transport, verifyMembers: true }), (e: FuseError) => e.code === "bad-input" && /^member 0: a hashed member cannot be verified in full/.test(e.message) && e.member === 0);
|
|
790
|
-
assert.equal(quiet.calls.length, 0);
|
|
791
|
-
const throwing = honest(key, slot);
|
|
792
|
-
await assert.rejects(fuseSet([{ ...hashed, fusedDigest: () => { throw new Error("no state"); } }], { transport: throwing.transport }), (e: FuseError) => e.code === "builder-failed" && /^member 0: fusedDigest threw: no state/.test(e.message) && e.member === 0);
|
|
793
|
-
assert.equal(commits(throwing.calls).length, 0);
|
|
794
|
-
await assert.rejects(fuseSet([{ ...hashed, fusedDigest: () => new Uint8Array(31) }], { transport: honest(key, slot).transport }), (e: FuseError) => e.code === "builder-failed" && /^member 0: fusedDigest must return a 32-byte digest/.test(e.message));
|
|
795
|
-
// The same original as a hashed member and as a bytes member under one placement is the duplicate it always was.
|
|
796
|
-
await assert.rejects(fuseSet([hashed, { original, placement: "trailer/1" }], { transport: honest(key, slot).transport }), (e: FuseError) => e.code === "bad-input" && /^members 0 and 1 are the same original/.test(e.message));
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
test("33. progress counts every shape: hash before the slot for all three, fuse after it, one commit", async () => {
|
|
800
|
-
const seen: FuseSetProgress[] = [];
|
|
801
|
-
const state = savedState(original);
|
|
802
|
-
await fuseSet([
|
|
803
|
-
{ originDigest: sha256(original), placement: "trailer/1", fusedDigest: ({ commitment }) => finish(state, trailerBytesFor(commitment)) },
|
|
804
|
-
{ load: () => note, originDigest: sha256(note), placement: "container/1" },
|
|
805
|
-
{ original: png, placement: "container/1" },
|
|
806
|
-
], { transport: honest(key, slot).transport, onProgress: (p) => seen.push({ ...p }) });
|
|
807
|
-
assert.deepEqual(seen.map((p) => `${p.phase} ${p.done}/${p.total}`), ["hash 1/3", "hash 2/3", "hash 3/3", "fuse 1/3", "fuse 2/3", "fuse 3/3", "commit 0/1", "commit 1/1"]);
|
|
808
|
-
});
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
describe("fuseSet(): set/2, a Merkle root over the rows", () => {
|
|
812
|
-
test("34. set/2 commits the root document, returns every member's leaf index, path and evidence, and the real verifier reads each member with it", async () => {
|
|
813
|
-
const { calls, transport } = honest(key, slot);
|
|
814
|
-
const members: FuseSetBytesMember[] = [{ original, placement: "trailer/1", name: "photo.jpg" }, { original: png, placement: "container/1" }, { original: note, placement: "container/2" }];
|
|
815
|
-
const seen: FuseSetProgress[] = [];
|
|
816
|
-
const r = await fuseSet(members, { transport, set: "set/2", onProgress: (p) => seen.push({ ...p }) });
|
|
817
|
-
assert.equal(r.set, "set/2");
|
|
818
|
-
assert.equal(commits(calls).length, 1);
|
|
819
|
-
assert.equal(r.proof.attribution?.title, "set/2");
|
|
820
|
-
assert.equal(r.verification.category, "FUSED_DIRECT");
|
|
821
|
-
assert.equal(r.verification.placement, "set/2");
|
|
822
|
-
assert.ok(r.treeRootB64);
|
|
823
|
-
const doc = parseSetRoot(r.manifestBytes)!;
|
|
824
|
-
assert.equal(doc.count, 3);
|
|
825
|
-
assert.equal(bytesToBase64(doc.root), r.treeRootB64);
|
|
826
|
-
assert.equal(bytesToBase64(sha256(r.manifestBytes)), r.proof.artifact.digestB64);
|
|
827
|
-
assert.deepEqual(r.proof.metadata?.[SET_METADATA_KEY], r.manifest, "the root document rides under metadata");
|
|
828
|
-
assert.ok(seen.some((p) => p.phase === "tree" && p.done === 1), "a tree phase is reported");
|
|
829
|
-
const commitment = computeSlotCommitment(slot);
|
|
830
|
-
for (const m of r.members) {
|
|
831
|
-
assert.ok(m.path && m.memberProof, `member ${m.index} carries its path and evidence`);
|
|
832
|
-
assert.equal(m.memberProof!.index, m.manifestIndex);
|
|
833
|
-
assert.equal(m.memberProof!.count, 3);
|
|
834
|
-
const built = getPlacement(m.placement)!.build({ original: members[m.index]!.original, commitment });
|
|
835
|
-
assert.equal(bytesToBase64(sha256(built)), m.artifactDigestB64);
|
|
836
|
-
const direct = await verifyFuseMember({ proof: r.proof, bytes: built, member: m.memberProof });
|
|
837
|
-
assert.equal(direct.category, "SET_MEMBER_DIRECT", direct.reason ?? "");
|
|
838
|
-
assert.equal(direct.set!.member!.index, m.manifestIndex);
|
|
839
|
-
const fromOrigin = await verifyFuseMember({ proof: r.proof, bytes: members[m.index]!.original, member: m.memberProof });
|
|
840
|
-
assert.equal(fromOrigin.category, "SET_MEMBER_FROM_ORIGIN", fromOrigin.reason ?? "");
|
|
841
|
-
assert.equal((await verifyFuseMember({ proof: r.proof, bytes: built })).category, "SET_MEMBERSHIP_UNPROVEN", "no evidence in hand");
|
|
842
|
-
}
|
|
843
|
-
// set/1 keeps its cap; set/2 takes more than MAX_SET_MEMBERS.
|
|
844
|
-
await assert.rejects(fuseSet([{ original, placement: "trailer/1" }], { transport: honest(key, slot).transport, set: "set/3" as never }), (e: FuseError) => e.code === "bad-input");
|
|
845
|
-
const many: FuseSetMember[] = Array.from({ length: MAX_SET_MEMBERS + 1 }, (_, i) => ({ originDigest: sha256(utf8(`m${i}`)), placement: "trailer/1", fusedDigest: () => sha256(utf8(`f${i}`)) }));
|
|
846
|
-
await assert.rejects(fuseSet(many, { transport: honest(key, slot).transport }), (e: FuseError) => e.code === "bad-input" && /at most 2000/.test(e.message));
|
|
847
|
-
const big = await fuseSet(many, { transport: honest(key, slot).transport, set: "set/2" });
|
|
848
|
-
assert.equal(big.members.length, MAX_SET_MEMBERS + 1);
|
|
849
|
-
assert.equal(parseSetRoot(big.manifestBytes)!.count, MAX_SET_MEMBERS + 1);
|
|
850
|
-
assert.ok(big.members.every((m) => m.path!.length <= 11), "paths of at most ceil(log2 2001) = 11 nodes");
|
|
851
|
-
});
|
|
852
|
-
});
|