@cogcoin/client 1.1.3 → 1.1.5

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.
Files changed (88) hide show
  1. package/README.md +4 -5
  2. package/dist/bitcoind/node.js +2 -1
  3. package/dist/bitcoind/progress/tty-renderer.js +3 -2
  4. package/dist/bitcoind/service.js +6 -24
  5. package/dist/bitcoind/types.d.ts +1 -0
  6. package/dist/bitcoind/types.js +1 -0
  7. package/dist/cli/command-registry.d.ts +39 -0
  8. package/dist/cli/command-registry.js +1132 -0
  9. package/dist/cli/commands/client-admin.js +6 -56
  10. package/dist/cli/commands/mining-admin.js +9 -32
  11. package/dist/cli/commands/mining-read.js +15 -56
  12. package/dist/cli/commands/mining-runtime.js +258 -57
  13. package/dist/cli/commands/service-runtime.js +1 -64
  14. package/dist/cli/commands/status.js +2 -15
  15. package/dist/cli/commands/update.js +6 -21
  16. package/dist/cli/commands/wallet-admin.js +18 -120
  17. package/dist/cli/commands/wallet-mutation.js +4 -7
  18. package/dist/cli/commands/wallet-read.js +31 -138
  19. package/dist/cli/context.js +2 -4
  20. package/dist/cli/mining-format.js +8 -2
  21. package/dist/cli/mutation-command-groups.d.ts +11 -11
  22. package/dist/cli/mutation-command-groups.js +9 -18
  23. package/dist/cli/mutation-json.d.ts +1 -17
  24. package/dist/cli/mutation-json.js +1 -28
  25. package/dist/cli/mutation-success.d.ts +0 -1
  26. package/dist/cli/mutation-success.js +0 -19
  27. package/dist/cli/output.d.ts +1 -10
  28. package/dist/cli/output.js +52 -481
  29. package/dist/cli/parse.d.ts +1 -1
  30. package/dist/cli/parse.js +38 -695
  31. package/dist/cli/runner.js +28 -113
  32. package/dist/cli/types.d.ts +7 -8
  33. package/dist/cli/update-notifier.js +1 -1
  34. package/dist/cli/wallet-format.js +1 -1
  35. package/dist/wallet/lifecycle/managed-core.d.ts +23 -0
  36. package/dist/wallet/lifecycle/managed-core.js +257 -0
  37. package/dist/wallet/lifecycle/repair-mining.d.ts +49 -0
  38. package/dist/wallet/lifecycle/repair-mining.js +304 -0
  39. package/dist/wallet/lifecycle/repair-runtime.d.ts +36 -0
  40. package/dist/wallet/lifecycle/repair-runtime.js +206 -0
  41. package/dist/wallet/lifecycle/repair.d.ts +11 -0
  42. package/dist/wallet/lifecycle/repair.js +368 -0
  43. package/dist/wallet/lifecycle/setup.d.ts +16 -0
  44. package/dist/wallet/lifecycle/setup.js +430 -0
  45. package/dist/wallet/lifecycle/types.d.ts +125 -0
  46. package/dist/wallet/lifecycle/types.js +1 -0
  47. package/dist/wallet/lifecycle.d.ts +4 -165
  48. package/dist/wallet/lifecycle.js +3 -1656
  49. package/dist/wallet/mining/candidate.d.ts +60 -0
  50. package/dist/wallet/mining/candidate.js +290 -0
  51. package/dist/wallet/mining/competitiveness.d.ts +22 -0
  52. package/dist/wallet/mining/competitiveness.js +640 -0
  53. package/dist/wallet/mining/control.js +7 -251
  54. package/dist/wallet/mining/cycle.d.ts +39 -0
  55. package/dist/wallet/mining/cycle.js +542 -0
  56. package/dist/wallet/mining/engine-state.d.ts +66 -0
  57. package/dist/wallet/mining/engine-state.js +211 -0
  58. package/dist/wallet/mining/engine-types.d.ts +173 -0
  59. package/dist/wallet/mining/engine-types.js +1 -0
  60. package/dist/wallet/mining/engine-utils.d.ts +7 -0
  61. package/dist/wallet/mining/engine-utils.js +75 -0
  62. package/dist/wallet/mining/events.d.ts +2 -0
  63. package/dist/wallet/mining/events.js +19 -0
  64. package/dist/wallet/mining/lifecycle.d.ts +71 -0
  65. package/dist/wallet/mining/lifecycle.js +355 -0
  66. package/dist/wallet/mining/projection.d.ts +61 -0
  67. package/dist/wallet/mining/projection.js +319 -0
  68. package/dist/wallet/mining/publish.d.ts +79 -0
  69. package/dist/wallet/mining/publish.js +614 -0
  70. package/dist/wallet/mining/runner.d.ts +12 -418
  71. package/dist/wallet/mining/runner.js +274 -3433
  72. package/dist/wallet/mining/supervisor.d.ts +134 -0
  73. package/dist/wallet/mining/supervisor.js +558 -0
  74. package/dist/wallet/mining/visualizer-sync.d.ts +42 -0
  75. package/dist/wallet/mining/visualizer-sync.js +166 -0
  76. package/dist/wallet/mining/visualizer.d.ts +1 -0
  77. package/dist/wallet/mining/visualizer.js +33 -18
  78. package/dist/wallet/read/context.d.ts +5 -1
  79. package/dist/wallet/read/context.js +19 -4
  80. package/dist/wallet/reset.d.ts +1 -1
  81. package/dist/wallet/reset.js +35 -11
  82. package/dist/wallet/runtime.d.ts +0 -6
  83. package/dist/wallet/runtime.js +2 -38
  84. package/dist/wallet/tx/common.d.ts +18 -0
  85. package/dist/wallet/tx/common.js +40 -26
  86. package/package.json +1 -1
  87. package/dist/wallet/state/seed-index.d.ts +0 -43
  88. package/dist/wallet/state/seed-index.js +0 -151
