@hardkas/localnet 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.
@@ -2,6 +2,7 @@
2
2
  import fs from "fs/promises";
3
3
  import path from "path";
4
4
  import { existsSync } from "fs";
5
+ import { writeFileAtomic } from "@hardkas/core";
5
6
  function getDefaultTracesDir(cwd = process.cwd()) {
6
7
  return path.join(cwd, ".hardkas", "traces");
7
8
  }
@@ -20,7 +21,7 @@ async function saveSimulatedTrace(trace, options) {
20
21
  await fs.mkdir(dir, { recursive: true });
21
22
  }
22
23
  const filePath = getTracePath(trace.txId, options?.cwd);
23
- await fs.writeFile(filePath, JSON.stringify(trace, null, 2), "utf-8");
24
+ await writeFileAtomic(filePath, JSON.stringify(trace, null, 2), { encoding: "utf-8" });
24
25
  return filePath;
25
26
  }
26
27
  async function loadSimulatedTrace(txId, options) {
@@ -2,6 +2,7 @@
2
2
  import fs from "fs/promises";
3
3
  import path from "path";
4
4
  import { existsSync } from "fs";
5
+ import { writeFileAtomic } from "@hardkas/core";
5
6
  function getDefaultReceiptsDir(cwd = process.cwd()) {
6
7
  return path.join(cwd, ".hardkas", "receipts");
7
8
  }
@@ -20,7 +21,7 @@ async function saveSimulatedReceipt(receipt, options) {
20
21
  await fs.mkdir(dir, { recursive: true });
21
22
  }
22
23
  const filePath = getReceiptPath(receipt.txId, options?.cwd);
23
- await fs.writeFile(filePath, JSON.stringify(receipt, null, 2), "utf-8");
24
+ await writeFileAtomic(filePath, JSON.stringify(receipt, null, 2), { encoding: "utf-8" });
24
25
  return filePath;
25
26
  }
26
27
  async function loadSimulatedReceipt(txId, options) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
+ import * as _hardkas_artifacts from '@hardkas/artifacts';
1
2
  import { HardkasArtifactBase, Snapshot as Snapshot$1, ARTIFACT_SCHEMAS, TxReceipt, TxPlan } from '@hardkas/artifacts';
3
+ import * as _hardkas_simulator from '@hardkas/simulator';
2
4
  import { ExecutionMode, NetworkId } from '@hardkas/core';
5
+ import { KaspaRpcClient } from '@hardkas/kaspa-rpc';
3
6
 
