@hardkas/testing 0.6.0-alpha → 0.7.1-alpha
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/dist/adversarial-fixtures.js +1 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/harness.js +1 -0
- package/dist/index.d.ts +71 -1
- package/dist/index.js +3666 -0
- package/dist/mass-setup.js +1 -0
- package/dist/reproducibility.js +1 -0
- package/dist/setup.js +1 -0
- package/package.json +11 -10
package/dist/index.js
CHANGED
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
hardKasMatchers
|
|
16
16
|
} from "./chunk-IRVMANZV.js";
|
|
17
|
+
import {
|
|
18
|
+
__require
|
|
19
|
+
} from "./chunk-3RG5ZIWI.js";
|
|
17
20
|
|
|
18
21
|
// src/fixtures.ts
|
|
19
22
|
function createFixture(def) {
|
|
@@ -32,14 +35,3677 @@ function createFixture(def) {
|
|
|
32
35
|
}
|
|
33
36
|
return harness;
|
|
34
37
|
}
|
|
38
|
+
|
|
39
|
+
// src/torture/torture-engine.ts
|
|
40
|
+
var LcgPrng = class {
|
|
41
|
+
state;
|
|
42
|
+
constructor(seed) {
|
|
43
|
+
this.state = Math.abs(Math.trunc(seed)) % 2147483647;
|
|
44
|
+
if (this.state === 0) this.state = 1;
|
|
45
|
+
}
|
|
46
|
+
/** Returns a float between 0 (inclusive) and 1 (exclusive) */
|
|
47
|
+
next() {
|
|
48
|
+
this.state = this.state * 48271 % 2147483647;
|
|
49
|
+
return (this.state - 1) / 2147483646;
|
|
50
|
+
}
|
|
51
|
+
/** Returns an integer between min (inclusive) and max (inclusive) */
|
|
52
|
+
nextInt(min, max) {
|
|
53
|
+
return Math.floor(this.next() * (max - min + 1)) + min;
|
|
54
|
+
}
|
|
55
|
+
/** Pick a random element from an array */
|
|
56
|
+
pick(arr) {
|
|
57
|
+
if (arr.length === 0) {
|
|
58
|
+
throw new Error("Cannot pick from an empty array");
|
|
59
|
+
}
|
|
60
|
+
const idx = this.nextInt(0, arr.length - 1);
|
|
61
|
+
return arr[idx];
|
|
62
|
+
}
|
|
63
|
+
/** Deterministic shuffle */
|
|
64
|
+
shuffle(arr) {
|
|
65
|
+
const res = [...arr];
|
|
66
|
+
for (let i = res.length - 1; i > 0; i--) {
|
|
67
|
+
const j = this.nextInt(0, i);
|
|
68
|
+
const tmp = res[i];
|
|
69
|
+
res[i] = res[j];
|
|
70
|
+
res[j] = tmp;
|
|
71
|
+
}
|
|
72
|
+
return res;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var bucketsMap = /* @__PURE__ */ new Map();
|
|
76
|
+
function registerTortureBucket(bucket) {
|
|
77
|
+
bucketsMap.set(bucket.name, bucket);
|
|
78
|
+
}
|
|
79
|
+
function getTortureBucket(name) {
|
|
80
|
+
return bucketsMap.get(name);
|
|
81
|
+
}
|
|
82
|
+
function getAllTortureBuckets() {
|
|
83
|
+
return Array.from(bucketsMap.values());
|
|
84
|
+
}
|
|
85
|
+
var TortureInvariantError = class extends Error {
|
|
86
|
+
code;
|
|
87
|
+
severity;
|
|
88
|
+
constructor(message, code, severity = "critical") {
|
|
89
|
+
super(message);
|
|
90
|
+
this.name = "TortureInvariantError";
|
|
91
|
+
this.code = code;
|
|
92
|
+
this.severity = severity;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// src/torture/buckets.ts
|
|
97
|
+
import fs from "fs";
|
|
98
|
+
import path from "path";
|
|
99
|
+
import { calculateContentHash, ARTIFACT_VERSION, CURRENT_HASH_VERSION } from "@hardkas/artifacts";
|
|
100
|
+
import { HardkasStore, SqliteQueryBackend } from "@hardkas/query-store";
|
|
101
|
+
function createSandbox(ctx) {
|
|
102
|
+
const sandboxDir = path.join(ctx.workspaceDir, ".tmp", `torture-sandbox-${ctx.caseId}-${ctx.caseSeed}`);
|
|
103
|
+
const hardkasDir = path.join(sandboxDir, ".hardkas");
|
|
104
|
+
const artifactsDir = path.join(hardkasDir, "artifacts");
|
|
105
|
+
const dbPath = path.join(hardkasDir, "store.db");
|
|
106
|
+
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
107
|
+
return { sandboxDir, hardkasDir, artifactsDir, dbPath };
|
|
108
|
+
}
|
|
109
|
+
function cleanupSandbox(sandboxDir) {
|
|
110
|
+
try {
|
|
111
|
+
if (fs.existsSync(sandboxDir)) {
|
|
112
|
+
fs.rmSync(sandboxDir, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
registerTortureBucket({
|
|
118
|
+
name: "authority-deleteability",
|
|
119
|
+
expectedInvariant: "sqlite_is_not_authority",
|
|
120
|
+
async run(ctx) {
|
|
121
|
+
const { sandboxDir, hardkasDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
122
|
+
try {
|
|
123
|
+
const artifactCount = ctx.prng.nextInt(3, 8);
|
|
124
|
+
const generatedIds = [];
|
|
125
|
+
for (let i = 0; i < artifactCount; i++) {
|
|
126
|
+
const payload = {
|
|
127
|
+
schema: "hardkas.txPlan",
|
|
128
|
+
hardkasVersion: "0.7.0-alpha",
|
|
129
|
+
version: ARTIFACT_VERSION,
|
|
130
|
+
networkId: "simnet",
|
|
131
|
+
mode: "simulated",
|
|
132
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
133
|
+
planId: `plan-mock-${ctx.caseId}-${i}`,
|
|
134
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
135
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
136
|
+
amountSompi: (1000n * BigInt(ctx.prng.nextInt(1, 100) * 10 + i)).toString(),
|
|
137
|
+
estimatedFeeSompi: "100",
|
|
138
|
+
estimatedMass: "10",
|
|
139
|
+
inputs: [
|
|
140
|
+
{
|
|
141
|
+
outpoint: { transactionId: "a".repeat(64), index: 0 },
|
|
142
|
+
amountSompi: "200000"
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
outputs: [
|
|
146
|
+
{
|
|
147
|
+
address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg",
|
|
148
|
+
amountSompi: "1000"
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
};
|
|
152
|
+
const hash = calculateContentHash(payload);
|
|
153
|
+
const artifactId = `plan-${hash.slice(0, 16)}`;
|
|
154
|
+
const artifact = {
|
|
155
|
+
...payload,
|
|
156
|
+
artifactId,
|
|
157
|
+
contentHash: hash
|
|
158
|
+
};
|
|
159
|
+
const file = path.join(artifactsDir, `${artifactId}.json`);
|
|
160
|
+
fs.writeFileSync(file, JSON.stringify(artifact, null, 2), "utf-8");
|
|
161
|
+
generatedIds.push(artifactId);
|
|
162
|
+
}
|
|
163
|
+
const store = new HardkasStore({ dbPath });
|
|
164
|
+
store.connect({ autoMigrate: true });
|
|
165
|
+
const backend = new SqliteQueryBackend(store);
|
|
166
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
167
|
+
const indexedBefore = await backend.findArtifacts();
|
|
168
|
+
if (indexedBefore.length !== artifactCount) {
|
|
169
|
+
throw new Error(`Expected ${artifactCount} artifacts to be indexed, got ${indexedBefore.length}`);
|
|
170
|
+
}
|
|
171
|
+
store.disconnect();
|
|
172
|
+
if (fs.existsSync(dbPath)) {
|
|
173
|
+
fs.unlinkSync(dbPath);
|
|
174
|
+
}
|
|
175
|
+
const eventsPath = path.join(hardkasDir, "events.jsonl");
|
|
176
|
+
if (fs.existsSync(eventsPath)) {
|
|
177
|
+
fs.unlinkSync(eventsPath);
|
|
178
|
+
}
|
|
179
|
+
const newStore = new HardkasStore({ dbPath });
|
|
180
|
+
newStore.connect({ autoMigrate: true });
|
|
181
|
+
const newBackend = new SqliteQueryBackend(newStore);
|
|
182
|
+
const rebuildResult = await newBackend.rebuild({ strict: true, cwd: sandboxDir });
|
|
183
|
+
if (!rebuildResult.ok) {
|
|
184
|
+
throw new Error(`Rebuild failed: ${rebuildResult.errors.join(", ")}`);
|
|
185
|
+
}
|
|
186
|
+
const indexedAfter = await newBackend.findArtifacts();
|
|
187
|
+
newStore.disconnect();
|
|
188
|
+
if (indexedAfter.length !== artifactCount) {
|
|
189
|
+
throw new Error(`State authority failure. Rebuilt database has ${indexedAfter.length} artifacts, expected ${artifactCount}`);
|
|
190
|
+
}
|
|
191
|
+
for (const artId of generatedIds) {
|
|
192
|
+
const found = indexedAfter.find((a) => a.artifactId === artId);
|
|
193
|
+
if (!found) {
|
|
194
|
+
throw new Error(`State authority failure. Missing reconstructed artifact: ${artId}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
flow: "Synchronize and reconstruct query store",
|
|
199
|
+
mutation: "Delete SQLite store.db and verify rebuild",
|
|
200
|
+
expectedInvariant: "sqlite_is_not_authority",
|
|
201
|
+
artifactsBefore: generatedIds
|
|
202
|
+
};
|
|
203
|
+
} finally {
|
|
204
|
+
cleanupSandbox(sandboxDir);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
registerTortureBucket({
|
|
209
|
+
name: "artifact-corruption",
|
|
210
|
+
expectedInvariant: "corrupted_artifacts_must_fail",
|
|
211
|
+
async run(ctx) {
|
|
212
|
+
const { sandboxDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
213
|
+
try {
|
|
214
|
+
const payload = {
|
|
215
|
+
schema: "hardkas.txPlan",
|
|
216
|
+
hardkasVersion: "0.7.0-alpha",
|
|
217
|
+
version: ARTIFACT_VERSION,
|
|
218
|
+
networkId: "simnet",
|
|
219
|
+
mode: "simulated",
|
|
220
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
221
|
+
planId: `plan-mock-${ctx.caseId}`,
|
|
222
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
223
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
224
|
+
amountSompi: "5000",
|
|
225
|
+
estimatedFeeSompi: "100",
|
|
226
|
+
estimatedMass: "10",
|
|
227
|
+
inputs: [
|
|
228
|
+
{
|
|
229
|
+
outpoint: { transactionId: "a".repeat(64), index: 0 },
|
|
230
|
+
amountSompi: "10000"
|
|
231
|
+
}
|
|
232
|
+
],
|
|
233
|
+
outputs: [
|
|
234
|
+
{
|
|
235
|
+
address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg",
|
|
236
|
+
amountSompi: "5000"
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
};
|
|
240
|
+
const validHash = calculateContentHash(payload);
|
|
241
|
+
const artifactId = `plan-${validHash.slice(0, 16)}`;
|
|
242
|
+
const validArtifact = {
|
|
243
|
+
...payload,
|
|
244
|
+
artifactId,
|
|
245
|
+
contentHash: validHash
|
|
246
|
+
};
|
|
247
|
+
const file = path.join(artifactsDir, `${artifactId}.json`);
|
|
248
|
+
fs.writeFileSync(file, JSON.stringify(validArtifact, null, 2), "utf-8");
|
|
249
|
+
const store = new HardkasStore({ dbPath });
|
|
250
|
+
store.connect({ autoMigrate: true });
|
|
251
|
+
const backend = new SqliteQueryBackend(store);
|
|
252
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
253
|
+
const indexed = await backend.findArtifacts();
|
|
254
|
+
if (indexed.length !== 1) {
|
|
255
|
+
throw new Error("Failed to index initial valid artifact");
|
|
256
|
+
}
|
|
257
|
+
const corruptedArtifact = {
|
|
258
|
+
...validArtifact,
|
|
259
|
+
amountSompi: "9999999999"
|
|
260
|
+
// altered content, causing hash mismatch!
|
|
261
|
+
};
|
|
262
|
+
fs.writeFileSync(file, JSON.stringify(corruptedArtifact, null, 2), "utf-8");
|
|
263
|
+
let threwCorrectly = false;
|
|
264
|
+
try {
|
|
265
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
266
|
+
} catch (err) {
|
|
267
|
+
threwCorrectly = true;
|
|
268
|
+
}
|
|
269
|
+
store.disconnect();
|
|
270
|
+
if (!threwCorrectly) {
|
|
271
|
+
throw new Error("Invariant violated: corrupted artifact synchronized successfully without error under strict mode");
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
flow: "Verify tampered artifact integrity",
|
|
275
|
+
mutation: "Alter artifact content Sompi amount on disk leaving original contentHash",
|
|
276
|
+
expectedInvariant: "corrupted_artifacts_must_fail",
|
|
277
|
+
artifactsBefore: [artifactId]
|
|
278
|
+
};
|
|
279
|
+
} finally {
|
|
280
|
+
cleanupSandbox(sandboxDir);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
registerTortureBucket({
|
|
285
|
+
name: "deterministic-repeatability",
|
|
286
|
+
expectedInvariant: "same_input_same_seed_equals_same_hash",
|
|
287
|
+
async run(ctx) {
|
|
288
|
+
const prngA = new LcgPrng(ctx.caseSeed);
|
|
289
|
+
const amountSompiA = (1000n * BigInt(prngA.nextInt(1, 100))).toString();
|
|
290
|
+
const nonceA = prngA.nextInt(1e4, 99999);
|
|
291
|
+
const orderA = prngA.shuffle(["alice", "bob", "carol", "dave"]);
|
|
292
|
+
const payloadA = {
|
|
293
|
+
schema: "hardkas.txPlan",
|
|
294
|
+
version: "0.7.0-alpha",
|
|
295
|
+
networkId: "simnet",
|
|
296
|
+
mode: "simulated",
|
|
297
|
+
amountSompi: amountSompiA,
|
|
298
|
+
nonce: nonceA,
|
|
299
|
+
signers: orderA
|
|
300
|
+
};
|
|
301
|
+
const hashA = calculateContentHash(payloadA);
|
|
302
|
+
const prngB = new LcgPrng(ctx.caseSeed);
|
|
303
|
+
const amountSompiB = (1000n * BigInt(prngB.nextInt(1, 100))).toString();
|
|
304
|
+
const nonceB = prngB.nextInt(1e4, 99999);
|
|
305
|
+
const orderB = prngB.shuffle(["alice", "bob", "carol", "dave"]);
|
|
306
|
+
const payloadB = {
|
|
307
|
+
schema: "hardkas.txPlan",
|
|
308
|
+
version: "0.7.0-alpha",
|
|
309
|
+
networkId: "simnet",
|
|
310
|
+
mode: "simulated",
|
|
311
|
+
amountSompi: amountSompiB,
|
|
312
|
+
nonce: nonceB,
|
|
313
|
+
signers: orderB
|
|
314
|
+
};
|
|
315
|
+
const hashB = calculateContentHash(payloadB);
|
|
316
|
+
if (hashA !== hashB) {
|
|
317
|
+
throw new Error(`Divergence detected under same seed ${ctx.caseSeed}. Hash A: ${hashA}, Hash B: ${hashB}`);
|
|
318
|
+
}
|
|
319
|
+
if (JSON.stringify(payloadA) !== JSON.stringify(payloadB)) {
|
|
320
|
+
throw new Error("Semantic payloads differ despite identical seed execution");
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
flow: "Run parallel deterministic generation pipelines",
|
|
324
|
+
mutation: "Execute generator with identical seed A and seed B",
|
|
325
|
+
expectedInvariant: "same_input_same_seed_equals_same_hash",
|
|
326
|
+
artifactsBefore: [hashA]
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
registerTortureBucket({
|
|
331
|
+
name: "projection-staleness",
|
|
332
|
+
expectedInvariant: "projection_cache_matches_artifacts",
|
|
333
|
+
async run(ctx) {
|
|
334
|
+
const { sandboxDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
335
|
+
try {
|
|
336
|
+
const payloadV1 = {
|
|
337
|
+
schema: "hardkas.txPlan",
|
|
338
|
+
hardkasVersion: "0.7.0-alpha",
|
|
339
|
+
version: ARTIFACT_VERSION,
|
|
340
|
+
networkId: "simnet",
|
|
341
|
+
mode: "simulated",
|
|
342
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
343
|
+
planId: `plan-mock-${ctx.caseId}`,
|
|
344
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
345
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
346
|
+
amountSompi: "1000",
|
|
347
|
+
estimatedFeeSompi: "100",
|
|
348
|
+
estimatedMass: "10",
|
|
349
|
+
inputs: [
|
|
350
|
+
{
|
|
351
|
+
outpoint: { transactionId: "a".repeat(64), index: 0 },
|
|
352
|
+
amountSompi: "2000"
|
|
353
|
+
}
|
|
354
|
+
],
|
|
355
|
+
outputs: [
|
|
356
|
+
{
|
|
357
|
+
address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg",
|
|
358
|
+
amountSompi: "1000"
|
|
359
|
+
}
|
|
360
|
+
],
|
|
361
|
+
state: "initial"
|
|
362
|
+
// Custom field for test assertion
|
|
363
|
+
};
|
|
364
|
+
const hashV1 = calculateContentHash(payloadV1);
|
|
365
|
+
const artifactId = `plan-${hashV1.slice(0, 16)}`;
|
|
366
|
+
const file = path.join(artifactsDir, `${artifactId}.json`);
|
|
367
|
+
const artifactV1 = {
|
|
368
|
+
...payloadV1,
|
|
369
|
+
artifactId,
|
|
370
|
+
contentHash: hashV1
|
|
371
|
+
};
|
|
372
|
+
fs.writeFileSync(file, JSON.stringify(artifactV1, null, 2), "utf-8");
|
|
373
|
+
const originalTime = Date.now() - 1e4;
|
|
374
|
+
fs.utimesSync(file, new Date(originalTime), new Date(originalTime));
|
|
375
|
+
const store = new HardkasStore({ dbPath });
|
|
376
|
+
store.connect({ autoMigrate: true });
|
|
377
|
+
const backend = new SqliteQueryBackend(store);
|
|
378
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
379
|
+
const artBefore = await backend.getArtifact(artifactId);
|
|
380
|
+
if (!artBefore || artBefore.payload.state !== "initial") {
|
|
381
|
+
throw new Error("Failed to index initial version of the artifact");
|
|
382
|
+
}
|
|
383
|
+
const payloadV2 = {
|
|
384
|
+
...payloadV1,
|
|
385
|
+
state: "progressed",
|
|
386
|
+
nonce: ctx.prng.nextInt(100, 999)
|
|
387
|
+
};
|
|
388
|
+
const hashV2 = calculateContentHash(payloadV2);
|
|
389
|
+
const artifactV2 = {
|
|
390
|
+
...payloadV2,
|
|
391
|
+
artifactId,
|
|
392
|
+
// same ID, updated content/hash
|
|
393
|
+
contentHash: hashV2
|
|
394
|
+
};
|
|
395
|
+
fs.writeFileSync(file, JSON.stringify(artifactV2, null, 2), "utf-8");
|
|
396
|
+
const updatedTime = Date.now();
|
|
397
|
+
fs.utimesSync(file, new Date(updatedTime), new Date(updatedTime));
|
|
398
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
399
|
+
const artAfter = await backend.getArtifact(artifactId);
|
|
400
|
+
store.disconnect();
|
|
401
|
+
if (!artAfter) {
|
|
402
|
+
throw new Error("Artifact deleted unexpectedly after update sync");
|
|
403
|
+
}
|
|
404
|
+
if (artAfter.contentHash !== hashV2 || artAfter.payload.state !== "progressed") {
|
|
405
|
+
throw new Error(`Staleness invariant failure. Expected hash ${hashV2} and state 'progressed', got hash ${artAfter.contentHash} and state '${artAfter.payload.state}'`);
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
flow: "Verify incremental update detection",
|
|
409
|
+
mutation: "Modify file payload state and update file mtime on disk",
|
|
410
|
+
expectedInvariant: "projection_cache_matches_artifacts",
|
|
411
|
+
artifactsBefore: [artifactId]
|
|
412
|
+
};
|
|
413
|
+
} finally {
|
|
414
|
+
cleanupSandbox(sandboxDir);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
registerTortureBucket({
|
|
419
|
+
name: "workflow-crash-resume",
|
|
420
|
+
expectedInvariant: "workflow_crash_resume_integrity",
|
|
421
|
+
async run(ctx) {
|
|
422
|
+
const { sandboxDir, hardkasDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
423
|
+
try {
|
|
424
|
+
const { createTestHarness: createTestHarness2 } = await import("./index.js");
|
|
425
|
+
const harness = createTestHarness2({ accounts: 3, initialBalance: 100000000000n });
|
|
426
|
+
const names = harness.accountNames();
|
|
427
|
+
const tx = harness.send({
|
|
428
|
+
from: names[0],
|
|
429
|
+
to: names[1],
|
|
430
|
+
amountSompi: 5000000000n
|
|
431
|
+
});
|
|
432
|
+
if (!tx.ok || !tx.plan) {
|
|
433
|
+
throw new Error("Harness failed to build payment transaction plan");
|
|
434
|
+
}
|
|
435
|
+
const planArtifact = {
|
|
436
|
+
schema: "hardkas.txPlan",
|
|
437
|
+
hardkasVersion: "0.7.0-alpha",
|
|
438
|
+
version: ARTIFACT_VERSION,
|
|
439
|
+
networkId: "simnet",
|
|
440
|
+
mode: "simulated",
|
|
441
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
442
|
+
planId: `plan-${tx.plan.estimatedMass}-${ctx.caseSeed}`,
|
|
443
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
444
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
445
|
+
amountSompi: tx.plan.outputs[0].amountSompi.toString(),
|
|
446
|
+
estimatedFeeSompi: tx.plan.estimatedFeeSompi.toString(),
|
|
447
|
+
estimatedMass: tx.plan.estimatedMass.toString(),
|
|
448
|
+
inputs: tx.plan.inputs.map((inp) => ({
|
|
449
|
+
outpoint: {
|
|
450
|
+
transactionId: inp.outpoint.transactionId,
|
|
451
|
+
index: inp.outpoint.index
|
|
452
|
+
},
|
|
453
|
+
amountSompi: inp.amountSompi.toString()
|
|
454
|
+
})),
|
|
455
|
+
outputs: tx.plan.outputs.map((out) => ({
|
|
456
|
+
address: out.address,
|
|
457
|
+
amountSompi: out.amountSompi.toString()
|
|
458
|
+
}))
|
|
459
|
+
};
|
|
460
|
+
const planHash = calculateContentHash(planArtifact);
|
|
461
|
+
const planId = `plan-${planHash.slice(0, 16)}`;
|
|
462
|
+
const conformingPlan = {
|
|
463
|
+
...planArtifact,
|
|
464
|
+
artifactId: planId,
|
|
465
|
+
contentHash: planHash
|
|
466
|
+
};
|
|
467
|
+
const planFile = path.join(artifactsDir, `${planId}.json`);
|
|
468
|
+
fs.writeFileSync(planFile, JSON.stringify(conformingPlan, null, 2), "utf-8");
|
|
469
|
+
const store = new HardkasStore({ dbPath });
|
|
470
|
+
store.connect({ autoMigrate: true });
|
|
471
|
+
const backend = new SqliteQueryBackend(store);
|
|
472
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
473
|
+
const indexedPlan = await backend.getArtifact(planId);
|
|
474
|
+
if (!indexedPlan) {
|
|
475
|
+
throw new Error("Failed to index plan prior to simulated system crash");
|
|
476
|
+
}
|
|
477
|
+
store.disconnect();
|
|
478
|
+
if (fs.existsSync(dbPath)) {
|
|
479
|
+
fs.unlinkSync(dbPath);
|
|
480
|
+
}
|
|
481
|
+
const newStore = new HardkasStore({ dbPath });
|
|
482
|
+
newStore.connect({ autoMigrate: true });
|
|
483
|
+
const newBackend = new SqliteQueryBackend(newStore);
|
|
484
|
+
const rebuildResult = await newBackend.rebuild({ strict: true, cwd: sandboxDir });
|
|
485
|
+
if (!rebuildResult.ok) {
|
|
486
|
+
throw new Error("Query store failed to rebuild state projections after crash");
|
|
487
|
+
}
|
|
488
|
+
const recoveredPlan = await newBackend.getArtifact(planId);
|
|
489
|
+
if (!recoveredPlan) {
|
|
490
|
+
throw new Error("Crash recovery failed: plan artifact was not reconstructed");
|
|
491
|
+
}
|
|
492
|
+
const receiptArtifact = {
|
|
493
|
+
schema: "hardkas.txReceipt",
|
|
494
|
+
hardkasVersion: "0.7.0-alpha",
|
|
495
|
+
version: ARTIFACT_VERSION,
|
|
496
|
+
networkId: "simnet",
|
|
497
|
+
mode: "simulated",
|
|
498
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
499
|
+
txId: tx.receipt.txId,
|
|
500
|
+
status: "confirmed",
|
|
501
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
502
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
503
|
+
amountSompi: tx.plan.outputs[0].amountSompi.toString(),
|
|
504
|
+
feeSompi: tx.plan.estimatedFeeSompi.toString(),
|
|
505
|
+
mass: tx.plan.estimatedMass.toString(),
|
|
506
|
+
lineage: {
|
|
507
|
+
artifactId: `receipt-${tx.receipt.txId.slice(0, 16)}`,
|
|
508
|
+
lineageId: "l-" + ctx.caseSeed,
|
|
509
|
+
parentArtifactId: planId,
|
|
510
|
+
// linked to the parent plan!
|
|
511
|
+
rootArtifactId: planId
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
const receiptHash = calculateContentHash(receiptArtifact);
|
|
515
|
+
const receiptId = `receipt-${receiptHash.slice(0, 16)}`;
|
|
516
|
+
const conformingReceipt = {
|
|
517
|
+
...receiptArtifact,
|
|
518
|
+
artifactId: receiptId,
|
|
519
|
+
contentHash: receiptHash
|
|
520
|
+
};
|
|
521
|
+
const receiptFile = path.join(artifactsDir, `${receiptId}.json`);
|
|
522
|
+
fs.writeFileSync(receiptFile, JSON.stringify(conformingReceipt, null, 2), "utf-8");
|
|
523
|
+
await newBackend.sync({ strict: true, cwd: sandboxDir });
|
|
524
|
+
const finalReceipt = await newBackend.getArtifact(receiptId);
|
|
525
|
+
newStore.disconnect();
|
|
526
|
+
if (!finalReceipt) {
|
|
527
|
+
throw new Error("Failed to index final transaction receipt after resuming workflow");
|
|
528
|
+
}
|
|
529
|
+
if (finalReceipt.payload.lineage.parentArtifactId !== planId) {
|
|
530
|
+
throw new Error("Lineage continuity violated: resumed receipt parentId does not match pre-crash planId");
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
flow: "Recover from intermediate workflow crash and complete pipeline",
|
|
534
|
+
mutation: "Wipe sqlite database, rebuild from raw plan, then append signed receipt",
|
|
535
|
+
expectedInvariant: "workflow_crash_resume_integrity",
|
|
536
|
+
artifactsBefore: [planId],
|
|
537
|
+
artifactsAfter: [receiptId]
|
|
538
|
+
};
|
|
539
|
+
} finally {
|
|
540
|
+
cleanupSandbox(sandboxDir);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
registerTortureBucket({
|
|
545
|
+
name: "partial-artifact-commit",
|
|
546
|
+
expectedInvariant: "no_partial_artifact_becomes_canonical",
|
|
547
|
+
async run(ctx) {
|
|
548
|
+
const { sandboxDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
549
|
+
const subCase = ctx.prng.nextInt(1, 12);
|
|
550
|
+
try {
|
|
551
|
+
const store = new HardkasStore({ dbPath });
|
|
552
|
+
store.connect({ autoMigrate: true });
|
|
553
|
+
const backend = new SqliteQueryBackend(store);
|
|
554
|
+
const planId = `plan-mock-${ctx.caseId}-${ctx.caseSeed}`;
|
|
555
|
+
const payload = {
|
|
556
|
+
schema: "hardkas.txPlan",
|
|
557
|
+
hardkasVersion: "0.7.0-alpha",
|
|
558
|
+
version: ARTIFACT_VERSION,
|
|
559
|
+
networkId: "simnet",
|
|
560
|
+
mode: "simulated",
|
|
561
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
562
|
+
// hardkas-determinism-allow: mock creation timestamp
|
|
563
|
+
planId,
|
|
564
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
565
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
566
|
+
amountSompi: "1000",
|
|
567
|
+
estimatedFeeSompi: "100",
|
|
568
|
+
estimatedMass: "10",
|
|
569
|
+
inputs: [{ outpoint: { transactionId: "a".repeat(64), index: 0 }, amountSompi: "2000" }],
|
|
570
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "1000" }]
|
|
571
|
+
};
|
|
572
|
+
const validHash = calculateContentHash(payload);
|
|
573
|
+
const artifactId = `plan-${validHash.slice(0, 16)}`;
|
|
574
|
+
const file = path.join(artifactsDir, `${artifactId}.json`);
|
|
575
|
+
const validArtifact = { ...payload, artifactId, contentHash: validHash };
|
|
576
|
+
fs.writeFileSync(file, JSON.stringify(validArtifact, null, 2));
|
|
577
|
+
const badFile = path.join(artifactsDir, `bad-artifact-${subCase}.json`);
|
|
578
|
+
let shouldSyncThrow = false;
|
|
579
|
+
let checkIgnored = false;
|
|
580
|
+
switch (subCase) {
|
|
581
|
+
case 1:
|
|
582
|
+
fs.writeFileSync(badFile, `{"schema": "hardkas.txPlan", "planId": "${planId}"`, "utf-8");
|
|
583
|
+
shouldSyncThrow = true;
|
|
584
|
+
break;
|
|
585
|
+
case 2:
|
|
586
|
+
fs.writeFileSync(badFile, Buffer.from([123, 34, 195, 40, 125]));
|
|
587
|
+
shouldSyncThrow = true;
|
|
588
|
+
break;
|
|
589
|
+
case 3: {
|
|
590
|
+
const tempFile = path.join(artifactsDir, `.tmp-plan-${subCase}.json`);
|
|
591
|
+
fs.writeFileSync(tempFile, JSON.stringify(validArtifact, null, 2));
|
|
592
|
+
checkIgnored = true;
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
case 4: {
|
|
596
|
+
const hashMismatchArt = { ...validArtifact, amountSompi: "9999", artifactId: "plan-mismatch" };
|
|
597
|
+
fs.writeFileSync(badFile, JSON.stringify(hashMismatchArt, null, 2));
|
|
598
|
+
shouldSyncThrow = true;
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
case 5:
|
|
602
|
+
fs.writeFileSync(badFile, JSON.stringify({ schema: "hardkas.txPlan", version: ARTIFACT_VERSION, contentHash: validHash, artifactId: "plan-meta-only" }, null, 2));
|
|
603
|
+
shouldSyncThrow = true;
|
|
604
|
+
break;
|
|
605
|
+
case 6:
|
|
606
|
+
fs.writeFileSync(badFile, JSON.stringify(payload, null, 2));
|
|
607
|
+
shouldSyncThrow = true;
|
|
608
|
+
break;
|
|
609
|
+
case 7: {
|
|
610
|
+
const missingHashArt = { ...validArtifact };
|
|
611
|
+
delete missingHashArt.contentHash;
|
|
612
|
+
fs.writeFileSync(badFile, JSON.stringify(missingHashArt, null, 2));
|
|
613
|
+
shouldSyncThrow = true;
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
case 8: {
|
|
617
|
+
const orphanReceipt = {
|
|
618
|
+
schema: "hardkas.txReceipt",
|
|
619
|
+
hardkasVersion: "0.7.0-alpha",
|
|
620
|
+
version: ARTIFACT_VERSION,
|
|
621
|
+
networkId: "simnet",
|
|
622
|
+
mode: "simulated",
|
|
623
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
624
|
+
// hardkas-determinism-allow: mock timestamp
|
|
625
|
+
txId: "b".repeat(64),
|
|
626
|
+
status: "confirmed",
|
|
627
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
628
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
629
|
+
amountSompi: "1000",
|
|
630
|
+
feeSompi: "100",
|
|
631
|
+
lineage: {
|
|
632
|
+
artifactId: "receipt-orphan",
|
|
633
|
+
lineageId: "lineage-orphan",
|
|
634
|
+
parentArtifactId: "non-existent-parent-id",
|
|
635
|
+
rootArtifactId: "non-existent-parent-id"
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
const receiptHash = calculateContentHash(orphanReceipt);
|
|
639
|
+
const receiptId = `receipt-${receiptHash.slice(0, 16)}`;
|
|
640
|
+
fs.writeFileSync(path.join(artifactsDir, `${receiptId}.json`), JSON.stringify({ ...orphanReceipt, artifactId: receiptId, contentHash: receiptHash }, null, 2));
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
case 9:
|
|
644
|
+
break;
|
|
645
|
+
case 10: {
|
|
646
|
+
const dupFile = path.join(artifactsDir, `dup-artifact-${subCase}.json`);
|
|
647
|
+
const dupArt = { ...validArtifact, amountSompi: "500" };
|
|
648
|
+
fs.writeFileSync(dupFile, JSON.stringify(dupArt, null, 2));
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
case 11: {
|
|
652
|
+
const crashFile = path.join(artifactsDir, `.tmp-crash-${subCase}`);
|
|
653
|
+
fs.writeFileSync(crashFile, JSON.stringify(validArtifact, null, 2));
|
|
654
|
+
checkIgnored = true;
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
case 12:
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
let threw = false;
|
|
661
|
+
try {
|
|
662
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
663
|
+
} catch (err) {
|
|
664
|
+
threw = true;
|
|
665
|
+
}
|
|
666
|
+
if (shouldSyncThrow && !threw) {
|
|
667
|
+
throw new TortureInvariantError(
|
|
668
|
+
`Sync with strict: true did not throw on partial/corrupted file for case ${subCase}`,
|
|
669
|
+
"PARTIAL_ARTIFACT_NOT_REJECTED",
|
|
670
|
+
"critical"
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
const records = await backend.findArtifacts();
|
|
674
|
+
if (checkIgnored) {
|
|
675
|
+
const badFound = records.some((r) => r.artifactId.includes(`bad-artifact-${subCase}`) || r.artifactId.includes(`.tmp-`));
|
|
676
|
+
if (badFound) {
|
|
677
|
+
throw new TortureInvariantError(
|
|
678
|
+
`Temp or ignored file was indexed into the query-store under case ${subCase}`,
|
|
679
|
+
"TEMP_FILE_INDEXED",
|
|
680
|
+
"critical"
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (subCase === 8 || subCase === 9) {
|
|
685
|
+
const edges = await backend.getLineageEdges();
|
|
686
|
+
const hasBadEdge = edges.some((e) => e.parentArtifactId === "non-existent-parent-id");
|
|
687
|
+
if (hasBadEdge) {
|
|
688
|
+
throw new TortureInvariantError(
|
|
689
|
+
"Orphan child created a valid lineage edge to a missing parent",
|
|
690
|
+
"ORPHAN_LINEAGE_CREATED",
|
|
691
|
+
"critical"
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (subCase === 10) {
|
|
696
|
+
const matched = records.filter((r) => r.artifactId === artifactId);
|
|
697
|
+
if (matched.length > 1) {
|
|
698
|
+
throw new TortureInvariantError(
|
|
699
|
+
"Duplicate artifact ID produced multiple canonical query-store records",
|
|
700
|
+
"DUPLICATE_CANONICAL_RECORDS",
|
|
701
|
+
"critical"
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
store.disconnect();
|
|
706
|
+
return {
|
|
707
|
+
flow: `Verify partial commit under subcase ${subCase}`,
|
|
708
|
+
mutation: `Write partial/corrupted/temp file under subcase type ${subCase}`,
|
|
709
|
+
expectedInvariant: "no_partial_artifact_becomes_canonical",
|
|
710
|
+
artifactsBefore: [artifactId]
|
|
711
|
+
};
|
|
712
|
+
} finally {
|
|
713
|
+
cleanupSandbox(sandboxDir);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
registerTortureBucket({
|
|
718
|
+
name: "concurrent-workflow-race",
|
|
719
|
+
expectedInvariant: "single_canonical_lineage_under_concurrent_writes",
|
|
720
|
+
async run(ctx) {
|
|
721
|
+
const { sandboxDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
722
|
+
try {
|
|
723
|
+
const store = new HardkasStore({ dbPath });
|
|
724
|
+
store.connect({ autoMigrate: true });
|
|
725
|
+
const backend = new SqliteQueryBackend(store);
|
|
726
|
+
const promises = [];
|
|
727
|
+
const generatedIds = [];
|
|
728
|
+
for (let i = 0; i < 5; i++) {
|
|
729
|
+
const pId = `plan-race-${i}-${ctx.caseSeed}`;
|
|
730
|
+
const payload = {
|
|
731
|
+
schema: "hardkas.txPlan",
|
|
732
|
+
hardkasVersion: "0.7.0-alpha",
|
|
733
|
+
version: ARTIFACT_VERSION,
|
|
734
|
+
networkId: "simnet",
|
|
735
|
+
mode: "simulated",
|
|
736
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
737
|
+
// hardkas-determinism-allow: mock timestamp
|
|
738
|
+
planId: pId,
|
|
739
|
+
from: { address: `kaspa:sim_from_${i}` },
|
|
740
|
+
to: { address: `kaspa:sim_to_${i}` },
|
|
741
|
+
amountSompi: "5000",
|
|
742
|
+
estimatedFeeSompi: "100",
|
|
743
|
+
estimatedMass: "10",
|
|
744
|
+
inputs: [{ outpoint: { transactionId: "c".repeat(64), index: i }, amountSompi: "10000" }],
|
|
745
|
+
outputs: [{ address: `kaspa:sim_to_${i}`, amountSompi: "5000" }]
|
|
746
|
+
};
|
|
747
|
+
const hash = calculateContentHash(payload);
|
|
748
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
749
|
+
generatedIds.push(artId);
|
|
750
|
+
promises.push((async () => {
|
|
751
|
+
const file = path.join(artifactsDir, `${artId}.json`);
|
|
752
|
+
fs.writeFileSync(file, JSON.stringify({ ...payload, artifactId: artId, contentHash: hash }, null, 2));
|
|
753
|
+
try {
|
|
754
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
755
|
+
} catch (err) {
|
|
756
|
+
}
|
|
757
|
+
})());
|
|
758
|
+
}
|
|
759
|
+
await Promise.all(promises);
|
|
760
|
+
const syncRes = await backend.sync({ strict: true, cwd: sandboxDir });
|
|
761
|
+
const rebuildRes = await backend.rebuild({ strict: true, cwd: sandboxDir });
|
|
762
|
+
if (!syncRes.ok || !rebuildRes.ok) {
|
|
763
|
+
throw new TortureInvariantError(
|
|
764
|
+
"Concurrent sync or rebuild failed to complete successfully",
|
|
765
|
+
"CONCURRENT_SYNC_FAILED",
|
|
766
|
+
"critical"
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
const records = await backend.findArtifacts();
|
|
770
|
+
const ids = records.map((r) => r.artifactId);
|
|
771
|
+
const uniqueIds = new Set(ids);
|
|
772
|
+
if (ids.length !== uniqueIds.size) {
|
|
773
|
+
throw new TortureInvariantError(
|
|
774
|
+
"Duplicate canonical IDs produced during concurrent writes",
|
|
775
|
+
"DUPLICATE_CANONICAL_IDS",
|
|
776
|
+
"critical"
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
store.disconnect();
|
|
780
|
+
return {
|
|
781
|
+
flow: "Trigger concurrent writes and intermediate query-store syncs",
|
|
782
|
+
mutation: "Write multiple artifacts in async parallel tasks",
|
|
783
|
+
expectedInvariant: "single_canonical_lineage_under_concurrent_writes",
|
|
784
|
+
artifactsBefore: generatedIds
|
|
785
|
+
};
|
|
786
|
+
} finally {
|
|
787
|
+
cleanupSandbox(sandboxDir);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
registerTortureBucket({
|
|
792
|
+
name: "dashboard-stale-truth",
|
|
793
|
+
expectedInvariant: "dashboard_never_claims_verified_from_stale_projection",
|
|
794
|
+
async run(ctx) {
|
|
795
|
+
const { sandboxDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
796
|
+
try {
|
|
797
|
+
const store = new HardkasStore({ dbPath });
|
|
798
|
+
store.connect({ autoMigrate: true });
|
|
799
|
+
const backend = new SqliteQueryBackend(store);
|
|
800
|
+
const planId = `plan-stale-${ctx.caseSeed}`;
|
|
801
|
+
const payload = {
|
|
802
|
+
schema: "hardkas.txPlan",
|
|
803
|
+
hardkasVersion: "0.7.0-alpha",
|
|
804
|
+
version: ARTIFACT_VERSION,
|
|
805
|
+
networkId: "simnet",
|
|
806
|
+
mode: "simulated",
|
|
807
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
808
|
+
// hardkas-determinism-allow: mock timestamp
|
|
809
|
+
planId,
|
|
810
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
811
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
812
|
+
amountSompi: "1000",
|
|
813
|
+
estimatedFeeSompi: "100",
|
|
814
|
+
estimatedMass: "10",
|
|
815
|
+
inputs: [{ outpoint: { transactionId: "d".repeat(64), index: 0 }, amountSompi: "2000" }],
|
|
816
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "1000" }]
|
|
817
|
+
};
|
|
818
|
+
const hash = calculateContentHash(payload);
|
|
819
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
820
|
+
const file = path.join(artifactsDir, `${artId}.json`);
|
|
821
|
+
fs.writeFileSync(file, JSON.stringify({ ...payload, artifactId: artId, contentHash: hash }, null, 2));
|
|
822
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
823
|
+
const statusBefore = await backend.doctor();
|
|
824
|
+
if (!statusBefore.ok) {
|
|
825
|
+
throw new Error("Doctor check failed on initial fresh index");
|
|
826
|
+
}
|
|
827
|
+
const mutatedPayload = { ...payload, amountSompi: "9999" };
|
|
828
|
+
fs.writeFileSync(file, JSON.stringify({ ...mutatedPayload, artifactId: artId, contentHash: hash }, null, 2));
|
|
829
|
+
const futureTime = Date.now() + 5e4;
|
|
830
|
+
fs.utimesSync(file, new Date(futureTime), new Date(futureTime));
|
|
831
|
+
const statusAfter = await backend.doctor();
|
|
832
|
+
store.disconnect();
|
|
833
|
+
if (statusAfter.ok || statusAfter.staleArtifacts === 0) {
|
|
834
|
+
throw new TortureInvariantError(
|
|
835
|
+
"Stale projection was reported as VERIFIED / OK by query backend doctor",
|
|
836
|
+
"STALE_PROJECTION_NOT_VERIFIED",
|
|
837
|
+
"critical"
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
flow: "Mutate disk artifacts and inspect query-store status",
|
|
842
|
+
mutation: "Modify file on disk without triggering incremental query-store sync",
|
|
843
|
+
expectedInvariant: "dashboard_never_claims_verified_from_stale_projection",
|
|
844
|
+
artifactsBefore: [artId]
|
|
845
|
+
};
|
|
846
|
+
} finally {
|
|
847
|
+
cleanupSandbox(sandboxDir);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
registerTortureBucket({
|
|
852
|
+
name: "runtime-impurity-detection",
|
|
853
|
+
expectedInvariant: "deterministic_runtime_rejects_ambient_impurity",
|
|
854
|
+
async run(ctx) {
|
|
855
|
+
const rootDir = ctx.workspaceDir;
|
|
856
|
+
const targetDirs = [
|
|
857
|
+
"packages/sdk/src",
|
|
858
|
+
"packages/cli/src",
|
|
859
|
+
"packages/artifacts/src",
|
|
860
|
+
"packages/query-store/src",
|
|
861
|
+
"packages/localnet/src"
|
|
862
|
+
];
|
|
863
|
+
const forbiddenPatterns = [
|
|
864
|
+
{ regex: /Math\.random\(/, name: "Math.random()" },
|
|
865
|
+
{ regex: /Date\.now\(/, name: "Date.now()" },
|
|
866
|
+
{ regex: /new Date\(/, name: "new Date()" },
|
|
867
|
+
{ regex: /randomUUID\(/, name: "randomUUID()" },
|
|
868
|
+
{ regex: /crypto\.randomUUID\(/, name: "crypto.randomUUID()" },
|
|
869
|
+
{ regex: /readdirSync\(/, name: "readdirSync()" },
|
|
870
|
+
{ regex: /process\.cwd\(/, name: "process.cwd()" },
|
|
871
|
+
{ regex: /process\.env\b/, name: "process.env" },
|
|
872
|
+
{ regex: /os\.tmpdir\(/, name: "os.tmpdir()" }
|
|
873
|
+
];
|
|
874
|
+
const findings = [];
|
|
875
|
+
const walkAndScan = (dir) => {
|
|
876
|
+
const absDir = path.join(rootDir, dir);
|
|
877
|
+
if (!fs.existsSync(absDir)) return;
|
|
878
|
+
const list = fs.readdirSync(absDir);
|
|
879
|
+
for (const item of list) {
|
|
880
|
+
const itemPath = path.join(absDir, item);
|
|
881
|
+
const relPath = path.join(dir, item);
|
|
882
|
+
const stat = fs.statSync(itemPath);
|
|
883
|
+
if (stat.isDirectory()) {
|
|
884
|
+
walkAndScan(relPath);
|
|
885
|
+
} else if (item.endsWith(".ts") || item.endsWith(".js") || item.endsWith(".mts")) {
|
|
886
|
+
const content = fs.readFileSync(itemPath, "utf-8");
|
|
887
|
+
const lines = content.split("\n");
|
|
888
|
+
for (let i = 0; i < lines.length; i++) {
|
|
889
|
+
const line = lines[i];
|
|
890
|
+
const lineNum = i + 1;
|
|
891
|
+
if (line.includes("hardkas-determinism-allow")) continue;
|
|
892
|
+
if (i > 0 && lines[i - 1].includes("hardkas-determinism-allow")) continue;
|
|
893
|
+
for (const pat of forbiddenPatterns) {
|
|
894
|
+
if (pat.regex.test(line)) {
|
|
895
|
+
if (pat.name === "readdirSync()" && line.includes(".sort(")) continue;
|
|
896
|
+
const isCoreDeterministic = relPath.includes("packages/artifacts/src/") || relPath.includes("packages/query-store/src/") || relPath.includes("packages/sdk/src/replay.ts") || relPath.includes("packages/sdk/src/workflow.ts") || relPath.includes("packages/localnet/src/");
|
|
897
|
+
if (isCoreDeterministic) {
|
|
898
|
+
findings.push(`${relPath}:${lineNum} - Found core determinism violation: ${pat.name}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
for (const dir of targetDirs) {
|
|
907
|
+
walkAndScan(dir);
|
|
908
|
+
}
|
|
909
|
+
if (findings.length > 0) {
|
|
910
|
+
throw new TortureInvariantError(
|
|
911
|
+
`Ambient non-determinism detected in deterministic runtime paths without allowlist comments:
|
|
912
|
+
${findings.join("\n")}`,
|
|
913
|
+
"DETERMINISM_VIOLATION",
|
|
914
|
+
"critical"
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
flow: "Scan monorepo source files for forbidden ambient impurities",
|
|
919
|
+
mutation: "Perform semantic scanning across core packages",
|
|
920
|
+
expectedInvariant: "deterministic_runtime_rejects_ambient_impurity",
|
|
921
|
+
artifactsBefore: []
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
registerTortureBucket({
|
|
926
|
+
name: "watcher-sse-storm",
|
|
927
|
+
expectedInvariant: "event_storm_cannot_create_canonical_divergence",
|
|
928
|
+
async run(ctx) {
|
|
929
|
+
const { sandboxDir, hardkasDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
930
|
+
try {
|
|
931
|
+
const store = new HardkasStore({ dbPath });
|
|
932
|
+
store.connect({ autoMigrate: true });
|
|
933
|
+
const backend = new SqliteQueryBackend(store);
|
|
934
|
+
const generatedIds = [];
|
|
935
|
+
const eventsFile = path.join(hardkasDir, "events.jsonl");
|
|
936
|
+
for (let i = 0; i < 10; i++) {
|
|
937
|
+
const payload = {
|
|
938
|
+
schema: "hardkas.txPlan",
|
|
939
|
+
hardkasVersion: "0.7.0-alpha",
|
|
940
|
+
version: ARTIFACT_VERSION,
|
|
941
|
+
networkId: "simnet",
|
|
942
|
+
mode: "simulated",
|
|
943
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
944
|
+
// hardkas-determinism-allow: mock timestamp
|
|
945
|
+
planId: `plan-storm-${i}-${ctx.caseSeed}`,
|
|
946
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
947
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
948
|
+
amountSompi: (1000n * BigInt(i + 1)).toString(),
|
|
949
|
+
estimatedFeeSompi: "100",
|
|
950
|
+
estimatedMass: "10",
|
|
951
|
+
inputs: [{ outpoint: { transactionId: "e".repeat(64), index: i }, amountSompi: "20000" }],
|
|
952
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "1000" }]
|
|
953
|
+
};
|
|
954
|
+
const hash = calculateContentHash(payload);
|
|
955
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
956
|
+
generatedIds.push(artId);
|
|
957
|
+
const file = path.join(artifactsDir, `${artId}.json`);
|
|
958
|
+
fs.writeFileSync(file, JSON.stringify({ ...payload, artifactId: artId, contentHash: hash }, null, 2));
|
|
959
|
+
const evEnvelope = {
|
|
960
|
+
schema: "hardkas.event",
|
|
961
|
+
eventId: `ev-storm-${i}`,
|
|
962
|
+
kind: "workflow.completed",
|
|
963
|
+
domain: "workflow",
|
|
964
|
+
workflowId: `wf-${ctx.caseSeed}`,
|
|
965
|
+
correlationId: `corr-${i}`,
|
|
966
|
+
networkId: "simnet",
|
|
967
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
968
|
+
payload: { txId: "f".repeat(64) }
|
|
969
|
+
};
|
|
970
|
+
fs.appendFileSync(eventsFile, JSON.stringify(evEnvelope) + "\n");
|
|
971
|
+
fs.appendFileSync(eventsFile, JSON.stringify(evEnvelope) + "\n");
|
|
972
|
+
}
|
|
973
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
974
|
+
if (fs.existsSync(eventsFile)) {
|
|
975
|
+
fs.unlinkSync(eventsFile);
|
|
976
|
+
}
|
|
977
|
+
const rebuildResult = await backend.rebuild({ strict: true, cwd: sandboxDir });
|
|
978
|
+
if (!rebuildResult.ok) {
|
|
979
|
+
throw new Error("Rebuild failed after event storm simulation");
|
|
980
|
+
}
|
|
981
|
+
const records = await backend.findArtifacts();
|
|
982
|
+
store.disconnect();
|
|
983
|
+
if (records.length !== generatedIds.length) {
|
|
984
|
+
throw new TortureInvariantError(
|
|
985
|
+
`Parity check failed: filesystem has ${generatedIds.length} artifacts, database has ${records.length} after event storm and rebuild`,
|
|
986
|
+
"EVENT_STORM_DIVIDED_TRUTH",
|
|
987
|
+
"critical"
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
flow: "Generate watch event storm with duplicates and dropped events",
|
|
992
|
+
mutation: "Append duplicates to event log, delete events.jsonl, then trigger rebuild",
|
|
993
|
+
expectedInvariant: "event_storm_cannot_create_canonical_divergence",
|
|
994
|
+
artifactsBefore: generatedIds
|
|
995
|
+
};
|
|
996
|
+
} finally {
|
|
997
|
+
cleanupSandbox(sandboxDir);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
registerTortureBucket({
|
|
1002
|
+
name: "workspace-filesystem-chaos",
|
|
1003
|
+
expectedInvariant: "workspace_boundaries_and_canonical_paths_are_stable",
|
|
1004
|
+
async run(ctx) {
|
|
1005
|
+
const { sandboxDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
1006
|
+
try {
|
|
1007
|
+
const store = new HardkasStore({ dbPath });
|
|
1008
|
+
store.connect({ autoMigrate: true });
|
|
1009
|
+
const backend = new SqliteQueryBackend(store);
|
|
1010
|
+
const traverseArtId = "../../traversal-escape";
|
|
1011
|
+
const payload = {
|
|
1012
|
+
schema: "hardkas.txPlan",
|
|
1013
|
+
hardkasVersion: "0.7.0-alpha",
|
|
1014
|
+
version: ARTIFACT_VERSION,
|
|
1015
|
+
networkId: "simnet",
|
|
1016
|
+
mode: "simulated",
|
|
1017
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1018
|
+
// hardkas-determinism-allow: mock timestamp
|
|
1019
|
+
planId: "plan-traversal",
|
|
1020
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
1021
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
1022
|
+
amountSompi: "1000",
|
|
1023
|
+
estimatedFeeSompi: "100",
|
|
1024
|
+
estimatedMass: "10",
|
|
1025
|
+
inputs: [{ outpoint: { transactionId: "g".repeat(64), index: 0 }, amountSompi: "2000" }],
|
|
1026
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "1000" }]
|
|
1027
|
+
};
|
|
1028
|
+
const hash = calculateContentHash(payload);
|
|
1029
|
+
const traverseFile = path.join(artifactsDir, "..", "..", `traversal-${ctx.caseSeed}.json`);
|
|
1030
|
+
fs.writeFileSync(traverseFile, JSON.stringify({ ...payload, artifactId: traverseArtId, contentHash: hash }, null, 2));
|
|
1031
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
1032
|
+
const records = await backend.findArtifacts();
|
|
1033
|
+
const escaped = records.some((r) => r.artifactId.includes("traversal"));
|
|
1034
|
+
if (escaped) {
|
|
1035
|
+
throw new TortureInvariantError(
|
|
1036
|
+
"Path traversal artifact escaped sandbox and was indexed",
|
|
1037
|
+
"PATH_TRAVERSAL_ESCAPED",
|
|
1038
|
+
"catastrophic"
|
|
1039
|
+
// Catastrophic severity for sandboxing escape!
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
const validId = `plan-move-${ctx.caseSeed}`;
|
|
1043
|
+
const validFile = path.join(artifactsDir, `${validId}.json`);
|
|
1044
|
+
fs.writeFileSync(validFile, JSON.stringify({ ...payload, artifactId: validId, contentHash: hash }, null, 2));
|
|
1045
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
1046
|
+
store.disconnect();
|
|
1047
|
+
const newSandboxDir = `${sandboxDir}-moved`;
|
|
1048
|
+
if (fs.existsSync(newSandboxDir)) {
|
|
1049
|
+
fs.rmSync(newSandboxDir, { recursive: true, force: true });
|
|
1050
|
+
}
|
|
1051
|
+
fs.renameSync(sandboxDir, newSandboxDir);
|
|
1052
|
+
const newDbPath = path.join(newSandboxDir, ".hardkas", "store.db");
|
|
1053
|
+
const newStore = new HardkasStore({ dbPath: newDbPath });
|
|
1054
|
+
newStore.connect({ autoMigrate: true });
|
|
1055
|
+
const newBackend = new SqliteQueryBackend(newStore);
|
|
1056
|
+
const rebuildResult = await newBackend.rebuild({ strict: true, cwd: newSandboxDir });
|
|
1057
|
+
if (!rebuildResult.ok) {
|
|
1058
|
+
throw new Error("Failed to rebuild after moving workspace folder");
|
|
1059
|
+
}
|
|
1060
|
+
const movedRecords = await newBackend.findArtifacts();
|
|
1061
|
+
const movedRecord = movedRecords.find((r) => r.artifactId === validId);
|
|
1062
|
+
newStore.disconnect();
|
|
1063
|
+
cleanupSandbox(newSandboxDir);
|
|
1064
|
+
if (!movedRecord || !movedRecord.path.startsWith(newSandboxDir)) {
|
|
1065
|
+
throw new TortureInvariantError(
|
|
1066
|
+
"Rebuild failed to canonicalize paths to the new moved workspace directory",
|
|
1067
|
+
"CANONICAL_PATH_NOT_STABLE",
|
|
1068
|
+
"critical"
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
return {
|
|
1072
|
+
flow: "Verify path sandboxing and directory migration canonicalization",
|
|
1073
|
+
mutation: "Perform relative path traversal writes and move sandbox directory",
|
|
1074
|
+
expectedInvariant: "workspace_boundaries_and_canonical_paths_are_stable"
|
|
1075
|
+
};
|
|
1076
|
+
} finally {
|
|
1077
|
+
cleanupSandbox(sandboxDir);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
registerTortureBucket({
|
|
1082
|
+
name: "capability-agent-denial",
|
|
1083
|
+
expectedInvariant: "agents_cannot_mutate_without_explicit_capability",
|
|
1084
|
+
async run(ctx) {
|
|
1085
|
+
const { sandboxDir } = createSandbox(ctx);
|
|
1086
|
+
try {
|
|
1087
|
+
const { Hardkas } = await import("@hardkas/sdk");
|
|
1088
|
+
const sdk = await Hardkas.open({
|
|
1089
|
+
cwd: sandboxDir,
|
|
1090
|
+
mode: "agent",
|
|
1091
|
+
policy: {
|
|
1092
|
+
requireDryRun: true,
|
|
1093
|
+
// mutation requires dryRun
|
|
1094
|
+
allowNetwork: false
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
let directWriteDenied = false;
|
|
1098
|
+
try {
|
|
1099
|
+
sdk.enforcePolicy("mutation", "Direct write in torture suite");
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
if (err.code === "POLICY_VIOLATION") {
|
|
1102
|
+
directWriteDenied = true;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
let workflowDenied = false;
|
|
1106
|
+
try {
|
|
1107
|
+
await sdk.workflow.run({
|
|
1108
|
+
steps: [
|
|
1109
|
+
{
|
|
1110
|
+
type: "tx.plan",
|
|
1111
|
+
from: "alice",
|
|
1112
|
+
to: "bob",
|
|
1113
|
+
amount: "1000"
|
|
1114
|
+
}
|
|
1115
|
+
],
|
|
1116
|
+
dryRun: false
|
|
1117
|
+
// Prohibited direct mutation!
|
|
1118
|
+
});
|
|
1119
|
+
} catch (err) {
|
|
1120
|
+
if (err.code === "POLICY_VIOLATION" || err.code === "POLICY_DENIED") {
|
|
1121
|
+
workflowDenied = true;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (!directWriteDenied && !workflowDenied) {
|
|
1125
|
+
throw new TortureInvariantError(
|
|
1126
|
+
"SDK Agent Mode permitted direct mutation or network bypass silently",
|
|
1127
|
+
"CAPABILITY_BYPASS",
|
|
1128
|
+
"critical"
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
return {
|
|
1132
|
+
flow: "Attempt unauthorized SDK mutations under restricted Agent mode",
|
|
1133
|
+
mutation: "Call direct mutation and non-dry-run workflow APIs",
|
|
1134
|
+
expectedInvariant: "agents_cannot_mutate_without_explicit_capability"
|
|
1135
|
+
};
|
|
1136
|
+
} finally {
|
|
1137
|
+
cleanupSandbox(sandboxDir);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
registerTortureBucket({
|
|
1142
|
+
name: "contract-compile-replay",
|
|
1143
|
+
expectedInvariant: "same_contract_inputs_produce_same_compile_artifact",
|
|
1144
|
+
async run(ctx) {
|
|
1145
|
+
const compileContract = (source2, config) => {
|
|
1146
|
+
const payloadToHash = {
|
|
1147
|
+
source: source2,
|
|
1148
|
+
optimizer: config.optimizer,
|
|
1149
|
+
version: config.version
|
|
1150
|
+
};
|
|
1151
|
+
const bytecodeHash = calculateContentHash(payloadToHash);
|
|
1152
|
+
const mockBytecode = `0x608060405234801561001057600080fd5b5061${bytecodeHash.slice(0, 16)}`;
|
|
1153
|
+
return {
|
|
1154
|
+
bytecode: mockBytecode,
|
|
1155
|
+
bytecodeHash,
|
|
1156
|
+
sourceHash: calculateContentHash(source2),
|
|
1157
|
+
compilerVersion: config.version,
|
|
1158
|
+
optimizer: config.optimizer
|
|
1159
|
+
};
|
|
1160
|
+
};
|
|
1161
|
+
const source = "contract HardKAS { uint256 public balance; }";
|
|
1162
|
+
const configA = { optimizer: true, version: "0.8.20" };
|
|
1163
|
+
const configB = { optimizer: true, version: "0.8.20" };
|
|
1164
|
+
const configC = { optimizer: false, version: "0.8.20" };
|
|
1165
|
+
const configD = { optimizer: true, version: "0.8.24" };
|
|
1166
|
+
const resA = compileContract(source, configA);
|
|
1167
|
+
const resB = compileContract(source, configB);
|
|
1168
|
+
const resC = compileContract(source, configC);
|
|
1169
|
+
const resD = compileContract(source, configD);
|
|
1170
|
+
if (resA.bytecodeHash !== resB.bytecodeHash || resA.bytecode !== resB.bytecode) {
|
|
1171
|
+
throw new TortureInvariantError(
|
|
1172
|
+
"Compiler drift: identical Solidity inputs yielded mismatched bytecodes",
|
|
1173
|
+
"COMPILER_DRIFT",
|
|
1174
|
+
"critical"
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
if (resA.bytecodeHash === resC.bytecodeHash) {
|
|
1178
|
+
throw new TortureInvariantError(
|
|
1179
|
+
"Drift failure: changed optimizer settings did not reflect in contract artifact identity",
|
|
1180
|
+
"OPTIMIZER_DRIFT_NOT_REFLECTED",
|
|
1181
|
+
"critical"
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
if (resA.bytecodeHash === resD.bytecodeHash) {
|
|
1185
|
+
throw new TortureInvariantError(
|
|
1186
|
+
"Drift failure: changed compiler version did not reflect in contract artifact identity",
|
|
1187
|
+
"COMPILER_VERSION_DRIFT_NOT_REFLECTED",
|
|
1188
|
+
"critical"
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
return {
|
|
1192
|
+
flow: "Execute contract mock compilation with settings and version adjustments",
|
|
1193
|
+
mutation: "Modify Solidity compiler optimizer and version parameters deterministically",
|
|
1194
|
+
expectedInvariant: "same_contract_inputs_produce_same_compile_artifact"
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
registerTortureBucket({
|
|
1199
|
+
name: "semantic-replay-from-zero",
|
|
1200
|
+
expectedInvariant: "canonical_history_survives_full_environment_reconstruction",
|
|
1201
|
+
async run(ctx) {
|
|
1202
|
+
const { sandboxDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
1203
|
+
const originalCwd = process.cwd();
|
|
1204
|
+
try {
|
|
1205
|
+
const { createTestHarness: createTestHarness2 } = await import("./index.js");
|
|
1206
|
+
fs.writeFileSync(path.join(sandboxDir, "hardkas.config.ts"), "export default {};", "utf-8");
|
|
1207
|
+
const harness = createTestHarness2({ accounts: 3, initialBalance: 100000000000n });
|
|
1208
|
+
const names = harness.accountNames();
|
|
1209
|
+
const tx = harness.send({
|
|
1210
|
+
from: names[0],
|
|
1211
|
+
to: names[1],
|
|
1212
|
+
amountSompi: 5000000000n
|
|
1213
|
+
});
|
|
1214
|
+
if (!tx.ok || !tx.plan || !tx.receipt) {
|
|
1215
|
+
throw new Error("Harness failed to build initial transaction plan or receipt");
|
|
1216
|
+
}
|
|
1217
|
+
const planId = tx.plan.planId;
|
|
1218
|
+
const conformingPlan = {
|
|
1219
|
+
...tx.plan,
|
|
1220
|
+
artifactId: planId
|
|
1221
|
+
};
|
|
1222
|
+
conformingPlan.contentHash = calculateContentHash(conformingPlan, CURRENT_HASH_VERSION);
|
|
1223
|
+
fs.writeFileSync(path.join(artifactsDir, `${planId}.json`), JSON.stringify(conformingPlan, null, 2), "utf-8");
|
|
1224
|
+
fs.writeFileSync(path.join(sandboxDir, "tx-plan.json"), JSON.stringify(conformingPlan, null, 2), "utf-8");
|
|
1225
|
+
const receiptId = `receipt-${tx.receipt.txId.slice(0, 16)}`;
|
|
1226
|
+
const conformingReceipt = {
|
|
1227
|
+
...tx.receipt,
|
|
1228
|
+
artifactId: receiptId,
|
|
1229
|
+
lineage: {
|
|
1230
|
+
artifactId: receiptId,
|
|
1231
|
+
lineageId: "l-" + ctx.caseSeed,
|
|
1232
|
+
parentArtifactId: planId,
|
|
1233
|
+
rootArtifactId: planId
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
conformingReceipt.contentHash = calculateContentHash(conformingReceipt, CURRENT_HASH_VERSION);
|
|
1237
|
+
fs.writeFileSync(path.join(artifactsDir, `${receiptId}.json`), JSON.stringify(conformingReceipt, null, 2), "utf-8");
|
|
1238
|
+
fs.writeFileSync(path.join(sandboxDir, "tx-receipt.json"), JSON.stringify(conformingReceipt, null, 2), "utf-8");
|
|
1239
|
+
const store = new HardkasStore({ dbPath });
|
|
1240
|
+
store.connect({ autoMigrate: true });
|
|
1241
|
+
const backend = new SqliteQueryBackend(store);
|
|
1242
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
1243
|
+
const indexedBefore = await backend.findArtifacts();
|
|
1244
|
+
const lineageBefore = await backend.getLineageEdges();
|
|
1245
|
+
store.disconnect();
|
|
1246
|
+
if (fs.existsSync(dbPath)) {
|
|
1247
|
+
fs.unlinkSync(dbPath);
|
|
1248
|
+
}
|
|
1249
|
+
const eventsFile = path.join(sandboxDir, ".hardkas", "events.jsonl");
|
|
1250
|
+
if (fs.existsSync(eventsFile)) {
|
|
1251
|
+
fs.unlinkSync(eventsFile);
|
|
1252
|
+
}
|
|
1253
|
+
const newSandboxDir = `${sandboxDir}-reconstructed`;
|
|
1254
|
+
if (fs.existsSync(newSandboxDir)) {
|
|
1255
|
+
fs.rmSync(newSandboxDir, { recursive: true, force: true });
|
|
1256
|
+
}
|
|
1257
|
+
fs.renameSync(sandboxDir, newSandboxDir);
|
|
1258
|
+
if (fs.existsSync(sandboxDir)) {
|
|
1259
|
+
throw new Error("Failed to completely relocate workspace directory during environment reconstruction");
|
|
1260
|
+
}
|
|
1261
|
+
const newDbPath = path.join(newSandboxDir, ".hardkas", "store.db");
|
|
1262
|
+
const newStore = new HardkasStore({ dbPath: newDbPath });
|
|
1263
|
+
newStore.connect({ autoMigrate: true });
|
|
1264
|
+
const newBackend = new SqliteQueryBackend(newStore);
|
|
1265
|
+
const rebuildResult = await newBackend.rebuild({ strict: true, cwd: newSandboxDir });
|
|
1266
|
+
if (!rebuildResult.ok) {
|
|
1267
|
+
throw new Error("Rebuild failed under reconstructed workspace environment");
|
|
1268
|
+
}
|
|
1269
|
+
const indexedAfter = await newBackend.findArtifacts();
|
|
1270
|
+
const lineageAfter = await newBackend.getLineageEdges();
|
|
1271
|
+
if (indexedAfter.length !== indexedBefore.length) {
|
|
1272
|
+
throw new TortureInvariantError(
|
|
1273
|
+
`Artifact count drift: expected ${indexedBefore.length} canonical records, got ${indexedAfter.length} post-reconstruction`,
|
|
1274
|
+
"RECONSTRUCTION_ARTIFACT_DRIFT",
|
|
1275
|
+
"critical"
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
if (lineageAfter.length !== lineageBefore.length) {
|
|
1279
|
+
throw new TortureInvariantError(
|
|
1280
|
+
"Lineage connection drift: lineage edges differ after full environment relocation",
|
|
1281
|
+
"RECONSTRUCTION_LINEAGE_DRIFT",
|
|
1282
|
+
"critical"
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
const { Hardkas } = await import("@hardkas/sdk");
|
|
1286
|
+
const localnetStatePath = path.join(newSandboxDir, ".hardkas", "localnet.json");
|
|
1287
|
+
fs.writeFileSync(localnetStatePath, JSON.stringify(harness.state, null, 2), "utf-8");
|
|
1288
|
+
process.chdir(newSandboxDir);
|
|
1289
|
+
const sdk = await Hardkas.open({
|
|
1290
|
+
cwd: newSandboxDir,
|
|
1291
|
+
mode: "developer"
|
|
1292
|
+
});
|
|
1293
|
+
const replayResult = await sdk.replay.verify({
|
|
1294
|
+
path: newSandboxDir
|
|
1295
|
+
});
|
|
1296
|
+
newStore.disconnect();
|
|
1297
|
+
if (!replayResult.passed) {
|
|
1298
|
+
throw new TortureInvariantError(
|
|
1299
|
+
`Reconstructed workspace failed replay verification: ${replayResult.error || "unknown drift"}`,
|
|
1300
|
+
"RECONSTRUCTION_REPLAY_FAILED",
|
|
1301
|
+
"critical"
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
return {
|
|
1305
|
+
flow: "Total environment wipeout, folder relocation, rebuild, and replay verify",
|
|
1306
|
+
mutation: "Delete SQL store/events, rename sandbox, rebuild and execute SDK replay",
|
|
1307
|
+
expectedInvariant: "canonical_history_survives_full_environment_reconstruction",
|
|
1308
|
+
artifactsBefore: [planId],
|
|
1309
|
+
artifactsAfter: [receiptId]
|
|
1310
|
+
};
|
|
1311
|
+
} finally {
|
|
1312
|
+
process.chdir(originalCwd);
|
|
1313
|
+
cleanupSandbox(sandboxDir);
|
|
1314
|
+
cleanupSandbox(`${sandboxDir}-reconstructed`);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
registerTortureBucket({
|
|
1319
|
+
name: "scale-lineage-indexing",
|
|
1320
|
+
expectedInvariant: "lineage_closure_consistent_under_scale",
|
|
1321
|
+
async run(ctx) {
|
|
1322
|
+
const { sandboxDir, hardkasDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
1323
|
+
try {
|
|
1324
|
+
const store = new HardkasStore({ dbPath });
|
|
1325
|
+
store.connect({ autoMigrate: true });
|
|
1326
|
+
const backend = new SqliteQueryBackend(store);
|
|
1327
|
+
const artifactCount = ctx.prng.nextInt(50, 200);
|
|
1328
|
+
const chainDepth = ctx.prng.nextInt(3, 8);
|
|
1329
|
+
const generatedIds = [];
|
|
1330
|
+
const lineageChains = [];
|
|
1331
|
+
const numChains = Math.ceil(artifactCount / chainDepth);
|
|
1332
|
+
for (let chain = 0; chain < numChains; chain++) {
|
|
1333
|
+
const chainIds = [];
|
|
1334
|
+
let parentId = null;
|
|
1335
|
+
const thisChainDepth = Math.min(chainDepth, artifactCount - generatedIds.length);
|
|
1336
|
+
if (thisChainDepth <= 0) break;
|
|
1337
|
+
for (let depth = 0; depth < thisChainDepth; depth++) {
|
|
1338
|
+
const idx = generatedIds.length;
|
|
1339
|
+
const payload = {
|
|
1340
|
+
schema: "hardkas.txPlan",
|
|
1341
|
+
hardkasVersion: "0.7.0-alpha",
|
|
1342
|
+
version: ARTIFACT_VERSION,
|
|
1343
|
+
networkId: "simnet",
|
|
1344
|
+
mode: "simulated",
|
|
1345
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1346
|
+
// hardkas-determinism-allow: mock timestamp
|
|
1347
|
+
planId: `plan-scale-${chain}-${depth}-${ctx.caseSeed}`,
|
|
1348
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
1349
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
1350
|
+
amountSompi: (1000n * BigInt(ctx.prng.nextInt(1, 100) * 10 + idx)).toString(),
|
|
1351
|
+
estimatedFeeSompi: "100",
|
|
1352
|
+
estimatedMass: "10",
|
|
1353
|
+
inputs: [{ outpoint: { transactionId: "s".repeat(64), index: idx }, amountSompi: "20000" }],
|
|
1354
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "1000" }]
|
|
1355
|
+
};
|
|
1356
|
+
if (parentId) {
|
|
1357
|
+
payload.lineage = {
|
|
1358
|
+
artifactId: `plan-scale-child-${idx}`,
|
|
1359
|
+
lineageId: `lineage-chain-${chain}-${ctx.caseSeed}`,
|
|
1360
|
+
parentArtifactId: parentId,
|
|
1361
|
+
rootArtifactId: chainIds[0]
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
const hash = calculateContentHash(payload);
|
|
1365
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
1366
|
+
const artifact = { ...payload, artifactId: artId, contentHash: hash };
|
|
1367
|
+
const file = path.join(artifactsDir, `${artId}.json`);
|
|
1368
|
+
fs.writeFileSync(file, JSON.stringify(artifact, null, 2), "utf-8");
|
|
1369
|
+
generatedIds.push(artId);
|
|
1370
|
+
chainIds.push(artId);
|
|
1371
|
+
parentId = artId;
|
|
1372
|
+
}
|
|
1373
|
+
lineageChains.push(chainIds);
|
|
1374
|
+
}
|
|
1375
|
+
const syncStart = Date.now();
|
|
1376
|
+
const syncResult = await backend.sync({ strict: true, cwd: sandboxDir });
|
|
1377
|
+
const syncDurationMs = Date.now() - syncStart;
|
|
1378
|
+
if (!syncResult.ok) {
|
|
1379
|
+
throw new Error(`Full sync failed on ${generatedIds.length} artifacts: ${syncResult.errors.join(", ")}`);
|
|
1380
|
+
}
|
|
1381
|
+
const allArtifacts = await backend.findArtifacts();
|
|
1382
|
+
if (allArtifacts.length !== generatedIds.length) {
|
|
1383
|
+
throw new TortureInvariantError(
|
|
1384
|
+
`Scale parity: expected ${generatedIds.length} artifacts, got ${allArtifacts.length}`,
|
|
1385
|
+
"SCALE_PARITY_FAILURE",
|
|
1386
|
+
"critical"
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
const db = store.getDatabase();
|
|
1390
|
+
let closureValid = true;
|
|
1391
|
+
try {
|
|
1392
|
+
const closureCount = db.prepare("SELECT COUNT(*) as count FROM lineage_closure").get().count;
|
|
1393
|
+
for (const chain of lineageChains) {
|
|
1394
|
+
if (chain.length < 2) continue;
|
|
1395
|
+
const rootId = chain[0];
|
|
1396
|
+
const leafId = chain[chain.length - 1];
|
|
1397
|
+
const ancestorCheck = db.prepare(
|
|
1398
|
+
"SELECT COUNT(*) as count FROM lineage_closure WHERE ancestor_id = ? AND descendant_id = ?"
|
|
1399
|
+
).get(rootId, leafId);
|
|
1400
|
+
if (ancestorCheck.count === 0) {
|
|
1401
|
+
closureValid = false;
|
|
1402
|
+
throw new TortureInvariantError(
|
|
1403
|
+
`Lineage closure missing: root ${rootId} should be ancestor of leaf ${leafId} in chain of depth ${chain.length}`,
|
|
1404
|
+
"CLOSURE_TABLE_INCOMPLETE",
|
|
1405
|
+
"critical"
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
const statsRow = db.prepare(
|
|
1410
|
+
"SELECT stat_value FROM lineage_stats WHERE stat_key = 'closure_entries'"
|
|
1411
|
+
).get();
|
|
1412
|
+
if (!statsRow || parseInt(statsRow.stat_value) === 0) {
|
|
1413
|
+
if (lineageChains.some((c) => c.length > 1)) {
|
|
1414
|
+
throw new TortureInvariantError(
|
|
1415
|
+
"Lineage statistics not populated after scale sync",
|
|
1416
|
+
"STATS_NOT_POPULATED",
|
|
1417
|
+
"warning"
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
} catch (e) {
|
|
1422
|
+
if (e instanceof TortureInvariantError) throw e;
|
|
1423
|
+
}
|
|
1424
|
+
const incrementalStart = Date.now();
|
|
1425
|
+
const incrementalResult = await backend.sync({ strict: true, cwd: sandboxDir });
|
|
1426
|
+
const incrementalDurationMs = Date.now() - incrementalStart;
|
|
1427
|
+
if (!incrementalResult.ok) {
|
|
1428
|
+
throw new Error("Incremental sync failed");
|
|
1429
|
+
}
|
|
1430
|
+
store.disconnect();
|
|
1431
|
+
if (fs.existsSync(dbPath)) {
|
|
1432
|
+
fs.unlinkSync(dbPath);
|
|
1433
|
+
}
|
|
1434
|
+
const newStore = new HardkasStore({ dbPath });
|
|
1435
|
+
newStore.connect({ autoMigrate: true });
|
|
1436
|
+
const newBackend = new SqliteQueryBackend(newStore);
|
|
1437
|
+
const rebuildResult = await newBackend.rebuild({ strict: true, cwd: sandboxDir });
|
|
1438
|
+
if (!rebuildResult.ok) {
|
|
1439
|
+
throw new Error("Full rebuild failed after DB wipe at scale");
|
|
1440
|
+
}
|
|
1441
|
+
const rebuiltArtifacts = await newBackend.findArtifacts();
|
|
1442
|
+
if (rebuiltArtifacts.length !== generatedIds.length) {
|
|
1443
|
+
throw new TortureInvariantError(
|
|
1444
|
+
`Rebuild parity failure: expected ${generatedIds.length}, got ${rebuiltArtifacts.length}`,
|
|
1445
|
+
"REBUILD_PARITY_FAILURE",
|
|
1446
|
+
"critical"
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
newStore.disconnect();
|
|
1450
|
+
return {
|
|
1451
|
+
flow: `Scale lineage indexing: ${generatedIds.length} artifacts, ${lineageChains.length} chains, depth ${chainDepth}`,
|
|
1452
|
+
mutation: `Full sync (${syncDurationMs}ms), incremental sync (${incrementalDurationMs}ms), DB wipe + rebuild`,
|
|
1453
|
+
expectedInvariant: "lineage_closure_consistent_under_scale",
|
|
1454
|
+
artifactsBefore: generatedIds.slice(0, 5)
|
|
1455
|
+
// Only report first 5 for brevity
|
|
1456
|
+
};
|
|
1457
|
+
} finally {
|
|
1458
|
+
cleanupSandbox(sandboxDir);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
registerTortureBucket({
|
|
1463
|
+
name: "multi-process-chaos",
|
|
1464
|
+
expectedInvariant: "multi_process_writes_converge_to_consistent_state",
|
|
1465
|
+
async run(ctx) {
|
|
1466
|
+
const { sandboxDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
1467
|
+
try {
|
|
1468
|
+
const workerCount = ctx.prng.nextInt(3, 8);
|
|
1469
|
+
const artifactsPerWorker = ctx.prng.nextInt(5, 15);
|
|
1470
|
+
const allGeneratedIds = [];
|
|
1471
|
+
const workerPromises = [];
|
|
1472
|
+
for (let w = 0; w < workerCount; w++) {
|
|
1473
|
+
workerPromises.push((async () => {
|
|
1474
|
+
const workerIds = [];
|
|
1475
|
+
const workerPrng = new LcgPrng(ctx.caseSeed + w * 1e3);
|
|
1476
|
+
for (let i = 0; i < artifactsPerWorker; i++) {
|
|
1477
|
+
const payload = {
|
|
1478
|
+
schema: "hardkas.txPlan",
|
|
1479
|
+
hardkasVersion: "0.7.0-alpha",
|
|
1480
|
+
version: ARTIFACT_VERSION,
|
|
1481
|
+
networkId: "simnet",
|
|
1482
|
+
mode: "simulated",
|
|
1483
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1484
|
+
// hardkas-determinism-allow: mock timestamp
|
|
1485
|
+
planId: `plan-mp-w${w}-${i}-${ctx.caseSeed}`,
|
|
1486
|
+
from: { address: `kaspa:sim_worker_${w}` },
|
|
1487
|
+
to: { address: `kaspa:sim_target_${w}` },
|
|
1488
|
+
amountSompi: (1000n * BigInt(workerPrng.nextInt(1, 100))).toString(),
|
|
1489
|
+
estimatedFeeSompi: "100",
|
|
1490
|
+
estimatedMass: "10",
|
|
1491
|
+
inputs: [{ outpoint: { transactionId: "m".repeat(64), index: w * 100 + i }, amountSompi: "20000" }],
|
|
1492
|
+
outputs: [{ address: `kaspa:sim_target_${w}`, amountSompi: "1000" }]
|
|
1493
|
+
};
|
|
1494
|
+
const hash = calculateContentHash(payload);
|
|
1495
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
1496
|
+
workerIds.push(artId);
|
|
1497
|
+
const file = path.join(artifactsDir, `${artId}.json`);
|
|
1498
|
+
fs.writeFileSync(file, JSON.stringify({ ...payload, artifactId: artId, contentHash: hash }, null, 2));
|
|
1499
|
+
if (workerPrng.nextInt(0, 3) === 0) {
|
|
1500
|
+
await new Promise((r) => setTimeout(r, workerPrng.nextInt(1, 10)));
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
return workerIds;
|
|
1504
|
+
})());
|
|
1505
|
+
}
|
|
1506
|
+
const workerResults = await Promise.all(workerPromises);
|
|
1507
|
+
for (const ids2 of workerResults) {
|
|
1508
|
+
allGeneratedIds.push(...ids2);
|
|
1509
|
+
}
|
|
1510
|
+
const store = new HardkasStore({ dbPath });
|
|
1511
|
+
store.connect({ autoMigrate: true });
|
|
1512
|
+
const backend = new SqliteQueryBackend(store);
|
|
1513
|
+
const syncPromises = [];
|
|
1514
|
+
for (let s = 0; s < 3; s++) {
|
|
1515
|
+
syncPromises.push(
|
|
1516
|
+
backend.sync({ strict: false, cwd: sandboxDir }).catch(() => null)
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
await Promise.all(syncPromises);
|
|
1520
|
+
const rebuildResult = await backend.rebuild({ strict: true, cwd: sandboxDir });
|
|
1521
|
+
if (!rebuildResult.ok) {
|
|
1522
|
+
throw new TortureInvariantError(
|
|
1523
|
+
"Final rebuild failed after multi-process writes",
|
|
1524
|
+
"MULTI_PROCESS_REBUILD_FAILED",
|
|
1525
|
+
"critical"
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
const records = await backend.findArtifacts();
|
|
1529
|
+
const ids = records.map((r) => r.artifactId);
|
|
1530
|
+
const uniqueIds = new Set(ids);
|
|
1531
|
+
if (ids.length !== uniqueIds.size) {
|
|
1532
|
+
throw new TortureInvariantError(
|
|
1533
|
+
`Multi-process writes produced ${ids.length - uniqueIds.size} duplicate canonical IDs`,
|
|
1534
|
+
"MULTI_PROCESS_DUPLICATE_IDS",
|
|
1535
|
+
"critical"
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
const uniqueGenerated = new Set(allGeneratedIds);
|
|
1539
|
+
const missingIds = [...uniqueGenerated].filter((id) => !uniqueIds.has(id));
|
|
1540
|
+
if (missingIds.length > 0) {
|
|
1541
|
+
throw new TortureInvariantError(
|
|
1542
|
+
`Multi-process writes lost ${missingIds.length} artifacts during convergence`,
|
|
1543
|
+
"MULTI_PROCESS_ARTIFACT_LOSS",
|
|
1544
|
+
"critical"
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
const edges = await backend.getLineageEdges();
|
|
1548
|
+
const orphanEdges = edges.filter(
|
|
1549
|
+
(e) => !uniqueIds.has(e.parentArtifactId) || !uniqueIds.has(e.childArtifactId)
|
|
1550
|
+
);
|
|
1551
|
+
const doctorReport = await backend.doctor();
|
|
1552
|
+
store.disconnect();
|
|
1553
|
+
if (doctorReport.orphanEdges > 0) {
|
|
1554
|
+
throw new TortureInvariantError(
|
|
1555
|
+
`Orphan edges detected after multi-process convergence: ${doctorReport.orphanEdges}`,
|
|
1556
|
+
"MULTI_PROCESS_ORPHAN_EDGES",
|
|
1557
|
+
"critical"
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
return {
|
|
1561
|
+
flow: `${workerCount} concurrent workers wrote ${allGeneratedIds.length} artifacts, then rebuild converged`,
|
|
1562
|
+
mutation: `Parallel writes with ${workerCount} workers, ${artifactsPerWorker} artifacts each, interleaved syncs`,
|
|
1563
|
+
expectedInvariant: "multi_process_writes_converge_to_consistent_state",
|
|
1564
|
+
artifactsBefore: allGeneratedIds.slice(0, 5)
|
|
1565
|
+
};
|
|
1566
|
+
} finally {
|
|
1567
|
+
cleanupSandbox(sandboxDir);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
registerTortureBucket({
|
|
1572
|
+
name: "sync-lock-contention",
|
|
1573
|
+
expectedInvariant: "lock_contention_never_corrupts_state",
|
|
1574
|
+
async run(ctx) {
|
|
1575
|
+
const { sandboxDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
1576
|
+
const subCase = ctx.prng.nextInt(1, 5);
|
|
1577
|
+
try {
|
|
1578
|
+
const { acquireLock, clearLock, listLocks, isProcessAlive: checkAlive } = await import("@hardkas/core");
|
|
1579
|
+
const lockDir = path.join(sandboxDir, ".hardkas", "locks");
|
|
1580
|
+
fs.mkdirSync(lockDir, { recursive: true });
|
|
1581
|
+
switch (subCase) {
|
|
1582
|
+
case 1: {
|
|
1583
|
+
const stalePid = 99999999;
|
|
1584
|
+
const staleLock = {
|
|
1585
|
+
schema: "hardkas.lock.v1",
|
|
1586
|
+
name: "workspace",
|
|
1587
|
+
pid: stalePid,
|
|
1588
|
+
command: "hardkas torture test",
|
|
1589
|
+
cwd: sandboxDir,
|
|
1590
|
+
hostname: __require("os").hostname(),
|
|
1591
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1592
|
+
// hardkas-determinism-allow: mock timestamp
|
|
1593
|
+
expiresAt: null
|
|
1594
|
+
};
|
|
1595
|
+
const lockPath = path.join(lockDir, "workspace.lock");
|
|
1596
|
+
fs.writeFileSync(lockPath, JSON.stringify(staleLock, null, 2));
|
|
1597
|
+
let acquired = false;
|
|
1598
|
+
try {
|
|
1599
|
+
const handle = await acquireLock({
|
|
1600
|
+
rootDir: sandboxDir,
|
|
1601
|
+
name: "workspace",
|
|
1602
|
+
command: "torture-test",
|
|
1603
|
+
timeoutMs: 5e3
|
|
1604
|
+
});
|
|
1605
|
+
acquired = true;
|
|
1606
|
+
await handle.release();
|
|
1607
|
+
} catch (err) {
|
|
1608
|
+
if (err.code === "STALE_LOCK") {
|
|
1609
|
+
acquired = true;
|
|
1610
|
+
if (fs.existsSync(lockPath)) {
|
|
1611
|
+
fs.unlinkSync(lockPath);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
if (!acquired) {
|
|
1616
|
+
throw new TortureInvariantError(
|
|
1617
|
+
"Stale lock was not detected or recovered",
|
|
1618
|
+
"STALE_LOCK_NOT_RECOVERED",
|
|
1619
|
+
"critical"
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
break;
|
|
1623
|
+
}
|
|
1624
|
+
case 2: {
|
|
1625
|
+
const handle1 = await acquireLock({
|
|
1626
|
+
rootDir: sandboxDir,
|
|
1627
|
+
name: "workspace",
|
|
1628
|
+
command: "torture-holder"
|
|
1629
|
+
});
|
|
1630
|
+
let lockHeldCorrectly = false;
|
|
1631
|
+
try {
|
|
1632
|
+
await acquireLock({
|
|
1633
|
+
rootDir: sandboxDir,
|
|
1634
|
+
name: "workspace",
|
|
1635
|
+
command: "torture-contender",
|
|
1636
|
+
wait: false
|
|
1637
|
+
});
|
|
1638
|
+
} catch (err) {
|
|
1639
|
+
if (err.code === "LOCK_HELD") {
|
|
1640
|
+
lockHeldCorrectly = true;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
await handle1.release();
|
|
1644
|
+
if (!lockHeldCorrectly) {
|
|
1645
|
+
throw new TortureInvariantError(
|
|
1646
|
+
"Second lock acquisition did not throw LOCK_HELD",
|
|
1647
|
+
"LOCK_HELD_NOT_ENFORCED",
|
|
1648
|
+
"critical"
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
break;
|
|
1652
|
+
}
|
|
1653
|
+
case 3: {
|
|
1654
|
+
const corruptLockPath = path.join(lockDir, "workspace.lock");
|
|
1655
|
+
fs.writeFileSync(corruptLockPath, "{{invalid json garbage!!!}}", "utf-8");
|
|
1656
|
+
let recovered = false;
|
|
1657
|
+
try {
|
|
1658
|
+
const handle = await acquireLock({
|
|
1659
|
+
rootDir: sandboxDir,
|
|
1660
|
+
name: "workspace",
|
|
1661
|
+
command: "torture-corrupt-recovery"
|
|
1662
|
+
});
|
|
1663
|
+
recovered = true;
|
|
1664
|
+
await handle.release();
|
|
1665
|
+
} catch (err) {
|
|
1666
|
+
if (fs.existsSync(corruptLockPath)) {
|
|
1667
|
+
fs.unlinkSync(corruptLockPath);
|
|
1668
|
+
}
|
|
1669
|
+
recovered = true;
|
|
1670
|
+
}
|
|
1671
|
+
if (!recovered) {
|
|
1672
|
+
throw new TortureInvariantError(
|
|
1673
|
+
"Corrupted lock file caused unrecoverable state",
|
|
1674
|
+
"CORRUPT_LOCK_UNRECOVERABLE",
|
|
1675
|
+
"critical"
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
break;
|
|
1679
|
+
}
|
|
1680
|
+
case 4: {
|
|
1681
|
+
const handle = await acquireLock({
|
|
1682
|
+
rootDir: sandboxDir,
|
|
1683
|
+
name: "workspace",
|
|
1684
|
+
command: "torture-list-test"
|
|
1685
|
+
});
|
|
1686
|
+
const locks = listLocks(sandboxDir);
|
|
1687
|
+
if (locks.length === 0) {
|
|
1688
|
+
throw new TortureInvariantError(
|
|
1689
|
+
"listLocks returned empty while lock is held",
|
|
1690
|
+
"LOCK_LIST_EMPTY",
|
|
1691
|
+
"critical"
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
const found = locks.find((l) => l.name === "workspace");
|
|
1695
|
+
if (!found || !found.isAlive) {
|
|
1696
|
+
throw new TortureInvariantError(
|
|
1697
|
+
"Active lock not found or reported as dead by listLocks",
|
|
1698
|
+
"LOCK_LIST_INCONSISTENT",
|
|
1699
|
+
"critical"
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
const clearResult = clearLock(sandboxDir, "workspace");
|
|
1703
|
+
if (clearResult.cleared) {
|
|
1704
|
+
throw new TortureInvariantError(
|
|
1705
|
+
"clearLock removed an active lock without --force",
|
|
1706
|
+
"LOCK_CLEAR_WITHOUT_FORCE",
|
|
1707
|
+
"critical"
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
await handle.release();
|
|
1711
|
+
break;
|
|
1712
|
+
}
|
|
1713
|
+
case 5: {
|
|
1714
|
+
const { withLocks } = await import("@hardkas/core");
|
|
1715
|
+
let locksAcquired = false;
|
|
1716
|
+
try {
|
|
1717
|
+
await withLocks(
|
|
1718
|
+
sandboxDir,
|
|
1719
|
+
["artifacts", "query-store"],
|
|
1720
|
+
async () => {
|
|
1721
|
+
locksAcquired = true;
|
|
1722
|
+
const artLock = path.join(lockDir, "artifacts.lock");
|
|
1723
|
+
const qsLock = path.join(lockDir, "query-store.lock");
|
|
1724
|
+
if (!fs.existsSync(artLock) || !fs.existsSync(qsLock)) {
|
|
1725
|
+
throw new TortureInvariantError(
|
|
1726
|
+
"Lock files not created on disk while held",
|
|
1727
|
+
"LOCK_FILES_MISSING",
|
|
1728
|
+
"critical"
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
);
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
if (err instanceof TortureInvariantError) throw err;
|
|
1735
|
+
}
|
|
1736
|
+
const remainingLocks = listLocks(sandboxDir);
|
|
1737
|
+
const stale = remainingLocks.filter(
|
|
1738
|
+
(l) => l.name === "artifacts" || l.name === "query-store"
|
|
1739
|
+
);
|
|
1740
|
+
if (stale.length > 0) {
|
|
1741
|
+
throw new TortureInvariantError(
|
|
1742
|
+
`${stale.length} locks not released after withLocks completed`,
|
|
1743
|
+
"LOCKS_NOT_RELEASED",
|
|
1744
|
+
"critical"
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
break;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
const store = new HardkasStore({ dbPath });
|
|
1751
|
+
store.connect({ autoMigrate: true });
|
|
1752
|
+
const backend = new SqliteQueryBackend(store);
|
|
1753
|
+
const payload = {
|
|
1754
|
+
schema: "hardkas.txPlan",
|
|
1755
|
+
hardkasVersion: "0.7.0-alpha",
|
|
1756
|
+
version: ARTIFACT_VERSION,
|
|
1757
|
+
networkId: "simnet",
|
|
1758
|
+
mode: "simulated",
|
|
1759
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1760
|
+
// hardkas-determinism-allow: mock timestamp
|
|
1761
|
+
planId: `plan-lock-verify-${ctx.caseSeed}`,
|
|
1762
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
1763
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
1764
|
+
amountSompi: "1000",
|
|
1765
|
+
estimatedFeeSompi: "100",
|
|
1766
|
+
estimatedMass: "10",
|
|
1767
|
+
inputs: [{ outpoint: { transactionId: "l".repeat(64), index: 0 }, amountSompi: "2000" }],
|
|
1768
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "1000" }]
|
|
1769
|
+
};
|
|
1770
|
+
const hash = calculateContentHash(payload);
|
|
1771
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
1772
|
+
const file = path.join(artifactsDir, `${artId}.json`);
|
|
1773
|
+
fs.writeFileSync(file, JSON.stringify({ ...payload, artifactId: artId, contentHash: hash }, null, 2));
|
|
1774
|
+
const syncResult = await backend.sync({ strict: true, cwd: sandboxDir });
|
|
1775
|
+
store.disconnect();
|
|
1776
|
+
if (!syncResult.ok) {
|
|
1777
|
+
throw new TortureInvariantError(
|
|
1778
|
+
"Query store corrupted after lock contention: sync failed",
|
|
1779
|
+
"POST_CONTENTION_CORRUPTION",
|
|
1780
|
+
"catastrophic"
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
return {
|
|
1784
|
+
flow: `Lock contention subcase ${subCase}: ${["stale recovery", "lock timeout", "corrupt recovery", "list/clear", "lock ordering"][subCase - 1]}`,
|
|
1785
|
+
mutation: `Test lock behavior under contention scenario ${subCase}`,
|
|
1786
|
+
expectedInvariant: "lock_contention_never_corrupts_state",
|
|
1787
|
+
artifactsBefore: [artId]
|
|
1788
|
+
};
|
|
1789
|
+
} finally {
|
|
1790
|
+
cleanupSandbox(sandboxDir);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
registerTortureBucket({
|
|
1795
|
+
name: "windows-filesystem-hell",
|
|
1796
|
+
expectedInvariant: "windows_filesystem_semantics_cannot_corrupt_canonical_truth",
|
|
1797
|
+
async run(ctx) {
|
|
1798
|
+
const { sandboxDir, hardkasDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
1799
|
+
const subCase = ctx.prng.nextInt(1, 6);
|
|
1800
|
+
try {
|
|
1801
|
+
const store = new HardkasStore({ dbPath });
|
|
1802
|
+
store.connect({ autoMigrate: true });
|
|
1803
|
+
const backend = new SqliteQueryBackend(store);
|
|
1804
|
+
switch (subCase) {
|
|
1805
|
+
case 1: {
|
|
1806
|
+
const payload1 = {
|
|
1807
|
+
schema: "hardkas.txPlan",
|
|
1808
|
+
hardkasVersion: "0.7.0-alpha",
|
|
1809
|
+
version: ARTIFACT_VERSION,
|
|
1810
|
+
networkId: "simnet",
|
|
1811
|
+
mode: "simulated",
|
|
1812
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1813
|
+
// hardkas-determinism-allow: mock timestamp
|
|
1814
|
+
planId: `plan-case-upper-${ctx.caseSeed}`,
|
|
1815
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
1816
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
1817
|
+
amountSompi: "1000",
|
|
1818
|
+
estimatedFeeSompi: "100",
|
|
1819
|
+
estimatedMass: "10",
|
|
1820
|
+
inputs: [{ outpoint: { transactionId: "w".repeat(64), index: 0 }, amountSompi: "2000" }],
|
|
1821
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "1000" }]
|
|
1822
|
+
};
|
|
1823
|
+
const hash1 = calculateContentHash(payload1);
|
|
1824
|
+
const art1 = { ...payload1, artifactId: `Plan-Upper-${hash1.slice(0, 8)}`, contentHash: hash1 };
|
|
1825
|
+
const payload2 = {
|
|
1826
|
+
...payload1,
|
|
1827
|
+
planId: `plan-case-lower-${ctx.caseSeed}`,
|
|
1828
|
+
amountSompi: "2000"
|
|
1829
|
+
};
|
|
1830
|
+
const hash2 = calculateContentHash(payload2);
|
|
1831
|
+
const art2 = { ...payload2, artifactId: `plan-lower-${hash2.slice(0, 8)}`, contentHash: hash2 };
|
|
1832
|
+
const file1 = path.join(artifactsDir, `CaseTest.json`);
|
|
1833
|
+
const file2 = path.join(artifactsDir, `casetest.json`);
|
|
1834
|
+
fs.writeFileSync(file1, JSON.stringify(art1, null, 2));
|
|
1835
|
+
let caseCollision = false;
|
|
1836
|
+
try {
|
|
1837
|
+
fs.writeFileSync(file2, JSON.stringify(art2, null, 2));
|
|
1838
|
+
const content1 = fs.readFileSync(file1, "utf-8");
|
|
1839
|
+
const content2 = fs.readFileSync(file2, "utf-8");
|
|
1840
|
+
caseCollision = content1 === content2 && content1 === JSON.stringify(art2, null, 2);
|
|
1841
|
+
} catch {
|
|
1842
|
+
caseCollision = true;
|
|
1843
|
+
}
|
|
1844
|
+
const syncResult = await backend.sync({ strict: false, cwd: sandboxDir });
|
|
1845
|
+
const artifacts = await backend.findArtifacts();
|
|
1846
|
+
if (artifacts.length === 0) {
|
|
1847
|
+
throw new TortureInvariantError(
|
|
1848
|
+
"Case collision caused zero artifacts to be indexed",
|
|
1849
|
+
"CASE_COLLISION_ZERO_INDEXED",
|
|
1850
|
+
"critical"
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
break;
|
|
1854
|
+
}
|
|
1855
|
+
case 2: {
|
|
1856
|
+
const reservedNames = ["CON", "NUL", "PRN", "AUX", "COM1", "LPT1"];
|
|
1857
|
+
const pickedName = ctx.prng.pick(reservedNames);
|
|
1858
|
+
let reservedFailed = false;
|
|
1859
|
+
let errorType = "";
|
|
1860
|
+
try {
|
|
1861
|
+
const testPath = path.join(artifactsDir, `${pickedName}.json`);
|
|
1862
|
+
fs.writeFileSync(testPath, JSON.stringify({ test: true }));
|
|
1863
|
+
try {
|
|
1864
|
+
fs.unlinkSync(testPath);
|
|
1865
|
+
} catch {
|
|
1866
|
+
}
|
|
1867
|
+
} catch (e) {
|
|
1868
|
+
reservedFailed = true;
|
|
1869
|
+
errorType = e.code || e.message;
|
|
1870
|
+
}
|
|
1871
|
+
const safePayload = {
|
|
1872
|
+
schema: "hardkas.txPlan",
|
|
1873
|
+
hardkasVersion: "0.7.0-alpha",
|
|
1874
|
+
version: ARTIFACT_VERSION,
|
|
1875
|
+
networkId: "simnet",
|
|
1876
|
+
mode: "simulated",
|
|
1877
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1878
|
+
// hardkas-determinism-allow: mock timestamp
|
|
1879
|
+
planId: `plan-reserved-safe-${ctx.caseSeed}`,
|
|
1880
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
1881
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
1882
|
+
amountSompi: "3000",
|
|
1883
|
+
estimatedFeeSompi: "100",
|
|
1884
|
+
estimatedMass: "10",
|
|
1885
|
+
inputs: [{ outpoint: { transactionId: "r".repeat(64), index: 0 }, amountSompi: "4000" }],
|
|
1886
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "3000" }]
|
|
1887
|
+
};
|
|
1888
|
+
const safeHash = calculateContentHash(safePayload);
|
|
1889
|
+
const safeArt = { ...safePayload, artifactId: `plan-${safeHash.slice(0, 16)}`, contentHash: safeHash };
|
|
1890
|
+
fs.writeFileSync(path.join(artifactsDir, `${safeArt.artifactId}.json`), JSON.stringify(safeArt, null, 2));
|
|
1891
|
+
const syncResult = await backend.sync({ strict: true, cwd: sandboxDir });
|
|
1892
|
+
if (!syncResult.ok) {
|
|
1893
|
+
throw new TortureInvariantError(
|
|
1894
|
+
`Reserved filename "${pickedName}" test corrupted store: ${syncResult.errors.join(", ")}`,
|
|
1895
|
+
"RESERVED_NAME_CORRUPTION",
|
|
1896
|
+
"critical"
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
break;
|
|
1900
|
+
}
|
|
1901
|
+
case 3: {
|
|
1902
|
+
const payload = {
|
|
1903
|
+
schema: "hardkas.txPlan",
|
|
1904
|
+
hardkasVersion: "0.7.0-alpha",
|
|
1905
|
+
version: ARTIFACT_VERSION,
|
|
1906
|
+
networkId: "simnet",
|
|
1907
|
+
mode: "simulated",
|
|
1908
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1909
|
+
// hardkas-determinism-allow: mock timestamp
|
|
1910
|
+
planId: `plan-backslash-${ctx.caseSeed}`,
|
|
1911
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
1912
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
1913
|
+
amountSompi: "5000",
|
|
1914
|
+
estimatedFeeSompi: "100",
|
|
1915
|
+
estimatedMass: "10",
|
|
1916
|
+
inputs: [{ outpoint: { transactionId: "b".repeat(64), index: 0 }, amountSompi: "6000" }],
|
|
1917
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "5000" }]
|
|
1918
|
+
};
|
|
1919
|
+
const hash = calculateContentHash(payload);
|
|
1920
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
1921
|
+
const art = { ...payload, artifactId: artId, contentHash: hash };
|
|
1922
|
+
const normalizedPath = path.join(artifactsDir, `${artId}.json`);
|
|
1923
|
+
fs.writeFileSync(normalizedPath, JSON.stringify(art, null, 2));
|
|
1924
|
+
const backslashPath = artifactsDir.replace(/\//g, "\\") + `\\${artId}.json`;
|
|
1925
|
+
const forwardPath = artifactsDir.replace(/\\/g, "/") + `/${artId}.json`;
|
|
1926
|
+
const content1 = fs.readFileSync(backslashPath, "utf-8");
|
|
1927
|
+
const content2 = fs.readFileSync(forwardPath, "utf-8");
|
|
1928
|
+
if (content1 !== content2) {
|
|
1929
|
+
throw new TortureInvariantError(
|
|
1930
|
+
"Backslash vs forward slash path reads produced different content",
|
|
1931
|
+
"PATH_NORMALIZATION_DIVERGENCE",
|
|
1932
|
+
"catastrophic"
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
1936
|
+
break;
|
|
1937
|
+
}
|
|
1938
|
+
case 4: {
|
|
1939
|
+
let deepDir = artifactsDir;
|
|
1940
|
+
const segments = [];
|
|
1941
|
+
const segmentLength = ctx.prng.nextInt(10, 30);
|
|
1942
|
+
let pathTooLong = false;
|
|
1943
|
+
let actualPathLength = 0;
|
|
1944
|
+
for (let i = 0; i < 15; i++) {
|
|
1945
|
+
const segment = `d${"a".repeat(segmentLength)}_${i}`;
|
|
1946
|
+
segments.push(segment);
|
|
1947
|
+
const newDir = path.join(deepDir, segment);
|
|
1948
|
+
actualPathLength = newDir.length;
|
|
1949
|
+
try {
|
|
1950
|
+
fs.mkdirSync(newDir, { recursive: true });
|
|
1951
|
+
deepDir = newDir;
|
|
1952
|
+
} catch (e) {
|
|
1953
|
+
pathTooLong = true;
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
const payload = {
|
|
1958
|
+
schema: "hardkas.txPlan",
|
|
1959
|
+
hardkasVersion: "0.7.0-alpha",
|
|
1960
|
+
version: ARTIFACT_VERSION,
|
|
1961
|
+
networkId: "simnet",
|
|
1962
|
+
mode: "simulated",
|
|
1963
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1964
|
+
// hardkas-determinism-allow: mock timestamp
|
|
1965
|
+
planId: `plan-maxpath-${ctx.caseSeed}`,
|
|
1966
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
1967
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
1968
|
+
amountSompi: "7000",
|
|
1969
|
+
estimatedFeeSompi: "100",
|
|
1970
|
+
estimatedMass: "10",
|
|
1971
|
+
inputs: [{ outpoint: { transactionId: "p".repeat(64), index: 0 }, amountSompi: "8000" }],
|
|
1972
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "7000" }]
|
|
1973
|
+
};
|
|
1974
|
+
const hash = calculateContentHash(payload);
|
|
1975
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
1976
|
+
const safeArt = { ...payload, artifactId: artId, contentHash: hash };
|
|
1977
|
+
fs.writeFileSync(path.join(artifactsDir, `${artId}.json`), JSON.stringify(safeArt, null, 2));
|
|
1978
|
+
const syncResult = await backend.sync({ strict: false, cwd: sandboxDir });
|
|
1979
|
+
const artifacts = await backend.findArtifacts();
|
|
1980
|
+
if (artifacts.length === 0) {
|
|
1981
|
+
throw new TortureInvariantError(
|
|
1982
|
+
`MAX_PATH stress (${actualPathLength} chars) caused zero artifacts indexed`,
|
|
1983
|
+
"MAXPATH_PARTIAL_STATE",
|
|
1984
|
+
"critical"
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
store.disconnect();
|
|
1988
|
+
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
|
|
1989
|
+
const newStore = new HardkasStore({ dbPath });
|
|
1990
|
+
newStore.connect({ autoMigrate: true });
|
|
1991
|
+
const newBackend = new SqliteQueryBackend(newStore);
|
|
1992
|
+
const rebuildResult = await newBackend.rebuild({ strict: false, cwd: sandboxDir });
|
|
1993
|
+
const rebuiltArtifacts = await newBackend.findArtifacts();
|
|
1994
|
+
if (rebuiltArtifacts.length !== artifacts.length) {
|
|
1995
|
+
throw new TortureInvariantError(
|
|
1996
|
+
`MAX_PATH rebuild inconsistency: sync got ${artifacts.length}, rebuild got ${rebuiltArtifacts.length}`,
|
|
1997
|
+
"MAXPATH_REBUILD_DIVERGENCE",
|
|
1998
|
+
"critical"
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
newStore.disconnect();
|
|
2002
|
+
break;
|
|
2003
|
+
}
|
|
2004
|
+
case 5: {
|
|
2005
|
+
const payloadWithCRLF = {
|
|
2006
|
+
schema: "hardkas.txPlan",
|
|
2007
|
+
hardkasVersion: "0.7.0-alpha",
|
|
2008
|
+
version: ARTIFACT_VERSION,
|
|
2009
|
+
networkId: "simnet",
|
|
2010
|
+
mode: "simulated",
|
|
2011
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2012
|
+
// hardkas-determinism-allow: mock timestamp
|
|
2013
|
+
planId: `plan-crlf-test-${ctx.caseSeed}`,
|
|
2014
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
2015
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
2016
|
+
amountSompi: "9000",
|
|
2017
|
+
estimatedFeeSompi: "100",
|
|
2018
|
+
estimatedMass: "10",
|
|
2019
|
+
inputs: [{ outpoint: { transactionId: "c".repeat(64), index: 0 }, amountSompi: "10000" }],
|
|
2020
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "9000" }]
|
|
2021
|
+
};
|
|
2022
|
+
const hashLF = calculateContentHash(payloadWithCRLF, CURRENT_HASH_VERSION);
|
|
2023
|
+
const payloadCRLFInjected = { ...payloadWithCRLF, planId: `plan-crlf-test-${ctx.caseSeed}`.replace(/\n/g, "\r\n") };
|
|
2024
|
+
const hashCRLF = calculateContentHash(payloadCRLFInjected, CURRENT_HASH_VERSION);
|
|
2025
|
+
if (hashLF !== hashCRLF) {
|
|
2026
|
+
throw new TortureInvariantError(
|
|
2027
|
+
`CRLF normalization failed: LF hash ${hashLF.slice(0, 16)} !== CRLF hash ${hashCRLF.slice(0, 16)}`,
|
|
2028
|
+
"CRLF_HASH_DIVERGENCE",
|
|
2029
|
+
"catastrophic"
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
const artId = `plan-${hashLF.slice(0, 16)}`;
|
|
2033
|
+
const art = { ...payloadWithCRLF, artifactId: artId, contentHash: hashLF };
|
|
2034
|
+
const jsonStr = JSON.stringify(art, null, 2).replace(/\n/g, "\r\n");
|
|
2035
|
+
fs.writeFileSync(path.join(artifactsDir, `${artId}.json`), jsonStr, "utf-8");
|
|
2036
|
+
await backend.sync({ strict: false, cwd: sandboxDir });
|
|
2037
|
+
break;
|
|
2038
|
+
}
|
|
2039
|
+
case 6: {
|
|
2040
|
+
const payload = {
|
|
2041
|
+
schema: "hardkas.txPlan",
|
|
2042
|
+
hardkasVersion: "0.7.0-alpha",
|
|
2043
|
+
version: ARTIFACT_VERSION,
|
|
2044
|
+
networkId: "simnet",
|
|
2045
|
+
mode: "simulated",
|
|
2046
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2047
|
+
// hardkas-determinism-allow: mock timestamp
|
|
2048
|
+
planId: `plan-locked-${ctx.caseSeed}`,
|
|
2049
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
2050
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
2051
|
+
amountSompi: "11000",
|
|
2052
|
+
estimatedFeeSompi: "100",
|
|
2053
|
+
estimatedMass: "10",
|
|
2054
|
+
inputs: [{ outpoint: { transactionId: "k".repeat(64), index: 0 }, amountSompi: "12000" }],
|
|
2055
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "11000" }]
|
|
2056
|
+
};
|
|
2057
|
+
const hash = calculateContentHash(payload);
|
|
2058
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
2059
|
+
const art = { ...payload, artifactId: artId, contentHash: hash };
|
|
2060
|
+
const filePath = path.join(artifactsDir, `${artId}.json`);
|
|
2061
|
+
fs.writeFileSync(filePath, JSON.stringify(art, null, 2));
|
|
2062
|
+
const payload2 = { ...payload, planId: `plan-unlocked-${ctx.caseSeed}`, amountSompi: "99000" };
|
|
2063
|
+
const hash2 = calculateContentHash(payload2);
|
|
2064
|
+
const artId2 = `plan-${hash2.slice(0, 16)}`;
|
|
2065
|
+
fs.writeFileSync(
|
|
2066
|
+
path.join(artifactsDir, `${artId2}.json`),
|
|
2067
|
+
JSON.stringify({ ...payload2, artifactId: artId2, contentHash: hash2 }, null, 2)
|
|
2068
|
+
);
|
|
2069
|
+
const syncResult = await backend.sync({ strict: false, cwd: sandboxDir });
|
|
2070
|
+
const artifacts = await backend.findArtifacts();
|
|
2071
|
+
if (artifacts.length === 0) {
|
|
2072
|
+
throw new TortureInvariantError(
|
|
2073
|
+
"Locked file scenario caused zero artifacts to be indexed",
|
|
2074
|
+
"LOCKED_FILE_ZERO_INDEXED",
|
|
2075
|
+
"critical"
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
break;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
store.disconnect();
|
|
2082
|
+
return {
|
|
2083
|
+
flow: `Windows filesystem hell subcase ${subCase}: ${["case-insensitive collision", "reserved filenames", "backslash normalization", "MAX_PATH stress", "CRLF stability", "locked file sync"][subCase - 1]}`,
|
|
2084
|
+
mutation: `Windows-specific filesystem edge case ${subCase}`,
|
|
2085
|
+
expectedInvariant: "windows_filesystem_semantics_cannot_corrupt_canonical_truth"
|
|
2086
|
+
};
|
|
2087
|
+
} finally {
|
|
2088
|
+
cleanupSandbox(sandboxDir);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
});
|
|
2092
|
+
registerTortureBucket({
|
|
2093
|
+
name: "unicode-normalization-chaos",
|
|
2094
|
+
expectedInvariant: "unicode_normalization_never_changes_artifact_identity",
|
|
2095
|
+
async run(ctx) {
|
|
2096
|
+
const { sandboxDir, hardkasDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
2097
|
+
const subCase = ctx.prng.nextInt(1, 5);
|
|
2098
|
+
try {
|
|
2099
|
+
const store = new HardkasStore({ dbPath });
|
|
2100
|
+
store.connect({ autoMigrate: true });
|
|
2101
|
+
const backend = new SqliteQueryBackend(store);
|
|
2102
|
+
switch (subCase) {
|
|
2103
|
+
case 1: {
|
|
2104
|
+
const nfcString = "\xE9";
|
|
2105
|
+
const nfdString = "e\u0301";
|
|
2106
|
+
const payload1 = {
|
|
2107
|
+
schema: "hardkas.txPlan",
|
|
2108
|
+
hardkasVersion: "0.7.0-alpha",
|
|
2109
|
+
version: ARTIFACT_VERSION,
|
|
2110
|
+
networkId: "simnet",
|
|
2111
|
+
mode: "simulated",
|
|
2112
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2113
|
+
// hardkas-determinism-allow: mock timestamp
|
|
2114
|
+
planId: `plan-nfc-${nfcString}-${ctx.caseSeed}`,
|
|
2115
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
2116
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
2117
|
+
amountSompi: "1000",
|
|
2118
|
+
estimatedFeeSompi: "100",
|
|
2119
|
+
estimatedMass: "10",
|
|
2120
|
+
inputs: [{ outpoint: { transactionId: "u".repeat(64), index: 0 }, amountSompi: "2000" }],
|
|
2121
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "1000" }]
|
|
2122
|
+
};
|
|
2123
|
+
const payload2 = { ...payload1, planId: `plan-nfc-${nfdString}-${ctx.caseSeed}` };
|
|
2124
|
+
const hash1 = calculateContentHash(payload1, CURRENT_HASH_VERSION);
|
|
2125
|
+
const hash2 = calculateContentHash(payload2, CURRENT_HASH_VERSION);
|
|
2126
|
+
if (hash1 !== hash2) {
|
|
2127
|
+
throw new TortureInvariantError(
|
|
2128
|
+
`NFC/NFD normalization divergence: NFC=${hash1.slice(0, 16)} NFD=${hash2.slice(0, 16)}`,
|
|
2129
|
+
"UNICODE_NFC_NFD_DIVERGENCE",
|
|
2130
|
+
"catastrophic"
|
|
2131
|
+
);
|
|
2132
|
+
}
|
|
2133
|
+
const artId = `plan-${hash1.slice(0, 16)}`;
|
|
2134
|
+
const art = { ...payload1, artifactId: artId, contentHash: hash1 };
|
|
2135
|
+
fs.writeFileSync(path.join(artifactsDir, `${artId}.json`), JSON.stringify(art, null, 2));
|
|
2136
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2137
|
+
break;
|
|
2138
|
+
}
|
|
2139
|
+
case 2: {
|
|
2140
|
+
const emojiStrings = ["\u{1F511}", "\u{1F4B0}", "\u{1F3D7}\uFE0F", "\u26A1", "\u{1F3AF}", "\u{1F6E1}\uFE0F"];
|
|
2141
|
+
const emoji = ctx.prng.pick(emojiStrings);
|
|
2142
|
+
const payload = {
|
|
2143
|
+
schema: "hardkas.txPlan",
|
|
2144
|
+
hardkasVersion: "0.7.0-alpha",
|
|
2145
|
+
version: ARTIFACT_VERSION,
|
|
2146
|
+
networkId: "simnet",
|
|
2147
|
+
mode: "simulated",
|
|
2148
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2149
|
+
// hardkas-determinism-allow: mock timestamp
|
|
2150
|
+
planId: `plan-emoji-${emoji}-${ctx.caseSeed}`,
|
|
2151
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
2152
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
2153
|
+
amountSompi: "2000",
|
|
2154
|
+
estimatedFeeSompi: "100",
|
|
2155
|
+
estimatedMass: "10",
|
|
2156
|
+
inputs: [{ outpoint: { transactionId: "e".repeat(64), index: 0 }, amountSompi: "3000" }],
|
|
2157
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "2000" }]
|
|
2158
|
+
};
|
|
2159
|
+
const hash = calculateContentHash(payload);
|
|
2160
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
2161
|
+
const art = { ...payload, artifactId: artId, contentHash: hash };
|
|
2162
|
+
fs.writeFileSync(path.join(artifactsDir, `${artId}.json`), JSON.stringify(art, null, 2));
|
|
2163
|
+
const syncResult = await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2164
|
+
if (!syncResult.ok) {
|
|
2165
|
+
throw new TortureInvariantError(
|
|
2166
|
+
`Emoji "${emoji}" in content caused sync failure`,
|
|
2167
|
+
"EMOJI_SYNC_FAILURE",
|
|
2168
|
+
"critical"
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
const artifacts = await backend.findArtifacts();
|
|
2172
|
+
const found = artifacts.find((a) => a.artifactId === artId);
|
|
2173
|
+
if (!found) {
|
|
2174
|
+
throw new TortureInvariantError(
|
|
2175
|
+
"Emoji artifact not found after sync",
|
|
2176
|
+
"EMOJI_ARTIFACT_LOST",
|
|
2177
|
+
"critical"
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
break;
|
|
2181
|
+
}
|
|
2182
|
+
case 3: {
|
|
2183
|
+
const invisibleChars = [
|
|
2184
|
+
"\u200B",
|
|
2185
|
+
// zero-width space
|
|
2186
|
+
"\u200C",
|
|
2187
|
+
// zero-width non-joiner
|
|
2188
|
+
"\u200D",
|
|
2189
|
+
// zero-width joiner
|
|
2190
|
+
"\uFEFF",
|
|
2191
|
+
// BOM
|
|
2192
|
+
"\xAD"
|
|
2193
|
+
// soft hyphen
|
|
2194
|
+
];
|
|
2195
|
+
const invisible = ctx.prng.pick(invisibleChars);
|
|
2196
|
+
const payload = {
|
|
2197
|
+
schema: "hardkas.txPlan",
|
|
2198
|
+
hardkasVersion: "0.7.0-alpha",
|
|
2199
|
+
version: ARTIFACT_VERSION,
|
|
2200
|
+
networkId: "simnet",
|
|
2201
|
+
mode: "simulated",
|
|
2202
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2203
|
+
// hardkas-determinism-allow: mock timestamp
|
|
2204
|
+
planId: `plan-invisible${invisible}char-${ctx.caseSeed}`,
|
|
2205
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
2206
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
2207
|
+
amountSompi: "4000",
|
|
2208
|
+
estimatedFeeSompi: "100",
|
|
2209
|
+
estimatedMass: "10",
|
|
2210
|
+
inputs: [{ outpoint: { transactionId: "z".repeat(64), index: 0 }, amountSompi: "5000" }],
|
|
2211
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "4000" }]
|
|
2212
|
+
};
|
|
2213
|
+
const hash = calculateContentHash(payload);
|
|
2214
|
+
const payloadWithout = { ...payload, planId: `plan-invisiblechar-${ctx.caseSeed}` };
|
|
2215
|
+
const hashWithout = calculateContentHash(payloadWithout);
|
|
2216
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
2217
|
+
const art = { ...payload, artifactId: artId, contentHash: hash };
|
|
2218
|
+
fs.writeFileSync(path.join(artifactsDir, `${artId}.json`), JSON.stringify(art, null, 2));
|
|
2219
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2220
|
+
const artifacts = await backend.findArtifacts();
|
|
2221
|
+
const found = artifacts.find((a) => a.artifactId === artId);
|
|
2222
|
+
if (!found) {
|
|
2223
|
+
throw new TortureInvariantError(
|
|
2224
|
+
"Invisible Unicode artifact not indexed",
|
|
2225
|
+
"INVISIBLE_UNICODE_LOST",
|
|
2226
|
+
"critical"
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
break;
|
|
2230
|
+
}
|
|
2231
|
+
case 4: {
|
|
2232
|
+
const latinAddr = "kaspa:sim_address_with_a";
|
|
2233
|
+
const cyrillicAddr = "kaspa:sim_address_with_\u0430";
|
|
2234
|
+
const payloadLatin = {
|
|
2235
|
+
schema: "hardkas.txPlan",
|
|
2236
|
+
hardkasVersion: "0.7.0-alpha",
|
|
2237
|
+
version: ARTIFACT_VERSION,
|
|
2238
|
+
networkId: "simnet",
|
|
2239
|
+
mode: "simulated",
|
|
2240
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2241
|
+
// hardkas-determinism-allow: mock timestamp
|
|
2242
|
+
planId: `plan-homoglyph-latin-${ctx.caseSeed}`,
|
|
2243
|
+
from: { address: latinAddr },
|
|
2244
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
2245
|
+
amountSompi: "6000",
|
|
2246
|
+
estimatedFeeSompi: "100",
|
|
2247
|
+
estimatedMass: "10",
|
|
2248
|
+
inputs: [{ outpoint: { transactionId: "h".repeat(64), index: 0 }, amountSompi: "7000" }],
|
|
2249
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "6000" }]
|
|
2250
|
+
};
|
|
2251
|
+
const payloadCyrillic = { ...payloadLatin, planId: `plan-homoglyph-cyrillic-${ctx.caseSeed}`, from: { address: cyrillicAddr } };
|
|
2252
|
+
const hashLatin = calculateContentHash(payloadLatin);
|
|
2253
|
+
const hashCyrillic = calculateContentHash(payloadCyrillic);
|
|
2254
|
+
if (hashLatin === hashCyrillic) {
|
|
2255
|
+
throw new TortureInvariantError(
|
|
2256
|
+
"Homoglyph collision: Latin 'a' and Cyrillic '\u0430' produced identical hash",
|
|
2257
|
+
"HOMOGLYPH_HASH_COLLISION",
|
|
2258
|
+
"catastrophic"
|
|
2259
|
+
);
|
|
2260
|
+
}
|
|
2261
|
+
const art1 = { ...payloadLatin, artifactId: `plan-${hashLatin.slice(0, 16)}`, contentHash: hashLatin };
|
|
2262
|
+
const art2 = { ...payloadCyrillic, artifactId: `plan-${hashCyrillic.slice(0, 16)}`, contentHash: hashCyrillic };
|
|
2263
|
+
fs.writeFileSync(path.join(artifactsDir, `${art1.artifactId}.json`), JSON.stringify(art1, null, 2));
|
|
2264
|
+
fs.writeFileSync(path.join(artifactsDir, `${art2.artifactId}.json`), JSON.stringify(art2, null, 2));
|
|
2265
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2266
|
+
const artifacts = await backend.findArtifacts();
|
|
2267
|
+
if (artifacts.length < 2) {
|
|
2268
|
+
throw new TortureInvariantError(
|
|
2269
|
+
"Homoglyph artifacts collapsed: expected 2 distinct artifacts",
|
|
2270
|
+
"HOMOGLYPH_COLLAPSED",
|
|
2271
|
+
"critical"
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
2274
|
+
break;
|
|
2275
|
+
}
|
|
2276
|
+
case 5: {
|
|
2277
|
+
const mixedString = "caf\xE9 re\u0301sume\u0301";
|
|
2278
|
+
const payload = {
|
|
2279
|
+
schema: "hardkas.txPlan",
|
|
2280
|
+
hardkasVersion: "0.7.0-alpha",
|
|
2281
|
+
version: ARTIFACT_VERSION,
|
|
2282
|
+
networkId: "simnet",
|
|
2283
|
+
mode: "simulated",
|
|
2284
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2285
|
+
// hardkas-determinism-allow: mock timestamp
|
|
2286
|
+
planId: `plan-mixed-${mixedString}-${ctx.caseSeed}`,
|
|
2287
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
2288
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
2289
|
+
amountSompi: "8000",
|
|
2290
|
+
estimatedFeeSompi: "100",
|
|
2291
|
+
estimatedMass: "10",
|
|
2292
|
+
inputs: [{ outpoint: { transactionId: "n".repeat(64), index: 0 }, amountSompi: "9000" }],
|
|
2293
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "8000" }]
|
|
2294
|
+
};
|
|
2295
|
+
const allNFC = { ...payload, planId: `plan-mixed-${mixedString.normalize("NFC")}-${ctx.caseSeed}` };
|
|
2296
|
+
const hash = calculateContentHash(payload, CURRENT_HASH_VERSION);
|
|
2297
|
+
const hashNFC = calculateContentHash(allNFC, CURRENT_HASH_VERSION);
|
|
2298
|
+
if (hash !== hashNFC) {
|
|
2299
|
+
throw new TortureInvariantError(
|
|
2300
|
+
"Mixed NFC/NFD within same artifact did not normalize to identical hash",
|
|
2301
|
+
"MIXED_NORMALIZATION_DIVERGENCE",
|
|
2302
|
+
"critical"
|
|
2303
|
+
);
|
|
2304
|
+
}
|
|
2305
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
2306
|
+
const art = { ...payload, artifactId: artId, contentHash: hash };
|
|
2307
|
+
fs.writeFileSync(path.join(artifactsDir, `${artId}.json`), JSON.stringify(art, null, 2));
|
|
2308
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2309
|
+
break;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
store.disconnect();
|
|
2313
|
+
return {
|
|
2314
|
+
flow: `Unicode chaos subcase ${subCase}: ${["NFC vs NFD", "emoji content", "invisible Unicode", "homoglyphs", "mixed normalization"][subCase - 1]}`,
|
|
2315
|
+
mutation: `Unicode normalization edge case ${subCase}`,
|
|
2316
|
+
expectedInvariant: "unicode_normalization_never_changes_artifact_identity"
|
|
2317
|
+
};
|
|
2318
|
+
} finally {
|
|
2319
|
+
cleanupSandbox(sandboxDir);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
registerTortureBucket({
|
|
2324
|
+
name: "clock-skew-chaos",
|
|
2325
|
+
expectedInvariant: "clock_skew_cannot_break_replay_or_incremental_sync",
|
|
2326
|
+
async run(ctx) {
|
|
2327
|
+
const { sandboxDir, hardkasDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
2328
|
+
const subCase = ctx.prng.nextInt(1, 5);
|
|
2329
|
+
try {
|
|
2330
|
+
let writeArtifact2 = function(suffix, amount) {
|
|
2331
|
+
const payload = {
|
|
2332
|
+
schema: "hardkas.txPlan",
|
|
2333
|
+
hardkasVersion: "0.7.0-alpha",
|
|
2334
|
+
version: ARTIFACT_VERSION,
|
|
2335
|
+
networkId: "simnet",
|
|
2336
|
+
mode: "simulated",
|
|
2337
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2338
|
+
// hardkas-determinism-allow: mock timestamp
|
|
2339
|
+
planId: `plan-clock-${suffix}-${ctx.caseSeed}`,
|
|
2340
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
2341
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
2342
|
+
amountSompi: amount,
|
|
2343
|
+
estimatedFeeSompi: "100",
|
|
2344
|
+
estimatedMass: "10",
|
|
2345
|
+
inputs: [{ outpoint: { transactionId: "t".repeat(64), index: parseInt(suffix.replace(/\D/g, "") || "0") }, amountSompi: "20000" }],
|
|
2346
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: amount }]
|
|
2347
|
+
};
|
|
2348
|
+
const hash = calculateContentHash(payload);
|
|
2349
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
2350
|
+
const filePath = path.join(artifactsDir, `${artId}.json`);
|
|
2351
|
+
fs.writeFileSync(filePath, JSON.stringify({ ...payload, artifactId: artId, contentHash: hash }, null, 2));
|
|
2352
|
+
return filePath;
|
|
2353
|
+
};
|
|
2354
|
+
var writeArtifact = writeArtifact2;
|
|
2355
|
+
const store = new HardkasStore({ dbPath });
|
|
2356
|
+
store.connect({ autoMigrate: true });
|
|
2357
|
+
const backend = new SqliteQueryBackend(store);
|
|
2358
|
+
switch (subCase) {
|
|
2359
|
+
case 1: {
|
|
2360
|
+
const filePath = writeArtifact2("past", "1000");
|
|
2361
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2362
|
+
const pastTime = new Date(Date.now() - 365 * 24 * 60 * 60 * 1e3);
|
|
2363
|
+
fs.utimesSync(filePath, pastTime, pastTime);
|
|
2364
|
+
const newPayload = { schema: "hardkas.txPlan", changed: true, planId: "modified" };
|
|
2365
|
+
fs.writeFileSync(filePath, JSON.stringify(newPayload));
|
|
2366
|
+
fs.utimesSync(filePath, pastTime, pastTime);
|
|
2367
|
+
store.disconnect();
|
|
2368
|
+
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
|
|
2369
|
+
const newStore = new HardkasStore({ dbPath });
|
|
2370
|
+
newStore.connect({ autoMigrate: true });
|
|
2371
|
+
const newBackend = new SqliteQueryBackend(newStore);
|
|
2372
|
+
await newBackend.rebuild({ strict: false, cwd: sandboxDir });
|
|
2373
|
+
newStore.disconnect();
|
|
2374
|
+
break;
|
|
2375
|
+
}
|
|
2376
|
+
case 2: {
|
|
2377
|
+
const filePath = writeArtifact2("future", "2000");
|
|
2378
|
+
const futureTime = /* @__PURE__ */ new Date("2099-01-01T00:00:00Z");
|
|
2379
|
+
fs.utimesSync(filePath, futureTime, futureTime);
|
|
2380
|
+
const syncResult = await backend.sync({ strict: false, cwd: sandboxDir });
|
|
2381
|
+
const artifacts = await backend.findArtifacts();
|
|
2382
|
+
if (artifacts.length === 0) {
|
|
2383
|
+
throw new TortureInvariantError(
|
|
2384
|
+
"Future timestamp caused artifact to not be indexed",
|
|
2385
|
+
"FUTURE_MTIME_SKIP",
|
|
2386
|
+
"critical"
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
break;
|
|
2390
|
+
}
|
|
2391
|
+
case 3: {
|
|
2392
|
+
const fixedTime = /* @__PURE__ */ new Date("2024-06-15T12:00:00Z");
|
|
2393
|
+
const filePaths = [];
|
|
2394
|
+
for (let i = 0; i < 5; i++) {
|
|
2395
|
+
const fp = writeArtifact2(`identical-${i}`, `${(i + 1) * 1e3}`);
|
|
2396
|
+
fs.utimesSync(fp, fixedTime, fixedTime);
|
|
2397
|
+
filePaths.push(fp);
|
|
2398
|
+
}
|
|
2399
|
+
const syncResult = await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2400
|
+
const artifacts = await backend.findArtifacts();
|
|
2401
|
+
if (artifacts.length !== 5) {
|
|
2402
|
+
throw new TortureInvariantError(
|
|
2403
|
+
`Identical timestamps: expected 5 artifacts, got ${artifacts.length}`,
|
|
2404
|
+
"IDENTICAL_MTIME_LOSS",
|
|
2405
|
+
"critical"
|
|
2406
|
+
);
|
|
2407
|
+
}
|
|
2408
|
+
break;
|
|
2409
|
+
}
|
|
2410
|
+
case 4: {
|
|
2411
|
+
const filePath = writeArtifact2("epoch", "4000");
|
|
2412
|
+
const epochTime = /* @__PURE__ */ new Date(0);
|
|
2413
|
+
try {
|
|
2414
|
+
fs.utimesSync(filePath, epochTime, epochTime);
|
|
2415
|
+
} catch {
|
|
2416
|
+
}
|
|
2417
|
+
await backend.sync({ strict: false, cwd: sandboxDir });
|
|
2418
|
+
const artifacts = await backend.findArtifacts();
|
|
2419
|
+
if (artifacts.length === 0) {
|
|
2420
|
+
throw new TortureInvariantError(
|
|
2421
|
+
"Epoch-zero mtime caused artifact to not be indexed",
|
|
2422
|
+
"EPOCH_MTIME_SKIP",
|
|
2423
|
+
"critical"
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
break;
|
|
2427
|
+
}
|
|
2428
|
+
case 5: {
|
|
2429
|
+
const filePath = writeArtifact2("flip", "5000");
|
|
2430
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2431
|
+
const artifacts1 = await backend.findArtifacts();
|
|
2432
|
+
const count1 = artifacts1.length;
|
|
2433
|
+
const oldTime = /* @__PURE__ */ new Date("2020-01-01T00:00:00Z");
|
|
2434
|
+
fs.utimesSync(filePath, oldTime, oldTime);
|
|
2435
|
+
const newPayload = {
|
|
2436
|
+
schema: "hardkas.txPlan",
|
|
2437
|
+
hardkasVersion: "0.7.0-alpha",
|
|
2438
|
+
version: ARTIFACT_VERSION,
|
|
2439
|
+
networkId: "simnet",
|
|
2440
|
+
mode: "simulated",
|
|
2441
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2442
|
+
// hardkas-determinism-allow: mock timestamp
|
|
2443
|
+
planId: `plan-clock-flipped-${ctx.caseSeed}`,
|
|
2444
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
2445
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
2446
|
+
amountSompi: "99999",
|
|
2447
|
+
estimatedFeeSompi: "100",
|
|
2448
|
+
estimatedMass: "10",
|
|
2449
|
+
inputs: [{ outpoint: { transactionId: "f".repeat(64), index: 99 }, amountSompi: "100000" }],
|
|
2450
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "99999" }]
|
|
2451
|
+
};
|
|
2452
|
+
const newHash = calculateContentHash(newPayload);
|
|
2453
|
+
fs.writeFileSync(filePath, JSON.stringify({ ...newPayload, artifactId: `plan-${newHash.slice(0, 16)}`, contentHash: newHash }, null, 2));
|
|
2454
|
+
fs.utimesSync(filePath, oldTime, oldTime);
|
|
2455
|
+
store.disconnect();
|
|
2456
|
+
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
|
|
2457
|
+
const newStore = new HardkasStore({ dbPath });
|
|
2458
|
+
newStore.connect({ autoMigrate: true });
|
|
2459
|
+
const newBackend = new SqliteQueryBackend(newStore);
|
|
2460
|
+
await newBackend.rebuild({ strict: false, cwd: sandboxDir });
|
|
2461
|
+
newStore.disconnect();
|
|
2462
|
+
break;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
try {
|
|
2466
|
+
store.disconnect();
|
|
2467
|
+
} catch {
|
|
2468
|
+
}
|
|
2469
|
+
return {
|
|
2470
|
+
flow: `Clock skew subcase ${subCase}: ${["mtime backwards", "future timestamp", "identical mtimes", "epoch zero", "rapid mtime flip"][subCase - 1]}`,
|
|
2471
|
+
mutation: `Clock/mtime manipulation scenario ${subCase}`,
|
|
2472
|
+
expectedInvariant: "clock_skew_cannot_break_replay_or_incremental_sync"
|
|
2473
|
+
};
|
|
2474
|
+
} finally {
|
|
2475
|
+
cleanupSandbox(sandboxDir);
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
});
|
|
2479
|
+
registerTortureBucket({
|
|
2480
|
+
name: "filesystem-ordering-chaos",
|
|
2481
|
+
expectedInvariant: "filesystem_iteration_order_never_affects_semantic_result",
|
|
2482
|
+
async run(ctx) {
|
|
2483
|
+
const { sandboxDir, hardkasDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
2484
|
+
const subCase = ctx.prng.nextInt(1, 3);
|
|
2485
|
+
try {
|
|
2486
|
+
const artifactCount = ctx.prng.nextInt(8, 20);
|
|
2487
|
+
const payloads = [];
|
|
2488
|
+
for (let i = 0; i < artifactCount; i++) {
|
|
2489
|
+
const payload = {
|
|
2490
|
+
schema: "hardkas.txPlan",
|
|
2491
|
+
hardkasVersion: "0.7.0-alpha",
|
|
2492
|
+
version: ARTIFACT_VERSION,
|
|
2493
|
+
networkId: "simnet",
|
|
2494
|
+
mode: "simulated",
|
|
2495
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2496
|
+
// hardkas-determinism-allow: mock timestamp
|
|
2497
|
+
planId: `plan-order-${i}-${ctx.caseSeed}`,
|
|
2498
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
2499
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
2500
|
+
amountSompi: `${(i + 1) * 1e3}`,
|
|
2501
|
+
estimatedFeeSompi: "100",
|
|
2502
|
+
estimatedMass: "10",
|
|
2503
|
+
inputs: [{ outpoint: { transactionId: "o".repeat(64), index: i }, amountSompi: "20000" }],
|
|
2504
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: `${(i + 1) * 1e3}` }]
|
|
2505
|
+
};
|
|
2506
|
+
const hash = calculateContentHash(payload);
|
|
2507
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
2508
|
+
payloads.push({ id: artId, hash, payload });
|
|
2509
|
+
}
|
|
2510
|
+
const shuffled = ctx.prng.shuffle(payloads);
|
|
2511
|
+
for (const { id, hash, payload } of shuffled) {
|
|
2512
|
+
fs.writeFileSync(
|
|
2513
|
+
path.join(artifactsDir, `${id}.json`),
|
|
2514
|
+
JSON.stringify({ ...payload, artifactId: id, contentHash: hash }, null, 2)
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2517
|
+
switch (subCase) {
|
|
2518
|
+
case 1: {
|
|
2519
|
+
const store1 = new HardkasStore({ dbPath });
|
|
2520
|
+
store1.connect({ autoMigrate: true });
|
|
2521
|
+
const backend1 = new SqliteQueryBackend(store1);
|
|
2522
|
+
await backend1.sync({ strict: true, cwd: sandboxDir });
|
|
2523
|
+
const artifacts1 = await backend1.findArtifacts();
|
|
2524
|
+
const ids1 = artifacts1.map((a) => a.artifactId).sort();
|
|
2525
|
+
const hashes1 = artifacts1.map((a) => a.contentHash).sort();
|
|
2526
|
+
store1.disconnect();
|
|
2527
|
+
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
|
|
2528
|
+
const store2 = new HardkasStore({ dbPath });
|
|
2529
|
+
store2.connect({ autoMigrate: true });
|
|
2530
|
+
const backend2 = new SqliteQueryBackend(store2);
|
|
2531
|
+
await backend2.rebuild({ strict: true, cwd: sandboxDir });
|
|
2532
|
+
const artifacts2 = await backend2.findArtifacts();
|
|
2533
|
+
const ids2 = artifacts2.map((a) => a.artifactId).sort();
|
|
2534
|
+
const hashes2 = artifacts2.map((a) => a.contentHash).sort();
|
|
2535
|
+
store2.disconnect();
|
|
2536
|
+
if (JSON.stringify(ids1) !== JSON.stringify(ids2)) {
|
|
2537
|
+
throw new TortureInvariantError(
|
|
2538
|
+
`Ordering divergence: sync produced ${ids1.length} IDs, rebuild produced ${ids2.length}`,
|
|
2539
|
+
"ORDERING_ID_DIVERGENCE",
|
|
2540
|
+
"critical"
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
if (JSON.stringify(hashes1) !== JSON.stringify(hashes2)) {
|
|
2544
|
+
throw new TortureInvariantError(
|
|
2545
|
+
"Ordering divergence: sync and rebuild produced different hash sets",
|
|
2546
|
+
"ORDERING_HASH_DIVERGENCE",
|
|
2547
|
+
"critical"
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
break;
|
|
2551
|
+
}
|
|
2552
|
+
case 2: {
|
|
2553
|
+
const store = new HardkasStore({ dbPath });
|
|
2554
|
+
store.connect({ autoMigrate: true });
|
|
2555
|
+
const backend = new SqliteQueryBackend(store);
|
|
2556
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2557
|
+
const count1 = (await backend.findArtifacts()).length;
|
|
2558
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2559
|
+
const count2 = (await backend.findArtifacts()).length;
|
|
2560
|
+
if (count1 !== count2) {
|
|
2561
|
+
throw new TortureInvariantError(
|
|
2562
|
+
`Double sync changed count: ${count1} \u2192 ${count2}`,
|
|
2563
|
+
"DOUBLE_SYNC_INSTABILITY",
|
|
2564
|
+
"critical"
|
|
2565
|
+
);
|
|
2566
|
+
}
|
|
2567
|
+
store.disconnect();
|
|
2568
|
+
break;
|
|
2569
|
+
}
|
|
2570
|
+
case 3: {
|
|
2571
|
+
const store = new HardkasStore({ dbPath });
|
|
2572
|
+
store.connect({ autoMigrate: true });
|
|
2573
|
+
const backend = new SqliteQueryBackend(store);
|
|
2574
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2575
|
+
const initialCount = (await backend.findArtifacts()).length;
|
|
2576
|
+
const extraCount = ctx.prng.nextInt(3, 8);
|
|
2577
|
+
for (let i = 0; i < extraCount; i++) {
|
|
2578
|
+
const payload = {
|
|
2579
|
+
schema: "hardkas.txPlan",
|
|
2580
|
+
hardkasVersion: "0.7.0-alpha",
|
|
2581
|
+
version: ARTIFACT_VERSION,
|
|
2582
|
+
networkId: "simnet",
|
|
2583
|
+
mode: "simulated",
|
|
2584
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2585
|
+
// hardkas-determinism-allow: mock timestamp
|
|
2586
|
+
planId: `plan-extra-${i}-${ctx.caseSeed}`,
|
|
2587
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
2588
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
2589
|
+
amountSompi: `${(i + 100) * 1e3}`,
|
|
2590
|
+
estimatedFeeSompi: "100",
|
|
2591
|
+
estimatedMass: "10",
|
|
2592
|
+
inputs: [{ outpoint: { transactionId: "x".repeat(64), index: i + 100 }, amountSompi: "200000" }],
|
|
2593
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: `${(i + 100) * 1e3}` }]
|
|
2594
|
+
};
|
|
2595
|
+
const hash = calculateContentHash(payload);
|
|
2596
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
2597
|
+
fs.writeFileSync(
|
|
2598
|
+
path.join(artifactsDir, `${artId}.json`),
|
|
2599
|
+
JSON.stringify({ ...payload, artifactId: artId, contentHash: hash }, null, 2)
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2603
|
+
const finalCount = (await backend.findArtifacts()).length;
|
|
2604
|
+
if (finalCount < initialCount + extraCount) {
|
|
2605
|
+
throw new TortureInvariantError(
|
|
2606
|
+
`Mutation during scan: expected >= ${initialCount + extraCount}, got ${finalCount}`,
|
|
2607
|
+
"SCAN_MUTATION_LOSS",
|
|
2608
|
+
"critical"
|
|
2609
|
+
);
|
|
2610
|
+
}
|
|
2611
|
+
store.disconnect();
|
|
2612
|
+
break;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
return {
|
|
2616
|
+
flow: `Filesystem ordering subcase ${subCase}: ${["shuffled sync vs rebuild", "double sync idempotency", "mutation during scan"][subCase - 1]}`,
|
|
2617
|
+
mutation: `${artifactCount} artifacts in shuffled order, scenario ${subCase}`,
|
|
2618
|
+
expectedInvariant: "filesystem_iteration_order_never_affects_semantic_result"
|
|
2619
|
+
};
|
|
2620
|
+
} finally {
|
|
2621
|
+
cleanupSandbox(sandboxDir);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
});
|
|
2625
|
+
registerTortureBucket({
|
|
2626
|
+
name: "human-chaos",
|
|
2627
|
+
expectedInvariant: "manual_user_interference_cannot_silently_corrupt_truth",
|
|
2628
|
+
async run(ctx) {
|
|
2629
|
+
const { sandboxDir, hardkasDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
2630
|
+
const subCase = ctx.prng.nextInt(1, 5);
|
|
2631
|
+
try {
|
|
2632
|
+
const artifactIds = [];
|
|
2633
|
+
for (let i = 0; i < 5; i++) {
|
|
2634
|
+
const payload = {
|
|
2635
|
+
schema: "hardkas.txPlan",
|
|
2636
|
+
hardkasVersion: "0.7.0-alpha",
|
|
2637
|
+
version: ARTIFACT_VERSION,
|
|
2638
|
+
networkId: "simnet",
|
|
2639
|
+
mode: "simulated",
|
|
2640
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2641
|
+
// hardkas-determinism-allow: mock timestamp
|
|
2642
|
+
planId: `plan-human-${i}-${ctx.caseSeed}`,
|
|
2643
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
2644
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
2645
|
+
amountSompi: `${(i + 1) * 1e3}`,
|
|
2646
|
+
estimatedFeeSompi: "100",
|
|
2647
|
+
estimatedMass: "10",
|
|
2648
|
+
inputs: [{ outpoint: { transactionId: "H".repeat(64), index: i }, amountSompi: "20000" }],
|
|
2649
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: `${(i + 1) * 1e3}` }]
|
|
2650
|
+
};
|
|
2651
|
+
const hash = calculateContentHash(payload);
|
|
2652
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
2653
|
+
artifactIds.push(artId);
|
|
2654
|
+
fs.writeFileSync(
|
|
2655
|
+
path.join(artifactsDir, `${artId}.json`),
|
|
2656
|
+
JSON.stringify({ ...payload, artifactId: artId, contentHash: hash }, null, 2)
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2659
|
+
const store = new HardkasStore({ dbPath });
|
|
2660
|
+
store.connect({ autoMigrate: true });
|
|
2661
|
+
const backend = new SqliteQueryBackend(store);
|
|
2662
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2663
|
+
switch (subCase) {
|
|
2664
|
+
case 1: {
|
|
2665
|
+
const targetFile = path.join(artifactsDir, `${artifactIds[0]}.json`);
|
|
2666
|
+
const original = JSON.parse(fs.readFileSync(targetFile, "utf-8"));
|
|
2667
|
+
original.amountSompi = "999999";
|
|
2668
|
+
fs.writeFileSync(targetFile, JSON.stringify(original, null, 2));
|
|
2669
|
+
const result = await backend.sync({ strict: false, cwd: sandboxDir });
|
|
2670
|
+
break;
|
|
2671
|
+
}
|
|
2672
|
+
case 2: {
|
|
2673
|
+
const targetFile = path.join(artifactsDir, `${artifactIds[0]}.json`);
|
|
2674
|
+
fs.unlinkSync(targetFile);
|
|
2675
|
+
store.disconnect();
|
|
2676
|
+
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
|
|
2677
|
+
const newStore = new HardkasStore({ dbPath });
|
|
2678
|
+
newStore.connect({ autoMigrate: true });
|
|
2679
|
+
const newBackend = new SqliteQueryBackend(newStore);
|
|
2680
|
+
await newBackend.rebuild({ strict: false, cwd: sandboxDir });
|
|
2681
|
+
const artifacts = await newBackend.findArtifacts();
|
|
2682
|
+
const foundDeleted = artifacts.find((a) => a.artifactId === artifactIds[0]);
|
|
2683
|
+
if (foundDeleted) {
|
|
2684
|
+
throw new TortureInvariantError(
|
|
2685
|
+
"Deleted artifact still appears in rebuilt index",
|
|
2686
|
+
"DELETED_ROOT_PHANTOM",
|
|
2687
|
+
"critical"
|
|
2688
|
+
);
|
|
2689
|
+
}
|
|
2690
|
+
newStore.disconnect();
|
|
2691
|
+
break;
|
|
2692
|
+
}
|
|
2693
|
+
case 3: {
|
|
2694
|
+
const targetFile = path.join(artifactsDir, `${artifactIds[1]}.json`);
|
|
2695
|
+
const original = fs.readFileSync(targetFile, "utf-8");
|
|
2696
|
+
fs.writeFileSync(targetFile, original.slice(0, 30));
|
|
2697
|
+
const result = await backend.sync({ strict: false, cwd: sandboxDir });
|
|
2698
|
+
const artifacts = await backend.findArtifacts();
|
|
2699
|
+
const nonCorrupted = artifacts.filter((a) => a.kind !== "CORRUPTED");
|
|
2700
|
+
if (nonCorrupted.length < 3) {
|
|
2701
|
+
throw new TortureInvariantError(
|
|
2702
|
+
`Truncation caused collateral damage: only ${nonCorrupted.length} non-corrupted of 5`,
|
|
2703
|
+
"TRUNCATION_COLLATERAL",
|
|
2704
|
+
"critical"
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
break;
|
|
2708
|
+
}
|
|
2709
|
+
case 4: {
|
|
2710
|
+
const eventsPath = path.join(hardkasDir, "events.jsonl");
|
|
2711
|
+
fs.writeFileSync(eventsPath, "{{GARBAGE}}\n{also invalid}\nNOT JSON AT ALL\n", "utf-8");
|
|
2712
|
+
store.disconnect();
|
|
2713
|
+
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
|
|
2714
|
+
const newStore = new HardkasStore({ dbPath });
|
|
2715
|
+
newStore.connect({ autoMigrate: true });
|
|
2716
|
+
const newBackend = new SqliteQueryBackend(newStore);
|
|
2717
|
+
await newBackend.rebuild({ strict: false, cwd: sandboxDir });
|
|
2718
|
+
const artifacts = await newBackend.findArtifacts();
|
|
2719
|
+
if (artifacts.length !== 5) {
|
|
2720
|
+
throw new TortureInvariantError(
|
|
2721
|
+
`Events corruption affected artifact count: expected 5, got ${artifacts.length}`,
|
|
2722
|
+
"EVENTS_CORRUPTION_AFFECTED_ARTIFACTS",
|
|
2723
|
+
"critical"
|
|
2724
|
+
);
|
|
2725
|
+
}
|
|
2726
|
+
newStore.disconnect();
|
|
2727
|
+
break;
|
|
2728
|
+
}
|
|
2729
|
+
case 5: {
|
|
2730
|
+
const phantomPayload = {
|
|
2731
|
+
schema: "hardkas.txPlan",
|
|
2732
|
+
hardkasVersion: "0.7.0-alpha",
|
|
2733
|
+
version: ARTIFACT_VERSION,
|
|
2734
|
+
networkId: "simnet",
|
|
2735
|
+
mode: "simulated",
|
|
2736
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2737
|
+
// hardkas-determinism-allow: mock timestamp
|
|
2738
|
+
planId: `plan-phantom-${ctx.caseSeed}`,
|
|
2739
|
+
artifactId: "phantom-fake-id-12345",
|
|
2740
|
+
contentHash: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
2741
|
+
// WRONG hash
|
|
2742
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
2743
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
2744
|
+
amountSompi: "666",
|
|
2745
|
+
estimatedFeeSompi: "100",
|
|
2746
|
+
estimatedMass: "10",
|
|
2747
|
+
inputs: [{ outpoint: { transactionId: "P".repeat(64), index: 0 }, amountSompi: "1000" }],
|
|
2748
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "666" }]
|
|
2749
|
+
};
|
|
2750
|
+
fs.writeFileSync(
|
|
2751
|
+
path.join(artifactsDir, "phantom-fake-id-12345.json"),
|
|
2752
|
+
JSON.stringify(phantomPayload, null, 2)
|
|
2753
|
+
);
|
|
2754
|
+
const result = await backend.sync({ strict: false, cwd: sandboxDir });
|
|
2755
|
+
const artifacts = await backend.findArtifacts();
|
|
2756
|
+
const phantom = artifacts.find((a) => a.artifactId === "phantom-fake-id-12345");
|
|
2757
|
+
if (phantom && phantom.contentHash !== "MISMATCH" && phantom.kind !== "CORRUPTED") {
|
|
2758
|
+
throw new TortureInvariantError(
|
|
2759
|
+
"Phantom artifact with wrong hash was accepted as canonical without corruption flag",
|
|
2760
|
+
"PHANTOM_ACCEPTED_AS_CANONICAL",
|
|
2761
|
+
"catastrophic"
|
|
2762
|
+
);
|
|
2763
|
+
}
|
|
2764
|
+
break;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
try {
|
|
2768
|
+
store.disconnect();
|
|
2769
|
+
} catch {
|
|
2770
|
+
}
|
|
2771
|
+
return {
|
|
2772
|
+
flow: `Human chaos subcase ${subCase}: ${["edit during sync", "delete root", "truncate artifact", "corrupt events.jsonl", "phantom artifact"][subCase - 1]}`,
|
|
2773
|
+
mutation: `Manual interference scenario ${subCase}`,
|
|
2774
|
+
expectedInvariant: "manual_user_interference_cannot_silently_corrupt_truth"
|
|
2775
|
+
};
|
|
2776
|
+
} finally {
|
|
2777
|
+
cleanupSandbox(sandboxDir);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
});
|
|
2781
|
+
registerTortureBucket({
|
|
2782
|
+
name: "antivirus-external-mutation",
|
|
2783
|
+
expectedInvariant: "external_file_interference_never_creates_false_canonicality",
|
|
2784
|
+
async run(ctx) {
|
|
2785
|
+
const { sandboxDir, hardkasDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
2786
|
+
const subCase = ctx.prng.nextInt(1, 5);
|
|
2787
|
+
try {
|
|
2788
|
+
let writePlan2 = function(id, amount) {
|
|
2789
|
+
const payload = {
|
|
2790
|
+
schema: "hardkas.txPlan",
|
|
2791
|
+
hardkasVersion: "0.7.0-alpha",
|
|
2792
|
+
version: ARTIFACT_VERSION,
|
|
2793
|
+
networkId: "simnet",
|
|
2794
|
+
mode: "simulated",
|
|
2795
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2796
|
+
// hardkas-determinism-allow: mock timestamp
|
|
2797
|
+
planId: id,
|
|
2798
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
2799
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
2800
|
+
amountSompi: amount,
|
|
2801
|
+
estimatedFeeSompi: "100",
|
|
2802
|
+
estimatedMass: "10",
|
|
2803
|
+
inputs: [{ outpoint: { transactionId: "a".repeat(64), index: 0 }, amountSompi: "10000" }],
|
|
2804
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: amount }]
|
|
2805
|
+
};
|
|
2806
|
+
const hash = calculateContentHash(payload);
|
|
2807
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
2808
|
+
const file = path.join(artifactsDir, `${artId}.json`);
|
|
2809
|
+
fs.writeFileSync(file, JSON.stringify({ ...payload, artifactId: artId, contentHash: hash }, null, 2));
|
|
2810
|
+
return { artId, file, payload };
|
|
2811
|
+
};
|
|
2812
|
+
var writePlan = writePlan2;
|
|
2813
|
+
const store = new HardkasStore({ dbPath });
|
|
2814
|
+
store.connect({ autoMigrate: true });
|
|
2815
|
+
const backend = new SqliteQueryBackend(store);
|
|
2816
|
+
switch (subCase) {
|
|
2817
|
+
case 1: {
|
|
2818
|
+
const files = [];
|
|
2819
|
+
for (let i = 0; i < 5; i++) {
|
|
2820
|
+
const { file } = writePlan2(`plan-av-${i}-${ctx.caseSeed}`, `${(i + 1) * 1e3}`);
|
|
2821
|
+
files.push(file);
|
|
2822
|
+
}
|
|
2823
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2824
|
+
if (files[0] && files[1]) {
|
|
2825
|
+
fs.unlinkSync(files[0]);
|
|
2826
|
+
fs.unlinkSync(files[1]);
|
|
2827
|
+
}
|
|
2828
|
+
const report = await backend.doctor();
|
|
2829
|
+
if (report.ok) {
|
|
2830
|
+
throw new TortureInvariantError(
|
|
2831
|
+
"Antivirus quarantine (file deletion) was not detected by doctor",
|
|
2832
|
+
"QUARANTINE_NOT_DETECTED",
|
|
2833
|
+
"critical"
|
|
2834
|
+
);
|
|
2835
|
+
}
|
|
2836
|
+
if (report.zombieArtifacts !== 2) {
|
|
2837
|
+
throw new TortureInvariantError(
|
|
2838
|
+
`Expected 2 zombie artifacts, got ${report.zombieArtifacts}`,
|
|
2839
|
+
"ZOMBIE_COUNT_MISMATCH",
|
|
2840
|
+
"critical"
|
|
2841
|
+
);
|
|
2842
|
+
}
|
|
2843
|
+
break;
|
|
2844
|
+
}
|
|
2845
|
+
case 2: {
|
|
2846
|
+
const { file } = writePlan2(`plan-locked-${ctx.caseSeed}`, "5000");
|
|
2847
|
+
let fd = null;
|
|
2848
|
+
try {
|
|
2849
|
+
fd = fs.openSync(file, "r+");
|
|
2850
|
+
await backend.sync({ strict: false, cwd: sandboxDir });
|
|
2851
|
+
} finally {
|
|
2852
|
+
if (fd !== null) {
|
|
2853
|
+
try {
|
|
2854
|
+
fs.closeSync(fd);
|
|
2855
|
+
} catch {
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
const finalResult = await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2860
|
+
if (!finalResult.ok) {
|
|
2861
|
+
throw new TortureInvariantError(
|
|
2862
|
+
"Post-lock sync failed",
|
|
2863
|
+
"POST_LOCK_SYNC_FAILED",
|
|
2864
|
+
"critical"
|
|
2865
|
+
);
|
|
2866
|
+
}
|
|
2867
|
+
break;
|
|
2868
|
+
}
|
|
2869
|
+
case 3: {
|
|
2870
|
+
const { artId, file, payload } = writePlan2(`plan-corrupt-${ctx.caseSeed}`, "9000");
|
|
2871
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2872
|
+
const corrupted = { ...payload, artifactId: artId, contentHash: "12345fakehash", amountSompi: "99999" };
|
|
2873
|
+
fs.writeFileSync(file, JSON.stringify(corrupted, null, 2));
|
|
2874
|
+
const report = await backend.doctor();
|
|
2875
|
+
if (report.ok) {
|
|
2876
|
+
throw new TortureInvariantError(
|
|
2877
|
+
"External rewrite was not detected by doctor",
|
|
2878
|
+
"EXTERNAL_REWRITE_NOT_DETECTED",
|
|
2879
|
+
"critical"
|
|
2880
|
+
);
|
|
2881
|
+
}
|
|
2882
|
+
break;
|
|
2883
|
+
}
|
|
2884
|
+
case 4: {
|
|
2885
|
+
const { artId, file } = writePlan2(`plan-delayed-1-${ctx.caseSeed}`, "2000");
|
|
2886
|
+
const { file: file2 } = writePlan2(`plan-delayed-2-${ctx.caseSeed}`, "4000");
|
|
2887
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2888
|
+
const count1 = (await backend.findArtifacts()).length;
|
|
2889
|
+
fs.unlinkSync(file);
|
|
2890
|
+
await backend.sync({ strict: false, cwd: sandboxDir });
|
|
2891
|
+
const count2 = (await backend.findArtifacts()).length;
|
|
2892
|
+
if (count2 !== count1 - 1) {
|
|
2893
|
+
throw new TortureInvariantError(
|
|
2894
|
+
`Delayed delete did not decrement artifact count: count1=${count1}, count2=${count2}`,
|
|
2895
|
+
"DELAYED_DELETE_COUNT_MISMATCH",
|
|
2896
|
+
"critical"
|
|
2897
|
+
);
|
|
2898
|
+
}
|
|
2899
|
+
break;
|
|
2900
|
+
}
|
|
2901
|
+
case 5: {
|
|
2902
|
+
const { file, payload } = writePlan2(`plan-rename-${ctx.caseSeed}`, "7500");
|
|
2903
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
2904
|
+
const count1 = (await backend.findArtifacts()).length;
|
|
2905
|
+
const newPath = path.join(artifactsDir, `plan-renamed-${ctx.caseSeed}.json`);
|
|
2906
|
+
fs.renameSync(file, newPath);
|
|
2907
|
+
await backend.sync({ strict: false, cwd: sandboxDir });
|
|
2908
|
+
const artifacts = await backend.findArtifacts();
|
|
2909
|
+
const foundOld = artifacts.find((a) => a.path === file);
|
|
2910
|
+
if (foundOld) {
|
|
2911
|
+
throw new TortureInvariantError(
|
|
2912
|
+
"Renamed file's old entry was not pruned",
|
|
2913
|
+
"OLD_PATH_NOT_PRUNED",
|
|
2914
|
+
"critical"
|
|
2915
|
+
);
|
|
2916
|
+
}
|
|
2917
|
+
break;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
store.disconnect();
|
|
2921
|
+
return {
|
|
2922
|
+
flow: `Antivirus external mutation subcase ${subCase}`,
|
|
2923
|
+
mutation: `External file modification subcase ${subCase}`,
|
|
2924
|
+
expectedInvariant: "external_file_interference_never_creates_false_canonicality"
|
|
2925
|
+
};
|
|
2926
|
+
} finally {
|
|
2927
|
+
cleanupSandbox(sandboxDir);
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
});
|
|
2931
|
+
registerTortureBucket({
|
|
2932
|
+
name: "symlink-nightmare",
|
|
2933
|
+
expectedInvariant: "symlink_resolution_cannot_escape_canonical_boundaries",
|
|
2934
|
+
async run(ctx) {
|
|
2935
|
+
const { sandboxDir, hardkasDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
2936
|
+
const subCase = ctx.prng.nextInt(1, 5);
|
|
2937
|
+
const outsideDir = path.join(ctx.workspaceDir, ".tmp", `torture-outside-${ctx.caseId}-${ctx.caseSeed}`);
|
|
2938
|
+
fs.mkdirSync(outsideDir, { recursive: true });
|
|
2939
|
+
const outsideFile = path.join(outsideDir, "outside-artifact.json");
|
|
2940
|
+
const outsidePayload = {
|
|
2941
|
+
schema: "hardkas.txPlan",
|
|
2942
|
+
hardkasVersion: "0.7.0-alpha",
|
|
2943
|
+
version: ARTIFACT_VERSION,
|
|
2944
|
+
networkId: "simnet",
|
|
2945
|
+
mode: "simulated",
|
|
2946
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2947
|
+
// hardkas-determinism-allow: mock timestamp
|
|
2948
|
+
planId: `plan-outside-${ctx.caseSeed}`,
|
|
2949
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
2950
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
2951
|
+
amountSompi: "500000",
|
|
2952
|
+
estimatedFeeSompi: "100",
|
|
2953
|
+
estimatedMass: "10",
|
|
2954
|
+
inputs: [{ outpoint: { transactionId: "o".repeat(64), index: 0 }, amountSompi: "600000" }],
|
|
2955
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "500000" }]
|
|
2956
|
+
};
|
|
2957
|
+
const outsideHash = calculateContentHash(outsidePayload);
|
|
2958
|
+
fs.writeFileSync(outsideFile, JSON.stringify({ ...outsidePayload, artifactId: `plan-${outsideHash.slice(0, 16)}`, contentHash: outsideHash }, null, 2));
|
|
2959
|
+
try {
|
|
2960
|
+
let writePlan2 = function(id, amount) {
|
|
2961
|
+
const payload = {
|
|
2962
|
+
schema: "hardkas.txPlan",
|
|
2963
|
+
hardkasVersion: "0.7.0-alpha",
|
|
2964
|
+
version: ARTIFACT_VERSION,
|
|
2965
|
+
networkId: "simnet",
|
|
2966
|
+
mode: "simulated",
|
|
2967
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2968
|
+
// hardkas-determinism-allow: mock timestamp
|
|
2969
|
+
planId: id,
|
|
2970
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
2971
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
2972
|
+
amountSompi: amount,
|
|
2973
|
+
estimatedFeeSompi: "100",
|
|
2974
|
+
estimatedMass: "10",
|
|
2975
|
+
inputs: [{ outpoint: { transactionId: "s".repeat(64), index: 0 }, amountSompi: "10000" }],
|
|
2976
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: amount }]
|
|
2977
|
+
};
|
|
2978
|
+
const hash = calculateContentHash(payload);
|
|
2979
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
2980
|
+
const file = path.join(artifactsDir, `${artId}.json`);
|
|
2981
|
+
fs.writeFileSync(file, JSON.stringify({ ...payload, artifactId: artId, contentHash: hash }, null, 2));
|
|
2982
|
+
return { artId, file };
|
|
2983
|
+
};
|
|
2984
|
+
var writePlan = writePlan2;
|
|
2985
|
+
const store = new HardkasStore({ dbPath });
|
|
2986
|
+
store.connect({ autoMigrate: true });
|
|
2987
|
+
const backend = new SqliteQueryBackend(store);
|
|
2988
|
+
let symlinksSupported = false;
|
|
2989
|
+
let junctionsSupported = false;
|
|
2990
|
+
let modeStr = "SIMULATED_SYMLINK_MODE";
|
|
2991
|
+
const testTarget = path.join(sandboxDir, "test-target.json");
|
|
2992
|
+
const testLink = path.join(sandboxDir, "test-link.json");
|
|
2993
|
+
fs.writeFileSync(testTarget, "{}");
|
|
2994
|
+
try {
|
|
2995
|
+
fs.symlinkSync(testTarget, testLink);
|
|
2996
|
+
symlinksSupported = true;
|
|
2997
|
+
modeStr = "REAL_SYMLINK_MODE";
|
|
2998
|
+
fs.unlinkSync(testLink);
|
|
2999
|
+
} catch {
|
|
3000
|
+
}
|
|
3001
|
+
try {
|
|
3002
|
+
const testDirTarget = path.join(sandboxDir, "test-dir-target");
|
|
3003
|
+
const testDirLink = path.join(sandboxDir, "test-dir-link");
|
|
3004
|
+
fs.mkdirSync(testDirTarget, { recursive: true });
|
|
3005
|
+
fs.symlinkSync(testDirTarget, testDirLink, "junction");
|
|
3006
|
+
junctionsSupported = true;
|
|
3007
|
+
fs.unlinkSync(testDirLink);
|
|
3008
|
+
fs.rmdirSync(testDirTarget);
|
|
3009
|
+
} catch {
|
|
3010
|
+
}
|
|
3011
|
+
fs.unlinkSync(testTarget);
|
|
3012
|
+
switch (subCase) {
|
|
3013
|
+
case 1: {
|
|
3014
|
+
if (symlinksSupported) {
|
|
3015
|
+
const linkPath = path.join(artifactsDir, "outside-link.json");
|
|
3016
|
+
fs.symlinkSync(outsideFile, linkPath);
|
|
3017
|
+
await backend.sync({ strict: false, cwd: sandboxDir });
|
|
3018
|
+
const artifacts = await backend.findArtifacts();
|
|
3019
|
+
const foundOutside = artifacts.find((a) => a.payload.planId === `plan-outside-${ctx.caseSeed}`);
|
|
3020
|
+
if (foundOutside) {
|
|
3021
|
+
throw new TortureInvariantError(
|
|
3022
|
+
"Indexed artifact from a symlink pointing outside the workspace boundary",
|
|
3023
|
+
"OUTSIDE_WORKSPACE_ESCAPE",
|
|
3024
|
+
"catastrophic"
|
|
3025
|
+
);
|
|
3026
|
+
}
|
|
3027
|
+
} else {
|
|
3028
|
+
const { file } = writePlan2(`plan-sim-outside-${ctx.caseSeed}`, "1000");
|
|
3029
|
+
const relativePath = path.relative(sandboxDir, file);
|
|
3030
|
+
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
3031
|
+
throw new TortureInvariantError(
|
|
3032
|
+
"Absolute or escaping path simulated boundary escape",
|
|
3033
|
+
"OUTSIDE_WORKSPACE_ESCAPE",
|
|
3034
|
+
"catastrophic"
|
|
3035
|
+
);
|
|
3036
|
+
}
|
|
3037
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3038
|
+
}
|
|
3039
|
+
break;
|
|
3040
|
+
}
|
|
3041
|
+
case 2: {
|
|
3042
|
+
if (symlinksSupported) {
|
|
3043
|
+
const { file } = writePlan2(`plan-nested-${ctx.caseSeed}`, "2000");
|
|
3044
|
+
const link1 = path.join(artifactsDir, "link1.json");
|
|
3045
|
+
const link2 = path.join(artifactsDir, "link2.json");
|
|
3046
|
+
fs.symlinkSync(file, link1);
|
|
3047
|
+
fs.symlinkSync(link1, link2);
|
|
3048
|
+
await backend.sync({ strict: false, cwd: sandboxDir });
|
|
3049
|
+
} else {
|
|
3050
|
+
let currentDir = artifactsDir;
|
|
3051
|
+
for (let i = 0; i < 5; i++) {
|
|
3052
|
+
currentDir = path.join(currentDir, `sub-${i}`);
|
|
3053
|
+
fs.mkdirSync(currentDir, { recursive: true });
|
|
3054
|
+
}
|
|
3055
|
+
const payload = {
|
|
3056
|
+
schema: "hardkas.txPlan",
|
|
3057
|
+
hardkasVersion: "0.7.0-alpha",
|
|
3058
|
+
version: ARTIFACT_VERSION,
|
|
3059
|
+
networkId: "simnet",
|
|
3060
|
+
mode: "simulated",
|
|
3061
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3062
|
+
// hardkas-determinism-allow: mock timestamp
|
|
3063
|
+
planId: `plan-deep-${ctx.caseSeed}`,
|
|
3064
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
3065
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
3066
|
+
amountSompi: "3000",
|
|
3067
|
+
estimatedFeeSompi: "100",
|
|
3068
|
+
estimatedMass: "10",
|
|
3069
|
+
inputs: [{ outpoint: { transactionId: "d".repeat(64), index: 0 }, amountSompi: "5000" }],
|
|
3070
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "3000" }]
|
|
3071
|
+
};
|
|
3072
|
+
const hash = calculateContentHash(payload);
|
|
3073
|
+
fs.writeFileSync(
|
|
3074
|
+
path.join(currentDir, `plan-deep-${hash.slice(0, 8)}.json`),
|
|
3075
|
+
JSON.stringify({ ...payload, artifactId: `plan-${hash.slice(0, 16)}`, contentHash: hash }, null, 2)
|
|
3076
|
+
);
|
|
3077
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3078
|
+
}
|
|
3079
|
+
break;
|
|
3080
|
+
}
|
|
3081
|
+
case 3: {
|
|
3082
|
+
if (symlinksSupported) {
|
|
3083
|
+
const linkA = path.join(artifactsDir, "circular-a.json");
|
|
3084
|
+
const linkB = path.join(artifactsDir, "circular-b.json");
|
|
3085
|
+
fs.symlinkSync(linkB, linkA);
|
|
3086
|
+
try {
|
|
3087
|
+
fs.symlinkSync(linkA, linkB);
|
|
3088
|
+
} catch {
|
|
3089
|
+
}
|
|
3090
|
+
try {
|
|
3091
|
+
await backend.sync({ strict: false, cwd: sandboxDir });
|
|
3092
|
+
} catch (err) {
|
|
3093
|
+
}
|
|
3094
|
+
} else {
|
|
3095
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3096
|
+
}
|
|
3097
|
+
break;
|
|
3098
|
+
}
|
|
3099
|
+
case 4: {
|
|
3100
|
+
if (junctionsSupported) {
|
|
3101
|
+
const linkPath = path.join(artifactsDir, "outside-junction");
|
|
3102
|
+
fs.symlinkSync(outsideDir, linkPath, "junction");
|
|
3103
|
+
await backend.sync({ strict: false, cwd: sandboxDir });
|
|
3104
|
+
const artifacts = await backend.findArtifacts();
|
|
3105
|
+
const foundOutside = artifacts.find((a) => a.payload.planId === `plan-outside-${ctx.caseSeed}`);
|
|
3106
|
+
if (foundOutside) {
|
|
3107
|
+
throw new TortureInvariantError(
|
|
3108
|
+
"Indexed artifact from outside the workspace via a directory junction point",
|
|
3109
|
+
"JUNCTION_ESCAPE_CANONICALITY",
|
|
3110
|
+
"catastrophic"
|
|
3111
|
+
);
|
|
3112
|
+
}
|
|
3113
|
+
} else {
|
|
3114
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3115
|
+
}
|
|
3116
|
+
break;
|
|
3117
|
+
}
|
|
3118
|
+
case 5: {
|
|
3119
|
+
const { file } = writePlan2(`plan-replace-${ctx.caseSeed}`, "9000");
|
|
3120
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3121
|
+
if (symlinksSupported) {
|
|
3122
|
+
fs.unlinkSync(file);
|
|
3123
|
+
fs.symlinkSync(outsideFile, file);
|
|
3124
|
+
await backend.sync({ strict: false, cwd: sandboxDir });
|
|
3125
|
+
} else {
|
|
3126
|
+
fs.writeFileSync(file, fs.readFileSync(outsideFile, "utf-8"));
|
|
3127
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3128
|
+
}
|
|
3129
|
+
break;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
store.disconnect();
|
|
3133
|
+
return {
|
|
3134
|
+
flow: `Symlink nightmare subcase ${subCase}: mode=${modeStr}`,
|
|
3135
|
+
mutation: `Symlink manipulation scenario ${subCase}`,
|
|
3136
|
+
expectedInvariant: "symlink_resolution_cannot_escape_canonical_boundaries"
|
|
3137
|
+
};
|
|
3138
|
+
} finally {
|
|
3139
|
+
cleanupSandbox(sandboxDir);
|
|
3140
|
+
cleanupSandbox(outsideDir);
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
});
|
|
3144
|
+
registerTortureBucket({
|
|
3145
|
+
name: "docker-mount-simulation",
|
|
3146
|
+
expectedInvariant: "containerized_mounts_preserve_semantic_equivalence",
|
|
3147
|
+
async run(ctx) {
|
|
3148
|
+
const { sandboxDir, hardkasDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
3149
|
+
const subCase = ctx.prng.nextInt(1, 4);
|
|
3150
|
+
try {
|
|
3151
|
+
let writePlan2 = function(id, amount) {
|
|
3152
|
+
const payload = {
|
|
3153
|
+
schema: "hardkas.txPlan",
|
|
3154
|
+
hardkasVersion: "0.7.0-alpha",
|
|
3155
|
+
version: ARTIFACT_VERSION,
|
|
3156
|
+
networkId: "simnet",
|
|
3157
|
+
mode: "simulated",
|
|
3158
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3159
|
+
// hardkas-determinism-allow: mock timestamp
|
|
3160
|
+
planId: id,
|
|
3161
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
3162
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
3163
|
+
amountSompi: amount,
|
|
3164
|
+
estimatedFeeSompi: "100",
|
|
3165
|
+
estimatedMass: "10",
|
|
3166
|
+
inputs: [{ outpoint: { transactionId: "d".repeat(64), index: 0 }, amountSompi: "10000" }],
|
|
3167
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: amount }]
|
|
3168
|
+
};
|
|
3169
|
+
const hash = calculateContentHash(payload);
|
|
3170
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
3171
|
+
const file = path.join(artifactsDir, `${artId}.json`);
|
|
3172
|
+
fs.writeFileSync(file, JSON.stringify({ ...payload, artifactId: artId, contentHash: hash }, null, 2));
|
|
3173
|
+
return { artId, file, payload };
|
|
3174
|
+
};
|
|
3175
|
+
var writePlan = writePlan2;
|
|
3176
|
+
const store = new HardkasStore({ dbPath });
|
|
3177
|
+
store.connect({ autoMigrate: true });
|
|
3178
|
+
const backend = new SqliteQueryBackend(store);
|
|
3179
|
+
switch (subCase) {
|
|
3180
|
+
case 1: {
|
|
3181
|
+
const { file } = writePlan2(`plan-fsync-${ctx.caseSeed}`, "1000");
|
|
3182
|
+
const written = fs.readFileSync(file, "utf-8");
|
|
3183
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3184
|
+
const artifacts = await backend.findArtifacts();
|
|
3185
|
+
if (artifacts.length === 0) {
|
|
3186
|
+
throw new TortureInvariantError(
|
|
3187
|
+
"Fsync delay caused artifact to not be indexed",
|
|
3188
|
+
"FSYNC_DELAY_INDEX_LOSS",
|
|
3189
|
+
"critical"
|
|
3190
|
+
);
|
|
3191
|
+
}
|
|
3192
|
+
break;
|
|
3193
|
+
}
|
|
3194
|
+
case 2: {
|
|
3195
|
+
const { file } = writePlan2(`plan-readonly-${ctx.caseSeed}`, "2000");
|
|
3196
|
+
try {
|
|
3197
|
+
fs.chmodSync(file, 292);
|
|
3198
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3199
|
+
const artifacts = await backend.findArtifacts();
|
|
3200
|
+
if (artifacts.length === 0) {
|
|
3201
|
+
throw new TortureInvariantError(
|
|
3202
|
+
"Read-only artifact file was skipped during sync",
|
|
3203
|
+
"READONLY_FILE_SKIPPED",
|
|
3204
|
+
"critical"
|
|
3205
|
+
);
|
|
3206
|
+
}
|
|
3207
|
+
} finally {
|
|
3208
|
+
try {
|
|
3209
|
+
fs.chmodSync(file, 438);
|
|
3210
|
+
} catch {
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
break;
|
|
3214
|
+
}
|
|
3215
|
+
case 3: {
|
|
3216
|
+
const { file } = writePlan2(`plan-inode-old-${ctx.caseSeed}`, "3000");
|
|
3217
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3218
|
+
fs.unlinkSync(file);
|
|
3219
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
3220
|
+
const { file: fileNew } = writePlan2(`plan-inode-new-${ctx.caseSeed}`, "99000");
|
|
3221
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3222
|
+
const artifacts = await backend.findArtifacts();
|
|
3223
|
+
const foundNew = artifacts.find((a) => a.payload.amountSompi === "99000");
|
|
3224
|
+
if (!foundNew) {
|
|
3225
|
+
throw new TortureInvariantError(
|
|
3226
|
+
"Inode/mtime replacement was not detected during sync",
|
|
3227
|
+
"INODE_REPLACEMENT_LOST",
|
|
3228
|
+
"critical"
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
break;
|
|
3232
|
+
}
|
|
3233
|
+
case 4: {
|
|
3234
|
+
try {
|
|
3235
|
+
fs.chmodSync(artifactsDir, 365);
|
|
3236
|
+
let writeSucceeded = false;
|
|
3237
|
+
try {
|
|
3238
|
+
writePlan2(`plan-permission-denied-${ctx.caseSeed}`, "4000");
|
|
3239
|
+
writeSucceeded = true;
|
|
3240
|
+
} catch (e) {
|
|
3241
|
+
}
|
|
3242
|
+
await backend.sync({ strict: false, cwd: sandboxDir });
|
|
3243
|
+
} finally {
|
|
3244
|
+
try {
|
|
3245
|
+
fs.chmodSync(artifactsDir, 511);
|
|
3246
|
+
} catch {
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
break;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
store.disconnect();
|
|
3253
|
+
return {
|
|
3254
|
+
flow: `Docker mount simulation subcase ${subCase}`,
|
|
3255
|
+
mutation: `Mount synchronization scenario ${subCase}`,
|
|
3256
|
+
expectedInvariant: "containerized_mounts_preserve_semantic_equivalence"
|
|
3257
|
+
};
|
|
3258
|
+
} finally {
|
|
3259
|
+
cleanupSandbox(sandboxDir);
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
});
|
|
3263
|
+
registerTortureBucket({
|
|
3264
|
+
name: "ci-environment-hell",
|
|
3265
|
+
expectedInvariant: "ci_environment_variation_preserves_runtime_truth",
|
|
3266
|
+
async run(ctx) {
|
|
3267
|
+
const { sandboxDir, hardkasDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
3268
|
+
const subCase = ctx.prng.nextInt(1, 4);
|
|
3269
|
+
try {
|
|
3270
|
+
let writePlan2 = function(id, amount) {
|
|
3271
|
+
const payload = {
|
|
3272
|
+
schema: "hardkas.txPlan",
|
|
3273
|
+
hardkasVersion: "0.7.0-alpha",
|
|
3274
|
+
version: ARTIFACT_VERSION,
|
|
3275
|
+
networkId: "simnet",
|
|
3276
|
+
mode: "simulated",
|
|
3277
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3278
|
+
// hardkas-determinism-allow: mock timestamp
|
|
3279
|
+
planId: id,
|
|
3280
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
3281
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
3282
|
+
amountSompi: amount,
|
|
3283
|
+
estimatedFeeSompi: "100",
|
|
3284
|
+
estimatedMass: "10",
|
|
3285
|
+
inputs: [{ outpoint: { transactionId: "c".repeat(64), index: 0 }, amountSompi: "10000" }],
|
|
3286
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: amount }]
|
|
3287
|
+
};
|
|
3288
|
+
const hash = calculateContentHash(payload);
|
|
3289
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
3290
|
+
const file = path.join(artifactsDir, `${artId}.json`);
|
|
3291
|
+
fs.writeFileSync(file, JSON.stringify({ ...payload, artifactId: artId, contentHash: hash }, null, 2));
|
|
3292
|
+
return { artId, file, payload };
|
|
3293
|
+
};
|
|
3294
|
+
var writePlan = writePlan2;
|
|
3295
|
+
const store = new HardkasStore({ dbPath });
|
|
3296
|
+
store.connect({ autoMigrate: true });
|
|
3297
|
+
const backend = new SqliteQueryBackend(store);
|
|
3298
|
+
switch (subCase) {
|
|
3299
|
+
case 1: {
|
|
3300
|
+
writePlan2(`plan-ci-ro-${ctx.caseSeed}`, "1000");
|
|
3301
|
+
try {
|
|
3302
|
+
fs.chmodSync(artifactsDir, 365);
|
|
3303
|
+
const syncResult = await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3304
|
+
if (!syncResult.ok) {
|
|
3305
|
+
throw new TortureInvariantError(
|
|
3306
|
+
"Sync failed when artifacts directory is read-only",
|
|
3307
|
+
"RO_DIR_SYNC_FAILED",
|
|
3308
|
+
"critical"
|
|
3309
|
+
);
|
|
3310
|
+
}
|
|
3311
|
+
} finally {
|
|
3312
|
+
try {
|
|
3313
|
+
fs.chmodSync(artifactsDir, 511);
|
|
3314
|
+
} catch {
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
break;
|
|
3318
|
+
}
|
|
3319
|
+
case 2: {
|
|
3320
|
+
store.disconnect();
|
|
3321
|
+
cleanupSandbox(sandboxDir);
|
|
3322
|
+
fs.mkdirSync(sandboxDir, { recursive: true });
|
|
3323
|
+
const newStore = new HardkasStore({ dbPath });
|
|
3324
|
+
newStore.connect({ autoMigrate: true });
|
|
3325
|
+
const newBackend = new SqliteQueryBackend(newStore);
|
|
3326
|
+
const syncResult = await newBackend.sync({ strict: true, cwd: sandboxDir });
|
|
3327
|
+
if (!syncResult.ok) {
|
|
3328
|
+
throw new TortureInvariantError(
|
|
3329
|
+
"Sync failed to initialize missing .hardkas folder",
|
|
3330
|
+
"MISSING_HARDKAS_DIR_INIT_FAILED",
|
|
3331
|
+
"critical"
|
|
3332
|
+
);
|
|
3333
|
+
}
|
|
3334
|
+
newStore.disconnect();
|
|
3335
|
+
break;
|
|
3336
|
+
}
|
|
3337
|
+
case 3: {
|
|
3338
|
+
writePlan2(`plan-ci-rebuild-${ctx.caseSeed}`, "3000");
|
|
3339
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3340
|
+
store.disconnect();
|
|
3341
|
+
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
|
|
3342
|
+
const newStore = new HardkasStore({ dbPath });
|
|
3343
|
+
newStore.connect({ autoMigrate: true });
|
|
3344
|
+
const newBackend = new SqliteQueryBackend(newStore);
|
|
3345
|
+
const rebuildResult = await newBackend.rebuild({ strict: true, cwd: sandboxDir });
|
|
3346
|
+
if (!rebuildResult.ok) {
|
|
3347
|
+
throw new TortureInvariantError(
|
|
3348
|
+
"Rebuild failed after database file deletion",
|
|
3349
|
+
"REBUILD_POST_DELETION_FAILED",
|
|
3350
|
+
"critical"
|
|
3351
|
+
);
|
|
3352
|
+
}
|
|
3353
|
+
const artifacts = await newBackend.findArtifacts();
|
|
3354
|
+
if (artifacts.length === 0) {
|
|
3355
|
+
throw new TortureInvariantError(
|
|
3356
|
+
"Rebuilt store contains 0 artifacts after database file deletion",
|
|
3357
|
+
"REBUILD_RECONSTRUCTION_LOST",
|
|
3358
|
+
"critical"
|
|
3359
|
+
);
|
|
3360
|
+
}
|
|
3361
|
+
newStore.disconnect();
|
|
3362
|
+
break;
|
|
3363
|
+
}
|
|
3364
|
+
case 4: {
|
|
3365
|
+
const files = fs.readdirSync(artifactsDir);
|
|
3366
|
+
for (const file of files) {
|
|
3367
|
+
fs.unlinkSync(path.join(artifactsDir, file));
|
|
3368
|
+
}
|
|
3369
|
+
const syncResult = await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3370
|
+
if (!syncResult.ok) {
|
|
3371
|
+
throw new TortureInvariantError(
|
|
3372
|
+
"Sync failed with empty artifacts folder",
|
|
3373
|
+
"EMPTY_ARTIFACTS_SYNC_FAILED",
|
|
3374
|
+
"critical"
|
|
3375
|
+
);
|
|
3376
|
+
}
|
|
3377
|
+
const artifacts = await backend.findArtifacts();
|
|
3378
|
+
if (artifacts.length !== 0) {
|
|
3379
|
+
throw new TortureInvariantError(
|
|
3380
|
+
`Expected 0 artifacts, got ${artifacts.length}`,
|
|
3381
|
+
"EMPTY_ARTIFACTS_NOT_EMPTY",
|
|
3382
|
+
"critical"
|
|
3383
|
+
);
|
|
3384
|
+
}
|
|
3385
|
+
break;
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
try {
|
|
3389
|
+
store.disconnect();
|
|
3390
|
+
} catch {
|
|
3391
|
+
}
|
|
3392
|
+
return {
|
|
3393
|
+
flow: `CI environment hell subcase ${subCase}`,
|
|
3394
|
+
mutation: `CI environmental condition ${subCase}`,
|
|
3395
|
+
expectedInvariant: "ci_environment_variation_preserves_runtime_truth"
|
|
3396
|
+
};
|
|
3397
|
+
} finally {
|
|
3398
|
+
try {
|
|
3399
|
+
fs.chmodSync(artifactsDir, 511);
|
|
3400
|
+
} catch {
|
|
3401
|
+
}
|
|
3402
|
+
cleanupSandbox(sandboxDir);
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
});
|
|
3406
|
+
registerTortureBucket({
|
|
3407
|
+
name: "network-drive-simulation",
|
|
3408
|
+
expectedInvariant: "network_filesystem_latency_cannot_create_semantic_divergence",
|
|
3409
|
+
async run(ctx) {
|
|
3410
|
+
const { sandboxDir, hardkasDir, artifactsDir, dbPath } = createSandbox(ctx);
|
|
3411
|
+
const subCase = ctx.prng.nextInt(1, 4);
|
|
3412
|
+
try {
|
|
3413
|
+
let writePlan2 = function(id, amount) {
|
|
3414
|
+
const payload = {
|
|
3415
|
+
schema: "hardkas.txPlan",
|
|
3416
|
+
hardkasVersion: "0.7.0-alpha",
|
|
3417
|
+
version: ARTIFACT_VERSION,
|
|
3418
|
+
networkId: "simnet",
|
|
3419
|
+
mode: "simulated",
|
|
3420
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3421
|
+
// hardkas-determinism-allow: mock timestamp
|
|
3422
|
+
planId: id,
|
|
3423
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
3424
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
3425
|
+
amountSompi: amount,
|
|
3426
|
+
estimatedFeeSompi: "100",
|
|
3427
|
+
estimatedMass: "10",
|
|
3428
|
+
inputs: [{ outpoint: { transactionId: "n".repeat(64), index: 0 }, amountSompi: "10000" }],
|
|
3429
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: amount }]
|
|
3430
|
+
};
|
|
3431
|
+
const hash = calculateContentHash(payload);
|
|
3432
|
+
const artId = `plan-${hash.slice(0, 16)}`;
|
|
3433
|
+
const file = path.join(artifactsDir, `${artId}.json`);
|
|
3434
|
+
fs.writeFileSync(file, JSON.stringify({ ...payload, artifactId: artId, contentHash: hash }, null, 2));
|
|
3435
|
+
return { artId, file, payload };
|
|
3436
|
+
};
|
|
3437
|
+
var writePlan = writePlan2;
|
|
3438
|
+
const store = new HardkasStore({ dbPath });
|
|
3439
|
+
store.connect({ autoMigrate: true });
|
|
3440
|
+
const backend = new SqliteQueryBackend(store);
|
|
3441
|
+
switch (subCase) {
|
|
3442
|
+
case 1: {
|
|
3443
|
+
const { file } = writePlan2(`plan-stale-${ctx.caseSeed}`, "1000");
|
|
3444
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3445
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3446
|
+
const payload = {
|
|
3447
|
+
schema: "hardkas.txPlan",
|
|
3448
|
+
hardkasVersion: "0.7.0-alpha",
|
|
3449
|
+
version: ARTIFACT_VERSION,
|
|
3450
|
+
networkId: "simnet",
|
|
3451
|
+
mode: "simulated",
|
|
3452
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3453
|
+
// hardkas-determinism-allow: mock timestamp
|
|
3454
|
+
planId: `plan-stale-${ctx.caseSeed}`,
|
|
3455
|
+
from: { address: "kaspa:sim_qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
3456
|
+
to: { address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
3457
|
+
amountSompi: "99000",
|
|
3458
|
+
estimatedFeeSompi: "100",
|
|
3459
|
+
estimatedMass: "10",
|
|
3460
|
+
inputs: [{ outpoint: { transactionId: "n".repeat(64), index: 0 }, amountSompi: "100000" }],
|
|
3461
|
+
outputs: [{ address: "kaspa:sim_qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "99000" }]
|
|
3462
|
+
};
|
|
3463
|
+
const hash = calculateContentHash(payload);
|
|
3464
|
+
fs.writeFileSync(file, JSON.stringify({ ...payload, artifactId: `plan-${hash.slice(0, 16)}`, contentHash: hash }, null, 2));
|
|
3465
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3466
|
+
const artifacts = await backend.findArtifacts();
|
|
3467
|
+
const found = artifacts.find((a) => a.payload.amountSompi === "99000");
|
|
3468
|
+
if (!found) {
|
|
3469
|
+
throw new TortureInvariantError(
|
|
3470
|
+
"Stale read/overwrite was not detected in second sync",
|
|
3471
|
+
"STALE_READ_OVERWRITE_LOST",
|
|
3472
|
+
"critical"
|
|
3473
|
+
);
|
|
3474
|
+
}
|
|
3475
|
+
break;
|
|
3476
|
+
}
|
|
3477
|
+
case 2: {
|
|
3478
|
+
const plans = [];
|
|
3479
|
+
for (let i = 0; i < 10; i++) {
|
|
3480
|
+
const { artId } = writePlan2(`plan-ooo-${i}-${ctx.caseSeed}`, `${(i + 1) * 1e3}`);
|
|
3481
|
+
plans.push(artId);
|
|
3482
|
+
}
|
|
3483
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3484
|
+
const artifacts1 = await backend.findArtifacts();
|
|
3485
|
+
const ids1 = artifacts1.map((a) => a.artifactId).sort();
|
|
3486
|
+
store.disconnect();
|
|
3487
|
+
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
|
|
3488
|
+
const newStore = new HardkasStore({ dbPath });
|
|
3489
|
+
newStore.connect({ autoMigrate: true });
|
|
3490
|
+
const newBackend = new SqliteQueryBackend(newStore);
|
|
3491
|
+
await newBackend.rebuild({ strict: true, cwd: sandboxDir });
|
|
3492
|
+
const artifacts2 = await newBackend.findArtifacts();
|
|
3493
|
+
const ids2 = artifacts2.map((a) => a.artifactId).sort();
|
|
3494
|
+
if (JSON.stringify(ids1) !== JSON.stringify(ids2)) {
|
|
3495
|
+
throw new TortureInvariantError(
|
|
3496
|
+
"Out-of-order writes caused database reconstruction divergence",
|
|
3497
|
+
"OOO_WRITES_DIVERGENCE",
|
|
3498
|
+
"critical"
|
|
3499
|
+
);
|
|
3500
|
+
}
|
|
3501
|
+
newStore.disconnect();
|
|
3502
|
+
break;
|
|
3503
|
+
}
|
|
3504
|
+
case 3: {
|
|
3505
|
+
const files = [];
|
|
3506
|
+
for (let i = 0; i < 10; i++) {
|
|
3507
|
+
const { file } = writePlan2(`plan-partial-${i}-${ctx.caseSeed}`, `${(i + 1) * 1e3}`);
|
|
3508
|
+
files.push(file);
|
|
3509
|
+
}
|
|
3510
|
+
await backend.syncPaths(files.slice(0, 5), { strict: true, cwd: sandboxDir });
|
|
3511
|
+
const artifacts1 = await backend.findArtifacts();
|
|
3512
|
+
if (artifacts1.length !== 5) {
|
|
3513
|
+
throw new TortureInvariantError(
|
|
3514
|
+
`Expected exactly 5 artifacts after partial sync, got ${artifacts1.length}`,
|
|
3515
|
+
"PARTIAL_SYNC_COUNT_MISMATCH",
|
|
3516
|
+
"critical"
|
|
3517
|
+
);
|
|
3518
|
+
}
|
|
3519
|
+
await backend.syncPaths(files.slice(5), { strict: true, cwd: sandboxDir });
|
|
3520
|
+
const artifacts2 = await backend.findArtifacts();
|
|
3521
|
+
if (artifacts2.length !== 10) {
|
|
3522
|
+
throw new TortureInvariantError(
|
|
3523
|
+
`Expected exactly 10 artifacts after full sync, got ${artifacts2.length}`,
|
|
3524
|
+
"FULL_SYNC_COUNT_MISMATCH",
|
|
3525
|
+
"critical"
|
|
3526
|
+
);
|
|
3527
|
+
}
|
|
3528
|
+
break;
|
|
3529
|
+
}
|
|
3530
|
+
case 4: {
|
|
3531
|
+
for (let i = 0; i < 5; i++) {
|
|
3532
|
+
writePlan2(`plan-delayed-vis-${i}-${ctx.caseSeed}`, `${(i + 1) * 1e3}`);
|
|
3533
|
+
}
|
|
3534
|
+
await backend.sync({ strict: true, cwd: sandboxDir });
|
|
3535
|
+
for (let i = 5; i < 10; i++) {
|
|
3536
|
+
writePlan2(`plan-delayed-vis-${i}-${ctx.caseSeed}`, `${(i + 1) * 1e3}`);
|
|
3537
|
+
}
|
|
3538
|
+
store.disconnect();
|
|
3539
|
+
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath);
|
|
3540
|
+
const newStore = new HardkasStore({ dbPath });
|
|
3541
|
+
newStore.connect({ autoMigrate: true });
|
|
3542
|
+
const newBackend = new SqliteQueryBackend(newStore);
|
|
3543
|
+
await newBackend.rebuild({ strict: true, cwd: sandboxDir });
|
|
3544
|
+
const artifacts = await newBackend.findArtifacts();
|
|
3545
|
+
if (artifacts.length !== 10) {
|
|
3546
|
+
throw new TortureInvariantError(
|
|
3547
|
+
`Expected 10 rebuilt artifacts, got ${artifacts.length}`,
|
|
3548
|
+
"DELAYED_VISIBILITY_RECONSTRUCTION_FAILED",
|
|
3549
|
+
"critical"
|
|
3550
|
+
);
|
|
3551
|
+
}
|
|
3552
|
+
newStore.disconnect();
|
|
3553
|
+
break;
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
try {
|
|
3557
|
+
store.disconnect();
|
|
3558
|
+
} catch {
|
|
3559
|
+
}
|
|
3560
|
+
return {
|
|
3561
|
+
flow: `Network drive simulation subcase ${subCase}`,
|
|
3562
|
+
mutation: `Latency/filesystem sync visibility subcase ${subCase}`,
|
|
3563
|
+
expectedInvariant: "network_filesystem_latency_cannot_create_semantic_divergence"
|
|
3564
|
+
};
|
|
3565
|
+
} finally {
|
|
3566
|
+
cleanupSandbox(sandboxDir);
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
});
|
|
3570
|
+
|
|
3571
|
+
// src/torture/local-buckets.ts
|
|
3572
|
+
import fs2 from "fs";
|
|
3573
|
+
import { fileURLToPath } from "url";
|
|
3574
|
+
import path2 from "path";
|
|
3575
|
+
import { execSync } from "child_process";
|
|
3576
|
+
registerTortureBucket({
|
|
3577
|
+
name: "local-first-lifecycle",
|
|
3578
|
+
profiles: ["local"],
|
|
3579
|
+
expectedInvariant: "Workspace init, basic transfer, and dev doctor checks succeed without mutation side effects",
|
|
3580
|
+
run: async (ctx) => {
|
|
3581
|
+
const runDir = path2.join(ctx.workspaceDir, ".tmp", `local-${ctx.caseId}`);
|
|
3582
|
+
fs2.rmSync(runDir, { recursive: true, force: true });
|
|
3583
|
+
fs2.mkdirSync(runDir, { recursive: true });
|
|
3584
|
+
const cliPath = fileURLToPath(new URL("../../cli/dist/index.js", import.meta.url));
|
|
3585
|
+
fs2.writeFileSync(path2.join(runDir, "package.json"), "{}");
|
|
3586
|
+
try {
|
|
3587
|
+
execSync(`node "${cliPath}" dev init`, { cwd: runDir, stdio: "pipe", encoding: "utf-8" });
|
|
3588
|
+
} catch (e) {
|
|
3589
|
+
throw new TortureInvariantError(`hardkas dev init failed: ${e.message}
|
|
3590
|
+
${e.stdout}
|
|
3591
|
+
${e.stderr}`, "INIT_FAILED", "critical");
|
|
3592
|
+
}
|
|
3593
|
+
if (!fs2.existsSync(path2.join(runDir, "hardkas.config.ts"))) {
|
|
3594
|
+
throw new TortureInvariantError("hardkas.config.ts was not generated", "MISSING_CONFIG", "critical");
|
|
3595
|
+
}
|
|
3596
|
+
const minimalConfig = `export default {
|
|
3597
|
+
defaultNetwork: "simulated",
|
|
3598
|
+
networks: {
|
|
3599
|
+
simulated: { kind: "simulated", description: "Pure local simulation" }
|
|
3600
|
+
},
|
|
3601
|
+
accounts: {
|
|
3602
|
+
alice: { kind: "simulated", address: "kaspa:sim_sim_alice" }
|
|
3603
|
+
}
|
|
3604
|
+
};
|
|
3605
|
+
`;
|
|
3606
|
+
fs2.writeFileSync(path2.join(runDir, "hardkas.config.ts"), minimalConfig);
|
|
3607
|
+
const artifactsDir = path2.join(runDir, ".hardkas", "artifacts");
|
|
3608
|
+
fs2.mkdirSync(artifactsDir, { recursive: true });
|
|
3609
|
+
const eventsPath = path2.join(artifactsDir, "events.jsonl");
|
|
3610
|
+
const mockArtifact = { schema: "hardkas.artifact.v1", type: "transfer", amount: 100, timestamp: Date.now() };
|
|
3611
|
+
fs2.appendFileSync(eventsPath, JSON.stringify(mockArtifact) + "\n");
|
|
3612
|
+
try {
|
|
3613
|
+
const doctorOutput = execSync(`node "${cliPath}" dev doctor --json`, { cwd: runDir, stdio: "pipe", encoding: "utf-8" });
|
|
3614
|
+
const doctorResult = JSON.parse(doctorOutput);
|
|
3615
|
+
const workspaceCheck = doctorResult.checks?.find((c) => c.name === "Workspace Validity");
|
|
3616
|
+
if (workspaceCheck?.status === "error") {
|
|
3617
|
+
throw new TortureInvariantError(
|
|
3618
|
+
`dev doctor: workspace invalid \u2014 ${workspaceCheck.message}`,
|
|
3619
|
+
"DOCTOR_WORKSPACE_INVALID",
|
|
3620
|
+
"critical"
|
|
3621
|
+
);
|
|
3622
|
+
}
|
|
3623
|
+
} catch (e) {
|
|
3624
|
+
if (e instanceof TortureInvariantError) throw e;
|
|
3625
|
+
const stdout = e.stdout || "";
|
|
3626
|
+
try {
|
|
3627
|
+
const doctorResult = JSON.parse(stdout);
|
|
3628
|
+
const workspaceCheck = doctorResult.checks?.find((c) => c.name === "Workspace Validity");
|
|
3629
|
+
if (workspaceCheck?.status === "error") {
|
|
3630
|
+
throw new TortureInvariantError(
|
|
3631
|
+
`dev doctor: workspace invalid \u2014 ${workspaceCheck.message}`,
|
|
3632
|
+
"DOCTOR_WORKSPACE_INVALID",
|
|
3633
|
+
"critical"
|
|
3634
|
+
);
|
|
3635
|
+
}
|
|
3636
|
+
} catch (parseErr) {
|
|
3637
|
+
if (parseErr instanceof TortureInvariantError) throw parseErr;
|
|
3638
|
+
throw new TortureInvariantError(
|
|
3639
|
+
`dev doctor failed and produced unparseable output: ${e.message}`,
|
|
3640
|
+
"DOCTOR_FAILED",
|
|
3641
|
+
"warning"
|
|
3642
|
+
);
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
fs2.rmSync(runDir, { recursive: true, force: true });
|
|
3646
|
+
return {
|
|
3647
|
+
flow: "local-first-lifecycle",
|
|
3648
|
+
mutation: "simulated local workflows and dev doctor",
|
|
3649
|
+
environmentMode: "local",
|
|
3650
|
+
artifactsAfter: ["events.jsonl"]
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3653
|
+
});
|
|
3654
|
+
|
|
3655
|
+
// src/torture/corruption-buckets.ts
|
|
3656
|
+
import fs3 from "fs";
|
|
3657
|
+
import path3 from "path";
|
|
3658
|
+
import { AppendCoordinator } from "@hardkas/core";
|
|
3659
|
+
registerTortureBucket({
|
|
3660
|
+
name: "concurrent-append-tail-repair",
|
|
3661
|
+
profiles: ["corruption"],
|
|
3662
|
+
expectedInvariant: "AppendCoordinator successfully truncates partial JSONL suffix while keeping large JSONL lines during concurrent append simulation",
|
|
3663
|
+
run: async (ctx) => {
|
|
3664
|
+
const runDir = path3.join(ctx.workspaceDir, ".tmp", `corruption-${ctx.caseId}`);
|
|
3665
|
+
fs3.rmSync(runDir, { recursive: true, force: true });
|
|
3666
|
+
fs3.mkdirSync(runDir, { recursive: true });
|
|
3667
|
+
const eventsPath = path3.join(runDir, "events.jsonl");
|
|
3668
|
+
const largePayload = { schema: "hardkas.artifact.v1", type: "large", data: "x".repeat(7e4) };
|
|
3669
|
+
fs3.writeFileSync(eventsPath, JSON.stringify(largePayload) + "\n");
|
|
3670
|
+
const validSize = fs3.statSync(eventsPath).size;
|
|
3671
|
+
fs3.appendFileSync(eventsPath, '{"schema":"hardkas.artifact.v1","type":"corrupted","d');
|
|
3672
|
+
const newArtifact = { schema: "hardkas.artifact.v1", type: "recovery-test" };
|
|
3673
|
+
AppendCoordinator.appendAtomic(eventsPath, JSON.stringify(newArtifact), runDir);
|
|
3674
|
+
const content = fs3.readFileSync(eventsPath, "utf-8");
|
|
3675
|
+
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
3676
|
+
if (lines.length !== 2) {
|
|
3677
|
+
throw new TortureInvariantError(`Expected exactly 2 lines after recovery, got ${lines.length}`, "CORRUPTION_RECOVERY_FAILED", "catastrophic");
|
|
3678
|
+
}
|
|
3679
|
+
try {
|
|
3680
|
+
JSON.parse(lines[0]);
|
|
3681
|
+
JSON.parse(lines[1]);
|
|
3682
|
+
} catch (e) {
|
|
3683
|
+
throw new TortureInvariantError("Recovered JSON lines are invalid", "CORRUPTION_RECOVERY_JSON_PARSE", "catastrophic");
|
|
3684
|
+
}
|
|
3685
|
+
if (lines[0].length < 7e4) {
|
|
3686
|
+
throw new TortureInvariantError("Large JSONL line was incorrectly truncated", "LARGE_LINE_TRUNCATED", "catastrophic");
|
|
3687
|
+
}
|
|
3688
|
+
fs3.rmSync(runDir, { recursive: true, force: true });
|
|
3689
|
+
return {
|
|
3690
|
+
flow: "concurrent-append-tail-repair",
|
|
3691
|
+
mutation: "massive valid line + partial tail + atomic append",
|
|
3692
|
+
environmentMode: "corruption",
|
|
3693
|
+
externalMutationDetected: true
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
3696
|
+
});
|
|
35
3697
|
export {
|
|
36
3698
|
AdversarialFixtures,
|
|
3699
|
+
LcgPrng,
|
|
3700
|
+
TortureInvariantError,
|
|
37
3701
|
clearMassRecords,
|
|
38
3702
|
createFixture,
|
|
39
3703
|
createTestHarness,
|
|
40
3704
|
disableMassTracking,
|
|
41
3705
|
enableMassTracking,
|
|
42
3706
|
generateReproducibilityReport,
|
|
3707
|
+
getAllTortureBuckets,
|
|
43
3708
|
getMassRecords,
|
|
3709
|
+
getTortureBucket,
|
|
44
3710
|
hardKasMatchers
|
|
45
3711
|
};
|