@@ -1,11 +1,7 @@
1
- import { createHash, randomBytes } from "node:crypto";
1
+ import { createHash } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
- import { rm } from "node:fs/promises";
4
- import { join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
3
  import { getBalance, getBlockWinners, lookupDomain, lookupDomainById, } from "@cogcoin/indexer/queries";
7
4
  import { assaySentences, deriveBlendSeed, displayToInternalBlockhash, getWords, settleBlock, } from "@cogcoin/scoring";
8
- import { wordlist as englishWordlist } from "@scure/bip39/wordlists/english.js";
9
5
  import { probeIndexerDaemon } from "../../bitcoind/indexer-daemon.js";
10
6
  import { isRetryableManagedRpcError } from "../../bitcoind/retryable-rpc.js";
11
7
  import { FOLLOW_VISIBLE_PRIOR_BLOCKS } from "../../bitcoind/client/follow-block-times.js";
@@ -13,2606 +9,145 @@ import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, stopM
13
9
  import { createRpcClient } from "../../bitcoind/node.js";
14
10
  import { COG_OPCODES, COG_PREFIX } from "../cogop/constants.js";
15
11
  import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
16
- import { assertFixedInputPrefixMatches, buildWalletMutationTransaction, isInsufficientFundsError, outpointKey as walletMutationOutpointKey, isAlreadyAcceptedError, isBroadcastUnknownError, reconcilePersistentPolicyLocks, resolveWalletMutationFeeSelection, saveWalletStatePreservingUnlock, } from "../tx/common.js";
17
- import { FileLockBusyError, acquireFileLock, clearOrphanedFileLock, readLockMetadata, } from "../fs/lock.js";
12
+ import { assertFixedInputPrefixMatches, buildWalletMutationTransaction, fundAndValidateWalletMutationDraft, isInsufficientFundsError, outpointKey as walletMutationOutpointKey, isAlreadyAcceptedError, isBroadcastUnknownError, reconcilePersistentPolicyLocks, resolveWalletMutationFeeSelection, saveWalletStatePreservingUnlock, } from "../tx/common.js";
18
13
  import { isMineableWalletDomain, openWalletReadContext, } from "../read/index.js";
19
14
  import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
20
15
  import { createDefaultWalletSecretProvider, } from "../state/provider.js";
21
16
  import { serializeMine } from "../cogop/index.js";
22
- import { appendMiningEvent, loadMiningRuntimeStatus, saveMiningRuntimeStatus, } from "./runtime-artifacts.js";
17
+ import { appendMiningEvent } from "./runtime-artifacts.js";
23
18
  import { loadClientConfig } from "./config.js";
24
- import { MINING_LOOP_INTERVAL_MS, MINING_NETWORK_SETTLE_WINDOW_MS, MINING_PROVIDER_BACKOFF_BASE_MS, MINING_PROVIDER_BACKOFF_MAX_MS, MINING_SHUTDOWN_GRACE_MS, MINING_STATUS_HEARTBEAT_INTERVAL_MS, MINING_SUSPEND_GAP_THRESHOLD_MS, MINING_TIP_SETTLE_WINDOW_MS, MINING_WORKER_API_VERSION, } from "./constants.js";
25
- import { inspectMiningControlPlane, setupBuiltInMining } from "./control.js";
19
+ import { MINING_LOOP_INTERVAL_MS, MINING_STATUS_HEARTBEAT_INTERVAL_MS, MINING_SUSPEND_GAP_THRESHOLD_MS, } from "./constants.js";
20
+ import { setupBuiltInMining } from "./control.js";
21
+ import { applyMiningRuntimeStatusOverrides, buildPrePublishStatusOverrides, } from "./projection.js";
22
+ import { buildMiningGenerationRequest as buildMiningGenerationRequestModule, chooseBestLocalCandidate as chooseBestLocalCandidateModule, determineCorePublishState as determineCorePublishStateModule, ensureIndexerTruthIsCurrent as ensureIndexerTruthIsCurrentModule, generateCandidatesForDomains as generateCandidatesForDomainsModule, getIndexerTruthKey as getIndexerTruthKeyModule, refreshMiningCandidateFromCurrentState as refreshMiningCandidateFromCurrentStateModule, resolveEligibleAnchoredRoots as resolveEligibleAnchoredRootsModule, } from "./candidate.js";
23
+ import { clearMiningGateCache as clearMiningGateCacheModule, runCompetitivenessGate as runCompetitivenessGateModule, } from "./competitiveness.js";
24
+ import { createMiningEventRecord } from "./events.js";
25
+ import { buildMiningSettleWindowStatusOverrides, clearMiningProviderWait, createMiningRuntimeLoopState, defaultMiningStatePatch, discardMiningLoopTransientWork, hasBlockingMutation, setMiningTipSettleWindow, } from "./engine-state.js";
26
+ import { createInsufficientFundsMiningPublishErrorMessage as createInsufficientFundsMiningPublishErrorMessageModule, createInsufficientFundsMiningPublishWaitingNote as createInsufficientFundsMiningPublishWaitingNoteModule, createMiningPlan as createMiningPlanModule, publishCandidate as publishCandidateModule, probeMiningFundingAvailability as probeMiningFundingAvailabilityModule, publishCandidateOnce as publishCandidateOnceModule, reconcileLiveMiningState as reconcileLiveMiningStateModule, resolveMiningConflictOutpoint as resolveMiningConflictOutpointModule, validateMiningDraft as validateMiningDraftModule, } from "./publish.js";
27
+ import { runMiningPhaseMachine } from "./cycle.js";
28
+ import { attemptSaveMempool, handleDetectedMiningRuntimeResume, handleRecoverableMiningBitcoindFailure, isRecoverableMiningBitcoindError, refreshAndSaveMiningRuntimeStatus, resetMiningBitcoindRecoveryState, saveStopSnapshot, } from "./lifecycle.js";
29
+ import { compareLexicographically, deriveMiningWordIndices, getBlockRewardCogtoshi, numberToSats, resolveBip39WordsFromIndices, rootDomain, tieBreakHash, } from "./engine-utils.js";
26
30
  import { isMiningGenerationAbortRequested, markMiningGenerationActive, markMiningGenerationInactive, readMiningGenerationActivity, readMiningPreemptionRequest, requestMiningGenerationPreemption, } from "./coordination.js";
27
31
  import { clearMiningPublishState, miningPublishIsInMempool, miningPublishMayStillExist, normalizeMiningPublishState, normalizeMiningStateRecord, } from "./state.js";
32
+ import { runBackgroundMiningWorker as runBackgroundMiningWorkerSupervisor, runForegroundMining as runForegroundMiningSupervisor, startBackgroundMining as startBackgroundMiningSupervisor, stopBackgroundMining as stopBackgroundMiningSupervisor, waitForBackgroundHealthy as waitForBackgroundHealthySupervisor, } from "./supervisor.js";
28
33
  import { createMiningSentenceRequestLimits } from "./sentence-protocol.js";
29
34
  import { generateMiningSentences, MiningProviderRequestError } from "./sentences.js";
30
- import { createEmptyMiningFollowVisualizerState, MiningFollowVisualizer, } from "./visualizer.js";
35
+ import { MiningFollowVisualizer, } from "./visualizer.js";
36
+ import { createIndexedMiningFollowVisualizerState, findRecentMiningWin, loadMiningVisibleFollowBlockTimes, resolveFundingDisplaySats, resolveSettledBoard, syncMiningUiForCurrentTip, syncMiningVisualizerBalances, syncMiningVisualizerBlockTimes, } from "./visualizer-sync.js";
31
37
  const BEST_BLOCK_POLL_INTERVAL_MS = 500;
32
- const BACKGROUND_START_TIMEOUT_MS = 15_000;
33
- const MINING_BITCOIN_RECOVERY_GRACE_MS = 15_000;
34
- const MINING_BITCOIN_RECOVERY_RESTART_COOLDOWN_MS = 60_000;
35
38
  const MINING_SUSPEND_HEARTBEAT_INTERVAL_MS = 1_000;
36
- const MINING_MEMPOOL_COOPERATIVE_YIELD_EVERY = 25;
37
- const MINING_BITCOIN_RECOVERY_NOTE = "Mining lost contact with the local Bitcoin RPC service and is waiting for it to recover.";
38
- function resolveBip39WordsFromIndices(indices) {
39
- if (indices === null || indices === undefined) {
40
- return [];
41
- }
42
- const words = [];
43
- for (const index of indices) {
44
- if (!Number.isInteger(index) || index < 0 || index >= englishWordlist.length) {
45
- continue;
46
- }
47
- words.push(englishWordlist[index]);
48
- }
49
- return words;
50
- }
51
- function resolveSettledWinnerRequiredWords(options) {
52
- const storedWords = resolveBip39WordsFromIndices(options.bip39WordIndices);
53
- if (storedWords.length > 0) {
54
- return storedWords;
55
- }
56
- if (options.snapshotTipPreviousHashHex === null
57
- || options.snapshotTipPreviousHashHex === undefined
58
- || !Number.isInteger(options.domainId)
59
- || options.domainId <= 0) {
60
- return [];
61
- }
62
- return resolveBip39WordsFromIndices(deriveMiningWordIndices(Buffer.from(displayToInternalBlockhash(options.snapshotTipPreviousHashHex), "hex"), options.domainId));
63
- }
64
- function resolveSnapshotOverride(override, fallback) {
65
- return override === undefined ? fallback : override;
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
- }
99
39
  class MiningSuspendDetectedError extends Error {
100
- detectedAtUnixMs;
101
- constructor(detectedAtUnixMs) {
102
- super("mining_runtime_resumed");
103
- this.detectedAtUnixMs = detectedAtUnixMs;
104
- }
105
- }
106
- class MiningPublishRejectedError extends Error {
107
- revertedState;
108
- constructor(message, revertedState) {
109
- super(message);
110
- this.name = "MiningPublishRejectedError";
111
- this.revertedState = revertedState;
112
- }
113
- }
114
- const miningGateCache = new Map();
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
- },
152
- };
153
- heartbeat = scheduler.every(MINING_SUSPEND_HEARTBEAT_INTERVAL_MS, () => {
154
- refreshMiningSuspendDetector(detector);
155
- });
156
- return detector;
157
- }
158
- function throwIfMiningSuspendDetected(detector) {
159
- if (detector === undefined) {
160
- return;
161
- }
162
- refreshMiningSuspendDetector(detector);
163
- if (detector.detectedAtUnixMs === null) {
164
- return;
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)();
184
- }
185
- function clearMiningGateCache(walletRootId) {
186
- if (walletRootId === null || walletRootId === undefined) {
187
- miningGateCache.clear();
188
- return;
189
- }
190
- miningGateCache.delete(walletRootId);
191
- }
192
- function sleep(ms, signal) {
193
- return new Promise((resolve) => {
194
- const timer = setTimeout(resolve, ms);
195
- signal?.addEventListener("abort", () => {
196
- clearTimeout(timer);
197
- resolve();
198
- }, { once: true });
199
- });
200
- }
201
- async function isProcessAlive(pid) {
202
- if (pid === null) {
203
- return false;
204
- }
205
- try {
206
- process.kill(pid, 0);
207
- return true;
208
- }
209
- catch (error) {
210
- if (error instanceof Error && "code" in error && error.code === "ESRCH") {
211
- return false;
212
- }
213
- return true;
214
- }
215
- }
216
- function normalizeMiningPid(value) {
217
- return typeof value === "number" && Number.isInteger(value) && value > 0
218
- ? value
219
- : null;
220
- }
221
- function resolveMiningGenerationRequestPath(paths) {
222
- return join(paths.miningRoot, "generation-request.json");
223
- }
224
- function resolveMiningGenerationActivityPath(paths) {
225
- return join(paths.miningRoot, "generation-activity.json");
226
- }
227
- function createTakeoverStoppedMiningNote(livePublishInMempool) {
228
- return livePublishInMempool
229
- ? "Mining runtime replaced. The last mining transaction may still confirm from mempool."
230
- : "Mining runtime replaced.";
231
- }
232
- function createStoppedMiningRuntimeSnapshotForTakeover(options) {
233
- const note = createTakeoverStoppedMiningNote(options.snapshot?.livePublishInMempool);
234
- if (options.snapshot !== null) {
235
- return {
236
- ...options.snapshot,
237
- updatedAtUnixMs: options.nowUnixMs,
238
- runMode: "stopped",
239
- backgroundWorkerPid: null,
240
- backgroundWorkerRunId: null,
241
- backgroundWorkerHeartbeatAtUnixMs: null,
242
- backgroundWorkerHealth: null,
243
- currentPhase: "idle",
244
- note,
245
- };
246
- }
247
- return {
248
- schemaVersion: 1,
249
- walletRootId: options.walletRootId,
250
- workerApiVersion: null,
251
- workerBinaryVersion: null,
252
- workerBuildId: null,
253
- updatedAtUnixMs: options.nowUnixMs,
254
- runMode: "stopped",
255
- backgroundWorkerPid: null,
256
- backgroundWorkerRunId: null,
257
- backgroundWorkerHeartbeatAtUnixMs: null,
258
- backgroundWorkerHealth: null,
259
- indexerDaemonState: null,
260
- indexerDaemonInstanceId: null,
261
- indexerSnapshotSeq: null,
262
- indexerSnapshotOpenedAtUnixMs: null,
263
- indexerTruthSource: undefined,
264
- indexerHeartbeatAtUnixMs: null,
265
- coreBestHeight: null,
266
- coreBestHash: null,
267
- indexerTipHeight: null,
268
- indexerTipHash: null,
269
- indexerReorgDepth: null,
270
- indexerTipAligned: null,
271
- corePublishState: null,
272
- providerState: null,
273
- lastSuspendDetectedAtUnixMs: null,
274
- reconnectSettledUntilUnixMs: null,
275
- tipSettledUntilUnixMs: null,
276
- miningState: "idle",
277
- currentPhase: "idle",
278
- currentPublishState: "none",
279
- targetBlockHeight: null,
280
- referencedBlockHashDisplay: null,
281
- currentDomainId: null,
282
- currentDomainName: null,
283
- currentSentenceDisplay: null,
284
- currentCanonicalBlend: null,
285
- currentTxid: null,
286
- currentWtxid: null,
287
- livePublishInMempool: null,
288
- currentFeeRateSatVb: null,
289
- currentAbsoluteFeeSats: null,
290
- currentBlockFeeSpentSats: "0",
291
- sessionFeeSpentSats: "0",
292
- lifetimeFeeSpentSats: "0",
293
- sameDomainCompetitorSuppressed: null,
294
- higherRankedCompetitorDomainCount: null,
295
- dedupedCompetitorDomainCount: null,
296
- competitivenessGateIndeterminate: null,
297
- mempoolSequenceCacheStatus: null,
298
- currentPublishDecision: null,
299
- lastMempoolSequence: null,
300
- lastCompetitivenessGateAtUnixMs: null,
301
- pauseReason: null,
302
- providerConfigured: false,
303
- providerKind: null,
304
- bitcoindHealth: "unavailable",
305
- bitcoindServiceState: null,
306
- bitcoindReplicaStatus: null,
307
- nodeHealth: "unavailable",
308
- indexerHealth: "unavailable",
309
- tipsAligned: null,
310
- lastEventAtUnixMs: null,
311
- lastError: null,
312
- note,
313
- };
314
- }
315
- async function waitForMiningProcessExit(pid, timeoutMs, sleepImpl = sleep) {
316
- const deadline = Date.now() + timeoutMs;
317
- while (Date.now() < deadline) {
318
- if (!await isProcessAlive(pid)) {
319
- return true;
320
- }
321
- await sleepImpl(Math.min(250, Math.max(timeoutMs, 1)));
322
- }
323
- return !await isProcessAlive(pid);
324
- }
325
- async function terminateMiningRuntimePid(options) {
326
- if (!await isProcessAlive(options.pid)) {
327
- return false;
328
- }
329
- try {
330
- process.kill(options.pid, "SIGTERM");
331
- }
332
- catch (error) {
333
- if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
334
- throw error;
335
- }
336
- }
337
- if (await waitForMiningProcessExit(options.pid, options.shutdownGraceMs, options.sleepImpl)) {
338
- return true;
339
- }
340
- try {
341
- process.kill(options.pid, "SIGKILL");
342
- }
343
- catch (error) {
344
- if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
345
- throw error;
346
- }
347
- }
348
- if (await waitForMiningProcessExit(options.pid, options.shutdownGraceMs, options.sleepImpl)) {
349
- return true;
350
- }
351
- throw new Error("mining_process_stop_timeout");
352
- }
353
- async function takeOverMiningRuntime(options) {
354
- const snapshot = await loadMiningRuntimeStatus(options.paths.miningStatusPath).catch(() => null);
355
- const controlLockMetadata = options.controlLockMetadata ?? (options.clearControlLockFile === true
356
- ? await readLockMetadata(options.paths.miningControlLockPath).catch(() => null)
357
- : null);
358
- const generationActivity = await readMiningGenerationActivity(options.paths).catch(() => null);
359
- const shutdownGraceMs = options.shutdownGraceMs ?? MINING_SHUTDOWN_GRACE_MS;
360
- const requestPreemption = options.requestMiningPreemption ?? requestMiningGenerationPreemption;
361
- const controlLockPid = normalizeMiningPid(controlLockMetadata?.processId);
362
- const backgroundWorkerPid = normalizeMiningPid(snapshot?.backgroundWorkerPid);
363
- const generationOwnerPid = normalizeMiningPid(generationActivity?.generationOwnerPid);
364
- const terminatedPids = [];
365
- const discoveredPids = new Set();
366
- for (const pid of [controlLockPid, backgroundWorkerPid, generationOwnerPid]) {
367
- if (pid === null
368
- || pid === process.pid
369
- || discoveredPids.has(pid)
370
- || !await isProcessAlive(pid)) {
371
- continue;
372
- }
373
- discoveredPids.add(pid);
374
- }
375
- const shouldPreemptGeneration = discoveredPids.size > 0 && (generationActivity?.generationActive === true
376
- || snapshot?.currentPhase === "generating"
377
- || snapshot?.currentPhase === "scoring");
378
- const preemption = shouldPreemptGeneration
379
- ? await requestPreemption({
380
- paths: options.paths,
381
- reason: options.reason,
382
- timeoutMs: Math.min(shutdownGraceMs, 15_000),
383
- }).catch(() => null)
384
- : null;
385
- try {
386
- for (const pid of discoveredPids) {
387
- if (await terminateMiningRuntimePid({
388
- pid,
389
- shutdownGraceMs,
390
- sleepImpl: options.sleepImpl,
391
- })) {
392
- terminatedPids.push(pid);
393
- }
394
- }
395
- }
396
- finally {
397
- await preemption?.release().catch(() => undefined);
398
- }
399
- const controlLockCleared = options.clearControlLockFile === true
400
- ? await clearOrphanedFileLock(options.paths.miningControlLockPath, isProcessAlive).catch(() => false)
401
- : false;
402
- await rm(resolveMiningGenerationRequestPath(options.paths), { force: true }).catch(() => undefined);
403
- await rm(resolveMiningGenerationActivityPath(options.paths), { force: true }).catch(() => undefined);
404
- const walletRootId = snapshot?.walletRootId
405
- ?? (typeof controlLockMetadata?.walletRootId === "string" ? controlLockMetadata.walletRootId : null);
406
- if (snapshot !== null || walletRootId !== null || terminatedPids.length > 0 || controlLockCleared) {
407
- await saveMiningRuntimeStatus(options.paths.miningStatusPath, createStoppedMiningRuntimeSnapshotForTakeover({
408
- snapshot,
409
- walletRootId,
410
- nowUnixMs: Date.now(),
411
- }));
412
- }
413
- return {
414
- controlLockCleared,
415
- replaced: terminatedPids.length > 0,
416
- snapshot,
417
- terminatedPids,
418
- };
419
- }
420
- async function acquireMiningStartControlLock(options) {
421
- while (true) {
422
- try {
423
- return await acquireFileLock(options.paths.miningControlLockPath, {
424
- purpose: options.purpose,
425
- });
426
- }
427
- catch (error) {
428
- if (!(error instanceof FileLockBusyError)) {
429
- throw error;
430
- }
431
- if (error.existingMetadata?.processId === process.pid) {
432
- throw error;
433
- }
434
- const takeover = await takeOverMiningRuntime({
435
- paths: options.paths,
436
- reason: options.takeoverReason,
437
- clearControlLockFile: true,
438
- controlLockMetadata: error.existingMetadata,
439
- requestMiningPreemption: options.requestMiningPreemption,
440
- shutdownGraceMs: options.shutdownGraceMs,
441
- sleepImpl: options.sleepImpl,
442
- });
443
- if (!takeover.replaced && !takeover.controlLockCleared) {
444
- throw error;
445
- }
446
- }
447
- }
448
- }
449
- function writeStdout(stream, line) {
450
- if (stream === undefined) {
451
- return;
452
- }
453
- stream.write(`${line}\n`);
454
- }
455
- function createEvent(kind, message, options = {}) {
456
- return {
457
- schemaVersion: 1,
458
- timestampUnixMs: options.timestampUnixMs ?? Date.now(),
459
- level: options.level ?? "info",
460
- kind,
461
- message,
462
- targetBlockHeight: options.targetBlockHeight ?? null,
463
- referencedBlockHashDisplay: options.referencedBlockHashDisplay ?? null,
464
- domainId: options.domainId ?? null,
465
- domainName: options.domainName ?? null,
466
- txid: options.txid ?? null,
467
- feeRateSatVb: options.feeRateSatVb ?? null,
468
- feeSats: options.feeSats ?? null,
469
- score: options.score ?? null,
470
- reason: options.reason ?? null,
471
- runId: options.runId ?? null,
472
- };
473
- }
474
- function cloneMiningState(state) {
475
- const normalized = normalizeMiningStateRecord(state);
476
- return {
477
- ...normalized,
478
- currentBip39WordIndices: normalized.currentBip39WordIndices === null ? null : [...normalized.currentBip39WordIndices],
479
- sharedMiningConflictOutpoint: normalized.sharedMiningConflictOutpoint === null
480
- ? null
481
- : { ...normalized.sharedMiningConflictOutpoint },
482
- };
483
- }
484
- function hasBlockingMutation(state) {
485
- return (state.pendingMutations ?? []).some((mutation) => mutation.status === "draft"
486
- || mutation.status === "broadcasting"
487
- || mutation.status === "broadcast-unknown"
488
- || mutation.status === "live"
489
- || mutation.status === "repair-required");
490
- }
491
- function rootDomain(name) {
492
- return !name.includes("-");
493
- }
494
- function uint32BigEndian(value) {
495
- const buffer = Buffer.alloc(4);
496
- buffer.writeUInt32BE(value >>> 0, 0);
497
- return buffer;
498
- }
499
- function getBlockRewardCogtoshi(height) {
500
- const halvingEra = Math.floor(height / 210_000);
501
- if (halvingEra >= 33) {
502
- return 0n;
503
- }
504
- return 5000000000n >> BigInt(halvingEra);
505
- }
506
- function deriveMiningWordIndices(referencedBlockhash, miningDomainId) {
507
- const seed = createHash("sha256")
508
- .update(Buffer.from(referencedBlockhash))
509
- .update(uint32BigEndian(miningDomainId))
510
- .digest();
511
- const indices = [];
512
- for (let index = 0; index < 5; index += 1) {
513
- const chunkOffset = index * 4;
514
- let wordIndex = seed.readUInt32BE(chunkOffset) % 2048;
515
- while (indices.includes(wordIndex)) {
516
- wordIndex = (wordIndex + 1) % 2048;
517
- }
518
- indices.push(wordIndex);
519
- }
520
- return indices;
521
- }
522
- function outpointKey(outpoint) {
523
- return outpoint === null ? null : `${outpoint.txid}:${outpoint.vout}`;
524
- }
525
- function numberToSats(value) {
526
- const text = typeof value === "number" ? value.toFixed(8) : value;
527
- const match = /^(-?)(\d+)(?:\.(\d{0,8}))?$/.exec(text.trim());
528
- if (match == null) {
529
- throw new Error(`mining_invalid_amount_${text}`);
530
- }
531
- const sign = match[1] === "-" ? -1n : 1n;
532
- const whole = BigInt(match[2] ?? "0");
533
- const fraction = BigInt((match[3] ?? "").padEnd(8, "0"));
534
- return sign * ((whole * 100000000n) + fraction);
535
- }
536
- function satsToBtc(value) {
537
- return Number(value) / 100_000_000;
538
- }
539
- function compareLexicographically(left, right) {
540
- const length = Math.min(left.length, right.length);
541
- for (let index = 0; index < length; index += 1) {
542
- if (left[index] !== right[index]) {
543
- return left[index] < right[index] ? -1 : 1;
544
- }
545
- }
546
- if (left.length === right.length) {
547
- return 0;
548
- }
549
- return left.length < right.length ? -1 : 1;
550
- }
551
- function tieBreakHash(blendSeed, miningDomainId) {
552
- return createHash("sha256")
553
- .update(Buffer.from(blendSeed))
554
- .update(uint32BigEndian(miningDomainId))
555
- .digest();
556
- }
557
- function createMiningLoopState() {
558
- return {
559
- attemptedTipKey: null,
560
- currentTipKey: null,
561
- selectedCandidateTipKey: null,
562
- selectedCandidate: null,
563
- ui: createEmptyMiningFollowVisualizerState(),
564
- waitingNote: null,
565
- providerWaitState: null,
566
- providerWaitLastError: null,
567
- providerWaitNextRetryAtUnixMs: null,
568
- providerTransientFailureCount: 0,
569
- bitcoinRecoveryFirstFailureAtUnixMs: null,
570
- bitcoinRecoveryFirstUnreachableAtUnixMs: null,
571
- bitcoinRecoveryLastRestartAttemptAtUnixMs: null,
572
- bitcoinRecoveryServiceInstanceId: null,
573
- bitcoinRecoveryProcessId: null,
574
- reconnectSettledUntilUnixMs: null,
575
- tipSettledUntilUnixMs: null,
576
- };
577
- }
578
- export function createMiningLoopStateForTesting() {
579
- return createMiningLoopState();
580
- }
581
- function expireMiningSettleWindows(loopState, nowUnixMs) {
582
- if (loopState.reconnectSettledUntilUnixMs !== null
583
- && loopState.reconnectSettledUntilUnixMs <= nowUnixMs) {
584
- loopState.reconnectSettledUntilUnixMs = null;
585
- }
586
- if (loopState.tipSettledUntilUnixMs !== null
587
- && loopState.tipSettledUntilUnixMs <= nowUnixMs) {
588
- loopState.tipSettledUntilUnixMs = null;
589
- }
590
- }
591
- function setMiningReconnectSettleWindow(loopState, nowUnixMs) {
592
- loopState.reconnectSettledUntilUnixMs = nowUnixMs + MINING_NETWORK_SETTLE_WINDOW_MS;
593
- }
594
- function setMiningTipSettleWindow(loopState, nowUnixMs) {
595
- loopState.tipSettledUntilUnixMs = nowUnixMs + MINING_TIP_SETTLE_WINDOW_MS;
596
- }
597
- function buildMiningSettleWindowStatusOverrides(loopState, nowUnixMs) {
598
- expireMiningSettleWindows(loopState, nowUnixMs);
599
- return {
600
- reconnectSettledUntilUnixMs: loopState.reconnectSettledUntilUnixMs,
601
- tipSettledUntilUnixMs: loopState.tipSettledUntilUnixMs,
602
- };
603
- }
604
- function buildMiningTipKey(bestBlockHash, targetBlockHeight) {
605
- if (bestBlockHash === null || targetBlockHeight === null) {
606
- return null;
607
- }
608
- return `${bestBlockHash}:${targetBlockHeight}`;
609
- }
610
- function resetMiningUiForTip(loopState, targetBlockHeight) {
611
- const preservedTxid = loopState.ui.latestTxid;
612
- loopState.ui = {
613
- ...createEmptyMiningFollowVisualizerState(),
614
- latestTxid: preservedTxid,
615
- };
616
- loopState.selectedCandidateTipKey = null;
617
- loopState.selectedCandidate = null;
618
- loopState.waitingNote = null;
619
- }
620
- export function resetMiningUiForTipForTesting(loopState, targetBlockHeight) {
621
- resetMiningUiForTip(loopState, targetBlockHeight);
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
- }
642
- function fallbackSettledWinnerDomainName(domainId) {
643
- return `domain-${domainId}`;
644
- }
645
- function resolveCurrentMinedBlockBoard(options) {
646
- const settledBlockHeight = options.snapshotTipHeight ?? null;
647
- if (settledBlockHeight === null) {
648
- return {
649
- settledBlockHeight,
650
- settledBoardEntries: [],
651
- };
652
- }
653
- if (options.snapshotState === null || options.snapshotState === undefined) {
654
- return {
655
- settledBlockHeight,
656
- settledBoardEntries: [],
657
- };
658
- }
659
- const settledBoardEntries = (getBlockWinners(options.snapshotState, settledBlockHeight) ?? [])
660
- .slice()
661
- .sort((left, right) => left.rank - right.rank || left.txIndex - right.txIndex)
662
- .slice(0, 5)
663
- .map((winner) => ({
664
- rank: winner.rank,
665
- domainName: lookupDomainById(options.snapshotState, winner.domainId)?.name ?? fallbackSettledWinnerDomainName(winner.domainId),
666
- sentence: winner.sentenceText ?? "[unavailable]",
667
- requiredWords: resolveSettledWinnerRequiredWords({
668
- domainId: winner.domainId,
669
- bip39WordIndices: winner.bip39WordIndices,
670
- snapshotTipPreviousHashHex: options.snapshotTipPreviousHashHex,
671
- }),
672
- }));
673
- return {
674
- settledBlockHeight,
675
- settledBoardEntries,
676
- };
677
- }
678
- export function resolveSettledBoardForTesting(options) {
679
- return resolveCurrentMinedBlockBoard({
680
- ...options,
681
- snapshotTipPreviousHashHex: options.snapshotTipPreviousHashHex ?? null,
682
- });
683
- }
684
- function syncMiningUiSettledBoard(loopState, snapshotState, snapshotTipHeight, snapshotTipPreviousHashHex) {
685
- const settledBoard = resolveCurrentMinedBlockBoard({
686
- snapshotState,
687
- snapshotTipHeight,
688
- snapshotTipPreviousHashHex,
689
- nodeBestHeight: null,
690
- });
691
- loopState.ui.settledBlockHeight = settledBoard.settledBlockHeight;
692
- loopState.ui.settledBoardEntries = settledBoard.settledBoardEntries;
693
- }
694
- function syncMiningUiForCurrentTip(options) {
695
- const targetBlockHeight = options.nodeBestHeight === null
696
- ? null
697
- : options.nodeBestHeight + 1;
698
- const tipKey = buildMiningTipKey(options.nodeBestHash, targetBlockHeight);
699
- const priorTipKey = options.loopState.currentTipKey;
700
- const tipChanged = tipKey !== null && tipKey !== priorTipKey;
701
- if (tipKey !== priorTipKey) {
702
- options.loopState.currentTipKey = tipKey;
703
- resetMiningUiForTip(options.loopState, targetBlockHeight);
704
- if (options.recentWin !== null) {
705
- options.loopState.ui.recentWin = options.recentWin;
706
- }
707
- }
708
- syncMiningUiSettledBoard(options.loopState, options.snapshotState, options.snapshotTipHeight, options.snapshotTipPreviousHashHex);
709
- return {
710
- targetBlockHeight,
711
- tipKey,
712
- tipChanged,
713
- };
714
- }
715
- function setMiningUiCandidate(loopState, candidate, liveState) {
716
- loopState.ui.latestSentence = candidate.sentence;
717
- loopState.ui.provisionalRequiredWords = [...candidate.bip39Words];
718
- loopState.ui.provisionalEntry = {
719
- domainName: candidate.domainName,
720
- sentence: candidate.sentence,
721
- };
722
- loopState.ui.provisionalBroadcastTxid = resolveProvisionalBroadcastTxidForCandidate({
723
- candidate,
724
- liveState,
725
- });
726
- }
727
- function getSelectedCandidateForTip(loopState, tipKey) {
728
- if (tipKey === null || loopState.selectedCandidateTipKey !== tipKey) {
729
- return null;
730
- }
731
- return loopState.selectedCandidate;
732
- }
733
- export function getSelectedCandidateForTipForTesting(loopState, tipKey) {
734
- return getSelectedCandidateForTip(loopState, tipKey);
735
- }
736
- function cacheSelectedCandidateForTip(loopState, tipKey, candidate, liveState) {
737
- loopState.selectedCandidateTipKey = tipKey;
738
- loopState.selectedCandidate = candidate;
739
- setMiningUiCandidate(loopState, candidate, liveState);
740
- }
741
- export function cacheSelectedCandidateForTipForTesting(loopState, tipKey, candidate, liveState) {
742
- cacheSelectedCandidateForTip(loopState, tipKey, candidate, liveState);
743
- }
744
- function clearSelectedCandidate(loopState) {
745
- loopState.selectedCandidateTipKey = null;
746
- loopState.selectedCandidate = null;
747
- }
748
- function clearMiningUiTransientCandidate(loopState) {
749
- loopState.ui.provisionalRequiredWords = [];
750
- loopState.ui.provisionalEntry = {
751
- domainName: null,
752
- sentence: null,
753
- };
754
- loopState.ui.provisionalBroadcastTxid = null;
755
- loopState.ui.latestSentence = null;
756
- }
757
- function discardMiningLoopTransientWork(loopState, walletRootId) {
758
- clearMiningGateCache(walletRootId);
759
- clearSelectedCandidate(loopState);
760
- clearMiningUiTransientCandidate(loopState);
761
- loopState.waitingNote = null;
762
- clearMiningProviderWait(loopState);
763
- }
764
- function resolveMiningBitcoindRecoveryIdentity(value) {
765
- const raw = (value ?? {});
766
- return {
767
- serviceInstanceId: raw.serviceInstanceId ?? null,
768
- processId: raw.processId ?? raw.pid ?? null,
769
- };
770
- }
771
- function miningBitcoindRecoveryIdentityMatches(left, right) {
772
- if (left.serviceInstanceId !== null && right.serviceInstanceId !== null) {
773
- return left.serviceInstanceId === right.serviceInstanceId;
774
- }
775
- if (left.processId !== null && right.processId !== null) {
776
- return left.processId === right.processId;
777
- }
778
- return false;
779
- }
780
- function rememberMiningBitcoindRecoveryIdentity(loopState, value) {
781
- const next = resolveMiningBitcoindRecoveryIdentity(value);
782
- if (next.serviceInstanceId === null && next.processId === null) {
783
- return false;
784
- }
785
- const previous = {
786
- serviceInstanceId: loopState.bitcoinRecoveryServiceInstanceId,
787
- processId: loopState.bitcoinRecoveryProcessId,
788
- };
789
- const changed = (previous.serviceInstanceId !== null
790
- || previous.processId !== null) && !miningBitcoindRecoveryIdentityMatches(previous, next);
791
- loopState.bitcoinRecoveryServiceInstanceId = next.serviceInstanceId ?? (next.processId !== null && previous.processId === next.processId
792
- ? previous.serviceInstanceId
793
- : null);
794
- loopState.bitcoinRecoveryProcessId = next.processId ?? (next.serviceInstanceId !== null && previous.serviceInstanceId === next.serviceInstanceId
795
- ? previous.processId
796
- : null);
797
- return changed;
798
- }
799
- function resetMiningBitcoindRecoveryState(loopState, value) {
800
- const hadRecovery = loopState.bitcoinRecoveryFirstFailureAtUnixMs !== null;
801
- loopState.bitcoinRecoveryFirstFailureAtUnixMs = null;
802
- loopState.bitcoinRecoveryFirstUnreachableAtUnixMs = null;
803
- loopState.bitcoinRecoveryLastRestartAttemptAtUnixMs = null;
804
- if (value !== undefined) {
805
- rememberMiningBitcoindRecoveryIdentity(loopState, value);
806
- }
807
- return hadRecovery;
808
- }
809
- function isMiningBitcoindRecoveryPidAlive(pid) {
810
- if (pid === null || pid === undefined || !Number.isInteger(pid) || pid <= 0) {
811
- return false;
812
- }
813
- try {
814
- process.kill(pid, 0);
815
- return true;
816
- }
817
- catch (error) {
818
- if (error instanceof Error && "code" in error && error.code === "EPERM") {
819
- return true;
820
- }
821
- return false;
822
- }
823
- }
824
- function describeRecoverableMiningBitcoindError(error) {
825
- return error instanceof Error ? error.message : String(error);
826
- }
827
- function isRecoverableMiningBitcoindError(error) {
828
- if (isRetryableManagedRpcError(error)) {
829
- return true;
830
- }
831
- if (!(error instanceof Error)) {
832
- return false;
833
- }
834
- if ("code" in error) {
835
- const code = error.code;
836
- if (code === "ENOENT" || code === "ECONNREFUSED" || code === "ECONNRESET") {
837
- return true;
838
- }
839
- }
840
- return error.message === "managed_bitcoind_service_start_timeout"
841
- || error.message === "bitcoind_cookie_timeout"
842
- || error.message.includes("cookie file is unavailable")
843
- || error.message.includes("cookie file could not be read")
844
- || error.message.includes("ECONNREFUSED")
845
- || error.message.includes("ECONNRESET")
846
- || error.message.includes("socket hang up");
847
- }
848
- async function attachManagedBitcoindForRecovery(options) {
849
- try {
850
- const service = await options.attachService({
851
- dataDir: options.dataDir,
852
- chain: "main",
853
- startHeight: 0,
854
- walletRootId: options.walletRootId,
855
- });
856
- const serviceStatus = await service.refreshServiceStatus?.().catch(() => null);
857
- rememberMiningBitcoindRecoveryIdentity(options.loopState, serviceStatus ?? { pid: service.pid });
858
- return true;
859
- }
860
- catch (error) {
861
- if (!isRecoverableMiningBitcoindError(error)) {
862
- throw error;
863
- }
864
- return false;
865
- }
866
- }
867
- async function resolveFundingDisplaySats(state, rpc) {
868
- const utxos = await rpc.listUnspent(state.managedCoreWallet.walletName, 0);
869
- return utxos.reduce((sum, entry) => {
870
- if (entry.scriptPubKey !== state.funding.scriptPubKeyHex
871
- || entry.spendable === false) {
872
- return sum;
873
- }
874
- return sum + numberToSats(entry.amount);
875
- }, 0n);
876
- }
877
- export async function resolveFundingDisplaySatsForTesting(state, rpc) {
878
- return resolveFundingDisplaySats(state, rpc);
879
- }
880
- async function loadMiningVisibleFollowBlockTimes(options) {
881
- if (options.indexedTipHeight === null || options.indexedTipHashHex === null) {
882
- return {};
883
- }
884
- const blockTimesByHeight = {};
885
- let currentHeight = options.indexedTipHeight;
886
- let currentHashHex = options.indexedTipHashHex;
887
- for (let offset = 0; offset <= FOLLOW_VISIBLE_PRIOR_BLOCKS; offset += 1) {
888
- if (currentHeight < 0 || currentHashHex === null) {
889
- break;
890
- }
891
- const block = await options.rpc.getBlock(currentHashHex);
892
- if (typeof block.time === "number") {
893
- blockTimesByHeight[currentHeight] = block.time;
894
- }
895
- currentHashHex = block.previousblockhash ?? null;
896
- currentHeight -= 1;
897
- }
898
- return blockTimesByHeight;
899
- }
900
- export async function loadMiningVisibleFollowBlockTimesForTesting(options) {
901
- return loadMiningVisibleFollowBlockTimes(options);
902
- }
903
- function syncMiningVisualizerBalances(loopState, readContext, balanceSats) {
904
- loopState.ui.balanceCogtoshi = readContext.snapshot === null
905
- ? null
906
- : getBalance(readContext.snapshot.state, readContext.localState.state.funding.scriptPubKeyHex);
907
- loopState.ui.balanceSats = balanceSats;
908
- }
909
- function createIndexedMiningFollowVisualizerState(readContext) {
910
- const uiState = createEmptyMiningFollowVisualizerState();
911
- const localState = readContext.localState;
912
- const settledBoard = resolveCurrentMinedBlockBoard({
913
- snapshotState: readContext.snapshot?.state ?? null,
914
- snapshotTipHeight: readContext.snapshot?.tip?.height ?? readContext.indexer.snapshotTip?.height ?? null,
915
- snapshotTipPreviousHashHex: readContext.snapshot?.tip?.previousHashHex ?? readContext.indexer.snapshotTip?.previousHashHex ?? null,
916
- nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
917
- });
918
- uiState.settledBlockHeight = settledBoard.settledBlockHeight;
919
- uiState.settledBoardEntries = settledBoard.settledBoardEntries;
920
- if (readContext.snapshot !== null && localState.availability === "ready" && localState.state !== null) {
921
- uiState.balanceCogtoshi = getBalance(readContext.snapshot.state, localState.state.funding.scriptPubKeyHex);
922
- }
923
- return uiState;
924
- }
925
- function syncMiningVisualizerBlockTimes(loopState, blockTimesByHeight) {
926
- loopState.ui.visibleBlockTimesByHeight = { ...blockTimesByHeight };
927
- }
928
- export function syncMiningVisualizerBlockTimesForTesting(loopState, blockTimesByHeight) {
929
- syncMiningVisualizerBlockTimes(loopState, blockTimesByHeight);
930
- }
931
- function findRecentMiningWin(snapshotState, txid, targetBlockHeight) {
932
- if (snapshotState === null || snapshotState === undefined || txid === null || targetBlockHeight === null) {
933
- return null;
934
- }
935
- const winners = getBlockWinners(snapshotState, targetBlockHeight) ?? [];
936
- const winner = winners.find((entry) => entry.txidHex === txid) ?? null;
937
- if (winner === null) {
938
- return null;
939
- }
940
- return {
941
- rank: winner.rank,
942
- rewardCogtoshi: winner.rewardCogtoshi,
943
- blockHeight: winner.height,
944
- };
945
- }
946
- function computeIntentFingerprint(state, candidate) {
947
- return createHash("sha256")
948
- .update([
949
- "mine",
950
- state.walletRootId,
951
- candidate.domainId,
952
- candidate.referencedBlockHashDisplay,
953
- Buffer.from(candidate.encodedSentenceBytes).toString("hex"),
954
- ].join("\n"))
955
- .digest("hex");
956
- }
957
- function defaultMiningStatePatch(state, patch) {
958
- return {
959
- ...state,
960
- miningState: {
961
- ...cloneMiningState(state.miningState),
962
- ...patch,
963
- currentPublishState: normalizeMiningPublishState(patch.currentPublishState ?? state.miningState.currentPublishState),
964
- },
965
- };
966
- }
967
- function decodeMinePayload(payload) {
968
- if (payload.length < 68 || Buffer.from(payload.subarray(0, 3)).toString("utf8") !== "COG" || payload[3] !== 0x01) {
969
- return null;
970
- }
971
- return {
972
- domainId: Buffer.from(payload).readUInt32BE(4),
973
- referencedBlockPrefixHex: Buffer.from(payload.subarray(8, 12)).toString("hex"),
974
- sentenceBytes: payload.subarray(12, 72),
975
- };
976
- }
977
- function bytesToHex(value) {
978
- return value == null ? null : Buffer.from(value).toString("hex");
979
- }
980
- function readU32BE(bytes, offset) {
981
- if ((offset + 4) > bytes.length) {
982
- return null;
983
- }
984
- return Buffer.from(bytes.subarray(offset, offset + 4)).readUInt32BE(0);
985
- }
986
- function readLenPrefixedScriptHex(bytes, offset) {
987
- const length = bytes[offset];
988
- if (length === undefined || (offset + 1 + length) > bytes.length) {
989
- return null;
990
- }
991
- return {
992
- scriptHex: Buffer.from(bytes.subarray(offset + 1, offset + 1 + length)).toString("hex"),
993
- nextOffset: offset + 1 + length,
994
- };
995
- }
996
- function parseSupportedAncestorOperation(context) {
997
- const payload = context.payload;
998
- if (payload === null) {
999
- return null;
1000
- }
1001
- if (payload.length < 4
1002
- || payload[0] !== COG_PREFIX[0]
1003
- || payload[1] !== COG_PREFIX[1]
1004
- || payload[2] !== COG_PREFIX[2]) {
1005
- return null;
1006
- }
1007
- const opcode = payload[3];
1008
- if (opcode === COG_OPCODES.DOMAIN_REG) {
1009
- const nameLength = payload[4];
1010
- if (nameLength === undefined || (5 + nameLength) !== payload.length) {
1011
- return "unsupported";
1012
- }
1013
- return {
1014
- kind: "domain-reg",
1015
- name: Buffer.from(payload.subarray(5, 5 + nameLength)).toString("utf8"),
1016
- senderScriptHex: context.senderScriptHex,
1017
- };
1018
- }
1019
- if (opcode === COG_OPCODES.DOMAIN_TRANSFER) {
1020
- const domainId = readU32BE(payload, 4);
1021
- const recipient = domainId === null ? null : readLenPrefixedScriptHex(payload, 8);
1022
- if (domainId === null || recipient === null || recipient.nextOffset !== payload.length) {
1023
- return "unsupported";
1024
- }
1025
- return {
1026
- kind: "domain-transfer",
1027
- domainId,
1028
- recipientScriptHex: recipient.scriptHex,
1029
- senderScriptHex: context.senderScriptHex,
1030
- };
1031
- }
1032
- if (opcode === COG_OPCODES.DOMAIN_ANCHOR) {
1033
- const domainId = readU32BE(payload, 4);
1034
- if (domainId === null) {
1035
- return "unsupported";
1036
- }
1037
- return {
1038
- kind: "domain-anchor",
1039
- domainId,
1040
- senderScriptHex: context.senderScriptHex,
1041
- };
1042
- }
1043
- if (opcode === COG_OPCODES.SET_DELEGATE || opcode === COG_OPCODES.SET_MINER) {
1044
- const domainId = readU32BE(payload, 4);
1045
- if (domainId === null) {
1046
- return "unsupported";
1047
- }
1048
- if (payload.length === 8) {
1049
- return opcode === COG_OPCODES.SET_DELEGATE
1050
- ? { kind: "set-delegate", domainId, delegateScriptHex: null }
1051
- : { kind: "set-miner", domainId, minerScriptHex: null };
1052
- }
1053
- const target = readLenPrefixedScriptHex(payload, 8);
1054
- if (target === null || target.nextOffset !== payload.length) {
1055
- return "unsupported";
1056
- }
1057
- return opcode === COG_OPCODES.SET_DELEGATE
1058
- ? { kind: "set-delegate", domainId, delegateScriptHex: target.scriptHex }
1059
- : { kind: "set-miner", domainId, minerScriptHex: target.scriptHex };
1060
- }
1061
- return "unsupported";
1062
- }
1063
- function getAncestorTxids(context, txContexts) {
1064
- return context.rawTransaction.vin
1065
- .map((vin) => vin.txid ?? null)
1066
- .filter((txid) => txid !== null && txContexts.has(txid));
1067
- }
1068
- function topologicallyOrderAncestorContexts(options) {
1069
- const visited = new Map();
1070
- const ordered = [];
1071
- const root = options.txContexts.get(options.txid);
1072
- if (root === undefined) {
1073
- return [];
1074
- }
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") {
1099
- return null;
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
- }
1124
- }
1125
- return ordered;
1126
- }
1127
- function cloneOverlayDomainFromConfirmed(readContext, domainId) {
1128
- const domain = lookupDomainById(readContext.snapshot.state, domainId);
1129
- if (domain === null) {
1130
- return null;
1131
- }
1132
- return {
1133
- domainId,
1134
- name: domain.name,
1135
- anchored: domain.anchored,
1136
- ownerScriptHex: bytesToHex(domain.ownerScriptPubKey),
1137
- delegateScriptHex: bytesToHex(domain.delegate),
1138
- minerScriptHex: bytesToHex(domain.miner),
1139
- };
1140
- }
1141
- function applySupportedAncestorOperation(options) {
1142
- const ensureDomain = (domainId) => {
1143
- const existing = options.overlay.get(domainId);
1144
- if (existing !== undefined) {
1145
- return existing;
1146
- }
1147
- const confirmed = cloneOverlayDomainFromConfirmed(options.readContext, domainId);
1148
- if (confirmed === null) {
1149
- return null;
1150
- }
1151
- options.overlay.set(domainId, confirmed);
1152
- return confirmed;
1153
- };
1154
- if (options.operation.kind === "domain-reg") {
1155
- if (!rootDomain(options.operation.name)) {
1156
- return { nextDomainId: options.nextDomainId, indeterminate: true };
1157
- }
1158
- if (lookupDomain(options.readContext.snapshot.state, options.operation.name) !== null) {
1159
- return { nextDomainId: options.nextDomainId, indeterminate: true };
1160
- }
1161
- options.overlay.set(options.nextDomainId, {
1162
- domainId: options.nextDomainId,
1163
- name: options.operation.name,
1164
- anchored: false,
1165
- ownerScriptHex: options.operation.senderScriptHex,
1166
- delegateScriptHex: null,
1167
- minerScriptHex: null,
1168
- });
1169
- return {
1170
- nextDomainId: options.nextDomainId + 1,
1171
- indeterminate: false,
1172
- };
1173
- }
1174
- const domain = ensureDomain(options.operation.domainId);
1175
- if (domain === null) {
1176
- return { nextDomainId: options.nextDomainId, indeterminate: true };
1177
- }
1178
- if (options.operation.kind === "domain-transfer") {
1179
- domain.ownerScriptHex = options.operation.recipientScriptHex;
1180
- options.overlay.set(domain.domainId, domain);
1181
- return { nextDomainId: options.nextDomainId, indeterminate: false };
1182
- }
1183
- if (options.operation.kind === "domain-anchor") {
1184
- domain.anchored = true;
1185
- if (options.operation.senderScriptHex !== null) {
1186
- domain.ownerScriptHex = options.operation.senderScriptHex;
1187
- }
1188
- options.overlay.set(domain.domainId, domain);
1189
- return { nextDomainId: options.nextDomainId, indeterminate: false };
1190
- }
1191
- if (options.operation.kind === "set-delegate") {
1192
- domain.delegateScriptHex = options.operation.delegateScriptHex;
1193
- options.overlay.set(domain.domainId, domain);
1194
- return { nextDomainId: options.nextDomainId, indeterminate: false };
1195
- }
1196
- domain.minerScriptHex = options.operation.minerScriptHex;
1197
- options.overlay.set(domain.domainId, domain);
1198
- return { nextDomainId: options.nextDomainId, indeterminate: false };
1199
- }
1200
- async function resolveOverlayAuthorizedMiningDomain(options) {
1201
- const orderedAncestors = topologicallyOrderAncestorContexts({
1202
- txid: options.txid,
1203
- txContexts: options.txContexts,
1204
- });
1205
- if (orderedAncestors === null) {
1206
- return "indeterminate";
1207
- }
1208
- const overlay = new Map();
1209
- let nextDomainId = options.readContext.snapshot.state.consensus.nextDomainId;
1210
- for (const ancestor of orderedAncestors) {
1211
- const parsed = parseSupportedAncestorOperation(ancestor);
1212
- if (parsed === "unsupported") {
1213
- return "indeterminate";
1214
- }
1215
- if (parsed === null) {
1216
- continue;
1217
- }
1218
- const applied = applySupportedAncestorOperation({
1219
- readContext: options.readContext,
1220
- overlay,
1221
- nextDomainId,
1222
- operation: parsed,
1223
- });
1224
- nextDomainId = applied.nextDomainId;
1225
- if (applied.indeterminate) {
1226
- return "indeterminate";
1227
- }
1228
- }
1229
- const domain = overlay.get(options.domainId) ?? cloneOverlayDomainFromConfirmed(options.readContext, options.domainId);
1230
- if (domain === null || domain.name === null || !rootDomain(domain.name) || !domain.anchored) {
1231
- return null;
1232
- }
1233
- const authorized = domain.ownerScriptHex === options.senderScriptHex
1234
- || domain.delegateScriptHex === options.senderScriptHex
1235
- || domain.minerScriptHex === options.senderScriptHex;
1236
- return authorized ? domain : null;
1237
- }
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";
1243
- return {
1244
- ...view.runtime,
1245
- runMode: resolveSnapshotOverride(overrides.runMode, view.runtime.runMode),
1246
- backgroundWorkerPid: resolveSnapshotOverride(overrides.backgroundWorkerPid, view.runtime.backgroundWorkerPid),
1247
- backgroundWorkerRunId: resolveSnapshotOverride(overrides.backgroundWorkerRunId, view.runtime.backgroundWorkerRunId),
1248
- backgroundWorkerHeartbeatAtUnixMs: resolveSnapshotOverride(overrides.backgroundWorkerHeartbeatAtUnixMs, view.runtime.backgroundWorkerHeartbeatAtUnixMs),
1249
- currentPhase: resolvedCurrentPhase,
1250
- currentPublishState: resolveSnapshotOverride(overrides.currentPublishState, view.runtime.currentPublishState),
1251
- targetBlockHeight: resolveSnapshotOverride(overrides.targetBlockHeight, view.runtime.targetBlockHeight),
1252
- referencedBlockHashDisplay: resolveSnapshotOverride(overrides.referencedBlockHashDisplay, view.runtime.referencedBlockHashDisplay),
1253
- currentDomainId: resolveSnapshotOverride(overrides.currentDomainId, view.runtime.currentDomainId),
1254
- currentDomainName: resolveSnapshotOverride(overrides.currentDomainName, view.runtime.currentDomainName),
1255
- currentSentenceDisplay: resolveSnapshotOverride(overrides.currentSentenceDisplay, view.runtime.currentSentenceDisplay),
1256
- currentCanonicalBlend: resolveSnapshotOverride(overrides.currentCanonicalBlend, view.runtime.currentCanonicalBlend),
1257
- currentTxid: resolveSnapshotOverride(overrides.currentTxid, view.runtime.currentTxid),
1258
- currentWtxid: resolveSnapshotOverride(overrides.currentWtxid, view.runtime.currentWtxid),
1259
- currentFeeRateSatVb: resolveSnapshotOverride(overrides.currentFeeRateSatVb, view.runtime.currentFeeRateSatVb),
1260
- currentAbsoluteFeeSats: resolveSnapshotOverride(overrides.currentAbsoluteFeeSats, view.runtime.currentAbsoluteFeeSats),
1261
- currentBlockFeeSpentSats: resolveSnapshotOverride(overrides.currentBlockFeeSpentSats, view.runtime.currentBlockFeeSpentSats),
1262
- lastSuspendDetectedAtUnixMs: resolveSnapshotOverride(overrides.lastSuspendDetectedAtUnixMs, view.runtime.lastSuspendDetectedAtUnixMs),
1263
- reconnectSettledUntilUnixMs: resolveSnapshotOverride(overrides.reconnectSettledUntilUnixMs, view.runtime.reconnectSettledUntilUnixMs),
1264
- tipSettledUntilUnixMs: resolveSnapshotOverride(overrides.tipSettledUntilUnixMs, view.runtime.tipSettledUntilUnixMs),
1265
- providerState: resolveSnapshotOverride(overrides.providerState, clearProviderWaitCarryover
1266
- ? (view.provider.status === "ready" ? "ready" : "unavailable")
1267
- : view.runtime.providerState),
1268
- corePublishState: resolveSnapshotOverride(overrides.corePublishState, view.runtime.corePublishState),
1269
- currentPublishDecision: resolveSnapshotOverride(overrides.currentPublishDecision, view.runtime.currentPublishDecision),
1270
- sameDomainCompetitorSuppressed: resolveSnapshotOverride(overrides.sameDomainCompetitorSuppressed, view.runtime.sameDomainCompetitorSuppressed),
1271
- higherRankedCompetitorDomainCount: resolveSnapshotOverride(overrides.higherRankedCompetitorDomainCount, view.runtime.higherRankedCompetitorDomainCount),
1272
- dedupedCompetitorDomainCount: resolveSnapshotOverride(overrides.dedupedCompetitorDomainCount, view.runtime.dedupedCompetitorDomainCount),
1273
- competitivenessGateIndeterminate: resolveSnapshotOverride(overrides.competitivenessGateIndeterminate, view.runtime.competitivenessGateIndeterminate),
1274
- mempoolSequenceCacheStatus: resolveSnapshotOverride(overrides.mempoolSequenceCacheStatus, view.runtime.mempoolSequenceCacheStatus),
1275
- lastMempoolSequence: resolveSnapshotOverride(overrides.lastMempoolSequence, view.runtime.lastMempoolSequence),
1276
- lastCompetitivenessGateAtUnixMs: resolveSnapshotOverride(overrides.lastCompetitivenessGateAtUnixMs, view.runtime.lastCompetitivenessGateAtUnixMs),
1277
- lastError: resolveSnapshotOverride(overrides.lastError, clearProviderWaitCarryover ? null : view.runtime.lastError),
1278
- note: resolveSnapshotOverride(overrides.note, clearProviderWaitCarryover ? null : view.runtime.note),
1279
- livePublishInMempool: resolveSnapshotOverride(overrides.livePublishInMempool, view.runtime.livePublishInMempool),
1280
- updatedAtUnixMs: Date.now(),
1281
- };
1282
- }
1283
- function buildPrePublishStatusOverrides(options) {
1284
- const replacing = options.state.miningState.currentTxid !== null;
1285
- const replacingAcrossTips = replacing && !livePublishTargetsCandidateTip({
1286
- liveState: options.state.miningState,
1287
- candidate: options.candidate,
1288
- });
1289
- return {
1290
- currentPhase: replacing ? "replacing" : "publishing",
1291
- currentPublishDecision: replacing ? "replacing" : "publishing",
1292
- targetBlockHeight: options.candidate.targetBlockHeight,
1293
- referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
1294
- currentDomainId: options.candidate.domainId,
1295
- currentDomainName: options.candidate.domainName,
1296
- currentSentenceDisplay: options.candidate.sentence,
1297
- currentCanonicalBlend: options.candidate.canonicalBlend.toString(),
1298
- note: replacing
1299
- ? "Replacing the live mining transaction for the current tip."
1300
- : "Broadcasting the best mining candidate for the current tip.",
1301
- ...(replacingAcrossTips
1302
- ? {
1303
- currentPublishState: "none",
1304
- currentTxid: null,
1305
- currentWtxid: null,
1306
- livePublishInMempool: false,
1307
- currentFeeRateSatVb: null,
1308
- currentAbsoluteFeeSats: null,
1309
- currentBlockFeeSpentSats: "0",
1310
- }
1311
- : {}),
1312
- };
1313
- }
1314
- async function refreshAndSaveStatus(options) {
1315
- const view = await inspectMiningControlPlane({
1316
- provider: options.provider,
1317
- localState: options.readContext.localState,
1318
- bitcoind: options.readContext.bitcoind,
1319
- nodeStatus: options.readContext.nodeStatus,
1320
- nodeHealth: options.readContext.nodeHealth,
1321
- indexer: options.readContext.indexer,
1322
- paths: options.paths,
1323
- });
1324
- const snapshot = buildStatusSnapshot(view, options.overrides);
1325
- await saveMiningRuntimeStatus(options.paths.miningStatusPath, snapshot);
1326
- options.visualizer?.update(snapshot, options.visualizerState);
1327
- return snapshot;
1328
- }
1329
- async function appendEvent(paths, event) {
1330
- await appendMiningEvent(paths.miningEventsPath, event);
1331
- }
1332
- async function handleRecoverableMiningBitcoindFailure(options) {
1333
- const failureMessage = describeRecoverableMiningBitcoindError(options.error);
1334
- const walletRootId = options.readContext.localState.walletRootId ?? undefined;
1335
- if (options.loopState.bitcoinRecoveryFirstFailureAtUnixMs === null) {
1336
- options.loopState.bitcoinRecoveryFirstFailureAtUnixMs = options.nowUnixMs;
1337
- }
1338
- let restartedService = false;
1339
- const probe = await options.probeService({
1340
- dataDir: options.dataDir,
1341
- chain: "main",
1342
- startHeight: 0,
1343
- walletRootId,
1344
- }).catch((probeError) => {
1345
- if (!isRecoverableMiningBitcoindError(probeError)) {
1346
- throw probeError;
1347
- }
1348
- return null;
1349
- });
1350
- if (probe !== null) {
1351
- if (probe.compatibility === "compatible") {
1352
- rememberMiningBitcoindRecoveryIdentity(options.loopState, probe.status);
1353
- options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs = null;
1354
- }
1355
- else if (probe.compatibility === "unreachable") {
1356
- const identityChanged = rememberMiningBitcoindRecoveryIdentity(options.loopState, probe.status);
1357
- const livePid = isMiningBitcoindRecoveryPidAlive(probe.status?.processId ?? null);
1358
- if (identityChanged || options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs === null) {
1359
- options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs = options.nowUnixMs;
1360
- }
1361
- if (!livePid) {
1362
- restartedService = await attachManagedBitcoindForRecovery({
1363
- dataDir: options.dataDir,
1364
- walletRootId,
1365
- attachService: options.attachService,
1366
- loopState: options.loopState,
1367
- });
1368
- }
1369
- else {
1370
- const graceElapsed = (options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs !== null
1371
- && options.nowUnixMs - options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs
1372
- >= MINING_BITCOIN_RECOVERY_GRACE_MS);
1373
- const cooldownElapsed = (options.loopState.bitcoinRecoveryLastRestartAttemptAtUnixMs === null
1374
- || options.nowUnixMs - options.loopState.bitcoinRecoveryLastRestartAttemptAtUnixMs
1375
- >= MINING_BITCOIN_RECOVERY_RESTART_COOLDOWN_MS);
1376
- if (graceElapsed && cooldownElapsed) {
1377
- options.loopState.bitcoinRecoveryLastRestartAttemptAtUnixMs = options.nowUnixMs;
1378
- await options.stopService({
1379
- dataDir: options.dataDir,
1380
- walletRootId,
1381
- }).catch((stopError) => {
1382
- if (!isRecoverableMiningBitcoindError(stopError)) {
1383
- throw stopError;
1384
- }
1385
- });
1386
- await attachManagedBitcoindForRecovery({
1387
- dataDir: options.dataDir,
1388
- walletRootId,
1389
- attachService: options.attachService,
1390
- loopState: options.loopState,
1391
- });
1392
- restartedService = true;
1393
- }
1394
- }
1395
- }
1396
- else {
1397
- throw new Error(probe.error ?? "managed_bitcoind_protocol_error");
1398
- }
1399
- }
1400
- if (restartedService) {
1401
- discardMiningLoopTransientWork(options.loopState, walletRootId);
1402
- setMiningReconnectSettleWindow(options.loopState, options.nowUnixMs);
1403
- }
1404
- await refreshAndSaveStatus({
1405
- paths: options.paths,
1406
- provider: options.provider,
1407
- readContext: options.readContext,
1408
- overrides: {
1409
- runMode: options.runMode,
1410
- currentPhase: "waiting-bitcoin-network",
1411
- lastError: failureMessage,
1412
- note: MINING_BITCOIN_RECOVERY_NOTE,
1413
- ...buildMiningSettleWindowStatusOverrides(options.loopState, options.nowUnixMs),
1414
- },
1415
- visualizer: options.visualizer,
1416
- visualizerState: options.loopState.ui,
1417
- });
1418
- }
1419
- async function handleDetectedMiningRuntimeResume(options) {
1420
- const readContext = await options.openReadContext({
1421
- dataDir: options.dataDir,
1422
- databasePath: options.databasePath,
1423
- secretProvider: options.provider,
1424
- paths: options.paths,
1425
- });
1426
- try {
1427
- clearMiningGateCache(readContext.localState.walletRootId);
1428
- setMiningReconnectSettleWindow(options.loopState, options.detectedAtUnixMs);
1429
- await refreshAndSaveStatus({
1430
- paths: options.paths,
1431
- provider: options.provider,
1432
- readContext,
1433
- overrides: {
1434
- runMode: options.runMode,
1435
- backgroundWorkerPid: options.backgroundWorkerPid,
1436
- backgroundWorkerRunId: options.backgroundWorkerRunId,
1437
- backgroundWorkerHeartbeatAtUnixMs: options.runMode === "background" ? Date.now() : null,
1438
- currentPhase: "resuming",
1439
- lastSuspendDetectedAtUnixMs: options.detectedAtUnixMs,
1440
- note: "Mining discarded stale in-flight work after a large local runtime gap and is rechecking health.",
1441
- ...buildMiningSettleWindowStatusOverrides(options.loopState, options.detectedAtUnixMs),
1442
- },
1443
- visualizer: options.visualizer,
1444
- visualizerState: createIndexedMiningFollowVisualizerState(readContext),
1445
- });
1446
- }
1447
- finally {
1448
- await readContext.close();
1449
- }
1450
- await appendEvent(options.paths, createEvent("system-resumed", "Detected a large local runtime gap, discarded stale in-flight mining work, and resumed health checks from scratch.", {
1451
- level: "warn",
1452
- runId: options.backgroundWorkerRunId,
1453
- timestampUnixMs: options.detectedAtUnixMs,
1454
- }));
1455
- }
1456
- function getIndexerTruthKey(readContext) {
1457
- if (readContext.snapshot.daemonInstanceId == null
1458
- || readContext.snapshot.snapshotSeq == null) {
1459
- return null;
1460
- }
1461
- return {
1462
- walletRootId: readContext.localState.state.walletRootId,
1463
- daemonInstanceId: readContext.snapshot.daemonInstanceId,
1464
- snapshotSeq: readContext.snapshot.snapshotSeq,
1465
- };
1466
- }
1467
- async function indexerTruthIsCurrent(options) {
1468
- if (options.truthKey === null) {
1469
- return false;
1470
- }
1471
- const probe = await probeIndexerDaemon({
1472
- dataDir: options.dataDir,
1473
- walletRootId: options.truthKey.walletRootId,
1474
- });
1475
- try {
1476
- return probe.compatibility === "compatible"
1477
- && probe.status !== null
1478
- && probe.status.state === "synced"
1479
- && probe.status.daemonInstanceId === options.truthKey.daemonInstanceId
1480
- && probe.status.snapshotSeq === options.truthKey.snapshotSeq;
1481
- }
1482
- finally {
1483
- await probe.client?.close().catch(() => undefined);
1484
- }
1485
- }
1486
- async function ensureIndexerTruthIsCurrent(options) {
1487
- if (!await indexerTruthIsCurrent(options)) {
1488
- throw new Error("mining_generation_stale_indexer_truth");
1489
- }
1490
- }
1491
- function determineCorePublishState(info) {
1492
- if (info.network.networkactive === false) {
1493
- return "network-inactive";
1494
- }
1495
- if ((info.network.connections_out ?? 0) <= 0) {
1496
- return "no-outbound-peers";
1497
- }
1498
- if (info.blockchain.initialblockdownload === true) {
1499
- return "ibd";
1500
- }
1501
- if (info.mempool.loaded === false) {
1502
- return "mempool-loading";
1503
- }
1504
- return "healthy";
1505
- }
1506
- function createMiningPlan(options) {
1507
- const fundingUtxos = options.allUtxos.filter((entry) => entry.scriptPubKey === options.state.funding.scriptPubKeyHex
1508
- && entry.confirmations >= MINING_FUNDING_MIN_CONF
1509
- && entry.spendable !== false
1510
- && entry.safe !== false
1511
- && !(options.conflictOutpoint !== null
1512
- && entry.txid === options.conflictOutpoint.txid
1513
- && entry.vout === options.conflictOutpoint.vout));
1514
- const opReturnData = serializeMine(options.candidate.domainId, options.candidate.referencedBlockHashInternal, options.candidate.encodedSentenceBytes).opReturnData;
1515
- const expectedOpReturnScriptHex = Buffer.concat([
1516
- Buffer.from([0x6a, opReturnData.length]),
1517
- Buffer.from(opReturnData),
1518
- ]).toString("hex");
1519
- return {
1520
- sender: options.candidate.sender,
1521
- fixedInputs: options.conflictOutpoint === null ? [] : [options.conflictOutpoint],
1522
- outputs: [{ data: Buffer.from(opReturnData).toString("hex") }],
1523
- changeAddress: options.state.funding.address,
1524
- changePosition: 1,
1525
- expectedOpReturnScriptHex,
1526
- allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
1527
- eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => walletMutationOutpointKey({ txid: entry.txid, vout: entry.vout }))),
1528
- expectedConflictOutpoint: options.conflictOutpoint,
1529
- feeRateSatVb: options.feeRateSatVb,
1530
- };
1531
- }
1532
- function validateMiningDraft(decoded, funded, plan) {
1533
- const inputs = decoded.tx.vin;
1534
- const outputs = decoded.tx.vout;
1535
- if (inputs.length === 0) {
1536
- throw new Error("wallet_mining_missing_inputs");
1537
- }
1538
- assertFixedInputPrefixMatches(inputs, plan.fixedInputs, "wallet_mining_missing_inputs");
1539
- if (plan.expectedConflictOutpoint !== null
1540
- && (inputs[0]?.txid !== plan.expectedConflictOutpoint.txid
1541
- || inputs[0]?.vout !== plan.expectedConflictOutpoint.vout)) {
1542
- throw new Error("wallet_mining_conflict_input_mismatch");
1543
- }
1544
- if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
1545
- throw new Error("wallet_mining_opreturn_mismatch");
1546
- }
1547
- if (funded.changepos !== -1 && (funded.changepos !== plan.changePosition || outputs[funded.changepos]?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex)) {
1548
- throw new Error("wallet_mining_change_output_mismatch");
1549
- }
1550
- }
1551
- async function buildMiningTransaction(options) {
1552
- return buildWalletMutationTransaction({
1553
- rpc: options.rpc,
1554
- walletName: options.walletName,
1555
- state: options.state,
1556
- plan: options.plan,
1557
- validateFundedDraft: validateMiningDraft,
1558
- finalizeErrorCode: "wallet_mining_finalize_failed",
1559
- mempoolRejectPrefix: "wallet_mining_mempool_rejected",
1560
- feeRate: options.plan.feeRateSatVb,
1561
- availableFundingMinConf: MINING_FUNDING_MIN_CONF,
1562
- });
1563
- }
1564
- export function createMiningPlanForTesting(options) {
1565
- return createMiningPlan(options);
1566
- }
1567
- export function validateMiningDraftForTesting(decoded, funded, plan) {
1568
- validateMiningDraft(decoded, funded, plan);
1569
- }
1570
- function resolveEligibleAnchoredRoots(context) {
1571
- const state = context.localState.state;
1572
- const model = context.model;
1573
- const snapshot = context.snapshot;
1574
- if (state === null || model === null || snapshot === null) {
1575
- return [];
1576
- }
1577
- const domains = [];
1578
- for (const domain of model.domains) {
1579
- if (!isMineableWalletDomain(context, domain)) {
1580
- continue;
1581
- }
1582
- const domainId = domain.domainId;
1583
- if (domainId === null
1584
- || domainId === undefined
1585
- || domain.ownerAddress == null
1586
- || domain.ownerScriptPubKeyHex !== model.walletScriptPubKeyHex) {
1587
- continue;
1588
- }
1589
- const chainDomain = lookupDomain(snapshot.state, domain.name);
1590
- if (chainDomain === null || !chainDomain.anchored) {
1591
- continue;
1592
- }
1593
- domains.push({
1594
- domainId,
1595
- domainName: domain.name,
1596
- localIndex: 0,
1597
- sender: {
1598
- localIndex: 0,
1599
- scriptPubKeyHex: model.walletScriptPubKeyHex,
1600
- address: domain.ownerAddress,
1601
- },
1602
- });
1603
- }
1604
- return domains.sort((left, right) => left.domainId - right.domainId || left.domainName.localeCompare(right.domainName));
1605
- }
1606
- function refreshMiningCandidateFromCurrentState(context, candidate) {
1607
- const refreshed = resolveEligibleAnchoredRoots(context).find((domain) => domain.domainId === candidate.domainId);
1608
- if (refreshed === undefined) {
1609
- return null;
1610
- }
1611
- return {
1612
- ...candidate,
1613
- domainName: refreshed.domainName,
1614
- localIndex: refreshed.localIndex,
1615
- sender: refreshed.sender,
1616
- };
1617
- }
1618
- export function refreshMiningCandidateFromCurrentStateForTesting(context, candidate) {
1619
- return refreshMiningCandidateFromCurrentState(context, candidate);
1620
- }
1621
- function resolveMiningConflictOutpoint(options) {
1622
- const normalizedMiningState = normalizeMiningStateRecord(options.state.miningState);
1623
- if (miningPublishIsInMempool(normalizedMiningState) && normalizedMiningState.sharedMiningConflictOutpoint !== null) {
1624
- return { ...normalizedMiningState.sharedMiningConflictOutpoint };
1625
- }
1626
- void options.allUtxos;
1627
- return null;
1628
- }
1629
- export function resolveMiningConflictOutpointForTesting(options) {
1630
- return resolveMiningConflictOutpoint(options);
1631
- }
1632
- function createStaleMiningCandidateWaitingNote() {
1633
- return "Mining candidate changed before broadcast: the selected root domain is no longer locally mineable. Skipping this tip and waiting for the next block.";
1634
- }
1635
- function createRetryableMiningPublishWaitingNote() {
1636
- return "Selected mining candidate did not reach mempool and will be retried on the current tip with refreshed wallet state.";
1637
- }
1638
- const MINING_FUNDING_MIN_CONF = 0;
1639
- function createInsufficientFundsMiningPublishWaitingNote() {
1640
- return "Mining is waiting for enough safe BTC funding that Bitcoin Core can use for the next publish.";
1641
- }
1642
- function createInsufficientFundsMiningPublishErrorMessage() {
1643
- return "Bitcoin Core could not fund the next mining publish with safe BTC.";
1644
- }
1645
- function buildMiningGenerationRequest(options) {
1646
- return {
1647
- schemaVersion: 1,
1648
- requestId: options.requestId ?? `mining-${options.targetBlockHeight}-${randomBytes(8).toString("hex")}`,
1649
- targetBlockHeight: options.targetBlockHeight,
1650
- referencedBlockHashDisplay: options.referencedBlockHashDisplay,
1651
- generatedAtUnixMs: options.generatedAtUnixMs ?? Date.now(),
1652
- extraPrompt: options.extraPrompt,
1653
- limits: createMiningSentenceRequestLimits(),
1654
- rootDomains: options.domains.map((domain) => ({
1655
- domainId: domain.domainId,
1656
- domainName: domain.domainName,
1657
- requiredWords: domain.requiredWords,
1658
- extraPrompt: options.domainExtraPrompts[domain.domainName.toLowerCase()] ?? null,
1659
- })),
1660
- };
1661
- }
1662
- export function buildMiningGenerationRequestForTesting(options) {
1663
- return buildMiningGenerationRequest({
1664
- ...options,
1665
- domainExtraPrompts: options.domainExtraPrompts ?? {},
1666
- extraPrompt: options.extraPrompt ?? null,
1667
- });
1668
- }
1669
- async function generateCandidatesForDomains(options) {
1670
- const bestBlockHash = options.readContext.nodeStatus?.nodeBestHashHex;
1671
- if (bestBlockHash === null || bestBlockHash === undefined) {
1672
- return [];
1673
- }
1674
- const targetBlockHeight = (options.readContext.nodeStatus?.nodeBestHeight ?? 0) + 1;
1675
- const referencedBlockHashInternal = Buffer.from(displayToInternalBlockhash(bestBlockHash), "hex");
1676
- const rootDomains = options.domains.map((domain) => ({
1677
- ...domain,
1678
- requiredWords: getWords(domain.domainId, referencedBlockHashInternal),
1679
- }));
1680
- const clientConfig = await loadClientConfig({
1681
- path: options.paths.clientConfigPath,
1682
- provider: options.provider,
1683
- }).catch(() => null);
1684
- const abortController = new AbortController();
1685
- let stale = false;
1686
- let staleIndexerTruth = false;
1687
- let preempted = false;
1688
- const timer = setInterval(async () => {
1689
- try {
1690
- const [current, truthCurrent] = await Promise.all([
1691
- options.rpc.getBlockchainInfo(),
1692
- indexerTruthIsCurrent({
1693
- dataDir: options.readContext.dataDir,
1694
- truthKey: options.indexerTruthKey,
1695
- }),
1696
- ]);
1697
- if (current.bestblockhash !== bestBlockHash) {
1698
- stale = true;
1699
- abortController.abort();
1700
- return;
1701
- }
1702
- if (!truthCurrent) {
1703
- staleIndexerTruth = true;
1704
- abortController.abort();
1705
- return;
1706
- }
1707
- if (await isMiningGenerationAbortRequested(options.paths)) {
1708
- preempted = true;
1709
- abortController.abort();
1710
- }
1711
- }
1712
- catch {
1713
- // Ignore transient polling failures and let the main cycle degrade on the next tick.
1714
- }
1715
- }, BEST_BLOCK_POLL_INTERVAL_MS);
1716
- try {
1717
- await markMiningGenerationActive({
1718
- paths: options.paths,
1719
- runId: options.runId ?? null,
1720
- pid: process.pid ?? null,
1721
- });
1722
- const generationRequest = buildMiningGenerationRequest({
1723
- targetBlockHeight,
1724
- referencedBlockHashDisplay: bestBlockHash,
1725
- domains: rootDomains,
1726
- domainExtraPrompts: clientConfig?.mining.domainExtraPrompts ?? {},
1727
- extraPrompt: clientConfig?.mining.builtIn?.extraPrompt ?? null,
1728
- });
1729
- let generated;
1730
- try {
1731
- generated = await generateMiningSentences(generationRequest, {
1732
- paths: options.paths,
1733
- provider: options.provider,
1734
- signal: abortController.signal,
1735
- fetchImpl: options.fetchImpl,
1736
- });
1737
- }
1738
- catch (error) {
1739
- if (stale) {
1740
- throw new Error("mining_generation_stale_tip");
1741
- }
1742
- if (staleIndexerTruth) {
1743
- throw new Error("mining_generation_stale_indexer_truth");
1744
- }
1745
- if (preempted) {
1746
- throw new Error("mining_generation_preempted");
1747
- }
1748
- throw error;
1749
- }
1750
- if (stale) {
1751
- throw new Error("mining_generation_stale_tip");
1752
- }
1753
- if (staleIndexerTruth) {
1754
- throw new Error("mining_generation_stale_indexer_truth");
1755
- }
1756
- if (preempted) {
1757
- throw new Error("mining_generation_preempted");
1758
- }
1759
- await ensureIndexerTruthIsCurrent({
1760
- dataDir: options.readContext.dataDir,
1761
- truthKey: options.indexerTruthKey,
1762
- });
1763
- const sentencesByDomain = new Map();
1764
- for (const candidate of generated.candidates) {
1765
- const existing = sentencesByDomain.get(candidate.domainId) ?? [];
1766
- existing.push(candidate.sentence);
1767
- sentencesByDomain.set(candidate.domainId, existing);
1768
- }
1769
- const candidates = [];
1770
- for (const domain of rootDomains) {
1771
- const domainSentences = sentencesByDomain.get(domain.domainId) ?? [];
1772
- if (domainSentences.length === 0) {
1773
- continue;
1774
- }
1775
- const assayed = await assaySentences(domain.domainId, referencedBlockHashInternal, domainSentences);
1776
- const best = assayed.find((entry) => entry.gatesPass && entry.encodedSentenceBytes !== null && entry.rank === 1);
1777
- if (best === undefined || best.encodedSentenceBytes === null || best.canonicalBlend === null) {
1778
- continue;
1779
- }
1780
- candidates.push({
1781
- domainId: domain.domainId,
1782
- domainName: domain.domainName,
1783
- localIndex: domain.localIndex,
1784
- sender: domain.sender,
1785
- sentence: best.sentence,
1786
- encodedSentenceBytes: best.encodedSentenceBytes,
1787
- bip39WordIndices: [...best.bip39WordIndices],
1788
- bip39Words: best.bip39Words,
1789
- canonicalBlend: best.canonicalBlend,
1790
- referencedBlockHashDisplay: bestBlockHash,
1791
- referencedBlockHashInternal,
1792
- targetBlockHeight,
1793
- });
1794
- }
1795
- return candidates;
1796
- }
1797
- finally {
1798
- clearInterval(timer);
1799
- await markMiningGenerationInactive({
1800
- paths: options.paths,
1801
- runId: options.runId ?? null,
1802
- pid: process.pid ?? null,
1803
- }).catch(() => undefined);
1804
- }
1805
- }
1806
- async function chooseBestLocalCandidate(candidates) {
1807
- if (candidates.length === 0) {
1808
- return null;
1809
- }
1810
- if (candidates.length === 1) {
1811
- return candidates[0];
1812
- }
1813
- const blendSeed = deriveBlendSeed(candidates[0].referencedBlockHashInternal);
1814
- const winners = await settleBlock({
1815
- blendSeed,
1816
- blockRewardCogtoshi: 100n,
1817
- submissions: candidates
1818
- .slice()
1819
- .sort((left, right) => left.domainId - right.domainId || left.domainName.localeCompare(right.domainName))
1820
- .map((candidate, index) => ({
1821
- miningDomainId: candidate.domainId,
1822
- rawSentenceBytes: candidate.encodedSentenceBytes,
1823
- recipientScriptPubKey: Buffer.from(candidate.sender.scriptPubKeyHex, "hex"),
1824
- bip39WordIndices: candidate.bip39WordIndices,
1825
- txIndex: index,
1826
- })),
1827
- });
1828
- const winner = winners[0];
1829
- if (winner === undefined) {
1830
- return null;
1831
- }
1832
- return candidates.find((candidate) => candidate.domainId === winner.miningDomainId) ?? null;
1833
- }
1834
- function isBetterVisibleCompetitor(candidate, current) {
1835
- if (current === undefined) {
1836
- return true;
1837
- }
1838
- if (candidate.canonicalBlend !== current.canonicalBlend) {
1839
- return candidate.canonicalBlend > current.canonicalBlend;
1840
- }
1841
- if (candidate.effectiveFeeRate !== current.effectiveFeeRate) {
1842
- return candidate.effectiveFeeRate > current.effectiveFeeRate;
1843
- }
1844
- return candidate.txid.localeCompare(current.txid) < 0;
1845
- }
1846
- function rankMiningSentenceEntries(entries, blendSeed) {
1847
- return entries
1848
- .map((entry) => ({
1849
- ...entry,
1850
- tieBreak: tieBreakHash(blendSeed, entry.domainId),
1851
- }))
1852
- .sort((left, right) => {
1853
- if (left.canonicalBlend !== right.canonicalBlend) {
1854
- return left.canonicalBlend > right.canonicalBlend ? -1 : 1;
1855
- }
1856
- const tieBreakOrder = compareLexicographically(left.tieBreak, right.tieBreak);
1857
- if (tieBreakOrder !== 0) {
1858
- return tieBreakOrder;
1859
- }
1860
- return left.txIndex - right.txIndex;
1861
- })
1862
- .map((entry, index) => ({
1863
- ...entry,
1864
- rank: index + 1,
1865
- }));
1866
- }
1867
- function toSentenceBoardEntries(entries) {
1868
- return entries.slice(0, 5).map((entry) => ({
1869
- rank: entry.rank,
1870
- domainName: entry.domainName,
1871
- sentence: entry.sentence,
1872
- requiredWords: resolveBip39WordsFromIndices(entry.bip39WordIndices),
1873
- }));
1874
- }
1875
- async function runCompetitivenessGate(options) {
1876
- const createDecision = (overrides) => ({
1877
- allowed: overrides.allowed ?? false,
1878
- decision: overrides.decision ?? "indeterminate-mempool-gate",
1879
- sameDomainCompetitorSuppressed: overrides.sameDomainCompetitorSuppressed ?? false,
1880
- higherRankedCompetitorDomainCount: overrides.higherRankedCompetitorDomainCount ?? 0,
1881
- dedupedCompetitorDomainCount: overrides.dedupedCompetitorDomainCount ?? 0,
1882
- competitivenessGateIndeterminate: overrides.competitivenessGateIndeterminate ?? false,
1883
- mempoolSequenceCacheStatus: overrides.mempoolSequenceCacheStatus ?? null,
1884
- lastMempoolSequence: overrides.lastMempoolSequence ?? null,
1885
- visibleBoardEntries: overrides.visibleBoardEntries ?? [],
1886
- candidateRank: overrides.candidateRank ?? null,
1887
- });
1888
- const walletRootId = options.readContext.localState.walletRootId ?? "uninitialized-wallet-root";
1889
- const assaySentencesImpl = options.assaySentencesImpl ?? assaySentences;
1890
- const indexerTruthKey = getIndexerTruthKey(options.readContext);
1891
- const excludedTxids = [options.currentTxid].filter((value) => value !== null).sort();
1892
- const localAssayTupleKey = [
1893
- options.candidate.domainId,
1894
- Buffer.from(options.candidate.encodedSentenceBytes).toString("hex"),
1895
- options.candidate.canonicalBlend.toString(),
1896
- options.candidate.sender.scriptPubKeyHex,
1897
- ].join(":");
1898
- let mempoolVerbose;
1899
- try {
1900
- mempoolVerbose = await options.rpc.getRawMempoolVerbose();
1901
- }
1902
- catch {
1903
- return createDecision({
1904
- competitivenessGateIndeterminate: true,
1905
- });
1906
- }
1907
- const mempoolSequence = String(mempoolVerbose.mempool_sequence);
1908
- const cached = miningGateCache.get(walletRootId);
1909
- const cachedTruthMatches = cached !== undefined
1910
- && indexerTruthKey !== null
1911
- && cached.indexerDaemonInstanceId === indexerTruthKey.daemonInstanceId
1912
- && cached.indexerSnapshotSeq === indexerTruthKey.snapshotSeq;
1913
- const cachedReferencedBlockMatches = cached !== undefined
1914
- && cached.referencedBlockHashDisplay === options.candidate.referencedBlockHashDisplay;
1915
- if (cached !== undefined && (!cachedTruthMatches || !cachedReferencedBlockMatches)) {
1916
- clearMiningGateCache(walletRootId);
1917
- }
1918
- if (cached !== undefined
1919
- && cachedTruthMatches
1920
- && cachedReferencedBlockMatches
1921
- && cached.localAssayTupleKey === localAssayTupleKey
1922
- && cached.excludedTxidsKey === excludedTxids.join(",")
1923
- && cached.mempoolSequence === mempoolSequence) {
1924
- return {
1925
- ...cached.decision,
1926
- mempoolSequenceCacheStatus: "reused",
1927
- };
1928
- }
1929
- const referencedPrefix = Buffer.from(options.candidate.referencedBlockHashInternal.subarray(0, 4)).toString("hex");
1930
- const visibleTxids = mempoolVerbose.txids.filter((txid) => !excludedTxids.includes(txid));
1931
- const txContexts = cachedTruthMatches && cachedReferencedBlockMatches
1932
- ? (cached?.txContexts ?? new Map())
1933
- : new Map();
1934
- for (const txid of [...txContexts.keys()]) {
1935
- if (!visibleTxids.includes(txid)) {
1936
- txContexts.delete(txid);
1937
- }
1938
- }
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];
1946
- if (txContexts.has(txid)) {
1947
- continue;
1948
- }
1949
- const [tx, mempoolEntry] = await Promise.all([
1950
- options.rpc.getRawTransaction(txid, true).catch(() => null),
1951
- options.rpc.getMempoolEntry(txid).catch(() => null),
1952
- ]);
1953
- if (tx === null || mempoolEntry === null) {
1954
- continue;
1955
- }
1956
- const effectiveFeeRate = Number([
1957
- mempoolEntry.vsize > 0 ? (numberToSats(mempoolEntry.fees.base) / BigInt(mempoolEntry.vsize)) : 0n,
1958
- (mempoolEntry.ancestorsize ?? 0) > 0 ? (numberToSats(mempoolEntry.fees.ancestor) / BigInt(mempoolEntry.ancestorsize ?? 1)) : 0n,
1959
- (mempoolEntry.descendantsize ?? 0) > 0 ? (numberToSats(mempoolEntry.fees.descendant) / BigInt(mempoolEntry.descendantsize ?? 1)) : 0n,
1960
- ].reduce((best, candidate) => (candidate > best ? candidate : best), 0n));
1961
- const payloadHex = tx.vout.find((entry) => entry.scriptPubKey?.hex?.startsWith("6a") === true)?.scriptPubKey?.hex;
1962
- txContexts.set(txid, {
1963
- txid,
1964
- effectiveFeeRate,
1965
- senderScriptHex: tx.vin[0]?.prevout?.scriptPubKey?.hex ?? null,
1966
- rawTransaction: tx,
1967
- payload: payloadHex === undefined ? null : extractOpReturnPayloadFromScriptHex(payloadHex),
1968
- });
1969
- }
1970
- const entries = new Map();
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];
1978
- const context = txContexts.get(txid);
1979
- if (context === undefined || context.payload === null || context.senderScriptHex === null) {
1980
- continue;
1981
- }
1982
- const decoded = decodeMinePayload(context.payload);
1983
- if (decoded === null || decoded.referencedBlockPrefixHex !== referencedPrefix) {
1984
- continue;
1985
- }
1986
- const overlayDomain = await resolveOverlayAuthorizedMiningDomain({
1987
- readContext: options.readContext,
1988
- txid,
1989
- txContexts,
1990
- domainId: decoded.domainId,
1991
- senderScriptHex: context.senderScriptHex,
1992
- });
1993
- if (overlayDomain === "indeterminate") {
1994
- const decision = createDecision({
1995
- competitivenessGateIndeterminate: true,
1996
- decision: "indeterminate-mempool-gate",
1997
- mempoolSequenceCacheStatus: "refreshed",
1998
- lastMempoolSequence: mempoolSequence,
1999
- });
2000
- miningGateCache.set(walletRootId, {
2001
- indexerDaemonInstanceId: indexerTruthKey?.daemonInstanceId ?? "none",
2002
- indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
2003
- referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
2004
- localAssayTupleKey,
2005
- excludedTxidsKey: excludedTxids.join(","),
2006
- mempoolSequence,
2007
- txids: [...visibleTxids],
2008
- txContexts,
2009
- decision,
2010
- });
2011
- return decision;
2012
- }
2013
- if (overlayDomain === null || overlayDomain.name === null || !rootDomain(overlayDomain.name)) {
2014
- continue;
2015
- }
2016
- const assayed = await assaySentencesImpl(decoded.domainId, options.candidate.referencedBlockHashInternal, [Buffer.from(decoded.sentenceBytes).toString("utf8")]).catch(() => []);
2017
- const scored = assayed[0];
2018
- if (scored === undefined || !scored.gatesPass || scored.encodedSentenceBytes === null || scored.canonicalBlend === null) {
2019
- continue;
2020
- }
2021
- entries.set(txid, {
2022
- txid,
2023
- effectiveFeeRate: context.effectiveFeeRate,
2024
- domainId: decoded.domainId,
2025
- domainName: overlayDomain.name,
2026
- sentence: Buffer.from(decoded.sentenceBytes).toString("utf8"),
2027
- senderScriptHex: context.senderScriptHex,
2028
- encodedSentenceBytesHex: Buffer.from(scored.encodedSentenceBytes).toString("hex"),
2029
- bip39WordIndices: [...scored.bip39WordIndices],
2030
- canonicalBlend: scored.canonicalBlend,
2031
- });
2032
- }
2033
- const blendSeed = deriveBlendSeed(options.candidate.referencedBlockHashInternal);
2034
- const visibleBestByDomain = new Map();
2035
- for (const entry of entries.values()) {
2036
- const current = visibleBestByDomain.get(entry.domainId);
2037
- if (isBetterVisibleCompetitor(entry, current)) {
2038
- visibleBestByDomain.set(entry.domainId, entry);
2039
- }
2040
- }
2041
- const visibleRankedEntries = rankMiningSentenceEntries([...visibleBestByDomain.values()]
2042
- .sort((left, right) => left.domainId - right.domainId || left.txid.localeCompare(right.txid))
2043
- .map((entry, index) => ({
2044
- domainId: entry.domainId,
2045
- domainName: entry.domainName,
2046
- sentence: entry.sentence,
2047
- canonicalBlend: entry.canonicalBlend,
2048
- senderScriptHex: entry.senderScriptHex,
2049
- encodedSentenceBytesHex: entry.encodedSentenceBytesHex,
2050
- bip39WordIndices: entry.bip39WordIndices,
2051
- txid: entry.txid,
2052
- txIndex: index,
2053
- })), blendSeed);
2054
- const sameDomainCompetitors = [...visibleBestByDomain.values()].filter((entry) => entry.domainId === options.candidate.domainId);
2055
- const sameDomainCompetitorSuppressed = sameDomainCompetitors.some((competitor) => competitor.canonicalBlend > options.candidate.canonicalBlend
2056
- || competitor.canonicalBlend === options.candidate.canonicalBlend);
2057
- let decision;
2058
- const otherDomainBest = new Map();
2059
- for (const entry of visibleBestByDomain.values()) {
2060
- if (entry.domainId === options.candidate.domainId) {
2061
- continue;
2062
- }
2063
- const best = otherDomainBest.get(entry.domainId);
2064
- if (isBetterVisibleCompetitor(entry, best)) {
2065
- otherDomainBest.set(entry.domainId, entry);
2066
- }
2067
- }
2068
- if (sameDomainCompetitorSuppressed) {
2069
- decision = createDecision({
2070
- allowed: false,
2071
- decision: "suppressed-same-domain-mempool",
2072
- sameDomainCompetitorSuppressed: true,
2073
- higherRankedCompetitorDomainCount: 1,
2074
- dedupedCompetitorDomainCount: otherDomainBest.size,
2075
- competitivenessGateIndeterminate: false,
2076
- mempoolSequenceCacheStatus: "refreshed",
2077
- lastMempoolSequence: mempoolSequence,
2078
- visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
2079
- });
2080
- }
2081
- else {
2082
- try {
2083
- const candidateRankedEntries = rankMiningSentenceEntries([
2084
- {
2085
- domainId: options.candidate.domainId,
2086
- domainName: options.candidate.domainName,
2087
- sentence: options.candidate.sentence,
2088
- canonicalBlend: options.candidate.canonicalBlend,
2089
- senderScriptHex: options.candidate.sender.scriptPubKeyHex,
2090
- encodedSentenceBytesHex: Buffer.from(options.candidate.encodedSentenceBytes).toString("hex"),
2091
- bip39WordIndices: options.candidate.bip39WordIndices,
2092
- txid: null,
2093
- txIndex: 0,
2094
- },
2095
- ...[...otherDomainBest.values()]
2096
- .sort((left, right) => left.domainId - right.domainId || left.txid.localeCompare(right.txid))
2097
- .map((entry, index) => ({
2098
- domainId: entry.domainId,
2099
- domainName: entry.domainName,
2100
- sentence: entry.sentence,
2101
- canonicalBlend: entry.canonicalBlend,
2102
- senderScriptHex: entry.senderScriptHex,
2103
- encodedSentenceBytesHex: entry.encodedSentenceBytesHex,
2104
- bip39WordIndices: entry.bip39WordIndices,
2105
- txid: entry.txid,
2106
- txIndex: index + 1,
2107
- })),
2108
- ], blendSeed);
2109
- const localEntry = candidateRankedEntries.find((entry) => entry.txid === null) ?? null;
2110
- const candidateRank = localEntry?.rank ?? null;
2111
- const higherRankedCompetitorDomainCount = candidateRank === null ? 0 : Math.max(0, candidateRank - 1);
2112
- if (candidateRank !== null && candidateRank > 5) {
2113
- decision = createDecision({
2114
- allowed: false,
2115
- decision: "suppressed-top5-mempool",
2116
- sameDomainCompetitorSuppressed: false,
2117
- higherRankedCompetitorDomainCount,
2118
- dedupedCompetitorDomainCount: otherDomainBest.size,
2119
- competitivenessGateIndeterminate: false,
2120
- mempoolSequenceCacheStatus: "refreshed",
2121
- lastMempoolSequence: mempoolSequence,
2122
- visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
2123
- candidateRank,
2124
- });
2125
- }
2126
- else {
2127
- decision = createDecision({
2128
- allowed: candidateRank !== null,
2129
- decision: "publish",
2130
- sameDomainCompetitorSuppressed: false,
2131
- higherRankedCompetitorDomainCount,
2132
- dedupedCompetitorDomainCount: otherDomainBest.size,
2133
- competitivenessGateIndeterminate: false,
2134
- mempoolSequenceCacheStatus: "refreshed",
2135
- lastMempoolSequence: mempoolSequence,
2136
- visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
2137
- candidateRank,
2138
- });
2139
- }
2140
- }
2141
- catch {
2142
- decision = createDecision({
2143
- allowed: false,
2144
- decision: "indeterminate-mempool-gate",
2145
- sameDomainCompetitorSuppressed: false,
2146
- higherRankedCompetitorDomainCount: 0,
2147
- dedupedCompetitorDomainCount: otherDomainBest.size,
2148
- competitivenessGateIndeterminate: true,
2149
- mempoolSequenceCacheStatus: "refreshed",
2150
- lastMempoolSequence: mempoolSequence,
2151
- visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
2152
- });
2153
- }
2154
- }
2155
- miningGateCache.set(walletRootId, {
2156
- indexerDaemonInstanceId: indexerTruthKey?.daemonInstanceId ?? "none",
2157
- indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
2158
- referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
2159
- localAssayTupleKey,
2160
- excludedTxidsKey: excludedTxids.join(","),
2161
- mempoolSequence,
2162
- txids: [...visibleTxids],
2163
- txContexts,
2164
- decision,
2165
- });
2166
- return decision;
2167
- }
2168
- function livePublishTargetsCandidateTip(options) {
2169
- const liveState = normalizeMiningStateRecord(options.liveState);
2170
- return liveState.currentTxid !== null
2171
- && liveState.currentPublishState === "in-mempool"
2172
- && liveState.livePublishInMempool === true
2173
- && liveState.currentReferencedBlockHashDisplay === options.candidate.referencedBlockHashDisplay
2174
- && liveState.currentBlockTargetHeight === options.candidate.targetBlockHeight;
2175
- }
2176
- function miningCandidateIsCurrent(options) {
2177
- return options.state.currentReferencedBlockHashDisplay !== null
2178
- && options.nodeBestHash !== null
2179
- && options.state.currentReferencedBlockHashDisplay === options.nodeBestHash
2180
- && options.state.currentBlockTargetHeight !== null
2181
- && options.nodeBestHeight !== null
2182
- && options.state.currentBlockTargetHeight === (options.nodeBestHeight + 1);
2183
- }
2184
- async function reconcileLiveMiningState(options) {
2185
- let state = {
2186
- ...options.state,
2187
- miningState: normalizeMiningStateRecord(options.state.miningState),
2188
- };
2189
- const currentTxid = state.miningState.currentTxid;
2190
- if (currentTxid === null || !miningPublishMayStillExist(state.miningState)) {
2191
- await reconcilePersistentPolicyLocks({
2192
- rpc: options.rpc,
2193
- walletName: state.managedCoreWallet.walletName,
2194
- state,
2195
- fixedInputs: [],
2196
- });
2197
- return {
2198
- state,
2199
- recentWin: null,
2200
- };
2201
- }
2202
- const walletName = state.managedCoreWallet.walletName;
2203
- const [mempoolVerbose, walletTx] = await Promise.all([
2204
- options.rpc.getRawMempoolVerbose().catch(() => ({
2205
- txids: [],
2206
- mempool_sequence: "unknown",
2207
- })),
2208
- options.rpc.getTransaction(walletName, currentTxid).catch(() => null),
2209
- ]);
2210
- const inMempool = mempoolVerbose.txids.includes(currentTxid);
2211
- if (walletTx !== null && walletTx.confirmations > 0) {
2212
- const recentWin = findRecentMiningWin(options.snapshotState ?? null, currentTxid, state.miningState.currentBlockTargetHeight);
2213
- state = {
2214
- ...state,
2215
- miningState: {
2216
- ...clearMiningPublishState(state.miningState),
2217
- currentPublishDecision: "tx-confirmed-while-down",
2218
- },
2219
- };
2220
- await reconcilePersistentPolicyLocks({
2221
- rpc: options.rpc,
2222
- walletName: state.managedCoreWallet.walletName,
2223
- state,
2224
- fixedInputs: [],
2225
- });
2226
- return {
2227
- state,
2228
- recentWin,
2229
- };
2230
- }
2231
- if (inMempool) {
2232
- const stale = !miningCandidateIsCurrent({
2233
- state: state.miningState,
2234
- nodeBestHash: options.nodeBestHash,
2235
- nodeBestHeight: options.nodeBestHeight,
2236
- });
2237
- state = defaultMiningStatePatch(state, {
2238
- livePublishInMempool: true,
2239
- currentPublishState: "in-mempool",
2240
- state: stale
2241
- ? "paused-stale"
2242
- : state.miningState.runMode === "stopped"
2243
- ? "paused"
2244
- : "live",
2245
- pauseReason: stale
2246
- ? "stale-block-context"
2247
- : state.miningState.runMode === "stopped"
2248
- ? "user-stopped"
2249
- : null,
2250
- currentPublishDecision: stale ? "paused-stale-mempool" : "restored-live-publish",
2251
- });
2252
- await reconcilePersistentPolicyLocks({
2253
- rpc: options.rpc,
2254
- walletName: state.managedCoreWallet.walletName,
2255
- state,
2256
- fixedInputs: [],
2257
- });
2258
- return {
2259
- state,
2260
- recentWin: null,
2261
- };
2262
- }
2263
- if ((walletTx?.walletconflicts?.length ?? 0) > 0) {
2264
- state = defaultMiningStatePatch(state, {
2265
- state: "repair-required",
2266
- pauseReason: state.miningState.currentPublishState === "broadcast-unknown"
2267
- ? "broadcast-unknown-conflict"
2268
- : "wallet-conflict-observed",
2269
- livePublishInMempool: false,
2270
- currentPublishDecision: state.miningState.currentPublishState === "broadcast-unknown"
2271
- ? "repair-required-broadcast-conflict"
2272
- : "repair-required-wallet-conflict",
2273
- });
2274
- await reconcilePersistentPolicyLocks({
2275
- rpc: options.rpc,
2276
- walletName: state.managedCoreWallet.walletName,
2277
- state,
2278
- fixedInputs: [],
2279
- });
2280
- return {
2281
- state,
2282
- recentWin: null,
2283
- };
2284
- }
2285
- state = defaultMiningStatePatch(state, {
2286
- ...clearMiningPublishState(state.miningState),
2287
- currentPublishDecision: state.miningState.currentPublishState === "broadcast-unknown"
2288
- ? "broadcast-unknown-not-seen"
2289
- : "live-publish-not-seen",
2290
- });
2291
- await reconcilePersistentPolicyLocks({
2292
- rpc: options.rpc,
2293
- walletName: state.managedCoreWallet.walletName,
2294
- state,
2295
- fixedInputs: [],
2296
- });
2297
- return {
2298
- state,
2299
- recentWin: null,
2300
- };
2301
- }
2302
- async function publishCandidateOnce(options) {
2303
- const service = await options.attachService({
2304
- dataDir: options.dataDir,
2305
- chain: "main",
2306
- startHeight: 0,
2307
- walletRootId: options.readContext.localState.state.walletRootId,
2308
- });
2309
- const rpc = options.rpcFactory(service.rpc);
2310
- let state = (await reconcileLiveMiningState({
2311
- state: options.readContext.localState.state,
2312
- rpc,
2313
- nodeBestHash: options.readContext.nodeStatus?.nodeBestHashHex ?? null,
2314
- nodeBestHeight: options.readContext.nodeStatus?.nodeBestHeight ?? null,
2315
- snapshotState: options.readContext.snapshot.state,
2316
- })).state;
2317
- const allUtxos = await rpc.listUnspent(state.managedCoreWallet.walletName, MINING_FUNDING_MIN_CONF);
2318
- const conflictOutpoint = resolveMiningConflictOutpoint({
2319
- state,
2320
- allUtxos,
2321
- });
2322
- const priorMiningState = cloneMiningState(state.miningState);
2323
- if (livePublishTargetsCandidateTip({
2324
- liveState: state.miningState,
2325
- candidate: options.candidate,
2326
- })) {
2327
- return {
2328
- state: defaultMiningStatePatch(state, {
2329
- currentPublishDecision: "kept-live-publish",
2330
- }),
2331
- txid: state.miningState.currentTxid,
2332
- decision: "kept-live-publish",
2333
- };
2334
- }
2335
- const feeSelection = await resolveWalletMutationFeeSelection({
2336
- rpc,
2337
- });
2338
- const nextFeeRate = feeSelection.feeRateSatVb;
2339
- const plan = createMiningPlan({
2340
- state,
2341
- candidate: options.candidate,
2342
- conflictOutpoint,
2343
- allUtxos,
2344
- feeRateSatVb: nextFeeRate,
2345
- });
2346
- const built = await buildMiningTransaction({
2347
- rpc,
2348
- walletName: state.managedCoreWallet.walletName,
2349
- state,
2350
- plan,
2351
- });
2352
- const intentFingerprintHex = computeIntentFingerprint(state, options.candidate);
2353
- state = defaultMiningStatePatch(state, {
2354
- state: "live",
2355
- currentPublishState: "broadcasting",
2356
- currentDomain: options.candidate.domainName,
2357
- currentDomainId: options.candidate.domainId,
2358
- currentDomainIndex: options.candidate.localIndex,
2359
- currentSenderScriptPubKeyHex: options.candidate.sender.scriptPubKeyHex,
2360
- currentTxid: built.txid,
2361
- currentWtxid: built.wtxid,
2362
- currentFeeRateSatVb: nextFeeRate,
2363
- currentAbsoluteFeeSats: numberToSats(built.funded.fee).toString() === "0" ? 0 : Number(numberToSats(built.funded.fee)),
2364
- currentScore: options.candidate.canonicalBlend.toString(),
2365
- currentSentence: options.candidate.sentence,
2366
- currentEncodedSentenceBytesHex: Buffer.from(options.candidate.encodedSentenceBytes).toString("hex"),
2367
- currentBip39WordIndices: [...options.candidate.bip39WordIndices],
2368
- currentBlendSeedHex: Buffer.from(deriveBlendSeed(options.candidate.referencedBlockHashInternal)).toString("hex"),
2369
- currentBlockTargetHeight: options.candidate.targetBlockHeight,
2370
- currentReferencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
2371
- currentIntentFingerprintHex: intentFingerprintHex,
2372
- sharedMiningConflictOutpoint: conflictOutpoint,
2373
- livePublishInMempool: null,
2374
- currentPublishDecision: priorMiningState.currentTxid === null
2375
- ? "publishing"
2376
- : "replacing",
2377
- });
2378
- await saveWalletStatePreservingUnlock({
2379
- state,
2380
- provider: options.provider,
2381
- paths: options.paths,
2382
- });
2383
- try {
2384
- await rpc.sendRawTransaction(built.rawHex);
2385
- }
2386
- catch (error) {
2387
- if (isAlreadyAcceptedError(error)) {
2388
- state = defaultMiningStatePatch(state, {
2389
- currentPublishState: "in-mempool",
2390
- livePublishInMempool: true,
2391
- });
2392
- await saveWalletStatePreservingUnlock({
2393
- state,
2394
- provider: options.provider,
2395
- paths: options.paths,
2396
- });
2397
- await appendEvent(options.paths, createEvent(state.miningState.currentPublishDecision === "replacing" ? "tx-replaced" : "tx-broadcast", `Mining transaction ${built.txid} is already accepted by the local node.`, {
2398
- runId: options.runId,
2399
- targetBlockHeight: options.candidate.targetBlockHeight,
2400
- referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
2401
- domainId: options.candidate.domainId,
2402
- domainName: options.candidate.domainName,
2403
- txid: built.txid,
2404
- feeRateSatVb: nextFeeRate,
2405
- feeSats: numberToSats(built.funded.fee).toString(),
2406
- score: options.candidate.canonicalBlend.toString(),
2407
- }));
2408
- return {
2409
- state,
2410
- txid: built.txid,
2411
- decision: state.miningState.currentPublishDecision === "replacing"
2412
- ? "replaced"
2413
- : "broadcast",
2414
- };
2415
- }
2416
- if (isBroadcastUnknownError(error)) {
2417
- state = defaultMiningStatePatch(state, {
2418
- currentPublishState: "broadcast-unknown",
2419
- currentPublishDecision: "broadcast-unknown",
2420
- });
2421
- await saveWalletStatePreservingUnlock({
2422
- state,
2423
- provider: options.provider,
2424
- paths: options.paths,
2425
- });
2426
- await appendEvent(options.paths, createEvent("error", `Mining broadcast became uncertain for ${built.txid}.`, {
2427
- level: "warn",
2428
- runId: options.runId,
2429
- targetBlockHeight: options.candidate.targetBlockHeight,
2430
- referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
2431
- domainId: options.candidate.domainId,
2432
- domainName: options.candidate.domainName,
2433
- txid: built.txid,
2434
- feeRateSatVb: nextFeeRate,
2435
- feeSats: numberToSats(built.funded.fee).toString(),
2436
- score: options.candidate.canonicalBlend.toString(),
2437
- reason: "broadcast-unknown",
2438
- }));
2439
- return {
2440
- state,
2441
- txid: built.txid,
2442
- decision: "broadcast-unknown",
2443
- };
2444
- }
2445
- state = {
2446
- ...state,
2447
- miningState: cloneMiningState(priorMiningState),
2448
- };
2449
- await saveWalletStatePreservingUnlock({
2450
- state,
2451
- provider: options.provider,
2452
- paths: options.paths,
2453
- });
2454
- throw new MiningPublishRejectedError(error instanceof Error ? error.message : String(error), state);
40
+ detectedAtUnixMs;
41
+ constructor(detectedAtUnixMs) {
42
+ super("mining_runtime_resumed");
43
+ this.detectedAtUnixMs = detectedAtUnixMs;
2455
44
  }
2456
- const absoluteFeeSats = numberToSats(built.funded.fee);
2457
- const replacementCount = priorMiningState.currentTxid === null
2458
- ? priorMiningState.replacementCount
2459
- : priorMiningState.replacementCount + 1;
2460
- state = defaultMiningStatePatch(state, {
2461
- currentPublishState: "in-mempool",
2462
- livePublishInMempool: true,
2463
- currentPublishDecision: state.miningState.currentPublishDecision === "replacing"
2464
- ? "replaced"
2465
- : "broadcast",
2466
- replacementCount,
2467
- currentAbsoluteFeeSats: Number(absoluteFeeSats),
2468
- currentBlockFeeSpentSats: (BigInt(state.miningState.currentBlockFeeSpentSats) + absoluteFeeSats).toString(),
2469
- sessionFeeSpentSats: (BigInt(state.miningState.sessionFeeSpentSats) + absoluteFeeSats).toString(),
2470
- lifetimeFeeSpentSats: (BigInt(state.miningState.lifetimeFeeSpentSats) + absoluteFeeSats).toString(),
2471
- });
2472
- await saveWalletStatePreservingUnlock({
2473
- state,
2474
- provider: options.provider,
2475
- paths: options.paths,
2476
- });
2477
- await appendEvent(options.paths, createEvent(state.miningState.currentPublishDecision === "replaced"
2478
- ? "tx-replaced"
2479
- : "tx-broadcast", `${state.miningState.currentPublishDecision === "replaced"
2480
- ? "Replaced"
2481
- : "Broadcast"} mining transaction ${built.txid}.`, {
2482
- runId: options.runId,
2483
- targetBlockHeight: options.candidate.targetBlockHeight,
2484
- referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
2485
- domainId: options.candidate.domainId,
2486
- domainName: options.candidate.domainName,
2487
- txid: built.txid,
2488
- feeRateSatVb: nextFeeRate,
2489
- feeSats: absoluteFeeSats.toString(),
2490
- score: options.candidate.canonicalBlend.toString(),
2491
- }));
2492
- return {
2493
- state,
2494
- txid: built.txid,
2495
- decision: state.miningState.currentPublishDecision === "replaced"
2496
- ? "replaced"
2497
- : "broadcast",
2498
- };
2499
45
  }
2500
- async function publishCandidate(options) {
2501
- const publishAttempt = options.publishAttempt ?? publishCandidateOnce;
2502
- const appendEventFn = options.appendEventFn ?? appendEvent;
2503
- const createStaleCandidateSkipResult = async (state) => {
2504
- const note = createStaleMiningCandidateWaitingNote();
2505
- await appendEventFn(options.paths, createEvent("publish-skipped-stale-candidate", "Skipped mining publish for the current tip because the selected root domain is no longer locally mineable.", {
2506
- level: "warn",
2507
- runId: options.runId,
2508
- targetBlockHeight: options.candidate.targetBlockHeight,
2509
- referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
2510
- domainId: options.candidate.domainId,
2511
- domainName: options.candidate.domainName,
2512
- score: options.candidate.canonicalBlend.toString(),
2513
- reason: "candidate-unavailable",
2514
- }));
46
+ const defaultMiningSuspendScheduler = {
47
+ every(intervalMs, callback) {
48
+ const timer = setInterval(callback, intervalMs);
49
+ timer.unref?.();
2515
50
  return {
2516
- state,
2517
- txid: null,
2518
- decision: "publish-skipped-stale-candidate",
2519
- note,
2520
- skipped: true,
2521
- candidate: null,
51
+ clear() {
52
+ clearInterval(timer);
53
+ },
2522
54
  };
55
+ },
56
+ };
57
+ function refreshMiningSuspendDetector(detector) {
58
+ if (detector === undefined) {
59
+ return;
60
+ }
61
+ const monotonicNow = detector.monotonicNow();
62
+ const gapMs = monotonicNow - detector.lastHeartbeatMonotonicMs;
63
+ detector.lastHeartbeatMonotonicMs = monotonicNow;
64
+ if (gapMs > MINING_SUSPEND_GAP_THRESHOLD_MS
65
+ && detector.detectedAtUnixMs === null) {
66
+ detector.detectedAtUnixMs = detector.nowUnixMs();
67
+ }
68
+ }
69
+ function createMiningSuspendDetector(options = {}) {
70
+ const monotonicNow = options.monotonicNow ?? (() => performance.now());
71
+ const nowUnixMs = options.nowUnixMs ?? Date.now;
72
+ const scheduler = options.scheduler ?? defaultMiningSuspendScheduler;
73
+ let heartbeat = null;
74
+ const detector = {
75
+ lastHeartbeatMonotonicMs: monotonicNow(),
76
+ detectedAtUnixMs: null,
77
+ monotonicNow,
78
+ nowUnixMs,
79
+ stop() {
80
+ heartbeat?.clear();
81
+ heartbeat = null;
82
+ },
2523
83
  };
2524
- const lockedReadContext = await options.openReadContext({
2525
- dataDir: options.dataDir,
2526
- databasePath: options.databasePath,
2527
- secretProvider: options.provider,
2528
- walletControlLockHeld: true,
2529
- paths: options.paths,
84
+ heartbeat = scheduler.every(MINING_SUSPEND_HEARTBEAT_INTERVAL_MS, () => {
85
+ refreshMiningSuspendDetector(detector);
2530
86
  });
2531
- try {
2532
- if (lockedReadContext.localState.availability !== "ready"
2533
- || lockedReadContext.localState.state === null
2534
- || lockedReadContext.snapshot === null
2535
- || lockedReadContext.model === null) {
2536
- return await createStaleCandidateSkipResult(options.fallbackState);
2537
- }
2538
- const readyReadContext = lockedReadContext;
2539
- const refreshedCandidate = refreshMiningCandidateFromCurrentState(readyReadContext, options.candidate);
2540
- if (refreshedCandidate === null) {
2541
- return await createStaleCandidateSkipResult(readyReadContext.localState.state);
2542
- }
2543
- try {
2544
- const published = await publishAttempt({
2545
- readContext: readyReadContext,
2546
- candidate: refreshedCandidate,
2547
- dataDir: options.dataDir,
2548
- provider: options.provider,
2549
- paths: options.paths,
2550
- attachService: options.attachService,
2551
- rpcFactory: options.rpcFactory,
2552
- runId: options.runId,
2553
- });
2554
- return {
2555
- ...published,
2556
- candidate: refreshedCandidate,
2557
- };
2558
- }
2559
- catch (error) {
2560
- if (error instanceof Error && error.message === "wallet_mining_mempool_rejected_missing-inputs") {
2561
- const note = createRetryableMiningPublishWaitingNote();
2562
- const revertedState = error instanceof MiningPublishRejectedError
2563
- ? error.revertedState
2564
- : readyReadContext.localState.state;
2565
- await appendEventFn(options.paths, createEvent("publish-retry-pending", "Selected mining candidate did not reach mempool and will be retried on the current tip with refreshed wallet state.", {
2566
- level: "warn",
2567
- runId: options.runId,
2568
- targetBlockHeight: refreshedCandidate.targetBlockHeight,
2569
- referencedBlockHashDisplay: refreshedCandidate.referencedBlockHashDisplay,
2570
- domainId: refreshedCandidate.domainId,
2571
- domainName: refreshedCandidate.domainName,
2572
- score: refreshedCandidate.canonicalBlend.toString(),
2573
- reason: "missing-inputs",
2574
- }));
2575
- return {
2576
- state: revertedState,
2577
- txid: null,
2578
- decision: "publish-retry-pending",
2579
- note,
2580
- retryable: true,
2581
- candidate: refreshedCandidate,
2582
- };
2583
- }
2584
- if (isInsufficientFundsError(error)) {
2585
- const note = createInsufficientFundsMiningPublishWaitingNote();
2586
- const lastError = createInsufficientFundsMiningPublishErrorMessage();
2587
- await appendEventFn(options.paths, createEvent("publish-paused-insufficient-funds", "Paused mining publish because Bitcoin Core could not fund the next mining transaction with safe BTC.", {
2588
- level: "warn",
2589
- runId: options.runId,
2590
- targetBlockHeight: refreshedCandidate.targetBlockHeight,
2591
- referencedBlockHashDisplay: refreshedCandidate.referencedBlockHashDisplay,
2592
- domainId: refreshedCandidate.domainId,
2593
- domainName: refreshedCandidate.domainName,
2594
- score: refreshedCandidate.canonicalBlend.toString(),
2595
- reason: "insufficient-funds",
2596
- }));
2597
- return {
2598
- state: readyReadContext.localState.state,
2599
- txid: null,
2600
- decision: "publish-paused-insufficient-funds",
2601
- note,
2602
- lastError,
2603
- skipped: true,
2604
- candidate: null,
2605
- };
2606
- }
2607
- throw error;
2608
- }
87
+ return detector;
88
+ }
89
+ function throwIfMiningSuspendDetected(detector) {
90
+ if (detector === undefined) {
91
+ return;
2609
92
  }
2610
- finally {
2611
- await lockedReadContext.close();
93
+ refreshMiningSuspendDetector(detector);
94
+ if (detector.detectedAtUnixMs === null) {
95
+ return;
96
+ }
97
+ const detectedAtUnixMs = detector.detectedAtUnixMs;
98
+ detector.detectedAtUnixMs = null;
99
+ throw new MiningSuspendDetectedError(detectedAtUnixMs);
100
+ }
101
+ function stopMiningSuspendDetector(detector) {
102
+ detector?.stop();
103
+ }
104
+ function clearMiningGateCache(walletRootId) {
105
+ clearMiningGateCacheModule(walletRootId);
106
+ }
107
+ function sleep(ms, signal) {
108
+ return new Promise((resolve) => {
109
+ const timer = setTimeout(resolve, ms);
110
+ signal?.addEventListener("abort", () => {
111
+ clearTimeout(timer);
112
+ resolve();
113
+ }, { once: true });
114
+ });
115
+ }
116
+ function writeStdout(stream, line) {
117
+ if (stream === undefined) {
118
+ return;
2612
119
  }
120
+ stream.write(`${line}\n`);
121
+ }
122
+ function createEvent(kind, message, options = {}) {
123
+ return createMiningEventRecord(kind, message, options);
124
+ }
125
+ function createMiningLoopState() {
126
+ return createMiningRuntimeLoopState();
127
+ }
128
+ async function appendEvent(paths, event) {
129
+ await appendMiningEvent(paths.miningEventsPath, event);
130
+ }
131
+ function getIndexerTruthKey(readContext) {
132
+ return getIndexerTruthKeyModule(readContext);
133
+ }
134
+ async function ensureIndexerTruthIsCurrent(options) {
135
+ await ensureIndexerTruthIsCurrentModule(options);
136
+ }
137
+ function determineCorePublishState(info) {
138
+ return determineCorePublishStateModule(info);
139
+ }
140
+ async function generateCandidatesForDomains(options) {
141
+ return await generateCandidatesForDomainsModule(options);
2613
142
  }
2614
- export async function publishCandidateForTesting(options) {
2615
- return await publishCandidate(options);
143
+ async function chooseBestLocalCandidate(candidates) {
144
+ return await chooseBestLocalCandidateModule(candidates);
145
+ }
146
+ async function runCompetitivenessGate(options) {
147
+ return await runCompetitivenessGateModule(options);
148
+ }
149
+ async function reconcileLiveMiningState(options) {
150
+ return await reconcileLiveMiningStateModule(options);
2616
151
  }
2617
152
  export async function ensureBuiltInMiningSetupIfNeeded(options) {
2618
153
  const config = await loadClientConfig({
@@ -2654,7 +189,7 @@ async function performMiningCycle(options) {
2654
189
  lastError: null,
2655
190
  }
2656
191
  : overrides;
2657
- return await refreshAndSaveStatus({
192
+ return await refreshAndSaveMiningRuntimeStatus({
2658
193
  paths: options.paths,
2659
194
  provider: options.provider,
2660
195
  readContext,
@@ -2786,524 +321,103 @@ async function performMiningCycle(options) {
2786
321
  if (preemptionRequest !== null) {
2787
322
  clearMiningProviderWait(options.loopState);
2788
323
  const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
2789
- state: effectiveReadContext.localState.state.miningState.livePublishInMempool
2790
- && effectiveReadContext.localState.state.miningState.state === "paused-stale"
2791
- ? "paused-stale"
2792
- : "paused",
2793
- pauseReason: preemptionRequest.reason,
2794
- });
2795
- await saveWalletStatePreservingUnlock({
2796
- state: nextState,
2797
- provider: options.provider,
2798
- paths: options.paths,
2799
- });
2800
- await saveCycleStatus({
2801
- ...effectiveReadContext,
2802
- localState: {
2803
- ...effectiveReadContext.localState,
2804
- state: nextState,
2805
- },
2806
- }, {
2807
- runMode: options.runMode,
2808
- currentPhase: "waiting",
2809
- lastError: null,
2810
- note: "Mining is paused while another wallet command is preempting sentence generation.",
2811
- });
2812
- return;
2813
- }
2814
- const [blockchainInfo, networkInfo, mempoolInfo] = await Promise.all([
2815
- rpc.getBlockchainInfo(),
2816
- rpc.getNetworkInfo(),
2817
- rpc.getMempoolInfo(),
2818
- ]);
2819
- throwIfMiningSuspendDetected(options.suspendDetector);
2820
- const corePublishState = determineCorePublishState({
2821
- blockchain: blockchainInfo,
2822
- network: networkInfo,
2823
- mempool: mempoolInfo,
2824
- });
2825
- clearRecoveredBitcoindError = resetMiningBitcoindRecoveryState(options.loopState, effectiveReadContext.nodeStatus?.serviceStatus ?? { pid: service.pid });
2826
- if (corePublishState !== "healthy") {
2827
- clearMiningProviderWait(options.loopState);
2828
- await saveCycleStatus(effectiveReadContext, {
2829
- runMode: options.runMode,
2830
- currentPhase: "waiting-bitcoin-network",
2831
- corePublishState,
2832
- note: "Mining is waiting for the local Bitcoin node to become publishable.",
2833
- });
2834
- return;
2835
- }
2836
- if (effectiveReadContext.indexer.health !== "synced" || effectiveReadContext.nodeHealth !== "synced") {
2837
- clearMiningProviderWait(options.loopState);
2838
- await saveCycleStatus(effectiveReadContext, {
2839
- runMode: options.runMode,
2840
- currentPhase: effectiveReadContext.indexer.health !== "synced"
2841
- ? "waiting-indexer"
2842
- : "waiting-bitcoin-network",
2843
- note: effectiveReadContext.indexer.health !== "synced"
2844
- ? "Mining is waiting for Bitcoin Core and the indexer to align."
2845
- : "Mining is waiting for the local Bitcoin node to become publishable.",
2846
- });
2847
- return;
2848
- }
2849
- if (targetBlockHeight === null) {
2850
- clearMiningProviderWait(options.loopState);
2851
- await saveCycleStatus(effectiveReadContext, {
2852
- runMode: options.runMode,
2853
- currentPhase: "waiting-bitcoin-network",
2854
- note: "Mining is waiting for the local Bitcoin node to become publishable.",
2855
- });
2856
- return;
2857
- }
2858
- if (getBlockRewardCogtoshi(targetBlockHeight) === 0n) {
2859
- clearMiningProviderWait(options.loopState);
2860
- const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
2861
- state: "paused",
2862
- pauseReason: "zero-reward",
2863
- });
2864
- await saveWalletStatePreservingUnlock({
2865
- state: nextState,
2866
- provider: options.provider,
2867
- paths: options.paths,
2868
- });
2869
- await saveCycleStatus({
2870
- ...effectiveReadContext,
2871
- localState: {
2872
- ...effectiveReadContext.localState,
2873
- state: nextState,
2874
- },
2875
- }, {
2876
- runMode: options.runMode,
2877
- currentPhase: "idle",
2878
- currentPublishDecision: "publish-skipped-zero-reward",
2879
- lastError: null,
2880
- note: "Mining is disabled because the target block reward is zero.",
2881
- });
2882
- await appendEvent(options.paths, createEvent("publish-skipped-zero-reward", "Skipped mining because the target block reward is zero.", {
2883
- targetBlockHeight,
2884
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
2885
- runId: options.backgroundWorkerRunId,
2886
- }));
2887
- return;
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
- }
2916
- if (tipKey !== null && options.loopState.attemptedTipKey === tipKey) {
2917
- await saveCycleStatus(effectiveReadContext, {
2918
- runMode: options.runMode,
2919
- currentPhase: "waiting",
2920
- lastError: null,
2921
- note: options.loopState.waitingNote ?? "Waiting for the next block after the last mining attempt on this tip.",
2922
- });
2923
- return;
2924
- }
2925
- const indexerTruthKey = getIndexerTruthKey(effectiveReadContext);
2926
- const walletRootId = effectiveReadContext.localState.walletRootId;
2927
- const ensureCurrentIndexerTruthOrRestart = async () => {
2928
- try {
2929
- await ensureIndexerTruthIsCurrent({
2930
- dataDir: effectiveReadContext.dataDir,
2931
- truthKey: indexerTruthKey,
2932
- });
2933
- return true;
2934
- }
2935
- catch (error) {
2936
- if (!(error instanceof Error) || error.message !== "mining_generation_stale_indexer_truth") {
2937
- throw error;
2938
- }
2939
- clearMiningGateCache(walletRootId);
2940
- await appendEvent(options.paths, createEvent("generation-restarted-indexer-truth", "Detected updated coherent indexer truth during mining; restarting on the next tick.", {
2941
- level: "warn",
2942
- targetBlockHeight,
2943
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
2944
- runId: options.backgroundWorkerRunId,
2945
- }));
2946
- return false;
2947
- }
2948
- };
2949
- let selectedCandidate = getSelectedCandidateForTip(options.loopState, tipKey);
2950
- let gateSnapshot = {
2951
- higherRankedCompetitorDomainCount: 0,
2952
- dedupedCompetitorDomainCount: 0,
2953
- mempoolSequenceCacheStatus: null,
2954
- lastMempoolSequence: null,
2955
- };
2956
- if (selectedCandidate === null) {
2957
- const domains = resolveEligibleAnchoredRoots(effectiveReadContext);
2958
- if (domains.length === 0) {
2959
- clearMiningProviderWait(options.loopState);
2960
- await saveCycleStatus(effectiveReadContext, {
2961
- runMode: options.runMode,
2962
- currentPhase: "idle",
2963
- lastError: null,
2964
- note: "No locally controlled anchored root domains are currently eligible to mine.",
2965
- });
2966
- return;
2967
- }
2968
- await saveCycleStatus(effectiveReadContext, {
2969
- runMode: options.runMode,
2970
- currentPhase: "generating",
2971
- lastError: null,
2972
- note: "Generating mining sentences for eligible root domains.",
2973
- });
2974
- await appendEvent(options.paths, createEvent("sentence-generation-start", "Started mining sentence generation.", {
2975
- targetBlockHeight,
2976
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
2977
- runId: options.backgroundWorkerRunId,
2978
- }));
2979
- let candidates;
2980
- try {
2981
- candidates = await generateCandidatesForDomainsImpl({
2982
- rpc,
2983
- readContext: effectiveReadContext,
2984
- domains,
2985
- provider: options.provider,
2986
- paths: options.paths,
2987
- indexerTruthKey,
2988
- runId: options.backgroundWorkerRunId,
2989
- fetchImpl: options.fetchImpl,
2990
- });
2991
- throwIfMiningSuspendDetected(options.suspendDetector);
2992
- }
2993
- catch (error) {
2994
- if (error instanceof MiningProviderRequestError) {
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) {
3009
- options.loopState.attemptedTipKey = tipKey;
3010
- }
3011
- await saveCycleStatus(effectiveReadContext, {
3012
- runMode: options.runMode,
3013
- currentPhase: "waiting-provider",
3014
- providerState: options.loopState.providerWaitState ?? error.providerState,
3015
- lastError: error.message,
3016
- note: "Mining is waiting for the sentence provider to recover.",
3017
- });
3018
- await appendEvent(options.paths, createEvent("publish-paused-provider", error.message, {
3019
- level: "warn",
3020
- targetBlockHeight,
3021
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
3022
- runId: options.backgroundWorkerRunId,
3023
- }));
3024
- return;
3025
- }
3026
- if (error instanceof Error && error.message === "mining_generation_stale_tip") {
3027
- await appendEvent(options.paths, createEvent("generation-restarted-new-tip", "Detected a new best tip during sentence generation; restarting on the next tick.", {
3028
- level: "warn",
3029
- targetBlockHeight,
3030
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
3031
- runId: options.backgroundWorkerRunId,
3032
- }));
3033
- return;
3034
- }
3035
- if (error instanceof Error && error.message === "mining_generation_stale_indexer_truth") {
3036
- clearMiningProviderWait(options.loopState);
3037
- clearMiningGateCache(walletRootId);
3038
- await appendEvent(options.paths, createEvent("generation-restarted-indexer-truth", "Detected updated coherent indexer truth during mining; restarting on the next tick.", {
3039
- level: "warn",
3040
- targetBlockHeight,
3041
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
3042
- runId: options.backgroundWorkerRunId,
3043
- }));
3044
- return;
3045
- }
3046
- if (error instanceof Error && error.message === "mining_generation_preempted") {
3047
- clearMiningProviderWait(options.loopState);
3048
- await appendEvent(options.paths, createEvent("generation-paused-preempted", "Stopped sentence generation because another wallet command requested mining preemption.", {
3049
- level: "warn",
3050
- targetBlockHeight,
3051
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
3052
- runId: options.backgroundWorkerRunId,
3053
- }));
3054
- return;
3055
- }
3056
- clearMiningProviderWait(options.loopState);
3057
- const failureMessage = error instanceof Error ? error.message : String(error);
3058
- if (tipKey !== null) {
3059
- options.loopState.attemptedTipKey = tipKey;
3060
- options.loopState.waitingNote = "Mining sentence generation failed for the current tip.";
3061
- }
3062
- await saveCycleStatus(effectiveReadContext, {
3063
- runMode: options.runMode,
3064
- currentPhase: "waiting-provider",
3065
- providerState: "unavailable",
3066
- lastError: failureMessage,
3067
- note: "Mining sentence generation failed for the current tip.",
3068
- });
3069
- await appendEvent(options.paths, createEvent("sentence-generation-failed", failureMessage, {
3070
- level: "error",
3071
- targetBlockHeight,
3072
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
3073
- runId: options.backgroundWorkerRunId,
3074
- }));
3075
- return;
3076
- }
3077
- clearMiningProviderWait(options.loopState);
3078
- await saveCycleStatus(effectiveReadContext, {
324
+ state: effectiveReadContext.localState.state.miningState.livePublishInMempool
325
+ && effectiveReadContext.localState.state.miningState.state === "paused-stale"
326
+ ? "paused-stale"
327
+ : "paused",
328
+ pauseReason: preemptionRequest.reason,
329
+ });
330
+ await saveWalletStatePreservingUnlock({
331
+ state: nextState,
332
+ provider: options.provider,
333
+ paths: options.paths,
334
+ });
335
+ await saveCycleStatus({
336
+ ...effectiveReadContext,
337
+ localState: {
338
+ ...effectiveReadContext.localState,
339
+ state: nextState,
340
+ },
341
+ }, {
3079
342
  runMode: options.runMode,
3080
- currentPhase: "scoring",
343
+ currentPhase: "waiting",
3081
344
  lastError: null,
3082
- note: "Scoring mining candidates for the current tip.",
3083
- });
3084
- const best = await chooseBestLocalCandidate(candidates);
3085
- if (best === null) {
3086
- if (tipKey !== null) {
3087
- options.loopState.attemptedTipKey = tipKey;
3088
- options.loopState.waitingNote = "No publishable mining candidate passed scoring gates for the current tip.";
3089
- }
3090
- clearSelectedCandidate(options.loopState);
3091
- await saveCycleStatus(effectiveReadContext, {
3092
- runMode: options.runMode,
3093
- currentPhase: "idle",
3094
- currentPublishDecision: "publish-skipped-no-candidate",
3095
- note: "No publishable mining candidate passed scoring gates for the current tip.",
3096
- });
3097
- await appendEvent(options.paths, createEvent("publish-skipped-no-candidate", "No publishable mining candidate passed scoring gates.", {
3098
- targetBlockHeight,
3099
- referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
3100
- runId: options.backgroundWorkerRunId,
3101
- }));
3102
- return;
3103
- }
3104
- if (!await ensureCurrentIndexerTruthOrRestart()) {
3105
- return;
3106
- }
3107
- options.loopState.ui.recentWin = null;
3108
- cacheSelectedCandidateForTip(options.loopState, tipKey, best, effectiveReadContext.localState.state.miningState);
3109
- selectedCandidate = best;
3110
- await appendEvent(options.paths, createEvent("candidate-selected", `Selected ${best.domainName} with score ${best.canonicalBlend.toString()}.`, {
3111
- targetBlockHeight: best.targetBlockHeight,
3112
- referencedBlockHashDisplay: best.referencedBlockHashDisplay,
3113
- domainId: best.domainId,
3114
- domainName: best.domainName,
3115
- score: best.canonicalBlend.toString(),
3116
- runId: options.backgroundWorkerRunId,
3117
- }));
3118
- const gate = await runCompetitivenessGateImpl({
3119
- rpc,
3120
- readContext: effectiveReadContext,
3121
- candidate: best,
3122
- currentTxid: effectiveReadContext.localState.state.miningState.currentTxid,
3123
- assaySentencesImpl: options.assaySentencesImpl,
3124
- cooperativeYield: options.cooperativeYieldImpl,
3125
- cooperativeYieldEvery: options.cooperativeYieldEvery,
345
+ note: "Mining is paused while another wallet command is preempting sentence generation.",
3126
346
  });
3127
- throwIfMiningSuspendDetected(options.suspendDetector);
3128
- gateSnapshot = {
3129
- higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
3130
- dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
3131
- mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
3132
- lastMempoolSequence: gate.lastMempoolSequence,
3133
- };
3134
- if (!gate.allowed) {
3135
- if (tipKey !== null) {
3136
- options.loopState.attemptedTipKey = tipKey;
3137
- }
3138
- clearSelectedCandidate(options.loopState);
3139
- setMiningUiCandidate(options.loopState, best, effectiveReadContext.localState.state.miningState);
3140
- options.loopState.waitingNote = gate.decision === "suppressed-same-domain-mempool"
3141
- ? "Best local sentence found, but a same-domain mempool competitor already matches or beats it."
3142
- : gate.decision === "suppressed-top5-mempool"
3143
- ? `Best local sentence found, but ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
3144
- : "Mining skipped this tick because the mempool competitiveness gate could not be verified safely.";
3145
- await saveCycleStatus(effectiveReadContext, {
3146
- runMode: options.runMode,
3147
- currentPhase: "waiting",
3148
- currentPublishDecision: gate.decision,
3149
- sameDomainCompetitorSuppressed: gate.sameDomainCompetitorSuppressed,
3150
- higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
3151
- dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
3152
- competitivenessGateIndeterminate: gate.competitivenessGateIndeterminate,
3153
- mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
3154
- lastMempoolSequence: gate.lastMempoolSequence,
3155
- lastCompetitivenessGateAtUnixMs: now(),
3156
- note: options.loopState.waitingNote,
3157
- });
3158
- await appendEvent(options.paths, createEvent(gate.decision === "suppressed-same-domain-mempool"
3159
- ? "publish-skipped-same-domain-mempool"
3160
- : gate.decision === "suppressed-top5-mempool"
3161
- ? "publish-skipped-top5-mempool"
3162
- : "publish-skipped-gate-indeterminate", gate.decision === "suppressed-same-domain-mempool"
3163
- ? "Skipped publish because a same-domain mempool competitor already outranks the local candidate."
3164
- : gate.decision === "suppressed-top5-mempool"
3165
- ? `Skipped publish because ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
3166
- : "Skipped publish because the competitiveness gate could not be evaluated safely.", {
3167
- targetBlockHeight: best.targetBlockHeight,
3168
- referencedBlockHashDisplay: best.referencedBlockHashDisplay,
3169
- domainId: best.domainId,
3170
- domainName: best.domainName,
3171
- score: best.canonicalBlend.toString(),
3172
- runId: options.backgroundWorkerRunId,
3173
- reason: gate.decision,
3174
- }));
3175
- return;
3176
- }
3177
- }
3178
- else {
3179
- options.loopState.ui.recentWin = null;
3180
- setMiningUiCandidate(options.loopState, selectedCandidate, effectiveReadContext.localState.state.miningState);
3181
- }
3182
- if (!await ensureCurrentIndexerTruthOrRestart()) {
3183
347
  return;
3184
348
  }
3185
- await saveCycleStatus(effectiveReadContext, {
3186
- runMode: options.runMode,
3187
- ...buildPrePublishStatusOverrides({
3188
- state: effectiveReadContext.localState.state,
3189
- candidate: selectedCandidate,
3190
- }),
3191
- });
3192
- const publishLock = await acquireFileLock(options.paths.walletControlLockPath, {
3193
- purpose: "wallet-mine",
3194
- walletRootId: effectiveReadContext.localState.state.walletRootId,
349
+ const [blockchainInfo, networkInfo, mempoolInfo] = await Promise.all([
350
+ rpc.getBlockchainInfo(),
351
+ rpc.getNetworkInfo(),
352
+ rpc.getMempoolInfo(),
353
+ ]);
354
+ throwIfMiningSuspendDetected(options.suspendDetector);
355
+ const corePublishState = determineCorePublishState({
356
+ blockchain: blockchainInfo,
357
+ network: networkInfo,
358
+ mempool: mempoolInfo,
3195
359
  });
3196
- try {
3197
- if (!await ensureCurrentIndexerTruthOrRestart()) {
3198
- return;
3199
- }
3200
- throwIfMiningSuspendDetected(options.suspendDetector);
3201
- const published = await publishCandidate({
3202
- dataDir: options.dataDir,
3203
- databasePath: options.databasePath,
360
+ clearRecoveredBitcoindError = resetMiningBitcoindRecoveryState(options.loopState, effectiveReadContext.nodeStatus?.serviceStatus ?? { pid: service.pid });
361
+ if (targetBlockHeight !== null && getBlockRewardCogtoshi(targetBlockHeight) === 0n) {
362
+ clearMiningProviderWait(options.loopState);
363
+ const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
364
+ state: "paused",
365
+ pauseReason: "zero-reward",
366
+ });
367
+ await saveWalletStatePreservingUnlock({
368
+ state: nextState,
3204
369
  provider: options.provider,
3205
370
  paths: options.paths,
3206
- fallbackState: effectiveReadContext.localState.state,
3207
- openReadContext: options.openReadContext,
3208
- attachService: options.attachService,
3209
- rpcFactory: options.rpcFactory,
3210
- candidate: selectedCandidate,
3211
- runId: options.backgroundWorkerRunId,
3212
371
  });
3213
- if (tipKey !== null && published.retryable !== true) {
3214
- options.loopState.attemptedTipKey = tipKey;
3215
- }
3216
- if (published.retryable === true) {
3217
- cacheSelectedCandidateForTip(options.loopState, tipKey, published.candidate, published.state.miningState);
3218
- options.loopState.waitingNote = published.note;
3219
- await saveCycleStatus({
3220
- ...effectiveReadContext,
3221
- localState: {
3222
- ...effectiveReadContext.localState,
3223
- state: published.state,
3224
- },
3225
- }, {
3226
- runMode: options.runMode,
3227
- currentPhase: "waiting",
3228
- currentPublishDecision: published.decision,
3229
- sameDomainCompetitorSuppressed: false,
3230
- higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
3231
- dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
3232
- competitivenessGateIndeterminate: false,
3233
- mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
3234
- lastMempoolSequence: gateSnapshot.lastMempoolSequence,
3235
- lastCompetitivenessGateAtUnixMs: now(),
3236
- note: published.note,
3237
- livePublishInMempool: published.state.miningState.livePublishInMempool,
3238
- });
3239
- return;
3240
- }
3241
- if (published.skipped === true) {
3242
- clearSelectedCandidate(options.loopState);
3243
- setMiningUiCandidate(options.loopState, selectedCandidate, published.state.miningState);
3244
- options.loopState.waitingNote = published.note;
3245
- const lastError = published.decision === "publish-paused-insufficient-funds"
3246
- ? published.lastError ?? createInsufficientFundsMiningPublishErrorMessage()
3247
- : undefined;
3248
- await saveCycleStatus({
3249
- ...effectiveReadContext,
3250
- localState: {
3251
- ...effectiveReadContext.localState,
3252
- state: published.state,
3253
- },
3254
- }, {
3255
- runMode: options.runMode,
3256
- currentPhase: "waiting",
3257
- currentPublishDecision: published.decision,
3258
- sameDomainCompetitorSuppressed: false,
3259
- higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
3260
- dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
3261
- competitivenessGateIndeterminate: false,
3262
- mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
3263
- lastMempoolSequence: gateSnapshot.lastMempoolSequence,
3264
- lastCompetitivenessGateAtUnixMs: now(),
3265
- lastError,
3266
- note: published.note,
3267
- livePublishInMempool: published.state.miningState.livePublishInMempool,
3268
- });
3269
- return;
3270
- }
3271
- clearSelectedCandidate(options.loopState);
3272
- if (published.txid !== null) {
3273
- options.loopState.ui.latestTxid = published.txid;
3274
- }
3275
- setMiningUiCandidate(options.loopState, published.candidate, published.state.miningState);
3276
- options.loopState.waitingNote = published.decision === "kept-live-publish"
3277
- ? "Existing live mining publish already covers this block attempt. Waiting for the next block."
3278
- : published.txid === null
3279
- ? "Mining candidate was evaluated but the existing live publish stayed in place."
3280
- : `Mining candidate ${published.decision === "replaced"
3281
- ? "replaced"
3282
- : "broadcast"} as ${published.txid}. Waiting for the next block.`;
3283
372
  await saveCycleStatus({
3284
373
  ...effectiveReadContext,
3285
374
  localState: {
3286
375
  ...effectiveReadContext.localState,
3287
- state: published.state,
376
+ state: nextState,
3288
377
  },
3289
378
  }, {
3290
379
  runMode: options.runMode,
3291
- currentPhase: "waiting",
3292
- currentPublishDecision: published.decision,
3293
- sameDomainCompetitorSuppressed: false,
3294
- higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
3295
- dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
3296
- competitivenessGateIndeterminate: false,
3297
- mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
3298
- lastMempoolSequence: gateSnapshot.lastMempoolSequence,
3299
- lastCompetitivenessGateAtUnixMs: now(),
3300
- note: options.loopState.waitingNote,
3301
- livePublishInMempool: published.state.miningState.livePublishInMempool,
380
+ currentPhase: "idle",
381
+ currentPublishDecision: "publish-skipped-zero-reward",
382
+ lastError: null,
383
+ note: "Mining is disabled because the target block reward is zero.",
3302
384
  });
385
+ await appendEvent(options.paths, createEvent("publish-skipped-zero-reward", "Skipped mining because the target block reward is zero.", {
386
+ targetBlockHeight,
387
+ referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
388
+ runId: options.backgroundWorkerRunId,
389
+ }));
390
+ return;
3303
391
  }
3304
- finally {
3305
- await publishLock.release();
3306
- }
392
+ await runMiningPhaseMachine({
393
+ dataDir: options.dataDir,
394
+ databasePath: options.databasePath,
395
+ provider: options.provider,
396
+ paths: options.paths,
397
+ runMode: options.runMode,
398
+ backgroundWorkerRunId: options.backgroundWorkerRunId,
399
+ readContext: effectiveReadContext,
400
+ rpc,
401
+ targetBlockHeight,
402
+ tipKey,
403
+ corePublishState,
404
+ loopState: options.loopState,
405
+ openReadContext: options.openReadContext,
406
+ attachService: options.attachService,
407
+ rpcFactory: options.rpcFactory,
408
+ fetchImpl: options.fetchImpl,
409
+ generateCandidatesForDomainsImpl,
410
+ runCompetitivenessGateImpl,
411
+ assaySentencesImpl: options.assaySentencesImpl,
412
+ cooperativeYieldImpl: options.cooperativeYieldImpl,
413
+ cooperativeYieldEvery: options.cooperativeYieldEvery,
414
+ nowImpl: now,
415
+ saveCycleStatus: async (context, overrides) => await saveCycleStatus(context, overrides),
416
+ appendEvent: async (event) => await appendEvent(options.paths, event),
417
+ throwIfSuspendDetected: () => {
418
+ throwIfMiningSuspendDetected(options.suspendDetector);
419
+ },
420
+ });
3307
421
  }
3308
422
  catch (error) {
3309
423
  if (error instanceof MiningSuspendDetectedError) {
@@ -3352,93 +466,6 @@ async function performMiningCycle(options) {
3352
466
  }
3353
467
  }
3354
468
  }
3355
- async function saveStopSnapshot(options) {
3356
- const readContext = await openWalletReadContext({
3357
- dataDir: options.dataDir,
3358
- databasePath: options.databasePath,
3359
- secretProvider: options.provider,
3360
- paths: options.paths,
3361
- });
3362
- try {
3363
- let localState = readContext.localState;
3364
- if (localState.availability === "ready" && localState.state !== null) {
3365
- const service = await attachOrStartManagedBitcoindService({
3366
- dataDir: options.dataDir,
3367
- chain: "main",
3368
- startHeight: 0,
3369
- walletRootId: localState.state.walletRootId,
3370
- }).catch(() => null);
3371
- if (service !== null) {
3372
- const rpc = createRpcClient(service.rpc);
3373
- const reconciledState = (await reconcileLiveMiningState({
3374
- state: localState.state,
3375
- rpc,
3376
- nodeBestHash: readContext.nodeStatus?.nodeBestHashHex ?? null,
3377
- nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
3378
- snapshotState: readContext.snapshot?.state ?? null,
3379
- })).state;
3380
- const stopState = defaultMiningStatePatch(reconciledState, {
3381
- runMode: "stopped",
3382
- state: reconciledState.miningState.livePublishInMempool
3383
- ? reconciledState.miningState.state === "paused-stale"
3384
- ? "paused-stale"
3385
- : "paused"
3386
- : reconciledState.miningState.state === "repair-required"
3387
- ? "repair-required"
3388
- : "idle",
3389
- pauseReason: reconciledState.miningState.livePublishInMempool
3390
- ? reconciledState.miningState.state === "paused-stale"
3391
- ? "stale-block-context"
3392
- : "user-stopped"
3393
- : reconciledState.miningState.state === "repair-required"
3394
- ? reconciledState.miningState.pauseReason
3395
- : null,
3396
- });
3397
- await saveWalletStatePreservingUnlock({
3398
- state: stopState,
3399
- provider: options.provider,
3400
- paths: options.paths,
3401
- });
3402
- localState = {
3403
- ...localState,
3404
- state: stopState,
3405
- };
3406
- }
3407
- }
3408
- await refreshAndSaveStatus({
3409
- paths: options.paths,
3410
- provider: options.provider,
3411
- readContext: {
3412
- ...readContext,
3413
- localState,
3414
- },
3415
- overrides: {
3416
- runMode: "stopped",
3417
- backgroundWorkerPid: options.runMode === "background" ? null : options.backgroundWorkerPid,
3418
- backgroundWorkerRunId: options.runMode === "background" ? null : options.backgroundWorkerRunId,
3419
- backgroundWorkerHeartbeatAtUnixMs: options.runMode === "background" ? null : Date.now(),
3420
- currentPhase: "idle",
3421
- note: options.note,
3422
- },
3423
- });
3424
- }
3425
- finally {
3426
- await readContext.close();
3427
- }
3428
- }
3429
- async function attemptSaveMempool(rpc, paths, runId) {
3430
- try {
3431
- await rpc.saveMempool?.();
3432
- }
3433
- catch {
3434
- // ignore
3435
- }
3436
- finally {
3437
- await appendEvent(paths, createEvent("savemempool-attempted", "Attempted to persist the local mempool before stopping mining.", {
3438
- runId,
3439
- }));
3440
- }
3441
- }
3442
469
  async function runMiningLoop(options) {
3443
470
  const suspendDetector = createMiningSuspendDetector({
3444
471
  monotonicNow: options.suspendMonotonicNowImpl,
@@ -3496,7 +523,11 @@ async function runMiningLoop(options) {
3496
523
  walletRootId: undefined,
3497
524
  }).catch(() => null);
3498
525
  if (service !== null) {
3499
- await attemptSaveMempool(options.rpcFactory(service.rpc), options.paths, options.backgroundWorkerRunId);
526
+ await attemptSaveMempool({
527
+ rpc: options.rpcFactory(service.rpc),
528
+ paths: options.paths,
529
+ runId: options.backgroundWorkerRunId,
530
+ });
3500
531
  }
3501
532
  await appendEvent(options.paths, createEvent("runtime-stop", `Stopped ${options.runMode} mining runtime.`, {
3502
533
  runId: options.backgroundWorkerRunId,
@@ -3507,17 +538,7 @@ async function runMiningLoop(options) {
3507
538
  }
3508
539
  }
3509
540
  async function waitForBackgroundHealthy(paths) {
3510
- const deadline = Date.now() + BACKGROUND_START_TIMEOUT_MS;
3511
- while (Date.now() < deadline) {
3512
- const snapshot = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
3513
- if (snapshot !== null
3514
- && snapshot.runMode === "background"
3515
- && snapshot.backgroundWorkerHealth === "healthy") {
3516
- return snapshot;
3517
- }
3518
- await sleep(250);
3519
- }
3520
- return loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
541
+ return await waitForBackgroundHealthySupervisor(paths);
3521
542
  }
3522
543
  export async function runForegroundMining(options) {
3523
544
  if (!options.prompter.isInteractive) {
@@ -3529,9 +550,6 @@ export async function runForegroundMining(options) {
3529
550
  const attachService = options.attachService ?? attachOrStartManagedBitcoindService;
3530
551
  const rpcFactory = options.rpcFactory ?? createRpcClient;
3531
552
  const requestMiningPreemption = options.requestMiningPreemption ?? requestMiningGenerationPreemption;
3532
- const runMiningLoopImpl = options.runMiningLoopImpl ?? runMiningLoop;
3533
- const saveStopSnapshotImpl = options.saveStopSnapshotImpl ?? saveStopSnapshot;
3534
- let visualizer = null;
3535
553
  const setupReady = options.builtInSetupEnsured === true
3536
554
  ? true
3537
555
  : await ensureBuiltInMiningSetupIfNeeded({
@@ -3542,78 +560,41 @@ export async function runForegroundMining(options) {
3542
560
  if (!setupReady) {
3543
561
  throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
3544
562
  }
3545
- const controlLock = await acquireMiningStartControlLock({
3546
- paths,
3547
- purpose: "mine-foreground",
3548
- takeoverReason: "mine-foreground-replace",
3549
- requestMiningPreemption,
563
+ await runForegroundMiningSupervisor({
564
+ dataDir: options.dataDir,
565
+ databasePath: options.databasePath,
566
+ clientVersion: options.clientVersion,
567
+ updateAvailable: options.updateAvailable,
568
+ stdout: options.stdout,
569
+ stderr: options.stderr,
570
+ signal: options.signal,
571
+ progressOutput: options.progressOutput,
572
+ visualizer: options.visualizer,
573
+ fetchImpl: options.fetchImpl,
3550
574
  shutdownGraceMs: options.shutdownGraceMs,
3551
- sleepImpl: options.sleepImpl,
3552
- });
3553
- const abortController = new AbortController();
3554
- const abortListener = () => {
3555
- abortController.abort();
3556
- };
3557
- const handleSigint = () => abortController.abort();
3558
- const handleSigterm = () => abortController.abort();
3559
- try {
3560
- await takeOverMiningRuntime({
3561
- paths,
3562
- reason: "mine-foreground-replace",
3563
- requestMiningPreemption,
3564
- shutdownGraceMs: options.shutdownGraceMs,
3565
- sleepImpl: options.sleepImpl,
3566
- });
3567
- visualizer = new MiningFollowVisualizer({
3568
- clientVersion: options.clientVersion,
3569
- updateAvailable: options.updateAvailable,
3570
- progressOutput: options.progressOutput ?? "auto",
3571
- stream: options.stderr,
3572
- });
3573
- options.signal?.addEventListener("abort", abortListener, { once: true });
3574
- process.on("SIGINT", handleSigint);
3575
- process.on("SIGTERM", handleSigterm);
3576
- await runMiningLoopImpl({
3577
- dataDir: options.dataDir,
3578
- databasePath: options.databasePath,
575
+ runtime: {
3579
576
  provider,
3580
577
  paths,
3581
- runMode: "foreground",
3582
- backgroundWorkerPid: null,
3583
- backgroundWorkerRunId: null,
3584
- signal: abortController.signal,
3585
- fetchImpl: options.fetchImpl,
3586
578
  openReadContext,
3587
579
  attachService,
3588
580
  rpcFactory,
3589
- stdout: options.stdout,
3590
- visualizer,
3591
- });
3592
- await saveStopSnapshotImpl({
3593
- dataDir: options.dataDir,
3594
- databasePath: options.databasePath,
3595
- provider,
3596
- paths,
3597
- runMode: "foreground",
3598
- backgroundWorkerPid: null,
3599
- backgroundWorkerRunId: null,
3600
- note: "Foreground mining stopped cleanly.",
3601
- });
3602
- }
3603
- finally {
3604
- options.signal?.removeEventListener("abort", abortListener);
3605
- process.off("SIGINT", handleSigint);
3606
- process.off("SIGTERM", handleSigterm);
3607
- visualizer?.close();
3608
- await controlLock.release();
3609
- }
581
+ },
582
+ deps: {
583
+ requestMiningPreemption,
584
+ runMiningLoop: options.runMiningLoopImpl ?? runMiningLoop,
585
+ saveStopSnapshot: options.saveStopSnapshotImpl,
586
+ sleep: options.sleepImpl,
587
+ },
588
+ });
3610
589
  }
3611
590
  export async function startBackgroundMining(options) {
3612
591
  const provider = options.provider ?? createDefaultWalletSecretProvider();
3613
592
  const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
3614
593
  const requestMiningPreemption = options.requestMiningPreemption ?? requestMiningGenerationPreemption;
3615
- const spawnWorkerProcess = options.spawnWorkerProcess ?? spawn;
3616
594
  const waitForBackgroundHealthyImpl = options.waitForBackgroundHealthyImpl ?? waitForBackgroundHealthy;
595
+ const openReadContext = options.openReadContext ?? openWalletReadContext;
596
+ const attachService = options.attachService ?? attachOrStartManagedBitcoindService;
597
+ const rpcFactory = options.rpcFactory ?? createRpcClient;
3617
598
  const setupReady = options.builtInSetupEnsured === true
3618
599
  ? true
3619
600
  : await ensureBuiltInMiningSetupIfNeeded({
@@ -3624,109 +605,48 @@ export async function startBackgroundMining(options) {
3624
605
  if (!setupReady) {
3625
606
  throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
3626
607
  }
3627
- let controlLock;
3628
- try {
3629
- controlLock = await acquireMiningStartControlLock({
3630
- paths,
3631
- purpose: "mine-start",
3632
- takeoverReason: "mine-start-replace",
3633
- requestMiningPreemption,
3634
- shutdownGraceMs: options.shutdownGraceMs,
3635
- sleepImpl: options.sleepImpl,
3636
- });
3637
- }
3638
- catch (error) {
3639
- if (error instanceof FileLockBusyError && error.existingMetadata?.processId === process.pid) {
3640
- return {
3641
- started: false,
3642
- snapshot: await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null),
3643
- };
3644
- }
3645
- throw error;
3646
- }
3647
- try {
3648
- await takeOverMiningRuntime({
608
+ return await startBackgroundMiningSupervisor({
609
+ dataDir: options.dataDir,
610
+ databasePath: options.databasePath,
611
+ shutdownGraceMs: options.shutdownGraceMs,
612
+ waitForBackgroundHealthy: waitForBackgroundHealthyImpl,
613
+ runtime: {
614
+ provider,
3649
615
  paths,
3650
- reason: "mine-start-replace",
616
+ openReadContext,
617
+ attachService,
618
+ rpcFactory,
619
+ },
620
+ deps: {
3651
621
  requestMiningPreemption,
3652
- shutdownGraceMs: options.shutdownGraceMs,
3653
- sleepImpl: options.sleepImpl,
3654
- });
3655
- const runId = randomBytes(16).toString("hex");
3656
- const workerMainPath = fileURLToPath(new URL("./worker-main.js", import.meta.url));
3657
- const child = spawnWorkerProcess(process.execPath, [
3658
- workerMainPath,
3659
- `--data-dir=${options.dataDir}`,
3660
- `--database-path=${options.databasePath}`,
3661
- `--run-id=${runId}`,
3662
- ], {
3663
- detached: true,
3664
- stdio: "ignore",
3665
- });
3666
- child.unref();
3667
- const snapshot = await waitForBackgroundHealthyImpl(paths);
3668
- return {
3669
- started: true,
3670
- snapshot,
3671
- };
3672
- }
3673
- finally {
3674
- await controlLock.release();
3675
- }
622
+ spawnWorkerProcess: options.spawnWorkerProcess,
623
+ sleep: options.sleepImpl,
624
+ },
625
+ });
3676
626
  }
3677
627
  export async function stopBackgroundMining(options) {
3678
628
  const provider = options.provider ?? createDefaultWalletSecretProvider();
3679
629
  const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
3680
- const controlLock = await acquireFileLock(paths.miningControlLockPath, {
3681
- purpose: "mine-stop",
3682
- });
3683
- try {
3684
- const snapshot = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
3685
- if (snapshot === null || snapshot.runMode !== "background" || snapshot.backgroundWorkerPid === null) {
3686
- return snapshot;
3687
- }
3688
- const preemption = await requestMiningGenerationPreemption({
3689
- paths,
3690
- reason: "mine-stop",
3691
- timeoutMs: Math.min(MINING_SHUTDOWN_GRACE_MS, 15_000),
3692
- }).catch(() => null);
3693
- process.kill(snapshot.backgroundWorkerPid, "SIGTERM");
3694
- const deadline = Date.now() + MINING_SHUTDOWN_GRACE_MS;
3695
- while (Date.now() < deadline) {
3696
- try {
3697
- process.kill(snapshot.backgroundWorkerPid, 0);
3698
- await sleep(250);
3699
- }
3700
- catch (error) {
3701
- if (error instanceof Error && "code" in error && error.code === "ESRCH") {
3702
- break;
3703
- }
3704
- }
3705
- }
3706
- try {
3707
- process.kill(snapshot.backgroundWorkerPid, "SIGKILL");
3708
- }
3709
- catch {
3710
- // ignore
3711
- }
3712
- await saveStopSnapshot({
3713
- dataDir: options.dataDir,
3714
- databasePath: options.databasePath,
630
+ const openReadContext = options.openReadContext ?? openWalletReadContext;
631
+ const attachService = options.attachService ?? attachOrStartManagedBitcoindService;
632
+ const rpcFactory = options.rpcFactory ?? createRpcClient;
633
+ return await stopBackgroundMiningSupervisor({
634
+ dataDir: options.dataDir,
635
+ databasePath: options.databasePath,
636
+ shutdownGraceMs: options.shutdownGraceMs,
637
+ runtime: {
3715
638
  provider,
3716
639
  paths,
3717
- runMode: "background",
3718
- backgroundWorkerPid: snapshot.backgroundWorkerPid,
3719
- backgroundWorkerRunId: snapshot.backgroundWorkerRunId,
3720
- note: snapshot.livePublishInMempool
3721
- ? "Background mining stopped. The last mining transaction may still confirm from mempool."
3722
- : "Background mining stopped.",
3723
- });
3724
- await preemption?.release().catch(() => undefined);
3725
- return loadMiningRuntimeStatus(paths.miningStatusPath);
3726
- }
3727
- finally {
3728
- await controlLock.release();
3729
- }
640
+ openReadContext,
641
+ attachService,
642
+ rpcFactory,
643
+ },
644
+ deps: {
645
+ requestMiningPreemption: options.requestMiningPreemption,
646
+ saveStopSnapshot: options.saveStopSnapshotImpl,
647
+ sleep: options.sleepImpl,
648
+ },
649
+ });
3730
650
  }
3731
651
  export async function runBackgroundMiningWorker(options) {
3732
652
  const provider = options.provider ?? createDefaultWalletSecretProvider();
@@ -3734,76 +654,24 @@ export async function runBackgroundMiningWorker(options) {
3734
654
  const openReadContext = options.openReadContext ?? openWalletReadContext;
3735
655
  const attachService = options.attachService ?? attachOrStartManagedBitcoindService;
3736
656
  const rpcFactory = options.rpcFactory ?? createRpcClient;
3737
- const abortController = new AbortController();
3738
- process.on("SIGINT", () => abortController.abort());
3739
- process.on("SIGTERM", () => abortController.abort());
3740
- const initialContext = await openReadContext({
657
+ await runBackgroundMiningWorkerSupervisor({
3741
658
  dataDir: options.dataDir,
3742
659
  databasePath: options.databasePath,
3743
- secretProvider: provider,
3744
- paths,
3745
- });
3746
- try {
3747
- const initialView = await inspectMiningControlPlane({
660
+ runId: options.runId,
661
+ fetchImpl: options.fetchImpl,
662
+ runtime: {
3748
663
  provider,
3749
- localState: initialContext.localState,
3750
- bitcoind: initialContext.bitcoind,
3751
- nodeStatus: initialContext.nodeStatus,
3752
- nodeHealth: initialContext.nodeHealth,
3753
- indexer: initialContext.indexer,
3754
664
  paths,
3755
- });
3756
- await saveMiningRuntimeStatus(paths.miningStatusPath, {
3757
- ...initialView.runtime,
3758
- walletRootId: initialContext.localState.walletRootId,
3759
- workerApiVersion: MINING_WORKER_API_VERSION,
3760
- workerBinaryVersion: process.version,
3761
- workerBuildId: options.runId,
3762
- runMode: "background",
3763
- backgroundWorkerPid: process.pid,
3764
- backgroundWorkerRunId: options.runId,
3765
- backgroundWorkerHeartbeatAtUnixMs: Date.now(),
3766
- currentPhase: "idle",
3767
- updatedAtUnixMs: Date.now(),
3768
- });
3769
- }
3770
- finally {
3771
- await initialContext.close();
3772
- }
3773
- await runMiningLoop({
3774
- dataDir: options.dataDir,
3775
- databasePath: options.databasePath,
3776
- provider,
3777
- paths,
3778
- runMode: "background",
3779
- backgroundWorkerPid: process.pid,
3780
- backgroundWorkerRunId: options.runId,
3781
- signal: abortController.signal,
3782
- fetchImpl: options.fetchImpl,
3783
- openReadContext,
3784
- attachService,
3785
- rpcFactory,
3786
- });
3787
- await saveStopSnapshot({
3788
- dataDir: options.dataDir,
3789
- databasePath: options.databasePath,
3790
- provider,
3791
- paths,
3792
- runMode: "background",
3793
- backgroundWorkerPid: process.pid,
3794
- backgroundWorkerRunId: options.runId,
3795
- note: "Background mining worker stopped cleanly.",
3796
- });
3797
- }
3798
- export async function handleDetectedMiningRuntimeResumeForTesting(options) {
3799
- await handleDetectedMiningRuntimeResume({
3800
- ...options,
3801
- loopState: options.loopState ?? createMiningLoopState(),
665
+ openReadContext,
666
+ attachService,
667
+ rpcFactory,
668
+ },
669
+ deps: {
670
+ runMiningLoop: options.runMiningLoopImpl ?? runMiningLoop,
671
+ saveStopSnapshot: options.saveStopSnapshotImpl,
672
+ },
3802
673
  });
3803
674
  }
3804
- export async function takeOverMiningRuntimeForTesting(options) {
3805
- return await takeOverMiningRuntime(options);
3806
- }
3807
675
  export async function performMiningCycleForTesting(options) {
3808
676
  await performMiningCycle({
3809
677
  ...options,
@@ -3817,36 +685,9 @@ export async function runMiningLoopForTesting(options) {
3817
685
  ...options,
3818
686
  });
3819
687
  }
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
688
  export function createMiningSuspendDetectorForTesting(options = {}) {
3832
689
  return createMiningSuspendDetector(options);
3833
690
  }
3834
691
  export function throwIfMiningSuspendDetectedForTesting(detector) {
3835
692
  throwIfMiningSuspendDetected(detector);
3836
693
  }
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
- }
3844
- export function buildPrePublishStatusOverridesForTesting(options) {
3845
- return buildPrePublishStatusOverrides(options);
3846
- }
3847
- export function buildStatusSnapshotForTesting(view, overrides = {}) {
3848
- return buildStatusSnapshot(view, overrides);
3849
- }
3850
- export function shouldKeepCurrentTipLivePublishForTesting(options) {
3851
- return livePublishTargetsCandidateTip(options);
3852
- }