@hardkas/simulator 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/index.d.ts CHANGED
@@ -24,6 +24,354 @@ interface SimulationResult {
24
24
  declare class TxSimulator {
25
25
  simulate(phases: readonly TxLifecyclePhase[], run?: (phase: TxLifecyclePhase) => Promise<void>): Promise<SimulationResult>;
26
26
  }
27
+ /**
28
+ * Creates a non-deterministic trace ID for debug purposes.
29
+ * Intentionally uses Date.now() + Math.random().
30
+ * Trace IDs are debug metadata only — they do NOT enter
31
+ * canonical hashing or artifact identity.
32
+ */
27
33
  declare function createTraceId(prefix?: string): string;
28
34
 
29
- export { type SimulationResult, type TxLifecyclePhase, TxSimulator, type TxTraceEvent, createTraceId };
35
+ /**
36
+ * Block hash as 64-char hex string.
37
+ * In simulation, derived via SHA-256 of block parameters.
38
+ * NOT a real Kaspa block hash — simulation IDs only.
39
+ */
40
+ type BlockHash = string;
41
+ /**
42
+ * Accumulated PoW work as bigint.
43
+ * rusty-kaspa uses U256; we use bigint (arbitrary precision in JS).
44
+ */
45
+ type BlueWorkType = bigint;
46
+ declare const GENESIS_HASH: BlockHash;
47
+ /**
48
+ * Full GHOSTDAG data for a block.
49
+ *
50
+ * Source: consensus/src/model/stores/ghostdag.rs lines 22-30
51
+ * [UNVALIDATED]: field semantics are structurally plausible
52
+ * but not fixture-validated against rusty-kaspa.
53
+ */
54
+ interface GhostdagData {
55
+ /** Count of blue blocks in the causal past, including this block's mergeset blues. */
56
+ readonly blueScore: number;
57
+ /** Cumulative PoW work of all blue blocks in causal past. */
58
+ readonly blueWork: BlueWorkType;
59
+ /** Parent with highest (blueWork, hash). */
60
+ readonly selectedParent: BlockHash;
61
+ /** Blue blocks in mergeset, ascending (blueWork, hash). First = selected parent. */
62
+ readonly mergesetBlues: readonly BlockHash[];
63
+ /** Red blocks in mergeset, ascending (blueWork, hash). */
64
+ readonly mergesetReds: readonly BlockHash[];
65
+ /** Anticone sizes for each blue in mergesetBlues. */
66
+ readonly bluesAnticoneSizes: readonly number[];
67
+ }
68
+ /**
69
+ * Compact projection for chain-walking.
70
+ * Source: consensus/src/model/stores/ghostdag.rs lines 32-37
71
+ */
72
+ interface CompactGhostdagData {
73
+ readonly blueScore: number;
74
+ readonly blueWork: BlueWorkType;
75
+ readonly selectedParent: BlockHash;
76
+ }
77
+ /**
78
+ * Simulated block header (simulation layer only — not Kaspa wire format).
79
+ */
80
+ interface SimBlockHeader {
81
+ readonly hash: BlockHash;
82
+ readonly parents: readonly BlockHash[];
83
+ readonly timestampUs: number;
84
+ readonly minerId: number;
85
+ /** Compact difficulty target. Work = BigInt(2**128) / BigInt(bits + 1). */
86
+ readonly bits: number;
87
+ readonly nonce: number;
88
+ }
89
+ /**
90
+ * A block as seen by the simulation (header + optional GHOSTDAG result).
91
+ */
92
+ interface SimBlock {
93
+ readonly header: SimBlockHeader;
94
+ readonly ghostdag?: GhostdagData;
95
+ }
96
+ declare function blockHash(block: SimBlock): BlockHash;
97
+ declare function blockParents(block: SimBlock): readonly BlockHash[];
98
+ declare function blockBlueWork(block: SimBlock): BlueWorkType;
99
+ declare function blockBlueScore(block: SimBlock): number;
100
+ declare function headerWork(header: SimBlockHeader): BlueWorkType;
101
+ declare function compactFromFull(g: GhostdagData): CompactGhostdagData;
102
+
103
+ /**
104
+ * Pairs a block hash with its blueWork for ordering.
105
+ * Mirrors SortableBlock in rusty-kaspa ordering.rs lines 14-24.
106
+ */
107
+ interface SortableBlock {
108
+ readonly hash: BlockHash;
109
+ readonly blueWork: BlueWorkType;
110
+ }
111
+ /**
112
+ * Compare two SortableBlocks: ascending by (blueWork, hash).
113
+ * Returns negative if a < b, positive if a > b, 0 if equal.
114
+ *
115
+ * Source: ordering.rs lines 38-42
116
+ */
117
+ declare function compareSortableBlocks(a: SortableBlock, b: SortableBlock): number;
118
+ /**
119
+ * Sort blocks in ascending (blueWork, hash) order.
120
+ * Source: ordering.rs lines 44-51 sort_blocks()
121
+ *
122
+ * Returns a NEW sorted array (does not mutate input).
123
+ */
124
+ declare function sortBlocks(blocks: readonly SortableBlock[]): SortableBlock[];
125
+ /**
126
+ * Find the selected parent: argmax over parents of (blueWork, hash).
127
+ * Source: protocol.rs lines 99-106 find_selected_parent()
128
+ *
129
+ * selected_parent = parents
130
+ * .map(p => SortableBlock { hash: p.hash, blueWork: p.blueWork })
131
+ * .max()
132
+ *
133
+ * Returns undefined only if parents is empty (invalid for any non-genesis block).
134
+ */
135
+ declare function findSelectedParent(parents: ReadonlyArray<{
136
+ hash: BlockHash;
137
+ blueWork: BlueWorkType;
138
+ }>): BlockHash | undefined;
139
+
140
+ /**
141
+ * In-memory GHOSTDAG data store.
142
+ * Mirrors MemoryGhostdagStore in rusty-kaspa.
143
+ */
144
+ declare class GhostdagStore {
145
+ private readonly full;
146
+ private readonly compact;
147
+ private readonly parents;
148
+ /**
149
+ * Insert full + compact entries atomically.
150
+ * Source: ghostdag.rs insert_batch() / update_batch() write both.
151
+ */
152
+ insert(hash: BlockHash, data: GhostdagData, parents?: BlockHash[]): void;
153
+ getData(hash: BlockHash): GhostdagData | undefined;
154
+ getBlueScore(hash: BlockHash): number | undefined;
155
+ getBlueWork(hash: BlockHash): BlueWorkType | undefined;
156
+ getSelectedParent(hash: BlockHash): BlockHash | undefined;
157
+ has(hash: BlockHash): boolean;
158
+ getParents(hash: BlockHash): BlockHash[] | undefined;
159
+ get size(): number;
160
+ }
161
+ /**
162
+ * Genesis GHOSTDAG data.
163
+ * [UNVALIDATED]: genesis blue_score=0, blue_work=0, selected_parent=genesis.
164
+ */
165
+ declare function genesisGhostdagData(genesisHash?: BlockHash): GhostdagData;
166
+
167
+ /**
168
+ * Compute the full past set of `start` via BFS.
169
+ * [RESEARCH_EXPERIMENTAL]: O(|past(start)|) per call.
170
+ * rusty-kaspa uses O(1) reachability interval tree.
171
+ */
172
+ declare function pastSet(start: BlockHash, allBlocks: ReadonlyMap<BlockHash, SimBlock>): Set<BlockHash>;
173
+ /**
174
+ * Check if `ancestorCandidate` is in the causal past of `descendant`.
175
+ * [RESEARCH_EXPERIMENTAL]: BFS approximation of ReachabilityService.
176
+ */
177
+ declare function isDagAncestorOf(ancestorCandidate: BlockHash, descendant: BlockHash, allBlocks: ReadonlyMap<BlockHash, SimBlock>): boolean;
178
+ /**
179
+ * Compute the unordered mergeset of `block`, excluding its `selectedParent`.
180
+ *
181
+ * Mergeset = blocks in past(block) that are NOT in past(selectedParent).
182
+ * Genesis is never in the mergeset.
183
+ *
184
+ * [RESEARCH_EXPERIMENTAL]: BFS-based. Source: mergeset.rs lines 9-40
185
+ */
186
+ declare function unorderedMergesetWithoutSelectedParent(block: SimBlock, selectedParent: BlockHash, allBlocks: ReadonlyMap<BlockHash, SimBlock>): BlockHash[];
187
+ /**
188
+ * Compute the mergeset sorted in ascending (blueWork, hash) order.
189
+ * Source: mergeset.rs lines 43-45 ordered_mergeset_without_selected_parent()
190
+ *
191
+ * [RESEARCH_EXPERIMENTAL]: sorting semantics correct, blue_work values approximate.
192
+ */
193
+ declare function orderedMergesetWithoutSelectedParent(block: SimBlock, selectedParent: BlockHash, allBlocks: ReadonlyMap<BlockHash, SimBlock>, gdStore: GhostdagStore): SortableBlock[];
194
+
195
+ /** The GHOSTDAG K parameter. Post-Crescendo mainnet (10 BPS): K = 18. */
196
+ declare const DEFAULT_K = 18;
197
+ type CandidateColor = "blue" | "red";
198
+ /**
199
+ * Approximate GHOSTDAG engine.
200
+ *
201
+ * Computes GhostdagData for each block given its DAG context.
202
+ * Uses BFS-based reachability and pairwise anticone checks.
203
+ *
204
+ * [RESEARCH_EXPERIMENTAL]: suitable for simulation, NOT for consensus equivalence.
205
+ */
206
+ declare class ApproxGhostdagEngine {
207
+ readonly k: number;
208
+ readonly genesisHash: BlockHash;
209
+ constructor(k?: number, genesisHash?: BlockHash);
210
+ /**
211
+ * Compute GhostdagData for `block`.
212
+ *
213
+ * Intentionally non-deterministic for TEMP_INDEXING_HASH as it uses Date.now()
214
+ * for dummy block headers.
215
+ *
216
+ * Source: protocol.rs lines 126-166 ghostdag()
217
+ *
218
+ * Steps:
219
+ * 1. findSelectedParent()
220
+ * 2. orderedMergeset()
221
+ * 3. color each candidate (check_blue_candidate)
222
+ * 4. blueScore = sp.blueScore + |mergesetBlues|
223
+ * 5. blueWork = sp.blueWork + Σ work(blues)
224
+ *
225
+ * All steps are [RESEARCH_EXPERIMENTAL].
226
+ */
227
+ ghostdag(parents: BlockHash[], gdStore: GhostdagStore): GhostdagData;
228
+ /**
229
+ * Compute GhostdagData for `block`.
230
+ */
231
+ computeGhostdag(block: SimBlock, allBlocks: ReadonlyMap<BlockHash, SimBlock>, gdStore: GhostdagStore): GhostdagData;
232
+ }
233
+
234
+ interface DagMetrics {
235
+ /** Total blocks in the DAG (excluding genesis). */
236
+ totalBlocks: number;
237
+ /** Number of blue blocks. */
238
+ blueBlocks: number;
239
+ /** Number of red blocks. */
240
+ redBlocks: number;
241
+ /** red / (blue + red). */
242
+ redRatio: number;
243
+ /** Average number of parents per block. */
244
+ meanParents: number;
245
+ /** Number of tip blocks (no children). */
246
+ dagWidth: number;
247
+ /** Highest blueScore in the DAG. */
248
+ maxBlueScore: number;
249
+ /** Highest blueWork in the DAG. */
250
+ maxBlueWork: bigint;
251
+ /** Length of the selected parent chain from sink to genesis. */
252
+ selectedChainLength: number;
253
+ /** Block IDs on the selected chain. */
254
+ selectedChain: string[];
255
+ }
256
+ declare function computeDagMetrics(allBlocks: Map<BlockHash, SimBlock>, gdStore: GhostdagStore, genesisHash: BlockHash): DagMetrics;
257
+
258
+ interface ScenarioConfig {
259
+ /** Scenario name. */
260
+ name: string;
261
+ /** GHOSTDAG K parameter. */
262
+ k?: number;
263
+ /** Number of blocks to generate (excluding genesis). */
264
+ blockCount: number;
265
+ /** Difficulty bits for all blocks. */
266
+ bits?: number;
267
+ }
268
+ interface ScenarioResult {
269
+ name: string;
270
+ config: ScenarioConfig;
271
+ metrics: DagMetrics;
272
+ /** Time to compute in milliseconds. */
273
+ computeTimeMs: number;
274
+ }
275
+ declare function runLinearChain(config: ScenarioConfig): ScenarioResult;
276
+ declare function runWideDag(config: ScenarioConfig): ScenarioResult;
277
+ declare function runForkResolution(config: ScenarioConfig & {
278
+ forkPoint: number;
279
+ }): ScenarioResult;
280
+ declare function runDiamondDag(config: ScenarioConfig): ScenarioResult;
281
+ declare function runAllScenarios(config: ScenarioConfig): ScenarioResult[];
282
+
283
+ declare function formatScenarioReport(results: ScenarioResult[]): string;
284
+
285
+ interface MassBreakdown {
286
+ /** Total estimated mass. */
287
+ totalMass: bigint;
288
+ /** Mass contributed by inputs. */
289
+ inputMass: bigint;
290
+ /** Mass contributed by outputs. */
291
+ outputMass: bigint;
292
+ /** Mass contributed by payload/metadata. */
293
+ payloadMass: bigint;
294
+ /** Estimated fee in sompi at the given fee rate. */
295
+ estimatedFeeSompi: bigint;
296
+ /** Fee rate used (sompi per mass unit). */
297
+ feeRate: bigint;
298
+ /** Number of inputs. */
299
+ inputCount: number;
300
+ /** Number of outputs. */
301
+ outputCount: number;
302
+ /** Payload size in bytes. */
303
+ payloadBytes: number;
304
+ }
305
+ interface MassComparison {
306
+ /** Current profile. */
307
+ current: MassBreakdown;
308
+ /** Previous profile to compare against. */
309
+ previous: MassBreakdown;
310
+ /** Absolute difference in total mass. */
311
+ massDelta: bigint;
312
+ /** Absolute difference in fee. */
313
+ feeDelta: bigint;
314
+ /** Percentage change in mass (positive = increase). */
315
+ massChangePercent: number;
316
+ /** Percentage change in fee. */
317
+ feeChangePercent: number;
318
+ /** True if mass increased (potential regression). */
319
+ isRegression: boolean;
320
+ /** Regression severity: "none" | "minor" (<10%) | "major" (>=10%) */
321
+ severity: "none" | "minor" | "major";
322
+ }
323
+ /**
324
+ * Profiles transaction mass based on counts and sizes.
325
+ */
326
+ declare function profileMass(opts: {
327
+ inputCount: number;
328
+ outputCount: number;
329
+ payloadBytes?: number;
330
+ feeRate?: bigint;
331
+ }): MassBreakdown;
332
+ /**
333
+ * Compares two mass profiles and detects regressions.
334
+ */
335
+ declare function compareMassProfiles(current: MassBreakdown, previous: MassBreakdown): MassComparison;
336
+ /**
337
+ * Formats a mass breakdown into a readable report.
338
+ */
339
+ declare function formatMassProfile(breakdown: MassBreakdown): string;
340
+ /**
341
+ * Formats a comparison report.
342
+ */
343
+ declare function formatMassComparison(comparison: MassComparison): string;
344
+
345
+ interface MassSnapshot {
346
+ /** Snapshot label (e.g., "basic-transfer", "multi-output"). */
347
+ label: string;
348
+ /** When the snapshot was taken. */
349
+ timestamp: string;
350
+ /** The mass breakdown. */
351
+ breakdown: MassBreakdown;
352
+ }
353
+ interface MassSnapshotStore {
354
+ snapshots: MassSnapshot[];
355
+ }
356
+ /**
357
+ * Saves a mass breakdown to a persistent snapshot.
358
+ */
359
+ declare function saveMassSnapshot(dir: string, label: string, breakdown: MassBreakdown): void;
360
+ /**
361
+ * Loads a saved mass snapshot.
362
+ */
363
+ declare function loadMassSnapshot(dir: string, label: string): MassSnapshot | undefined;
364
+ /**
365
+ * Profiles current mass, compares with previous if exists, and saves new snapshot.
366
+ */
367
+ declare function profileAndCompare(opts: {
368
+ inputCount: number;
369
+ outputCount: number;
370
+ payloadBytes?: number;
371
+ feeRate?: bigint;
372
+ }, snapshotDir: string, label: string): {
373
+ breakdown: MassBreakdown;
374
+ comparison?: MassComparison;
375
+ };
376
+
377
+ export { ApproxGhostdagEngine, type BlockHash, type BlueWorkType, type CandidateColor, type CompactGhostdagData, DEFAULT_K, type DagMetrics, GENESIS_HASH, type GhostdagData, GhostdagStore, type MassBreakdown, type MassComparison, type MassSnapshot, type MassSnapshotStore, type ScenarioConfig, type ScenarioResult, type SimBlock, type SimBlockHeader, type SimulationResult, type SortableBlock, type TxLifecyclePhase, TxSimulator, type TxTraceEvent, blockBlueScore, blockBlueWork, blockHash, blockParents, compactFromFull, compareMassProfiles, compareSortableBlocks, computeDagMetrics, createTraceId, findSelectedParent, formatMassComparison, formatMassProfile, formatScenarioReport, genesisGhostdagData, headerWork, isDagAncestorOf, loadMassSnapshot, orderedMergesetWithoutSelectedParent, pastSet, profileAndCompare, profileMass, runAllScenarios, runDiamondDag, runForkResolution, runLinearChain, runWideDag, saveMassSnapshot, sortBlocks, unorderedMergesetWithoutSelectedParent };
package/dist/index.js CHANGED
@@ -1,17 +1,11 @@
1
- // src/index.ts
1
+ // src/tx-simulator.ts
2
2
  var TxSimulator = class {
3
3
  async simulate(phases, run) {
4
4
  const events = [];
5
5
  for (const phase of phases) {
6
- events.push({
7
- type: "phase.started",
8
- phase,
9
- timestamp: Date.now()
10
- });
6
+ events.push({ type: "phase.started", phase, timestamp: Date.now() });
11
7
  try {
12
- if (run) {
13
- await run(phase);
14
- }
8
+ if (run) await run(phase);
15
9
  } catch (error) {
16
10
  events.push({
17
11
  type: "tx.failed",
@@ -19,27 +13,776 @@ var TxSimulator = class {
19
13
  reason: error instanceof Error ? error.message : "Unknown error",
20
14
  timestamp: Date.now()
21
15
  });
22
- return {
23
- ok: false,
24
- events
25
- };
16
+ return { ok: false, events };
26
17
  }
27
- events.push({
28
- type: "phase.completed",
29
- phase,
30
- timestamp: Date.now()
31
- });
18
+ events.push({ type: "phase.completed", phase, timestamp: Date.now() });
32
19
  }
33
- return {
34
- ok: true,
35
- events
36
- };
20
+ return { ok: true, events };
37
21
  }
38
22
  };
39
23
  function createTraceId(prefix = "trace") {
40
24
  return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
41
25
  }
26
+
27
+ // src/ghostdag-types.ts
28
+ var GENESIS_HASH = "0".repeat(64);
29
+ function blockHash(block) {
30
+ return block.header.hash;
31
+ }
32
+ function blockParents(block) {
33
+ return block.header.parents;
34
+ }
35
+ function blockBlueWork(block) {
36
+ return block.ghostdag?.blueWork ?? 0n;
37
+ }
38
+ function blockBlueScore(block) {
39
+ return block.ghostdag?.blueScore ?? 0;
40
+ }
41
+ var MAX_WORK = 2n ** 128n;
42
+ function headerWork(header) {
43
+ return MAX_WORK / (BigInt(header.bits) + 1n);
44
+ }
45
+ function compactFromFull(g) {
46
+ return {
47
+ blueScore: g.blueScore,
48
+ blueWork: g.blueWork,
49
+ selectedParent: g.selectedParent
50
+ };
51
+ }
52
+
53
+ // src/ordering.ts
54
+ function compareSortableBlocks(a, b) {
55
+ if (a.blueWork < b.blueWork) return -1;
56
+ if (a.blueWork > b.blueWork) return 1;
57
+ if (a.hash < b.hash) return -1;
58
+ if (a.hash > b.hash) return 1;
59
+ return 0;
60
+ }
61
+ function sortBlocks(blocks) {
62
+ return [...blocks].sort(compareSortableBlocks);
63
+ }
64
+ function findSelectedParent(parents) {
65
+ if (parents.length === 0) return void 0;
66
+ let best = { hash: parents[0].hash, blueWork: parents[0].blueWork };
67
+ for (let i = 1; i < parents.length; i++) {
68
+ const candidate = { hash: parents[i].hash, blueWork: parents[i].blueWork };
69
+ if (compareSortableBlocks(candidate, best) > 0) {
70
+ best = candidate;
71
+ }
72
+ }
73
+ return best.hash;
74
+ }
75
+
76
+ // src/ghostdag-store.ts
77
+ var GhostdagStore = class {
78
+ full = /* @__PURE__ */ new Map();
79
+ compact = /* @__PURE__ */ new Map();
80
+ parents = /* @__PURE__ */ new Map();
81
+ /**
82
+ * Insert full + compact entries atomically.
83
+ * Source: ghostdag.rs insert_batch() / update_batch() write both.
84
+ */
85
+ insert(hash, data, parents = []) {
86
+ this.compact.set(hash, compactFromFull(data));
87
+ this.full.set(hash, data);
88
+ this.parents.set(hash, parents);
89
+ }
90
+ getData(hash) {
91
+ return this.full.get(hash);
92
+ }
93
+ getBlueScore(hash) {
94
+ return this.compact.get(hash)?.blueScore;
95
+ }
96
+ getBlueWork(hash) {
97
+ return this.compact.get(hash)?.blueWork;
98
+ }
99
+ getSelectedParent(hash) {
100
+ return this.compact.get(hash)?.selectedParent;
101
+ }
102
+ has(hash) {
103
+ return this.compact.has(hash);
104
+ }
105
+ getParents(hash) {
106
+ return this.parents.get(hash);
107
+ }
108
+ get size() {
109
+ return this.full.size;
110
+ }
111
+ };
112
+ function genesisGhostdagData(genesisHash = GENESIS_HASH) {
113
+ return {
114
+ blueScore: 0,
115
+ blueWork: 0n,
116
+ selectedParent: genesisHash,
117
+ mergesetBlues: [genesisHash],
118
+ mergesetReds: [],
119
+ bluesAnticoneSizes: [0]
120
+ };
121
+ }
122
+
123
+ // src/reachability.ts
124
+ function pastSet(start, allBlocks) {
125
+ const past = /* @__PURE__ */ new Set();
126
+ const queue = [start];
127
+ past.add(start);
128
+ while (queue.length > 0) {
129
+ const h = queue.shift();
130
+ const block = allBlocks.get(h);
131
+ if (!block) continue;
132
+ for (const p of block.header.parents) {
133
+ if (!past.has(p)) {
134
+ past.add(p);
135
+ queue.push(p);
136
+ }
137
+ }
138
+ }
139
+ return past;
140
+ }
141
+ function isDagAncestorOf(ancestorCandidate, descendant, allBlocks) {
142
+ if (ancestorCandidate === GENESIS_HASH) return true;
143
+ return pastSet(descendant, allBlocks).has(ancestorCandidate);
144
+ }
145
+ function unorderedMergesetWithoutSelectedParent(block, selectedParent, allBlocks) {
146
+ const spPast = pastSet(selectedParent, allBlocks);
147
+ const mergeset = /* @__PURE__ */ new Set();
148
+ const queue = [];
149
+ const visited = /* @__PURE__ */ new Set();
150
+ for (const parent of block.header.parents) {
151
+ if (parent === selectedParent) continue;
152
+ if (spPast.has(parent)) continue;
153
+ if (!visited.has(parent)) {
154
+ visited.add(parent);
155
+ mergeset.add(parent);
156
+ queue.push(parent);
157
+ }
158
+ }
159
+ while (queue.length > 0) {
160
+ const current = queue.shift();
161
+ const currentBlock = allBlocks.get(current);
162
+ if (!currentBlock) continue;
163
+ for (const parent of currentBlock.header.parents) {
164
+ if (parent === GENESIS_HASH) continue;
165
+ if (visited.has(parent)) continue;
166
+ visited.add(parent);
167
+ if (spPast.has(parent)) continue;
168
+ mergeset.add(parent);
169
+ queue.push(parent);
170
+ }
171
+ }
172
+ return Array.from(mergeset);
173
+ }
174
+ function orderedMergesetWithoutSelectedParent(block, selectedParent, allBlocks, gdStore) {
175
+ const unordered = unorderedMergesetWithoutSelectedParent(
176
+ block,
177
+ selectedParent,
178
+ allBlocks
179
+ );
180
+ const sortable = unordered.map((hash) => ({
181
+ hash,
182
+ blueWork: gdStore.getBlueWork(hash) ?? 0n
183
+ }));
184
+ return sortBlocks(sortable);
185
+ }
186
+
187
+ // src/ghostdag-engine.ts
188
+ var DEFAULT_K = 18;
189
+ var ApproxGhostdagEngine = class {
190
+ k;
191
+ genesisHash;
192
+ constructor(k = DEFAULT_K, genesisHash = GENESIS_HASH) {
193
+ this.k = k;
194
+ this.genesisHash = genesisHash;
195
+ }
196
+ /**
197
+ * Compute GhostdagData for `block`.
198
+ *
199
+ * Intentionally non-deterministic for TEMP_INDEXING_HASH as it uses Date.now()
200
+ * for dummy block headers.
201
+ *
202
+ * Source: protocol.rs lines 126-166 ghostdag()
203
+ *
204
+ * Steps:
205
+ * 1. findSelectedParent()
206
+ * 2. orderedMergeset()
207
+ * 3. color each candidate (check_blue_candidate)
208
+ * 4. blueScore = sp.blueScore + |mergesetBlues|
209
+ * 5. blueWork = sp.blueWork + Σ work(blues)
210
+ *
211
+ * All steps are [RESEARCH_EXPERIMENTAL].
212
+ */
213
+ ghostdag(parents, gdStore) {
214
+ const block = {
215
+ header: {
216
+ hash: "TEMP_INDEXING_HASH",
217
+ // Not used for internal GHOSTDAG logic
218
+ parents,
219
+ timestampUs: Date.now() * 1e3,
220
+ minerId: 0,
221
+ bits: 1,
222
+ // Minimum work contribution
223
+ nonce: 0
224
+ }
225
+ };
226
+ const allBlocksProxy = {
227
+ get: (hash) => {
228
+ const data = gdStore.getData(hash);
229
+ if (data) {
230
+ const parents2 = gdStore.getParents(hash) || [];
231
+ return {
232
+ header: {
233
+ hash,
234
+ parents: parents2,
235
+ timestampUs: 0,
236
+ minerId: 0,
237
+ bits: 1e3,
238
+ nonce: 0
239
+ },
240
+ ghostdag: data
241
+ };
242
+ }
243
+ return void 0;
244
+ },
245
+ has: (hash) => gdStore.has(hash)
246
+ };
247
+ return this.computeGhostdag(block, allBlocksProxy, gdStore);
248
+ }
249
+ /**
250
+ * Compute GhostdagData for `block`.
251
+ */
252
+ computeGhostdag(block, allBlocks, gdStore) {
253
+ const hash = block.header.hash;
254
+ if (hash === this.genesisHash) {
255
+ return genesisGhostdagData(this.genesisHash);
256
+ }
257
+ if (block.header.parents.length === 0) {
258
+ throw new Error(
259
+ `Non-genesis block ${hash.slice(0, 8)} has no parents \u2014 undefined in GHOSTDAG`
260
+ );
261
+ }
262
+ const parentBlueWorks = block.header.parents.map((p) => ({
263
+ hash: p,
264
+ blueWork: gdStore.getBlueWork(p) ?? 0n
265
+ }));
266
+ const selectedParent = findSelectedParent(parentBlueWorks);
267
+ if (selectedParent === void 0) {
268
+ throw new Error(
269
+ `findSelectedParent returned undefined for block ${hash.slice(0, 8)}`
270
+ );
271
+ }
272
+ const orderedMerge = orderedMergesetWithoutSelectedParent(
273
+ block,
274
+ selectedParent,
275
+ allBlocks,
276
+ gdStore
277
+ );
278
+ const mergesetBlues = [];
279
+ const bluesAnticoneSizes = [];
280
+ const mergesetReds = [];
281
+ const coloringContext = [selectedParent];
282
+ const contextAnticoneSizes = [0];
283
+ for (const candidate of orderedMerge) {
284
+ if (mergesetBlues.length > this.k) {
285
+ mergesetReds.push(candidate.hash);
286
+ continue;
287
+ }
288
+ const color = checkBlueCandidateApprox(
289
+ candidate.hash,
290
+ this.k,
291
+ coloringContext,
292
+ contextAnticoneSizes,
293
+ allBlocks
294
+ );
295
+ if (color === "blue") {
296
+ mergesetBlues.push(candidate.hash);
297
+ coloringContext.push(candidate.hash);
298
+ contextAnticoneSizes.push(0);
299
+ } else {
300
+ mergesetReds.push(candidate.hash);
301
+ }
302
+ }
303
+ const spBlueScore = gdStore.getBlueScore(selectedParent) ?? 0;
304
+ const blueScore = spBlueScore + 1 + mergesetBlues.length;
305
+ const spBlueWork = gdStore.getBlueWork(selectedParent) ?? 0n;
306
+ let addedBlueWork = headerWork(allBlocks.get(selectedParent).header);
307
+ for (const h of mergesetBlues) {
308
+ const b = allBlocks.get(h);
309
+ if (b) addedBlueWork += headerWork(b.header);
310
+ }
311
+ const blueWork = spBlueWork + addedBlueWork;
312
+ return {
313
+ blueScore,
314
+ blueWork,
315
+ selectedParent,
316
+ mergesetBlues: [selectedParent, ...mergesetBlues],
317
+ mergesetReds,
318
+ bluesAnticoneSizes: [0, ...bluesAnticoneSizes]
319
+ };
320
+ }
321
+ };
322
+ function checkBlueCandidateApprox(candidate, k, currentBlues, bluesAnticoneSizes, allBlocks) {
323
+ let candidateAnticoneSize = 0;
324
+ const toIncrement = [];
325
+ for (let i = 0; i < currentBlues.length; i++) {
326
+ const blue = currentBlues[i];
327
+ const blueInPast = isDagAncestorOf(blue, candidate, allBlocks);
328
+ const candidateInPast = isDagAncestorOf(candidate, blue, allBlocks);
329
+ if (blueInPast || candidateInPast) {
330
+ continue;
331
+ }
332
+ candidateAnticoneSize++;
333
+ if (candidateAnticoneSize > k) {
334
+ return "red";
335
+ }
336
+ const existing = bluesAnticoneSizes[i] ?? 0;
337
+ if (existing >= k) {
338
+ return "red";
339
+ }
340
+ toIncrement.push(i);
341
+ }
342
+ for (const idx of toIncrement) {
343
+ bluesAnticoneSizes[idx]++;
344
+ }
345
+ bluesAnticoneSizes.push(candidateAnticoneSize);
346
+ return "blue";
347
+ }
348
+
349
+ // src/metrics.ts
350
+ function computeDagMetrics(allBlocks, gdStore, genesisHash) {
351
+ const totalBlocks = allBlocks.size - 1;
352
+ const parentIds = /* @__PURE__ */ new Set();
353
+ let totalParentLinks = 0;
354
+ for (const block of allBlocks.values()) {
355
+ for (const p of block.header.parents) {
356
+ parentIds.add(p);
357
+ }
358
+ if (!block.header.parents.includes(genesisHash) && block.header.hash !== genesisHash) {
359
+ }
360
+ totalParentLinks += block.header.parents.length;
361
+ }
362
+ const tips = Array.from(allBlocks.keys()).filter((id) => !parentIds.has(id));
363
+ const dagWidth = tips.length;
364
+ const meanParents = totalBlocks > 0 ? totalParentLinks / (totalBlocks + 1) : 0;
365
+ const tipData = tips.map((id) => ({
366
+ hash: id,
367
+ blueWork: gdStore.getBlueWork(id) ?? 0n
368
+ }));
369
+ const sink = findSelectedParent(tipData) || genesisHash;
370
+ const selectedChain = [];
371
+ let currentId = sink;
372
+ while (currentId) {
373
+ selectedChain.unshift(currentId);
374
+ if (currentId === genesisHash) break;
375
+ currentId = gdStore.getSelectedParent(currentId);
376
+ }
377
+ let blueBlocks = 0;
378
+ let redBlocks = 0;
379
+ let maxBlueScore = 0;
380
+ let maxBlueWork = 0n;
381
+ const sinkData = gdStore.getData(sink);
382
+ if (sinkData) {
383
+ blueBlocks = sinkData.blueScore;
384
+ }
385
+ for (const id of allBlocks.keys()) {
386
+ if (id === genesisHash) continue;
387
+ const score = gdStore.getBlueScore(id) || 0;
388
+ const work = gdStore.getBlueWork(id) || 0n;
389
+ if (score > maxBlueScore) maxBlueScore = score;
390
+ if (work > maxBlueWork) maxBlueWork = work;
391
+ }
392
+ if (sinkData) {
393
+ const past = identifyReachableBlocks(allBlocks, sink);
394
+ const totalInPast = past.size - 1;
395
+ blueBlocks = sinkData.blueScore + 1;
396
+ redBlocks = Math.max(0, totalInPast - blueBlocks);
397
+ }
398
+ return {
399
+ totalBlocks,
400
+ blueBlocks,
401
+ redBlocks,
402
+ redRatio: blueBlocks + redBlocks > 0 ? redBlocks / (blueBlocks + redBlocks) : 0,
403
+ meanParents,
404
+ dagWidth,
405
+ maxBlueScore,
406
+ maxBlueWork,
407
+ selectedChainLength: selectedChain.length,
408
+ selectedChain
409
+ };
410
+ }
411
+ function identifyReachableBlocks(allBlocks, sinkId) {
412
+ const reachable = /* @__PURE__ */ new Set();
413
+ const stack = [sinkId];
414
+ while (stack.length > 0) {
415
+ const id = stack.pop();
416
+ if (reachable.has(id)) continue;
417
+ reachable.add(id);
418
+ const block = allBlocks.get(id);
419
+ if (block) {
420
+ for (const p of block.header.parents) {
421
+ stack.push(p);
422
+ }
423
+ }
424
+ }
425
+ return reachable;
426
+ }
427
+
428
+ // src/scenarios.ts
429
+ import { createHash } from "crypto";
430
+ function scenarioBlockHash(scenario, index) {
431
+ return createHash("sha256").update(`${scenario}:${index}`).digest("hex");
432
+ }
433
+ function createBlock(hash, parents, bits) {
434
+ return {
435
+ header: {
436
+ hash,
437
+ parents,
438
+ timestampUs: Date.now() * 1e3,
439
+ // Not deterministic but used for simulation
440
+ minerId: 0,
441
+ bits,
442
+ nonce: 0
443
+ }
444
+ };
445
+ }
446
+ function runLinearChain(config) {
447
+ const start = performance.now();
448
+ const blocks = /* @__PURE__ */ new Map();
449
+ const store = new GhostdagStore();
450
+ const engine = new ApproxGhostdagEngine(config.k ?? 18);
451
+ const bits = config.bits ?? 1e3;
452
+ const genesis = createBlock(GENESIS_HASH, [], bits);
453
+ blocks.set(GENESIS_HASH, genesis);
454
+ store.insert(GENESIS_HASH, genesisGhostdagData(GENESIS_HASH));
455
+ let prevHash = GENESIS_HASH;
456
+ for (let i = 0; i < config.blockCount; i++) {
457
+ const hash = scenarioBlockHash(config.name, i);
458
+ const block = createBlock(hash, [prevHash], bits);
459
+ blocks.set(hash, block);
460
+ const gdData = engine.computeGhostdag(block, blocks, store);
461
+ store.insert(hash, gdData);
462
+ prevHash = hash;
463
+ }
464
+ const metrics = computeDagMetrics(blocks, store, GENESIS_HASH);
465
+ return {
466
+ name: config.name,
467
+ config,
468
+ metrics,
469
+ computeTimeMs: performance.now() - start
470
+ };
471
+ }
472
+ function runWideDag(config) {
473
+ const start = performance.now();
474
+ const blocks = /* @__PURE__ */ new Map();
475
+ const store = new GhostdagStore();
476
+ const engine = new ApproxGhostdagEngine(config.k ?? 18);
477
+ const bits = config.bits ?? 1e3;
478
+ blocks.set(GENESIS_HASH, createBlock(GENESIS_HASH, [], bits));
479
+ store.insert(GENESIS_HASH, genesisGhostdagData(GENESIS_HASH));
480
+ const siblingCount = config.blockCount - 1;
481
+ for (let i = 0; i < siblingCount; i++) {
482
+ const hash = scenarioBlockHash(config.name, i);
483
+ const block = createBlock(hash, [GENESIS_HASH], bits);
484
+ blocks.set(hash, block);
485
+ const gdData = engine.computeGhostdag(block, blocks, store);
486
+ store.insert(hash, gdData);
487
+ }
488
+ const siblingHashes = Array.from(blocks.keys()).filter((h) => h !== GENESIS_HASH);
489
+ const mergerHash = scenarioBlockHash(config.name, 9999);
490
+ const mergerBlock = createBlock(mergerHash, siblingHashes, bits);
491
+ blocks.set(mergerHash, mergerBlock);
492
+ store.insert(mergerHash, engine.computeGhostdag(mergerBlock, blocks, store));
493
+ const metrics = computeDagMetrics(blocks, store, GENESIS_HASH);
494
+ return {
495
+ name: config.name,
496
+ config,
497
+ metrics,
498
+ computeTimeMs: performance.now() - start
499
+ };
500
+ }
501
+ function runForkResolution(config) {
502
+ const start = performance.now();
503
+ const blocks = /* @__PURE__ */ new Map();
504
+ const store = new GhostdagStore();
505
+ const engine = new ApproxGhostdagEngine(config.k ?? 18);
506
+ const bits = config.bits ?? 1e3;
507
+ blocks.set(GENESIS_HASH, createBlock(GENESIS_HASH, [], bits));
508
+ store.insert(GENESIS_HASH, genesisGhostdagData(GENESIS_HASH));
509
+ let forkHash = GENESIS_HASH;
510
+ for (let i = 0; i < config.forkPoint; i++) {
511
+ const hash = scenarioBlockHash(config.name + "-trunk", i);
512
+ const block = createBlock(hash, [forkHash], bits);
513
+ blocks.set(hash, block);
514
+ const gdData = engine.computeGhostdag(block, blocks, store);
515
+ store.insert(hash, gdData);
516
+ forkHash = hash;
517
+ }
518
+ const branchLength = Math.floor((config.blockCount - config.forkPoint) / 2);
519
+ let prevA = forkHash;
520
+ for (let i = 0; i < branchLength; i++) {
521
+ const hash = scenarioBlockHash(config.name + "-forkA", i);
522
+ const block = createBlock(hash, [prevA], bits);
523
+ blocks.set(hash, block);
524
+ const gdData = engine.computeGhostdag(block, blocks, store);
525
+ store.insert(hash, gdData);
526
+ prevA = hash;
527
+ }
528
+ let prevB = forkHash;
529
+ const branchBLength = config.blockCount - config.forkPoint - branchLength;
530
+ for (let i = 0; i < branchBLength; i++) {
531
+ const hash = scenarioBlockHash(config.name + "-forkB", i);
532
+ const block = createBlock(hash, [prevB], bits);
533
+ blocks.set(hash, block);
534
+ const gdData = engine.computeGhostdag(block, blocks, store);
535
+ store.insert(hash, gdData);
536
+ prevB = hash;
537
+ }
538
+ const metrics = computeDagMetrics(blocks, store, GENESIS_HASH);
539
+ return {
540
+ name: config.name,
541
+ config,
542
+ metrics,
543
+ computeTimeMs: performance.now() - start
544
+ };
545
+ }
546
+ function runDiamondDag(config) {
547
+ const start = performance.now();
548
+ const blocks = /* @__PURE__ */ new Map();
549
+ const store = new GhostdagStore();
550
+ const engine = new ApproxGhostdagEngine(config.k ?? 18);
551
+ const bits = config.bits ?? 1e3;
552
+ blocks.set(GENESIS_HASH, createBlock(GENESIS_HASH, [], bits));
553
+ store.insert(GENESIS_HASH, genesisGhostdagData(GENESIS_HASH));
554
+ let prevMerge = GENESIS_HASH;
555
+ const cycles = Math.floor(config.blockCount / 3);
556
+ for (let i = 0; i < cycles; i++) {
557
+ const hashA = scenarioBlockHash(config.name + "-A", i);
558
+ const blockA = createBlock(hashA, [prevMerge], bits);
559
+ blocks.set(hashA, blockA);
560
+ store.insert(hashA, engine.computeGhostdag(blockA, blocks, store));
561
+ const hashB = scenarioBlockHash(config.name + "-B", i);
562
+ const blockB = createBlock(hashB, [prevMerge], bits);
563
+ blocks.set(hashB, blockB);
564
+ store.insert(hashB, engine.computeGhostdag(blockB, blocks, store));
565
+ const hashM = scenarioBlockHash(config.name + "-M", i);
566
+ const blockM = createBlock(hashM, [hashA, hashB], bits);
567
+ blocks.set(hashM, blockM);
568
+ store.insert(hashM, engine.computeGhostdag(blockM, blocks, store));
569
+ prevMerge = hashM;
570
+ }
571
+ const metrics = computeDagMetrics(blocks, store, GENESIS_HASH);
572
+ return {
573
+ name: config.name,
574
+ config,
575
+ metrics,
576
+ computeTimeMs: performance.now() - start
577
+ };
578
+ }
579
+ function runAllScenarios(config) {
580
+ return [
581
+ runLinearChain({ ...config, name: config.name + ":linear" }),
582
+ runWideDag({ ...config, name: config.name + ":wide" }),
583
+ runForkResolution({ ...config, name: config.name + ":fork", forkPoint: Math.floor(config.blockCount / 2) }),
584
+ runDiamondDag({ ...config, name: config.name + ":diamond" })
585
+ ];
586
+ }
587
+
588
+ // src/report.ts
589
+ function formatScenarioReport(results) {
590
+ let report = "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n";
591
+ report += " GHOSTDAG Simulation Report\n";
592
+ report += "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n";
593
+ for (const res of results) {
594
+ const m = res.metrics;
595
+ report += `Scenario: ${res.name} (K=${res.config.k ?? 18}, ${res.config.blockCount} blocks)
596
+ `;
597
+ report += ` Total blocks : ${m.totalBlocks}
598
+ `;
599
+ report += ` Blue / Red : ${m.blueBlocks} / ${m.redBlocks}
600
+ `;
601
+ report += ` Red ratio : ${m.redRatio.toFixed(4)}
602
+ `;
603
+ report += ` DAG width : ${m.dagWidth}
604
+ `;
605
+ report += ` Mean parents : ${m.meanParents.toFixed(2)}
606
+ `;
607
+ report += ` Selected chain : ${m.selectedChainLength} blocks
608
+ `;
609
+ report += ` Max blueScore : ${m.maxBlueScore}
610
+ `;
611
+ report += ` Compute time : ${res.computeTimeMs.toFixed(0)}ms
612
+
613
+ `;
614
+ }
615
+ return report;
616
+ }
617
+
618
+ // src/mass-profile.ts
619
+ import { estimateTransactionMass, estimateFeeFromMass } from "@hardkas/tx-builder";
620
+ function formatBigInt(n) {
621
+ return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
622
+ }
623
+ function profileMass(opts) {
624
+ const inputCount = opts.inputCount;
625
+ const outputCount = opts.outputCount;
626
+ const payloadBytes = opts.payloadBytes ?? 0;
627
+ const feeRate = opts.feeRate ?? 1n;
628
+ const result = estimateTransactionMass({
629
+ inputCount,
630
+ outputs: Array(outputCount).fill({ address: "kaspa:qplaceholder" }),
631
+ payloadBytes,
632
+ hasChange: false
633
+ });
634
+ const breakdown = result.breakdown;
635
+ return {
636
+ totalMass: breakdown.total,
637
+ inputMass: breakdown.inputs + breakdown.base,
638
+ // Base overhead is often grouped with inputs or just kept as total
639
+ outputMass: breakdown.outputs,
640
+ payloadMass: breakdown.payload,
641
+ estimatedFeeSompi: estimateFeeFromMass(breakdown.total, feeRate),
642
+ feeRate,
643
+ inputCount,
644
+ outputCount,
645
+ payloadBytes
646
+ };
647
+ }
648
+ function compareMassProfiles(current, previous) {
649
+ const massDelta = current.totalMass - previous.totalMass;
650
+ const feeDelta = current.estimatedFeeSompi - previous.estimatedFeeSompi;
651
+ const massChangePercent = previous.totalMass > 0n ? Number(massDelta * 10000n / previous.totalMass) / 100 : 0;
652
+ const feeChangePercent = previous.estimatedFeeSompi > 0n ? Number(feeDelta * 10000n / previous.estimatedFeeSompi) / 100 : 0;
653
+ const isRegression = massDelta > 0n;
654
+ let severity = "none";
655
+ if (isRegression) {
656
+ if (massChangePercent >= 10) {
657
+ severity = "major";
658
+ } else {
659
+ severity = "minor";
660
+ }
661
+ }
662
+ return {
663
+ current,
664
+ previous,
665
+ massDelta,
666
+ feeDelta,
667
+ massChangePercent,
668
+ feeChangePercent,
669
+ isRegression,
670
+ severity
671
+ };
672
+ }
673
+ function formatMassProfile(breakdown) {
674
+ const kasAmount = Number(breakdown.estimatedFeeSompi) / 1e8;
675
+ return [
676
+ "\u2550\u2550\u2550 Mass Profile \u2550\u2550\u2550",
677
+ ` Inputs : ${breakdown.inputCount} inputs \u2192 ${formatBigInt(breakdown.inputMass)} mass`,
678
+ ` Outputs : ${breakdown.outputCount} outputs \u2192 ${formatBigInt(breakdown.outputMass)} mass`,
679
+ ` Payload : ${breakdown.payloadBytes} bytes \u2192 ${formatBigInt(breakdown.payloadMass)} mass`,
680
+ ` Total mass : ${formatBigInt(breakdown.totalMass)}`,
681
+ ` Fee rate : ${formatBigInt(breakdown.feeRate)} sompi/mass`,
682
+ ` Est. fee : ${formatBigInt(breakdown.estimatedFeeSompi)} sompi (${kasAmount.toFixed(8)} KAS)`
683
+ ].join("\n");
684
+ }
685
+ function formatMassComparison(comparison) {
686
+ const massSign = comparison.massDelta >= 0n ? "+" : "";
687
+ const feeSign = comparison.feeDelta >= 0n ? "+" : "";
688
+ const statusLine = comparison.isRegression ? comparison.severity === "major" ? "\u26A0\uFE0F MAJOR REGRESSION (\u226510% increase)" : "\u2139\uFE0F MINOR REGRESSION (<10% increase)" : "\u2705 NO REGRESSION";
689
+ return [
690
+ "\u2550\u2550\u2550 Mass Comparison \u2550\u2550\u2550",
691
+ ` Previous : ${formatBigInt(comparison.previous.totalMass)} mass \u2192 ${formatBigInt(comparison.previous.estimatedFeeSompi)} sompi`,
692
+ ` Current : ${formatBigInt(comparison.current.totalMass)} mass \u2192 ${formatBigInt(comparison.current.estimatedFeeSompi)} sompi`,
693
+ ` Delta : ${massSign}${formatBigInt(comparison.massDelta)} mass (${massSign}${comparison.massChangePercent.toFixed(1)}%)`,
694
+ ` Fee delta : ${feeSign}${formatBigInt(comparison.feeDelta)} sompi (${feeSign}${comparison.feeChangePercent.toFixed(1)}%)`,
695
+ ` Status : ${statusLine}`
696
+ ].join("\n");
697
+ }
698
+
699
+ // src/mass-snapshot.ts
700
+ import { resolve, join } from "path";
701
+ import { existsSync, readFileSync } from "fs";
702
+ import { writeFileAtomicSync } from "@hardkas/core";
703
+ function saveMassSnapshot(dir, label, breakdown) {
704
+ const snapshotDir = resolve(dir, ".hardkas", "mass-snapshots");
705
+ const snapshot = {
706
+ label,
707
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
708
+ breakdown
709
+ };
710
+ const filePath = join(snapshotDir, `${label}.json`);
711
+ writeFileAtomicSync(filePath, JSON.stringify(snapshot, null, 2));
712
+ }
713
+ function loadMassSnapshot(dir, label) {
714
+ const filePath = resolve(dir, ".hardkas", "mass-snapshots", `${label}.json`);
715
+ if (!existsSync(filePath)) {
716
+ return void 0;
717
+ }
718
+ try {
719
+ const data = readFileSync(filePath, "utf8");
720
+ const snapshot = JSON.parse(data);
721
+ return reviveSnapshot(snapshot);
722
+ } catch (err) {
723
+ console.warn(`Failed to load mass snapshot "${label}":`, err);
724
+ return void 0;
725
+ }
726
+ }
727
+ function profileAndCompare(opts, snapshotDir, label) {
728
+ const currentBreakdown = profileMass(opts);
729
+ const previousSnapshot = loadMassSnapshot(snapshotDir, label);
730
+ let comparison;
731
+ if (previousSnapshot) {
732
+ comparison = compareMassProfiles(currentBreakdown, previousSnapshot.breakdown);
733
+ }
734
+ saveMassSnapshot(snapshotDir, label, currentBreakdown);
735
+ return {
736
+ breakdown: currentBreakdown,
737
+ ...comparison ? { comparison } : {}
738
+ };
739
+ }
740
+ function reviveSnapshot(snapshot) {
741
+ const b = snapshot.breakdown;
742
+ snapshot.breakdown = {
743
+ ...b,
744
+ totalMass: BigInt(b.totalMass),
745
+ inputMass: BigInt(b.inputMass),
746
+ outputMass: BigInt(b.outputMass),
747
+ payloadMass: BigInt(b.payloadMass),
748
+ estimatedFeeSompi: BigInt(b.estimatedFeeSompi),
749
+ feeRate: BigInt(b.feeRate)
750
+ };
751
+ return snapshot;
752
+ }
42
753
  export {
754
+ ApproxGhostdagEngine,
755
+ DEFAULT_K,
756
+ GENESIS_HASH,
757
+ GhostdagStore,
43
758
  TxSimulator,
44
- createTraceId
759
+ blockBlueScore,
760
+ blockBlueWork,
761
+ blockHash,
762
+ blockParents,
763
+ compactFromFull,
764
+ compareMassProfiles,
765
+ compareSortableBlocks,
766
+ computeDagMetrics,
767
+ createTraceId,
768
+ findSelectedParent,
769
+ formatMassComparison,
770
+ formatMassProfile,
771
+ formatScenarioReport,
772
+ genesisGhostdagData,
773
+ headerWork,
774
+ isDagAncestorOf,
775
+ loadMassSnapshot,
776
+ orderedMergesetWithoutSelectedParent,
777
+ pastSet,
778
+ profileAndCompare,
779
+ profileMass,
780
+ runAllScenarios,
781
+ runDiamondDag,
782
+ runForkResolution,
783
+ runLinearChain,
784
+ runWideDag,
785
+ saveMassSnapshot,
786
+ sortBlocks,
787
+ unorderedMergesetWithoutSelectedParent
45
788
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardkas/simulator",
3
- "version": "0.2.2-alpha",
3
+ "version": "0.3.0-alpha",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -8,7 +8,8 @@
8
8
  ".": "./dist/index.js"
9
9
  },
10
10
  "dependencies": {
11
- "@hardkas/tx-builder": "0.2.2-alpha"
11
+ "@hardkas/core": "0.3.0-alpha",
12
+ "@hardkas/tx-builder": "0.3.0-alpha"
12
13
  },
13
14
  "devDependencies": {
14
15
  "tsup": "^8.3.5",