@mikeargento/bitgraph 1.1.0 → 1.1.1
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/README.md +49 -40
- package/dist/__tests__/audit-anchors.test.d.ts +2 -0
- package/dist/__tests__/audit-anchors.test.d.ts.map +1 -0
- package/dist/__tests__/audit-anchors.test.js +152 -0
- package/dist/__tests__/audit-anchors.test.js.map +1 -0
- package/dist/__tests__/audit-anomalies.test.d.ts +2 -0
- package/dist/__tests__/audit-anomalies.test.d.ts.map +1 -0
- package/dist/__tests__/audit-anomalies.test.js +663 -0
- package/dist/__tests__/audit-anomalies.test.js.map +1 -0
- package/dist/__tests__/audit-attestation.test.d.ts +2 -0
- package/dist/__tests__/audit-attestation.test.d.ts.map +1 -0
- package/dist/__tests__/audit-attestation.test.js +403 -0
- package/dist/__tests__/audit-attestation.test.js.map +1 -0
- package/dist/__tests__/audit-bench-smoke.test.d.ts +2 -0
- package/dist/__tests__/audit-bench-smoke.test.d.ts.map +1 -0
- package/dist/__tests__/audit-bench-smoke.test.js +87 -0
- package/dist/__tests__/audit-bench-smoke.test.js.map +1 -0
- package/dist/__tests__/audit-boundary.test.d.ts +20 -0
- package/dist/__tests__/audit-boundary.test.d.ts.map +1 -0
- package/dist/__tests__/audit-boundary.test.js +84 -0
- package/dist/__tests__/audit-boundary.test.js.map +1 -0
- package/dist/__tests__/audit-cli.test.d.ts +2 -0
- package/dist/__tests__/audit-cli.test.d.ts.map +1 -0
- package/dist/__tests__/audit-cli.test.js +239 -0
- package/dist/__tests__/audit-cli.test.js.map +1 -0
- package/dist/__tests__/audit-dependencies.test.d.ts +2 -0
- package/dist/__tests__/audit-dependencies.test.d.ts.map +1 -0
- package/dist/__tests__/audit-dependencies.test.js +116 -0
- package/dist/__tests__/audit-dependencies.test.js.map +1 -0
- package/dist/__tests__/audit-fixtures.d.ts +265 -0
- package/dist/__tests__/audit-fixtures.d.ts.map +1 -0
- package/dist/__tests__/audit-fixtures.js +511 -0
- package/dist/__tests__/audit-fixtures.js.map +1 -0
- package/dist/__tests__/audit-ingest.test.d.ts +2 -0
- package/dist/__tests__/audit-ingest.test.d.ts.map +1 -0
- package/dist/__tests__/audit-ingest.test.js +473 -0
- package/dist/__tests__/audit-ingest.test.js.map +1 -0
- package/dist/__tests__/audit-memory-ingest.test.d.ts +12 -0
- package/dist/__tests__/audit-memory-ingest.test.d.ts.map +1 -0
- package/dist/__tests__/audit-memory-ingest.test.js +120 -0
- package/dist/__tests__/audit-memory-ingest.test.js.map +1 -0
- package/dist/__tests__/audit-property.test.d.ts +2 -0
- package/dist/__tests__/audit-property.test.d.ts.map +1 -0
- package/dist/__tests__/audit-property.test.js +771 -0
- package/dist/__tests__/audit-property.test.js.map +1 -0
- package/dist/__tests__/audit-real-fixtures.test.d.ts +2 -0
- package/dist/__tests__/audit-real-fixtures.test.d.ts.map +1 -0
- package/dist/__tests__/audit-real-fixtures.test.js +132 -0
- package/dist/__tests__/audit-real-fixtures.test.js.map +1 -0
- package/dist/__tests__/audit-real-live.test.d.ts +2 -0
- package/dist/__tests__/audit-real-live.test.d.ts.map +1 -0
- package/dist/__tests__/audit-real-live.test.js +107 -0
- package/dist/__tests__/audit-real-live.test.js.map +1 -0
- package/dist/__tests__/audit-reconstruct.test.d.ts +2 -0
- package/dist/__tests__/audit-reconstruct.test.d.ts.map +1 -0
- package/dist/__tests__/audit-reconstruct.test.js +287 -0
- package/dist/__tests__/audit-reconstruct.test.js.map +1 -0
- package/dist/__tests__/audit-report.test.d.ts +2 -0
- package/dist/__tests__/audit-report.test.d.ts.map +1 -0
- package/dist/__tests__/audit-report.test.js +186 -0
- package/dist/__tests__/audit-report.test.js.map +1 -0
- package/dist/__tests__/audit-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/audit-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/audit-roundtrip.test.js +368 -0
- package/dist/__tests__/audit-roundtrip.test.js.map +1 -0
- package/dist/__tests__/audit-temporal.test.d.ts +2 -0
- package/dist/__tests__/audit-temporal.test.d.ts.map +1 -0
- package/dist/__tests__/audit-temporal.test.js +364 -0
- package/dist/__tests__/audit-temporal.test.js.map +1 -0
- package/dist/__tests__/audit-tiers.test.d.ts +2 -0
- package/dist/__tests__/audit-tiers.test.d.ts.map +1 -0
- package/dist/__tests__/audit-tiers.test.js +282 -0
- package/dist/__tests__/audit-tiers.test.js.map +1 -0
- package/dist/__tests__/audit-witness-real.test.d.ts +19 -0
- package/dist/__tests__/audit-witness-real.test.d.ts.map +1 -0
- package/dist/__tests__/audit-witness-real.test.js +82 -0
- package/dist/__tests__/audit-witness-real.test.js.map +1 -0
- package/dist/__tests__/audit-witness.test.d.ts +2 -0
- package/dist/__tests__/audit-witness.test.d.ts.map +1 -0
- package/dist/__tests__/audit-witness.test.js +308 -0
- package/dist/__tests__/audit-witness.test.js.map +1 -0
- package/dist/__tests__/chain-hash-anchor.test.d.ts +22 -0
- package/dist/__tests__/chain-hash-anchor.test.d.ts.map +1 -0
- package/dist/__tests__/chain-hash-anchor.test.js +55 -0
- package/dist/__tests__/chain-hash-anchor.test.js.map +1 -0
- package/dist/__tests__/declared-proof-fixture.d.ts +66 -0
- package/dist/__tests__/declared-proof-fixture.d.ts.map +1 -0
- package/dist/__tests__/declared-proof-fixture.js +66 -0
- package/dist/__tests__/declared-proof-fixture.js.map +1 -0
- package/dist/__tests__/mock-proof-fixture.d.ts +15 -0
- package/dist/__tests__/mock-proof-fixture.d.ts.map +1 -0
- package/dist/__tests__/mock-proof-fixture.js +34 -0
- package/dist/__tests__/mock-proof-fixture.js.map +1 -0
- package/dist/__tests__/proof-hash-regression.test.js +4 -43
- package/dist/__tests__/proof-hash-regression.test.js.map +1 -1
- package/dist/__tests__/proof-hash.test.js +3 -21
- package/dist/__tests__/proof-hash.test.js.map +1 -1
- package/dist/__tests__/proof-integrity.test.d.ts +2 -0
- package/dist/__tests__/proof-integrity.test.d.ts.map +1 -0
- package/dist/__tests__/proof-integrity.test.js +426 -0
- package/dist/__tests__/proof-integrity.test.js.map +1 -0
- package/dist/__tests__/realistic-proof-fixture.d.ts +56 -0
- package/dist/__tests__/realistic-proof-fixture.d.ts.map +1 -0
- package/dist/__tests__/realistic-proof-fixture.js +57 -0
- package/dist/__tests__/realistic-proof-fixture.js.map +1 -0
- package/dist/__tests__/verifier.test.js +74 -0
- package/dist/__tests__/verifier.test.js.map +1 -1
- package/package.json +10 -5
- package/src/__tests__/audit-anchors.test.ts +188 -0
- package/src/__tests__/audit-anomalies.test.ts +815 -0
- package/src/__tests__/audit-attestation.test.ts +529 -0
- package/src/__tests__/audit-bench-smoke.test.ts +108 -0
- package/src/__tests__/audit-boundary.test.ts +96 -0
- package/src/__tests__/audit-cli.test.ts +288 -0
- package/src/__tests__/audit-dependencies.test.ts +141 -0
- package/src/__tests__/audit-fixtures.ts +736 -0
- package/src/__tests__/audit-ingest.test.ts +575 -0
- package/src/__tests__/audit-memory-ingest.test.ts +140 -0
- package/src/__tests__/audit-property.test.ts +892 -0
- package/src/__tests__/audit-real-fixtures.test.ts +153 -0
- package/src/__tests__/audit-real-live.test.ts +128 -0
- package/src/__tests__/audit-reconstruct.test.ts +353 -0
- package/src/__tests__/audit-report.test.ts +222 -0
- package/src/__tests__/audit-roundtrip.test.ts +464 -0
- package/src/__tests__/audit-temporal.test.ts +446 -0
- package/src/__tests__/audit-tiers.test.ts +343 -0
- package/src/__tests__/audit-witness-real.test.ts +92 -0
- package/src/__tests__/audit-witness.test.ts +345 -0
- package/src/__tests__/chain-hash-anchor.test.ts +67 -0
- package/src/__tests__/declared-proof-fixture.ts +65 -0
- package/src/__tests__/mock-proof-fixture.ts +36 -0
- package/src/__tests__/proof-hash-regression.test.ts +4 -44
- package/src/__tests__/proof-hash.test.ts +3 -22
- package/src/__tests__/proof-integrity.test.ts +541 -0
- package/src/__tests__/real-fixtures/anchor-after-112762.json +47 -0
- package/src/__tests__/real-fixtures/anchor-with-ethereum-112758.json +47 -0
- package/src/__tests__/real-fixtures/live-proof-0.json +42 -0
- package/src/__tests__/real-fixtures/live-proof-1.json +43 -0
- package/src/__tests__/real-fixtures/live-proof-2.json +43 -0
- package/src/__tests__/real-fixtures/successor-112760.json +38 -0
- package/src/__tests__/real-fixtures/witness-25525146.json +6 -0
- package/src/__tests__/real-fixtures/witness-25525147.json +6 -0
- package/src/__tests__/realistic-proof-fixture.ts +57 -0
- package/src/__tests__/verifier.test.ts +83 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// Copyright (c) Mike Argento. All rights reserved. See LICENSE.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tests for @mikeargento/bitgraph-audit verification tiers: full
|
|
5
|
+
* verification when artifact bytes are present (canonical verify()),
|
|
6
|
+
* bytes-free integrity when they are absent (verifyProofIntegrity()),
|
|
7
|
+
* exact failure reasons, VerificationPolicy passthrough, and per-run
|
|
8
|
+
* epoch link state reset.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, test, before, after } from "node:test";
|
|
12
|
+
import * as assert from "node:assert/strict";
|
|
13
|
+
import { rm, writeFile } from "node:fs/promises";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { computeProofHash } from "@mikeargento/bitgraph-verify";
|
|
16
|
+
import type { BitGraphProof } from "@mikeargento/bitgraph-verify";
|
|
17
|
+
import { ingestBundle, verifyObservedProofs } from "@mikeargento/bitgraph-audit";
|
|
18
|
+
import {
|
|
19
|
+
b64,
|
|
20
|
+
makeChainIdProof,
|
|
21
|
+
makeConstructorProof,
|
|
22
|
+
makeEpochLinkProof,
|
|
23
|
+
makeTar,
|
|
24
|
+
makeTempDir,
|
|
25
|
+
proofJson,
|
|
26
|
+
utf8,
|
|
27
|
+
writeBundleDir,
|
|
28
|
+
} from "./audit-fixtures.js";
|
|
29
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
30
|
+
|
|
31
|
+
const tempDirs: string[] = [];
|
|
32
|
+
|
|
33
|
+
async function newBundleDir(): Promise<string> {
|
|
34
|
+
const dir = await makeTempDir("bitgraph-audit-tiers-");
|
|
35
|
+
tempDirs.push(dir);
|
|
36
|
+
return dir;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
after(async () => {
|
|
40
|
+
for (const dir of tempDirs) {
|
|
41
|
+
await rm(dir, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let fxProof!: BitGraphProof;
|
|
46
|
+
let fxBytes!: Uint8Array;
|
|
47
|
+
let fxHash!: string;
|
|
48
|
+
|
|
49
|
+
before(async () => {
|
|
50
|
+
const made = await makeConstructorProof();
|
|
51
|
+
fxProof = made.proof;
|
|
52
|
+
fxBytes = made.bytes;
|
|
53
|
+
fxHash = computeProofHash(fxProof);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function clone(proof: BitGraphProof): BitGraphProof {
|
|
57
|
+
return JSON.parse(JSON.stringify(proof)) as BitGraphProof;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Tier routing
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
describe("audit tiers: artifact present", () => {
|
|
65
|
+
test("full verification through canonical verify(), status verified", async () => {
|
|
66
|
+
const dir = await newBundleDir();
|
|
67
|
+
await writeBundleDir(dir, {
|
|
68
|
+
"proofs/p.json": proofJson(fxProof),
|
|
69
|
+
"media/original.bin": fxBytes,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const ingest = await ingestBundle(dir);
|
|
73
|
+
const summary = await verifyObservedProofs(ingest);
|
|
74
|
+
|
|
75
|
+
assert.deepEqual(summary, {
|
|
76
|
+
total: 1,
|
|
77
|
+
verified: 1,
|
|
78
|
+
failed: 0,
|
|
79
|
+
artifactUnavailable: 0,
|
|
80
|
+
chainless: 1,
|
|
81
|
+
});
|
|
82
|
+
const verification = ingest.proofs[0]!.verification!;
|
|
83
|
+
assert.equal(verification.tier, "full");
|
|
84
|
+
assert.equal(verification.status, "verified");
|
|
85
|
+
assert.equal(verification.artifactPath, "media/original.bin");
|
|
86
|
+
assert.equal(verification.reason, undefined);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("tampered signature fails at the full tier with the verifier's exact reason", async () => {
|
|
90
|
+
const tampered = clone(fxProof);
|
|
91
|
+
const sigBytes = Buffer.from(tampered.signer.signatureB64, "base64");
|
|
92
|
+
sigBytes[0] = (sigBytes[0]! + 1) % 256;
|
|
93
|
+
tampered.signer.signatureB64 = sigBytes.toString("base64");
|
|
94
|
+
|
|
95
|
+
const dir = await newBundleDir();
|
|
96
|
+
await writeBundleDir(dir, {
|
|
97
|
+
"proofs/p.json": proofJson(tampered),
|
|
98
|
+
"media/original.bin": fxBytes,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const ingest = await ingestBundle(dir);
|
|
102
|
+
const summary = await verifyObservedProofs(ingest);
|
|
103
|
+
|
|
104
|
+
assert.equal(summary.failed, 1);
|
|
105
|
+
const verification = ingest.proofs[0]!.verification!;
|
|
106
|
+
assert.equal(verification.tier, "full");
|
|
107
|
+
assert.equal(verification.status, "failed");
|
|
108
|
+
assert.equal(verification.reason, "signature verification failed: signature does not match");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("full verification works through a tar container (bytes re-streamed)", async () => {
|
|
112
|
+
const dir = await newBundleDir();
|
|
113
|
+
const tar = makeTar([
|
|
114
|
+
{ name: "proofs/p.json", content: proofJson(fxProof) },
|
|
115
|
+
{ name: "media/original.bin", content: fxBytes },
|
|
116
|
+
]);
|
|
117
|
+
const tarPath = join(dir, "bundle.tar");
|
|
118
|
+
await writeFile(tarPath, tar);
|
|
119
|
+
|
|
120
|
+
const ingest = await ingestBundle(tarPath);
|
|
121
|
+
const summary = await verifyObservedProofs(ingest);
|
|
122
|
+
assert.equal(summary.verified, 1);
|
|
123
|
+
assert.equal(ingest.proofs[0]!.verification!.tier, "full");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a proof with a signed chainId verifies fully (unknown-field tolerance end to end)", async () => {
|
|
127
|
+
const made = await makeChainIdProof({ chainId: "bitgraph:main", counter: "2", epochId: "epoch-c" });
|
|
128
|
+
const dir = await newBundleDir();
|
|
129
|
+
await writeBundleDir(dir, {
|
|
130
|
+
"p.json": proofJson(made.proof),
|
|
131
|
+
"artifact.bin": made.bytes,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const ingest = await ingestBundle(dir);
|
|
135
|
+
const summary = await verifyObservedProofs(ingest);
|
|
136
|
+
assert.equal(summary.verified, 1);
|
|
137
|
+
assert.equal(ingest.proofs[0]!.chainId, "bitgraph:main");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("audit tiers: artifact absent", () => {
|
|
142
|
+
test("integrity tier records artifact-unavailable, never verified", async () => {
|
|
143
|
+
const dir = await newBundleDir();
|
|
144
|
+
await writeBundleDir(dir, {
|
|
145
|
+
"proofs/p.json": proofJson(fxProof),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const ingest = await ingestBundle(dir);
|
|
149
|
+
const summary = await verifyObservedProofs(ingest);
|
|
150
|
+
|
|
151
|
+
assert.deepEqual(summary, {
|
|
152
|
+
total: 1,
|
|
153
|
+
verified: 0,
|
|
154
|
+
failed: 0,
|
|
155
|
+
artifactUnavailable: 1,
|
|
156
|
+
chainless: 1,
|
|
157
|
+
});
|
|
158
|
+
const verification = ingest.proofs[0]!.verification!;
|
|
159
|
+
assert.equal(verification.tier, "integrity");
|
|
160
|
+
assert.equal(verification.status, "artifact-unavailable");
|
|
161
|
+
assert.equal(verification.artifactPath, undefined);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("a decoy file named like the artifact does not bind (matching is content-addressed)", async () => {
|
|
165
|
+
const dir = await newBundleDir();
|
|
166
|
+
await writeBundleDir(dir, {
|
|
167
|
+
"proofs/p.json": proofJson(fxProof),
|
|
168
|
+
// Named to look like the artifact, but the bytes hash differently.
|
|
169
|
+
"artifacts/original.bin": utf8("not the committed bytes"),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const ingest = await ingestBundle(dir);
|
|
173
|
+
const summary = await verifyObservedProofs(ingest);
|
|
174
|
+
assert.equal(summary.artifactUnavailable, 1);
|
|
175
|
+
assert.equal(summary.verified, 0);
|
|
176
|
+
assert.equal(ingest.proofs[0]!.verification!.tier, "integrity");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("a corrupted artifact digest fails integrity with the verifier's exact reason", async () => {
|
|
180
|
+
// Flip the committed digest to some other 32-byte value. Content
|
|
181
|
+
// addressing then finds no artifact (the original bytes no longer
|
|
182
|
+
// match), and the signature check fails because the digest sits
|
|
183
|
+
// inside the signed body.
|
|
184
|
+
const corrupted = clone(fxProof);
|
|
185
|
+
corrupted.artifact.digestB64 = b64(sha256(utf8("some other content")));
|
|
186
|
+
|
|
187
|
+
const dir = await newBundleDir();
|
|
188
|
+
await writeBundleDir(dir, {
|
|
189
|
+
"proofs/p.json": proofJson(corrupted),
|
|
190
|
+
"media/original.bin": fxBytes,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const ingest = await ingestBundle(dir);
|
|
194
|
+
const summary = await verifyObservedProofs(ingest);
|
|
195
|
+
|
|
196
|
+
assert.equal(summary.failed, 1);
|
|
197
|
+
assert.equal(summary.verified, 0);
|
|
198
|
+
const verification = ingest.proofs[0]!.verification!;
|
|
199
|
+
assert.equal(verification.tier, "integrity");
|
|
200
|
+
assert.equal(verification.status, "failed");
|
|
201
|
+
assert.equal(verification.reason, "signature verification failed: signature does not match");
|
|
202
|
+
// The genuine artifact bytes did not bind to the corrupted digest.
|
|
203
|
+
assert.equal(
|
|
204
|
+
ingest.artifacts.find((a) => a.paths.includes("media/original.bin"))!.matchedProofHashes.length,
|
|
205
|
+
0
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Deduplication and exclusion interplay
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
describe("audit tiers: unique verification", () => {
|
|
215
|
+
test("semantic duplicates are verified once, as one observed proof", async () => {
|
|
216
|
+
const dir = await newBundleDir();
|
|
217
|
+
await writeBundleDir(dir, {
|
|
218
|
+
"compact.json": JSON.stringify(fxProof),
|
|
219
|
+
"pretty.json": JSON.stringify(fxProof, null, 2),
|
|
220
|
+
"media/original.bin": fxBytes,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const ingest = await ingestBundle(dir);
|
|
224
|
+
const summary = await verifyObservedProofs(ingest);
|
|
225
|
+
assert.equal(summary.total, 1);
|
|
226
|
+
assert.equal(summary.verified, 1);
|
|
227
|
+
assert.equal(ingest.proofs[0]!.sources.length, 2);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("unsupported-version files are excluded from verification entirely", async () => {
|
|
231
|
+
const dir = await newBundleDir();
|
|
232
|
+
await writeBundleDir(dir, {
|
|
233
|
+
"legacy.json": JSON.stringify({
|
|
234
|
+
version: "occ/1",
|
|
235
|
+
artifact: { hashAlg: "sha256", digestB64: b64(sha256(fxBytes)) },
|
|
236
|
+
commit: { nonceB64: "AAAA" },
|
|
237
|
+
signer: { publicKeyB64: "AAAA", signatureB64: "AAAA" },
|
|
238
|
+
}),
|
|
239
|
+
"good.json": proofJson(fxProof),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const ingest = await ingestBundle(dir);
|
|
243
|
+
const summary = await verifyObservedProofs(ingest);
|
|
244
|
+
assert.equal(summary.total, 1);
|
|
245
|
+
assert.equal(ingest.counts.unsupportedVersion, 1);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Policy passthrough
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
describe("audit tiers: VerificationPolicy passthrough", () => {
|
|
254
|
+
test("trustAnchors constraints are applied by the canonical verifier", async () => {
|
|
255
|
+
const dir = await newBundleDir();
|
|
256
|
+
await writeBundleDir(dir, {
|
|
257
|
+
"proofs/p.json": proofJson(fxProof),
|
|
258
|
+
"media/original.bin": fxBytes,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const ingest = await ingestBundle(dir);
|
|
262
|
+
const summary = await verifyObservedProofs(ingest, {
|
|
263
|
+
trustAnchors: { allowedMeasurements: ["some-other-measurement"] },
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
assert.equal(summary.failed, 1);
|
|
267
|
+
const verification = ingest.proofs[0]!.verification!;
|
|
268
|
+
assert.equal(verification.status, "failed");
|
|
269
|
+
assert.match(verification.reason ?? "", /is not in the allowed set/);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("a satisfied policy leaves the proof verified", async () => {
|
|
273
|
+
const dir = await newBundleDir();
|
|
274
|
+
await writeBundleDir(dir, {
|
|
275
|
+
"proofs/p.json": proofJson(fxProof),
|
|
276
|
+
"media/original.bin": fxBytes,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const ingest = await ingestBundle(dir);
|
|
280
|
+
const summary = await verifyObservedProofs(ingest, {
|
|
281
|
+
trustAnchors: { allowedMeasurements: [fxProof.environment.measurement] },
|
|
282
|
+
});
|
|
283
|
+
assert.equal(summary.verified, 1);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// Epoch link state
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
describe("audit tiers: epoch link state", () => {
|
|
292
|
+
const predecessor = {
|
|
293
|
+
prevEpochId: "epoch-predecessor",
|
|
294
|
+
prevCounter: "42",
|
|
295
|
+
prevProofHashB64: b64(sha256(utf8("terminal-proof-of-predecessor"))),
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
test("within one run, a second consumption of the same predecessor is a detected fork", async () => {
|
|
299
|
+
const first = await makeEpochLinkProof({ ...predecessor, toEpochId: "epoch-successor-a" });
|
|
300
|
+
const second = await makeEpochLinkProof({ ...predecessor, toEpochId: "epoch-successor-b" });
|
|
301
|
+
|
|
302
|
+
const dir = await newBundleDir();
|
|
303
|
+
// Observation order is deterministic (sorted directory walk):
|
|
304
|
+
// a-genesis before b-genesis.
|
|
305
|
+
await writeBundleDir(dir, {
|
|
306
|
+
"a-genesis.json": proofJson(first.proof),
|
|
307
|
+
"b-genesis.json": proofJson(second.proof),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const ingest = await ingestBundle(dir);
|
|
311
|
+
const summary = await verifyObservedProofs(ingest);
|
|
312
|
+
|
|
313
|
+
assert.equal(summary.total, 2);
|
|
314
|
+
assert.equal(summary.artifactUnavailable, 1);
|
|
315
|
+
assert.equal(summary.failed, 1);
|
|
316
|
+
const a = ingest.proofs.find((p) => p.sources[0]!.path === "a-genesis.json")!;
|
|
317
|
+
const b = ingest.proofs.find((p) => p.sources[0]!.path === "b-genesis.json")!;
|
|
318
|
+
assert.equal(a.verification!.status, "artifact-unavailable");
|
|
319
|
+
assert.equal(b.verification!.status, "failed");
|
|
320
|
+
assert.match(b.verification!.reason ?? "", /FORK DETECTED/);
|
|
321
|
+
assert.equal(a.hasEpochLink, true);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("state is reset per run: a fresh audit is not poisoned by a previous one", async () => {
|
|
325
|
+
const runOne = await makeEpochLinkProof({ ...predecessor, toEpochId: "epoch-successor-x" });
|
|
326
|
+
const dirOne = await newBundleDir();
|
|
327
|
+
await writeBundleDir(dirOne, { "genesis-x.json": proofJson(runOne.proof) });
|
|
328
|
+
const ingestOne = await ingestBundle(dirOne);
|
|
329
|
+
const summaryOne = await verifyObservedProofs(ingestOne);
|
|
330
|
+
assert.equal(summaryOne.failed, 0);
|
|
331
|
+
|
|
332
|
+
// A different successor consumes the same predecessor in a separate
|
|
333
|
+
// bundle. Within one run that would be a fork; across runs the state
|
|
334
|
+
// must have been reset, so this verifies cleanly.
|
|
335
|
+
const runTwo = await makeEpochLinkProof({ ...predecessor, toEpochId: "epoch-successor-y" });
|
|
336
|
+
const dirTwo = await newBundleDir();
|
|
337
|
+
await writeBundleDir(dirTwo, { "genesis-y.json": proofJson(runTwo.proof) });
|
|
338
|
+
const ingestTwo = await ingestBundle(dirTwo);
|
|
339
|
+
const summaryTwo = await verifyObservedProofs(ingestTwo);
|
|
340
|
+
assert.equal(summaryTwo.failed, 0);
|
|
341
|
+
assert.equal(summaryTwo.artifactUnavailable, 1);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end: block-header witnesses produced by the exporter make an anchor's
|
|
3
|
+
* Ethereum time claim verifiable FULLY OFFLINE, and the audit accepts them.
|
|
4
|
+
*
|
|
5
|
+
* The two witness fixtures were produced by the same header-RLP encoder the
|
|
6
|
+
* website export uses (website/src/lib/eth-header.ts), for the two real
|
|
7
|
+
* Ethereum blocks the whitepaper bundle's anchors signed: 25525146 (the before
|
|
8
|
+
* anchor, block of commit 112758) and 25525147 (the after anchor, block of
|
|
9
|
+
* commit 112762). Each was self-checked at generation time: keccak256(headerRlp)
|
|
10
|
+
* equals the block hash.
|
|
11
|
+
*
|
|
12
|
+
* This test bundles the three real proofs plus the two witnesses and confirms
|
|
13
|
+
* the audit's zero-network witness procedure verifies both, so the exporter's
|
|
14
|
+
* output and the verifier's expectations actually meet. Before this feature the
|
|
15
|
+
* anchors carried a block hash but no header, so the timestamp could only be
|
|
16
|
+
* resolved online; now the bundle stands alone.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, it, before, after } from "node:test";
|
|
20
|
+
import * as assert from "node:assert/strict";
|
|
21
|
+
import { mkdtemp, mkdir, writeFile, rm, readFile } from "node:fs/promises";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { runAudit, buildJsonReport, computeExitFlags, type AuditResult } from "@mikeargento/bitgraph-audit";
|
|
26
|
+
import { computeProofHash, type BitGraphProof } from "@mikeargento/bitgraph-verify";
|
|
27
|
+
|
|
28
|
+
function safe(value: string): string {
|
|
29
|
+
return value.replace(/[^A-Za-z0-9]/g, "_");
|
|
30
|
+
}
|
|
31
|
+
async function fixture(name: string): Promise<Record<string, unknown>> {
|
|
32
|
+
const url = new URL(`../../src/__tests__/real-fixtures/${name}`, import.meta.url);
|
|
33
|
+
return JSON.parse(await readFile(fileURLToPath(url), "utf8"));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("audit: real block-header witnesses verify offline", () => {
|
|
37
|
+
let dir: string;
|
|
38
|
+
let result: AuditResult;
|
|
39
|
+
|
|
40
|
+
before(async () => {
|
|
41
|
+
const proofs = await Promise.all([
|
|
42
|
+
fixture("anchor-with-ethereum-112758.json"),
|
|
43
|
+
fixture("successor-112760.json"),
|
|
44
|
+
fixture("anchor-after-112762.json"),
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
dir = await mkdtemp(join(tmpdir(), "bitgraph-audit-witness-"));
|
|
48
|
+
for (const proof of proofs) {
|
|
49
|
+
const commit = proof.commit as { epochId: string; counter: string };
|
|
50
|
+
const epochDir = join(dir, "proofs", safe(commit.epochId));
|
|
51
|
+
await mkdir(epochDir, { recursive: true });
|
|
52
|
+
await writeFile(
|
|
53
|
+
join(epochDir, `${commit.counter}-${safe(computeProofHash(proof as unknown as BitGraphProof))}.json`),
|
|
54
|
+
JSON.stringify(proof)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
// Witnesses discovered by shape; path is conventional only.
|
|
58
|
+
const witnessDir = join(dir, "witnesses");
|
|
59
|
+
await mkdir(witnessDir, { recursive: true });
|
|
60
|
+
for (const block of [25525146, 25525147]) {
|
|
61
|
+
const w = await fixture(`witness-${block}.json`);
|
|
62
|
+
await writeFile(join(witnessDir, `${block}.json`), JSON.stringify(w));
|
|
63
|
+
}
|
|
64
|
+
result = await runAudit(dir);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
after(async () => {
|
|
68
|
+
if (dir !== undefined) await rm(dir, { recursive: true, force: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("ingests both witnesses and verifies both against the signed anchor hashes", () => {
|
|
72
|
+
const report = buildJsonReport(result);
|
|
73
|
+
assert.equal(report.input.counts.witnesses, 2);
|
|
74
|
+
assert.equal(report.summary.temporal.anchorsWithVerifiedWitness, 2);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("lower-bounds the whitepaper's causal segment and records its following anchor", () => {
|
|
78
|
+
const report = buildJsonReport(result);
|
|
79
|
+
// Both bounding anchors now carry a verified offline timestamp, so the
|
|
80
|
+
// segment holding the whitepaper commit is bracketed (upper AND lower).
|
|
81
|
+
assert.equal(report.summary.temporal.segmentsWithFollowingAnchor, 1);
|
|
82
|
+
assert.equal(report.summary.temporal.segmentsUnanchored, 0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("exits clean (code 0): witnesses only add evidence, never failure", () => {
|
|
86
|
+
assert.deepEqual(computeExitFlags(result), {
|
|
87
|
+
verificationFailures: false,
|
|
88
|
+
chainAnomaliesOrDivergences: false,
|
|
89
|
+
code: 0,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|