4
7
  interface HardkasAccount {
5
8
  readonly name: string;
@@ -32,6 +35,11 @@ declare class SimulatedKaspaChain {
32
35
  getBalance(address: string): bigint;
33
36
  getUtxos(address: string): readonly SimulatedUtxo[];
34
37
  fund(address: string, amountSompi: bigint): SimulatedUtxo;
38
+ /**
39
+ * Creates a snapshot of the current chain state.
40
+ * Snapshot IDs use Date.now() and are intended for session-based
41
+ * debugging and restore points, not for canonical identity.
42
+ */
35
43
  snapshot(): Snapshot;
36
44
  restore(snapshot: Snapshot): void;
37
45
  }
@@ -68,6 +76,13 @@ interface LocalnetState extends HardkasArtifactBase {
68
76
  utxos: LocalnetUtxo[];
69
77
  snapshots?: Snapshot$1[];
70
78
  dag?: SimulatedDag;
79
+ forkSource?: {
80
+ network: string;
81
+ rpcUrl: string;
82
+ daaScore: string;
83
+ forkedAt: string;
84
+ addresses: string[];
85
+ };
71
86
  }
72
87
  interface SimulatedBlock {
73
88
  id: string;
@@ -76,6 +91,12 @@ interface SimulatedBlock {
76
91
  daaScore: string;
77
92
  acceptedTxIds: string[];
78
93
  isGenesis?: boolean;
94
+ /** GHOSTDAG blue_work for this block (computed by ApproxGhostdagEngine). */
95
+ blueWork?: string;
96
+ /** True if GHOSTDAG colored this block blue, false if red. */
97
+ isBlue?: boolean;
98
+ /** Full GHOSTDAG data if computed. */
99
+ ghostdagData?: _hardkas_simulator.GhostdagData;
79
100
  }
80
101
  interface SimulatedDag {
81
102
  blocks: Record<string, SimulatedBlock>;
@@ -88,6 +109,10 @@ interface SimulatedDag {
88
109
  winnerTxId: string;
89
110
  loserTxIds: string[];
90
111
  }>;
112
+ /** Internal GHOSTDAG store for this DAG session. */
113
+ ghostdagStore?: _hardkas_simulator.GhostdagStore;
114
+ /** Internal GHOSTDAG engine for this DAG session. */
115
+ ghostdagEngine?: _hardkas_simulator.ApproxGhostdagEngine;
91
116
  }
92
117
  interface StateTransition {
93
118
  preStateHash: string;
@@ -97,8 +122,8 @@ interface StateTransition {
97
122
  interface SimulationResult {
98
123
  ok: boolean;
99
124
  state: LocalnetState;
100
- receipt: any;
101
- planArtifact?: any;
125
+ receipt: _hardkas_artifacts.TxReceipt;
126
+ planArtifact?: _hardkas_artifacts.TxPlan;
102
127
  errors: string[];
103
128
  }
104
129
  interface ReplayInvariantResult {
@@ -106,9 +131,25 @@ interface ReplayInvariantResult {
106
131
  mismatches: string[];
107
132
  }
108
133
  interface ReplayVerificationReport {
134
+ schema: "hardkas.replayReport.v1";
135
+ txId: string;
109
136
  planOk: boolean;
110
137
  receiptOk: boolean;
111
138
  invariantsOk: boolean;
139
+ /** Honest check status to avoid overclaiming. */
140
+ checks: {
141
+ /** Whether the internal HardKAS workflow (Plan -> Receipt) was reproduced. */
142
+ workflowDeterministic: "reproduced" | "diverged" | "skipped";
143
+ /** HardKAS DOES NOT currently validate full Kaspa consensus (GHOSTDAG, etc). */
144
+ consensusValidation: "unimplemented" | "partial" | "skipped";
145
+ /** HardKAS DOES NOT currently validate L2 bridge logic. */
146
+ l2BridgeCorrectness: "unimplemented" | "partial" | "skipped";
147
+ };
148
+ divergences: Array<{
149
+ path: string;
150
+ expected: any;
151
+ actual: any;
152
+ }>;
112
153
  errors: string[];
113
154
  }
114
155
  interface SnapshotVerificationResult {
@@ -210,6 +251,7 @@ options?: {
210
251
  interface StoredSimulatedTxReceipt extends HardkasArtifactBase {
211
252
  schema: typeof ARTIFACT_SCHEMAS.TX_RECEIPT;
212
253
  version: "1.0.0-alpha";
254
+ hashVersion?: number | string;
213
255
  txId: string;
214
256
  status: "confirmed" | "failed";
215
257
  mode: ExecutionMode;
@@ -259,6 +301,8 @@ type StoredTraceEvent = {
259
301
  };
260
302
  interface StoredSimulatedTxTrace extends HardkasArtifactBase {
261
303
  readonly schema: typeof ARTIFACT_SCHEMAS.TX_TRACE;
304
+ readonly version: string;
305
+ readonly hashVersion?: number | string;
262
306
  readonly txId: string;
263
307
  readonly mode: ExecutionMode;
264
308
  readonly networkId: NetworkId;
@@ -291,6 +335,8 @@ interface SimulatedReplaySummary {
291
335
  }
292
336
  /**
293
337
  * Verifies that a transaction replay matches the original artifacts.
338
+ * Implements an honest replay model that differentiates between
339
+ * reproduced results and unimplemented consensus/bridge features.
294
340
  */
295
341
  declare function verifyReplay(state: LocalnetState, originalPlan: TxPlan, originalReceipt: TxReceipt): ReplayVerificationReport;
296
342
  /**
@@ -303,7 +349,9 @@ declare function getSimulatedReplaySummary(txId: string, options?: {
303
349
  /**
304
350
  * Creates a fresh simulated DAG with a genesis block.
305
351
  */
306
- declare function createSimulatedDag(): SimulatedDag;
352
+ declare function createSimulatedDag(options?: {
353
+ k?: number;
354
+ }): SimulatedDag;
307
355
  /**
308
356
  * Adds a block to the DAG.
309
357
  */
@@ -330,5 +378,30 @@ declare function resolveConflictsDeterministically(txs: Array<{
330
378
  displaced: string[];
331
379
  conflicts: any[];
332
380
  };
381
+ /**
382
+ * Get the blue/red coloring of all blocks in the DAG.
383
+ * Returns a map of blockId → { isBlue, blueWork, blueScore }.
384
+ */
385
+ declare function getDagColoring(dag: SimulatedDag): Map<string, {
386
+ isBlue: boolean;
387
+ blueWork: string;
388
+ blueScore: number;
389
+ }>;
390
+ /**
391
+ * Get the selected parent chain from sink to genesis.
392
+ */
393
+ declare function getSelectedChain(dag: SimulatedDag): string[];
394
+ /**
395
+ * Helper to find the best tip block according to GHOSTDAG.
396
+ */
397
+ declare function findBestTip(dag: SimulatedDag): string;
398
+
399
+ interface ForkOptions {
400
+ network: string;
401
+ rpcUrl: string;
402
+ addresses: string[];
403
+ atDaaScore?: string;
404
+ }
405
+ declare function forkFromNetwork(rpc: KaspaRpcClient, opts: ForkOptions): Promise<LocalnetState>;
333
406
 
334
- export { type CreateInitialStateOptions, DUST_LIMIT_SOMPI, type FundAddressInput, type HardkasAccount, type HardkasDevnet, type LocalnetAccount, type LocalnetState, type LocalnetUtxo, type ReplayInvariantResult, type ReplayVerificationReport, type SimulatedBlock, type SimulatedDag, SimulatedKaspaChain, type SimulatedPaymentInput, type SimulatedReplaySummary, type SimulatedUtxo, type SimulationResult, type Snapshot, type SnapshotRestoreResult, type SnapshotVerificationResult, type StateTransition, type StoredSimulatedTxReceipt, type StoredSimulatedTxTrace, type StoredTraceEvent, addSimulatedBlock, applySimulatedPayment, applySimulatedPlan, calculateAccountsHash, calculateStateHash, calculateUtxoSetHash, createDeterministicAccounts, createInitialLocalnetState, createLocalnetSnapshot, createSimulatedDag, fundAddress, getAccountBalanceSompi, getAddressBalanceSompi, getDefaultLocalnetDir, getDefaultLocalnetStatePath, getDefaultReceiptsDir, getDefaultTracesDir, getReceiptPath, getSimulatedReplaySummary, getSpendableUtxos, getTracePath, listSimulatedReceipts, listSimulatedTraces, loadLocalnetState, loadOrCreateLocalnetState, loadSimulatedReceipt, loadSimulatedTrace, moveSink, resetLocalnetState, resolveAccountAddress, resolveAccountAddressFromState, resolveConflictsDeterministically, restoreLocalnetSnapshot, saveLocalnetState, saveSimulatedReceipt, saveSimulatedTrace, startSimulatedDevnet, verifyReplay, verifySnapshot };
407
+ export { type CreateInitialStateOptions, DUST_LIMIT_SOMPI, type ForkOptions, type FundAddressInput, type HardkasAccount, type HardkasDevnet, type LocalnetAccount, type LocalnetState, type LocalnetUtxo, type ReplayInvariantResult, type ReplayVerificationReport, type SimulatedBlock, type SimulatedDag, SimulatedKaspaChain, type SimulatedPaymentInput, type SimulatedReplaySummary, type SimulatedUtxo, type SimulationResult, type Snapshot, type SnapshotRestoreResult, type SnapshotVerificationResult, type StateTransition, type StoredSimulatedTxReceipt, type StoredSimulatedTxTrace, type StoredTraceEvent, addSimulatedBlock, applySimulatedPayment, applySimulatedPlan, calculateAccountsHash, calculateStateHash, calculateUtxoSetHash, createDeterministicAccounts, createInitialLocalnetState, createLocalnetSnapshot, createSimulatedDag, findBestTip, forkFromNetwork, fundAddress, getAccountBalanceSompi, getAddressBalanceSompi, getDagColoring, getDefaultLocalnetDir, getDefaultLocalnetStatePath, getDefaultReceiptsDir, getDefaultTracesDir, getReceiptPath, getSelectedChain, getSimulatedReplaySummary, getSpendableUtxos, getTracePath, listSimulatedReceipts, listSimulatedTraces, loadLocalnetState, loadOrCreateLocalnetState, loadSimulatedReceipt, loadSimulatedTrace, moveSink, resetLocalnetState, resolveAccountAddress, resolveAccountAddressFromState, resolveConflictsDeterministically, restoreLocalnetSnapshot, saveLocalnetState, saveSimulatedReceipt, saveSimulatedTrace, startSimulatedDevnet, verifyReplay, verifySnapshot };
package/dist/index.js CHANGED
@@ -4,14 +4,14 @@ import {
4
4
  listSimulatedReceipts,
5
5
  loadSimulatedReceipt,
6
6
  saveSimulatedReceipt
7
- } from "./chunk-CXDVB3K4.js";
7
+ } from "./chunk-GVBX3TPM.js";
8
8
  import {
9
9
  getDefaultTracesDir,
10
10
  getTracePath,
11
11
  listSimulatedTraces,
12
12
  loadSimulatedTrace,
13
13
  saveSimulatedTrace
14
- } from "./chunk-W7ZBGUNK.js";
14
+ } from "./chunk-DPHHOGGK.js";
15
15
 
16
16
  // src/accounts.ts
17
17
  function createDeterministicAccounts(input) {
@@ -22,21 +22,21 @@ function createDeterministicAccounts(input) {
22
22
  const name = names[index] ?? `account${index}`;
23
23
  return {
24
24
  name,
25
- address: `kaspa:sim_${name}`,
25
+ address: `kaspasim:${name}`,
26
26
  balanceSompi: initialBalanceSompi
27
27
  };
28
28
  });
29
29
  }
30
30
  function resolveAccountAddress(input) {
31
- if (input.startsWith("kaspa:")) {
31
+ if (input.startsWith("kaspa:") || input.startsWith("kaspasim:")) {
32
32
  return input;
33
33
  }
34
34
  const aliases = {
35
- alice: "kaspa:sim_alice",
36
- bob: "kaspa:sim_bob",
37
- carol: "kaspa:sim_carol",
38
- dave: "kaspa:sim_dave",
39
- erin: "kaspa:sim_erin"
35
+ alice: "kaspasim:alice",
36
+ bob: "kaspasim:bob",
37
+ carol: "kaspasim:carol",
38
+ dave: "kaspasim:dave",
39
+ erin: "kaspasim:erin"
40
40
  };
41
41
  const resolved = aliases[input.toLowerCase()];
42
42
  if (!resolved) {
@@ -77,7 +77,7 @@ var SimulatedKaspaChain = class {
77
77
  throw new Error("Faucet amount must be positive.");
78
78
  }
79
79
  const utxo = {
80
- id: `faucet:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`,
80
+ id: `faucet:${address.slice(-8)}:${this.daaScore}:${this.utxos.length}`,
81
81
  address,
82
82
  amountSompi,
83
83
  spent: false
@@ -86,6 +86,11 @@ var SimulatedKaspaChain = class {
86
86
  this.mineBlock();
87
87
  return utxo;
88
88
  }
89
+ /**
90
+ * Creates a snapshot of the current chain state.
91
+ * Snapshot IDs use Date.now() and are intended for session-based
92
+ * debugging and restore points, not for canonical identity.
93
+ */
89
94
  snapshot() {
90
95
  return {
91
96
  id: `snapshot:${Date.now().toString(36)}`,
@@ -161,6 +166,7 @@ function resolveAccountAddressFromState(state, nameOrAddress) {
161
166
  // src/store.ts
162
167
  import fs from "fs/promises";
163
168
  import path from "path";
169
+ import { writeFileAtomic } from "@hardkas/core";
164
170
  function getDefaultLocalnetDir(cwd = process.cwd()) {
165
171
  return path.join(cwd, ".hardkas");
166
172
  }
@@ -171,7 +177,7 @@ async function saveLocalnetState(state, filePath) {
171
177
  const targetPath = filePath ?? getDefaultLocalnetStatePath();
172
178
  const dir = path.dirname(targetPath);
173
179
  await fs.mkdir(dir, { recursive: true });
174
- await fs.writeFile(targetPath, JSON.stringify(state, null, 2), "utf-8");
180
+ await writeFileAtomic(targetPath, JSON.stringify(state, null, 2), { encoding: "utf-8" });
175
181
  }
176
182
  async function loadLocalnetState(filePath) {
177
183
  const targetPath = filePath ?? getDefaultLocalnetStatePath();
@@ -211,7 +217,7 @@ function fundAddress(state, input) {
211
217
  }
212
218
  const nextDaaScore = (BigInt(state.daaScore) + 1n).toString();
213
219
  const newUtxo = {
214
- id: `faucet:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`,
220
+ id: `faucet:${input.address.slice(-8)}:${nextDaaScore}:0`,
215
221
  address: input.address,
216
222
  amountSompi: input.amountSompi.toString(),
217
223
  spent: false,
@@ -339,6 +345,24 @@ import {
339
345
  createSimulatedTxReceipt,
340
346
  calculateContentHash as calculateContentHash2
341
347
  } from "@hardkas/artifacts";
348
+ function buildDagContextFromState(state) {
349
+ if (state.dag) {
350
+ return {
351
+ mode: "dag-light",
352
+ sink: state.dag.sink,
353
+ acceptedTxIds: state.dag.acceptedTxIds,
354
+ displacedTxIds: state.dag.displacedTxIds,
355
+ conflictSet: state.dag.conflictSet
356
+ };
357
+ }
358
+ return { mode: "linear", sink: "linear-pseudo-sink" };
359
+ }
360
+ function generateDeterministicFailedTxId(preStateHash, errorMessage, daaScore) {
361
+ const normalized = errorMessage.replace(/[^a-zA-Z0-9_:. -]/g, "");
362
+ const input = `failed:${preStateHash}:${normalized}:${daaScore}`;
363
+ const hash = createHash("sha256").update(input).digest("hex").slice(0, 32);
364
+ return `simtx_failed_${hash}`;
365
+ }
342
366
  function generateDeterministicTxId(planArtifact, preStateHash, daaScore) {
343
367
  const planHash = planArtifact.contentHash || calculateContentHash2(planArtifact);
344
368
  const input = `${planHash}:${preStateHash}:${daaScore}`;
@@ -441,21 +465,11 @@ function applySimulatedPayment(state, input) {
441
465
  const receipt = createSimulatedTxReceipt(planArtifact, txId, {
442
466
  spentUtxoIds,
443
467
  createdUtxoIds,
444
- daaScore: nextDaaScore
468
+ daaScore: nextDaaScore,
469
+ preStateHash,
470
+ postStateHash,
471
+ dagContext: buildDagContextFromState(state)
445
472
  });
446
- receipt.preStateHash = preStateHash;
447
- receipt.postStateHash = postStateHash;
448
- if (state.dag) {
449
- receipt.dagContext = {
450
- mode: "dag-light",
451
- sink: state.dag.sink,
452
- acceptedTxIds: state.dag.acceptedTxIds,
453
- displacedTxIds: state.dag.displacedTxIds,
454
- conflictSet: state.dag.conflictSet
455
- };
456
- } else {
457
- receipt.dagContext = { mode: "linear", sink: "linear-pseudo-sink" };
458
- }
459
473
  return {
460
474
  ok: true,
461
475
  state: nextState,
@@ -464,16 +478,18 @@ function applySimulatedPayment(state, input) {
464
478
  errors
465
479
  };
466
480
  } catch (error) {
467
- const txId = "failed-" + Date.now();
481
+ const daaScore = state.daaScore || "0";
482
+ const txId = generateDeterministicFailedTxId(preStateHash, error.message, daaScore);
468
483
  const receipt = {
469
484
  schema: "hardkas.txReceipt",
470
485
  status: "failed",
471
486
  mode: "simulated",
472
487
  txId,
473
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
488
+ createdAt: "1970-01-01T00:00:00.000Z",
474
489
  errors: [error.message],
475
490
  preStateHash,
476
- postStateHash: preStateHash
491
+ postStateHash: preStateHash,
492
+ dagContext: buildDagContextFromState(state)
477
493
  };
478
494
  return {
479
495
  ok: false,
@@ -530,32 +546,25 @@ function applySimulatedPlan(state, planArtifact, options) {
530
546
  const receipt = createSimulatedTxReceipt(planArtifact, txId, {
531
547
  spentUtxoIds,
532
548
  createdUtxoIds,
533
- daaScore: nextDaaScore
549
+ daaScore: nextDaaScore,
550
+ preStateHash,
551
+ postStateHash,
552
+ dagContext: buildDagContextFromState(state)
534
553
  });
535
- receipt.preStateHash = preStateHash;
536
- receipt.postStateHash = postStateHash;
537
- if (state.dag) {
538
- receipt.dagContext = {
539
- mode: "dag-light",
540
- sink: state.dag.sink,
541
- acceptedTxIds: state.dag.acceptedTxIds,
542
- displacedTxIds: state.dag.displacedTxIds,
543
- conflictSet: state.dag.conflictSet
544
- };
545
- } else {
546
- receipt.dagContext = { mode: "linear", sink: "linear-pseudo-sink" };
547
- }
548
554
  return { ok: true, state: nextState, receipt, planArtifact, errors };
549
555
  } catch (error) {
556
+ const daaScore = state.daaScore || "0";
557
+ const txId = generateDeterministicFailedTxId(preStateHash, error.message, daaScore);
550
558
  const receipt = {
551
559
  schema: "hardkas.txReceipt",
552
560
  status: "failed",
553
561
  mode: "simulated",
554
- txId: "failed-replay",
555
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
562
+ txId,
563
+ createdAt: "1970-01-01T00:00:00.000Z",
556
564
  errors: [error.message],
557
565
  preStateHash,
558
- postStateHash: preStateHash
566
+ postStateHash: preStateHash,
567
+ dagContext: buildDagContextFromState(state)
559
568
  };
560
569
  return { ok: false, state, receipt, errors: [error.message] };
561
570
  }
@@ -563,61 +572,90 @@ function applySimulatedPlan(state, planArtifact, options) {
563
572
 
564
573
  // src/replay.ts
565
574
  import {
566
- calculateContentHash as calculateContentHash3
575
+ calculateContentHash as calculateContentHash3,
576
+ diffArtifacts
567
577
  } from "@hardkas/artifacts";
568
578
  import { coreEvents } from "@hardkas/core";
569
579
  function verifyReplay(state, originalPlan, originalReceipt) {
570
580
  const errors = [];
581
+ const reportDivergences = [];
571
582
  const currentPlanHash = calculateContentHash3(originalPlan);
583
+ let planOk = true;
572
584
  if (originalPlan.contentHash && currentPlanHash !== originalPlan.contentHash) {
585
+ planOk = false;
573
586
  const errorMsg = `TxPlan contentHash mismatch: expected ${originalPlan.contentHash}, got ${currentPlanHash}`;
574
587
  errors.push(errorMsg);
575
- coreEvents.normalizeAndEmit({
576
- kind: "replay.divergence",
577
- txId: originalReceipt.txId,
578
- field: "planHash",
579
- expected: originalPlan.contentHash,
580
- actual: currentPlanHash
588
+ reportDivergences.push({ path: "plan.contentHash", expected: originalPlan.contentHash, actual: currentPlanHash });
589
+ }
590
+ const originalPreState = originalReceipt.preStateHash;
591
+ if (originalPreState) {
592
+ const currentStateHash = calculateStateHash(state);
593
+ if (currentStateHash !== originalPreState) {
594
+ const errorMsg = `preStateHash mismatch: expected ${originalPreState}, got ${currentStateHash}`;
595
+ errors.push(errorMsg);
596
+ reportDivergences.push({
597
+ path: "preStateHash",
598
+ expected: originalPreState,
599
+ actual: currentStateHash
600
+ });
601
+ }
602
+ } else {
603
+ reportDivergences.push({
604
+ path: "preStateHash",
605
+ expected: "present",
606
+ actual: "missing",
607
+ severity: "warning"
581
608
  });
582
609
  }
583
610
  const result = applySimulatedPlan(state, originalPlan, { txId: originalReceipt.txId });
584
611
  const replayReceipt = result.receipt;
585
- const checks = [
586
- { field: "status", expected: originalReceipt.status, actual: replayReceipt.status },
587
- { field: "mass", expected: String(originalReceipt.mass), actual: String(replayReceipt.mass) },
588
- { field: "feeSompi", expected: String(originalReceipt.feeSompi), actual: String(replayReceipt.feeSompi) },
589
- { field: "preStateHash", expected: String(originalReceipt.preStateHash), actual: String(replayReceipt.preStateHash) },
590
- { field: "postStateHash", expected: String(originalReceipt.postStateHash), actual: String(replayReceipt.postStateHash) },
591
- { field: "spentUtxos", expected: String(originalReceipt.spentUtxoIds?.length), actual: String(replayReceipt.spentUtxoIds?.length) }
592
- ];
593
- for (const check of checks) {
594
- if (check.expected !== check.actual) {
595
- errors.push(`${check.field} mismatch: expected ${check.expected}, got ${check.actual}`);
596
- coreEvents.normalizeAndEmit({
597
- kind: "replay.divergence",
598
- txId: originalReceipt.txId,
599
- field: check.field,
600
- expected: check.expected,
601
- actual: check.actual
612
+ const diff = diffArtifacts(originalReceipt, replayReceipt);
613
+ if (!diff.identical) {
614
+ for (const entry of diff.entries) {
615
+ reportDivergences.push({
616
+ path: `receipt.${entry.path}`,
617
+ expected: entry.left,
618
+ actual: entry.right
602
619
  });
620
+ errors.push(`Receipt divergence at ${entry.path}: expected ${JSON.stringify(entry.left)}, got ${JSON.stringify(entry.right)}`);
603
621
  }
604
622
  }
605
- if (errors.length === 0) {
623
+ for (const div of reportDivergences) {
624
+ coreEvents.normalizeAndEmit({
625
+ kind: "replay.divergence",
626
+ txId: originalReceipt.txId,
627
+ field: div.path,
628
+ expected: String(div.expected),
629
+ actual: String(div.actual)
630
+ });
631
+ }
632
+ const invariantsOk = errors.length === 0;
633
+ if (invariantsOk) {
606
634
  coreEvents.normalizeAndEmit({
607
635
  kind: "replay.verified",
608
636
  txId: originalReceipt.txId
609
637
  });
610
638
  }
611
639
  return {
612
- planOk: !errors.some((e) => e.includes("TxPlan")),
613
- receiptOk: !errors.some((e) => e.includes("Status") || e.includes("Hash")),
614
- invariantsOk: errors.length === 0,
640
+ schema: "hardkas.replayReport.v1",
641
+ txId: originalReceipt.txId,
642
+ planOk,
643
+ receiptOk: !diff.entries.some((e) => !e.path.startsWith("plan")),
644
+ invariantsOk,
645
+ checks: {
646
+ workflowDeterministic: invariantsOk ? "reproduced" : "diverged",
647
+ consensusValidation: "unimplemented",
648
+ // Explicit trust boundary
649
+ l2BridgeCorrectness: "unimplemented"
650
+ // Explicit trust boundary
651
+ },
652
+ divergences: reportDivergences,
615
653
  errors
616
654
  };
617
655
  }
618
656
  async function getSimulatedReplaySummary(txId, options = {}) {
619
- const { loadSimulatedReceipt: loadSimulatedReceipt2 } = await import("./receipts-ICI3GEJW.js");
620
- const { loadSimulatedTrace: loadSimulatedTrace2 } = await import("./traces-DSACQ53V.js");
657
+ const { loadSimulatedReceipt: loadSimulatedReceipt2 } = await import("./receipts-JSTNZMDD.js");
658
+ const { loadSimulatedTrace: loadSimulatedTrace2 } = await import("./traces-HDVV77MQ.js");
621
659
  const receipt = await loadSimulatedReceipt2(txId, options);
622
660
  const trace = await loadSimulatedTrace2(txId, options);
623
661
  if (!receipt || !trace) {
@@ -638,7 +676,51 @@ async function getSimulatedReplaySummary(txId, options = {}) {
638
676
  }
639
677
 
640
678
  // src/dag.ts
641
- function createSimulatedDag() {
679
+ import {
680
+ ApproxGhostdagEngine,
681
+ GhostdagStore,
682
+ genesisGhostdagData,
683
+ findSelectedParent,
684
+ GENESIS_HASH as SIM_GENESIS_HASH
685
+ } from "@hardkas/simulator";
686
+ var dagIdMaps = /* @__PURE__ */ new WeakMap();
687
+ var dagReverseIdMaps = /* @__PURE__ */ new WeakMap();
688
+ function getIdMap(dag) {
689
+ let m = dagIdMaps.get(dag);
690
+ if (!m) {
691
+ m = /* @__PURE__ */ new Map();
692
+ m.set("genesis", SIM_GENESIS_HASH);
693
+ dagIdMaps.set(dag, m);
694
+ }
695
+ return m;
696
+ }
697
+ function getReverseIdMap(dag) {
698
+ let m = dagReverseIdMaps.get(dag);
699
+ if (!m) {
700
+ m = /* @__PURE__ */ new Map();
701
+ m.set(SIM_GENESIS_HASH, "genesis");
702
+ dagReverseIdMaps.set(dag, m);
703
+ }
704
+ return m;
705
+ }
706
+ function toGhostdagSimBlock(block, idMap) {
707
+ const hash = idMap.get(block.id) || block.id;
708
+ const parents = block.parents.map((p) => idMap.get(p) || p);
709
+ return {
710
+ header: {
711
+ hash,
712
+ parents,
713
+ timestampUs: 0,
714
+ minerId: 0,
715
+ bits: 1e3,
716
+ // Default difficulty for simulation
717
+ nonce: 0
718
+ }
719
+ };
720
+ }
721
+ function createSimulatedDag(options) {
722
+ const k = options?.k ?? 18;
723
+ const engine = new ApproxGhostdagEngine(k);
642
724
  const genesis = {
643
725
  id: "genesis",
644
726
  parents: [],
@@ -647,21 +729,70 @@ function createSimulatedDag() {
647
729
  acceptedTxIds: [],
648
730
  isGenesis: true
649
731
  };
650
- return {
732
+ const gdStore = new GhostdagStore();
733
+ gdStore.insert(SIM_GENESIS_HASH, genesisGhostdagData(SIM_GENESIS_HASH));
734
+ const dag = {
651
735
  blocks: { [genesis.id]: genesis },
652
736
  sink: genesis.id,
653
737
  selectedPathToSink: [genesis.id],
654
738
  acceptedTxIds: [],
655
739
  displacedTxIds: [],
656
- conflictSet: []
740
+ conflictSet: [],
741
+ ghostdagStore: gdStore,
742
+ ghostdagEngine: engine
657
743
  };
744
+ const idMap = /* @__PURE__ */ new Map();
745
+ idMap.set("genesis", SIM_GENESIS_HASH);
746
+ dagIdMaps.set(dag, idMap);
747
+ const reverseIdMap = /* @__PURE__ */ new Map();
748
+ reverseIdMap.set(SIM_GENESIS_HASH, "genesis");
749
+ dagReverseIdMaps.set(dag, reverseIdMap);
750
+ return dag;
658
751
  }
659
752
  function addSimulatedBlock(dag, block) {
660
- const newBlocks = { ...dag.blocks, [block.id]: block };
661
- return {
753
+ const idMap = getIdMap(dag);
754
+ const reverseIdMap = getReverseIdMap(dag);
755
+ const gdBlock = toGhostdagSimBlock(block, idMap);
756
+ const allGdBlocks = /* @__PURE__ */ new Map();
757
+ for (const b of Object.values(dag.blocks)) {
758
+ allGdBlocks.set(idMap.get(b.id) || b.id, toGhostdagSimBlock(b, idMap));
759
+ }
760
+ allGdBlocks.set(gdBlock.header.hash, gdBlock);
761
+ const gdStore = dag.ghostdagStore || new GhostdagStore();
762
+ const dagEngine = dag.ghostdagEngine || new ApproxGhostdagEngine();
763
+ const gdData = dagEngine.computeGhostdag(gdBlock, allGdBlocks, gdStore);
764
+ gdStore.insert(gdBlock.header.hash, gdData);
765
+ const updatedBlock = {
766
+ ...block,
767
+ ghostdagData: gdData,
768
+ blueWork: gdData.blueWork.toString(),
769
+ isBlue: true
770
+ // The block itself is the new tip on its own path
771
+ };
772
+ if (!idMap.has(block.id)) {
773
+ idMap.set(block.id, block.id);
774
+ reverseIdMap.set(block.id, block.id);
775
+ }
776
+ const newBlocks = { ...dag.blocks, [updatedBlock.id]: updatedBlock };
777
+ for (const blueHash of gdData.mergesetBlues) {
778
+ const id = reverseIdMap.get(blueHash) || blueHash;
779
+ if (newBlocks[id]) {
780
+ newBlocks[id] = { ...newBlocks[id], isBlue: true };
781
+ }
782
+ }
783
+ for (const redHash of gdData.mergesetReds) {
784
+ const id = reverseIdMap.get(redHash) || redHash;
785
+ if (newBlocks[id]) {
786
+ newBlocks[id] = { ...newBlocks[id], isBlue: false };
787
+ }
788
+ }
789
+ const newDag = {
662
790
  ...dag,
663
791
  blocks: newBlocks
664
792
  };
793
+ dagIdMaps.set(newDag, idMap);
794
+ dagReverseIdMaps.set(newDag, reverseIdMap);
795
+ return newDag;
665
796
  }
666
797
  function moveSink(dag, newSinkId, txProvider) {
667
798
  if (!dag.blocks[newSinkId]) {
@@ -670,6 +801,12 @@ function moveSink(dag, newSinkId, txProvider) {
670
801
  const selectedPath = calculateSelectedPath(dag, newSinkId);
671
802
  const reachableBlocks = identifyReachableBlocks(dag, newSinkId);
672
803
  const sortedBlocks = reachableBlocks.sort((a, b) => {
804
+ if (a.ghostdagData && b.ghostdagData) {
805
+ const workA = BigInt(a.blueWork || "0");
806
+ const workB = BigInt(b.blueWork || "0");
807
+ if (workA !== workB) return workA < workB ? -1 : 1;
808
+ if (a.isBlue !== b.isBlue) return a.isBlue ? -1 : 1;
809
+ }
673
810
  const daaA = BigInt(a.daaScore);
674
811
  const daaB = BigInt(b.daaScore);
675
812
  if (daaA !== daaB) return daaA < daaB ? -1 : 1;
@@ -742,14 +879,21 @@ function moveSink(dag, newSinkId, txProvider) {
742
879
  };
743
880
  }
744
881
  function calculateSelectedPath(dag, sinkId) {
882
+ const reverseIdMap = getReverseIdMap(dag);
745
883
  const path2 = [];
746
884
  let currentId = sinkId;
747
885
  while (currentId) {
748
886
  const current = dag.blocks[currentId];
749
887
  if (!current) break;
750
888
  path2.unshift(current);
751
- if (current.parents.length === 0) break;
752
- currentId = current.parents[0];
889
+ if (current.isGenesis) break;
890
+ if (current.ghostdagData) {
891
+ const spHash = current.ghostdagData.selectedParent;
892
+ currentId = reverseIdMap.get(spHash) || spHash;
893
+ } else {
894
+ if (current.parents.length === 0) break;
895
+ currentId = current.parents[0];
896
+ }
753
897
  }
754
898
  return path2;
755
899
  }
@@ -780,6 +924,12 @@ function resolveConflictsDeterministically(txs, dag) {
780
924
  const inPathA = dag.selectedPathToSink.includes(a.blockId);
781
925
  const inPathB = dag.selectedPathToSink.includes(b.blockId);
782
926
  if (inPathA !== inPathB) return inPathA ? -1 : 1;
927
+ if (blockA.ghostdagData && blockB.ghostdagData) {
928
+ if (blockA.isBlue !== blockB.isBlue) return blockA.isBlue ? -1 : 1;
929
+ const workA = BigInt(blockA.blueWork || "0");
930
+ const workB = BigInt(blockB.blueWork || "0");
931
+ if (workA !== workB) return workA < workB ? -1 : 1;
932
+ }
783
933
  const daaA = BigInt(blockA.daaScore);
784
934
  const daaB = BigInt(blockB.daaScore);
785
935
  if (daaA !== daaB) return daaA < daaB ? -1 : 1;
@@ -816,6 +966,85 @@ function resolveConflictsDeterministically(txs, dag) {
816
966
  }
817
967
  return { accepted, displaced, conflicts };
818
968
  }
969
+ function getDagColoring(dag) {
970
+ const coloring = /* @__PURE__ */ new Map();
971
+ for (const [id, block] of Object.entries(dag.blocks)) {
972
+ coloring.set(id, {
973
+ isBlue: block.isBlue || false,
974
+ blueWork: block.blueWork || "0",
975
+ blueScore: block.ghostdagData?.blueScore || Number(block.blueScore) || 0
976
+ });
977
+ }
978
+ return coloring;
979
+ }
980
+ function getSelectedChain(dag) {
981
+ return calculateSelectedPath(dag, dag.sink).map((b) => b.id);
982
+ }
983
+ function findBestTip(dag) {
984
+ const idMap = getIdMap(dag);
985
+ const allBlockIds = Object.keys(dag.blocks);
986
+ if (allBlockIds.length === 0) return "genesis";
987
+ const parentIds = /* @__PURE__ */ new Set();
988
+ for (const block of Object.values(dag.blocks)) {
989
+ for (const p of block.parents) {
990
+ parentIds.add(p);
991
+ }
992
+ }
993
+ const tips = allBlockIds.filter((id) => !parentIds.has(id));
994
+ if (tips.length === 0) return dag.sink;
995
+ const tipData = tips.map((id) => ({
996
+ hash: idMap.get(id) || id,
997
+ blueWork: BigInt(dag.blocks[id].blueWork || "0")
998
+ }));
999
+ const bestTipHash = findSelectedParent(tipData);
1000
+ const bestTip = tips.find((id) => (idMap.get(id) || id) === bestTipHash);
1001
+ return bestTip || tips[0];
1002
+ }
1003
+
1004
+ // src/fork.ts
1005
+ import { ARTIFACT_SCHEMAS as ARTIFACT_SCHEMAS2, HARDKAS_VERSION as HARDKAS_VERSION4, ARTIFACT_VERSION as ARTIFACT_VERSION3 } from "@hardkas/artifacts";
1006
+ async function forkFromNetwork(rpc, opts) {
1007
+ const info = await rpc.getInfo();
1008
+ const networkId = info.networkId || opts.network;
1009
+ const currentDaaScore = info.virtualDaaScore?.toString() || "0";
1010
+ const targetDaaScore = opts.atDaaScore || currentDaaScore;
1011
+ const utxos = [];
1012
+ for (const address of opts.addresses) {
1013
+ const rpcUtxos = await rpc.getUtxosByAddress(address);
1014
+ for (const u of rpcUtxos) {
1015
+ utxos.push({
1016
+ id: `${u.outpoint.transactionId}:${u.outpoint.index}`,
1017
+ address: u.address,
1018
+ amountSompi: u.amountSompi.toString(),
1019
+ spent: false,
1020
+ createdAtDaaScore: u.blockDaaScore?.toString() || "0"
1021
+ });
1022
+ }
1023
+ }
1024
+ const state = {
1025
+ schema: ARTIFACT_SCHEMAS2.LOCALNET_STATE,
1026
+ hardkasVersion: HARDKAS_VERSION4,
1027
+ version: ARTIFACT_VERSION3,
1028
+ hashVersion: "sha256-canonical",
1029
+ mode: "simulated",
1030
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1031
+ networkId,
1032
+ daaScore: targetDaaScore,
1033
+ accounts: opts.addresses.map((addr, i) => ({
1034
+ name: `forked_${i}`,
1035
+ address: addr
1036
+ })),
1037
+ utxos,
1038
+ forkSource: {
1039
+ network: opts.network,
1040
+ rpcUrl: opts.rpcUrl,
1041
+ daaScore: targetDaaScore,
1042
+ forkedAt: (/* @__PURE__ */ new Date()).toISOString(),
1043
+ addresses: opts.addresses
1044
+ }
1045
+ };
1046
+ return state;
1047
+ }
819
1048
  export {
820
1049
  DUST_LIMIT_SOMPI,
821
1050
  SimulatedKaspaChain,
@@ -829,14 +1058,18 @@ export {
829
1058
  createInitialLocalnetState,
830
1059
  createLocalnetSnapshot,
831
1060
  createSimulatedDag,
1061
+ findBestTip,
1062
+ forkFromNetwork,
832
1063
  fundAddress,
833
1064
  getAccountBalanceSompi,
834
1065
  getAddressBalanceSompi,
1066
+ getDagColoring,
835
1067
  getDefaultLocalnetDir,
836
1068
  getDefaultLocalnetStatePath,
837
1069
  getDefaultReceiptsDir,
838
1070
  getDefaultTracesDir,
839
1071
  getReceiptPath,
1072
+ getSelectedChain,
840
1073
  getSimulatedReplaySummary,
841
1074
  getSpendableUtxos,
842
1075
  getTracePath,
@@ -4,7 +4,7 @@ import {
4
4
  listSimulatedReceipts,
5
5
  loadSimulatedReceipt,
6
6
  saveSimulatedReceipt
7
- } from "./chunk-CXDVB3K4.js";
7
+ } from "./chunk-GVBX3TPM.js";
8
8
  export {
9
9
  getDefaultReceiptsDir,
10
10
  getReceiptPath,
@@ -4,7 +4,7 @@ import {
4
4
  listSimulatedTraces,
5
5
  loadSimulatedTrace,
6
6
  saveSimulatedTrace
7
- } from "./chunk-W7ZBGUNK.js";
7
+ } from "./chunk-DPHHOGGK.js";
8
8
  export {
9
9
  getDefaultTracesDir,
10
10
  getTracePath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardkas/localnet",
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,12 +8,15 @@
8
8
  ".": "./dist/index.js"
9
9
  },
10
10
  "dependencies": {
11
- "@hardkas/artifacts": "0.2.2-alpha",
12
- "@hardkas/tx-builder": "0.2.2-alpha",
13
- "@hardkas/core": "0.2.2-alpha",
14
- "@hardkas/query": "0.2.2-alpha"
11
+ "@hardkas/artifacts": "0.3.0-alpha",
12
+ "@hardkas/query": "0.3.0-alpha",
13
+ "@hardkas/tx-builder": "0.3.0-alpha",
14
+ "@hardkas/core": "0.3.0-alpha",
15
+ "@hardkas/kaspa-rpc": "0.3.0-alpha",
16
+ "@hardkas/simulator": "0.3.0-alpha"
15
17
  },
16
18
  "devDependencies": {
19
+ "fast-check": "^4.8.0",
17
20
  "tsup": "^8.3.5",
18
21
  "typescript": "^5.7.2",
19
22
  "vitest": "^2.1.8"