@mikeargento/bitgraph 1.9.1 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -21
- package/dist/fuse-cli.js +2 -2
- package/dist/fuse-cli.js.map +1 -1
- package/dist/fuse.d.ts +15 -1
- package/dist/fuse.d.ts.map +1 -1
- package/dist/fuse.js +29 -12
- package/dist/fuse.js.map +1 -1
- package/package.json +12 -8
- package/src/fuse-cli.ts +2 -2
- package/src/fuse.ts +46 -13
- 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,815 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Tests for @mikeargento/bitgraph-audit anomaly classification and
|
|
5
|
-
* authority analysis: the G2 two-position gap logic, collisions, forks,
|
|
6
|
-
* chain breaks, multiple genesis, slot ordering, epochLink anomalies,
|
|
7
|
-
* divergence records (valid parties versus invalid observed context),
|
|
8
|
-
* and intra-epoch authority changes.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, test, after } from "node:test";
|
|
12
|
-
import * as assert from "node:assert/strict";
|
|
13
|
-
import { rm } from "node:fs/promises";
|
|
14
|
-
import { sha256 } from "@noble/hashes/sha256";
|
|
15
|
-
import {
|
|
16
|
-
analyzeAuthorities,
|
|
17
|
-
classifyAnomalies,
|
|
18
|
-
ingestBundle,
|
|
19
|
-
reconstructChains,
|
|
20
|
-
verifyObservedProofs,
|
|
21
|
-
} from "@mikeargento/bitgraph-audit";
|
|
22
|
-
import type {
|
|
23
|
-
AnomalyReport,
|
|
24
|
-
IngestResult,
|
|
25
|
-
ReconstructionResult,
|
|
26
|
-
UnexplainedPositionsDetail,
|
|
27
|
-
} from "@mikeargento/bitgraph-audit";
|
|
28
|
-
import type { BitGraphProof } from "@mikeargento/bitgraph-verify";
|
|
29
|
-
import { computeProofHash, computeChainHash } from "@mikeargento/bitgraph-verify";
|
|
30
|
-
import {
|
|
31
|
-
b64,
|
|
32
|
-
healthyPairs,
|
|
33
|
-
makeCounterChain,
|
|
34
|
-
makeEpochLinkProof,
|
|
35
|
-
makeKey,
|
|
36
|
-
makeTempDir,
|
|
37
|
-
proofJson,
|
|
38
|
-
signBody,
|
|
39
|
-
utf8,
|
|
40
|
-
writeBundleDir,
|
|
41
|
-
} from "./audit-fixtures.js";
|
|
42
|
-
|
|
43
|
-
const tempDirs: string[] = [];
|
|
44
|
-
|
|
45
|
-
after(async () => {
|
|
46
|
-
for (const dir of tempDirs) {
|
|
47
|
-
await rm(dir, { recursive: true, force: true });
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
async function auditBundle(files: Record<string, Uint8Array | string>): Promise<{
|
|
52
|
-
ingest: IngestResult;
|
|
53
|
-
reconstruction: ReconstructionResult;
|
|
54
|
-
report: AnomalyReport;
|
|
55
|
-
}> {
|
|
56
|
-
const dir = await makeTempDir("bitgraph-audit-anomalies-");
|
|
57
|
-
tempDirs.push(dir);
|
|
58
|
-
await writeBundleDir(dir, files);
|
|
59
|
-
const ingest = await ingestBundle(dir);
|
|
60
|
-
await verifyObservedProofs(ingest);
|
|
61
|
-
const reconstruction = await reconstructChains(ingest);
|
|
62
|
-
const report = await classifyAnomalies(ingest, reconstruction);
|
|
63
|
-
return { ingest, reconstruction, report };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function codes(report: AnomalyReport): string[] {
|
|
67
|
-
return report.anomalies.map((a) => a.code).sort();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
// G2 gap logic
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
|
|
74
|
-
describe("anomalies: G2 gap logic", () => {
|
|
75
|
-
test("interleaved slot allocation is healthy: no unexplained positions", async () => {
|
|
76
|
-
// Concurrent slot allocation: slot 1 and slot 2 are both taken before
|
|
77
|
-
// either commit lands (commits at 3 and 4). M is not N+1 here, and
|
|
78
|
-
// that is still a complete record.
|
|
79
|
-
const chain = await makeCounterChain({
|
|
80
|
-
epochId: "epoch-interleaved",
|
|
81
|
-
pairs: [
|
|
82
|
-
{ slot: "1", commit: "3" },
|
|
83
|
-
{ slot: "2", commit: "4" },
|
|
84
|
-
],
|
|
85
|
-
});
|
|
86
|
-
const { report } = await auditBundle({
|
|
87
|
-
"p0.json": proofJson(chain.proofs[0]!.proof),
|
|
88
|
-
"p1.json": proofJson(chain.proofs[1]!.proof),
|
|
89
|
-
});
|
|
90
|
-
assert.deepEqual(report.anomalies, []);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("a missing middle proof yields exactly its slot and commit positions plus a chain break", async () => {
|
|
94
|
-
const chain = await makeCounterChain({ epochId: "epoch-gap2", pairs: healthyPairs(3) });
|
|
95
|
-
// Drop the middle proof (slot 3 / commit 4).
|
|
96
|
-
const { report } = await auditBundle({
|
|
97
|
-
"p0.json": proofJson(chain.proofs[0]!.proof),
|
|
98
|
-
"p2.json": proofJson(chain.proofs[2]!.proof),
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
assert.deepEqual(codes(report), ["chain-break-missing", "unexplained-counter-positions"]);
|
|
102
|
-
|
|
103
|
-
const gap = report.anomalies.find((a) => a.code === "unexplained-counter-positions")!;
|
|
104
|
-
const detail = gap.details as unknown as UnexplainedPositionsDetail;
|
|
105
|
-
assert.deepEqual(detail.positions, ["3", "4"]);
|
|
106
|
-
assert.equal(detail.count, "2");
|
|
107
|
-
assert.deepEqual(detail.ranges, [{ start: "3", end: "4" }]);
|
|
108
|
-
assert.equal(detail.truncated, false);
|
|
109
|
-
// Report language: absence from the bundle, never asserted authority failure.
|
|
110
|
-
assert.match(gap.message, /absent\s+from the bundle/);
|
|
111
|
-
assert.match(gap.message, /does not, by itself, establish/);
|
|
112
|
-
// The message must NOT presume a proof exists: an uncommitted slot is a
|
|
113
|
-
// routine, benign alternative the offline audit cannot rule out.
|
|
114
|
-
assert.match(gap.message, /allocated but never committed/);
|
|
115
|
-
assert.match(gap.message, /failed to create or withheld any proof/);
|
|
116
|
-
|
|
117
|
-
const chainBreak = report.anomalies.find((a) => a.code === "chain-break-missing")!;
|
|
118
|
-
assert.deepEqual(chainBreak.proofHashes, [chain.proofs[2]!.proofHash]);
|
|
119
|
-
assert.equal(chainBreak.details!["prevB64"], chain.proofs[1]!.chainHash);
|
|
120
|
-
assert.deepEqual(report.divergences, []);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("a partition missing its head is an expected boundary, not an anomaly", async () => {
|
|
124
|
-
const chain = await makeCounterChain({ epochId: "epoch-headless", pairs: healthyPairs(3) });
|
|
125
|
-
// Only the last two proofs are in the bundle: positions 3,4,5,6.
|
|
126
|
-
const { report } = await auditBundle({
|
|
127
|
-
"p1.json": proofJson(chain.proofs[1]!.proof),
|
|
128
|
-
"p2.json": proofJson(chain.proofs[2]!.proof),
|
|
129
|
-
});
|
|
130
|
-
// Positions 1 and 2 are outside the observed [min, max] range and are not
|
|
131
|
-
// flagged. The earliest included proof's dangling prevB64 is the excerpt's
|
|
132
|
-
// starting boundary — an expected bundle frontier, not a chain-integrity
|
|
133
|
-
// anomaly — so there are no anomalies at all.
|
|
134
|
-
assert.deepEqual(codes(report), []);
|
|
135
|
-
assert.equal(report.boundaryEntryPoints.length, 1);
|
|
136
|
-
assert.equal(report.boundaryEntryPoints[0]!.proofHash, chain.proofs[1]!.proofHash);
|
|
137
|
-
assert.equal(report.boundaryEntryPoints[0]!.prevB64, chain.proofs[0]!.chainHash);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// ---------------------------------------------------------------------------
|
|
142
|
-
// Collisions and divergence
|
|
143
|
-
// ---------------------------------------------------------------------------
|
|
144
|
-
|
|
145
|
-
describe("anomalies: collisions", () => {
|
|
146
|
-
test("a commit counter collision between valid proofs produces a divergence with all parties", async () => {
|
|
147
|
-
const key = await makeKey();
|
|
148
|
-
const chain = await makeCounterChain({
|
|
149
|
-
key,
|
|
150
|
-
epochId: "epoch-collision",
|
|
151
|
-
pairs: healthyPairs(2), // p0: 1/2, p1: 3/4
|
|
152
|
-
});
|
|
153
|
-
// A second valid proof also claiming commit counter 4, linked after p1.
|
|
154
|
-
const rival = await makeCounterChain({
|
|
155
|
-
key,
|
|
156
|
-
epochId: "epoch-collision",
|
|
157
|
-
pairs: [{ commit: "4" }],
|
|
158
|
-
payloadPrefix: "rival",
|
|
159
|
-
});
|
|
160
|
-
const rivalProof = JSON.parse(proofJson(rival.proofs[0]!.proof)) as Record<string, unknown>;
|
|
161
|
-
(rivalProof["commit"] as Record<string, unknown>)["prevB64"] = chain.proofs[1]!.chainHash;
|
|
162
|
-
// Re-sign with the same key over the modified commit.
|
|
163
|
-
const resigned = await signBody(
|
|
164
|
-
key,
|
|
165
|
-
rival.proofs[0]!.proof.artifact,
|
|
166
|
-
rivalProof["commit"] as BitGraphProof["commit"],
|
|
167
|
-
"test-measurement-chain"
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
const { report } = await auditBundle({
|
|
171
|
-
"p0.json": proofJson(chain.proofs[0]!.proof),
|
|
172
|
-
"p1.json": proofJson(chain.proofs[1]!.proof),
|
|
173
|
-
"p2.json": proofJson(resigned),
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
assert.deepEqual(codes(report), ["counter-collision"]);
|
|
177
|
-
assert.equal(report.divergences.length, 1);
|
|
178
|
-
const divergence = report.divergences[0]!;
|
|
179
|
-
assert.equal(divergence.kind, "counter-collision");
|
|
180
|
-
assert.deepEqual(divergence.contested, { counter: "4" });
|
|
181
|
-
assert.equal(divergence.parties.length, 2);
|
|
182
|
-
assert.deepEqual(divergence.invalidContext, []);
|
|
183
|
-
assert.deepEqual(
|
|
184
|
-
divergence.parties.map((p) => p.proofHash).sort(),
|
|
185
|
-
[chain.proofs[1]!.proofHash, computeProofHash(resigned)].sort()
|
|
186
|
-
);
|
|
187
|
-
// Every party keeps its verification status; no winner is selected.
|
|
188
|
-
for (const party of divergence.parties) {
|
|
189
|
-
assert.equal(party.verificationStatus, "artifact-unavailable");
|
|
190
|
-
assert.equal(party.counter, "4");
|
|
191
|
-
}
|
|
192
|
-
assert.match(divergence.explanation, /does not choose/);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
test("a slot counter collision between valid proofs is classified separately", async () => {
|
|
196
|
-
const key = await makeKey();
|
|
197
|
-
const chain = await makeCounterChain({
|
|
198
|
-
key,
|
|
199
|
-
epochId: "epoch-slot-collision",
|
|
200
|
-
pairs: [
|
|
201
|
-
{ slot: "1", commit: "2" },
|
|
202
|
-
{ slot: "3", commit: "4" },
|
|
203
|
-
],
|
|
204
|
-
});
|
|
205
|
-
// A third proof consuming slot 3 again with a later commit counter.
|
|
206
|
-
const rival = await makeCounterChain({
|
|
207
|
-
key,
|
|
208
|
-
epochId: "epoch-slot-collision",
|
|
209
|
-
pairs: [{ slot: "3", commit: "5" }],
|
|
210
|
-
payloadPrefix: "slot-rival",
|
|
211
|
-
});
|
|
212
|
-
const rivalCommit = JSON.parse(proofJson(rival.proofs[0]!.proof)) as Record<string, unknown>;
|
|
213
|
-
(rivalCommit["commit"] as Record<string, unknown>)["prevB64"] = chain.proofs[1]!.chainHash;
|
|
214
|
-
const resigned = await signBody(
|
|
215
|
-
key,
|
|
216
|
-
rival.proofs[0]!.proof.artifact,
|
|
217
|
-
rivalCommit["commit"] as BitGraphProof["commit"],
|
|
218
|
-
"test-measurement-chain"
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
const { report } = await auditBundle({
|
|
222
|
-
"p0.json": proofJson(chain.proofs[0]!.proof),
|
|
223
|
-
"p1.json": proofJson(chain.proofs[1]!.proof),
|
|
224
|
-
"p2.json": proofJson(resigned),
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
assert.deepEqual(codes(report), ["slot-collision"]);
|
|
228
|
-
const divergence = report.divergences[0]!;
|
|
229
|
-
assert.equal(divergence.kind, "slot-collision");
|
|
230
|
-
assert.deepEqual(divergence.contested, { slotCounter: "3" });
|
|
231
|
-
assert.equal(divergence.parties.length, 2);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
test("an invalid proof sharing a counter is observed context, never a competing branch", async () => {
|
|
235
|
-
const key = await makeKey();
|
|
236
|
-
const chain = await makeCounterChain({
|
|
237
|
-
key,
|
|
238
|
-
epochId: "epoch-invalid-context",
|
|
239
|
-
pairs: healthyPairs(2), // p1 has commit 4
|
|
240
|
-
});
|
|
241
|
-
// A second VALID claimant of commit counter 4.
|
|
242
|
-
const rivalValid = await makeCounterChain({
|
|
243
|
-
key,
|
|
244
|
-
epochId: "epoch-invalid-context",
|
|
245
|
-
pairs: [{ commit: "4" }],
|
|
246
|
-
payloadPrefix: "valid-rival",
|
|
247
|
-
});
|
|
248
|
-
const validCommit = JSON.parse(proofJson(rivalValid.proofs[0]!.proof)) as Record<
|
|
249
|
-
string,
|
|
250
|
-
unknown
|
|
251
|
-
>;
|
|
252
|
-
(validCommit["commit"] as Record<string, unknown>)["prevB64"] = chain.proofs[1]!.chainHash;
|
|
253
|
-
const resignedValid = await signBody(
|
|
254
|
-
key,
|
|
255
|
-
rivalValid.proofs[0]!.proof.artifact,
|
|
256
|
-
validCommit["commit"] as BitGraphProof["commit"],
|
|
257
|
-
"test-measurement-chain"
|
|
258
|
-
);
|
|
259
|
-
// An INVALID claimant: distinct content, garbage signature.
|
|
260
|
-
const rivalInvalid = await makeCounterChain({
|
|
261
|
-
key,
|
|
262
|
-
epochId: "epoch-invalid-context",
|
|
263
|
-
pairs: [{ commit: "4" }],
|
|
264
|
-
payloadPrefix: "invalid-rival",
|
|
265
|
-
});
|
|
266
|
-
const forged = JSON.parse(proofJson(rivalInvalid.proofs[0]!.proof)) as BitGraphProof;
|
|
267
|
-
forged.signer.signatureB64 = b64(new Uint8Array(64)); // not a valid signature
|
|
268
|
-
|
|
269
|
-
const { report } = await auditBundle({
|
|
270
|
-
"p0.json": proofJson(chain.proofs[0]!.proof),
|
|
271
|
-
"p1.json": proofJson(chain.proofs[1]!.proof),
|
|
272
|
-
"valid-rival.json": proofJson(resignedValid),
|
|
273
|
-
"forged.json": JSON.stringify(forged),
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
const divergence = report.divergences.find((d) => d.kind === "counter-collision")!;
|
|
277
|
-
assert.equal(divergence.parties.length, 2);
|
|
278
|
-
assert.equal(divergence.invalidContext.length, 1);
|
|
279
|
-
assert.equal(divergence.invalidContext[0]!.proofHash, computeProofHash(forged));
|
|
280
|
-
assert.equal(divergence.invalidContext[0]!.verificationStatus, "failed");
|
|
281
|
-
assert.ok(
|
|
282
|
-
!divergence.parties.some((p) => p.proofHash === computeProofHash(forged)),
|
|
283
|
-
"the forged proof must never appear as a valid competing branch"
|
|
284
|
-
);
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// ---------------------------------------------------------------------------
|
|
289
|
-
// Cross-kind position reuse
|
|
290
|
-
// ---------------------------------------------------------------------------
|
|
291
|
-
|
|
292
|
-
describe("anomalies: cross-kind position reuse", () => {
|
|
293
|
-
test("one proof's commit counter equal to a different proof's slot counter is a double-allocation", async () => {
|
|
294
|
-
// Same signer, epoch, and chain. p0 commits position 5; p1 reserves
|
|
295
|
-
// position 5 as its slot. Positions stay contiguous (4,5,6) so no gap
|
|
296
|
-
// is produced, isolating the cross-kind detection.
|
|
297
|
-
const chain = await makeCounterChain({
|
|
298
|
-
epochId: "epoch-cross-kind",
|
|
299
|
-
pairs: [
|
|
300
|
-
{ slot: "4", commit: "5" },
|
|
301
|
-
{ slot: "5", commit: "6" },
|
|
302
|
-
],
|
|
303
|
-
});
|
|
304
|
-
const { report } = await auditBundle({
|
|
305
|
-
"p0.json": proofJson(chain.proofs[0]!.proof),
|
|
306
|
-
"p1.json": proofJson(chain.proofs[1]!.proof),
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
assert.deepEqual(codes(report), ["cross-kind-position-reuse"]);
|
|
310
|
-
const anomaly = report.anomalies[0]!;
|
|
311
|
-
assert.equal(anomaly.details!["position"], "5");
|
|
312
|
-
|
|
313
|
-
assert.equal(report.divergences.length, 1);
|
|
314
|
-
const divergence = report.divergences[0]!;
|
|
315
|
-
assert.equal(divergence.kind, "cross-kind-position-reuse");
|
|
316
|
-
assert.deepEqual(divergence.contested, { position: "5" });
|
|
317
|
-
assert.equal(divergence.parties.length, 2);
|
|
318
|
-
assert.deepEqual(
|
|
319
|
-
divergence.parties.map((p) => p.proofHash).sort(),
|
|
320
|
-
[chain.proofs[0]!.proofHash, chain.proofs[1]!.proofHash].sort()
|
|
321
|
-
);
|
|
322
|
-
assert.deepEqual(divergence.invalidContext, []);
|
|
323
|
-
assert.match(divergence.explanation, /does not choose/);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
test("a single proof whose own slot equals its own commit stays slot-order, never cross-kind", async () => {
|
|
327
|
-
const chain = await makeCounterChain({
|
|
328
|
-
epochId: "epoch-self-position",
|
|
329
|
-
pairs: [{ slot: "5", commit: "5" }],
|
|
330
|
-
});
|
|
331
|
-
const { report } = await auditBundle({ "p.json": proofJson(chain.proofs[0]!.proof) });
|
|
332
|
-
assert.deepEqual(codes(report), ["slot-order-violation"]);
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
// ---------------------------------------------------------------------------
|
|
337
|
-
// Predecessor reuse (forks)
|
|
338
|
-
// ---------------------------------------------------------------------------
|
|
339
|
-
|
|
340
|
-
describe("anomalies: predecessor reuse", () => {
|
|
341
|
-
test("one predecessor claimed by two valid successors is a fork with all branches preserved", async () => {
|
|
342
|
-
const key = await makeKey();
|
|
343
|
-
const chain = await makeCounterChain({
|
|
344
|
-
key,
|
|
345
|
-
epochId: "epoch-fork",
|
|
346
|
-
pairs: healthyPairs(2), // p0 (1/2) <- p1 (3/4)
|
|
347
|
-
});
|
|
348
|
-
// A second successor of p0 at different positions (5/6).
|
|
349
|
-
const branch = await makeCounterChain({
|
|
350
|
-
key,
|
|
351
|
-
epochId: "epoch-fork",
|
|
352
|
-
pairs: [{ slot: "5", commit: "6" }],
|
|
353
|
-
payloadPrefix: "branch",
|
|
354
|
-
});
|
|
355
|
-
const branchCommit = JSON.parse(proofJson(branch.proofs[0]!.proof)) as Record<string, unknown>;
|
|
356
|
-
(branchCommit["commit"] as Record<string, unknown>)["prevB64"] = chain.proofs[0]!.chainHash;
|
|
357
|
-
const resigned = await signBody(
|
|
358
|
-
key,
|
|
359
|
-
branch.proofs[0]!.proof.artifact,
|
|
360
|
-
branchCommit["commit"] as BitGraphProof["commit"],
|
|
361
|
-
"test-measurement-chain"
|
|
362
|
-
);
|
|
363
|
-
|
|
364
|
-
const { reconstruction, report } = await auditBundle({
|
|
365
|
-
"p0.json": proofJson(chain.proofs[0]!.proof),
|
|
366
|
-
"p1.json": proofJson(chain.proofs[1]!.proof),
|
|
367
|
-
"branch.json": proofJson(resigned),
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// One component: the fork is connected through the shared predecessor.
|
|
371
|
-
const partition = reconstruction.partitions[0]!;
|
|
372
|
-
assert.equal(partition.components.length, 1);
|
|
373
|
-
const component = partition.components[0]!;
|
|
374
|
-
assert.deepEqual(component.terminalProofHashes.length, 2);
|
|
375
|
-
assert.deepEqual(component.genesisProofHashes, [chain.proofs[0]!.proofHash]);
|
|
376
|
-
|
|
377
|
-
// Positions 1..6 all explained: the fork itself creates no gap.
|
|
378
|
-
assert.deepEqual(codes(report), ["predecessor-reuse"]);
|
|
379
|
-
const divergence = report.divergences[0]!;
|
|
380
|
-
assert.equal(divergence.kind, "predecessor-reuse");
|
|
381
|
-
assert.deepEqual(divergence.contested, { prevB64: chain.proofs[0]!.chainHash });
|
|
382
|
-
assert.equal(divergence.parties.length, 2);
|
|
383
|
-
assert.deepEqual(divergence.invalidContext, []);
|
|
384
|
-
assert.match(divergence.explanation, /fork/);
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
// ---------------------------------------------------------------------------
|
|
389
|
-
// Chain breaks
|
|
390
|
-
// ---------------------------------------------------------------------------
|
|
391
|
-
|
|
392
|
-
describe("anomalies: chain breaks", () => {
|
|
393
|
-
test("a malformed prevB64 is distinguished from a missing predecessor", async () => {
|
|
394
|
-
const key = await makeKey();
|
|
395
|
-
const first = await makeCounterChain({
|
|
396
|
-
key,
|
|
397
|
-
epochId: "epoch-malformed",
|
|
398
|
-
pairs: [{ slot: "1", commit: "2" }],
|
|
399
|
-
});
|
|
400
|
-
const badCommit = {
|
|
401
|
-
nonceB64: b64(crypto.getRandomValues(new Uint8Array(16))),
|
|
402
|
-
counter: "4",
|
|
403
|
-
slotCounter: "3",
|
|
404
|
-
prevB64: "!!!not-base64!!!",
|
|
405
|
-
epochId: "epoch-malformed",
|
|
406
|
-
} as BitGraphProof["commit"];
|
|
407
|
-
const malformed = await signBody(
|
|
408
|
-
key,
|
|
409
|
-
{ hashAlg: "sha256", digestB64: b64(sha256(utf8("malformed-prev-payload"))) },
|
|
410
|
-
badCommit,
|
|
411
|
-
"test-measurement-chain"
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
const { report } = await auditBundle({
|
|
415
|
-
"p0.json": proofJson(first.proofs[0]!.proof),
|
|
416
|
-
"p1.json": proofJson(malformed),
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
assert.deepEqual(codes(report), ["chain-break-malformed"]);
|
|
420
|
-
const anomaly = report.anomalies[0]!;
|
|
421
|
-
assert.deepEqual(anomaly.proofHashes, [computeProofHash(malformed)]);
|
|
422
|
-
assert.equal(anomaly.details!["prevB64"], "!!!not-base64!!!");
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
test("prevB64 resolving into another partition is a cross-partition break, not a merge", async () => {
|
|
426
|
-
const chainA = await makeCounterChain({ epochId: "epoch-cross-A", pairs: healthyPairs(1) });
|
|
427
|
-
const keyB = await makeKey();
|
|
428
|
-
const crossCommit = {
|
|
429
|
-
nonceB64: b64(crypto.getRandomValues(new Uint8Array(16))),
|
|
430
|
-
counter: "2",
|
|
431
|
-
slotCounter: "1",
|
|
432
|
-
prevB64: chainA.proofs[0]!.chainHash, // points into epoch A
|
|
433
|
-
epochId: "epoch-cross-B",
|
|
434
|
-
} as BitGraphProof["commit"];
|
|
435
|
-
const crossProof = await signBody(
|
|
436
|
-
keyB,
|
|
437
|
-
{ hashAlg: "sha256", digestB64: b64(sha256(utf8("cross-partition-payload"))) },
|
|
438
|
-
crossCommit,
|
|
439
|
-
"test-measurement-chain"
|
|
440
|
-
);
|
|
441
|
-
|
|
442
|
-
const { reconstruction, report } = await auditBundle({
|
|
443
|
-
"a0.json": proofJson(chainA.proofs[0]!.proof),
|
|
444
|
-
"b0.json": proofJson(crossProof),
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
// Two partitions; prevB64 never bridges them.
|
|
448
|
-
assert.equal(reconstruction.partitions.length, 2);
|
|
449
|
-
assert.deepEqual(codes(report), ["chain-break-cross-partition"]);
|
|
450
|
-
const anomaly = report.anomalies[0]!;
|
|
451
|
-
assert.equal(anomaly.details!["predecessorProofHash"], chainA.proofs[0]!.proofHash);
|
|
452
|
-
const predecessorPartition = anomaly.details!["predecessorPartition"] as Record<string, string>;
|
|
453
|
-
assert.equal(predecessorPartition["epochId"], "epoch-cross-A");
|
|
454
|
-
});
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
// ---------------------------------------------------------------------------
|
|
458
|
-
// Multiple genesis and slot ordering
|
|
459
|
-
// ---------------------------------------------------------------------------
|
|
460
|
-
|
|
461
|
-
describe("anomalies: multiple genesis", () => {
|
|
462
|
-
test("two valid no-prevB64 proofs in one partition are an anomaly and a divergence", async () => {
|
|
463
|
-
const key = await makeKey();
|
|
464
|
-
const first = await makeCounterChain({
|
|
465
|
-
key,
|
|
466
|
-
epochId: "epoch-two-genesis",
|
|
467
|
-
pairs: [{ slot: "1", commit: "2" }],
|
|
468
|
-
payloadPrefix: "genesis-one",
|
|
469
|
-
});
|
|
470
|
-
const second = await makeCounterChain({
|
|
471
|
-
key,
|
|
472
|
-
epochId: "epoch-two-genesis",
|
|
473
|
-
pairs: [{ slot: "3", commit: "4" }],
|
|
474
|
-
payloadPrefix: "genesis-two",
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
const { reconstruction, report } = await auditBundle({
|
|
478
|
-
"g1.json": proofJson(first.proofs[0]!.proof),
|
|
479
|
-
"g2.json": proofJson(second.proofs[0]!.proof),
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
assert.equal(reconstruction.partitions[0]!.components.length, 2);
|
|
483
|
-
assert.deepEqual(codes(report), ["multiple-genesis"]);
|
|
484
|
-
const divergence = report.divergences[0]!;
|
|
485
|
-
assert.equal(divergence.kind, "multiple-genesis");
|
|
486
|
-
assert.equal(divergence.parties.length, 2);
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
test("a single genesis without prevB64 is normal and never an anomaly (G1)", async () => {
|
|
490
|
-
const chain = await makeCounterChain({ epochId: "epoch-one-genesis", pairs: healthyPairs(1) });
|
|
491
|
-
const { report } = await auditBundle({ "g.json": proofJson(chain.proofs[0]!.proof) });
|
|
492
|
-
assert.deepEqual(report.anomalies, []);
|
|
493
|
-
});
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
describe("anomalies: slot ordering", () => {
|
|
497
|
-
test("slotCounter >= counter is classified even on an otherwise valid proof", async () => {
|
|
498
|
-
const chain = await makeCounterChain({
|
|
499
|
-
epochId: "epoch-slot-order",
|
|
500
|
-
pairs: [{ slot: "2", commit: "2" }],
|
|
501
|
-
});
|
|
502
|
-
const { report } = await auditBundle({ "p.json": proofJson(chain.proofs[0]!.proof) });
|
|
503
|
-
assert.deepEqual(codes(report), ["slot-order-violation"]);
|
|
504
|
-
assert.deepEqual(report.anomalies[0]!.details, { slotCounter: "2", counter: "2" });
|
|
505
|
-
});
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
// ---------------------------------------------------------------------------
|
|
509
|
-
// Epoch link anomalies
|
|
510
|
-
// ---------------------------------------------------------------------------
|
|
511
|
-
|
|
512
|
-
describe("anomalies: epoch links", () => {
|
|
513
|
-
test("terminal-missing when the prior epoch is observed but its terminal is not", async () => {
|
|
514
|
-
const chainA = await makeCounterChain({ epochId: "epoch-tm-A", pairs: healthyPairs(1) });
|
|
515
|
-
const genesis = await makeEpochLinkProof({
|
|
516
|
-
prevEpochId: "epoch-tm-A",
|
|
517
|
-
prevCounter: "4",
|
|
518
|
-
prevProofHashB64: b64(sha256(utf8("an-unobserved-terminal"))),
|
|
519
|
-
prevPublicKeyB64: chainA.key.publicKeyB64,
|
|
520
|
-
toEpochId: "epoch-tm-B",
|
|
521
|
-
counter: "2",
|
|
522
|
-
slotCounter: "1",
|
|
523
|
-
});
|
|
524
|
-
const { report } = await auditBundle({
|
|
525
|
-
"a0.json": proofJson(chainA.proofs[0]!.proof),
|
|
526
|
-
"b-genesis.json": proofJson(genesis.proof),
|
|
527
|
-
});
|
|
528
|
-
assert.deepEqual(codes(report), ["epochlink-terminal-missing"]);
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
test("dangling when neither the epoch nor the terminal is observed", async () => {
|
|
532
|
-
const genesis = await makeEpochLinkProof({
|
|
533
|
-
prevEpochId: "epoch-nowhere",
|
|
534
|
-
prevCounter: "8",
|
|
535
|
-
prevProofHashB64: b64(sha256(utf8("nowhere-terminal"))),
|
|
536
|
-
toEpochId: "epoch-dl-B",
|
|
537
|
-
counter: "2",
|
|
538
|
-
slotCounter: "1",
|
|
539
|
-
});
|
|
540
|
-
const { report } = await auditBundle({ "b.json": proofJson(genesis.proof) });
|
|
541
|
-
assert.deepEqual(codes(report), ["epochlink-dangling"]);
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
test("mismatch when the hash matches but the declared metadata disagrees", async () => {
|
|
545
|
-
const chainA = await makeCounterChain({ epochId: "epoch-mm-A", pairs: healthyPairs(1) });
|
|
546
|
-
const genesis = await makeEpochLinkProof({
|
|
547
|
-
prevEpochId: "epoch-mm-A",
|
|
548
|
-
prevCounter: "999", // the observed terminal's counter is 2
|
|
549
|
-
prevProofHashB64: chainA.proofs[0]!.chainHash,
|
|
550
|
-
prevPublicKeyB64: chainA.key.publicKeyB64,
|
|
551
|
-
toEpochId: "epoch-mm-B",
|
|
552
|
-
counter: "2",
|
|
553
|
-
slotCounter: "1",
|
|
554
|
-
});
|
|
555
|
-
const { reconstruction, report } = await auditBundle({
|
|
556
|
-
"a0.json": proofJson(chainA.proofs[0]!.proof),
|
|
557
|
-
"b-genesis.json": proofJson(genesis.proof),
|
|
558
|
-
});
|
|
559
|
-
assert.deepEqual(codes(report), ["epochlink-mismatch"]);
|
|
560
|
-
const edge = reconstruction.epochRelationships.edges[0]!;
|
|
561
|
-
assert.equal(edge.metadataConsistent, false);
|
|
562
|
-
assert.equal(edge.hardEdge, false);
|
|
563
|
-
assert.deepEqual(reconstruction.epochRelationships.orderedPairs, []);
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
test("two successor epochs consuming one terminal is a fork; both branches stay valid parties", async () => {
|
|
567
|
-
const chainA = await makeCounterChain({ epochId: "epoch-ef-A", pairs: healthyPairs(2) });
|
|
568
|
-
const terminal = chainA.proofs[1]!;
|
|
569
|
-
const linkFields = {
|
|
570
|
-
prevEpochId: "epoch-ef-A",
|
|
571
|
-
prevCounter: terminal.proof.commit.counter as string,
|
|
572
|
-
prevProofHashB64: terminal.chainHash,
|
|
573
|
-
prevPublicKeyB64: chainA.key.publicKeyB64,
|
|
574
|
-
};
|
|
575
|
-
const genesisB = await makeEpochLinkProof({
|
|
576
|
-
...linkFields,
|
|
577
|
-
toEpochId: "epoch-ef-B",
|
|
578
|
-
counter: "2",
|
|
579
|
-
slotCounter: "1",
|
|
580
|
-
});
|
|
581
|
-
const genesisC = await makeEpochLinkProof({
|
|
582
|
-
...linkFields,
|
|
583
|
-
toEpochId: "epoch-ef-C",
|
|
584
|
-
counter: "2",
|
|
585
|
-
slotCounter: "1",
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
const { ingest, reconstruction, report } = await auditBundle({
|
|
589
|
-
"a0.json": proofJson(chainA.proofs[0]!.proof),
|
|
590
|
-
"a1.json": proofJson(terminal.proof),
|
|
591
|
-
"b-genesis.json": proofJson(genesisB.proof),
|
|
592
|
-
"c-genesis.json": proofJson(genesisC.proof),
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
// Whichever genesis verified second failed the run with the canonical
|
|
596
|
-
// verifier's fork detection; that run status is preserved.
|
|
597
|
-
const failed = ingest.proofs.filter((p) => p.verification!.status === "failed");
|
|
598
|
-
assert.equal(failed.length, 1);
|
|
599
|
-
assert.match(failed[0]!.verification!.reason ?? "", /FORK DETECTED/);
|
|
600
|
-
|
|
601
|
-
assert.deepEqual(codes(report), ["epochlink-fork"]);
|
|
602
|
-
const divergence = report.divergences[0]!;
|
|
603
|
-
assert.equal(divergence.kind, "epochlink-fork");
|
|
604
|
-
// Both branches are intrinsically valid competing parties: the run
|
|
605
|
-
// failure was an artifact of verification order, and the audit never
|
|
606
|
-
// selects a winner by that order.
|
|
607
|
-
assert.equal(divergence.parties.length, 2);
|
|
608
|
-
assert.deepEqual(divergence.invalidContext, []);
|
|
609
|
-
assert.match(divergence.explanation, /verification order/);
|
|
610
|
-
|
|
611
|
-
// Both lineage edges are hard ordering evidence: the terminal existed
|
|
612
|
-
// before each successor genesis regardless of which branch wins.
|
|
613
|
-
const hardEdges = reconstruction.epochRelationships.edges.filter((e) => e.hardEdge);
|
|
614
|
-
assert.equal(hardEdges.length, 2);
|
|
615
|
-
assert.deepEqual(reconstruction.epochRelationships.orderedPairs, [
|
|
616
|
-
{ beforeEpochId: "epoch-ef-A", afterEpochId: "epoch-ef-B" },
|
|
617
|
-
{ beforeEpochId: "epoch-ef-A", afterEpochId: "epoch-ef-C" },
|
|
618
|
-
]);
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
test("a lineage cycle is classified and contradictory ordering is asserted in neither direction", async () => {
|
|
622
|
-
const keyA = await makeKey();
|
|
623
|
-
const keyB = await makeKey();
|
|
624
|
-
// Epoch A terminal.
|
|
625
|
-
const chainA = await makeCounterChain({
|
|
626
|
-
key: keyA,
|
|
627
|
-
epochId: "epoch-cy-A",
|
|
628
|
-
pairs: healthyPairs(1),
|
|
629
|
-
payloadPrefix: "cycle-a",
|
|
630
|
-
});
|
|
631
|
-
const terminalA = chainA.proofs[0]!;
|
|
632
|
-
// Epoch B genesis consumes A's terminal.
|
|
633
|
-
const genesisB = await makeEpochLinkProof({
|
|
634
|
-
prevEpochId: "epoch-cy-A",
|
|
635
|
-
prevCounter: "2",
|
|
636
|
-
prevProofHashB64: terminalA.chainHash,
|
|
637
|
-
prevPublicKeyB64: keyA.publicKeyB64,
|
|
638
|
-
key: keyB,
|
|
639
|
-
toEpochId: "epoch-cy-B",
|
|
640
|
-
counter: "2",
|
|
641
|
-
slotCounter: "1",
|
|
642
|
-
});
|
|
643
|
-
// Epoch B continues to a terminal.
|
|
644
|
-
const terminalB = await signBody(
|
|
645
|
-
keyB,
|
|
646
|
-
{ hashAlg: "sha256", digestB64: b64(sha256(utf8("cycle-b-terminal"))) },
|
|
647
|
-
{
|
|
648
|
-
nonceB64: b64(crypto.getRandomValues(new Uint8Array(16))),
|
|
649
|
-
counter: "4",
|
|
650
|
-
slotCounter: "3",
|
|
651
|
-
prevB64: computeChainHash(genesisB.proof),
|
|
652
|
-
epochId: "epoch-cy-B",
|
|
653
|
-
},
|
|
654
|
-
"test-measurement-epochlink"
|
|
655
|
-
);
|
|
656
|
-
// A later epoch A proof claims lineage FROM epoch B: the cycle.
|
|
657
|
-
const cycleCloser = await makeEpochLinkProof({
|
|
658
|
-
prevEpochId: "epoch-cy-B",
|
|
659
|
-
prevCounter: "4",
|
|
660
|
-
prevProofHashB64: computeChainHash(terminalB),
|
|
661
|
-
prevPublicKeyB64: keyB.publicKeyB64,
|
|
662
|
-
key: keyA,
|
|
663
|
-
toEpochId: "epoch-cy-A",
|
|
664
|
-
counter: "4",
|
|
665
|
-
slotCounter: "3",
|
|
666
|
-
prevB64: terminalA.chainHash,
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
const { reconstruction, report } = await auditBundle({
|
|
670
|
-
"a-terminal.json": proofJson(terminalA.proof),
|
|
671
|
-
"b-genesis.json": proofJson(genesisB.proof),
|
|
672
|
-
"b-terminal.json": proofJson(terminalB),
|
|
673
|
-
"a-closer.json": proofJson(cycleCloser.proof),
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
const cycleAnomalies = report.anomalies.filter((a) => a.code === "epochlink-cycle");
|
|
677
|
-
assert.equal(cycleAnomalies.length, 1);
|
|
678
|
-
assert.deepEqual(
|
|
679
|
-
(cycleAnomalies[0]!.details!["epochIds"] as string[]).slice().sort(),
|
|
680
|
-
["epoch-cy-A", "epoch-cy-B"]
|
|
681
|
-
);
|
|
682
|
-
// Contradictory ordering evidence: assert neither direction.
|
|
683
|
-
assert.deepEqual(reconstruction.epochRelationships.orderedPairs, []);
|
|
684
|
-
});
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
// ---------------------------------------------------------------------------
|
|
688
|
-
// Authority analysis
|
|
689
|
-
// ---------------------------------------------------------------------------
|
|
690
|
-
|
|
691
|
-
describe("authority: intra-epoch changes", () => {
|
|
692
|
-
test("two signer keys within one epochId is flagged", async () => {
|
|
693
|
-
const chainOne = await makeCounterChain({
|
|
694
|
-
epochId: "epoch-shared",
|
|
695
|
-
pairs: healthyPairs(1),
|
|
696
|
-
payloadPrefix: "signer-one",
|
|
697
|
-
});
|
|
698
|
-
const chainTwo = await makeCounterChain({
|
|
699
|
-
epochId: "epoch-shared",
|
|
700
|
-
pairs: [{ slot: "3", commit: "4" }],
|
|
701
|
-
payloadPrefix: "signer-two",
|
|
702
|
-
});
|
|
703
|
-
const dir = await makeTempDir("bitgraph-audit-authority-");
|
|
704
|
-
tempDirs.push(dir);
|
|
705
|
-
await writeBundleDir(dir, {
|
|
706
|
-
"one.json": proofJson(chainOne.proofs[0]!.proof),
|
|
707
|
-
"two.json": proofJson(chainTwo.proofs[0]!.proof),
|
|
708
|
-
});
|
|
709
|
-
const ingest = await ingestBundle(dir);
|
|
710
|
-
await verifyObservedProofs(ingest);
|
|
711
|
-
const authority = analyzeAuthorities(ingest);
|
|
712
|
-
|
|
713
|
-
assert.equal(authority.anomalies.length, 1);
|
|
714
|
-
const anomaly = authority.anomalies[0]!;
|
|
715
|
-
assert.equal(anomaly.code, "mid-epoch-signer-change");
|
|
716
|
-
assert.deepEqual(
|
|
717
|
-
anomaly.details!["publicKeysB64"],
|
|
718
|
-
[chainOne.key.publicKeyB64, chainTwo.key.publicKeyB64].sort()
|
|
719
|
-
);
|
|
720
|
-
assert.deepEqual(authority.sharedSignersAcrossEpochs, []);
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
test("two declared measurements within one epochId is flagged; declared is never attested", async () => {
|
|
724
|
-
const key = await makeKey();
|
|
725
|
-
const first = await makeCounterChain({
|
|
726
|
-
key,
|
|
727
|
-
epochId: "epoch-measure",
|
|
728
|
-
pairs: healthyPairs(1),
|
|
729
|
-
measurement: "measurement-one",
|
|
730
|
-
payloadPrefix: "m-one",
|
|
731
|
-
});
|
|
732
|
-
const secondCommit = {
|
|
733
|
-
nonceB64: b64(crypto.getRandomValues(new Uint8Array(16))),
|
|
734
|
-
counter: "4",
|
|
735
|
-
slotCounter: "3",
|
|
736
|
-
prevB64: first.proofs[0]!.chainHash,
|
|
737
|
-
epochId: "epoch-measure",
|
|
738
|
-
} as BitGraphProof["commit"];
|
|
739
|
-
const second = await signBody(
|
|
740
|
-
key,
|
|
741
|
-
{ hashAlg: "sha256", digestB64: b64(sha256(utf8("m-two-payload"))) },
|
|
742
|
-
secondCommit,
|
|
743
|
-
"measurement-two"
|
|
744
|
-
);
|
|
745
|
-
|
|
746
|
-
const dir = await makeTempDir("bitgraph-audit-authority-");
|
|
747
|
-
tempDirs.push(dir);
|
|
748
|
-
await writeBundleDir(dir, {
|
|
749
|
-
"one.json": proofJson(first.proofs[0]!.proof),
|
|
750
|
-
"two.json": proofJson(second),
|
|
751
|
-
});
|
|
752
|
-
const ingest = await ingestBundle(dir);
|
|
753
|
-
await verifyObservedProofs(ingest);
|
|
754
|
-
const authority = analyzeAuthorities(ingest);
|
|
755
|
-
|
|
756
|
-
assert.equal(authority.anomalies.length, 1);
|
|
757
|
-
assert.equal(authority.anomalies[0]!.code, "mid-epoch-measurement-change");
|
|
758
|
-
assert.deepEqual(authority.anomalies[0]!.details!["measurements"], [
|
|
759
|
-
"measurement-one",
|
|
760
|
-
"measurement-two",
|
|
761
|
-
]);
|
|
762
|
-
// The attested extension point is never populated by authority analysis.
|
|
763
|
-
for (const group of authority.groups) {
|
|
764
|
-
assert.equal(group.attested, undefined);
|
|
765
|
-
assert.equal(group.attestationPresent, false);
|
|
766
|
-
}
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
test("the same signer across different epochs is normal transition evidence, not an anomaly", async () => {
|
|
770
|
-
const key = await makeKey();
|
|
771
|
-
const epochOne = await makeCounterChain({
|
|
772
|
-
key,
|
|
773
|
-
epochId: "epoch-span-1",
|
|
774
|
-
pairs: healthyPairs(1),
|
|
775
|
-
payloadPrefix: "span-one",
|
|
776
|
-
});
|
|
777
|
-
const epochTwo = await makeCounterChain({
|
|
778
|
-
key,
|
|
779
|
-
epochId: "epoch-span-2",
|
|
780
|
-
pairs: healthyPairs(1),
|
|
781
|
-
payloadPrefix: "span-two",
|
|
782
|
-
});
|
|
783
|
-
const dir = await makeTempDir("bitgraph-audit-authority-");
|
|
784
|
-
tempDirs.push(dir);
|
|
785
|
-
await writeBundleDir(dir, {
|
|
786
|
-
"one.json": proofJson(epochOne.proofs[0]!.proof),
|
|
787
|
-
"two.json": proofJson(epochTwo.proofs[0]!.proof),
|
|
788
|
-
});
|
|
789
|
-
const ingest = await ingestBundle(dir);
|
|
790
|
-
await verifyObservedProofs(ingest);
|
|
791
|
-
const authority = analyzeAuthorities(ingest);
|
|
792
|
-
|
|
793
|
-
assert.deepEqual(authority.anomalies, []);
|
|
794
|
-
assert.deepEqual(authority.sharedSignersAcrossEpochs, [
|
|
795
|
-
{ publicKeyB64: key.publicKeyB64, epochIds: ["epoch-span-1", "epoch-span-2"] },
|
|
796
|
-
]);
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
test("a healthy single-authority chain has no authority anomalies", async () => {
|
|
800
|
-
const chain = await makeCounterChain({ epochId: "epoch-clean", pairs: healthyPairs(3) });
|
|
801
|
-
const dir = await makeTempDir("bitgraph-audit-authority-");
|
|
802
|
-
tempDirs.push(dir);
|
|
803
|
-
await writeBundleDir(dir, {
|
|
804
|
-
"p0.json": proofJson(chain.proofs[0]!.proof),
|
|
805
|
-
"p1.json": proofJson(chain.proofs[1]!.proof),
|
|
806
|
-
"p2.json": proofJson(chain.proofs[2]!.proof),
|
|
807
|
-
});
|
|
808
|
-
const ingest = await ingestBundle(dir);
|
|
809
|
-
await verifyObservedProofs(ingest);
|
|
810
|
-
const authority = analyzeAuthorities(ingest);
|
|
811
|
-
assert.deepEqual(authority.anomalies, []);
|
|
812
|
-
assert.equal(authority.groups.length, 1);
|
|
813
|
-
assert.equal(authority.groups[0]!.proofHashes.length, 3);
|
|
814
|
-
});
|
|
815
|
-
});
|