@cogcoin/client 1.1.0 → 1.1.2

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.
@@ -32,6 +32,8 @@ const BEST_BLOCK_POLL_INTERVAL_MS = 500;
32
32
  const BACKGROUND_START_TIMEOUT_MS = 15_000;
33
33
  const MINING_BITCOIN_RECOVERY_GRACE_MS = 15_000;
34
34
  const MINING_BITCOIN_RECOVERY_RESTART_COOLDOWN_MS = 60_000;
35
+ const MINING_SUSPEND_HEARTBEAT_INTERVAL_MS = 1_000;
36
+ const MINING_MEMPOOL_COOPERATIVE_YIELD_EVERY = 25;
35
37
  const MINING_BITCOIN_RECOVERY_NOTE = "Mining lost contact with the local Bitcoin RPC service and is waiting for it to recover.";
36
38
  function resolveBip39WordsFromIndices(indices) {
37
39
  if (indices === null || indices === undefined) {
@@ -62,6 +64,38 @@ function resolveSettledWinnerRequiredWords(options) {
62
64
  function resolveSnapshotOverride(override, fallback) {
63
65
  return override === undefined ? fallback : override;
64
66
  }
67
+ function resolveMiningProviderBackoffDelayMs(consecutiveFailureCount) {
68
+ const exponent = Math.max(consecutiveFailureCount - 1, 0);
69
+ return Math.min(MINING_PROVIDER_BACKOFF_BASE_MS * (2 ** exponent), MINING_PROVIDER_BACKOFF_MAX_MS);
70
+ }
71
+ function clearMiningProviderWait(loopState, resetTransientFailureCount = true) {
72
+ loopState.providerWaitState = null;
73
+ loopState.providerWaitLastError = null;
74
+ loopState.providerWaitNextRetryAtUnixMs = null;
75
+ if (resetTransientFailureCount) {
76
+ loopState.providerTransientFailureCount = 0;
77
+ }
78
+ }
79
+ function recordTransientMiningProviderWait(options) {
80
+ options.loopState.providerTransientFailureCount += 1;
81
+ options.loopState.providerWaitState = options.error.providerState === "rate-limited"
82
+ ? "rate-limited"
83
+ : "backoff";
84
+ options.loopState.providerWaitLastError = options.error.message;
85
+ options.loopState.providerWaitNextRetryAtUnixMs = options.nowUnixMs
86
+ + resolveMiningProviderBackoffDelayMs(options.loopState.providerTransientFailureCount);
87
+ }
88
+ function recordTerminalMiningProviderWait(options) {
89
+ clearMiningProviderWait(options.loopState);
90
+ if (options.error.providerState !== "auth-error" && options.error.providerState !== "not-found") {
91
+ throw new Error("mining_provider_wait_state_invalid");
92
+ }
93
+ options.loopState.providerWaitState = options.error.providerState;
94
+ options.loopState.providerWaitLastError = options.error.message;
95
+ }
96
+ function isTransientMiningProviderError(error) {
97
+ return error.providerState === "unavailable" || error.providerState === "rate-limited";
98
+ }
65
99
  class MiningSuspendDetectedError extends Error {
66
100
  detectedAtUnixMs;
67
101
  constructor(detectedAtUnixMs) {
@@ -78,20 +112,75 @@ class MiningPublishRejectedError extends Error {
78
112
  }
79
113
  }
80
114
  const miningGateCache = new Map();
81
- function createMiningSuspendDetector(monotonicNow = performance.now()) {
82
- return {
83
- lastMonotonicMs: monotonicNow,
115
+ const defaultMiningSuspendScheduler = {
116
+ every(intervalMs, callback) {
117
+ const timer = setInterval(callback, intervalMs);
118
+ timer.unref?.();
119
+ return {
120
+ clear() {
121
+ clearInterval(timer);
122
+ },
123
+ };
124
+ },
125
+ };
126
+ function refreshMiningSuspendDetector(detector) {
127
+ if (detector === undefined) {
128
+ return;
129
+ }
130
+ const monotonicNow = detector.monotonicNow();
131
+ const gapMs = monotonicNow - detector.lastHeartbeatMonotonicMs;
132
+ detector.lastHeartbeatMonotonicMs = monotonicNow;
133
+ if (gapMs > MINING_SUSPEND_GAP_THRESHOLD_MS
134
+ && detector.detectedAtUnixMs === null) {
135
+ detector.detectedAtUnixMs = detector.nowUnixMs();
136
+ }
137
+ }
138
+ function createMiningSuspendDetector(options = {}) {
139
+ const monotonicNow = options.monotonicNow ?? (() => performance.now());
140
+ const nowUnixMs = options.nowUnixMs ?? Date.now;
141
+ const scheduler = options.scheduler ?? defaultMiningSuspendScheduler;
142
+ let heartbeat = null;
143
+ const detector = {
144
+ lastHeartbeatMonotonicMs: monotonicNow(),
145
+ detectedAtUnixMs: null,
146
+ monotonicNow,
147
+ nowUnixMs,
148
+ stop() {
149
+ heartbeat?.clear();
150
+ heartbeat = null;
151
+ },
84
152
  };
153
+ heartbeat = scheduler.every(MINING_SUSPEND_HEARTBEAT_INTERVAL_MS, () => {
154
+ refreshMiningSuspendDetector(detector);
155
+ });
156
+ return detector;
85
157
  }
86
- function checkpointMiningSuspendDetector(detector, monotonicNow = performance.now()) {
158
+ function throwIfMiningSuspendDetected(detector) {
87
159
  if (detector === undefined) {
88
160
  return;
89
161
  }
90
- const gapMs = monotonicNow - detector.lastMonotonicMs;
91
- detector.lastMonotonicMs = monotonicNow;
92
- if (gapMs > MINING_SUSPEND_GAP_THRESHOLD_MS) {
93
- throw new MiningSuspendDetectedError(Date.now());
162
+ refreshMiningSuspendDetector(detector);
163
+ if (detector.detectedAtUnixMs === null) {
164
+ return;
94
165
  }
166
+ const detectedAtUnixMs = detector.detectedAtUnixMs;
167
+ detector.detectedAtUnixMs = null;
168
+ throw new MiningSuspendDetectedError(detectedAtUnixMs);
169
+ }
170
+ function stopMiningSuspendDetector(detector) {
171
+ detector?.stop();
172
+ }
173
+ function defaultMiningCooperativeYield() {
174
+ return new Promise((resolve) => {
175
+ setImmediate(resolve);
176
+ });
177
+ }
178
+ async function maybeYieldDuringMempoolScan(options) {
179
+ const yieldEvery = options.cooperativeYieldEvery ?? MINING_MEMPOOL_COOPERATIVE_YIELD_EVERY;
180
+ if (yieldEvery <= 0 || options.iteration === 0 || (options.iteration % yieldEvery) !== 0) {
181
+ return;
182
+ }
183
+ await (options.cooperativeYield ?? defaultMiningCooperativeYield)();
95
184
  }
96
185
  function clearMiningGateCache(walletRootId) {
97
186
  if (walletRootId === null || walletRootId === undefined) {
@@ -473,6 +562,10 @@ function createMiningLoopState() {
473
562
  selectedCandidate: null,
474
563
  ui: createEmptyMiningFollowVisualizerState(),
475
564
  waitingNote: null,
565
+ providerWaitState: null,
566
+ providerWaitLastError: null,
567
+ providerWaitNextRetryAtUnixMs: null,
568
+ providerTransientFailureCount: 0,
476
569
  bitcoinRecoveryFirstFailureAtUnixMs: null,
477
570
  bitcoinRecoveryFirstUnreachableAtUnixMs: null,
478
571
  bitcoinRecoveryLastRestartAttemptAtUnixMs: null,
@@ -527,6 +620,25 @@ function resetMiningUiForTip(loopState, targetBlockHeight) {
527
620
  export function resetMiningUiForTipForTesting(loopState, targetBlockHeight) {
528
621
  resetMiningUiForTip(loopState, targetBlockHeight);
529
622
  }
623
+ function resolveProvisionalBroadcastTxidForCandidate(options) {
624
+ if (options.liveState === null || options.liveState === undefined) {
625
+ return null;
626
+ }
627
+ const liveState = normalizeMiningStateRecord(options.liveState);
628
+ if (liveState.currentTxid === null
629
+ || liveState.currentPublishState !== "in-mempool"
630
+ || liveState.livePublishInMempool !== true) {
631
+ return null;
632
+ }
633
+ if (liveState.currentDomain !== options.candidate.domainName
634
+ || liveState.currentDomainId !== options.candidate.domainId
635
+ || liveState.currentSentence !== options.candidate.sentence
636
+ || liveState.currentBlockTargetHeight !== options.candidate.targetBlockHeight
637
+ || liveState.currentReferencedBlockHashDisplay !== options.candidate.referencedBlockHashDisplay) {
638
+ return null;
639
+ }
640
+ return liveState.currentTxid;
641
+ }
530
642
  function fallbackSettledWinnerDomainName(domainId) {
531
643
  return `domain-${domainId}`;
532
644
  }
@@ -600,13 +712,17 @@ function syncMiningUiForCurrentTip(options) {
600
712
  tipChanged,
601
713
  };
602
714
  }
603
- function setMiningUiCandidate(loopState, candidate) {
715
+ function setMiningUiCandidate(loopState, candidate, liveState) {
604
716
  loopState.ui.latestSentence = candidate.sentence;
605
717
  loopState.ui.provisionalRequiredWords = [...candidate.bip39Words];
606
718
  loopState.ui.provisionalEntry = {
607
719
  domainName: candidate.domainName,
608
720
  sentence: candidate.sentence,
609
721
  };
722
+ loopState.ui.provisionalBroadcastTxid = resolveProvisionalBroadcastTxidForCandidate({
723
+ candidate,
724
+ liveState,
725
+ });
610
726
  }
611
727
  function getSelectedCandidateForTip(loopState, tipKey) {
612
728
  if (tipKey === null || loopState.selectedCandidateTipKey !== tipKey) {
@@ -617,13 +733,13 @@ function getSelectedCandidateForTip(loopState, tipKey) {
617
733
  export function getSelectedCandidateForTipForTesting(loopState, tipKey) {
618
734
  return getSelectedCandidateForTip(loopState, tipKey);
619
735
  }
620
- function cacheSelectedCandidateForTip(loopState, tipKey, candidate) {
736
+ function cacheSelectedCandidateForTip(loopState, tipKey, candidate, liveState) {
621
737
  loopState.selectedCandidateTipKey = tipKey;
622
738
  loopState.selectedCandidate = candidate;
623
- setMiningUiCandidate(loopState, candidate);
739
+ setMiningUiCandidate(loopState, candidate, liveState);
624
740
  }
625
- export function cacheSelectedCandidateForTipForTesting(loopState, tipKey, candidate) {
626
- cacheSelectedCandidateForTip(loopState, tipKey, candidate);
741
+ export function cacheSelectedCandidateForTipForTesting(loopState, tipKey, candidate, liveState) {
742
+ cacheSelectedCandidateForTip(loopState, tipKey, candidate, liveState);
627
743
  }
628
744
  function clearSelectedCandidate(loopState) {
629
745
  loopState.selectedCandidateTipKey = null;
@@ -635,6 +751,7 @@ function clearMiningUiTransientCandidate(loopState) {
635
751
  domainName: null,
636
752
  sentence: null,
637
753
  };
754
+ loopState.ui.provisionalBroadcastTxid = null;
638
755
  loopState.ui.latestSentence = null;
639
756
  }
640
757
  function discardMiningLoopTransientWork(loopState, walletRootId) {
@@ -642,6 +759,7 @@ function discardMiningLoopTransientWork(loopState, walletRootId) {
642
759
  clearSelectedCandidate(loopState);
643
760
  clearMiningUiTransientCandidate(loopState);
644
761
  loopState.waitingNote = null;
762
+ clearMiningProviderWait(loopState);
645
763
  }
646
764
  function resolveMiningBitcoindRecoveryIdentity(value) {
647
765
  const raw = (value ?? {});
@@ -948,39 +1066,61 @@ function getAncestorTxids(context, txContexts) {
948
1066
  .filter((txid) => txid !== null && txContexts.has(txid));
949
1067
  }
950
1068
  function topologicallyOrderAncestorContexts(options) {
951
- const visited = new Set();
952
- const visiting = new Set();
1069
+ const visited = new Map();
953
1070
  const ordered = [];
954
- const visit = (txid) => {
955
- if (visited.has(txid)) {
956
- return true;
957
- }
958
- if (visiting.has(txid)) {
959
- return false;
960
- }
961
- const context = options.txContexts.get(txid);
962
- if (context === undefined) {
963
- return true;
964
- }
965
- visiting.add(txid);
966
- for (const parentTxid of getAncestorTxids(context, options.txContexts)) {
967
- if (!visit(parentTxid)) {
968
- return false;
969
- }
970
- }
971
- visiting.delete(txid);
972
- visited.add(txid);
973
- ordered.push(context);
974
- return true;
975
- };
976
1071
  const root = options.txContexts.get(options.txid);
977
1072
  if (root === undefined) {
978
1073
  return [];
979
1074
  }
980
- for (const parentTxid of getAncestorTxids(root, options.txContexts)) {
981
- if (!visit(parentTxid)) {
1075
+ const stack = getAncestorTxids(root, options.txContexts)
1076
+ .reverse()
1077
+ .map((txid) => ({
1078
+ txid,
1079
+ expanded: false,
1080
+ }));
1081
+ while (stack.length > 0) {
1082
+ const frame = stack.pop();
1083
+ const state = visited.get(frame.txid);
1084
+ if (frame.expanded) {
1085
+ if (state !== "visiting") {
1086
+ continue;
1087
+ }
1088
+ visited.set(frame.txid, "visited");
1089
+ const context = options.txContexts.get(frame.txid);
1090
+ if (context !== undefined) {
1091
+ ordered.push(context);
1092
+ }
1093
+ continue;
1094
+ }
1095
+ if (state === "visited") {
1096
+ continue;
1097
+ }
1098
+ if (state === "visiting") {
982
1099
  return null;
983
1100
  }
1101
+ const context = options.txContexts.get(frame.txid);
1102
+ if (context === undefined) {
1103
+ continue;
1104
+ }
1105
+ visited.set(frame.txid, "visiting");
1106
+ stack.push({
1107
+ txid: frame.txid,
1108
+ expanded: true,
1109
+ });
1110
+ const parents = getAncestorTxids(context, options.txContexts);
1111
+ for (let index = parents.length - 1; index >= 0; index -= 1) {
1112
+ const parentTxid = parents[index];
1113
+ const parentState = visited.get(parentTxid);
1114
+ if (parentState === "visiting") {
1115
+ return null;
1116
+ }
1117
+ if (parentState !== "visited") {
1118
+ stack.push({
1119
+ txid: parentTxid,
1120
+ expanded: false,
1121
+ });
1122
+ }
1123
+ }
984
1124
  }
985
1125
  return ordered;
986
1126
  }
@@ -1096,13 +1236,17 @@ async function resolveOverlayAuthorizedMiningDomain(options) {
1096
1236
  return authorized ? domain : null;
1097
1237
  }
1098
1238
  function buildStatusSnapshot(view, overrides = {}) {
1239
+ const resolvedCurrentPhase = resolveSnapshotOverride(overrides.currentPhase, view.runtime.currentPhase);
1240
+ const clearProviderWaitCarryover = overrides.currentPhase !== undefined
1241
+ && overrides.currentPhase !== "waiting-provider"
1242
+ && view.runtime.currentPhase === "waiting-provider";
1099
1243
  return {
1100
1244
  ...view.runtime,
1101
1245
  runMode: resolveSnapshotOverride(overrides.runMode, view.runtime.runMode),
1102
1246
  backgroundWorkerPid: resolveSnapshotOverride(overrides.backgroundWorkerPid, view.runtime.backgroundWorkerPid),
1103
1247
  backgroundWorkerRunId: resolveSnapshotOverride(overrides.backgroundWorkerRunId, view.runtime.backgroundWorkerRunId),
1104
1248
  backgroundWorkerHeartbeatAtUnixMs: resolveSnapshotOverride(overrides.backgroundWorkerHeartbeatAtUnixMs, view.runtime.backgroundWorkerHeartbeatAtUnixMs),
1105
- currentPhase: resolveSnapshotOverride(overrides.currentPhase, view.runtime.currentPhase),
1249
+ currentPhase: resolvedCurrentPhase,
1106
1250
  currentPublishState: resolveSnapshotOverride(overrides.currentPublishState, view.runtime.currentPublishState),
1107
1251
  targetBlockHeight: resolveSnapshotOverride(overrides.targetBlockHeight, view.runtime.targetBlockHeight),
1108
1252
  referencedBlockHashDisplay: resolveSnapshotOverride(overrides.referencedBlockHashDisplay, view.runtime.referencedBlockHashDisplay),
@@ -1118,7 +1262,9 @@ function buildStatusSnapshot(view, overrides = {}) {
1118
1262
  lastSuspendDetectedAtUnixMs: resolveSnapshotOverride(overrides.lastSuspendDetectedAtUnixMs, view.runtime.lastSuspendDetectedAtUnixMs),
1119
1263
  reconnectSettledUntilUnixMs: resolveSnapshotOverride(overrides.reconnectSettledUntilUnixMs, view.runtime.reconnectSettledUntilUnixMs),
1120
1264
  tipSettledUntilUnixMs: resolveSnapshotOverride(overrides.tipSettledUntilUnixMs, view.runtime.tipSettledUntilUnixMs),
1121
- providerState: resolveSnapshotOverride(overrides.providerState, view.runtime.providerState),
1265
+ providerState: resolveSnapshotOverride(overrides.providerState, clearProviderWaitCarryover
1266
+ ? (view.provider.status === "ready" ? "ready" : "unavailable")
1267
+ : view.runtime.providerState),
1122
1268
  corePublishState: resolveSnapshotOverride(overrides.corePublishState, view.runtime.corePublishState),
1123
1269
  currentPublishDecision: resolveSnapshotOverride(overrides.currentPublishDecision, view.runtime.currentPublishDecision),
1124
1270
  sameDomainCompetitorSuppressed: resolveSnapshotOverride(overrides.sameDomainCompetitorSuppressed, view.runtime.sameDomainCompetitorSuppressed),
@@ -1128,8 +1274,8 @@ function buildStatusSnapshot(view, overrides = {}) {
1128
1274
  mempoolSequenceCacheStatus: resolveSnapshotOverride(overrides.mempoolSequenceCacheStatus, view.runtime.mempoolSequenceCacheStatus),
1129
1275
  lastMempoolSequence: resolveSnapshotOverride(overrides.lastMempoolSequence, view.runtime.lastMempoolSequence),
1130
1276
  lastCompetitivenessGateAtUnixMs: resolveSnapshotOverride(overrides.lastCompetitivenessGateAtUnixMs, view.runtime.lastCompetitivenessGateAtUnixMs),
1131
- lastError: resolveSnapshotOverride(overrides.lastError, view.runtime.lastError),
1132
- note: resolveSnapshotOverride(overrides.note, view.runtime.note),
1277
+ lastError: resolveSnapshotOverride(overrides.lastError, clearProviderWaitCarryover ? null : view.runtime.lastError),
1278
+ note: resolveSnapshotOverride(overrides.note, clearProviderWaitCarryover ? null : view.runtime.note),
1133
1279
  livePublishInMempool: resolveSnapshotOverride(overrides.livePublishInMempool, view.runtime.livePublishInMempool),
1134
1280
  updatedAtUnixMs: Date.now(),
1135
1281
  };
@@ -1740,6 +1886,7 @@ async function runCompetitivenessGate(options) {
1740
1886
  candidateRank: overrides.candidateRank ?? null,
1741
1887
  });
1742
1888
  const walletRootId = options.readContext.localState.walletRootId ?? "uninitialized-wallet-root";
1889
+ const assaySentencesImpl = options.assaySentencesImpl ?? assaySentences;
1743
1890
  const indexerTruthKey = getIndexerTruthKey(options.readContext);
1744
1891
  const excludedTxids = [options.currentTxid].filter((value) => value !== null).sort();
1745
1892
  const localAssayTupleKey = [
@@ -1789,7 +1936,13 @@ async function runCompetitivenessGate(options) {
1789
1936
  txContexts.delete(txid);
1790
1937
  }
1791
1938
  }
1792
- for (const txid of visibleTxids) {
1939
+ for (let index = 0; index < visibleTxids.length; index += 1) {
1940
+ await maybeYieldDuringMempoolScan({
1941
+ iteration: index,
1942
+ cooperativeYield: options.cooperativeYield,
1943
+ cooperativeYieldEvery: options.cooperativeYieldEvery,
1944
+ });
1945
+ const txid = visibleTxids[index];
1793
1946
  if (txContexts.has(txid)) {
1794
1947
  continue;
1795
1948
  }
@@ -1815,7 +1968,13 @@ async function runCompetitivenessGate(options) {
1815
1968
  });
1816
1969
  }
1817
1970
  const entries = new Map();
1818
- for (const txid of visibleTxids) {
1971
+ for (let index = 0; index < visibleTxids.length; index += 1) {
1972
+ await maybeYieldDuringMempoolScan({
1973
+ iteration: index,
1974
+ cooperativeYield: options.cooperativeYield,
1975
+ cooperativeYieldEvery: options.cooperativeYieldEvery,
1976
+ });
1977
+ const txid = visibleTxids[index];
1819
1978
  const context = txContexts.get(txid);
1820
1979
  if (context === undefined || context.payload === null || context.senderScriptHex === null) {
1821
1980
  continue;
@@ -1854,7 +2013,7 @@ async function runCompetitivenessGate(options) {
1854
2013
  if (overlayDomain === null || overlayDomain.name === null || !rootDomain(overlayDomain.name)) {
1855
2014
  continue;
1856
2015
  }
1857
- const assayed = await assaySentences(decoded.domainId, options.candidate.referencedBlockHashInternal, [Buffer.from(decoded.sentenceBytes).toString("utf8")]).catch(() => []);
2016
+ const assayed = await assaySentencesImpl(decoded.domainId, options.candidate.referencedBlockHashInternal, [Buffer.from(decoded.sentenceBytes).toString("utf8")]).catch(() => []);
1858
2017
  const scored = assayed[0];
1859
2018
  if (scored === undefined || !scored.gatesPass || scored.encodedSentenceBytes === null || scored.canonicalBlend === null) {
1860
2019
  continue;
@@ -2475,6 +2634,8 @@ export async function ensureBuiltInMiningSetupIfNeeded(options) {
2475
2634
  }
2476
2635
  async function performMiningCycle(options) {
2477
2636
  const now = options.nowImpl ?? Date.now;
2637
+ const generateCandidatesForDomainsImpl = options.generateCandidatesForDomainsImpl ?? generateCandidatesForDomains;
2638
+ const runCompetitivenessGateImpl = options.runCompetitivenessGateImpl ?? runCompetitivenessGate;
2478
2639
  let readContext = await options.openReadContext({
2479
2640
  dataDir: options.dataDir,
2480
2641
  databasePath: options.databasePath,
@@ -2483,6 +2644,7 @@ async function performMiningCycle(options) {
2483
2644
  });
2484
2645
  let readContextClosed = false;
2485
2646
  try {
2647
+ throwIfMiningSuspendDetected(options.suspendDetector);
2486
2648
  let clearRecoveredBitcoindError = false;
2487
2649
  const saveCycleStatus = async (readContext, overrides, includeVisualizer = true) => {
2488
2650
  const statusNowUnixMs = now();
@@ -2504,7 +2666,6 @@ async function performMiningCycle(options) {
2504
2666
  visualizerState: includeVisualizer ? options.loopState.ui : undefined,
2505
2667
  });
2506
2668
  };
2507
- checkpointMiningSuspendDetector(options.suspendDetector);
2508
2669
  await saveCycleStatus(readContext, {
2509
2670
  runMode: options.runMode,
2510
2671
  backgroundWorkerPid: options.backgroundWorkerPid,
@@ -2512,9 +2673,11 @@ async function performMiningCycle(options) {
2512
2673
  backgroundWorkerHeartbeatAtUnixMs: options.runMode === "background" ? now() : null,
2513
2674
  }, false);
2514
2675
  if (readContext.localState.availability !== "ready" || readContext.localState.state === null) {
2676
+ clearMiningProviderWait(options.loopState);
2515
2677
  await saveCycleStatus(readContext, {
2516
2678
  runMode: options.runMode,
2517
2679
  currentPhase: "waiting",
2680
+ lastError: null,
2518
2681
  note: "Wallet state must be locally available for mining to continue.",
2519
2682
  });
2520
2683
  return;
@@ -2525,7 +2688,7 @@ async function performMiningCycle(options) {
2525
2688
  startHeight: 0,
2526
2689
  walletRootId: readContext.localState.state.walletRootId,
2527
2690
  });
2528
- checkpointMiningSuspendDetector(options.suspendDetector);
2691
+ throwIfMiningSuspendDetected(options.suspendDetector);
2529
2692
  const rpc = options.rpcFactory(service.rpc);
2530
2693
  const reconciliation = await reconcileLiveMiningState({
2531
2694
  state: readContext.localState.state,
@@ -2535,7 +2698,7 @@ async function performMiningCycle(options) {
2535
2698
  snapshotState: readContext.snapshot?.state ?? null,
2536
2699
  });
2537
2700
  const reconciledState = reconciliation.state;
2538
- checkpointMiningSuspendDetector(options.suspendDetector);
2701
+ throwIfMiningSuspendDetected(options.suspendDetector);
2539
2702
  let effectiveReadContext = readContext;
2540
2703
  if (JSON.stringify(reconciledState.miningState) !== JSON.stringify(readContext.localState.state.miningState)) {
2541
2704
  await saveWalletStatePreservingUnlock({
@@ -2576,18 +2739,24 @@ async function performMiningCycle(options) {
2576
2739
  });
2577
2740
  if (tipChanged) {
2578
2741
  setMiningTipSettleWindow(options.loopState, now());
2742
+ if (options.loopState.providerWaitNextRetryAtUnixMs === null) {
2743
+ clearMiningProviderWait(options.loopState);
2744
+ }
2579
2745
  }
2580
2746
  const displaySats = await resolveFundingDisplaySats(effectiveReadContext.localState.state, rpc).catch(() => null);
2581
2747
  syncMiningVisualizerBalances(options.loopState, effectiveReadContext, displaySats);
2582
2748
  if (effectiveReadContext.localState.state.miningState.state === "repair-required") {
2749
+ clearMiningProviderWait(options.loopState);
2583
2750
  await saveCycleStatus(effectiveReadContext, {
2584
2751
  runMode: options.runMode,
2585
2752
  currentPhase: "waiting",
2753
+ lastError: null,
2586
2754
  note: "Mining is blocked until the current mining publish is repaired or reconciled.",
2587
2755
  });
2588
2756
  return;
2589
2757
  }
2590
2758
  if (hasBlockingMutation(effectiveReadContext.localState.state)) {
2759
+ clearMiningProviderWait(options.loopState);
2591
2760
  const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
2592
2761
  state: "paused",
2593
2762
  pauseReason: "wallet-busy",
@@ -2608,12 +2777,14 @@ async function performMiningCycle(options) {
2608
2777
  await saveCycleStatus(effectiveReadContext, {
2609
2778
  runMode: options.runMode,
2610
2779
  currentPhase: "waiting",
2780
+ lastError: null,
2611
2781
  note: "Mining is paused while another wallet mutation is active.",
2612
2782
  });
2613
2783
  return;
2614
2784
  }
2615
2785
  const preemptionRequest = await readMiningPreemptionRequest(options.paths);
2616
2786
  if (preemptionRequest !== null) {
2787
+ clearMiningProviderWait(options.loopState);
2617
2788
  const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
2618
2789
  state: effectiveReadContext.localState.state.miningState.livePublishInMempool
2619
2790
  && effectiveReadContext.localState.state.miningState.state === "paused-stale"
@@ -2635,6 +2806,7 @@ async function performMiningCycle(options) {
2635
2806
  }, {
2636
2807
  runMode: options.runMode,
2637
2808
  currentPhase: "waiting",
2809
+ lastError: null,
2638
2810
  note: "Mining is paused while another wallet command is preempting sentence generation.",
2639
2811
  });
2640
2812
  return;
@@ -2644,7 +2816,7 @@ async function performMiningCycle(options) {
2644
2816
  rpc.getNetworkInfo(),
2645
2817
  rpc.getMempoolInfo(),
2646
2818
  ]);
2647
- checkpointMiningSuspendDetector(options.suspendDetector);
2819
+ throwIfMiningSuspendDetected(options.suspendDetector);
2648
2820
  const corePublishState = determineCorePublishState({
2649
2821
  blockchain: blockchainInfo,
2650
2822
  network: networkInfo,
@@ -2652,6 +2824,7 @@ async function performMiningCycle(options) {
2652
2824
  });
2653
2825
  clearRecoveredBitcoindError = resetMiningBitcoindRecoveryState(options.loopState, effectiveReadContext.nodeStatus?.serviceStatus ?? { pid: service.pid });
2654
2826
  if (corePublishState !== "healthy") {
2827
+ clearMiningProviderWait(options.loopState);
2655
2828
  await saveCycleStatus(effectiveReadContext, {
2656
2829
  runMode: options.runMode,
2657
2830
  currentPhase: "waiting-bitcoin-network",
@@ -2661,6 +2834,7 @@ async function performMiningCycle(options) {
2661
2834
  return;
2662
2835
  }
2663
2836
  if (effectiveReadContext.indexer.health !== "synced" || effectiveReadContext.nodeHealth !== "synced") {
2837
+ clearMiningProviderWait(options.loopState);
2664
2838
  await saveCycleStatus(effectiveReadContext, {
2665
2839
  runMode: options.runMode,
2666
2840
  currentPhase: effectiveReadContext.indexer.health !== "synced"
@@ -2673,6 +2847,7 @@ async function performMiningCycle(options) {
2673
2847
  return;
2674
2848
  }
2675
2849
  if (targetBlockHeight === null) {
2850
+ clearMiningProviderWait(options.loopState);
2676
2851
  await saveCycleStatus(effectiveReadContext, {
2677
2852
  runMode: options.runMode,
2678
2853
  currentPhase: "waiting-bitcoin-network",
@@ -2681,6 +2856,7 @@ async function performMiningCycle(options) {
2681
2856
  return;
2682
2857
  }
2683
2858
  if (getBlockRewardCogtoshi(targetBlockHeight) === 0n) {
2859
+ clearMiningProviderWait(options.loopState);
2684
2860
  const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
2685
2861
  state: "paused",
2686
2862
  pauseReason: "zero-reward",
@@ -2700,6 +2876,7 @@ async function performMiningCycle(options) {
2700
2876
  runMode: options.runMode,
2701
2877
  currentPhase: "idle",
2702
2878
  currentPublishDecision: "publish-skipped-zero-reward",
2879
+ lastError: null,
2703
2880
  note: "Mining is disabled because the target block reward is zero.",
2704
2881
  });
2705
2882
  await appendEvent(options.paths, createEvent("publish-skipped-zero-reward", "Skipped mining because the target block reward is zero.", {
@@ -2709,10 +2886,38 @@ async function performMiningCycle(options) {
2709
2886
  }));
2710
2887
  return;
2711
2888
  }
2889
+ if (options.loopState.providerWaitState !== null
2890
+ && options.loopState.providerWaitLastError !== null) {
2891
+ if (options.loopState.providerWaitNextRetryAtUnixMs !== null
2892
+ && now() < options.loopState.providerWaitNextRetryAtUnixMs) {
2893
+ await saveCycleStatus(effectiveReadContext, {
2894
+ runMode: options.runMode,
2895
+ currentPhase: "waiting-provider",
2896
+ providerState: options.loopState.providerWaitState,
2897
+ lastError: options.loopState.providerWaitLastError,
2898
+ note: "Mining is waiting for the sentence provider to recover.",
2899
+ });
2900
+ return;
2901
+ }
2902
+ if (options.loopState.providerWaitNextRetryAtUnixMs === null
2903
+ && tipKey !== null
2904
+ && options.loopState.attemptedTipKey === tipKey) {
2905
+ await saveCycleStatus(effectiveReadContext, {
2906
+ runMode: options.runMode,
2907
+ currentPhase: "waiting-provider",
2908
+ providerState: options.loopState.providerWaitState,
2909
+ lastError: options.loopState.providerWaitLastError,
2910
+ note: "Mining is waiting for the sentence provider to recover.",
2911
+ });
2912
+ return;
2913
+ }
2914
+ clearMiningProviderWait(options.loopState, options.loopState.providerWaitNextRetryAtUnixMs === null);
2915
+ }
2712
2916
  if (tipKey !== null && options.loopState.attemptedTipKey === tipKey) {
2713
2917
  await saveCycleStatus(effectiveReadContext, {
2714
2918
  runMode: options.runMode,
2715
2919
  currentPhase: "waiting",
2920
+ lastError: null,
2716
2921
  note: options.loopState.waitingNote ?? "Waiting for the next block after the last mining attempt on this tip.",
2717
2922
  });
2718
2923
  return;
@@ -2751,9 +2956,11 @@ async function performMiningCycle(options) {
2751
2956
  if (selectedCandidate === null) {
2752
2957
  const domains = resolveEligibleAnchoredRoots(effectiveReadContext);
2753
2958
  if (domains.length === 0) {
2959
+ clearMiningProviderWait(options.loopState);
2754
2960
  await saveCycleStatus(effectiveReadContext, {
2755
2961
  runMode: options.runMode,
2756
2962
  currentPhase: "idle",
2963
+ lastError: null,
2757
2964
  note: "No locally controlled anchored root domains are currently eligible to mine.",
2758
2965
  });
2759
2966
  return;
@@ -2761,6 +2968,7 @@ async function performMiningCycle(options) {
2761
2968
  await saveCycleStatus(effectiveReadContext, {
2762
2969
  runMode: options.runMode,
2763
2970
  currentPhase: "generating",
2971
+ lastError: null,
2764
2972
  note: "Generating mining sentences for eligible root domains.",
2765
2973
  });
2766
2974
  await appendEvent(options.paths, createEvent("sentence-generation-start", "Started mining sentence generation.", {
@@ -2770,7 +2978,7 @@ async function performMiningCycle(options) {
2770
2978
  }));
2771
2979
  let candidates;
2772
2980
  try {
2773
- candidates = await generateCandidatesForDomains({
2981
+ candidates = await generateCandidatesForDomainsImpl({
2774
2982
  rpc,
2775
2983
  readContext: effectiveReadContext,
2776
2984
  domains,
@@ -2780,18 +2988,30 @@ async function performMiningCycle(options) {
2780
2988
  runId: options.backgroundWorkerRunId,
2781
2989
  fetchImpl: options.fetchImpl,
2782
2990
  });
2783
- checkpointMiningSuspendDetector(options.suspendDetector);
2991
+ throwIfMiningSuspendDetected(options.suspendDetector);
2784
2992
  }
2785
2993
  catch (error) {
2786
2994
  if (error instanceof MiningProviderRequestError) {
2787
- if (tipKey !== null) {
2995
+ if (isTransientMiningProviderError(error)) {
2996
+ recordTransientMiningProviderWait({
2997
+ loopState: options.loopState,
2998
+ error,
2999
+ nowUnixMs: now(),
3000
+ });
3001
+ }
3002
+ else {
3003
+ recordTerminalMiningProviderWait({
3004
+ loopState: options.loopState,
3005
+ error,
3006
+ });
3007
+ }
3008
+ if (!isTransientMiningProviderError(error) && tipKey !== null) {
2788
3009
  options.loopState.attemptedTipKey = tipKey;
2789
- options.loopState.waitingNote = "Mining is waiting for the sentence provider to recover.";
2790
3010
  }
2791
3011
  await saveCycleStatus(effectiveReadContext, {
2792
3012
  runMode: options.runMode,
2793
3013
  currentPhase: "waiting-provider",
2794
- providerState: error.providerState,
3014
+ providerState: options.loopState.providerWaitState ?? error.providerState,
2795
3015
  lastError: error.message,
2796
3016
  note: "Mining is waiting for the sentence provider to recover.",
2797
3017
  });
@@ -2813,6 +3033,7 @@ async function performMiningCycle(options) {
2813
3033
  return;
2814
3034
  }
2815
3035
  if (error instanceof Error && error.message === "mining_generation_stale_indexer_truth") {
3036
+ clearMiningProviderWait(options.loopState);
2816
3037
  clearMiningGateCache(walletRootId);
2817
3038
  await appendEvent(options.paths, createEvent("generation-restarted-indexer-truth", "Detected updated coherent indexer truth during mining; restarting on the next tick.", {
2818
3039
  level: "warn",
@@ -2823,6 +3044,7 @@ async function performMiningCycle(options) {
2823
3044
  return;
2824
3045
  }
2825
3046
  if (error instanceof Error && error.message === "mining_generation_preempted") {
3047
+ clearMiningProviderWait(options.loopState);
2826
3048
  await appendEvent(options.paths, createEvent("generation-paused-preempted", "Stopped sentence generation because another wallet command requested mining preemption.", {
2827
3049
  level: "warn",
2828
3050
  targetBlockHeight,
@@ -2831,6 +3053,7 @@ async function performMiningCycle(options) {
2831
3053
  }));
2832
3054
  return;
2833
3055
  }
3056
+ clearMiningProviderWait(options.loopState);
2834
3057
  const failureMessage = error instanceof Error ? error.message : String(error);
2835
3058
  if (tipKey !== null) {
2836
3059
  options.loopState.attemptedTipKey = tipKey;
@@ -2851,9 +3074,11 @@ async function performMiningCycle(options) {
2851
3074
  }));
2852
3075
  return;
2853
3076
  }
3077
+ clearMiningProviderWait(options.loopState);
2854
3078
  await saveCycleStatus(effectiveReadContext, {
2855
3079
  runMode: options.runMode,
2856
3080
  currentPhase: "scoring",
3081
+ lastError: null,
2857
3082
  note: "Scoring mining candidates for the current tip.",
2858
3083
  });
2859
3084
  const best = await chooseBestLocalCandidate(candidates);
@@ -2880,7 +3105,7 @@ async function performMiningCycle(options) {
2880
3105
  return;
2881
3106
  }
2882
3107
  options.loopState.ui.recentWin = null;
2883
- cacheSelectedCandidateForTip(options.loopState, tipKey, best);
3108
+ cacheSelectedCandidateForTip(options.loopState, tipKey, best, effectiveReadContext.localState.state.miningState);
2884
3109
  selectedCandidate = best;
2885
3110
  await appendEvent(options.paths, createEvent("candidate-selected", `Selected ${best.domainName} with score ${best.canonicalBlend.toString()}.`, {
2886
3111
  targetBlockHeight: best.targetBlockHeight,
@@ -2890,13 +3115,16 @@ async function performMiningCycle(options) {
2890
3115
  score: best.canonicalBlend.toString(),
2891
3116
  runId: options.backgroundWorkerRunId,
2892
3117
  }));
2893
- const gate = await runCompetitivenessGate({
3118
+ const gate = await runCompetitivenessGateImpl({
2894
3119
  rpc,
2895
3120
  readContext: effectiveReadContext,
2896
3121
  candidate: best,
2897
3122
  currentTxid: effectiveReadContext.localState.state.miningState.currentTxid,
3123
+ assaySentencesImpl: options.assaySentencesImpl,
3124
+ cooperativeYield: options.cooperativeYieldImpl,
3125
+ cooperativeYieldEvery: options.cooperativeYieldEvery,
2898
3126
  });
2899
- checkpointMiningSuspendDetector(options.suspendDetector);
3127
+ throwIfMiningSuspendDetected(options.suspendDetector);
2900
3128
  gateSnapshot = {
2901
3129
  higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
2902
3130
  dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
@@ -2908,7 +3136,7 @@ async function performMiningCycle(options) {
2908
3136
  options.loopState.attemptedTipKey = tipKey;
2909
3137
  }
2910
3138
  clearSelectedCandidate(options.loopState);
2911
- setMiningUiCandidate(options.loopState, best);
3139
+ setMiningUiCandidate(options.loopState, best, effectiveReadContext.localState.state.miningState);
2912
3140
  options.loopState.waitingNote = gate.decision === "suppressed-same-domain-mempool"
2913
3141
  ? "Best local sentence found, but a same-domain mempool competitor already matches or beats it."
2914
3142
  : gate.decision === "suppressed-top5-mempool"
@@ -2949,7 +3177,7 @@ async function performMiningCycle(options) {
2949
3177
  }
2950
3178
  else {
2951
3179
  options.loopState.ui.recentWin = null;
2952
- setMiningUiCandidate(options.loopState, selectedCandidate);
3180
+ setMiningUiCandidate(options.loopState, selectedCandidate, effectiveReadContext.localState.state.miningState);
2953
3181
  }
2954
3182
  if (!await ensureCurrentIndexerTruthOrRestart()) {
2955
3183
  return;
@@ -2965,12 +3193,11 @@ async function performMiningCycle(options) {
2965
3193
  purpose: "wallet-mine",
2966
3194
  walletRootId: effectiveReadContext.localState.state.walletRootId,
2967
3195
  });
2968
- checkpointMiningSuspendDetector(options.suspendDetector);
2969
3196
  try {
2970
3197
  if (!await ensureCurrentIndexerTruthOrRestart()) {
2971
3198
  return;
2972
3199
  }
2973
- checkpointMiningSuspendDetector(options.suspendDetector);
3200
+ throwIfMiningSuspendDetected(options.suspendDetector);
2974
3201
  const published = await publishCandidate({
2975
3202
  dataDir: options.dataDir,
2976
3203
  databasePath: options.databasePath,
@@ -2983,12 +3210,11 @@ async function performMiningCycle(options) {
2983
3210
  candidate: selectedCandidate,
2984
3211
  runId: options.backgroundWorkerRunId,
2985
3212
  });
2986
- checkpointMiningSuspendDetector(options.suspendDetector);
2987
3213
  if (tipKey !== null && published.retryable !== true) {
2988
3214
  options.loopState.attemptedTipKey = tipKey;
2989
3215
  }
2990
3216
  if (published.retryable === true) {
2991
- cacheSelectedCandidateForTip(options.loopState, tipKey, published.candidate);
3217
+ cacheSelectedCandidateForTip(options.loopState, tipKey, published.candidate, published.state.miningState);
2992
3218
  options.loopState.waitingNote = published.note;
2993
3219
  await saveCycleStatus({
2994
3220
  ...effectiveReadContext,
@@ -3014,7 +3240,7 @@ async function performMiningCycle(options) {
3014
3240
  }
3015
3241
  if (published.skipped === true) {
3016
3242
  clearSelectedCandidate(options.loopState);
3017
- setMiningUiCandidate(options.loopState, selectedCandidate);
3243
+ setMiningUiCandidate(options.loopState, selectedCandidate, published.state.miningState);
3018
3244
  options.loopState.waitingNote = published.note;
3019
3245
  const lastError = published.decision === "publish-paused-insufficient-funds"
3020
3246
  ? published.lastError ?? createInsufficientFundsMiningPublishErrorMessage()
@@ -3046,7 +3272,7 @@ async function performMiningCycle(options) {
3046
3272
  if (published.txid !== null) {
3047
3273
  options.loopState.ui.latestTxid = published.txid;
3048
3274
  }
3049
- setMiningUiCandidate(options.loopState, published.candidate);
3275
+ setMiningUiCandidate(options.loopState, published.candidate, published.state.miningState);
3050
3276
  options.loopState.waitingNote = published.decision === "kept-live-publish"
3051
3277
  ? "Existing live mining publish already covers this block attempt. Waiting for the next block."
3052
3278
  : published.txid === null
@@ -3214,59 +3440,71 @@ async function attemptSaveMempool(rpc, paths, runId) {
3214
3440
  }
3215
3441
  }
3216
3442
  async function runMiningLoop(options) {
3217
- const suspendDetector = createMiningSuspendDetector();
3218
- const loopState = createMiningLoopState();
3443
+ const suspendDetector = createMiningSuspendDetector({
3444
+ monotonicNow: options.suspendMonotonicNowImpl,
3445
+ nowUnixMs: options.nowImpl ?? Date.now,
3446
+ scheduler: options.suspendScheduler,
3447
+ });
3448
+ const loopState = options.loopState ?? createMiningLoopState();
3219
3449
  const probeService = options.probeService ?? probeManagedBitcoindService;
3220
3450
  const stopService = options.stopService ?? stopManagedBitcoindService;
3221
3451
  const sleepImpl = options.sleepImpl ?? sleep;
3222
- await appendEvent(options.paths, createEvent("runtime-start", `Started ${options.runMode} mining runtime.`, {
3223
- runId: options.backgroundWorkerRunId,
3224
- }));
3225
- while (!options.signal?.aborted) {
3226
- try {
3227
- checkpointMiningSuspendDetector(suspendDetector);
3228
- }
3229
- catch (error) {
3230
- if (!(error instanceof MiningSuspendDetectedError)) {
3231
- throw error;
3452
+ try {
3453
+ await appendEvent(options.paths, createEvent("runtime-start", `Started ${options.runMode} mining runtime.`, {
3454
+ runId: options.backgroundWorkerRunId,
3455
+ }));
3456
+ while (!options.signal?.aborted) {
3457
+ try {
3458
+ throwIfMiningSuspendDetected(suspendDetector);
3232
3459
  }
3233
- discardMiningLoopTransientWork(loopState, null);
3234
- await handleDetectedMiningRuntimeResume({
3235
- dataDir: options.dataDir,
3236
- databasePath: options.databasePath,
3237
- provider: options.provider,
3238
- paths: options.paths,
3239
- runMode: options.runMode,
3240
- backgroundWorkerPid: options.backgroundWorkerPid,
3241
- backgroundWorkerRunId: options.backgroundWorkerRunId,
3242
- detectedAtUnixMs: error.detectedAtUnixMs,
3243
- openReadContext: options.openReadContext,
3244
- visualizer: options.visualizer,
3460
+ catch (error) {
3461
+ if (!(error instanceof MiningSuspendDetectedError)) {
3462
+ throw error;
3463
+ }
3464
+ discardMiningLoopTransientWork(loopState, null);
3465
+ await handleDetectedMiningRuntimeResume({
3466
+ dataDir: options.dataDir,
3467
+ databasePath: options.databasePath,
3468
+ provider: options.provider,
3469
+ paths: options.paths,
3470
+ runMode: options.runMode,
3471
+ backgroundWorkerPid: options.backgroundWorkerPid,
3472
+ backgroundWorkerRunId: options.backgroundWorkerRunId,
3473
+ detectedAtUnixMs: error.detectedAtUnixMs,
3474
+ openReadContext: options.openReadContext,
3475
+ visualizer: options.visualizer,
3476
+ loopState,
3477
+ });
3478
+ continue;
3479
+ }
3480
+ await performMiningCycle({
3481
+ ...options,
3482
+ suspendDetector,
3483
+ assaySentencesImpl: options.assaySentencesImpl,
3484
+ cooperativeYieldImpl: options.cooperativeYieldImpl,
3485
+ cooperativeYieldEvery: options.cooperativeYieldEvery,
3245
3486
  loopState,
3487
+ probeService,
3488
+ stopService,
3246
3489
  });
3247
- continue;
3490
+ await sleepImpl(Math.min(MINING_LOOP_INTERVAL_MS, MINING_STATUS_HEARTBEAT_INTERVAL_MS), options.signal);
3248
3491
  }
3249
- await performMiningCycle({
3250
- ...options,
3251
- suspendDetector,
3252
- loopState,
3253
- probeService,
3254
- stopService,
3255
- });
3256
- await sleepImpl(Math.min(MINING_LOOP_INTERVAL_MS, MINING_STATUS_HEARTBEAT_INTERVAL_MS), options.signal);
3492
+ const service = await options.attachService({
3493
+ dataDir: options.dataDir,
3494
+ chain: "main",
3495
+ startHeight: 0,
3496
+ walletRootId: undefined,
3497
+ }).catch(() => null);
3498
+ if (service !== null) {
3499
+ await attemptSaveMempool(options.rpcFactory(service.rpc), options.paths, options.backgroundWorkerRunId);
3500
+ }
3501
+ await appendEvent(options.paths, createEvent("runtime-stop", `Stopped ${options.runMode} mining runtime.`, {
3502
+ runId: options.backgroundWorkerRunId,
3503
+ }));
3257
3504
  }
3258
- const service = await options.attachService({
3259
- dataDir: options.dataDir,
3260
- chain: "main",
3261
- startHeight: 0,
3262
- walletRootId: undefined,
3263
- }).catch(() => null);
3264
- if (service !== null) {
3265
- await attemptSaveMempool(options.rpcFactory(service.rpc), options.paths, options.backgroundWorkerRunId);
3505
+ finally {
3506
+ stopMiningSuspendDetector(suspendDetector);
3266
3507
  }
3267
- await appendEvent(options.paths, createEvent("runtime-stop", `Stopped ${options.runMode} mining runtime.`, {
3268
- runId: options.backgroundWorkerRunId,
3269
- }));
3270
3508
  }
3271
3509
  async function waitForBackgroundHealthy(paths) {
3272
3510
  const deadline = Date.now() + BACKGROUND_START_TIMEOUT_MS;
@@ -3579,6 +3817,30 @@ export async function runMiningLoopForTesting(options) {
3579
3817
  ...options,
3580
3818
  });
3581
3819
  }
3820
+ export async function runCompetitivenessGateForTesting(options) {
3821
+ return await runCompetitivenessGate({
3822
+ rpc: options.rpc,
3823
+ readContext: options.readContext,
3824
+ candidate: options.candidate,
3825
+ currentTxid: options.currentTxid,
3826
+ assaySentencesImpl: options.assaySentencesImpl,
3827
+ cooperativeYield: options.cooperativeYieldImpl,
3828
+ cooperativeYieldEvery: options.cooperativeYieldEvery,
3829
+ });
3830
+ }
3831
+ export function createMiningSuspendDetectorForTesting(options = {}) {
3832
+ return createMiningSuspendDetector(options);
3833
+ }
3834
+ export function throwIfMiningSuspendDetectedForTesting(detector) {
3835
+ throwIfMiningSuspendDetected(detector);
3836
+ }
3837
+ export function topologicallyOrderAncestorTxidsForTesting(options) {
3838
+ const ordered = topologicallyOrderAncestorContexts({
3839
+ txid: options.txid,
3840
+ txContexts: options.txContexts,
3841
+ });
3842
+ return ordered?.map((context) => context.txid) ?? null;
3843
+ }
3582
3844
  export function buildPrePublishStatusOverridesForTesting(options) {
3583
3845
  return buildPrePublishStatusOverrides(options);
3584
3846
  }