@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.
- package/README.md +4 -5
- package/dist/bitcoind/node.js +2 -1
- package/dist/bitcoind/progress/tty-renderer.js +3 -2
- package/dist/bitcoind/service.js +6 -24
- package/dist/bitcoind/types.d.ts +1 -0
- package/dist/bitcoind/types.js +1 -0
- package/dist/cli/command-registry.d.ts +39 -0
- package/dist/cli/command-registry.js +1132 -0
- package/dist/cli/commands/client-admin.js +6 -56
- package/dist/cli/commands/mining-admin.js +9 -32
- package/dist/cli/commands/mining-read.js +15 -56
- package/dist/cli/commands/mining-runtime.js +258 -57
- package/dist/cli/commands/service-runtime.js +1 -64
- package/dist/cli/commands/status.js +2 -15
- package/dist/cli/commands/update.js +6 -21
- package/dist/cli/commands/wallet-admin.js +18 -120
- package/dist/cli/commands/wallet-mutation.js +4 -7
- package/dist/cli/commands/wallet-read.js +31 -138
- package/dist/cli/context.js +2 -4
- package/dist/cli/mining-format.js +8 -2
- package/dist/cli/mutation-command-groups.d.ts +11 -11
- package/dist/cli/mutation-command-groups.js +9 -18
- package/dist/cli/mutation-json.d.ts +1 -17
- package/dist/cli/mutation-json.js +1 -28
- package/dist/cli/mutation-success.d.ts +0 -1
- package/dist/cli/mutation-success.js +0 -19
- package/dist/cli/output.d.ts +1 -10
- package/dist/cli/output.js +52 -481
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +38 -695
- package/dist/cli/runner.js +28 -113
- package/dist/cli/types.d.ts +7 -8
- package/dist/cli/update-notifier.js +1 -1
- package/dist/cli/wallet-format.js +1 -1
- package/dist/wallet/lifecycle/managed-core.d.ts +23 -0
- package/dist/wallet/lifecycle/managed-core.js +257 -0
- package/dist/wallet/lifecycle/repair-mining.d.ts +49 -0
- package/dist/wallet/lifecycle/repair-mining.js +304 -0
- package/dist/wallet/lifecycle/repair-runtime.d.ts +36 -0
- package/dist/wallet/lifecycle/repair-runtime.js +206 -0
- package/dist/wallet/lifecycle/repair.d.ts +11 -0
- package/dist/wallet/lifecycle/repair.js +368 -0
- package/dist/wallet/lifecycle/setup.d.ts +16 -0
- package/dist/wallet/lifecycle/setup.js +430 -0
- package/dist/wallet/lifecycle/types.d.ts +125 -0
- package/dist/wallet/lifecycle/types.js +1 -0
- package/dist/wallet/lifecycle.d.ts +4 -165
- package/dist/wallet/lifecycle.js +3 -1656
- package/dist/wallet/mining/candidate.d.ts +60 -0
- package/dist/wallet/mining/candidate.js +290 -0
- package/dist/wallet/mining/competitiveness.d.ts +22 -0
- package/dist/wallet/mining/competitiveness.js +640 -0
- package/dist/wallet/mining/control.js +7 -251
- package/dist/wallet/mining/cycle.d.ts +39 -0
- package/dist/wallet/mining/cycle.js +542 -0
- package/dist/wallet/mining/engine-state.d.ts +66 -0
- package/dist/wallet/mining/engine-state.js +211 -0
- package/dist/wallet/mining/engine-types.d.ts +173 -0
- package/dist/wallet/mining/engine-types.js +1 -0
- package/dist/wallet/mining/engine-utils.d.ts +7 -0
- package/dist/wallet/mining/engine-utils.js +75 -0
- package/dist/wallet/mining/events.d.ts +2 -0
- package/dist/wallet/mining/events.js +19 -0
- package/dist/wallet/mining/lifecycle.d.ts +71 -0
- package/dist/wallet/mining/lifecycle.js +355 -0
- package/dist/wallet/mining/projection.d.ts +61 -0
- package/dist/wallet/mining/projection.js +319 -0
- package/dist/wallet/mining/publish.d.ts +79 -0
- package/dist/wallet/mining/publish.js +614 -0
- package/dist/wallet/mining/runner.d.ts +12 -418
- package/dist/wallet/mining/runner.js +274 -3433
- package/dist/wallet/mining/supervisor.d.ts +134 -0
- package/dist/wallet/mining/supervisor.js +558 -0
- package/dist/wallet/mining/visualizer-sync.d.ts +42 -0
- package/dist/wallet/mining/visualizer-sync.js +166 -0
- package/dist/wallet/mining/visualizer.d.ts +1 -0
- package/dist/wallet/mining/visualizer.js +33 -18
- package/dist/wallet/read/context.d.ts +5 -1
- package/dist/wallet/read/context.js +19 -4
- package/dist/wallet/reset.d.ts +1 -1
- package/dist/wallet/reset.js +35 -11
- package/dist/wallet/runtime.d.ts +0 -6
- package/dist/wallet/runtime.js +2 -38
- package/dist/wallet/tx/common.d.ts +18 -0
- package/dist/wallet/tx/common.js +40 -26
- package/package.json +1 -1
- package/dist/wallet/state/seed-index.d.ts +0 -43
- package/dist/wallet/state/seed-index.js +0 -151
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import { createHash
|
|
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
|
|
17
|
+
import { appendMiningEvent } from "./runtime-artifacts.js";
|
|
23
18
|
import { loadClientConfig } from "./config.js";
|
|
24
|
-
import { MINING_LOOP_INTERVAL_MS,
|
|
25
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
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
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
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
|
-
|
|
2525
|
-
|
|
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
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
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
|
-
|
|
2611
|
-
|
|
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
|
-
|
|
2615
|
-
return await
|
|
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
|
|
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: "
|
|
343
|
+
currentPhase: "waiting",
|
|
3081
344
|
lastError: null,
|
|
3082
|
-
note: "
|
|
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
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
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
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
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:
|
|
376
|
+
state: nextState,
|
|
3288
377
|
},
|
|
3289
378
|
}, {
|
|
3290
379
|
runMode: options.runMode,
|
|
3291
|
-
currentPhase: "
|
|
3292
|
-
currentPublishDecision:
|
|
3293
|
-
|
|
3294
|
-
|
|
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
|
-
|
|
3305
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
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
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
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
|
-
|
|
616
|
+
openReadContext,
|
|
617
|
+
attachService,
|
|
618
|
+
rpcFactory,
|
|
619
|
+
},
|
|
620
|
+
deps: {
|
|
3651
621
|
requestMiningPreemption,
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
}
|
|
3655
|
-
|
|
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
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
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
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3744
|
-
|
|
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
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
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
|
-
}
|