@hardkas/testing 0.2.2-alpha → 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 +95 -20
- package/dist/index.js +39 -44
- 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 -10
|
@@ -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,25 +1,100 @@
|
|
|
1
|
-
import {
|
|
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';
|
|
2
6
|
|
|
3
|
-
interface
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
}>;
|
|
6
17
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Create a fixture — a pre-configured harness with setup transactions already applied.
|
|
20
|
+
*/
|
|
21
|
+
declare function createFixture(def: FixtureDefinition): TestHarness;
|
|
22
|
+
|
|
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;
|
|
46
|
+
}
|
|
47
|
+
declare module "vitest" {
|
|
48
|
+
interface Assertion<T = any> extends HardKasMatchers<T> {
|
|
49
|
+
}
|
|
50
|
+
interface AsymmetricMatchersContaining extends HardKasMatchers {
|
|
51
|
+
}
|
|
19
52
|
}
|
|
20
|
-
declare
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
};
|
|
23
98
|
};
|
|
24
99
|
|
|
25
|
-
export { type
|
|
100
|
+
export { type FixtureDefinition, type HardKasMatchers, TestHarness, createFixture, hardKasMatchers };
|
package/dist/index.js
CHANGED
|
@@ -1,50 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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";
|
|
17
|
+
|
|
18
|
+
// src/fixtures.ts
|
|
19
|
+
function createFixture(def) {
|
|
20
|
+
const config = {
|
|
21
|
+
accounts: def.accounts,
|
|
22
|
+
initialBalance: def.initialBalance
|
|
22
23
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
createMockUtxo({
|
|
30
|
-
address,
|
|
31
|
-
amountSompi,
|
|
32
|
-
index: 0
|
|
33
|
-
})
|
|
34
|
-
]);
|
|
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"}`);
|
|
35
30
|
}
|
|
36
|
-
},
|
|
37
|
-
async reset() {
|
|
38
|
-
rpc.setUtxos(wallets.alice.address, []);
|
|
39
|
-
rpc.setUtxos(wallets.bob.address, []);
|
|
40
|
-
rpc.setUtxos(wallets.carol.address, []);
|
|
41
31
|
}
|
|
42
|
-
}
|
|
32
|
+
}
|
|
33
|
+
return harness;
|
|
43
34
|
}
|
|
44
|
-
var hardkas = {
|
|
45
|
-
localnet: createHardkasTestContext
|
|
46
|
-
};
|
|
47
35
|
export {
|
|
48
|
-
|
|
49
|
-
|
|
36
|
+
AdversarialFixtures,
|
|
37
|
+
clearMassRecords,
|
|
38
|
+
createFixture,
|
|
39
|
+
createTestHarness,
|
|
40
|
+
disableMassTracking,
|
|
41
|
+
enableMassTracking,
|
|
42
|
+
generateReproducibilityReport,
|
|
43
|
+
getMassRecords,
|
|
44
|
+
hardKasMatchers
|
|
50
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,19 +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/
|
|
15
|
-
"@hardkas/
|
|
16
|
-
"@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"
|
|
17
23
|
},
|
|
18
24
|
"devDependencies": {
|
|
19
25
|
"tsup": "^8.3.5",
|
|
@@ -37,8 +43,9 @@
|
|
|
37
43
|
"README.md"
|
|
38
44
|
],
|
|
39
45
|
"scripts": {
|
|
40
|
-
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
41
|
-
"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",
|
|
42
49
|
"typecheck": "tsc --noEmit",
|
|
43
50
|
"lint": "eslint ."
|
|
44
51
|
}
|