@mikeargento/bitgraph 1.9.0 → 1.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +58 -35
- package/dist/constructor.js +1 -1
- package/dist/constructor.js.map +1 -1
- package/dist/fuse-cli.js +1 -1
- package/dist/fuse-cli.js.map +1 -1
- package/dist/fuse.js +1 -1
- package/dist/fuse.js.map +1 -1
- package/dist/host.js +1 -1
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/policy.js +1 -1
- package/dist/policy.js.map +1 -1
- package/package.json +23 -8
- package/src/constructor.ts +1 -1
- package/src/fuse-cli.ts +1 -1
- package/src/fuse.ts +1 -1
- package/src/host.ts +1 -1
- package/src/index.ts +3 -2
- package/src/policy.ts +1 -1
- package/dist/__tests__/audit-anchors.test.d.ts +0 -2
- package/dist/__tests__/audit-anchors.test.d.ts.map +0 -1
- package/dist/__tests__/audit-anchors.test.js +0 -152
- package/dist/__tests__/audit-anchors.test.js.map +0 -1
- package/dist/__tests__/audit-anomalies.test.d.ts +0 -2
- package/dist/__tests__/audit-anomalies.test.d.ts.map +0 -1
- package/dist/__tests__/audit-anomalies.test.js +0 -663
- package/dist/__tests__/audit-anomalies.test.js.map +0 -1
- package/dist/__tests__/audit-attestation.test.d.ts +0 -2
- package/dist/__tests__/audit-attestation.test.d.ts.map +0 -1
- package/dist/__tests__/audit-attestation.test.js +0 -403
- package/dist/__tests__/audit-attestation.test.js.map +0 -1
- package/dist/__tests__/audit-bench-smoke.test.d.ts +0 -2
- package/dist/__tests__/audit-bench-smoke.test.d.ts.map +0 -1
- package/dist/__tests__/audit-bench-smoke.test.js +0 -87
- package/dist/__tests__/audit-bench-smoke.test.js.map +0 -1
- package/dist/__tests__/audit-boundary.test.d.ts +0 -20
- package/dist/__tests__/audit-boundary.test.d.ts.map +0 -1
- package/dist/__tests__/audit-boundary.test.js +0 -84
- package/dist/__tests__/audit-boundary.test.js.map +0 -1
- package/dist/__tests__/audit-cli.test.d.ts +0 -2
- package/dist/__tests__/audit-cli.test.d.ts.map +0 -1
- package/dist/__tests__/audit-cli.test.js +0 -239
- package/dist/__tests__/audit-cli.test.js.map +0 -1
- package/dist/__tests__/audit-dependencies.test.d.ts +0 -2
- package/dist/__tests__/audit-dependencies.test.d.ts.map +0 -1
- package/dist/__tests__/audit-dependencies.test.js +0 -116
- package/dist/__tests__/audit-dependencies.test.js.map +0 -1
- package/dist/__tests__/audit-fixtures.d.ts +0 -265
- package/dist/__tests__/audit-fixtures.d.ts.map +0 -1
- package/dist/__tests__/audit-fixtures.js +0 -511
- package/dist/__tests__/audit-fixtures.js.map +0 -1
- package/dist/__tests__/audit-ingest.test.d.ts +0 -2
- package/dist/__tests__/audit-ingest.test.d.ts.map +0 -1
- package/dist/__tests__/audit-ingest.test.js +0 -473
- package/dist/__tests__/audit-ingest.test.js.map +0 -1
- package/dist/__tests__/audit-memory-ingest.test.d.ts +0 -12
- package/dist/__tests__/audit-memory-ingest.test.d.ts.map +0 -1
- package/dist/__tests__/audit-memory-ingest.test.js +0 -135
- package/dist/__tests__/audit-memory-ingest.test.js.map +0 -1
- package/dist/__tests__/audit-property.test.d.ts +0 -2
- package/dist/__tests__/audit-property.test.d.ts.map +0 -1
- package/dist/__tests__/audit-property.test.js +0 -771
- package/dist/__tests__/audit-property.test.js.map +0 -1
- package/dist/__tests__/audit-real-fixtures.test.d.ts +0 -2
- package/dist/__tests__/audit-real-fixtures.test.d.ts.map +0 -1
- package/dist/__tests__/audit-real-fixtures.test.js +0 -132
- package/dist/__tests__/audit-real-fixtures.test.js.map +0 -1
- package/dist/__tests__/audit-real-live.test.d.ts +0 -2
- package/dist/__tests__/audit-real-live.test.d.ts.map +0 -1
- package/dist/__tests__/audit-real-live.test.js +0 -107
- package/dist/__tests__/audit-real-live.test.js.map +0 -1
- package/dist/__tests__/audit-reconstruct.test.d.ts +0 -2
- package/dist/__tests__/audit-reconstruct.test.d.ts.map +0 -1
- package/dist/__tests__/audit-reconstruct.test.js +0 -287
- package/dist/__tests__/audit-reconstruct.test.js.map +0 -1
- package/dist/__tests__/audit-report.test.d.ts +0 -2
- package/dist/__tests__/audit-report.test.d.ts.map +0 -1
- package/dist/__tests__/audit-report.test.js +0 -186
- package/dist/__tests__/audit-report.test.js.map +0 -1
- package/dist/__tests__/audit-roundtrip.test.d.ts +0 -2
- package/dist/__tests__/audit-roundtrip.test.d.ts.map +0 -1
- package/dist/__tests__/audit-roundtrip.test.js +0 -368
- package/dist/__tests__/audit-roundtrip.test.js.map +0 -1
- package/dist/__tests__/audit-temporal.test.d.ts +0 -2
- package/dist/__tests__/audit-temporal.test.d.ts.map +0 -1
- package/dist/__tests__/audit-temporal.test.js +0 -364
- package/dist/__tests__/audit-temporal.test.js.map +0 -1
- package/dist/__tests__/audit-tiers.test.d.ts +0 -2
- package/dist/__tests__/audit-tiers.test.d.ts.map +0 -1
- package/dist/__tests__/audit-tiers.test.js +0 -282
- package/dist/__tests__/audit-tiers.test.js.map +0 -1
- package/dist/__tests__/audit-witness-real.test.d.ts +0 -19
- package/dist/__tests__/audit-witness-real.test.d.ts.map +0 -1
- package/dist/__tests__/audit-witness-real.test.js +0 -82
- package/dist/__tests__/audit-witness-real.test.js.map +0 -1
- package/dist/__tests__/audit-witness.test.d.ts +0 -2
- package/dist/__tests__/audit-witness.test.d.ts.map +0 -1
- package/dist/__tests__/audit-witness.test.js +0 -308
- package/dist/__tests__/audit-witness.test.js.map +0 -1
- package/dist/__tests__/canonical.test.d.ts +0 -2
- package/dist/__tests__/canonical.test.d.ts.map +0 -1
- package/dist/__tests__/canonical.test.js +0 -162
- package/dist/__tests__/canonical.test.js.map +0 -1
- package/dist/__tests__/chain-hash-anchor.test.d.ts +0 -22
- package/dist/__tests__/chain-hash-anchor.test.d.ts.map +0 -1
- package/dist/__tests__/chain-hash-anchor.test.js +0 -55
- package/dist/__tests__/chain-hash-anchor.test.js.map +0 -1
- package/dist/__tests__/commit-service.integration.test.d.ts +0 -2
- package/dist/__tests__/commit-service.integration.test.d.ts.map +0 -1
- package/dist/__tests__/commit-service.integration.test.js +0 -95
- package/dist/__tests__/commit-service.integration.test.js.map +0 -1
- package/dist/__tests__/constructor.test.d.ts +0 -2
- package/dist/__tests__/constructor.test.d.ts.map +0 -1
- package/dist/__tests__/constructor.test.js +0 -267
- package/dist/__tests__/constructor.test.js.map +0 -1
- package/dist/__tests__/declared-proof-fixture.d.ts +0 -66
- package/dist/__tests__/declared-proof-fixture.d.ts.map +0 -1
- package/dist/__tests__/declared-proof-fixture.js +0 -66
- package/dist/__tests__/declared-proof-fixture.js.map +0 -1
- package/dist/__tests__/fuse-placement.test.d.ts +0 -2
- package/dist/__tests__/fuse-placement.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-placement.test.js +0 -31
- package/dist/__tests__/fuse-placement.test.js.map +0 -1
- package/dist/__tests__/fuse-roundtrip.test.d.ts +0 -2
- package/dist/__tests__/fuse-roundtrip.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-roundtrip.test.js +0 -241
- package/dist/__tests__/fuse-roundtrip.test.js.map +0 -1
- package/dist/__tests__/fuse-sdk.test.d.ts +0 -2
- package/dist/__tests__/fuse-sdk.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-sdk.test.js +0 -117
- package/dist/__tests__/fuse-sdk.test.js.map +0 -1
- package/dist/__tests__/fuse-set-sdk.test.d.ts +0 -2
- package/dist/__tests__/fuse-set-sdk.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-set-sdk.test.js +0 -721
- package/dist/__tests__/fuse-set-sdk.test.js.map +0 -1
- package/dist/__tests__/fuse-set.test.d.ts +0 -2
- package/dist/__tests__/fuse-set.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-set.test.js +0 -868
- package/dist/__tests__/fuse-set.test.js.map +0 -1
- package/dist/__tests__/fuse-set2.test.d.ts +0 -2
- package/dist/__tests__/fuse-set2.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-set2.test.js +0 -294
- package/dist/__tests__/fuse-set2.test.js.map +0 -1
- package/dist/__tests__/fuse-vectors.test.d.ts +0 -2
- package/dist/__tests__/fuse-vectors.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-vectors.test.js +0 -175
- package/dist/__tests__/fuse-vectors.test.js.map +0 -1
- package/dist/__tests__/fuse-verify.test.d.ts +0 -2
- package/dist/__tests__/fuse-verify.test.d.ts.map +0 -1
- package/dist/__tests__/fuse-verify.test.js +0 -250
- package/dist/__tests__/fuse-verify.test.js.map +0 -1
- package/dist/__tests__/hello-world.test.d.ts +0 -2
- package/dist/__tests__/hello-world.test.d.ts.map +0 -1
- package/dist/__tests__/hello-world.test.js +0 -9
- package/dist/__tests__/hello-world.test.js.map +0 -1
- package/dist/__tests__/mock-proof-fixture.d.ts +0 -15
- package/dist/__tests__/mock-proof-fixture.d.ts.map +0 -1
- package/dist/__tests__/mock-proof-fixture.js +0 -34
- package/dist/__tests__/mock-proof-fixture.js.map +0 -1
- package/dist/__tests__/policy-enforcement.test.d.ts +0 -2
- package/dist/__tests__/policy-enforcement.test.d.ts.map +0 -1
- package/dist/__tests__/policy-enforcement.test.js +0 -188
- package/dist/__tests__/policy-enforcement.test.js.map +0 -1
- package/dist/__tests__/proof-hash-regression.test.d.ts +0 -2
- package/dist/__tests__/proof-hash-regression.test.d.ts.map +0 -1
- package/dist/__tests__/proof-hash-regression.test.js +0 -95
- package/dist/__tests__/proof-hash-regression.test.js.map +0 -1
- package/dist/__tests__/proof-hash.test.d.ts +0 -2
- package/dist/__tests__/proof-hash.test.d.ts.map +0 -1
- package/dist/__tests__/proof-hash.test.js +0 -116
- package/dist/__tests__/proof-hash.test.js.map +0 -1
- package/dist/__tests__/proof-integrity.test.d.ts +0 -2
- package/dist/__tests__/proof-integrity.test.d.ts.map +0 -1
- package/dist/__tests__/proof-integrity.test.js +0 -426
- package/dist/__tests__/proof-integrity.test.js.map +0 -1
- package/dist/__tests__/realistic-proof-fixture.d.ts +0 -56
- package/dist/__tests__/realistic-proof-fixture.d.ts.map +0 -1
- package/dist/__tests__/realistic-proof-fixture.js +0 -57
- package/dist/__tests__/realistic-proof-fixture.js.map +0 -1
- package/dist/__tests__/verifier.test.d.ts +0 -2
- package/dist/__tests__/verifier.test.d.ts.map +0 -1
- package/dist/__tests__/verifier.test.js +0 -531
- package/dist/__tests__/verifier.test.js.map +0 -1
- package/dist/canonical.d.ts +0 -54
- package/dist/canonical.d.ts.map +0 -1
- package/dist/canonical.js +0 -119
- package/dist/canonical.js.map +0 -1
- package/dist/proof-hash.d.ts +0 -9
- package/dist/proof-hash.d.ts.map +0 -1
- package/dist/proof-hash.js +0 -75
- package/dist/proof-hash.js.map +0 -1
- package/dist/types.d.ts +0 -705
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/dist/verifier.d.ts +0 -27
- package/dist/verifier.d.ts.map +0 -1
- package/dist/verifier.js +0 -844
- package/dist/verifier.js.map +0 -1
- package/src/__tests__/audit-anchors.test.ts +0 -188
- package/src/__tests__/audit-anomalies.test.ts +0 -815
- package/src/__tests__/audit-attestation.test.ts +0 -529
- package/src/__tests__/audit-bench-smoke.test.ts +0 -108
- package/src/__tests__/audit-boundary.test.ts +0 -96
- package/src/__tests__/audit-cli.test.ts +0 -288
- package/src/__tests__/audit-dependencies.test.ts +0 -141
- package/src/__tests__/audit-fixtures.ts +0 -736
- package/src/__tests__/audit-ingest.test.ts +0 -575
- package/src/__tests__/audit-memory-ingest.test.ts +0 -156
- package/src/__tests__/audit-property.test.ts +0 -892
- package/src/__tests__/audit-real-fixtures.test.ts +0 -153
- package/src/__tests__/audit-real-live.test.ts +0 -128
- package/src/__tests__/audit-reconstruct.test.ts +0 -353
- package/src/__tests__/audit-report.test.ts +0 -222
- package/src/__tests__/audit-roundtrip.test.ts +0 -464
- package/src/__tests__/audit-temporal.test.ts +0 -446
- package/src/__tests__/audit-tiers.test.ts +0 -343
- package/src/__tests__/audit-witness-real.test.ts +0 -92
- package/src/__tests__/audit-witness.test.ts +0 -345
- package/src/__tests__/canonical.test.ts +0 -253
- package/src/__tests__/chain-hash-anchor.test.ts +0 -67
- package/src/__tests__/commit-service.integration.test.ts +0 -117
- package/src/__tests__/constructor.test.ts +0 -340
- package/src/__tests__/declared-proof-fixture.ts +0 -65
- package/src/__tests__/fuse-placement.test.ts +0 -32
- package/src/__tests__/fuse-roundtrip.test.ts +0 -243
- package/src/__tests__/fuse-sdk.test.ts +0 -142
- package/src/__tests__/fuse-set-sdk.test.ts +0 -852
- package/src/__tests__/fuse-set.test.ts +0 -1005
- package/src/__tests__/fuse-set2.test.ts +0 -343
- package/src/__tests__/fuse-vectors.test.ts +0 -218
- package/src/__tests__/fuse-verify.test.ts +0 -278
- package/src/__tests__/hello-world.test.ts +0 -10
- package/src/__tests__/mock-proof-fixture.ts +0 -36
- package/src/__tests__/policy-enforcement.test.ts +0 -226
- package/src/__tests__/proof-hash-regression.test.ts +0 -107
- package/src/__tests__/proof-hash.test.ts +0 -129
- package/src/__tests__/proof-integrity.test.ts +0 -541
- package/src/__tests__/real-fixtures/anchor-after-112762.json +0 -47
- package/src/__tests__/real-fixtures/anchor-with-ethereum-112758.json +0 -47
- package/src/__tests__/real-fixtures/live-proof-0.json +0 -42
- package/src/__tests__/real-fixtures/live-proof-1.json +0 -43
- package/src/__tests__/real-fixtures/live-proof-2.json +0 -43
- package/src/__tests__/real-fixtures/successor-112760.json +0 -38
- package/src/__tests__/real-fixtures/witness-25525146.json +0 -6
- package/src/__tests__/real-fixtures/witness-25525147.json +0 -6
- package/src/__tests__/realistic-proof-fixture.ts +0 -57
- package/src/__tests__/verifier.test.ts +0 -624
|
@@ -1,771 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Mike Argento. All rights reserved. See LICENSE.
|
|
2
|
-
/**
|
|
3
|
-
* Property-based topology tests for the audit pipeline (Phase 4e).
|
|
4
|
-
*
|
|
5
|
-
* A seeded deterministic PRNG (mulberry32, fixed literal seeds below, no
|
|
6
|
-
* Date.now, no unseeded Math.random) drives a synthetic topology generator
|
|
7
|
-
* that produces protocol-correct chains through the REAL signing path:
|
|
8
|
-
* real Ed25519 signatures over the canonical SignedBody, real
|
|
9
|
-
* computeProofHash links, and the G2 two-position slot/commit counter
|
|
10
|
-
* pattern (slot 1/commit 2, slot 3/commit 4, ...). A mutation engine then
|
|
11
|
-
* injects each anomaly class deliberately, each class in its own isolated
|
|
12
|
-
* partition (fresh key, fresh epochId) so expected codes compose by
|
|
13
|
-
* union.
|
|
14
|
-
*
|
|
15
|
-
* Assertions:
|
|
16
|
-
* - each class in isolation produces exactly its stable codes plus the
|
|
17
|
-
* class-specific structure (components, divergences, lineage edges);
|
|
18
|
-
* - the property loop composes a random subset of classes per seed and
|
|
19
|
-
* asserts EXACTLY the injected anomaly codes appear: no missing
|
|
20
|
-
* detections, no spurious extras (control chains contribute zero);
|
|
21
|
-
* - determinism: regenerating the same seed into a fresh directory
|
|
22
|
-
* yields a deep-equal JSON report modulo runMetadata.
|
|
23
|
-
*
|
|
24
|
-
* No verifier semantics are bypassed anywhere: every proof that must be
|
|
25
|
-
* valid is signed for real, and deliberately absent proofs are simply not
|
|
26
|
-
* written to the bundle.
|
|
27
|
-
*/
|
|
28
|
-
import { describe, it, after } from "node:test";
|
|
29
|
-
import * as assert from "node:assert/strict";
|
|
30
|
-
import { rm } from "node:fs/promises";
|
|
31
|
-
import { sha256 } from "@noble/hashes/sha256";
|
|
32
|
-
import { computeProofHash } from "@mikeargento/bitgraph-verify";
|
|
33
|
-
import { buildJsonReport, runAudit } from "@mikeargento/bitgraph-audit";
|
|
34
|
-
import { b64, makeCounterChain, makeEpochLinkProof, makeKey, makeTempDir, proofJson, signBody, utf8, writeBundleDir, healthyPairs, } from "./audit-fixtures.js";
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
// Seeded deterministic PRNG (mulberry32). Fixed seeds only; runs are
|
|
37
|
-
// reproducible byte for byte.
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
function mulberry32(seed) {
|
|
40
|
-
let a = seed >>> 0;
|
|
41
|
-
return () => {
|
|
42
|
-
a = (a + 0x6d2b79f5) >>> 0;
|
|
43
|
-
let t = a;
|
|
44
|
-
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
45
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
46
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function randomSource(rng) {
|
|
50
|
-
return (byteLength) => {
|
|
51
|
-
const out = new Uint8Array(byteLength);
|
|
52
|
-
for (let i = 0; i < byteLength; i++)
|
|
53
|
-
out[i] = Math.floor(rng() * 256);
|
|
54
|
-
return out;
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
function randInt(rng, min, max) {
|
|
58
|
-
return min + Math.floor(rng() * (max - min + 1));
|
|
59
|
-
}
|
|
60
|
-
/** Fixed seeds for the property loop. Chosen once; never derived from the clock. */
|
|
61
|
-
const PROPERTY_SEEDS = [
|
|
62
|
-
0x00000001, 0x1bad5eed, 0x2c0ffee5, 0x3d0d0bee, 0x4e1e5701, 0x5a5a5a5a,
|
|
63
|
-
0x6b17c0de, 0x7f00ba75, 0x8badf00d, 0x9e3779b9, 0xa11ce001, 0xb0bca742,
|
|
64
|
-
];
|
|
65
|
-
/** Seed used by the isolation tests and the determinism test. */
|
|
66
|
-
const ISOLATION_SEED = 0xf1c5e0e5;
|
|
67
|
-
function makeContext(seed) {
|
|
68
|
-
const rng = mulberry32(seed);
|
|
69
|
-
let scenarioIndex = 0;
|
|
70
|
-
return {
|
|
71
|
-
rng,
|
|
72
|
-
random: randomSource(rng),
|
|
73
|
-
files: {},
|
|
74
|
-
expectedCodes: [],
|
|
75
|
-
expectedObserved: 0,
|
|
76
|
-
expectedUnsupported: 0,
|
|
77
|
-
expectedChainless: 0,
|
|
78
|
-
nextPrefix: (className) => `s${String(scenarioIndex++).padStart(2, "0")}-${className}`,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
function addChainFiles(ctx, prefix, proofs) {
|
|
82
|
-
for (let i = 0; i < proofs.length; i++) {
|
|
83
|
-
ctx.files[`${prefix}/proof-${i}.json`] = proofJson(proofs[i].proof);
|
|
84
|
-
}
|
|
85
|
-
ctx.expectedObserved += proofs.length;
|
|
86
|
-
}
|
|
87
|
-
/** Random chainId variant: the named production chain or the enclave default. */
|
|
88
|
-
function pickChainId(ctx) {
|
|
89
|
-
return ctx.rng() < 0.5 ? "bitgraph:main" : undefined;
|
|
90
|
-
}
|
|
91
|
-
/** Continuous healthy chain: the control. Contributes zero codes. */
|
|
92
|
-
const buildControl = async (ctx) => {
|
|
93
|
-
const prefix = ctx.nextPrefix("control");
|
|
94
|
-
const length = randInt(ctx.rng, 3, 6);
|
|
95
|
-
const chainId = pickChainId(ctx);
|
|
96
|
-
const chain = await makeCounterChain({
|
|
97
|
-
epochId: `${prefix}-epoch`,
|
|
98
|
-
pairs: healthyPairs(length),
|
|
99
|
-
...(chainId !== undefined ? { chainId } : {}),
|
|
100
|
-
payloadPrefix: prefix,
|
|
101
|
-
random: ctx.random,
|
|
102
|
-
});
|
|
103
|
-
addChainFiles(ctx, prefix, chain.proofs);
|
|
104
|
-
};
|
|
105
|
-
/** Removed proof: its slot+commit positions unexplained plus a chain break. */
|
|
106
|
-
const buildRemovedProof = async (ctx) => {
|
|
107
|
-
const prefix = ctx.nextPrefix("removed");
|
|
108
|
-
const length = randInt(ctx.rng, 3, 6);
|
|
109
|
-
const dropIndex = randInt(ctx.rng, 1, length - 2); // never genesis, never tail
|
|
110
|
-
const chain = await makeCounterChain({
|
|
111
|
-
epochId: `${prefix}-epoch`,
|
|
112
|
-
pairs: healthyPairs(length),
|
|
113
|
-
payloadPrefix: prefix,
|
|
114
|
-
random: ctx.random,
|
|
115
|
-
});
|
|
116
|
-
for (let i = 0; i < chain.proofs.length; i++) {
|
|
117
|
-
if (i === dropIndex)
|
|
118
|
-
continue; // deliberately absent from the bundle
|
|
119
|
-
ctx.files[`${prefix}/proof-${i}.json`] = proofJson(chain.proofs[i].proof);
|
|
120
|
-
}
|
|
121
|
-
ctx.expectedObserved += length - 1;
|
|
122
|
-
ctx.expectedCodes.push("unexplained-counter-positions", "chain-break-missing");
|
|
123
|
-
};
|
|
124
|
-
/** Two proofs signed for the same commit counter (distinct slots, no shared prev). */
|
|
125
|
-
const buildCounterCollision = async (ctx) => {
|
|
126
|
-
const prefix = ctx.nextPrefix("counter-collision");
|
|
127
|
-
const length = randInt(ctx.rng, 3, 5);
|
|
128
|
-
const chain = await makeCounterChain({
|
|
129
|
-
epochId: `${prefix}-epoch`,
|
|
130
|
-
pairs: healthyPairs(length),
|
|
131
|
-
payloadPrefix: prefix,
|
|
132
|
-
random: ctx.random,
|
|
133
|
-
});
|
|
134
|
-
addChainFiles(ctx, prefix, chain.proofs);
|
|
135
|
-
const collideWith = chain.proofs[1].proof.commit.counter; // "4"
|
|
136
|
-
const tail = chain.proofs[length - 1];
|
|
137
|
-
// The collider reuses only the commit counter and carries no slotCounter
|
|
138
|
-
// of its own, so this scenario stays a pure counter collision: it never
|
|
139
|
-
// reuses another proof's slot (slot-collision) and never reuses a commit
|
|
140
|
-
// position as its own slot (cross-kind-position-reuse), both of which are
|
|
141
|
-
// exercised by their own dedicated scenarios.
|
|
142
|
-
const extra = await makeCounterChain({
|
|
143
|
-
epochId: `${prefix}-epoch`,
|
|
144
|
-
key: chain.key,
|
|
145
|
-
pairs: [{ commit: collideWith }],
|
|
146
|
-
payloadPrefix: `${prefix}-collider`,
|
|
147
|
-
prevB64OfFirst: tail.chainHash,
|
|
148
|
-
random: ctx.random,
|
|
149
|
-
});
|
|
150
|
-
ctx.files[`${prefix}/collider.json`] = proofJson(extra.proofs[0].proof);
|
|
151
|
-
ctx.expectedObserved += 1;
|
|
152
|
-
ctx.expectedCodes.push("counter-collision");
|
|
153
|
-
};
|
|
154
|
-
/** Two proofs referencing the same slot counter. */
|
|
155
|
-
const buildSlotCollision = async (ctx) => {
|
|
156
|
-
const prefix = ctx.nextPrefix("slot-collision");
|
|
157
|
-
const length = randInt(ctx.rng, 3, 5);
|
|
158
|
-
const chain = await makeCounterChain({
|
|
159
|
-
epochId: `${prefix}-epoch`,
|
|
160
|
-
pairs: healthyPairs(length),
|
|
161
|
-
payloadPrefix: prefix,
|
|
162
|
-
random: ctx.random,
|
|
163
|
-
});
|
|
164
|
-
addChainFiles(ctx, prefix, chain.proofs);
|
|
165
|
-
const collideWith = chain.proofs[1].proof.commit.slotCounter;
|
|
166
|
-
const tail = chain.proofs[length - 1];
|
|
167
|
-
const extra = await makeCounterChain({
|
|
168
|
-
epochId: `${prefix}-epoch`,
|
|
169
|
-
key: chain.key,
|
|
170
|
-
pairs: [{ slot: collideWith, commit: String(2 * length + 1) }],
|
|
171
|
-
payloadPrefix: `${prefix}-collider`,
|
|
172
|
-
prevB64OfFirst: tail.chainHash,
|
|
173
|
-
random: ctx.random,
|
|
174
|
-
});
|
|
175
|
-
ctx.files[`${prefix}/collider.json`] = proofJson(extra.proofs[0].proof);
|
|
176
|
-
ctx.expectedObserved += 1;
|
|
177
|
-
ctx.expectedCodes.push("slot-collision");
|
|
178
|
-
};
|
|
179
|
-
/** Predecessor-reuse fork: two valid successors consume the same tail. */
|
|
180
|
-
const buildPredecessorReuse = async (ctx) => {
|
|
181
|
-
const prefix = ctx.nextPrefix("fork");
|
|
182
|
-
const length = randInt(ctx.rng, 2, 4);
|
|
183
|
-
const chain = await makeCounterChain({
|
|
184
|
-
epochId: `${prefix}-epoch`,
|
|
185
|
-
pairs: healthyPairs(length),
|
|
186
|
-
payloadPrefix: prefix,
|
|
187
|
-
random: ctx.random,
|
|
188
|
-
});
|
|
189
|
-
addChainFiles(ctx, prefix, chain.proofs);
|
|
190
|
-
const tail = chain.proofs[length - 1];
|
|
191
|
-
for (const [branch, offset] of [
|
|
192
|
-
["a", 0],
|
|
193
|
-
["b", 2],
|
|
194
|
-
]) {
|
|
195
|
-
const child = await makeCounterChain({
|
|
196
|
-
epochId: `${prefix}-epoch`,
|
|
197
|
-
key: chain.key,
|
|
198
|
-
pairs: [{ slot: String(2 * length + 1 + offset), commit: String(2 * length + 2 + offset) }],
|
|
199
|
-
payloadPrefix: `${prefix}-branch-${branch}`,
|
|
200
|
-
prevB64OfFirst: tail.chainHash,
|
|
201
|
-
random: ctx.random,
|
|
202
|
-
});
|
|
203
|
-
ctx.files[`${prefix}/branch-${branch}.json`] = proofJson(child.proofs[0].proof);
|
|
204
|
-
ctx.expectedObserved += 1;
|
|
205
|
-
}
|
|
206
|
-
ctx.expectedCodes.push("predecessor-reuse");
|
|
207
|
-
};
|
|
208
|
-
/** Dangling prevB64: well-formed 32-byte reference into unobserved history. */
|
|
209
|
-
const buildMissingPredecessor = async (ctx) => {
|
|
210
|
-
const prefix = ctx.nextPrefix("dangling-prev");
|
|
211
|
-
const length = randInt(ctx.rng, 2, 4);
|
|
212
|
-
const chain = await makeCounterChain({
|
|
213
|
-
epochId: `${prefix}-epoch`,
|
|
214
|
-
pairs: healthyPairs(length),
|
|
215
|
-
payloadPrefix: prefix,
|
|
216
|
-
random: ctx.random,
|
|
217
|
-
});
|
|
218
|
-
addChainFiles(ctx, prefix, chain.proofs);
|
|
219
|
-
const stray = await makeCounterChain({
|
|
220
|
-
epochId: `${prefix}-epoch`,
|
|
221
|
-
key: chain.key,
|
|
222
|
-
pairs: [{ slot: String(2 * length + 1), commit: String(2 * length + 2) }],
|
|
223
|
-
payloadPrefix: `${prefix}-stray`,
|
|
224
|
-
prevB64OfFirst: b64(ctx.random(32)), // no observed proof has this hash
|
|
225
|
-
random: ctx.random,
|
|
226
|
-
});
|
|
227
|
-
ctx.files[`${prefix}/stray.json`] = proofJson(stray.proofs[0].proof);
|
|
228
|
-
ctx.expectedObserved += 1;
|
|
229
|
-
ctx.expectedCodes.push("chain-break-missing");
|
|
230
|
-
};
|
|
231
|
-
/** Malformed prevB64: not base64 of 32 bytes. The proof itself stays verifier-valid. */
|
|
232
|
-
const buildMalformedPrev = async (ctx) => {
|
|
233
|
-
const prefix = ctx.nextPrefix("malformed-prev");
|
|
234
|
-
const length = randInt(ctx.rng, 2, 4);
|
|
235
|
-
const chain = await makeCounterChain({
|
|
236
|
-
epochId: `${prefix}-epoch`,
|
|
237
|
-
pairs: healthyPairs(length),
|
|
238
|
-
payloadPrefix: prefix,
|
|
239
|
-
random: ctx.random,
|
|
240
|
-
});
|
|
241
|
-
addChainFiles(ctx, prefix, chain.proofs);
|
|
242
|
-
const malformed = await makeCounterChain({
|
|
243
|
-
epochId: `${prefix}-epoch`,
|
|
244
|
-
key: chain.key,
|
|
245
|
-
pairs: [{ slot: String(2 * length + 1), commit: String(2 * length + 2) }],
|
|
246
|
-
payloadPrefix: `${prefix}-malformed`,
|
|
247
|
-
prevB64OfFirst: "this-is-not-base64-of-32-bytes!",
|
|
248
|
-
random: ctx.random,
|
|
249
|
-
});
|
|
250
|
-
ctx.files[`${prefix}/malformed.json`] = proofJson(malformed.proofs[0].proof);
|
|
251
|
-
ctx.expectedObserved += 1;
|
|
252
|
-
ctx.expectedCodes.push("chain-break-malformed");
|
|
253
|
-
};
|
|
254
|
-
/** Byte-identical duplicate proof file. Benign; still a stable code. */
|
|
255
|
-
const buildExactDuplicate = async (ctx) => {
|
|
256
|
-
const prefix = ctx.nextPrefix("exact-dup");
|
|
257
|
-
const chain = await makeCounterChain({
|
|
258
|
-
epochId: `${prefix}-epoch`,
|
|
259
|
-
pairs: healthyPairs(2),
|
|
260
|
-
payloadPrefix: prefix,
|
|
261
|
-
random: ctx.random,
|
|
262
|
-
});
|
|
263
|
-
addChainFiles(ctx, prefix, chain.proofs);
|
|
264
|
-
ctx.files[`${prefix}/copy-of-0.json`] = proofJson(chain.proofs[0].proof);
|
|
265
|
-
ctx.expectedCodes.push("exact-duplicate");
|
|
266
|
-
};
|
|
267
|
-
/**
|
|
268
|
-
* Semantic duplicate: the same proof re-serialized with a different key
|
|
269
|
-
* order and an extra UNSIGNED metadata field, so the bytes differ but the
|
|
270
|
-
* canonical identity matches. The builder verifies the computeProofHash
|
|
271
|
-
* equality before relying on it (metadata sits outside the signed body).
|
|
272
|
-
*/
|
|
273
|
-
const buildSemanticDuplicate = async (ctx) => {
|
|
274
|
-
const prefix = ctx.nextPrefix("semantic-dup");
|
|
275
|
-
const chain = await makeCounterChain({
|
|
276
|
-
epochId: `${prefix}-epoch`,
|
|
277
|
-
pairs: healthyPairs(2),
|
|
278
|
-
payloadPrefix: prefix,
|
|
279
|
-
random: ctx.random,
|
|
280
|
-
});
|
|
281
|
-
addChainFiles(ctx, prefix, chain.proofs);
|
|
282
|
-
const original = chain.proofs[0].proof;
|
|
283
|
-
const reserialized = {
|
|
284
|
-
environment: original.environment,
|
|
285
|
-
signer: original.signer,
|
|
286
|
-
commit: original.commit,
|
|
287
|
-
artifact: original.artifact,
|
|
288
|
-
version: original.version,
|
|
289
|
-
metadata: { note: "unsigned metadata added by a relay", relay: prefix },
|
|
290
|
-
};
|
|
291
|
-
const reserializedJson = JSON.stringify(reserialized);
|
|
292
|
-
assert.notEqual(reserializedJson, proofJson(original), "bytes must differ");
|
|
293
|
-
assert.equal(computeProofHash(JSON.parse(reserializedJson)), chain.proofs[0].proofHash, "re-serialization with reordered keys and unsigned metadata must keep the canonical identity");
|
|
294
|
-
ctx.files[`${prefix}/reserialized-0.json`] = reserializedJson;
|
|
295
|
-
ctx.expectedCodes.push("semantic-duplicate");
|
|
296
|
-
};
|
|
297
|
-
/** Two disconnected components in one partition: a second no-prevB64 genesis. */
|
|
298
|
-
const buildDisconnectedChains = async (ctx) => {
|
|
299
|
-
const prefix = ctx.nextPrefix("disconnected");
|
|
300
|
-
const first = await makeCounterChain({
|
|
301
|
-
epochId: `${prefix}-epoch`,
|
|
302
|
-
pairs: healthyPairs(2),
|
|
303
|
-
payloadPrefix: `${prefix}-one`,
|
|
304
|
-
random: ctx.random,
|
|
305
|
-
});
|
|
306
|
-
addChainFiles(ctx, `${prefix}/one`, first.proofs);
|
|
307
|
-
// Same key and epoch, later counters, no link back: a second genesis.
|
|
308
|
-
const second = await makeCounterChain({
|
|
309
|
-
epochId: `${prefix}-epoch`,
|
|
310
|
-
key: first.key,
|
|
311
|
-
pairs: [
|
|
312
|
-
{ slot: "5", commit: "6" },
|
|
313
|
-
{ slot: "7", commit: "8" },
|
|
314
|
-
],
|
|
315
|
-
payloadPrefix: `${prefix}-two`,
|
|
316
|
-
random: ctx.random,
|
|
317
|
-
});
|
|
318
|
-
addChainFiles(ctx, `${prefix}/two`, second.proofs);
|
|
319
|
-
ctx.expectedCodes.push("multiple-genesis");
|
|
320
|
-
};
|
|
321
|
-
/** Valid epochLink transition: prior epoch terminal consumed by a new epoch. Zero codes. */
|
|
322
|
-
const buildValidEpochLink = async (ctx) => {
|
|
323
|
-
const prefix = ctx.nextPrefix("epochlink-valid");
|
|
324
|
-
const length = randInt(ctx.rng, 2, 3);
|
|
325
|
-
// One measurement per epoch: an epochId is boot-scoped, so the genesis
|
|
326
|
-
// and its continuation declare the same measurement (a mix would
|
|
327
|
-
// honestly flag mid-epoch-measurement-change).
|
|
328
|
-
const measurementB = `${prefix}-measurement-b`;
|
|
329
|
-
const prior = await makeCounterChain({
|
|
330
|
-
epochId: `${prefix}-epoch-a`,
|
|
331
|
-
pairs: healthyPairs(length),
|
|
332
|
-
measurement: `${prefix}-measurement-a`,
|
|
333
|
-
payloadPrefix: `${prefix}-a`,
|
|
334
|
-
random: ctx.random,
|
|
335
|
-
});
|
|
336
|
-
addChainFiles(ctx, `${prefix}/a`, prior.proofs);
|
|
337
|
-
const terminal = prior.proofs[length - 1];
|
|
338
|
-
const genesis = await makeEpochLinkProof({
|
|
339
|
-
prevEpochId: `${prefix}-epoch-a`,
|
|
340
|
-
prevCounter: terminal.proof.commit.counter,
|
|
341
|
-
prevProofHashB64: terminal.chainHash,
|
|
342
|
-
toEpochId: `${prefix}-epoch-b`,
|
|
343
|
-
prevPublicKeyB64: prior.key.publicKeyB64,
|
|
344
|
-
counter: "2",
|
|
345
|
-
slotCounter: "1",
|
|
346
|
-
measurement: measurementB,
|
|
347
|
-
payload: `${prefix}-b-genesis`,
|
|
348
|
-
random: ctx.random,
|
|
349
|
-
});
|
|
350
|
-
ctx.files[`${prefix}/b-genesis.json`] = proofJson(genesis.proof);
|
|
351
|
-
const continuation = await makeCounterChain({
|
|
352
|
-
epochId: `${prefix}-epoch-b`,
|
|
353
|
-
key: genesis.key,
|
|
354
|
-
pairs: [{ slot: "3", commit: "4" }],
|
|
355
|
-
measurement: measurementB,
|
|
356
|
-
payloadPrefix: `${prefix}-b`,
|
|
357
|
-
prevB64OfFirst: genesis.chainHash,
|
|
358
|
-
random: ctx.random,
|
|
359
|
-
});
|
|
360
|
-
ctx.files[`${prefix}/b-1.json`] = proofJson(continuation.proofs[0].proof);
|
|
361
|
-
ctx.expectedObserved += 2;
|
|
362
|
-
};
|
|
363
|
-
/** Broken epochLink: dangling reference to an unobserved epoch and terminal. */
|
|
364
|
-
const buildDanglingEpochLink = async (ctx) => {
|
|
365
|
-
const prefix = ctx.nextPrefix("epochlink-dangling");
|
|
366
|
-
const genesis = await makeEpochLinkProof({
|
|
367
|
-
prevEpochId: `${prefix}-unobserved-epoch`,
|
|
368
|
-
prevCounter: "88",
|
|
369
|
-
prevProofHashB64: b64(ctx.random(32)),
|
|
370
|
-
toEpochId: `${prefix}-epoch`,
|
|
371
|
-
counter: "2",
|
|
372
|
-
slotCounter: "1",
|
|
373
|
-
payload: `${prefix}-genesis`,
|
|
374
|
-
random: ctx.random,
|
|
375
|
-
});
|
|
376
|
-
ctx.files[`${prefix}/genesis.json`] = proofJson(genesis.proof);
|
|
377
|
-
ctx.expectedObserved += 1;
|
|
378
|
-
ctx.expectedCodes.push("epochlink-dangling");
|
|
379
|
-
};
|
|
380
|
-
/** Multiple independent lineages: two signers, two epochs. Zero codes, never merged. */
|
|
381
|
-
const buildMultipleLineages = async (ctx) => {
|
|
382
|
-
const prefix = ctx.nextPrefix("lineages");
|
|
383
|
-
for (const which of ["x", "y"]) {
|
|
384
|
-
const chain = await makeCounterChain({
|
|
385
|
-
epochId: `${prefix}-epoch-${which}`,
|
|
386
|
-
pairs: healthyPairs(randInt(ctx.rng, 2, 3)),
|
|
387
|
-
payloadPrefix: `${prefix}-${which}`,
|
|
388
|
-
random: ctx.random,
|
|
389
|
-
});
|
|
390
|
-
addChainFiles(ctx, `${prefix}/${which}`, chain.proofs);
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
/** Mid-epoch measurement transition: one signer, one epoch, linked chain, two measurements. */
|
|
394
|
-
const buildMeasurementChange = async (ctx) => {
|
|
395
|
-
const prefix = ctx.nextPrefix("measurement-change");
|
|
396
|
-
const first = await makeCounterChain({
|
|
397
|
-
epochId: `${prefix}-epoch`,
|
|
398
|
-
pairs: healthyPairs(2),
|
|
399
|
-
measurement: `${prefix}-measurement-one`,
|
|
400
|
-
payloadPrefix: `${prefix}-one`,
|
|
401
|
-
random: ctx.random,
|
|
402
|
-
});
|
|
403
|
-
addChainFiles(ctx, `${prefix}/one`, first.proofs);
|
|
404
|
-
const second = await makeCounterChain({
|
|
405
|
-
epochId: `${prefix}-epoch`,
|
|
406
|
-
key: first.key,
|
|
407
|
-
pairs: [{ slot: "5", commit: "6" }],
|
|
408
|
-
measurement: `${prefix}-measurement-two`,
|
|
409
|
-
payloadPrefix: `${prefix}-two`,
|
|
410
|
-
prevB64OfFirst: first.proofs[1].chainHash,
|
|
411
|
-
random: ctx.random,
|
|
412
|
-
});
|
|
413
|
-
ctx.files[`${prefix}/two.json`] = proofJson(second.proofs[0].proof);
|
|
414
|
-
ctx.expectedObserved += 1;
|
|
415
|
-
ctx.expectedCodes.push("mid-epoch-measurement-change");
|
|
416
|
-
};
|
|
417
|
-
/** Illegal mid-epoch signer change: two distinct keys under one epochId. */
|
|
418
|
-
const buildSignerChange = async (ctx) => {
|
|
419
|
-
const prefix = ctx.nextPrefix("signer-change");
|
|
420
|
-
const epochId = `${prefix}-epoch`;
|
|
421
|
-
const first = await makeCounterChain({
|
|
422
|
-
epochId,
|
|
423
|
-
pairs: healthyPairs(2),
|
|
424
|
-
payloadPrefix: `${prefix}-one`,
|
|
425
|
-
random: ctx.random,
|
|
426
|
-
});
|
|
427
|
-
addChainFiles(ctx, `${prefix}/one`, first.proofs);
|
|
428
|
-
const secondKey = await makeKey(ctx.random);
|
|
429
|
-
const second = await makeCounterChain({
|
|
430
|
-
epochId,
|
|
431
|
-
key: secondKey,
|
|
432
|
-
pairs: [
|
|
433
|
-
{ slot: "5", commit: "6" },
|
|
434
|
-
{ slot: "7", commit: "8" },
|
|
435
|
-
],
|
|
436
|
-
payloadPrefix: `${prefix}-two`,
|
|
437
|
-
random: ctx.random,
|
|
438
|
-
});
|
|
439
|
-
addChainFiles(ctx, `${prefix}/two`, second.proofs);
|
|
440
|
-
ctx.expectedCodes.push("mid-epoch-signer-change");
|
|
441
|
-
};
|
|
442
|
-
/** Chainless proofs: no counter, no epochId. Observed-but-unchained, never an anomaly. */
|
|
443
|
-
const buildChainless = async (ctx) => {
|
|
444
|
-
const prefix = ctx.nextPrefix("chainless");
|
|
445
|
-
const count = randInt(ctx.rng, 1, 2);
|
|
446
|
-
const key = await makeKey(ctx.random);
|
|
447
|
-
for (let i = 0; i < count; i++) {
|
|
448
|
-
const bytes = utf8(`${prefix}-payload-${i}`);
|
|
449
|
-
const proof = await signBody(key, { hashAlg: "sha256", digestB64: b64(sha256(bytes)) }, { nonceB64: b64(ctx.random(16)) }, `${prefix}-measurement`);
|
|
450
|
-
ctx.files[`${prefix}/proof-${i}.json`] = proofJson(proof);
|
|
451
|
-
}
|
|
452
|
-
ctx.expectedObserved += count;
|
|
453
|
-
ctx.expectedChainless += count;
|
|
454
|
-
};
|
|
455
|
-
/** occ/1 reject: pre-release beta data, rejected at ingest per the version policy. */
|
|
456
|
-
const buildOccReject = async (ctx) => {
|
|
457
|
-
const prefix = ctx.nextPrefix("occ1");
|
|
458
|
-
ctx.files[`${prefix}/legacy.json`] = JSON.stringify({
|
|
459
|
-
version: "occ/1",
|
|
460
|
-
artifact: { hashAlg: "sha256", digestB64: b64(ctx.random(32)) },
|
|
461
|
-
commit: { nonceB64: b64(ctx.random(16)), counter: "7" },
|
|
462
|
-
signer: { publicKeyB64: b64(ctx.random(32)), signatureB64: b64(ctx.random(64)) },
|
|
463
|
-
});
|
|
464
|
-
ctx.expectedUnsupported += 1;
|
|
465
|
-
ctx.expectedCodes.push("unsupported-version");
|
|
466
|
-
};
|
|
467
|
-
const MUTATIONS = [
|
|
468
|
-
["removed-proof", buildRemovedProof],
|
|
469
|
-
["counter-collision", buildCounterCollision],
|
|
470
|
-
["slot-collision", buildSlotCollision],
|
|
471
|
-
["predecessor-reuse", buildPredecessorReuse],
|
|
472
|
-
["missing-predecessor", buildMissingPredecessor],
|
|
473
|
-
["malformed-prev", buildMalformedPrev],
|
|
474
|
-
["exact-duplicate", buildExactDuplicate],
|
|
475
|
-
["semantic-duplicate", buildSemanticDuplicate],
|
|
476
|
-
["disconnected-chains", buildDisconnectedChains],
|
|
477
|
-
["epochlink-valid", buildValidEpochLink],
|
|
478
|
-
["epochlink-dangling", buildDanglingEpochLink],
|
|
479
|
-
["multiple-lineages", buildMultipleLineages],
|
|
480
|
-
["measurement-change", buildMeasurementChange],
|
|
481
|
-
["signer-change", buildSignerChange],
|
|
482
|
-
["chainless", buildChainless],
|
|
483
|
-
["occ1-reject", buildOccReject],
|
|
484
|
-
];
|
|
485
|
-
// ---------------------------------------------------------------------------
|
|
486
|
-
// Shared runner
|
|
487
|
-
// ---------------------------------------------------------------------------
|
|
488
|
-
const tempDirs = [];
|
|
489
|
-
after(async () => {
|
|
490
|
-
await Promise.all(tempDirs.map((dir) => rm(dir, { recursive: true, force: true })));
|
|
491
|
-
});
|
|
492
|
-
async function buildAndAudit(ctx) {
|
|
493
|
-
const dir = await makeTempDir("bitgraph-audit-property-");
|
|
494
|
-
tempDirs.push(dir);
|
|
495
|
-
await writeBundleDir(dir, ctx.files);
|
|
496
|
-
const result = await runAudit(dir);
|
|
497
|
-
return { dir, result };
|
|
498
|
-
}
|
|
499
|
-
function assertExactCodes(result, ctx, label) {
|
|
500
|
-
const report = buildJsonReport(result);
|
|
501
|
-
const actual = report.anomalies.map((a) => a.code).sort();
|
|
502
|
-
const expected = [...ctx.expectedCodes].sort();
|
|
503
|
-
assert.deepEqual(actual, expected, `${label}: exactly the injected anomaly codes appear`);
|
|
504
|
-
assert.equal(result.ingest.counts.observed, ctx.expectedObserved, `${label}: observed count`);
|
|
505
|
-
assert.equal(result.ingest.counts.unsupportedVersion, ctx.expectedUnsupported, `${label}: unsupported-version count`);
|
|
506
|
-
assert.equal(result.verification.chainless, ctx.expectedChainless, `${label}: chainless count`);
|
|
507
|
-
// Every synthetic proof is signed through the real path with no artifact
|
|
508
|
-
// bytes bundled: nothing may fail verification, nothing may be "verified".
|
|
509
|
-
assert.equal(result.verification.failed, 0, `${label}: no verification failures`);
|
|
510
|
-
assert.equal(result.verification.verified, 0, `${label}: no artifact bytes, no full tier`);
|
|
511
|
-
assert.equal(result.verification.artifactUnavailable, ctx.expectedObserved, `${label}: all proofs at the integrity tier`);
|
|
512
|
-
}
|
|
513
|
-
function findPartition(result, epochId) {
|
|
514
|
-
const report = buildJsonReport(result);
|
|
515
|
-
return report.partitions.find((p) => p.key.epochId === epochId);
|
|
516
|
-
}
|
|
517
|
-
// ---------------------------------------------------------------------------
|
|
518
|
-
// Isolation: each anomaly class alone produces exactly its codes plus the
|
|
519
|
-
// class-specific structure.
|
|
520
|
-
// ---------------------------------------------------------------------------
|
|
521
|
-
describe("audit property tests: anomaly classes in isolation", () => {
|
|
522
|
-
it("control: a continuous healthy chain produces zero anomaly codes", async () => {
|
|
523
|
-
const ctx = makeContext(ISOLATION_SEED);
|
|
524
|
-
await buildControl(ctx);
|
|
525
|
-
const { result } = await buildAndAudit(ctx);
|
|
526
|
-
assertExactCodes(result, ctx, "control");
|
|
527
|
-
assert.equal(result.reconstruction.partitions.length, 1);
|
|
528
|
-
assert.equal(result.reconstruction.partitions[0]?.components.length, 1);
|
|
529
|
-
assert.equal(buildJsonReport(result).summary.chainIntact, true);
|
|
530
|
-
});
|
|
531
|
-
it("removed proof: gap covers exactly the dropped slot and commit positions, plus a chain break", async () => {
|
|
532
|
-
const ctx = makeContext(ISOLATION_SEED + 1);
|
|
533
|
-
// Deterministic variant: length 4, drop index 1 (slot 3 / commit 4).
|
|
534
|
-
const prefix = ctx.nextPrefix("removed-fixed");
|
|
535
|
-
const chain = await makeCounterChain({
|
|
536
|
-
epochId: `${prefix}-epoch`,
|
|
537
|
-
pairs: healthyPairs(4),
|
|
538
|
-
payloadPrefix: prefix,
|
|
539
|
-
random: ctx.random,
|
|
540
|
-
});
|
|
541
|
-
for (let i = 0; i < 4; i++) {
|
|
542
|
-
if (i === 1)
|
|
543
|
-
continue;
|
|
544
|
-
ctx.files[`${prefix}/proof-${i}.json`] = proofJson(chain.proofs[i].proof);
|
|
545
|
-
}
|
|
546
|
-
ctx.expectedObserved += 3;
|
|
547
|
-
ctx.expectedCodes.push("unexplained-counter-positions", "chain-break-missing");
|
|
548
|
-
const { result } = await buildAndAudit(ctx);
|
|
549
|
-
assertExactCodes(result, ctx, "removed");
|
|
550
|
-
const gap = result.anomalies.anomalies.find((a) => a.code === "unexplained-counter-positions");
|
|
551
|
-
assert.deepEqual((gap?.details).positions, ["3", "4"]);
|
|
552
|
-
const brk = result.anomalies.anomalies.find((a) => a.code === "chain-break-missing");
|
|
553
|
-
assert.deepEqual(brk?.proofHashes, [chain.proofs[2].proofHash]);
|
|
554
|
-
assert.equal((brk?.details).prevB64, chain.proofs[1].chainHash);
|
|
555
|
-
// The removal splits the partition into two observed components.
|
|
556
|
-
assert.equal(findPartition(result, `${prefix}-epoch`)?.components.length, 2);
|
|
557
|
-
});
|
|
558
|
-
it("counter collision: two valid proofs on one commit counter, divergence preserved, no winner", async () => {
|
|
559
|
-
const ctx = makeContext(ISOLATION_SEED + 2);
|
|
560
|
-
await buildCounterCollision(ctx);
|
|
561
|
-
const { result } = await buildAndAudit(ctx);
|
|
562
|
-
assertExactCodes(result, ctx, "counter-collision");
|
|
563
|
-
assert.equal(result.anomalies.divergences.length, 1);
|
|
564
|
-
const divergence = result.anomalies.divergences[0];
|
|
565
|
-
assert.equal(divergence.kind, "counter-collision");
|
|
566
|
-
assert.equal(divergence.parties.length, 2);
|
|
567
|
-
assert.equal(divergence.invalidContext.length, 0);
|
|
568
|
-
assert.ok(divergence.explanation.includes("does not choose"));
|
|
569
|
-
});
|
|
570
|
-
it("slot collision: two valid proofs referencing one slot counter", async () => {
|
|
571
|
-
const ctx = makeContext(ISOLATION_SEED + 3);
|
|
572
|
-
await buildSlotCollision(ctx);
|
|
573
|
-
const { result } = await buildAndAudit(ctx);
|
|
574
|
-
assertExactCodes(result, ctx, "slot-collision");
|
|
575
|
-
assert.equal(result.anomalies.divergences.length, 1);
|
|
576
|
-
assert.equal(result.anomalies.divergences[0]?.kind, "slot-collision");
|
|
577
|
-
});
|
|
578
|
-
it("predecessor reuse: a fork with both branches preserved", async () => {
|
|
579
|
-
const ctx = makeContext(ISOLATION_SEED + 4);
|
|
580
|
-
await buildPredecessorReuse(ctx);
|
|
581
|
-
const { result } = await buildAndAudit(ctx);
|
|
582
|
-
assertExactCodes(result, ctx, "predecessor-reuse");
|
|
583
|
-
assert.equal(result.anomalies.divergences.length, 1);
|
|
584
|
-
const divergence = result.anomalies.divergences[0];
|
|
585
|
-
assert.equal(divergence.kind, "predecessor-reuse");
|
|
586
|
-
assert.equal(divergence.parties.length, 2);
|
|
587
|
-
// Both branches verifier-valid at the integrity tier.
|
|
588
|
-
for (const party of divergence.parties) {
|
|
589
|
-
assert.equal(party.verificationStatus, "artifact-unavailable");
|
|
590
|
-
}
|
|
591
|
-
});
|
|
592
|
-
it("missing predecessor: dangling prevB64 is a chain break into unobserved history", async () => {
|
|
593
|
-
const ctx = makeContext(ISOLATION_SEED + 5);
|
|
594
|
-
await buildMissingPredecessor(ctx);
|
|
595
|
-
const { result } = await buildAndAudit(ctx);
|
|
596
|
-
assertExactCodes(result, ctx, "missing-predecessor");
|
|
597
|
-
const brk = result.anomalies.anomalies.find((a) => a.code === "chain-break-missing");
|
|
598
|
-
assert.ok(brk?.message.includes("does not, by itself, establish"));
|
|
599
|
-
});
|
|
600
|
-
it("malformed prevB64: unusable link flagged; the proof itself stays verifier-valid", async () => {
|
|
601
|
-
const ctx = makeContext(ISOLATION_SEED + 6);
|
|
602
|
-
await buildMalformedPrev(ctx);
|
|
603
|
-
const { result } = await buildAndAudit(ctx);
|
|
604
|
-
assertExactCodes(result, ctx, "malformed-prev");
|
|
605
|
-
const anomaly = result.anomalies.anomalies.find((a) => a.code === "chain-break-malformed");
|
|
606
|
-
const flagged = result.ingest.proofs.find((p) => p.proofHash === anomaly?.proofHashes[0]);
|
|
607
|
-
// prevB64 is inside the signed body but the canonical verifier does not
|
|
608
|
-
// require it to be base64; topology and verification stay separate.
|
|
609
|
-
assert.equal(flagged?.verification?.status, "artifact-unavailable");
|
|
610
|
-
});
|
|
611
|
-
it("exact duplicate: byte-identical copy collapses to one observed proof", async () => {
|
|
612
|
-
const ctx = makeContext(ISOLATION_SEED + 7);
|
|
613
|
-
await buildExactDuplicate(ctx);
|
|
614
|
-
const { result } = await buildAndAudit(ctx);
|
|
615
|
-
assertExactCodes(result, ctx, "exact-duplicate");
|
|
616
|
-
assert.equal(result.ingest.counts.exactDuplicates, 1);
|
|
617
|
-
assert.equal(result.ingest.counts.semanticDuplicates, 0);
|
|
618
|
-
});
|
|
619
|
-
it("semantic duplicate: reordered keys plus unsigned metadata keep the canonical identity", async () => {
|
|
620
|
-
const ctx = makeContext(ISOLATION_SEED + 8);
|
|
621
|
-
await buildSemanticDuplicate(ctx);
|
|
622
|
-
const { result } = await buildAndAudit(ctx);
|
|
623
|
-
assertExactCodes(result, ctx, "semantic-duplicate");
|
|
624
|
-
assert.equal(result.ingest.counts.exactDuplicates, 0);
|
|
625
|
-
assert.equal(result.ingest.counts.semanticDuplicates, 1);
|
|
626
|
-
// The two byte encodings collapsed into one observed proof with two sources.
|
|
627
|
-
const withTwoSources = result.ingest.proofs.filter((p) => p.sources.length === 2);
|
|
628
|
-
assert.equal(withTwoSources.length, 1);
|
|
629
|
-
});
|
|
630
|
-
it("disconnected chains: two components in one partition via a second genesis", async () => {
|
|
631
|
-
const ctx = makeContext(ISOLATION_SEED + 9);
|
|
632
|
-
await buildDisconnectedChains(ctx);
|
|
633
|
-
const { result } = await buildAndAudit(ctx);
|
|
634
|
-
assertExactCodes(result, ctx, "disconnected");
|
|
635
|
-
assert.equal(result.reconstruction.partitions.length, 1);
|
|
636
|
-
assert.equal(result.reconstruction.partitions[0]?.components.length, 2);
|
|
637
|
-
// Both genesis proofs are valid: a multiple-genesis divergence.
|
|
638
|
-
assert.equal(result.anomalies.divergences.length, 1);
|
|
639
|
-
assert.equal(result.anomalies.divergences[0]?.kind, "multiple-genesis");
|
|
640
|
-
});
|
|
641
|
-
it("valid epochLink transition: zero codes, hard lineage edge, ordered epochs", async () => {
|
|
642
|
-
const ctx = makeContext(ISOLATION_SEED + 10);
|
|
643
|
-
await buildValidEpochLink(ctx);
|
|
644
|
-
const { result } = await buildAndAudit(ctx);
|
|
645
|
-
assertExactCodes(result, ctx, "epochlink-valid");
|
|
646
|
-
const edges = result.reconstruction.epochRelationships.edges;
|
|
647
|
-
assert.equal(edges.length, 1);
|
|
648
|
-
assert.equal(edges[0]?.resolution, "matched");
|
|
649
|
-
assert.equal(edges[0]?.hardEdge, true);
|
|
650
|
-
assert.equal(edges[0]?.metadataConsistent, true);
|
|
651
|
-
const pairs = result.reconstruction.epochRelationships.orderedPairs;
|
|
652
|
-
assert.equal(pairs.length, 1);
|
|
653
|
-
assert.ok(pairs[0]?.beforeEpochId.endsWith("epoch-a"));
|
|
654
|
-
assert.ok(pairs[0]?.afterEpochId.endsWith("epoch-b"));
|
|
655
|
-
for (const epoch of result.reconstruction.epochRelationships.epochs) {
|
|
656
|
-
assert.equal(epoch.ordering, "linked");
|
|
657
|
-
}
|
|
658
|
-
});
|
|
659
|
-
it("broken epochLink: dangling lineage claim flagged, no ordering asserted", async () => {
|
|
660
|
-
const ctx = makeContext(ISOLATION_SEED + 11);
|
|
661
|
-
await buildDanglingEpochLink(ctx);
|
|
662
|
-
const { result } = await buildAndAudit(ctx);
|
|
663
|
-
assertExactCodes(result, ctx, "epochlink-dangling");
|
|
664
|
-
const edges = result.reconstruction.epochRelationships.edges;
|
|
665
|
-
assert.equal(edges.length, 1);
|
|
666
|
-
assert.equal(edges[0]?.resolution, "dangling");
|
|
667
|
-
assert.equal(edges[0]?.hardEdge, false);
|
|
668
|
-
assert.equal(result.reconstruction.epochRelationships.orderedPairs.length, 0);
|
|
669
|
-
});
|
|
670
|
-
it("multiple lineages: two signers never merge and never anomalize", async () => {
|
|
671
|
-
const ctx = makeContext(ISOLATION_SEED + 12);
|
|
672
|
-
await buildMultipleLineages(ctx);
|
|
673
|
-
const { result } = await buildAndAudit(ctx);
|
|
674
|
-
assertExactCodes(result, ctx, "lineages");
|
|
675
|
-
assert.equal(result.reconstruction.partitions.length, 2);
|
|
676
|
-
const keys = new Set(result.reconstruction.partitions.map((p) => p.key.publicKeyB64));
|
|
677
|
-
assert.equal(keys.size, 2);
|
|
678
|
-
// No ordering evidence between the epochs: concurrent-or-unordered.
|
|
679
|
-
assert.equal(buildJsonReport(result).epochRelationships.unorderedPairs.length, 1);
|
|
680
|
-
});
|
|
681
|
-
it("mid-epoch measurement transition: flagged with both measurements listed", async () => {
|
|
682
|
-
const ctx = makeContext(ISOLATION_SEED + 13);
|
|
683
|
-
await buildMeasurementChange(ctx);
|
|
684
|
-
const { result } = await buildAndAudit(ctx);
|
|
685
|
-
assertExactCodes(result, ctx, "measurement-change");
|
|
686
|
-
const anomaly = result.authorities.anomalies.find((a) => a.code === "mid-epoch-measurement-change");
|
|
687
|
-
assert.ok(anomaly !== undefined);
|
|
688
|
-
const details = anomaly.details;
|
|
689
|
-
assert.equal(details.measurements.length, 2);
|
|
690
|
-
// One signer, one linked chain: still a single component.
|
|
691
|
-
assert.equal(result.reconstruction.partitions.length, 1);
|
|
692
|
-
assert.equal(result.reconstruction.partitions[0]?.components.length, 1);
|
|
693
|
-
// Two authority groups (measurement is a grouping facet).
|
|
694
|
-
assert.equal(result.authorities.groups.length, 2);
|
|
695
|
-
});
|
|
696
|
-
it("illegal mid-epoch signer change: flagged with both keys listed", async () => {
|
|
697
|
-
const ctx = makeContext(ISOLATION_SEED + 14);
|
|
698
|
-
await buildSignerChange(ctx);
|
|
699
|
-
const { result } = await buildAndAudit(ctx);
|
|
700
|
-
assertExactCodes(result, ctx, "signer-change");
|
|
701
|
-
const anomaly = result.authorities.anomalies.find((a) => a.code === "mid-epoch-signer-change");
|
|
702
|
-
assert.ok(anomaly !== undefined);
|
|
703
|
-
const details = anomaly.details;
|
|
704
|
-
assert.equal(details.publicKeysB64.length, 2);
|
|
705
|
-
// Two partitions: signer lineages are never merged.
|
|
706
|
-
assert.equal(result.reconstruction.partitions.length, 2);
|
|
707
|
-
});
|
|
708
|
-
it("chainless proofs: observed-but-unchained, reported separately, never an anomaly", async () => {
|
|
709
|
-
const ctx = makeContext(ISOLATION_SEED + 15);
|
|
710
|
-
await buildChainless(ctx);
|
|
711
|
-
const { result } = await buildAndAudit(ctx);
|
|
712
|
-
assertExactCodes(result, ctx, "chainless");
|
|
713
|
-
assert.equal(result.reconstruction.partitions.length, 0);
|
|
714
|
-
assert.equal(result.reconstruction.unchainedProofHashes.length, ctx.expectedChainless);
|
|
715
|
-
});
|
|
716
|
-
it("occ/1 reject: unsupported-version at ingest, excluded from observation and analysis", async () => {
|
|
717
|
-
const ctx = makeContext(ISOLATION_SEED + 16);
|
|
718
|
-
await buildOccReject(ctx);
|
|
719
|
-
await buildControl(ctx);
|
|
720
|
-
const { result } = await buildAndAudit(ctx);
|
|
721
|
-
assertExactCodes(result, ctx, "occ1");
|
|
722
|
-
assert.equal(result.ingest.unsupportedVersions.length, 1);
|
|
723
|
-
assert.equal(result.ingest.unsupportedVersions[0]?.version, "occ/1");
|
|
724
|
-
assert.ok(result.ingest.unsupportedVersions[0]?.path.includes("legacy.json"));
|
|
725
|
-
// Rejected inputs are not observed objects and join no partition.
|
|
726
|
-
assert.equal(result.ingest.counts.observed, ctx.expectedObserved);
|
|
727
|
-
});
|
|
728
|
-
});
|
|
729
|
-
// ---------------------------------------------------------------------------
|
|
730
|
-
// Property loop: random topologies across fixed seeds.
|
|
731
|
-
// ---------------------------------------------------------------------------
|
|
732
|
-
describe("audit property tests: random topologies across fixed seeds", () => {
|
|
733
|
-
for (const seed of PROPERTY_SEEDS) {
|
|
734
|
-
it(`seed 0x${seed.toString(16)}: exactly the injected anomaly codes appear`, async () => {
|
|
735
|
-
const ctx = makeContext(seed);
|
|
736
|
-
// Always a control chain; each mutation class joins with p = 0.5.
|
|
737
|
-
// Selection draws happen BEFORE any builder runs so the include/skip
|
|
738
|
-
// pattern is a pure function of the seed.
|
|
739
|
-
const included = MUTATIONS.map(() => ctx.rng() < 0.5);
|
|
740
|
-
await buildControl(ctx);
|
|
741
|
-
for (let i = 0; i < MUTATIONS.length; i++) {
|
|
742
|
-
if (included[i] === true)
|
|
743
|
-
await MUTATIONS[i][1](ctx);
|
|
744
|
-
}
|
|
745
|
-
const { result } = await buildAndAudit(ctx);
|
|
746
|
-
const label = `seed 0x${seed.toString(16)} [${MUTATIONS.filter((_, i) => included[i]).map(([n]) => n).join(", ")}]`;
|
|
747
|
-
assertExactCodes(result, ctx, label);
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
it("determinism: the same seed regenerated twice yields identical reports modulo runMetadata", async () => {
|
|
751
|
-
const build = async () => {
|
|
752
|
-
const ctx = makeContext(ISOLATION_SEED + 100);
|
|
753
|
-
const included = MUTATIONS.map(() => ctx.rng() < 0.5);
|
|
754
|
-
await buildControl(ctx);
|
|
755
|
-
for (let i = 0; i < MUTATIONS.length; i++) {
|
|
756
|
-
if (included[i] === true)
|
|
757
|
-
await MUTATIONS[i][1](ctx);
|
|
758
|
-
}
|
|
759
|
-
const { result } = await buildAndAudit(ctx);
|
|
760
|
-
return { report: buildJsonReport(result) };
|
|
761
|
-
};
|
|
762
|
-
const first = await build();
|
|
763
|
-
const second = await build();
|
|
764
|
-
const { runMetadata: metaA, ...restA } = first.report;
|
|
765
|
-
const { runMetadata: metaB, ...restB } = second.report;
|
|
766
|
-
assert.notEqual(metaA.bundlePath, metaB.bundlePath, "distinct temp dirs");
|
|
767
|
-
assert.deepEqual(restA, restB, "reports deep-equal modulo runMetadata");
|
|
768
|
-
assert.equal(JSON.stringify(restA), JSON.stringify(restB), "byte-equal serialization");
|
|
769
|
-
});
|
|
770
|
-
});
|
|
771
|
-
//# sourceMappingURL=audit-property.test.js.map
|