@hardkas/testing 0.2.2-alpha.1 → 0.3.0-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.d.ts +90 -0
- package/dist/adversarial-fixtures.js +6 -0
- package/dist/chunk-3EK7ATR5.js +152 -0
- package/dist/chunk-CUJL53GG.js +90 -0
- package/dist/chunk-UHH25II3.js +103 -0
- package/dist/chunk-WIN7YDBM.js +108 -0
- package/dist/harness.d.ts +55 -0
- package/dist/harness.js +14 -0
- package/dist/index.d.ts +92 -61
- package/dist/index.js +38 -163
- package/dist/mass-setup.d.ts +2 -0
- package/dist/mass-setup.js +8 -0
- package/dist/reproducibility.d.ts +24 -0
- package/dist/reproducibility.js +7 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.js +1 -0
- package/package.json +17 -11
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates artifacts with semantic or structural flaws for adversarial testing.
|
|
3
|
+
*/
|
|
4
|
+
declare const AdversarialFixtures: {
|
|
5
|
+
/**
|
|
6
|
+
* Circular lineage: A -> B -> A
|
|
7
|
+
*/
|
|
8
|
+
circularLineage(): {
|
|
9
|
+
artifactA: any;
|
|
10
|
+
artifactB: any;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Artifact where the contentHash does not match the actual content calculation.
|
|
14
|
+
*/
|
|
15
|
+
hashMismatch(): any;
|
|
16
|
+
/**
|
|
17
|
+
* Artifact with a parent from a different network (Security Violation).
|
|
18
|
+
*/
|
|
19
|
+
crossNetworkLineage(): {
|
|
20
|
+
parent: any;
|
|
21
|
+
child: any;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Trace with duplicate event IDs (Corruption).
|
|
25
|
+
*/
|
|
26
|
+
duplicateEventTrace(): {
|
|
27
|
+
schema: string;
|
|
28
|
+
workflowId: string;
|
|
29
|
+
events: {
|
|
30
|
+
eventId: string;
|
|
31
|
+
kind: string;
|
|
32
|
+
}[];
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Malformed JSONL snippet (truncated).
|
|
36
|
+
*/
|
|
37
|
+
malformedJsonl(): string;
|
|
38
|
+
/**
|
|
39
|
+
* Lineage with sequence rollback (corrupted history).
|
|
40
|
+
*/
|
|
41
|
+
sequenceRollback(): ({
|
|
42
|
+
artifactId: string;
|
|
43
|
+
lineage: {
|
|
44
|
+
sequenceId: number;
|
|
45
|
+
contentHash: string;
|
|
46
|
+
parentArtifactId?: never;
|
|
47
|
+
};
|
|
48
|
+
schema: string;
|
|
49
|
+
version: string;
|
|
50
|
+
networkId: string;
|
|
51
|
+
mode: string;
|
|
52
|
+
} | {
|
|
53
|
+
artifactId: string;
|
|
54
|
+
lineage: {
|
|
55
|
+
sequenceId: number;
|
|
56
|
+
contentHash: string;
|
|
57
|
+
parentArtifactId: string;
|
|
58
|
+
};
|
|
59
|
+
schema: string;
|
|
60
|
+
version: string;
|
|
61
|
+
networkId: string;
|
|
62
|
+
mode: string;
|
|
63
|
+
})[];
|
|
64
|
+
/**
|
|
65
|
+
* Artifact with a future timestamp (anomaly).
|
|
66
|
+
* Intentionally uses Date.now() to generate a timestamp far in the future.
|
|
67
|
+
*/
|
|
68
|
+
futureTimestamp(): {
|
|
69
|
+
schema: string;
|
|
70
|
+
version: string;
|
|
71
|
+
artifactId: string;
|
|
72
|
+
contentHash: string;
|
|
73
|
+
networkId: string;
|
|
74
|
+
mode: string;
|
|
75
|
+
createdAt: string;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Artifact with an unsupported version.
|
|
79
|
+
*/
|
|
80
|
+
unsupportedVersion(): {
|
|
81
|
+
schema: string;
|
|
82
|
+
version: string;
|
|
83
|
+
artifactId: string;
|
|
84
|
+
contentHash: string;
|
|
85
|
+
networkId: string;
|
|
86
|
+
mode: string;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export { AdversarialFixtures };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// src/adversarial-fixtures.ts
|
|
2
|
+
import { calculateContentHash, CURRENT_HASH_VERSION, ARTIFACT_VERSION } from "@hardkas/artifacts";
|
|
3
|
+
var AdversarialFixtures = {
|
|
4
|
+
/**
|
|
5
|
+
* Circular lineage: A -> B -> A
|
|
6
|
+
*/
|
|
7
|
+
circularLineage() {
|
|
8
|
+
const artifactA = {
|
|
9
|
+
schema: "hardkas.txPlan",
|
|
10
|
+
version: ARTIFACT_VERSION,
|
|
11
|
+
artifactId: "art-a",
|
|
12
|
+
contentHash: "hash-a",
|
|
13
|
+
networkId: "simnet",
|
|
14
|
+
mode: "simulated",
|
|
15
|
+
lineage: { parentArtifactId: "art-b" }
|
|
16
|
+
};
|
|
17
|
+
const artifactB = {
|
|
18
|
+
schema: "hardkas.txPlan",
|
|
19
|
+
version: ARTIFACT_VERSION,
|
|
20
|
+
artifactId: "art-b",
|
|
21
|
+
contentHash: "hash-b",
|
|
22
|
+
networkId: "simnet",
|
|
23
|
+
mode: "simulated",
|
|
24
|
+
lineage: { parentArtifactId: "art-a" }
|
|
25
|
+
};
|
|
26
|
+
return { artifactA, artifactB };
|
|
27
|
+
},
|
|
28
|
+
/**
|
|
29
|
+
* Artifact where the contentHash does not match the actual content calculation.
|
|
30
|
+
*/
|
|
31
|
+
hashMismatch() {
|
|
32
|
+
const artifact = {
|
|
33
|
+
schema: "hardkas.txPlan",
|
|
34
|
+
version: ARTIFACT_VERSION,
|
|
35
|
+
networkId: "simnet",
|
|
36
|
+
mode: "simulated",
|
|
37
|
+
amountSompi: "1000",
|
|
38
|
+
estimatedFeeSompi: "1",
|
|
39
|
+
estimatedMass: "1",
|
|
40
|
+
from: { address: "kaspasim:qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
41
|
+
to: { address: "kaspasim:qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
42
|
+
inputs: [],
|
|
43
|
+
outputs: [],
|
|
44
|
+
hashVersion: CURRENT_HASH_VERSION
|
|
45
|
+
};
|
|
46
|
+
const realHash = calculateContentHash(artifact, CURRENT_HASH_VERSION);
|
|
47
|
+
artifact.contentHash = "f" + realHash.slice(1);
|
|
48
|
+
artifact.artifactId = `plan-${artifact.contentHash.slice(0, 16)}`;
|
|
49
|
+
artifact.planId = artifact.artifactId;
|
|
50
|
+
return artifact;
|
|
51
|
+
},
|
|
52
|
+
/**
|
|
53
|
+
* Artifact with a parent from a different network (Security Violation).
|
|
54
|
+
*/
|
|
55
|
+
crossNetworkLineage() {
|
|
56
|
+
const parent = {
|
|
57
|
+
schema: "hardkas.txPlan",
|
|
58
|
+
version: ARTIFACT_VERSION,
|
|
59
|
+
artifactId: "parent-mainnet",
|
|
60
|
+
contentHash: "hash-mainnet",
|
|
61
|
+
networkId: "mainnet",
|
|
62
|
+
mode: "l1-rpc"
|
|
63
|
+
};
|
|
64
|
+
const child = {
|
|
65
|
+
schema: "hardkas.signedTx",
|
|
66
|
+
version: ARTIFACT_VERSION,
|
|
67
|
+
artifactId: "child-simnet",
|
|
68
|
+
contentHash: "hash-simnet",
|
|
69
|
+
networkId: "simnet",
|
|
70
|
+
mode: "simulated",
|
|
71
|
+
lineage: {
|
|
72
|
+
artifactId: "hash-simnet",
|
|
73
|
+
parentArtifactId: "parent-mainnet",
|
|
74
|
+
lineageId: "b".repeat(64),
|
|
75
|
+
rootArtifactId: "c".repeat(64)
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
return { parent, child };
|
|
79
|
+
},
|
|
80
|
+
/**
|
|
81
|
+
* Trace with duplicate event IDs (Corruption).
|
|
82
|
+
*/
|
|
83
|
+
duplicateEventTrace() {
|
|
84
|
+
return {
|
|
85
|
+
schema: "hardkas.trace",
|
|
86
|
+
workflowId: "wf-1",
|
|
87
|
+
events: [
|
|
88
|
+
{ eventId: "ev-1", kind: "start" },
|
|
89
|
+
{ eventId: "ev-1", kind: "step" }
|
|
90
|
+
// Duplicate ID
|
|
91
|
+
]
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
/**
|
|
95
|
+
* Malformed JSONL snippet (truncated).
|
|
96
|
+
*/
|
|
97
|
+
malformedJsonl() {
|
|
98
|
+
return `{"eventId":"ev-1","kind":"start"}
|
|
99
|
+
{"eventId":"ev-2","kind":"step",`;
|
|
100
|
+
},
|
|
101
|
+
/**
|
|
102
|
+
* Lineage with sequence rollback (corrupted history).
|
|
103
|
+
*/
|
|
104
|
+
sequenceRollback() {
|
|
105
|
+
const common = {
|
|
106
|
+
schema: "hardkas.txPlan",
|
|
107
|
+
version: ARTIFACT_VERSION,
|
|
108
|
+
networkId: "simnet",
|
|
109
|
+
mode: "simulated"
|
|
110
|
+
};
|
|
111
|
+
return [
|
|
112
|
+
{ ...common, artifactId: "art-1", lineage: { sequenceId: 1, contentHash: "hash-1" } },
|
|
113
|
+
{ ...common, artifactId: "art-2", lineage: { sequenceId: 2, contentHash: "hash-2", parentArtifactId: "art-1" } },
|
|
114
|
+
{ ...common, artifactId: "art-3", lineage: { sequenceId: 2, contentHash: "hash-3-BAD", parentArtifactId: "art-1" } }
|
|
115
|
+
// Duplicate sequenceId 2
|
|
116
|
+
];
|
|
117
|
+
},
|
|
118
|
+
/**
|
|
119
|
+
* Artifact with a future timestamp (anomaly).
|
|
120
|
+
* Intentionally uses Date.now() to generate a timestamp far in the future.
|
|
121
|
+
*/
|
|
122
|
+
futureTimestamp() {
|
|
123
|
+
const farFuture = new Date(Date.now() + 1e3 * 60 * 60 * 24 * 365 * 10).toISOString();
|
|
124
|
+
return {
|
|
125
|
+
schema: "hardkas.txPlan",
|
|
126
|
+
version: ARTIFACT_VERSION,
|
|
127
|
+
artifactId: "art-future",
|
|
128
|
+
contentHash: "hash-future",
|
|
129
|
+
networkId: "simnet",
|
|
130
|
+
mode: "simulated",
|
|
131
|
+
createdAt: farFuture
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
/**
|
|
135
|
+
* Artifact with an unsupported version.
|
|
136
|
+
*/
|
|
137
|
+
unsupportedVersion() {
|
|
138
|
+
return {
|
|
139
|
+
schema: "hardkas.txPlan",
|
|
140
|
+
version: "99.9.9",
|
|
141
|
+
// Future version
|
|
142
|
+
artifactId: "art-vnext",
|
|
143
|
+
contentHash: "hash-vnext",
|
|
144
|
+
networkId: "simnet",
|
|
145
|
+
mode: "simulated"
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export {
|
|
151
|
+
AdversarialFixtures
|
|
152
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/harness.ts
|
|
2
|
+
import {
|
|
3
|
+
createInitialLocalnetState,
|
|
4
|
+
applySimulatedPayment,
|
|
5
|
+
getAccountBalanceSompi,
|
|
6
|
+
createLocalnetSnapshot
|
|
7
|
+
} from "@hardkas/localnet";
|
|
8
|
+
var massRecords = [];
|
|
9
|
+
var massTrackingEnabled = false;
|
|
10
|
+
function enableMassTracking() {
|
|
11
|
+
massTrackingEnabled = true;
|
|
12
|
+
}
|
|
13
|
+
function disableMassTracking() {
|
|
14
|
+
massTrackingEnabled = false;
|
|
15
|
+
}
|
|
16
|
+
function getMassRecords() {
|
|
17
|
+
return [...massRecords];
|
|
18
|
+
}
|
|
19
|
+
function clearMassRecords() {
|
|
20
|
+
massRecords = [];
|
|
21
|
+
}
|
|
22
|
+
function createTestHarness(config) {
|
|
23
|
+
const accountCount = config?.accounts ?? 3;
|
|
24
|
+
const initialBalanceSompi = config?.initialBalance ?? 100000000000n;
|
|
25
|
+
let currentState = createInitialLocalnetState({
|
|
26
|
+
accounts: accountCount,
|
|
27
|
+
initialBalanceSompi
|
|
28
|
+
});
|
|
29
|
+
if (config?.networkId) {
|
|
30
|
+
currentState.networkId = config.networkId;
|
|
31
|
+
}
|
|
32
|
+
const initialState = structuredClone(currentState);
|
|
33
|
+
const harness = {
|
|
34
|
+
get state() {
|
|
35
|
+
return currentState;
|
|
36
|
+
},
|
|
37
|
+
send(opts) {
|
|
38
|
+
const preBalance = {
|
|
39
|
+
from: getAccountBalanceSompi(currentState, opts.from),
|
|
40
|
+
to: getAccountBalanceSompi(currentState, opts.to)
|
|
41
|
+
};
|
|
42
|
+
const result = applySimulatedPayment(currentState, opts);
|
|
43
|
+
if (result.ok) {
|
|
44
|
+
currentState = result.state;
|
|
45
|
+
}
|
|
46
|
+
const postBalance = {
|
|
47
|
+
from: getAccountBalanceSompi(currentState, opts.from),
|
|
48
|
+
to: getAccountBalanceSompi(currentState, opts.to)
|
|
49
|
+
};
|
|
50
|
+
if (result.ok && massTrackingEnabled && result.planArtifact) {
|
|
51
|
+
massRecords.push({
|
|
52
|
+
txId: result.receipt.txId,
|
|
53
|
+
inputCount: result.planArtifact.inputs.length,
|
|
54
|
+
outputCount: result.planArtifact.outputs.length,
|
|
55
|
+
estimatedMass: BigInt(result.planArtifact.estimatedMass),
|
|
56
|
+
estimatedFeeSompi: BigInt(result.planArtifact.estimatedFeeSompi),
|
|
57
|
+
timestamp: Date.now()
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
ok: result.ok,
|
|
62
|
+
receipt: result.receipt,
|
|
63
|
+
plan: result.planArtifact,
|
|
64
|
+
preBalance,
|
|
65
|
+
postBalance
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
balanceOf(name) {
|
|
69
|
+
return getAccountBalanceSompi(currentState, name);
|
|
70
|
+
},
|
|
71
|
+
accountNames() {
|
|
72
|
+
return currentState.accounts.map((a) => a.name);
|
|
73
|
+
},
|
|
74
|
+
snapshot() {
|
|
75
|
+
return createLocalnetSnapshot(currentState);
|
|
76
|
+
},
|
|
77
|
+
reset() {
|
|
78
|
+
currentState = structuredClone(initialState);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return harness;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
enableMassTracking,
|
|
86
|
+
disableMassTracking,
|
|
87
|
+
getMassRecords,
|
|
88
|
+
clearMassRecords,
|
|
89
|
+
createTestHarness
|
|
90
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// src/setup.ts
|
|
2
|
+
import { expect } from "vitest";
|
|
3
|
+
|
|
4
|
+
// src/matchers.ts
|
|
5
|
+
var hardKasMatchers = {
|
|
6
|
+
toBeAccepted(received) {
|
|
7
|
+
const pass = received?.status === "accepted";
|
|
8
|
+
return {
|
|
9
|
+
pass,
|
|
10
|
+
message: () => pass ? `Expected receipt NOT to be accepted, but status is "${received.status}"` : `Expected receipt to be accepted, but status is "${received?.status ?? "undefined"}"`
|
|
11
|
+
};
|
|
12
|
+
},
|
|
13
|
+
toBeFailed(received) {
|
|
14
|
+
const pass = received?.status === "failed";
|
|
15
|
+
return {
|
|
16
|
+
pass,
|
|
17
|
+
message: () => pass ? `Expected receipt NOT to be failed, but status is "failed"` : `Expected receipt to be failed, but status is "${received?.status ?? "undefined"}"`
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
toHaveValidTxId(received) {
|
|
21
|
+
const txId = received?.txId || (typeof received === "string" ? received : "");
|
|
22
|
+
const pass = typeof txId === "string" && (txId.startsWith("simtx_") || /^[0-9a-fA-F]{64}$/.test(txId));
|
|
23
|
+
return {
|
|
24
|
+
pass,
|
|
25
|
+
message: () => pass ? `Expected "${txId}" NOT to be a valid txId` : `Expected "${txId}" to be a valid txId (starts with "simtx_" or is 64-char hex)`
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
toHaveValidContentHash(received) {
|
|
29
|
+
const hash = received?.contentHash || (typeof received === "string" ? received : "");
|
|
30
|
+
const pass = typeof hash === "string" && /^[0-9a-fA-F]{64}$/.test(hash);
|
|
31
|
+
return {
|
|
32
|
+
pass,
|
|
33
|
+
message: () => pass ? `Expected "${hash}" NOT to be a valid content hash` : `Expected "${hash}" to be a valid content hash (64-char hex)`
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
toPassLineageCheck(received) {
|
|
37
|
+
const pass = Array.isArray(received?.lineage) && received.lineage.length > 0;
|
|
38
|
+
return {
|
|
39
|
+
pass,
|
|
40
|
+
message: () => pass ? `Expected artifact NOT to pass lineage check` : `Expected artifact to pass lineage check (missing or empty lineage)`
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
toBeBlueBlock(received) {
|
|
44
|
+
const pass = received?.isBlue === true;
|
|
45
|
+
return {
|
|
46
|
+
pass,
|
|
47
|
+
message: () => pass ? `Expected block NOT to be blue, but it is` : `Expected block to be blue, but it is red or unknown`
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
toBeRedBlock(received) {
|
|
51
|
+
const pass = received?.isBlue === false;
|
|
52
|
+
return {
|
|
53
|
+
pass,
|
|
54
|
+
message: () => pass ? `Expected block NOT to be red, but it is` : `Expected block to be red, but it is blue or unknown`
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
toHaveIncreasedBy(received, amount) {
|
|
58
|
+
const actual = BigInt(received);
|
|
59
|
+
const pass = actual >= amount;
|
|
60
|
+
return {
|
|
61
|
+
pass,
|
|
62
|
+
message: () => pass ? `Expected increase NOT to be at least ${amount}, but got ${actual}` : `Expected increase to be at least ${amount}, but got ${actual}`
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
toHaveDecreasedBy(received, amount) {
|
|
66
|
+
const actual = BigInt(received);
|
|
67
|
+
const pass = actual >= amount;
|
|
68
|
+
return {
|
|
69
|
+
pass,
|
|
70
|
+
message: () => pass ? `Expected decrease NOT to be at least ${amount}, but got ${actual}` : `Expected decrease to be at least ${amount}, but got ${actual}`
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
toHaveNoRedBlocks(received) {
|
|
74
|
+
const redBlocks = Object.values(received?.blocks || {}).filter((b) => b.isBlue === false);
|
|
75
|
+
const pass = redBlocks.length === 0;
|
|
76
|
+
return {
|
|
77
|
+
pass,
|
|
78
|
+
message: () => pass ? `Expected DAG to have red blocks, but it has none` : `Expected DAG to have no red blocks, but found ${redBlocks.length}`
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
toHaveDagWidth(received, width) {
|
|
82
|
+
const allBlockIds = Object.keys(received?.blocks || {});
|
|
83
|
+
const parentIds = /* @__PURE__ */ new Set();
|
|
84
|
+
for (const block of Object.values(received?.blocks || {})) {
|
|
85
|
+
for (const p of block.parents) {
|
|
86
|
+
parentIds.add(p);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const tips = allBlockIds.filter((id) => !parentIds.has(id));
|
|
90
|
+
const pass = tips.length === width;
|
|
91
|
+
return {
|
|
92
|
+
pass,
|
|
93
|
+
message: () => pass ? `Expected DAG NOT to have width ${width}, but it does` : `Expected DAG to have width ${width}, but got ${tips.length}`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// src/setup.ts
|
|
99
|
+
expect.extend(hardKasMatchers);
|
|
100
|
+
|
|
101
|
+
export {
|
|
102
|
+
hardKasMatchers
|
|
103
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTestHarness
|
|
3
|
+
} from "./chunk-CUJL53GG.js";
|
|
4
|
+
|
|
5
|
+
// src/reproducibility.ts
|
|
6
|
+
import { calculateContentHash } from "@hardkas/artifacts";
|
|
7
|
+
import { runLinearChain, runWideDag, profileMass } from "@hardkas/simulator";
|
|
8
|
+
function generateReproducibilityReport() {
|
|
9
|
+
const l1Plan = {
|
|
10
|
+
schema: "hardkas.txPlan",
|
|
11
|
+
networkId: "simnet",
|
|
12
|
+
mode: "simulated",
|
|
13
|
+
from: { address: "kaspatest:qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
|
|
14
|
+
to: { address: "kaspatest:qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
|
|
15
|
+
amountSompi: "100000000",
|
|
16
|
+
inputs: [
|
|
17
|
+
{ outpoint: { transactionId: "a".repeat(64), index: 0 }, amountSompi: "200000000" }
|
|
18
|
+
],
|
|
19
|
+
outputs: [
|
|
20
|
+
{ address: "kaspatest:qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "100000000" },
|
|
21
|
+
{ address: "kaspatest:qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv", amountSompi: "99998000" }
|
|
22
|
+
],
|
|
23
|
+
estimatedMass: "2000",
|
|
24
|
+
estimatedFeeSompi: "2000"
|
|
25
|
+
};
|
|
26
|
+
const l1Signed = {
|
|
27
|
+
schema: "hardkas.signedTx",
|
|
28
|
+
networkId: "simnet",
|
|
29
|
+
mode: "simulated",
|
|
30
|
+
rawTransaction: "0".repeat(128),
|
|
31
|
+
inputs: l1Plan.inputs,
|
|
32
|
+
outputs: l1Plan.outputs,
|
|
33
|
+
mass: "2000",
|
|
34
|
+
feeSompi: "2000"
|
|
35
|
+
};
|
|
36
|
+
const igraPlan = {
|
|
37
|
+
schema: "hardkas.igraTxPlan.v1",
|
|
38
|
+
networkId: "simnet",
|
|
39
|
+
mode: "l2-rpc",
|
|
40
|
+
l2Network: "igra-devnet",
|
|
41
|
+
chainId: 42069,
|
|
42
|
+
request: {
|
|
43
|
+
from: "0x1234567890123456789012345678901234567890",
|
|
44
|
+
to: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
|
|
45
|
+
data: "0x",
|
|
46
|
+
valueWei: "1000000000000000000"
|
|
47
|
+
},
|
|
48
|
+
status: "built"
|
|
49
|
+
};
|
|
50
|
+
const linearResult = runLinearChain({ name: "repro-linear", blockCount: 10 });
|
|
51
|
+
const wideResult = runWideDag({ name: "repro-wide", blockCount: 10, k: 18 });
|
|
52
|
+
const linearTotal = linearResult.metrics.blueBlocks + linearResult.metrics.redBlocks;
|
|
53
|
+
const linearRedPpm = linearTotal > 0 ? Math.trunc(linearResult.metrics.redBlocks * 1e6 / linearTotal) : 0;
|
|
54
|
+
const wideTotal = wideResult.metrics.blueBlocks + wideResult.metrics.redBlocks;
|
|
55
|
+
const wideRedPpm = wideTotal > 0 ? Math.trunc(wideResult.metrics.redBlocks * 1e6 / wideTotal) : 0;
|
|
56
|
+
const massResult = profileMass({ inputCount: 3, outputCount: 2, payloadBytes: 0, feeRate: 1n });
|
|
57
|
+
const nestedObj = {
|
|
58
|
+
z: { b: 2, a: 1 },
|
|
59
|
+
y: [3, 1, 2],
|
|
60
|
+
x: null,
|
|
61
|
+
w: "test",
|
|
62
|
+
v: 123456789012345678901234567890n
|
|
63
|
+
};
|
|
64
|
+
const harness = createTestHarness({ accounts: 3, initialBalance: 100000000000n });
|
|
65
|
+
const names = harness.accountNames();
|
|
66
|
+
const txResult = harness.send({
|
|
67
|
+
from: names[0],
|
|
68
|
+
to: names[1],
|
|
69
|
+
amountSompi: 10000000000n
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
proofVersion: "repro-v0",
|
|
73
|
+
hardkasVersion: "0.3.0-alpha",
|
|
74
|
+
artifacts: {
|
|
75
|
+
l1Plan: calculateContentHash(l1Plan),
|
|
76
|
+
l1Signed: calculateContentHash(l1Signed),
|
|
77
|
+
igraPlan: calculateContentHash(igraPlan),
|
|
78
|
+
dagLinearScenario: calculateContentHash({
|
|
79
|
+
totalBlocks: linearResult.metrics.totalBlocks,
|
|
80
|
+
blueBlocks: linearResult.metrics.blueBlocks,
|
|
81
|
+
redBlocks: linearResult.metrics.redBlocks,
|
|
82
|
+
redRatioPpm: linearRedPpm,
|
|
83
|
+
selectedChainLength: linearResult.metrics.selectedChainLength
|
|
84
|
+
}),
|
|
85
|
+
dagWideScenario: calculateContentHash({
|
|
86
|
+
totalBlocks: wideResult.metrics.totalBlocks,
|
|
87
|
+
blueBlocks: wideResult.metrics.blueBlocks,
|
|
88
|
+
redBlocks: wideResult.metrics.redBlocks,
|
|
89
|
+
redRatioPpm: wideRedPpm
|
|
90
|
+
}),
|
|
91
|
+
massProfile: calculateContentHash({
|
|
92
|
+
totalMass: massResult.totalMass.toString(),
|
|
93
|
+
inputMass: massResult.inputMass.toString(),
|
|
94
|
+
outputMass: massResult.outputMass.toString(),
|
|
95
|
+
estimatedFeeSompi: massResult.estimatedFeeSompi.toString()
|
|
96
|
+
}),
|
|
97
|
+
canonicalNested: calculateContentHash(nestedObj),
|
|
98
|
+
simulatedTxReceipt: txResult.ok ? calculateContentHash({
|
|
99
|
+
status: txResult.receipt.status,
|
|
100
|
+
txId: txResult.receipt.txId
|
|
101
|
+
}) : "TX_FAILED"
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export {
|
|
107
|
+
generateReproducibilityReport
|
|
108
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { LocalnetState } from '@hardkas/localnet';
|
|
2
|
+
|
|
3
|
+
interface HarnessConfig {
|
|
4
|
+
accounts?: number | undefined;
|
|
5
|
+
initialBalance?: bigint | undefined;
|
|
6
|
+
networkId?: string | undefined;
|
|
7
|
+
ghostdagK?: number | undefined;
|
|
8
|
+
}
|
|
9
|
+
interface TestHarness {
|
|
10
|
+
/** The current localnet state. */
|
|
11
|
+
state: LocalnetState;
|
|
12
|
+
/** Send KAS from one account to another. Returns the receipt. */
|
|
13
|
+
send(opts: {
|
|
14
|
+
from: string;
|
|
15
|
+
to: string;
|
|
16
|
+
amountSompi: bigint;
|
|
17
|
+
}): SendResult;
|
|
18
|
+
/** Get balance of an account by name (e.g., "alice"). */
|
|
19
|
+
balanceOf(name: string): bigint;
|
|
20
|
+
/** Get all account names. */
|
|
21
|
+
accountNames(): string[];
|
|
22
|
+
/** Take a snapshot of current state. */
|
|
23
|
+
snapshot(): any;
|
|
24
|
+
/** Reset to initial state. */
|
|
25
|
+
reset(): void;
|
|
26
|
+
}
|
|
27
|
+
interface SendResult {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
receipt: any;
|
|
30
|
+
plan: any;
|
|
31
|
+
preBalance: {
|
|
32
|
+
from: bigint;
|
|
33
|
+
to: bigint;
|
|
34
|
+
};
|
|
35
|
+
postBalance: {
|
|
36
|
+
from: bigint;
|
|
37
|
+
to: bigint;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
interface MassRecord {
|
|
41
|
+
testName?: string;
|
|
42
|
+
txId: string;
|
|
43
|
+
inputCount: number;
|
|
44
|
+
outputCount: number;
|
|
45
|
+
estimatedMass: bigint;
|
|
46
|
+
estimatedFeeSompi: bigint;
|
|
47
|
+
timestamp: number;
|
|
48
|
+
}
|
|
49
|
+
declare function enableMassTracking(): void;
|
|
50
|
+
declare function disableMassTracking(): void;
|
|
51
|
+
declare function getMassRecords(): MassRecord[];
|
|
52
|
+
declare function clearMassRecords(): void;
|
|
53
|
+
declare function createTestHarness(config?: HarnessConfig): TestHarness;
|
|
54
|
+
|
|
55
|
+
export { type HarnessConfig, type MassRecord, type SendResult, type TestHarness, clearMassRecords, createTestHarness, disableMassTracking, enableMassTracking, getMassRecords };
|
package/dist/harness.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearMassRecords,
|
|
3
|
+
createTestHarness,
|
|
4
|
+
disableMassTracking,
|
|
5
|
+
enableMassTracking,
|
|
6
|
+
getMassRecords
|
|
7
|
+
} from "./chunk-CUJL53GG.js";
|
|
8
|
+
export {
|
|
9
|
+
clearMassRecords,
|
|
10
|
+
createTestHarness,
|
|
11
|
+
disableMassTracking,
|
|
12
|
+
enableMassTracking,
|
|
13
|
+
getMassRecords
|
|
14
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,69 +1,100 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { TestHarness } from './harness.js';
|
|
2
|
+
export { HarnessConfig, SendResult, clearMassRecords, createTestHarness, disableMassTracking, enableMassTracking, getMassRecords } from './harness.js';
|
|
3
|
+
export { ReproducibilityReport, generateReproducibilityReport } from './reproducibility.js';
|
|
4
|
+
export { AdversarialFixtures } from './adversarial-fixtures.js';
|
|
5
|
+
import '@hardkas/localnet';
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Guarantee: A Lineage chain MUST be strictly monotonic and linked.
|
|
17
|
-
*/
|
|
18
|
-
verifyLineageContinuity(artifacts: BaseArtifact<any>[]): boolean;
|
|
19
|
-
/**
|
|
20
|
-
* Guarantee: Every emission to the event bus MUST have a timestamp.
|
|
21
|
-
*/
|
|
22
|
-
verifyEventCompliance(event: CoreEvent): boolean;
|
|
23
|
-
};
|
|
24
|
-
/**
|
|
25
|
-
* Invariant Watcher
|
|
26
|
-
* Can be hooked into the event bus to detect violations in real-time.
|
|
27
|
-
*/
|
|
28
|
-
declare class InvariantWatcher {
|
|
29
|
-
private violations;
|
|
30
|
-
constructor();
|
|
31
|
-
getViolations(): string[];
|
|
32
|
-
hasViolations(): boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface FuzzResult {
|
|
36
|
-
ok: boolean;
|
|
37
|
-
iterations: number;
|
|
38
|
-
violations: string[];
|
|
7
|
+
interface FixtureDefinition {
|
|
8
|
+
name: string;
|
|
9
|
+
accounts?: number;
|
|
10
|
+
initialBalance?: bigint;
|
|
11
|
+
/** Pre-run transactions to set up state. */
|
|
12
|
+
setup?: Array<{
|
|
13
|
+
from: string;
|
|
14
|
+
to: string;
|
|
15
|
+
amountSompi: bigint;
|
|
16
|
+
}>;
|
|
39
17
|
}
|
|
40
18
|
/**
|
|
41
|
-
*
|
|
42
|
-
* Verifies that sum(inputs) == sum(outputs) + fee across random transaction sequences.
|
|
19
|
+
* Create a fixture — a pre-configured harness with setup transactions already applied.
|
|
43
20
|
*/
|
|
44
|
-
declare function
|
|
21
|
+
declare function createFixture(def: FixtureDefinition): TestHarness;
|
|
45
22
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
23
|
+
interface HardKasMatchers<R = void> {
|
|
24
|
+
/** Assert a receipt has status "accepted". */
|
|
25
|
+
toBeAccepted(): R;
|
|
26
|
+
/** Assert a receipt has status "failed". */
|
|
27
|
+
toBeFailed(): R;
|
|
28
|
+
/** Assert a receipt contains a valid txId (starts with "simtx_" or is 64-char hex). */
|
|
29
|
+
toHaveValidTxId(): R;
|
|
30
|
+
/** Assert an artifact has a valid contentHash (64-char hex). */
|
|
31
|
+
toHaveValidContentHash(): R;
|
|
32
|
+
/** Assert an artifact passes lineage verification. */
|
|
33
|
+
toPassLineageCheck(): R;
|
|
34
|
+
/** Assert a GHOSTDAG result colors this block as blue. */
|
|
35
|
+
toBeBlueBlock(): R;
|
|
36
|
+
/** Assert a GHOSTDAG result colors this block as red. */
|
|
37
|
+
toBeRedBlock(): R;
|
|
38
|
+
/** Assert a balance (bigint sompi) increased by at least `amount`. */
|
|
39
|
+
toHaveIncreasedBy(amount: bigint): R;
|
|
40
|
+
/** Assert a balance (bigint sompi) decreased by at least `amount`. */
|
|
41
|
+
toHaveDecreasedBy(amount: bigint): R;
|
|
42
|
+
/** Assert a simulated DAG has no red blocks. */
|
|
43
|
+
toHaveNoRedBlocks(): R;
|
|
44
|
+
/** Assert a simulated DAG has exactly N tips. */
|
|
45
|
+
toHaveDagWidth(width: number): R;
|
|
56
46
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
47
|
+
declare module "vitest" {
|
|
48
|
+
interface Assertion<T = any> extends HardKasMatchers<T> {
|
|
49
|
+
}
|
|
50
|
+
interface AsymmetricMatchersContaining extends HardKasMatchers {
|
|
51
|
+
}
|
|
62
52
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
53
|
+
declare const hardKasMatchers: {
|
|
54
|
+
toBeAccepted(received: any): {
|
|
55
|
+
pass: boolean;
|
|
56
|
+
message: () => string;
|
|
57
|
+
};
|
|
58
|
+
toBeFailed(received: any): {
|
|
59
|
+
pass: boolean;
|
|
60
|
+
message: () => string;
|
|
61
|
+
};
|
|
62
|
+
toHaveValidTxId(received: any): {
|
|
63
|
+
pass: boolean;
|
|
64
|
+
message: () => string;
|
|
65
|
+
};
|
|
66
|
+
toHaveValidContentHash(received: any): {
|
|
67
|
+
pass: boolean;
|
|
68
|
+
message: () => string;
|
|
69
|
+
};
|
|
70
|
+
toPassLineageCheck(received: any): {
|
|
71
|
+
pass: boolean;
|
|
72
|
+
message: () => "Expected artifact NOT to pass lineage check" | "Expected artifact to pass lineage check (missing or empty lineage)";
|
|
73
|
+
};
|
|
74
|
+
toBeBlueBlock(received: any): {
|
|
75
|
+
pass: boolean;
|
|
76
|
+
message: () => "Expected block NOT to be blue, but it is" | "Expected block to be blue, but it is red or unknown";
|
|
77
|
+
};
|
|
78
|
+
toBeRedBlock(received: any): {
|
|
79
|
+
pass: boolean;
|
|
80
|
+
message: () => "Expected block NOT to be red, but it is" | "Expected block to be red, but it is blue or unknown";
|
|
81
|
+
};
|
|
82
|
+
toHaveIncreasedBy(received: any, amount: bigint): {
|
|
83
|
+
pass: boolean;
|
|
84
|
+
message: () => string;
|
|
85
|
+
};
|
|
86
|
+
toHaveDecreasedBy(received: any, amount: bigint): {
|
|
87
|
+
pass: boolean;
|
|
88
|
+
message: () => string;
|
|
89
|
+
};
|
|
90
|
+
toHaveNoRedBlocks(received: any): {
|
|
91
|
+
pass: boolean;
|
|
92
|
+
message: () => string;
|
|
93
|
+
};
|
|
94
|
+
toHaveDagWidth(received: any, width: number): {
|
|
95
|
+
pass: boolean;
|
|
96
|
+
message: () => string;
|
|
97
|
+
};
|
|
98
|
+
};
|
|
68
99
|
|
|
69
|
-
export { type
|
|
100
|
+
export { type FixtureDefinition, type HardKasMatchers, TestHarness, createFixture, hardKasMatchers };
|
package/dist/index.js
CHANGED
|
@@ -1,170 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
AdversarialFixtures
|
|
3
|
+
} from "./chunk-3EK7ATR5.js";
|
|
4
|
+
import {
|
|
5
|
+
generateReproducibilityReport
|
|
6
|
+
} from "./chunk-WIN7YDBM.js";
|
|
7
|
+
import {
|
|
8
|
+
clearMassRecords,
|
|
9
|
+
createTestHarness,
|
|
10
|
+
disableMassTracking,
|
|
11
|
+
enableMassTracking,
|
|
12
|
+
getMassRecords
|
|
13
|
+
} from "./chunk-CUJL53GG.js";
|
|
14
|
+
import {
|
|
15
|
+
hardKasMatchers
|
|
16
|
+
} from "./chunk-UHH25II3.js";
|
|
4
17
|
|
|
5
|
-
// src/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* Guarantee: A Lineage chain MUST be strictly monotonic and linked.
|
|
18
|
-
*/
|
|
19
|
-
verifyLineageContinuity(artifacts) {
|
|
20
|
-
if (artifacts.length < 2) return true;
|
|
21
|
-
const sorted = [...artifacts].sort(
|
|
22
|
-
(a, b) => (a.lineage?.sequence ?? 0) - (b.lineage?.sequence ?? 0)
|
|
23
|
-
);
|
|
24
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
25
|
-
const prev = sorted[i - 1];
|
|
26
|
-
const curr = sorted[i];
|
|
27
|
-
if (!prev || !curr) continue;
|
|
28
|
-
if (curr.lineage?.parentArtifactId !== prev.lineage?.artifactId) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
if (curr.lineage?.lineageId !== prev.lineage?.lineageId) {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return true;
|
|
36
|
-
},
|
|
37
|
-
/**
|
|
38
|
-
* Guarantee: Every emission to the event bus MUST have a timestamp.
|
|
39
|
-
*/
|
|
40
|
-
verifyEventCompliance(event) {
|
|
41
|
-
return !!event.timestamp;
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
var InvariantWatcher = class {
|
|
45
|
-
violations = [];
|
|
46
|
-
constructor() {
|
|
47
|
-
coreEvents.on((event) => {
|
|
48
|
-
if (!Invariants.verifyEventCompliance(event)) {
|
|
49
|
-
this.violations.push(`Event compliance violation: ${event.kind}`);
|
|
50
|
-
}
|
|
51
|
-
if (event.kind === "integrity.hash_mismatch") {
|
|
52
|
-
const payload = event.payload;
|
|
53
|
-
this.violations.push(`Hash mismatch detected: ${payload.artifactId || event.artifactId}`);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
getViolations() {
|
|
58
|
-
return this.violations;
|
|
59
|
-
}
|
|
60
|
-
hasViolations() {
|
|
61
|
-
return this.violations.length > 0;
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// src/utxo-fuzzer.ts
|
|
66
|
-
import { SOMPI_PER_KAS } from "@hardkas/core";
|
|
67
|
-
import { buildPaymentPlan } from "@hardkas/tx-builder";
|
|
68
|
-
import { applySimulatedPayment, createInitialLocalnetState } from "@hardkas/localnet";
|
|
69
|
-
async function runUtxoFuzzer(iterations = 50) {
|
|
70
|
-
let state = createInitialLocalnetState({ accounts: 5, initialBalanceSompi: 1000n * SOMPI_PER_KAS });
|
|
71
|
-
const violations = [];
|
|
72
|
-
for (let i = 0; i < iterations; i++) {
|
|
73
|
-
const fromIdx = Math.floor(Math.random() * state.accounts.length);
|
|
74
|
-
let toIdx = Math.floor(Math.random() * state.accounts.length);
|
|
75
|
-
if (fromIdx === toIdx) toIdx = (toIdx + 1) % state.accounts.length;
|
|
76
|
-
const fromAccount = state.accounts[fromIdx];
|
|
77
|
-
const toAccount = state.accounts[toIdx];
|
|
78
|
-
const amountSompi = BigInt(Math.floor(Math.random() * 10)) * SOMPI_PER_KAS + BigInt(Math.floor(Math.random() * 1e6));
|
|
79
|
-
try {
|
|
80
|
-
const unspent = state.utxos.filter((u) => u.address === fromAccount.address && !u.spent);
|
|
81
|
-
if (unspent.length === 0) continue;
|
|
82
|
-
const builderUtxos = unspent.map((u) => ({
|
|
83
|
-
outpoint: { transactionId: u.id.split(":")[0], index: 0 },
|
|
84
|
-
address: u.address,
|
|
85
|
-
amountSompi: BigInt(u.amountSompi),
|
|
86
|
-
scriptPublicKey: "mock"
|
|
87
|
-
}));
|
|
88
|
-
const plan = buildPaymentPlan({
|
|
89
|
-
fromAddress: fromAccount.address,
|
|
90
|
-
availableUtxos: builderUtxos,
|
|
91
|
-
outputs: [{ address: toAccount.address, amountSompi }],
|
|
92
|
-
feeRateSompiPerMass: 1n
|
|
93
|
-
});
|
|
94
|
-
const inputSum = plan.inputs.reduce((s, x) => s + x.amountSompi, 0n);
|
|
95
|
-
const outputSum = plan.outputs.reduce((s, x) => s + x.amountSompi, 0n) + (plan.change?.amountSompi || 0n);
|
|
96
|
-
const fee = plan.estimatedFeeSompi;
|
|
97
|
-
if (inputSum !== outputSum + fee) {
|
|
98
|
-
violations.push(`Iteration ${i}: Planning Invariant Violated! ${inputSum} != ${outputSum} + ${fee}`);
|
|
99
|
-
}
|
|
100
|
-
const result = applySimulatedPayment(state, {
|
|
101
|
-
from: fromAccount.name,
|
|
102
|
-
to: toAccount.name,
|
|
103
|
-
amountSompi
|
|
104
|
-
});
|
|
105
|
-
state = result.state;
|
|
106
|
-
const totalInState = state.utxos.filter((u) => !u.spent).reduce((s, x) => s + BigInt(x.amountSompi), 0n);
|
|
107
|
-
const expectedTotal = BigInt(state.accounts.length) * 1000n * SOMPI_PER_KAS - BigInt(i + 1) * fee;
|
|
108
|
-
const utxoIds = state.utxos.map((u) => u.id);
|
|
109
|
-
const uniqueIds = new Set(utxoIds);
|
|
110
|
-
if (utxoIds.length !== uniqueIds.size) {
|
|
111
|
-
violations.push(`Iteration ${i}: Duplicate UTXO IDs detected in state!`);
|
|
112
|
-
}
|
|
113
|
-
} catch (e) {
|
|
114
|
-
if (!e.message.includes("Insufficient funds")) {
|
|
115
|
-
violations.push(`Iteration ${i}: Unexpected Error: ${e.message}`);
|
|
18
|
+
// src/fixtures.ts
|
|
19
|
+
function createFixture(def) {
|
|
20
|
+
const config = {
|
|
21
|
+
accounts: def.accounts,
|
|
22
|
+
initialBalance: def.initialBalance
|
|
23
|
+
};
|
|
24
|
+
const harness = createTestHarness(config);
|
|
25
|
+
if (def.setup) {
|
|
26
|
+
for (const tx of def.setup) {
|
|
27
|
+
const result = harness.send(tx);
|
|
28
|
+
if (!result.ok) {
|
|
29
|
+
throw new Error(`Fixture "${def.name}" failed during setup: ${tx.from} -> ${tx.to} (${tx.amountSompi} sompi). Error: ${result.receipt?.errors?.join(", ") || "Unknown error"}`);
|
|
116
30
|
}
|
|
117
31
|
}
|
|
118
32
|
}
|
|
119
|
-
return
|
|
120
|
-
ok: violations.length === 0,
|
|
121
|
-
iterations,
|
|
122
|
-
violations
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// src/index.ts
|
|
127
|
-
function hardkasTest(options = {}) {
|
|
128
|
-
const cwd = options.cwd || process.env.HARDKAS_CWD || ".";
|
|
129
|
-
const network = options.network || process.env.HARDKAS_NETWORK || "simnet";
|
|
130
|
-
const autoStart = options.autoStartLocalnet ?? true;
|
|
131
|
-
const autoReset = options.resetBetweenTests ?? true;
|
|
132
|
-
let sdk;
|
|
133
|
-
beforeAll(async () => {
|
|
134
|
-
sdk = await Hardkas.open(cwd);
|
|
135
|
-
if (autoStart && network === "simnet") {
|
|
136
|
-
await sdk.localnet.start();
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
beforeEach(async () => {
|
|
140
|
-
if (autoReset && network === "simnet") {
|
|
141
|
-
await sdk.localnet.reset();
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
return {
|
|
145
|
-
get hardkas() {
|
|
146
|
-
return sdk;
|
|
147
|
-
},
|
|
148
|
-
get network() {
|
|
149
|
-
return network;
|
|
150
|
-
},
|
|
151
|
-
get accounts() {
|
|
152
|
-
return sdk.accounts;
|
|
153
|
-
},
|
|
154
|
-
get tx() {
|
|
155
|
-
return sdk.tx;
|
|
156
|
-
},
|
|
157
|
-
get localnet() {
|
|
158
|
-
return sdk.localnet;
|
|
159
|
-
},
|
|
160
|
-
get query() {
|
|
161
|
-
return sdk.query;
|
|
162
|
-
}
|
|
163
|
-
};
|
|
33
|
+
return harness;
|
|
164
34
|
}
|
|
165
35
|
export {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
36
|
+
AdversarialFixtures,
|
|
37
|
+
clearMassRecords,
|
|
38
|
+
createFixture,
|
|
39
|
+
createTestHarness,
|
|
40
|
+
disableMassTracking,
|
|
41
|
+
enableMassTracking,
|
|
42
|
+
generateReproducibilityReport,
|
|
43
|
+
getMassRecords,
|
|
44
|
+
hardKasMatchers
|
|
170
45
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
interface ReproducibilityReport {
|
|
2
|
+
/** Fixed proof version — does NOT change between releases. */
|
|
3
|
+
proofVersion: "repro-v0";
|
|
4
|
+
/** Informational only — NOT part of hash comparison. */
|
|
5
|
+
hardkasVersion: string;
|
|
6
|
+
artifacts: {
|
|
7
|
+
l1Plan: string;
|
|
8
|
+
l1Signed: string;
|
|
9
|
+
igraPlan: string;
|
|
10
|
+
dagLinearScenario: string;
|
|
11
|
+
dagWideScenario: string;
|
|
12
|
+
massProfile: string;
|
|
13
|
+
canonicalNested: string;
|
|
14
|
+
simulatedTxReceipt: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Generate the reproducibility report.
|
|
19
|
+
* This function MUST be fully deterministic.
|
|
20
|
+
* Same code version → same output → always → everywhere.
|
|
21
|
+
*/
|
|
22
|
+
declare function generateReproducibilityReport(): ReproducibilityReport;
|
|
23
|
+
|
|
24
|
+
export { type ReproducibilityReport, generateReproducibilityReport };
|
package/dist/setup.d.ts
ADDED
package/dist/setup.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./chunk-UHH25II3.js";
|
package/package.json
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hardkas/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-alpha",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
|
-
".": "./dist/index.js"
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./harness": "./dist/harness.js",
|
|
10
|
+
"./setup": "./dist/setup.js",
|
|
11
|
+
"./adversarial-fixtures": "./dist/adversarial-fixtures.js",
|
|
12
|
+
"./mass-setup": "./dist/mass-setup.js"
|
|
9
13
|
},
|
|
10
14
|
"dependencies": {
|
|
11
|
-
"@hardkas/artifacts": "0.
|
|
12
|
-
"@hardkas/
|
|
13
|
-
"@hardkas/
|
|
14
|
-
"@hardkas/core": "0.
|
|
15
|
-
"@hardkas/
|
|
16
|
-
"@hardkas/
|
|
17
|
-
"@hardkas/
|
|
15
|
+
"@hardkas/artifacts": "0.3.0-alpha",
|
|
16
|
+
"@hardkas/kaspa-rpc": "0.3.0-alpha",
|
|
17
|
+
"@hardkas/localnet": "0.3.0-alpha",
|
|
18
|
+
"@hardkas/core": "0.3.0-alpha",
|
|
19
|
+
"@hardkas/tx-builder": "0.3.0-alpha",
|
|
20
|
+
"@hardkas/query-store": "0.3.0-alpha",
|
|
21
|
+
"@hardkas/simulator": "0.3.0-alpha",
|
|
22
|
+
"@hardkas/sdk": "0.3.0-alpha"
|
|
18
23
|
},
|
|
19
24
|
"devDependencies": {
|
|
20
25
|
"tsup": "^8.3.5",
|
|
@@ -38,8 +43,9 @@
|
|
|
38
43
|
"README.md"
|
|
39
44
|
],
|
|
40
45
|
"scripts": {
|
|
41
|
-
"build": "tsup src/index.ts --format esm --dts --clean --external vitest",
|
|
42
|
-
"test": "vitest run",
|
|
46
|
+
"build": "tsup src/index.ts src/harness.ts src/setup.ts src/reproducibility.ts src/adversarial-fixtures.ts src/mass-setup.ts --format esm --dts --clean --external vitest",
|
|
47
|
+
"test": "vitest run && pnpm test:gauntlet",
|
|
48
|
+
"test:gauntlet": "node --import tsx --test test/gauntlet.node.ts",
|
|
43
49
|
"typecheck": "tsc --noEmit",
|
|
44
50
|
"lint": "eslint ."
|
|
45
51
|
}
|