@mikeargento/bitgraph 1.9.1 → 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/package.json +9 -6
- 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-inline.test.d.ts +0 -2
- package/dist/__tests__/fuse-inline.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-inline.test.js +0 -181
- package/dist/__tests__/fuse-inline.test.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 -889
- 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 -454
- 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__/run.test.d.ts +0 -2
- package/dist/__tests__/run.test.d.ts.map +0 -1
- package/dist/__tests__/run.test.js +0 -178
- package/dist/__tests__/run.test.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-inline.test.ts +0 -211
- 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 -1027
- 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 -576
- 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,1027 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* BitGraph Sets, phase 1: the set manifest codec, the set/1 placement,
|
|
5
|
-
* verifyFuse compatibility, and verifyFuseMember on locally signed set
|
|
6
|
-
* proofs. Every negative vector runs against the real verifier. Fixtures
|
|
7
|
-
* are minted with a local signer only: a 32-byte nonce (the commitment
|
|
8
|
-
* needs one), a slot body signed with the same key, commit.slotHashB64
|
|
9
|
-
* bound to it, the manifest as the artifact, fuseAttribution("set/1")
|
|
10
|
-
* inside the signed body, and the parsed manifest attached OUTSIDE it.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { describe, test, before } from "node:test";
|
|
14
|
-
import * as assert from "node:assert/strict";
|
|
15
|
-
import { readFileSync } from "node:fs";
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { signAsync } from "@noble/ed25519";
|
|
18
|
-
import { sha256 } from "@noble/hashes/sha256";
|
|
19
|
-
import {
|
|
20
|
-
PLACEMENTS,
|
|
21
|
-
getPlacement,
|
|
22
|
-
canonicalize,
|
|
23
|
-
canonicalSlotBody,
|
|
24
|
-
computeSlotCommitment,
|
|
25
|
-
buildFusePayload,
|
|
26
|
-
parseFusePayload,
|
|
27
|
-
buildSetManifest,
|
|
28
|
-
parseSetManifest,
|
|
29
|
-
readSetMetadata,
|
|
30
|
-
fuseAttribution,
|
|
31
|
-
verifyFuse,
|
|
32
|
-
verifyFuseMember,
|
|
33
|
-
resetEpochLinkState,
|
|
34
|
-
bytesToBase64,
|
|
35
|
-
base64ToBytes,
|
|
36
|
-
bytesToHex,
|
|
37
|
-
hexToBytes,
|
|
38
|
-
bytesEqual,
|
|
39
|
-
SET_METADATA_KEY,
|
|
40
|
-
SET_PLACEMENT_ID,
|
|
41
|
-
} from "@mikeargento/bitgraph-verify";
|
|
42
|
-
import type {
|
|
43
|
-
BitGraphProof,
|
|
44
|
-
SlotAllocation,
|
|
45
|
-
Attribution,
|
|
46
|
-
SetMember,
|
|
47
|
-
SetManifest,
|
|
48
|
-
FuseMemberOptions,
|
|
49
|
-
FuseMemberResult,
|
|
50
|
-
FuseVerifyResult,
|
|
51
|
-
} from "@mikeargento/bitgraph-verify";
|
|
52
|
-
import { makeKey, signBody, b64, utf8 } from "./audit-fixtures.js";
|
|
53
|
-
import type { ManualKey } from "./audit-fixtures.js";
|
|
54
|
-
|
|
55
|
-
/* ⚠️ UPPERCASE THE WHOLE DIGEST, NOT ITS FIRST EIGHT CHARACTERS.
|
|
56
|
-
*
|
|
57
|
-
* Both call sites used to do `.replace(/"digest":"([0-9a-f]{8})/, upper)`, and
|
|
58
|
-
* that is a COIN FLIP. A manifest is sorted ascending by artifact digest, so
|
|
59
|
-
* the first digest in the text is the minimum of the set — which skews toward
|
|
60
|
-
* leading zeros and digits — and when all eight of those characters happen to
|
|
61
|
-
* be 0-9, uppercasing them changes nothing at all. The bytes stay canonical,
|
|
62
|
-
* parseSetManifest correctly accepts them, and the assertion that it should
|
|
63
|
-
* have REFUSED them fails. Measured: about 3-4% of runs, so roughly one red CI
|
|
64
|
-
* run in twenty-five, on a test that is not wrong about anything.
|
|
65
|
-
*
|
|
66
|
-
* A full 64-character hex digest with no letter in it has probability ~1e-15.
|
|
67
|
-
* And the assertion below turns the remaining impossibility into a loud
|
|
68
|
-
* failure rather than a silent no-op that passes for the wrong reason — which
|
|
69
|
-
* is what the old version did every time it did not flake.
|
|
70
|
-
*/
|
|
71
|
-
function withUppercasedDigest(text: string): string {
|
|
72
|
-
const upper = text.replace(/"digest":"([0-9a-f]{64})"/, (_m, h: string) => `"digest":"${h.toUpperCase()}"`);
|
|
73
|
-
assert.notEqual(upper, text, "the fixture has no lowercase hex digest to uppercase; this check would pass vacuously");
|
|
74
|
-
return upper;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const FIX = fileURLToPath(new URL("../../src/__tests__/fuse-fixtures/", import.meta.url));
|
|
78
|
-
const bytes = (name: string) => new Uint8Array(readFileSync(FIX + name));
|
|
79
|
-
const proofOf = (name: string) => JSON.parse(readFileSync(FIX + name, "utf8")) as BitGraphProof;
|
|
80
|
-
|
|
81
|
-
interface Vec { commitmentHex: string; fusedDigestB64: string; originDigestB64: string }
|
|
82
|
-
const V = JSON.parse(readFileSync(FIX + "vectors.json", "utf8")) as { synthetic: { commitmentHex: string }; trailerFixture: Vec; containerFixture: Vec };
|
|
83
|
-
|
|
84
|
-
const original = bytes("original.txt");
|
|
85
|
-
const png = bytes("image.png");
|
|
86
|
-
const note = utf8("a third member, plain text made for the set\n");
|
|
87
|
-
const bOnly = utf8("a file that belongs to set B alone\n");
|
|
88
|
-
const unrelated = utf8("a file nobody recorded\n");
|
|
89
|
-
|
|
90
|
-
const trailer = getPlacement("trailer/1")!;
|
|
91
|
-
const container = getPlacement("container/1")!;
|
|
92
|
-
const enc = (s: string) => new TextEncoder().encode(s);
|
|
93
|
-
const dec = (b: Uint8Array) => new TextDecoder().decode(b);
|
|
94
|
-
const asObject = (m: Uint8Array) => JSON.parse(dec(m)) as SetManifest;
|
|
95
|
-
const compact = (o: unknown) => enc(JSON.stringify(o));
|
|
96
|
-
const digestField = (d: Uint8Array) => ({ algorithm: "sha256" as const, digest: bytesToHex(d) });
|
|
97
|
-
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
// Local signer: slots, set proofs, sets
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
|
|
102
|
-
const EPOCH = bytesToBase64(new Uint8Array(32).fill(0x5e));
|
|
103
|
-
|
|
104
|
-
/** A slot record signed by `key`, with a 32-byte nonce and the anchored chain. */
|
|
105
|
-
async function allocateSlot(key: ManualKey, counter: string): Promise<SlotAllocation> {
|
|
106
|
-
const body = {
|
|
107
|
-
version: "bitgraph/slot/1" as const,
|
|
108
|
-
nonceB64: b64(crypto.getRandomValues(new Uint8Array(32))),
|
|
109
|
-
counter,
|
|
110
|
-
epochId: EPOCH,
|
|
111
|
-
publicKeyB64: key.publicKeyB64,
|
|
112
|
-
chainId: "bitgraph:main",
|
|
113
|
-
};
|
|
114
|
-
return { ...body, signatureB64: b64(await signAsync(canonicalize(body), key.privateKey)) };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
interface MintOptions {
|
|
118
|
-
key: ManualKey;
|
|
119
|
-
slot: SlotAllocation;
|
|
120
|
-
commitCounter: string;
|
|
121
|
-
manifestBytes: Uint8Array;
|
|
122
|
-
/** Attach the parsed manifest under proof.metadata (default true). */
|
|
123
|
-
withMetadata?: boolean;
|
|
124
|
-
/** The signed attribution; default fuseAttribution("set/1"); null for none at all. */
|
|
125
|
-
attribution?: Attribution | null;
|
|
126
|
-
/** Override the artifact digest; the proof stays validly signed. */
|
|
127
|
-
artifactDigestB64?: string;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** A set proof: the manifest is the artifact, set/1 is the signed title, the slot is consumed. */
|
|
131
|
-
async function mintSetProof(o: MintOptions): Promise<BitGraphProof> {
|
|
132
|
-
const commit: BitGraphProof["commit"] = {
|
|
133
|
-
nonceB64: o.slot.nonceB64,
|
|
134
|
-
counter: o.commitCounter,
|
|
135
|
-
epochId: o.slot.epochId,
|
|
136
|
-
slotCounter: o.slot.counter,
|
|
137
|
-
slotHashB64: b64(sha256(canonicalize(canonicalSlotBody(o.slot)))),
|
|
138
|
-
};
|
|
139
|
-
(commit as unknown as Record<string, unknown>)["chainId"] = "bitgraph:main";
|
|
140
|
-
const attribution = o.attribution === undefined ? fuseAttribution("set/1") : o.attribution;
|
|
141
|
-
const proof = await signBody(
|
|
142
|
-
o.key,
|
|
143
|
-
{ hashAlg: "sha256", digestB64: o.artifactDigestB64 ?? b64(sha256(o.manifestBytes)) },
|
|
144
|
-
commit,
|
|
145
|
-
"test-measurement-set",
|
|
146
|
-
attribution === null ? undefined : { attribution },
|
|
147
|
-
);
|
|
148
|
-
proof.slotAllocation = o.slot;
|
|
149
|
-
if (o.withMetadata !== false) proof.metadata = { [SET_METADATA_KEY]: JSON.parse(dec(o.manifestBytes)) as SetManifest };
|
|
150
|
-
return proof;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
type ByteExact = "trailer/1" | "container/1";
|
|
154
|
-
interface Fused { name: string; original: Uint8Array; placement: ByteExact; bytes: Uint8Array; row: SetMember }
|
|
155
|
-
interface SetFixture {
|
|
156
|
-
key: ManualKey;
|
|
157
|
-
slot: SlotAllocation;
|
|
158
|
-
commitCounter: string;
|
|
159
|
-
commitment: Uint8Array;
|
|
160
|
-
fused: Fused[];
|
|
161
|
-
rows: SetMember[];
|
|
162
|
-
manifest: Uint8Array;
|
|
163
|
-
/** The rows as the manifest lists them: ascending by artifact digest. */
|
|
164
|
-
sorted: SetMember[];
|
|
165
|
-
proof: BitGraphProof;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function fuseWith(commitment: Uint8Array, original: Uint8Array, placement: ByteExact, name: string): Fused {
|
|
169
|
-
const fused = getPlacement(placement)!.build({ original, commitment });
|
|
170
|
-
return { name, original, placement, bytes: fused, row: { artifact: sha256(fused), origin: sha256(original), placement } };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async function makeSet(o: {
|
|
174
|
-
slotCounter: string;
|
|
175
|
-
commitCounter: string;
|
|
176
|
-
files: (commitment: Uint8Array) => Array<[Uint8Array, ByteExact, string]>;
|
|
177
|
-
key?: ManualKey;
|
|
178
|
-
}): Promise<SetFixture> {
|
|
179
|
-
const key = o.key ?? (await makeKey());
|
|
180
|
-
const slot = await allocateSlot(key, o.slotCounter);
|
|
181
|
-
const commitment = computeSlotCommitment(slot);
|
|
182
|
-
const fused = o.files(commitment).map(([orig, placement, name]) => fuseWith(commitment, orig, placement, name));
|
|
183
|
-
const rows = fused.map((f) => f.row);
|
|
184
|
-
const manifest = buildSetManifest(commitment, rows);
|
|
185
|
-
const proof = await mintSetProof({ key, slot, commitCounter: o.commitCounter, manifestBytes: manifest });
|
|
186
|
-
return { key, slot, commitCounter: o.commitCounter, commitment, fused, rows, manifest, sorted: parseSetManifest(manifest)!.members, proof };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const byName = (s: SetFixture, name: string) => s.fused.find((f) => f.name === name)!;
|
|
190
|
-
const sortedIndex = (s: SetFixture, f: Fused) => s.sorted.findIndex((r) => bytesEqual(r.artifact, f.row.artifact));
|
|
191
|
-
/** A fresh proof under set `s`'s slot and key, committing `manifestBytes` (or anything else). */
|
|
192
|
-
const mintUnder = (s: SetFixture, manifestBytes: Uint8Array, extra: Partial<MintOptions> = {}) =>
|
|
193
|
-
mintSetProof({ key: s.key, slot: s.slot, commitCounter: s.commitCounter, manifestBytes, ...extra });
|
|
194
|
-
/** Clone a proof and mutate its (unsigned) metadata manifest in place. */
|
|
195
|
-
function withMetadata(proof: BitGraphProof, mutate: (m: SetManifest) => void): BitGraphProof {
|
|
196
|
-
const p = structuredClone(proof);
|
|
197
|
-
mutate(p.metadata![SET_METADATA_KEY] as SetManifest);
|
|
198
|
-
return p;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
let A!: SetFixture;
|
|
202
|
-
let B!: SetFixture;
|
|
203
|
-
|
|
204
|
-
before(async () => {
|
|
205
|
-
A = await makeSet({
|
|
206
|
-
slotCounter: "10",
|
|
207
|
-
commitCounter: "14",
|
|
208
|
-
files: () => [[original, "trailer/1", "original.txt"], [png, "container/1", "image.png"], [note, "trailer/1", "note.txt"]],
|
|
209
|
-
});
|
|
210
|
-
B = await makeSet({
|
|
211
|
-
slotCounter: "3",
|
|
212
|
-
commitCounter: "4",
|
|
213
|
-
files: () => [[original, "trailer/1", "original.txt"], [bOnly, "container/1", "b-only.txt"]],
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// Every verifyFuseMember result produced in this file, for the invariants at the end.
|
|
218
|
-
const collected: Array<{ proof: BitGraphProof; bytes: Uint8Array; result: FuseMemberResult }> = [];
|
|
219
|
-
type Extra = Partial<Pick<FuseMemberOptions, "manifest" | "trustAnchors" | "maxPositions">>;
|
|
220
|
-
async function member(proof: BitGraphProof, file: Uint8Array, extra: Extra = {}): Promise<FuseMemberResult> {
|
|
221
|
-
resetEpochLinkState();
|
|
222
|
-
const result = await verifyFuseMember({ proof, bytes: file, ...extra });
|
|
223
|
-
collected.push({ proof, bytes: file, result });
|
|
224
|
-
return result;
|
|
225
|
-
}
|
|
226
|
-
async function fuse(proof: BitGraphProof, file: Uint8Array, extra: Partial<Parameters<typeof verifyFuse>[0]> = {}): Promise<FuseVerifyResult> {
|
|
227
|
-
resetEpochLinkState();
|
|
228
|
-
return verifyFuse({ proof, bytes: file, ...extra });
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// ---------------------------------------------------------------------------
|
|
232
|
-
// Codec
|
|
233
|
-
// ---------------------------------------------------------------------------
|
|
234
|
-
|
|
235
|
-
/** Pinned: the two-member manifest over the synthetic slot of vectors.json. */
|
|
236
|
-
const PINNED_MANIFEST =
|
|
237
|
-
'{"members":[{"artifact":{"algorithm":"sha256","digest":"7e4bd8972297bd71323bebe73839a276bf5c57b5fa9cfd6737617c3723600400"},"origin":{"algorithm":"sha256","digest":"2e824794554ffb36fdfa7a1c0b207ec958bb26b1dad54cebeff35358938e4327"},"placement":"trailer/1"},{"artifact":{"algorithm":"sha256","digest":"de5319f187580c1093f57e4b0578380aa500aee44f2938902ec4d6aeef5fff64"},"origin":{"algorithm":"sha256","digest":"497790947d4666760ce38f3c00e852c71fdb66cae849bae8e9ede352719e1581"},"placement":"container/1"}],"placement":"set/1","slotCommitment":{"algorithm":"sha256","digest":"0e658007a8aaecce318b2f594581b880e8ecc34af0ff54cd0c85624b42cb94b7"},"type":"bitgraph-fuse/1"}';
|
|
238
|
-
const PINNED_MANIFEST_SHA256_HEX = "0dc745cac9b6be669b97018feecd2e9e6e92d73af9de051f4b9d87791f0ace3d";
|
|
239
|
-
|
|
240
|
-
describe("codec: buildSetManifest", () => {
|
|
241
|
-
const vecRow = (v: Vec, placement: string): SetMember => ({ artifact: base64ToBytes(v.fusedDigestB64)!, origin: base64ToBytes(v.originDigestB64)!, placement });
|
|
242
|
-
|
|
243
|
-
test("1. pinned bytes over the synthetic slot: rows ascending by artifact, fixed key order, no whitespace", () => {
|
|
244
|
-
const c = hexToBytes(V.synthetic.commitmentHex)!;
|
|
245
|
-
const built = buildSetManifest(c, [vecRow(V.containerFixture, "container/1"), vecRow(V.trailerFixture, "trailer/1")]);
|
|
246
|
-
assert.equal(dec(built), PINNED_MANIFEST);
|
|
247
|
-
assert.equal(bytesToHex(sha256(built)), PINNED_MANIFEST_SHA256_HEX);
|
|
248
|
-
assert.equal(dec(buildSetManifest(c, [vecRow(V.trailerFixture, "trailer/1"), vecRow(V.containerFixture, "container/1")])), PINNED_MANIFEST);
|
|
249
|
-
const o = asObject(built);
|
|
250
|
-
assert.deepEqual(Object.keys(o), ["members", "placement", "slotCommitment", "type"]);
|
|
251
|
-
assert.deepEqual(Object.keys(o.members[0]!), ["artifact", "origin", "placement"]);
|
|
252
|
-
assert.ok(o.members[0]!.artifact.digest < o.members[1]!.artifact.digest, "ascending by artifact hex");
|
|
253
|
-
assert.doesNotMatch(dec(built), /\s/);
|
|
254
|
-
assert.equal(o.placement, "set/1");
|
|
255
|
-
assert.equal(o.type, "bitgraph-fuse/1");
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
test("2. the same members in every input order give identical bytes and digest", () => {
|
|
259
|
-
const rows = A.rows;
|
|
260
|
-
const orders = [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]];
|
|
261
|
-
const first = buildSetManifest(A.commitment, rows);
|
|
262
|
-
for (const order of orders) {
|
|
263
|
-
const built = buildSetManifest(A.commitment, order.map((i) => rows[i]!));
|
|
264
|
-
assert.deepEqual(built, first);
|
|
265
|
-
assert.equal(bytesToHex(sha256(built)), bytesToHex(sha256(first)));
|
|
266
|
-
}
|
|
267
|
-
assert.deepEqual(first, A.manifest);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
test("3. refuses a duplicate artifact, an empty list, wrong lengths, a bad placement, and a nested set", () => {
|
|
271
|
-
const [r0, r1] = A.rows as [SetMember, SetMember, SetMember];
|
|
272
|
-
assert.throws(() => buildSetManifest(A.commitment, [r0, r1, { ...r0, origin: r1.origin }]), /duplicate/);
|
|
273
|
-
assert.throws(() => buildSetManifest(A.commitment, []), /at least one/);
|
|
274
|
-
assert.throws(() => buildSetManifest(A.commitment, [{ ...r0, artifact: new Uint8Array(31) }]), /32 bytes/);
|
|
275
|
-
assert.throws(() => buildSetManifest(A.commitment, [{ ...r0, origin: new Uint8Array(31) }]), /32 bytes/);
|
|
276
|
-
assert.throws(() => buildSetManifest(new Uint8Array(33), [r0]), /32 bytes/);
|
|
277
|
-
assert.throws(() => buildSetManifest(A.commitment, [{ ...r0, placement: "xmp" }]), /placement/);
|
|
278
|
-
assert.throws(() => buildSetManifest(A.commitment, [{ ...r0, placement: "set/1" }]), /set/);
|
|
279
|
-
});
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
describe("codec: parseSetManifest", () => {
|
|
283
|
-
test("4. canonical bytes round-trip commitment and rows byte for byte, twice", () => {
|
|
284
|
-
for (let i = 0; i < 2; i++) {
|
|
285
|
-
const parsed = parseSetManifest(A.manifest)!;
|
|
286
|
-
assert.ok(parsed);
|
|
287
|
-
assert.deepEqual(parsed.commitment, A.commitment);
|
|
288
|
-
assert.equal(parsed.members.length, 3);
|
|
289
|
-
assert.deepEqual(buildSetManifest(parsed.commitment, parsed.members), A.manifest);
|
|
290
|
-
for (const row of A.rows) {
|
|
291
|
-
const found = parsed.members.find((m) => bytesEqual(m.artifact, row.artifact))!;
|
|
292
|
-
assert.deepEqual(found.origin, row.origin);
|
|
293
|
-
assert.equal(found.placement, row.placement);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
test("5. whitespace: pretty-printed JSON and a trailing newline are refused", () => {
|
|
299
|
-
assert.equal(parseSetManifest(enc(JSON.stringify(asObject(A.manifest), null, 2))), null, "pretty");
|
|
300
|
-
assert.equal(parseSetManifest(enc(dec(A.manifest) + "\n")), null, "trailing newline");
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
test("6. key order: reordered top-level keys and reordered row keys are refused", () => {
|
|
304
|
-
const o = asObject(A.manifest);
|
|
305
|
-
const top = { type: o.type, slotCommitment: o.slotCommitment, placement: o.placement, members: o.members };
|
|
306
|
-
assert.equal(parseSetManifest(compact(top)), null, "top level");
|
|
307
|
-
const row = o.members[0]!;
|
|
308
|
-
const rows = [{ origin: row.origin, artifact: row.artifact, placement: row.placement }, ...o.members.slice(1)];
|
|
309
|
-
assert.equal(parseSetManifest(compact({ ...o, members: rows })), null, "row");
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
test("7. duplicate keys: a repeated top-level type and a repeated row placement are refused", () => {
|
|
313
|
-
const text = dec(A.manifest);
|
|
314
|
-
assert.equal(parseSetManifest(enc(text.replace('"type":"bitgraph-fuse/1"', '"type":"bitgraph-fuse/1","type":"bitgraph-fuse/1"'))), null, "type");
|
|
315
|
-
const firstPlacement = /"placement":"(trailer|container)\/1"/.exec(text)![0];
|
|
316
|
-
assert.equal(parseSetManifest(enc(text.replace(firstPlacement, `${firstPlacement},${firstPlacement}`))), null, "row placement");
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
test("8. digests: uppercase hex, 63- and 65-char hex, and other algorithm spellings are refused", () => {
|
|
320
|
-
const text = dec(A.manifest);
|
|
321
|
-
assert.equal(parseSetManifest(enc(withUppercasedDigest(text))), null, "uppercase");
|
|
322
|
-
const o = asObject(A.manifest);
|
|
323
|
-
const short = structuredClone(o); short.members[0]!.artifact.digest = short.members[0]!.artifact.digest.slice(0, 63);
|
|
324
|
-
assert.equal(parseSetManifest(compact(short)), null, "63 chars");
|
|
325
|
-
const long = structuredClone(o); long.members[0]!.origin.digest = `${long.members[0]!.origin.digest}0`;
|
|
326
|
-
assert.equal(parseSetManifest(compact(long)), null, "65 chars");
|
|
327
|
-
const upper = structuredClone(o); (upper.slotCommitment as { algorithm: string }).algorithm = "SHA256";
|
|
328
|
-
assert.equal(parseSetManifest(compact(upper)), null, "SHA256");
|
|
329
|
-
const dashed = structuredClone(o); (dashed.members[1]!.artifact as { algorithm: string }).algorithm = "sha-256";
|
|
330
|
-
assert.equal(parseSetManifest(compact(dashed)), null, "sha-256");
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
test("9. shape: extra keys, a missing origin, members as an object or empty, a BOM, and invalid UTF-8 are refused", () => {
|
|
334
|
-
const o = asObject(A.manifest);
|
|
335
|
-
assert.equal(parseSetManifest(compact({ ...o, extra: 1 })), null, "extra top-level key");
|
|
336
|
-
assert.equal(parseSetManifest(compact({ ...o, members: [{ ...o.members[0]!, extra: 1 }, ...o.members.slice(1)] })), null, "extra row key");
|
|
337
|
-
const { origin: _o, ...noOrigin } = o.members[0]!;
|
|
338
|
-
assert.equal(parseSetManifest(compact({ ...o, members: [noOrigin, ...o.members.slice(1)] })), null, "row missing origin");
|
|
339
|
-
assert.equal(parseSetManifest(compact({ ...o, members: {} })), null, "members as object");
|
|
340
|
-
assert.equal(parseSetManifest(compact({ ...o, members: [] })), null, "members empty");
|
|
341
|
-
assert.equal(parseSetManifest(new Uint8Array([0xef, 0xbb, 0xbf, ...A.manifest])), null, "BOM");
|
|
342
|
-
assert.equal(parseSetManifest(new Uint8Array([0xff, 0xfe])), null, "invalid UTF-8");
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
test("10. rows: descending order and a repeated artifact digest are refused", () => {
|
|
346
|
-
const o = asObject(A.manifest);
|
|
347
|
-
assert.equal(parseSetManifest(compact({ ...o, members: [...o.members].reverse() })), null, "descending");
|
|
348
|
-
assert.equal(parseSetManifest(compact({ ...o, members: [o.members[0]!, o.members[0]!, o.members[1]!] })), null, "repeated artifact");
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
test("11. identity: another type, another set version, or a nested set is refused; an unregistered placement parses", () => {
|
|
352
|
-
const o = asObject(A.manifest);
|
|
353
|
-
assert.equal(parseSetManifest(compact({ ...o, type: "bitgraph-fuse/2" })), null, "type");
|
|
354
|
-
assert.equal(parseSetManifest(compact({ ...o, placement: "set/2" })), null, "set/2");
|
|
355
|
-
assert.equal(parseSetManifest(compact({ ...o, members: [{ ...o.members[0]!, placement: "set/1" }, ...o.members.slice(1)] })), null, "nested set");
|
|
356
|
-
const xmp = buildSetManifest(A.commitment, [{ ...A.rows[0]!, placement: "xmp/9" }]);
|
|
357
|
-
const parsed = parseSetManifest(xmp)!;
|
|
358
|
-
assert.ok(parsed, "syntax is valid; registration is a verify-time question");
|
|
359
|
-
assert.equal(parsed.members[0]!.placement, "xmp/9");
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
test("12. cross-shape: a produced/1 payload is not a set manifest and a set manifest is not a payload", () => {
|
|
363
|
-
assert.equal(parseSetManifest(bytes("produced-origin.json")), null);
|
|
364
|
-
assert.equal(parseSetManifest(bytes("produced-bare.json")), null);
|
|
365
|
-
assert.equal(parseSetManifest(buildFusePayload(A.commitment)), null);
|
|
366
|
-
assert.equal(parseFusePayload(A.manifest), null);
|
|
367
|
-
assert.equal(parseFusePayload(enc(PINNED_MANIFEST)), null);
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// ---------------------------------------------------------------------------
|
|
372
|
-
// Registry and metadata
|
|
373
|
-
// ---------------------------------------------------------------------------
|
|
374
|
-
|
|
375
|
-
describe("registry and metadata", () => {
|
|
376
|
-
test("13. PLACEMENTS is unchanged; set/1 resolves through getPlacement as Form C, not byte-exact, locate-only", () => {
|
|
377
|
-
assert.deepEqual(PLACEMENTS.map((p) => p.id), ["trailer/1", "container/1", "container/2", "produced/1"]);
|
|
378
|
-
const set1 = getPlacement("set/1")!;
|
|
379
|
-
assert.ok(set1);
|
|
380
|
-
assert.equal(set1.id, SET_PLACEMENT_ID);
|
|
381
|
-
assert.equal(set1.form, "C");
|
|
382
|
-
assert.equal(set1.byteExact, false);
|
|
383
|
-
assert.throws(() => set1.build({ commitment: A.commitment }), /buildSetManifest/);
|
|
384
|
-
const located = set1.locate(A.manifest)!;
|
|
385
|
-
assert.deepEqual(located, { commitment: A.commitment }, "exactly the commitment, nothing else");
|
|
386
|
-
assert.equal(set1.locate(bytes("produced-bare.json")), null);
|
|
387
|
-
assert.equal(set1.locate(bytes("fused-trailer.bin")), null);
|
|
388
|
-
assert.equal(set1.locate(original), null);
|
|
389
|
-
// set/2 resolves the same way: Form C, locate-only, and equally absent from the undeclared scan.
|
|
390
|
-
const set2 = getPlacement("set/2")!;
|
|
391
|
-
assert.equal(set2.form, "C");
|
|
392
|
-
assert.equal(set2.byteExact, false);
|
|
393
|
-
assert.equal(set2.locate(A.manifest), null, "a set/1 manifest is not a set/2 root document");
|
|
394
|
-
assert.equal(getPlacement("set/3"), undefined);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
test("14. readSetMetadata re-canonicalizes a plain object and returns null for anything else", async () => {
|
|
398
|
-
const o = asObject(A.manifest);
|
|
399
|
-
const shuffled = { type: o.type, slotCommitment: o.slotCommitment, placement: o.placement, members: o.members.map((r) => ({ placement: r.placement, origin: r.origin, artifact: r.artifact })) };
|
|
400
|
-
const p = structuredClone(A.proof);
|
|
401
|
-
p.metadata = { [SET_METADATA_KEY]: shuffled };
|
|
402
|
-
assert.deepEqual(readSetMetadata(p), A.manifest);
|
|
403
|
-
const absent = structuredClone(A.proof); delete absent.metadata;
|
|
404
|
-
assert.equal(readSetMetadata(absent), null, "no metadata");
|
|
405
|
-
const other = structuredClone(A.proof); other.metadata = { somethingElse: 1 };
|
|
406
|
-
assert.equal(readSetMetadata(other), null, "key absent");
|
|
407
|
-
const str = structuredClone(A.proof); str.metadata = { [SET_METADATA_KEY]: dec(A.manifest) };
|
|
408
|
-
assert.equal(readSetMetadata(str), null, "a string");
|
|
409
|
-
const arr = structuredClone(A.proof); arr.metadata = { [SET_METADATA_KEY]: [o] };
|
|
410
|
-
assert.equal(readSetMetadata(arr), null, "an array");
|
|
411
|
-
const nul = structuredClone(A.proof); nul.metadata = { [SET_METADATA_KEY]: null };
|
|
412
|
-
assert.equal(readSetMetadata(nul), null, "null");
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
// ---------------------------------------------------------------------------
|
|
417
|
-
// verifyFuse compatibility
|
|
418
|
-
// ---------------------------------------------------------------------------
|
|
419
|
-
|
|
420
|
-
describe("verifyFuse on set proofs and every existing fixture", () => {
|
|
421
|
-
test("15. the manifest bytes of a set proof verify FUSED_DIRECT under set/1 with one floor statement", async () => {
|
|
422
|
-
const r = await fuse(A.proof, A.manifest);
|
|
423
|
-
assert.equal(r.category, "FUSED_DIRECT", r.reason ?? "");
|
|
424
|
-
assert.equal(r.placement, "set/1");
|
|
425
|
-
assert.equal(r.marker?.source, "attribution");
|
|
426
|
-
assert.equal(r.marker?.placement, "set/1");
|
|
427
|
-
assert.equal(r.statements.length, 1);
|
|
428
|
-
assert.match(r.statements[0]!, /could not feasibly have been finalized before their signed slot allocation at position 10/);
|
|
429
|
-
assert.equal(r.span?.positions, "4");
|
|
430
|
-
assert.equal(r.originDigestB64, null, "a set has no single origin");
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
test("16. a member's fused bytes and a member's original are NO_MATCH to verifyFuse: it knows nothing about members", async () => {
|
|
434
|
-
for (const f of A.fused) {
|
|
435
|
-
assert.equal((await fuse(A.proof, f.bytes)).category, "NO_MATCH", f.name);
|
|
436
|
-
assert.equal((await fuse(A.proof, f.original)).category, "NO_MATCH", `${f.name} original`);
|
|
437
|
-
}
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
test("17. every existing fuse fixture keeps its category", async () => {
|
|
441
|
-
const cases: Array<[string, Uint8Array, string]> = [
|
|
442
|
-
["recorded.proof.json", original, "RECORDED"],
|
|
443
|
-
["trailer.proof.json", bytes("fused-trailer.bin"), "FUSED_DIRECT"],
|
|
444
|
-
["container.proof.json", bytes("fused-container.tar"), "FUSED_DIRECT"],
|
|
445
|
-
["produced-origin.proof.json", bytes("produced-origin.json"), "FUSED_DIRECT"],
|
|
446
|
-
["produced-bare.proof.json", bytes("produced-bare.json"), "FUSED_DIRECT"],
|
|
447
|
-
["trailer-undeclared.proof.json", bytes("fused-trailer-undeclared.bin"), "FUSED_DIRECT"],
|
|
448
|
-
["wrong-slot.proof.json", bytes("fused-wrong-slot.bin"), "INVALID_SLOT_COMMITMENT"],
|
|
449
|
-
["unregistered.proof.json", bytes("fused-unregistered.bin"), "UNDETERMINED_PLACEMENT"],
|
|
450
|
-
["trailer.proof.json", original, "FUSED_FROM_ORIGIN"],
|
|
451
|
-
["container.proof.json", png, "FUSED_FROM_ORIGIN"],
|
|
452
|
-
["produced-origin.proof.json", original, "RECONSTRUCTION_MISMATCH"],
|
|
453
|
-
["unregistered.proof.json", original, "UNDETERMINED_PLACEMENT"],
|
|
454
|
-
["wrong-slot.proof.json", original, "RECONSTRUCTION_MISMATCH"],
|
|
455
|
-
["trailer.proof.json", unrelated, "NO_MATCH"],
|
|
456
|
-
["container.proof.json", bytes("fused-trailer.bin"), "NO_MATCH"],
|
|
457
|
-
];
|
|
458
|
-
for (const [name, file, expected] of cases) {
|
|
459
|
-
const r = await fuse(proofOf(name), file);
|
|
460
|
-
assert.equal(r.category, expected, `${name}: ${r.reason ?? ""}`);
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
test("18. a manifest carrying another slot's commitment, committed under proof A, is INVALID_SLOT_COMMITMENT", async () => {
|
|
465
|
-
const foreign = buildSetManifest(B.commitment, A.rows);
|
|
466
|
-
const proof = await mintUnder(A, foreign);
|
|
467
|
-
const r = await fuse(proof, foreign);
|
|
468
|
-
assert.equal(r.category, "INVALID_SLOT_COMMITMENT");
|
|
469
|
-
assert.equal(r.placement, "set/1");
|
|
470
|
-
assert.equal(r.proof.valid, true);
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
// ---------------------------------------------------------------------------
|
|
475
|
-
// verifyFuseMember: positive members
|
|
476
|
-
// ---------------------------------------------------------------------------
|
|
477
|
-
|
|
478
|
-
describe("verifyFuseMember: members", () => {
|
|
479
|
-
test("19. each fused member is SET_MEMBER_DIRECT with its row, its sorted index, and the floor statement of the manifest", async () => {
|
|
480
|
-
const floor = (await fuse(A.proof, A.manifest)).statements[0]!;
|
|
481
|
-
for (const f of A.fused) {
|
|
482
|
-
const r = await member(A.proof, f.bytes);
|
|
483
|
-
assert.equal(r.category, "SET_MEMBER_DIRECT", `${f.name}: ${r.reason ?? ""}`);
|
|
484
|
-
assert.equal(r.placement, f.placement);
|
|
485
|
-
assert.equal(r.originDigestB64, bytesToBase64(f.row.origin));
|
|
486
|
-
assert.equal(r.artifactDigestB64, A.proof.artifact.digestB64);
|
|
487
|
-
assert.equal(r.slotCommitmentB64, bytesToBase64(A.commitment));
|
|
488
|
-
assert.ok(r.set);
|
|
489
|
-
assert.equal(r.set!.manifestSource, "metadata");
|
|
490
|
-
assert.equal(r.set!.memberCount, 3);
|
|
491
|
-
assert.equal(r.set!.manifestDigestB64, r.artifactDigestB64);
|
|
492
|
-
assert.ok(r.set!.member);
|
|
493
|
-
assert.equal(r.set!.member!.index, sortedIndex(A, f));
|
|
494
|
-
assert.equal(r.set!.member!.placement, f.placement);
|
|
495
|
-
assert.equal(r.set!.member!.fusedDigestB64, r.fileDigestB64);
|
|
496
|
-
assert.equal(r.set!.member!.originDigestB64, bytesToBase64(f.row.origin));
|
|
497
|
-
assert.match(r.statements[0]!, new RegExp(`member ${sortedIndex(A, f) + 1} of 3 listed in the set manifest committed at position 14`));
|
|
498
|
-
assert.match(r.statements.join(" "), /position 10/);
|
|
499
|
-
assert.match(r.statements.join(" "), /position 14/);
|
|
500
|
-
assert.equal(r.statements.at(-1), floor, "byte-identical to verifyFuse's floor statement");
|
|
501
|
-
assert.equal(r.statements.length, 3, "membership, origin consistency, floor");
|
|
502
|
-
assert.match(r.statements[1]!, /origin digest that matches the set manifest; the original itself was not supplied and was not checked/);
|
|
503
|
-
assert.equal(r.reason, null);
|
|
504
|
-
assert.equal(r.span?.positions, "4");
|
|
505
|
-
assert.equal(r.marker?.placement, "set/1");
|
|
506
|
-
}
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
test("20. an original rebuilds its member: SET_MEMBER_FROM_ORIGIN with the row's placement", async () => {
|
|
510
|
-
const t = await member(A.proof, original);
|
|
511
|
-
assert.equal(t.category, "SET_MEMBER_FROM_ORIGIN", t.reason ?? "");
|
|
512
|
-
assert.equal(t.placement, "trailer/1");
|
|
513
|
-
assert.match(t.statements[0]!, /rebuilds member \d+ of 3/);
|
|
514
|
-
assert.equal(t.statements.length, 2);
|
|
515
|
-
assert.equal(t.originDigestB64, bytesToBase64(sha256(original)));
|
|
516
|
-
assert.equal(t.set!.member!.index, sortedIndex(A, byName(A, "original.txt")));
|
|
517
|
-
const c = await member(A.proof, png);
|
|
518
|
-
assert.equal(c.category, "SET_MEMBER_FROM_ORIGIN", c.reason ?? "");
|
|
519
|
-
assert.equal(c.placement, "container/1");
|
|
520
|
-
assert.match(c.statements[0]!, /rebuilds member \d+ of 3/);
|
|
521
|
-
const n = await member(A.proof, note);
|
|
522
|
-
assert.equal(n.category, "SET_MEMBER_FROM_ORIGIN");
|
|
523
|
-
assert.equal(n.placement, "trailer/1");
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
test("21. the manifest as an explicit argument stands in for stripped metadata, and wins when both are present", async () => {
|
|
527
|
-
const f = byName(A, "image.png");
|
|
528
|
-
const stripped = structuredClone(A.proof);
|
|
529
|
-
delete stripped.metadata;
|
|
530
|
-
const fromArgument = await member(stripped, f.bytes, { manifest: A.manifest });
|
|
531
|
-
assert.equal(fromArgument.category, "SET_MEMBER_DIRECT", fromArgument.reason ?? "");
|
|
532
|
-
assert.equal(fromArgument.set!.manifestSource, "argument");
|
|
533
|
-
const both = await member(A.proof, f.bytes, { manifest: A.manifest });
|
|
534
|
-
assert.deepEqual(both, fromArgument);
|
|
535
|
-
const fromMetadata = await member(A.proof, f.bytes);
|
|
536
|
-
assert.equal(fromMetadata.set!.manifestSource, "metadata");
|
|
537
|
-
assert.deepEqual({ ...fromMetadata, set: { ...fromMetadata.set!, manifestSource: "argument" } }, fromArgument);
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
test("22. one original fused two ways is two members; the original reports the first row in manifest order", async () => {
|
|
541
|
-
const twice = await makeSet({ slotCounter: "20", commitCounter: "22", files: () => [[original, "trailer/1", "t"], [original, "container/1", "c"]] });
|
|
542
|
-
for (const f of twice.fused) {
|
|
543
|
-
const r = await member(twice.proof, f.bytes);
|
|
544
|
-
assert.equal(r.category, "SET_MEMBER_DIRECT", `${f.name}: ${r.reason ?? ""}`);
|
|
545
|
-
assert.equal(r.placement, f.placement);
|
|
546
|
-
}
|
|
547
|
-
const r = await member(twice.proof, original);
|
|
548
|
-
assert.equal(r.category, "SET_MEMBER_FROM_ORIGIN", r.reason ?? "");
|
|
549
|
-
assert.equal(r.set!.memberCount, 2);
|
|
550
|
-
assert.equal(r.set!.member!.index, 0, "the first row in manifest order");
|
|
551
|
-
assert.equal(r.placement, twice.sorted[0]!.placement);
|
|
552
|
-
assert.equal(r.set!.member!.placement, twice.sorted[0]!.placement);
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
test("23. the again case: a fused file listed only as the ORIGIN of a further member is an origin, never a stray", async () => {
|
|
556
|
-
const again = await makeSet({
|
|
557
|
-
slotCounter: "30",
|
|
558
|
-
commitCounter: "32",
|
|
559
|
-
files: (c) => [[trailer.build({ original, commitment: c }), "trailer/1", "again"], [png, "container/1", "image.png"]],
|
|
560
|
-
});
|
|
561
|
-
const oPrime = byName(again, "again").original;
|
|
562
|
-
assert.ok(trailer.locate(oPrime), "O' itself carries the commitment");
|
|
563
|
-
const r = await member(again.proof, oPrime);
|
|
564
|
-
assert.equal(r.category, "SET_MEMBER_FROM_ORIGIN", r.reason ?? "");
|
|
565
|
-
assert.notEqual(r.category, "SET_NOT_MEMBER");
|
|
566
|
-
assert.equal(r.placement, "trailer/1");
|
|
567
|
-
const direct = await member(again.proof, byName(again, "again").bytes);
|
|
568
|
-
assert.equal(direct.category, "SET_MEMBER_DIRECT");
|
|
569
|
-
});
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
// ---------------------------------------------------------------------------
|
|
573
|
-
// The stray (mandatory)
|
|
574
|
-
// ---------------------------------------------------------------------------
|
|
575
|
-
|
|
576
|
-
describe("verifyFuseMember: the 51st file", () => {
|
|
577
|
-
const stray51 = () => trailer.build({ original: unrelated, commitment: trailer.locate(byName(A, "original.txt").bytes)!.commitment });
|
|
578
|
-
|
|
579
|
-
test("24. unlisted bytes carrying c read off a member: SET_NOT_MEMBER with the two-part reason and no rendered floor", async () => {
|
|
580
|
-
const r = await member(A.proof, stray51());
|
|
581
|
-
assert.equal(r.category, "SET_NOT_MEMBER", r.reason ?? "");
|
|
582
|
-
assert.equal(r.placement, "trailer/1");
|
|
583
|
-
assert.ok(r.set, "the manifest was bound");
|
|
584
|
-
assert.equal(r.set!.member, null);
|
|
585
|
-
assert.equal(r.set!.memberCount, 3);
|
|
586
|
-
assert.deepEqual(r.statements, []);
|
|
587
|
-
assert.match(r.reason!, /made after that slot existed/);
|
|
588
|
-
assert.match(r.reason!, /not among the 3 members/);
|
|
589
|
-
assert.match(r.reason!, /committed at position 14/);
|
|
590
|
-
assert.match(r.reason!, /slot allocated at position 10/);
|
|
591
|
-
assert.match(r.reason!, /does not cover them/);
|
|
592
|
-
assert.equal(r.proof.valid, true);
|
|
593
|
-
assert.equal(r.originDigestB64, null);
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
test("25. the same construction as container/1 and as a produced/1 payload", async () => {
|
|
597
|
-
const asContainer = await member(A.proof, container.build({ original: unrelated, commitment: A.commitment }));
|
|
598
|
-
assert.equal(asContainer.category, "SET_NOT_MEMBER", asContainer.reason ?? "");
|
|
599
|
-
assert.equal(asContainer.placement, "container/1");
|
|
600
|
-
assert.match(asContainer.reason!, /made after that slot existed/);
|
|
601
|
-
assert.match(asContainer.reason!, /not among the 3 members/);
|
|
602
|
-
for (const payload of [buildFusePayload(A.commitment), buildFusePayload(A.commitment, sha256(unrelated))]) {
|
|
603
|
-
const asProduced = await member(A.proof, payload);
|
|
604
|
-
assert.equal(asProduced.category, "SET_NOT_MEMBER", asProduced.reason ?? "");
|
|
605
|
-
assert.equal(asProduced.placement, "produced/1");
|
|
606
|
-
assert.match(asProduced.reason!, /made after that slot existed/);
|
|
607
|
-
assert.match(asProduced.reason!, /not among the 3 members/);
|
|
608
|
-
assert.deepEqual(asProduced.statements, []);
|
|
609
|
-
}
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
test("26. the span policy reports on the stray without changing its category", async () => {
|
|
613
|
-
const r = await member(A.proof, stray51(), { maxPositions: 0 });
|
|
614
|
-
assert.equal(r.category, "SET_NOT_MEMBER");
|
|
615
|
-
assert.equal(r.policy.spanExceeded, true);
|
|
616
|
-
assert.equal(r.policy.maxPositions, "0");
|
|
617
|
-
});
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
// ---------------------------------------------------------------------------
|
|
621
|
-
// Foreign set and transplants
|
|
622
|
-
// ---------------------------------------------------------------------------
|
|
623
|
-
|
|
624
|
-
describe("verifyFuseMember: foreign set and transplants", () => {
|
|
625
|
-
test("27. a member of another set is NO_MATCH naming a different slot; the shared original is a member of each set separately", async () => {
|
|
626
|
-
const bMember = byName(B, "original.txt");
|
|
627
|
-
const aMember = byName(A, "original.txt");
|
|
628
|
-
assert.ok(!bytesEqual(bMember.bytes, aMember.bytes), "the two sets fuse the same original under different slots");
|
|
629
|
-
const bUnderA = await member(A.proof, bMember.bytes);
|
|
630
|
-
assert.equal(bUnderA.category, "NO_MATCH", bUnderA.reason ?? "");
|
|
631
|
-
assert.match(bUnderA.reason!, /different slot/);
|
|
632
|
-
assert.ok(bUnderA.set, "A's manifest was bound; the bytes are simply not A's");
|
|
633
|
-
const aUnderB = await member(B.proof, aMember.bytes);
|
|
634
|
-
assert.equal(aUnderB.category, "NO_MATCH");
|
|
635
|
-
assert.match(aUnderB.reason!, /different slot/);
|
|
636
|
-
const underA = await member(A.proof, original);
|
|
637
|
-
assert.equal(underA.category, "SET_MEMBER_FROM_ORIGIN");
|
|
638
|
-
assert.equal(underA.set!.memberCount, 3);
|
|
639
|
-
const underB = await member(B.proof, original);
|
|
640
|
-
assert.equal(underB.category, "SET_MEMBER_FROM_ORIGIN");
|
|
641
|
-
assert.equal(underB.set!.memberCount, 2);
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
test("28. transplant: B's member listed in a manifest signed under slot A is INVALID_SLOT_COMMITMENT, listed but on a different slot", async () => {
|
|
645
|
-
const bMember = byName(B, "original.txt");
|
|
646
|
-
const transplanted = buildSetManifest(A.commitment, [...A.rows, { artifact: bMember.row.artifact, origin: bMember.row.origin, placement: "trailer/1" }]);
|
|
647
|
-
const proof = await mintUnder(A, transplanted);
|
|
648
|
-
const r = await member(proof, bMember.bytes);
|
|
649
|
-
assert.equal(r.category, "INVALID_SLOT_COMMITMENT", r.reason ?? "");
|
|
650
|
-
assert.ok(r.set, "the manifest was bound");
|
|
651
|
-
assert.equal(r.set!.member, null);
|
|
652
|
-
assert.equal(r.set!.memberCount, 4);
|
|
653
|
-
assert.match(r.reason!, /listed/);
|
|
654
|
-
assert.match(r.reason!, /different slot/);
|
|
655
|
-
assert.match(r.reason!, /membership without the floor is not a member verdict/);
|
|
656
|
-
assert.equal(r.placement, "trailer/1");
|
|
657
|
-
assert.deepEqual(r.statements, []);
|
|
658
|
-
// Honest A's own members are unaffected by the extra row.
|
|
659
|
-
assert.equal((await member(proof, byName(A, "note.txt").bytes)).category, "SET_MEMBER_DIRECT");
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
test("29. membership-only: a manifest that lists an unfused file's digest is refused for want of the floor", async () => {
|
|
663
|
-
const listedPlain = buildSetManifest(A.commitment, [{ artifact: sha256(original), origin: sha256(original), placement: "trailer/1" }, ...A.rows]);
|
|
664
|
-
const proof = await mintUnder(A, listedPlain);
|
|
665
|
-
const r = await member(proof, original);
|
|
666
|
-
assert.equal(r.category, "INVALID_SLOT_COMMITMENT", r.reason ?? "");
|
|
667
|
-
assert.match(r.reason!, /no trailer\/1 commitment/);
|
|
668
|
-
assert.match(r.reason!, /membership without the floor/);
|
|
669
|
-
assert.ok(r.set);
|
|
670
|
-
assert.equal(r.set!.member, null);
|
|
671
|
-
assert.equal(r.placement, "trailer/1");
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
test("30. a row whose origin contradicts what the fused bytes embed is INVALID_ORIGIN_ATTRIBUTION; the wrongly listed origin cannot rebuild", async () => {
|
|
675
|
-
const pngContainer = byName(A, "image.png");
|
|
676
|
-
const noteTrailer = byName(A, "note.txt");
|
|
677
|
-
const lying = buildSetManifest(A.commitment, [
|
|
678
|
-
{ artifact: pngContainer.row.artifact, origin: sha256(original), placement: "container/1" },
|
|
679
|
-
{ artifact: noteTrailer.row.artifact, origin: sha256(png), placement: "trailer/1" },
|
|
680
|
-
]);
|
|
681
|
-
const proof = await mintUnder(A, lying);
|
|
682
|
-
const c = await member(proof, pngContainer.bytes);
|
|
683
|
-
assert.equal(c.category, "INVALID_ORIGIN_ATTRIBUTION", c.reason ?? "");
|
|
684
|
-
assert.match(c.reason!, /origin digest listed for this member does not match the origin inside the fused bytes/);
|
|
685
|
-
assert.equal(c.placement, "container/1");
|
|
686
|
-
assert.equal(c.set!.member, null);
|
|
687
|
-
const t = await member(proof, noteTrailer.bytes);
|
|
688
|
-
assert.equal(t.category, "INVALID_ORIGIN_ATTRIBUTION", t.reason ?? "");
|
|
689
|
-
assert.equal(t.placement, "trailer/1");
|
|
690
|
-
const o = await member(proof, original);
|
|
691
|
-
assert.equal(o.category, "RECONSTRUCTION_MISMATCH", o.reason ?? "");
|
|
692
|
-
assert.match(o.reason!, /rebuilding container\/1 from this file/);
|
|
693
|
-
const p = await member(proof, png);
|
|
694
|
-
assert.equal(p.category, "RECONSTRUCTION_MISMATCH", p.reason ?? "");
|
|
695
|
-
assert.match(p.reason!, /rebuilding trailer\/1 from this file/);
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
test("31. a listed origin whose rebuild does not reproduce the row, or whose placement is not byte-exact, is RECONSTRUCTION_MISMATCH", async () => {
|
|
699
|
-
const wrong = buildSetManifest(A.commitment, [{ artifact: sha256(utf8("not the trailer")), origin: sha256(original), placement: "trailer/1" }]);
|
|
700
|
-
const r = await member(await mintUnder(A, wrong), original);
|
|
701
|
-
assert.equal(r.category, "RECONSTRUCTION_MISMATCH", r.reason ?? "");
|
|
702
|
-
assert.equal(r.placement, "trailer/1");
|
|
703
|
-
assert.match(r.reason!, /does not reproduce the listed member digest/);
|
|
704
|
-
const produced = buildSetManifest(A.commitment, [{ artifact: sha256(buildFusePayload(A.commitment, sha256(original))), origin: sha256(original), placement: "produced/1" }]);
|
|
705
|
-
const p = await member(await mintUnder(A, produced), original);
|
|
706
|
-
assert.equal(p.category, "RECONSTRUCTION_MISMATCH", p.reason ?? "");
|
|
707
|
-
assert.equal(p.placement, "produced/1");
|
|
708
|
-
});
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
// ---------------------------------------------------------------------------
|
|
712
|
-
// Manifest binding failures
|
|
713
|
-
// ---------------------------------------------------------------------------
|
|
714
|
-
|
|
715
|
-
describe("verifyFuseMember: manifest binding", () => {
|
|
716
|
-
const stray51 = () => trailer.build({ original: unrelated, commitment: A.commitment });
|
|
717
|
-
|
|
718
|
-
test("32. metadata with the stray appended as a row is INVALID_SET_MANIFEST for the stray and for a genuine member alike", async () => {
|
|
719
|
-
const stray = stray51();
|
|
720
|
-
const tampered = withMetadata(A.proof, (m) => {
|
|
721
|
-
m.members.push({ artifact: digestField(sha256(stray)), origin: digestField(sha256(unrelated)), placement: "trailer/1" });
|
|
722
|
-
m.members.sort((x, y) => (x.artifact.digest < y.artifact.digest ? -1 : 1));
|
|
723
|
-
});
|
|
724
|
-
const s = await member(tampered, stray);
|
|
725
|
-
assert.equal(s.category, "INVALID_SET_MANIFEST", s.reason ?? "");
|
|
726
|
-
assert.notEqual(s.category, "SET_MEMBER_DIRECT");
|
|
727
|
-
assert.equal(s.proof.valid, true);
|
|
728
|
-
assert.equal(s.set, null);
|
|
729
|
-
assert.match(s.reason!, /does not hash to the committed artifact digest/);
|
|
730
|
-
const g = await member(tampered, byName(A, "original.txt").bytes);
|
|
731
|
-
assert.equal(g.category, "INVALID_SET_MANIFEST");
|
|
732
|
-
assert.equal(g.proof.valid, true);
|
|
733
|
-
assert.equal(g.set, null);
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
test("33. a removed row, reordered rows, a changed placement, a changed digest, or an uppercased digest: INVALID_SET_MANIFEST", async () => {
|
|
737
|
-
const genuine = byName(A, "note.txt").bytes;
|
|
738
|
-
const variants: Array<[string, (m: SetManifest) => void]> = [
|
|
739
|
-
["row removed", (m) => { m.members.pop(); }],
|
|
740
|
-
["rows reordered", (m) => { m.members.reverse(); }],
|
|
741
|
-
["placement changed", (m) => { m.members[0]!.placement = m.members[0]!.placement === "trailer/1" ? "container/1" : "trailer/1"; }],
|
|
742
|
-
["digest changed", (m) => { m.members[1]!.origin.digest = bytesToHex(sha256(unrelated)); }],
|
|
743
|
-
["digest uppercased", (m) => { m.members[2]!.artifact.digest = m.members[2]!.artifact.digest.toUpperCase(); }],
|
|
744
|
-
["commitment changed", (m) => { m.slotCommitment.digest = bytesToHex(B.commitment); }],
|
|
745
|
-
];
|
|
746
|
-
for (const [label, mutate] of variants) {
|
|
747
|
-
const r = await member(withMetadata(A.proof, mutate), genuine);
|
|
748
|
-
assert.equal(r.category, "INVALID_SET_MANIFEST", label);
|
|
749
|
-
assert.equal(r.set, null, label);
|
|
750
|
-
assert.equal(r.proof.valid, true, label);
|
|
751
|
-
}
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
test("34. metadata with shuffled object keys but identical content is laundered by re-canonicalization", async () => {
|
|
755
|
-
const o = asObject(A.manifest);
|
|
756
|
-
const p = structuredClone(A.proof);
|
|
757
|
-
p.metadata = { [SET_METADATA_KEY]: { type: o.type, slotCommitment: o.slotCommitment, placement: o.placement, members: o.members.map((r) => ({ placement: r.placement, origin: { digest: r.origin.digest, algorithm: r.origin.algorithm }, artifact: r.artifact })) } };
|
|
758
|
-
const r = await member(p, byName(A, "image.png").bytes);
|
|
759
|
-
assert.equal(r.category, "SET_MEMBER_DIRECT", r.reason ?? "");
|
|
760
|
-
assert.equal(r.set!.manifestSource, "metadata");
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
test("35. no metadata, or a JSON string under the key, is INVALID_SET_MANIFEST naming the key", async () => {
|
|
764
|
-
const bare = await mintUnder(A, A.manifest, { withMetadata: false });
|
|
765
|
-
const r = await member(bare, byName(A, "original.txt").bytes);
|
|
766
|
-
assert.equal(r.category, "INVALID_SET_MANIFEST");
|
|
767
|
-
assert.match(r.reason!, /bitgraph-fuse\/1/);
|
|
768
|
-
assert.match(r.reason!, /none supplied/);
|
|
769
|
-
assert.equal(r.set, null);
|
|
770
|
-
const str = structuredClone(A.proof);
|
|
771
|
-
str.metadata = { [SET_METADATA_KEY]: dec(A.manifest) };
|
|
772
|
-
const s = await member(str, byName(A, "original.txt").bytes);
|
|
773
|
-
assert.equal(s.category, "INVALID_SET_MANIFEST");
|
|
774
|
-
assert.match(s.reason!, /bitgraph-fuse\/1/);
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
test("36. a hash match never rescues encoding: pretty-printed bytes signed as the artifact are not canonical", async () => {
|
|
778
|
-
const pretty = enc(JSON.stringify(asObject(A.manifest), null, 2));
|
|
779
|
-
const proof = await mintUnder(A, pretty);
|
|
780
|
-
assert.equal(proof.artifact.digestB64, bytesToBase64(sha256(pretty)));
|
|
781
|
-
const r = await member(proof, byName(A, "original.txt").bytes, { manifest: pretty });
|
|
782
|
-
assert.equal(r.category, "INVALID_SET_MANIFEST");
|
|
783
|
-
assert.match(r.reason!, /not canonical/);
|
|
784
|
-
assert.equal(r.set, null);
|
|
785
|
-
const viaMetadata = await member(proof, byName(A, "original.txt").bytes);
|
|
786
|
-
assert.equal(viaMetadata.category, "INVALID_SET_MANIFEST", "the re-canonicalized metadata no longer hashes to the pretty artifact");
|
|
787
|
-
assert.match(viaMetadata.reason!, /does not hash/);
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
test("37. explicit bytes win: bad explicit bytes are refused despite correct metadata; correct explicit bytes rescue tampered metadata", async () => {
|
|
791
|
-
const genuine = byName(A, "note.txt").bytes;
|
|
792
|
-
const text = dec(A.manifest);
|
|
793
|
-
const bad: Array<[string, Uint8Array]> = [
|
|
794
|
-
["duplicate keys", enc(text.replace('"type":"bitgraph-fuse/1"', '"type":"bitgraph-fuse/1","type":"bitgraph-fuse/1"'))],
|
|
795
|
-
["uppercase hex", enc(withUppercasedDigest(text))],
|
|
796
|
-
["whitespace", enc(JSON.stringify(asObject(A.manifest), null, 1))],
|
|
797
|
-
];
|
|
798
|
-
for (const [label, explicit] of bad) {
|
|
799
|
-
const r = await member(A.proof, genuine, { manifest: explicit });
|
|
800
|
-
assert.equal(r.category, "INVALID_SET_MANIFEST", label);
|
|
801
|
-
assert.match(r.reason!, /not canonical/, label);
|
|
802
|
-
assert.equal(r.set, null, label);
|
|
803
|
-
}
|
|
804
|
-
const foreign = await member(A.proof, genuine, { manifest: B.manifest });
|
|
805
|
-
assert.equal(foreign.category, "INVALID_SET_MANIFEST");
|
|
806
|
-
assert.match(foreign.reason!, /does not hash to the committed artifact digest/);
|
|
807
|
-
const tampered = withMetadata(A.proof, (m) => { m.members.pop(); });
|
|
808
|
-
assert.equal((await member(tampered, genuine)).category, "INVALID_SET_MANIFEST");
|
|
809
|
-
const rescued = await member(tampered, genuine, { manifest: A.manifest });
|
|
810
|
-
assert.equal(rescued.category, "SET_MEMBER_DIRECT", rescued.reason ?? "");
|
|
811
|
-
assert.equal(rescued.set!.manifestSource, "argument");
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
test("38. a manifest carrying another commitment, signed as the artifact, is INVALID_SLOT_COMMITMENT for every member", async () => {
|
|
815
|
-
const foreign = buildSetManifest(B.commitment, A.rows);
|
|
816
|
-
const proof = await mintUnder(A, foreign);
|
|
817
|
-
for (const f of A.fused) {
|
|
818
|
-
const r = await member(proof, f.bytes);
|
|
819
|
-
assert.equal(r.category, "INVALID_SLOT_COMMITMENT", f.name);
|
|
820
|
-
assert.match(r.reason!, /committed set manifest/);
|
|
821
|
-
assert.equal(r.set, null, "never bound");
|
|
822
|
-
assert.equal(r.proof.valid, true);
|
|
823
|
-
const o = await member(proof, f.original);
|
|
824
|
-
assert.equal(o.category, "INVALID_SLOT_COMMITMENT", `${f.name} original`);
|
|
825
|
-
}
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
test("39. the manifest bytes themselves are NO_MATCH here: verifyFuse answers for the manifest", async () => {
|
|
829
|
-
const r = await member(A.proof, A.manifest);
|
|
830
|
-
assert.equal(r.category, "NO_MATCH");
|
|
831
|
-
assert.match(r.reason!, /verifyFuse/);
|
|
832
|
-
assert.ok(r.set, "the manifest was bound before the answer");
|
|
833
|
-
assert.equal(r.set!.member, null);
|
|
834
|
-
});
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
// ---------------------------------------------------------------------------
|
|
838
|
-
// Proof-level failures
|
|
839
|
-
// ---------------------------------------------------------------------------
|
|
840
|
-
|
|
841
|
-
describe("verifyFuseMember: the proof", () => {
|
|
842
|
-
test("40. a set proof without its slot record is INVALID_SLOT_COMMITMENT", async () => {
|
|
843
|
-
const p = structuredClone(A.proof);
|
|
844
|
-
delete p.slotAllocation;
|
|
845
|
-
const r = await member(p, byName(A, "original.txt").bytes);
|
|
846
|
-
assert.equal(r.category, "INVALID_SLOT_COMMITMENT", r.reason ?? "");
|
|
847
|
-
assert.equal(r.proof.valid, true);
|
|
848
|
-
assert.match(r.reason!, /no slot record/);
|
|
849
|
-
assert.equal(r.set, null);
|
|
850
|
-
});
|
|
851
|
-
|
|
852
|
-
test("41. a proof not marked set/1 is INVALID_SET_MANIFEST; the fused name with no title is refused, and verifyFuse agrees on its manifest", async () => {
|
|
853
|
-
const genuine = byName(A, "original.txt").bytes;
|
|
854
|
-
const titled = await mintUnder(A, A.manifest, { attribution: fuseAttribution("trailer/1") });
|
|
855
|
-
const t = await member(titled, genuine);
|
|
856
|
-
assert.equal(t.category, "INVALID_SET_MANIFEST");
|
|
857
|
-
assert.match(t.reason!, /not marked set\/1/);
|
|
858
|
-
assert.equal(t.set, null);
|
|
859
|
-
const none = await mintUnder(A, A.manifest, { attribution: null });
|
|
860
|
-
const n = await member(none, genuine);
|
|
861
|
-
assert.equal(n.category, "INVALID_SET_MANIFEST");
|
|
862
|
-
assert.match(n.reason!, /not marked set\/1/);
|
|
863
|
-
for (const name of ["trailer.proof.json", "recorded.proof.json"]) {
|
|
864
|
-
const r = await member(proofOf(name), bytes("fused-trailer.bin"));
|
|
865
|
-
assert.equal(r.category, "INVALID_SET_MANIFEST", name);
|
|
866
|
-
assert.match(r.reason!, /not marked set\/1/);
|
|
867
|
-
assert.equal(r.proof.valid, true, name);
|
|
868
|
-
}
|
|
869
|
-
// The fused name with no title: set/1 is not in the undeclared scan, so
|
|
870
|
-
// verifyFuse finds no commitment in the manifest and reports
|
|
871
|
-
// INVALID_SLOT_COMMITMENT. The member verifier must not admit members
|
|
872
|
-
// of a proof whose manifest verifyFuse rejects: the two agree.
|
|
873
|
-
const undeclared = await mintUnder(A, A.manifest, { attribution: { name: "bitgraph-fuse/1" } });
|
|
874
|
-
const uf = await fuse(undeclared, A.manifest);
|
|
875
|
-
assert.equal(uf.category, "INVALID_SLOT_COMMITMENT");
|
|
876
|
-
assert.equal(uf.marker?.placement, null);
|
|
877
|
-
for (const file of [genuine, byName(A, "original.txt").original, byName(A, "image.png").bytes]) {
|
|
878
|
-
const u = await member(undeclared, file);
|
|
879
|
-
assert.equal(u.category, "INVALID_SET_MANIFEST", u.reason ?? "");
|
|
880
|
-
assert.match(u.reason!, /not marked set\/1/);
|
|
881
|
-
assert.equal(u.marker?.placement, null);
|
|
882
|
-
assert.equal(u.set, null);
|
|
883
|
-
}
|
|
884
|
-
});
|
|
885
|
-
|
|
886
|
-
test("49. a set marker never carries an origin: fuseAttribution refuses one, and a hand-built set/1 marker with an origin is INVALID_SET_MANIFEST", async () => {
|
|
887
|
-
const o = byName(A, "original.txt");
|
|
888
|
-
assert.throws(() => fuseAttribution("set/1", new Uint8Array(32)), /no single origin/);
|
|
889
|
-
assert.throws(() => fuseAttribution("set/1", sha256(o.original)), /no single origin/);
|
|
890
|
-
assert.doesNotThrow(() => fuseAttribution("set/1"));
|
|
891
|
-
assert.doesNotThrow(() => fuseAttribution("trailer/1", sha256(o.original)));
|
|
892
|
-
// An out-of-profile producer signs the origin of a listed member into a set/1 marker.
|
|
893
|
-
const withOrigin: Attribution = { name: "bitgraph-fuse/1", title: "set/1", message: b64(sha256(o.original)) };
|
|
894
|
-
const proof = await mintUnder(A, A.manifest, { attribution: withOrigin });
|
|
895
|
-
assert.equal((await fuse(proof, A.manifest)).category, "FUSED_DIRECT");
|
|
896
|
-
for (const file of [o.original, o.bytes, byName(A, "image.png").bytes]) {
|
|
897
|
-
const r = await member(proof, file);
|
|
898
|
-
assert.equal(r.category, "INVALID_SET_MANIFEST", r.reason ?? "");
|
|
899
|
-
assert.match(r.reason!, /no single origin/);
|
|
900
|
-
assert.equal(r.proof.valid, true);
|
|
901
|
-
assert.equal(r.set, null);
|
|
902
|
-
assert.equal(r.marker?.placement, "set/1");
|
|
903
|
-
}
|
|
904
|
-
});
|
|
905
|
-
|
|
906
|
-
test("42. tampering with the signed proof or the slot record is INVALID_UNDERLYING_PROOF and never downgrades; a trust policy applies", async () => {
|
|
907
|
-
const genuine = byName(A, "original.txt").bytes;
|
|
908
|
-
const cases: Array<[string, (p: BitGraphProof) => void]> = [
|
|
909
|
-
["counter", (p) => { p.commit.counter = "15"; }],
|
|
910
|
-
["attribution.title", (p) => { p.attribution!.title = "trailer/1"; }],
|
|
911
|
-
["artifact digest", (p) => { p.artifact.digestB64 = bytesToBase64(sha256(unrelated)); }],
|
|
912
|
-
["slot nonce", (p) => { p.slotAllocation!.nonceB64 = bytesToBase64(new Uint8Array(32)); }],
|
|
913
|
-
["signature", (p) => { p.signer.signatureB64 = bytesToBase64(new Uint8Array(64)); }],
|
|
914
|
-
];
|
|
915
|
-
for (const [label, mutate] of cases) {
|
|
916
|
-
const p = structuredClone(A.proof);
|
|
917
|
-
mutate(p);
|
|
918
|
-
const r = await member(p, genuine);
|
|
919
|
-
assert.equal(r.category, "INVALID_UNDERLYING_PROOF", label);
|
|
920
|
-
assert.equal(r.proof.valid, false, label);
|
|
921
|
-
assert.deepEqual(r.statements, [], label);
|
|
922
|
-
assert.equal(r.set, null, label);
|
|
923
|
-
assert.ok(r.reason, label);
|
|
924
|
-
}
|
|
925
|
-
const policy = await member(A.proof, genuine, { trustAnchors: { allowedMeasurements: ["00".repeat(48)] } });
|
|
926
|
-
assert.equal(policy.category, "INVALID_UNDERLYING_PROOF");
|
|
927
|
-
assert.match(policy.reason!, /measurement/i);
|
|
928
|
-
const allowed = await member(A.proof, genuine, { trustAnchors: { allowedMeasurements: ["test-measurement-set"], requireSlot: true } });
|
|
929
|
-
assert.equal(allowed.category, "SET_MEMBER_DIRECT");
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
test("43. an unregistered row placement is UNDETERMINED_PLACEMENT for that member and its origin only", async () => {
|
|
933
|
-
const noteTrailer = byName(A, "note.txt");
|
|
934
|
-
const xmp = buildSetManifest(A.commitment, [byName(A, "original.txt").row, byName(A, "image.png").row, { ...noteTrailer.row, placement: "xmp/9" }]);
|
|
935
|
-
const proof = await mintUnder(A, xmp);
|
|
936
|
-
const direct = await member(proof, noteTrailer.bytes);
|
|
937
|
-
assert.equal(direct.category, "UNDETERMINED_PLACEMENT", direct.reason ?? "");
|
|
938
|
-
assert.match(direct.reason!, /xmp\/9/);
|
|
939
|
-
assert.equal(direct.placement, null);
|
|
940
|
-
assert.equal(direct.set!.member, null);
|
|
941
|
-
const origin = await member(proof, note);
|
|
942
|
-
assert.equal(origin.category, "UNDETERMINED_PLACEMENT", origin.reason ?? "");
|
|
943
|
-
assert.match(origin.reason!, /xmp\/9/);
|
|
944
|
-
assert.equal((await member(proof, byName(A, "original.txt").bytes)).category, "SET_MEMBER_DIRECT");
|
|
945
|
-
assert.equal((await member(proof, byName(A, "image.png").bytes)).category, "SET_MEMBER_DIRECT");
|
|
946
|
-
assert.equal((await member(proof, png)).category, "SET_MEMBER_FROM_ORIGIN");
|
|
947
|
-
});
|
|
948
|
-
|
|
949
|
-
test("44. unrelated bytes and a member with a flipped commitment bit are NO_MATCH; a flipped content bit is the 51st file", async () => {
|
|
950
|
-
const r = await member(A.proof, unrelated);
|
|
951
|
-
assert.equal(r.category, "NO_MATCH");
|
|
952
|
-
assert.match(r.reason!, /neither a listed member nor a listed original/);
|
|
953
|
-
const f = byName(A, "original.txt").bytes.slice();
|
|
954
|
-
f[f.length - 1] = f[f.length - 1]! ^ 1; // a commitment byte
|
|
955
|
-
const c = await member(A.proof, f);
|
|
956
|
-
assert.equal(c.category, "NO_MATCH");
|
|
957
|
-
assert.match(c.reason!, /different slot/);
|
|
958
|
-
// A content bit flipped leaves the trailer intact: the bytes still carry c
|
|
959
|
-
// and are listed nowhere, which is byte for byte the 51st-file shape. The
|
|
960
|
-
// two-part verdict is mandatory there, so it cannot be NO_MATCH.
|
|
961
|
-
const content = byName(A, "original.txt").bytes.slice();
|
|
962
|
-
content[3] = content[3]! ^ 1;
|
|
963
|
-
const s = await member(A.proof, content);
|
|
964
|
-
assert.equal(s.category, "SET_NOT_MEMBER");
|
|
965
|
-
assert.match(s.reason!, /made after that slot existed/);
|
|
966
|
-
assert.match(s.reason!, /not among the 3 members/);
|
|
967
|
-
});
|
|
968
|
-
});
|
|
969
|
-
|
|
970
|
-
// ---------------------------------------------------------------------------
|
|
971
|
-
// Invariants and wiring
|
|
972
|
-
// ---------------------------------------------------------------------------
|
|
973
|
-
|
|
974
|
-
describe("invariants and wiring", () => {
|
|
975
|
-
test("47. the span policy on a member reports separately and never changes the category", async () => {
|
|
976
|
-
const f = byName(A, "image.png");
|
|
977
|
-
const r = await member(A.proof, f.bytes, { maxPositions: 0 });
|
|
978
|
-
assert.equal(r.category, "SET_MEMBER_DIRECT");
|
|
979
|
-
assert.equal(r.policy.spanExceeded, true);
|
|
980
|
-
assert.equal(r.policy.maxPositions, "0");
|
|
981
|
-
const ok = await member(A.proof, f.bytes, { maxPositions: 1_000_000 });
|
|
982
|
-
assert.equal(ok.policy.spanExceeded, false);
|
|
983
|
-
assert.equal(ok.policy.maxPositions, "1000000");
|
|
984
|
-
const o = await member(A.proof, png, { maxPositions: 3 });
|
|
985
|
-
assert.equal(o.category, "SET_MEMBER_FROM_ORIGIN");
|
|
986
|
-
assert.equal(o.policy.spanExceeded, true);
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
test("45. over every result in this file: member iff member category; bound implies the signed digest; statements only on members; shape parity", async () => {
|
|
990
|
-
assert.ok(collected.length > 80, `collected ${collected.length} results`);
|
|
991
|
-
const memberCategories = new Set(["SET_MEMBER_DIRECT", "SET_MEMBER_FROM_ORIGIN"]);
|
|
992
|
-
let members = 0;
|
|
993
|
-
for (const { proof, bytes: file, result: r } of collected) {
|
|
994
|
-
const isMember = memberCategories.has(r.category);
|
|
995
|
-
if (isMember) members++;
|
|
996
|
-
assert.equal(r.set !== null && r.set.member !== null, isMember, `${r.category}: set.member`);
|
|
997
|
-
if (r.set !== null) assert.equal(r.set.manifestDigestB64, r.artifactDigestB64, `${r.category}: bound digest`);
|
|
998
|
-
if (!isMember) assert.deepEqual(r.statements, [], `${r.category}: statements`);
|
|
999
|
-
else assert.ok(r.statements.length >= 2, `${r.category}: statements`);
|
|
1000
|
-
if (r.category === "SET_NOT_MEMBER") assert.ok(r.set !== null && r.set.member === null && r.reason !== null);
|
|
1001
|
-
if (r.category === "INVALID_UNDERLYING_PROOF") assert.equal(r.set, null);
|
|
1002
|
-
resetEpochLinkState();
|
|
1003
|
-
const shape = await verifyFuse({ proof, bytes: file });
|
|
1004
|
-
assert.deepEqual(Object.keys(r).filter((k) => k !== "set"), Object.keys(shape), `${r.category}: field parity with verifyFuse`);
|
|
1005
|
-
}
|
|
1006
|
-
assert.ok(members >= 20, `${members} member verdicts`);
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
test("46. structural guard: each member category string is a value exactly once in dist/fuse-member.js, inside memberResult", () => {
|
|
1010
|
-
const src = readFileSync(fileURLToPath(new URL("../../packages/verify/dist/fuse-member.js", import.meta.url)), "utf8");
|
|
1011
|
-
for (const name of ["SET_MEMBER_DIRECT", "SET_MEMBER_FROM_ORIGIN"]) {
|
|
1012
|
-
const quoted = src.match(new RegExp(`"${name}"`, "g")) ?? [];
|
|
1013
|
-
assert.equal(quoted.length, 1, name);
|
|
1014
|
-
}
|
|
1015
|
-
const memberResult = src.indexOf("const memberResult = ");
|
|
1016
|
-
assert.ok(memberResult >= 0);
|
|
1017
|
-
assert.ok(src.indexOf('"SET_MEMBER_DIRECT"') > memberResult);
|
|
1018
|
-
assert.ok(src.indexOf('"SET_MEMBER_FROM_ORIGIN"') > memberResult);
|
|
1019
|
-
assert.ok(!src.includes("export function admit"), "admit is module-private");
|
|
1020
|
-
assert.ok(!src.includes("export function bindSetManifest"), "bindSetManifest is module-private");
|
|
1021
|
-
});
|
|
1022
|
-
|
|
1023
|
-
test("48. test:core lists this file", () => {
|
|
1024
|
-
const pkg = JSON.parse(readFileSync(fileURLToPath(new URL("../../package.json", import.meta.url)), "utf8")) as { scripts: Record<string, string> };
|
|
1025
|
-
assert.ok(pkg.scripts["test:core"]!.split(" ").includes("dist/__tests__/fuse-set.test.js"));
|
|
1026
|
-
});
|
|
1027
|
-
});
|