@cogcoin/client 1.0.1 → 1.1.0
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 -2
- package/dist/bitcoind/client/factory.d.ts +0 -8
- package/dist/bitcoind/client/factory.js +1 -59
- package/dist/bitcoind/client/managed-client.d.ts +1 -3
- package/dist/bitcoind/client/managed-client.js +3 -47
- package/dist/bitcoind/indexer-daemon-main.js +173 -28
- package/dist/bitcoind/indexer-daemon.d.ts +14 -3
- package/dist/bitcoind/indexer-daemon.js +145 -29
- package/dist/bitcoind/indexer-monitor.d.ts +12 -0
- package/dist/bitcoind/indexer-monitor.js +89 -0
- package/dist/bitcoind/progress/follow-scene.d.ts +7 -1
- package/dist/bitcoind/progress/follow-scene.js +87 -4
- package/dist/bitcoind/progress/tty-renderer.d.ts +2 -0
- package/dist/bitcoind/progress/tty-renderer.js +2 -0
- package/dist/bitcoind/retryable-rpc.js +3 -0
- package/dist/bitcoind/service.d.ts +1 -0
- package/dist/bitcoind/service.js +31 -9
- package/dist/bitcoind/testing.d.ts +0 -1
- package/dist/bitcoind/testing.js +0 -1
- package/dist/bitcoind/types.d.ts +5 -2
- package/dist/cli/commands/follow.js +44 -49
- package/dist/cli/commands/mining-admin.js +65 -2
- package/dist/cli/commands/mining-read.js +43 -3
- package/dist/cli/commands/mining-runtime.js +91 -73
- package/dist/cli/commands/service-runtime.js +42 -2
- package/dist/cli/commands/status.js +3 -1
- package/dist/cli/commands/sync.js +50 -90
- package/dist/cli/commands/update.d.ts +2 -0
- package/dist/cli/commands/update.js +101 -0
- package/dist/cli/commands/wallet-admin.js +21 -3
- package/dist/cli/commands/wallet-read.js +2 -0
- package/dist/cli/context.js +36 -1
- package/dist/cli/managed-indexer-observer.d.ts +33 -0
- package/dist/cli/managed-indexer-observer.js +163 -0
- package/dist/cli/mining-format.d.ts +3 -1
- package/dist/cli/mining-format.js +63 -0
- package/dist/cli/mining-json.d.ts +11 -1
- package/dist/cli/mining-json.js +15 -0
- package/dist/cli/output.js +74 -2
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +28 -0
- package/dist/cli/prompt.js +109 -0
- package/dist/cli/read-json.d.ts +26 -1
- package/dist/cli/read-json.js +48 -0
- package/dist/cli/runner.js +8 -2
- package/dist/cli/signals.d.ts +12 -0
- package/dist/cli/signals.js +31 -13
- package/dist/cli/types.d.ts +13 -4
- package/dist/cli/update-notifier.js +7 -222
- package/dist/cli/update-service.d.ts +34 -0
- package/dist/cli/update-service.js +152 -0
- package/dist/client/initialization.js +5 -0
- package/dist/semver.d.ts +12 -0
- package/dist/semver.js +68 -0
- package/dist/wallet/lifecycle.d.ts +10 -0
- package/dist/wallet/mining/config.js +64 -3
- package/dist/wallet/mining/control.d.ts +5 -1
- package/dist/wallet/mining/control.js +269 -26
- package/dist/wallet/mining/domain-prompts.d.ts +17 -0
- package/dist/wallet/mining/domain-prompts.js +130 -0
- package/dist/wallet/mining/index.d.ts +2 -1
- package/dist/wallet/mining/index.js +1 -0
- package/dist/wallet/mining/provider-model.d.ts +30 -0
- package/dist/wallet/mining/provider-model.js +134 -0
- package/dist/wallet/mining/runner.d.ts +156 -5
- package/dist/wallet/mining/runner.js +1019 -399
- package/dist/wallet/mining/runtime-artifacts.js +1 -0
- package/dist/wallet/mining/sentence-protocol.d.ts +1 -0
- package/dist/wallet/mining/sentences.d.ts +2 -2
- package/dist/wallet/mining/sentences.js +32 -6
- package/dist/wallet/mining/types.d.ts +35 -1
- package/dist/wallet/mining/visualizer.d.ts +3 -0
- package/dist/wallet/mining/visualizer.js +132 -15
- package/dist/wallet/read/context.d.ts +1 -0
- package/dist/wallet/read/context.js +15 -7
- package/dist/wallet/state/client-password-agent.js +4 -1
- package/dist/wallet/state/client-password.js +15 -8
- package/dist/wallet/tx/common.js +1 -1
- package/package.json +3 -2
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import { rm } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
3
5
|
import { fileURLToPath } from "node:url";
|
|
4
6
|
import { getBalance, getBlockWinners, lookupDomain, lookupDomainById, } from "@cogcoin/indexer/queries";
|
|
5
7
|
import { assaySentences, deriveBlendSeed, displayToInternalBlockhash, getWords, settleBlock, } from "@cogcoin/scoring";
|
|
8
|
+
import { wordlist as englishWordlist } from "@scure/bip39/wordlists/english.js";
|
|
6
9
|
import { probeIndexerDaemon } from "../../bitcoind/indexer-daemon.js";
|
|
10
|
+
import { isRetryableManagedRpcError } from "../../bitcoind/retryable-rpc.js";
|
|
7
11
|
import { FOLLOW_VISIBLE_PRIOR_BLOCKS } from "../../bitcoind/client/follow-block-times.js";
|
|
8
|
-
import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
|
|
12
|
+
import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, stopManagedBitcoindService, } from "../../bitcoind/service.js";
|
|
9
13
|
import { createRpcClient } from "../../bitcoind/node.js";
|
|
10
14
|
import { COG_OPCODES, COG_PREFIX } from "../cogop/constants.js";
|
|
11
15
|
import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
|
|
12
|
-
import { assertFixedInputPrefixMatches, buildWalletMutationTransaction, outpointKey as walletMutationOutpointKey, isAlreadyAcceptedError, isBroadcastUnknownError, reconcilePersistentPolicyLocks, resolveWalletMutationFeeSelection, saveWalletStatePreservingUnlock, } from "../tx/common.js";
|
|
13
|
-
import { acquireFileLock } from "../fs/lock.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";
|
|
14
18
|
import { isMineableWalletDomain, openWalletReadContext, } from "../read/index.js";
|
|
15
19
|
import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
16
20
|
import { createDefaultWalletSecretProvider, } from "../state/provider.js";
|
|
@@ -19,13 +23,45 @@ import { appendMiningEvent, loadMiningRuntimeStatus, saveMiningRuntimeStatus, }
|
|
|
19
23
|
import { loadClientConfig } from "./config.js";
|
|
20
24
|
import { MINING_LOOP_INTERVAL_MS, MINING_NETWORK_SETTLE_WINDOW_MS, MINING_PROVIDER_BACKOFF_BASE_MS, MINING_PROVIDER_BACKOFF_MAX_MS, MINING_SHUTDOWN_GRACE_MS, MINING_STATUS_HEARTBEAT_INTERVAL_MS, MINING_SUSPEND_GAP_THRESHOLD_MS, MINING_TIP_SETTLE_WINDOW_MS, MINING_WORKER_API_VERSION, } from "./constants.js";
|
|
21
25
|
import { inspectMiningControlPlane, setupBuiltInMining } from "./control.js";
|
|
22
|
-
import { isMiningGenerationAbortRequested, markMiningGenerationActive, markMiningGenerationInactive, readMiningPreemptionRequest, requestMiningGenerationPreemption, } from "./coordination.js";
|
|
26
|
+
import { isMiningGenerationAbortRequested, markMiningGenerationActive, markMiningGenerationInactive, readMiningGenerationActivity, readMiningPreemptionRequest, requestMiningGenerationPreemption, } from "./coordination.js";
|
|
23
27
|
import { clearMiningPublishState, miningPublishIsInMempool, miningPublishMayStillExist, normalizeMiningPublishState, normalizeMiningStateRecord, } from "./state.js";
|
|
24
28
|
import { createMiningSentenceRequestLimits } from "./sentence-protocol.js";
|
|
25
29
|
import { generateMiningSentences, MiningProviderRequestError } from "./sentences.js";
|
|
26
30
|
import { createEmptyMiningFollowVisualizerState, MiningFollowVisualizer, } from "./visualizer.js";
|
|
27
31
|
const BEST_BLOCK_POLL_INTERVAL_MS = 500;
|
|
28
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
|
+
const MINING_BITCOIN_RECOVERY_NOTE = "Mining lost contact with the local Bitcoin RPC service and is waiting for it to recover.";
|
|
36
|
+
function resolveBip39WordsFromIndices(indices) {
|
|
37
|
+
if (indices === null || indices === undefined) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const words = [];
|
|
41
|
+
for (const index of indices) {
|
|
42
|
+
if (!Number.isInteger(index) || index < 0 || index >= englishWordlist.length) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
words.push(englishWordlist[index]);
|
|
46
|
+
}
|
|
47
|
+
return words;
|
|
48
|
+
}
|
|
49
|
+
function resolveSettledWinnerRequiredWords(options) {
|
|
50
|
+
const storedWords = resolveBip39WordsFromIndices(options.bip39WordIndices);
|
|
51
|
+
if (storedWords.length > 0) {
|
|
52
|
+
return storedWords;
|
|
53
|
+
}
|
|
54
|
+
if (options.snapshotTipPreviousHashHex === null
|
|
55
|
+
|| options.snapshotTipPreviousHashHex === undefined
|
|
56
|
+
|| !Number.isInteger(options.domainId)
|
|
57
|
+
|| options.domainId <= 0) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
return resolveBip39WordsFromIndices(deriveMiningWordIndices(Buffer.from(displayToInternalBlockhash(options.snapshotTipPreviousHashHex), "hex"), options.domainId));
|
|
61
|
+
}
|
|
62
|
+
function resolveSnapshotOverride(override, fallback) {
|
|
63
|
+
return override === undefined ? fallback : override;
|
|
64
|
+
}
|
|
29
65
|
class MiningSuspendDetectedError extends Error {
|
|
30
66
|
detectedAtUnixMs;
|
|
31
67
|
constructor(detectedAtUnixMs) {
|
|
@@ -88,6 +124,239 @@ async function isProcessAlive(pid) {
|
|
|
88
124
|
return true;
|
|
89
125
|
}
|
|
90
126
|
}
|
|
127
|
+
function normalizeMiningPid(value) {
|
|
128
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0
|
|
129
|
+
? value
|
|
130
|
+
: null;
|
|
131
|
+
}
|
|
132
|
+
function resolveMiningGenerationRequestPath(paths) {
|
|
133
|
+
return join(paths.miningRoot, "generation-request.json");
|
|
134
|
+
}
|
|
135
|
+
function resolveMiningGenerationActivityPath(paths) {
|
|
136
|
+
return join(paths.miningRoot, "generation-activity.json");
|
|
137
|
+
}
|
|
138
|
+
function createTakeoverStoppedMiningNote(livePublishInMempool) {
|
|
139
|
+
return livePublishInMempool
|
|
140
|
+
? "Mining runtime replaced. The last mining transaction may still confirm from mempool."
|
|
141
|
+
: "Mining runtime replaced.";
|
|
142
|
+
}
|
|
143
|
+
function createStoppedMiningRuntimeSnapshotForTakeover(options) {
|
|
144
|
+
const note = createTakeoverStoppedMiningNote(options.snapshot?.livePublishInMempool);
|
|
145
|
+
if (options.snapshot !== null) {
|
|
146
|
+
return {
|
|
147
|
+
...options.snapshot,
|
|
148
|
+
updatedAtUnixMs: options.nowUnixMs,
|
|
149
|
+
runMode: "stopped",
|
|
150
|
+
backgroundWorkerPid: null,
|
|
151
|
+
backgroundWorkerRunId: null,
|
|
152
|
+
backgroundWorkerHeartbeatAtUnixMs: null,
|
|
153
|
+
backgroundWorkerHealth: null,
|
|
154
|
+
currentPhase: "idle",
|
|
155
|
+
note,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
schemaVersion: 1,
|
|
160
|
+
walletRootId: options.walletRootId,
|
|
161
|
+
workerApiVersion: null,
|
|
162
|
+
workerBinaryVersion: null,
|
|
163
|
+
workerBuildId: null,
|
|
164
|
+
updatedAtUnixMs: options.nowUnixMs,
|
|
165
|
+
runMode: "stopped",
|
|
166
|
+
backgroundWorkerPid: null,
|
|
167
|
+
backgroundWorkerRunId: null,
|
|
168
|
+
backgroundWorkerHeartbeatAtUnixMs: null,
|
|
169
|
+
backgroundWorkerHealth: null,
|
|
170
|
+
indexerDaemonState: null,
|
|
171
|
+
indexerDaemonInstanceId: null,
|
|
172
|
+
indexerSnapshotSeq: null,
|
|
173
|
+
indexerSnapshotOpenedAtUnixMs: null,
|
|
174
|
+
indexerTruthSource: undefined,
|
|
175
|
+
indexerHeartbeatAtUnixMs: null,
|
|
176
|
+
coreBestHeight: null,
|
|
177
|
+
coreBestHash: null,
|
|
178
|
+
indexerTipHeight: null,
|
|
179
|
+
indexerTipHash: null,
|
|
180
|
+
indexerReorgDepth: null,
|
|
181
|
+
indexerTipAligned: null,
|
|
182
|
+
corePublishState: null,
|
|
183
|
+
providerState: null,
|
|
184
|
+
lastSuspendDetectedAtUnixMs: null,
|
|
185
|
+
reconnectSettledUntilUnixMs: null,
|
|
186
|
+
tipSettledUntilUnixMs: null,
|
|
187
|
+
miningState: "idle",
|
|
188
|
+
currentPhase: "idle",
|
|
189
|
+
currentPublishState: "none",
|
|
190
|
+
targetBlockHeight: null,
|
|
191
|
+
referencedBlockHashDisplay: null,
|
|
192
|
+
currentDomainId: null,
|
|
193
|
+
currentDomainName: null,
|
|
194
|
+
currentSentenceDisplay: null,
|
|
195
|
+
currentCanonicalBlend: null,
|
|
196
|
+
currentTxid: null,
|
|
197
|
+
currentWtxid: null,
|
|
198
|
+
livePublishInMempool: null,
|
|
199
|
+
currentFeeRateSatVb: null,
|
|
200
|
+
currentAbsoluteFeeSats: null,
|
|
201
|
+
currentBlockFeeSpentSats: "0",
|
|
202
|
+
sessionFeeSpentSats: "0",
|
|
203
|
+
lifetimeFeeSpentSats: "0",
|
|
204
|
+
sameDomainCompetitorSuppressed: null,
|
|
205
|
+
higherRankedCompetitorDomainCount: null,
|
|
206
|
+
dedupedCompetitorDomainCount: null,
|
|
207
|
+
competitivenessGateIndeterminate: null,
|
|
208
|
+
mempoolSequenceCacheStatus: null,
|
|
209
|
+
currentPublishDecision: null,
|
|
210
|
+
lastMempoolSequence: null,
|
|
211
|
+
lastCompetitivenessGateAtUnixMs: null,
|
|
212
|
+
pauseReason: null,
|
|
213
|
+
providerConfigured: false,
|
|
214
|
+
providerKind: null,
|
|
215
|
+
bitcoindHealth: "unavailable",
|
|
216
|
+
bitcoindServiceState: null,
|
|
217
|
+
bitcoindReplicaStatus: null,
|
|
218
|
+
nodeHealth: "unavailable",
|
|
219
|
+
indexerHealth: "unavailable",
|
|
220
|
+
tipsAligned: null,
|
|
221
|
+
lastEventAtUnixMs: null,
|
|
222
|
+
lastError: null,
|
|
223
|
+
note,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
async function waitForMiningProcessExit(pid, timeoutMs, sleepImpl = sleep) {
|
|
227
|
+
const deadline = Date.now() + timeoutMs;
|
|
228
|
+
while (Date.now() < deadline) {
|
|
229
|
+
if (!await isProcessAlive(pid)) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
await sleepImpl(Math.min(250, Math.max(timeoutMs, 1)));
|
|
233
|
+
}
|
|
234
|
+
return !await isProcessAlive(pid);
|
|
235
|
+
}
|
|
236
|
+
async function terminateMiningRuntimePid(options) {
|
|
237
|
+
if (!await isProcessAlive(options.pid)) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
process.kill(options.pid, "SIGTERM");
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (await waitForMiningProcessExit(options.pid, options.shutdownGraceMs, options.sleepImpl)) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
process.kill(options.pid, "SIGKILL");
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (await waitForMiningProcessExit(options.pid, options.shutdownGraceMs, options.sleepImpl)) {
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
throw new Error("mining_process_stop_timeout");
|
|
263
|
+
}
|
|
264
|
+
async function takeOverMiningRuntime(options) {
|
|
265
|
+
const snapshot = await loadMiningRuntimeStatus(options.paths.miningStatusPath).catch(() => null);
|
|
266
|
+
const controlLockMetadata = options.controlLockMetadata ?? (options.clearControlLockFile === true
|
|
267
|
+
? await readLockMetadata(options.paths.miningControlLockPath).catch(() => null)
|
|
268
|
+
: null);
|
|
269
|
+
const generationActivity = await readMiningGenerationActivity(options.paths).catch(() => null);
|
|
270
|
+
const shutdownGraceMs = options.shutdownGraceMs ?? MINING_SHUTDOWN_GRACE_MS;
|
|
271
|
+
const requestPreemption = options.requestMiningPreemption ?? requestMiningGenerationPreemption;
|
|
272
|
+
const controlLockPid = normalizeMiningPid(controlLockMetadata?.processId);
|
|
273
|
+
const backgroundWorkerPid = normalizeMiningPid(snapshot?.backgroundWorkerPid);
|
|
274
|
+
const generationOwnerPid = normalizeMiningPid(generationActivity?.generationOwnerPid);
|
|
275
|
+
const terminatedPids = [];
|
|
276
|
+
const discoveredPids = new Set();
|
|
277
|
+
for (const pid of [controlLockPid, backgroundWorkerPid, generationOwnerPid]) {
|
|
278
|
+
if (pid === null
|
|
279
|
+
|| pid === process.pid
|
|
280
|
+
|| discoveredPids.has(pid)
|
|
281
|
+
|| !await isProcessAlive(pid)) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
discoveredPids.add(pid);
|
|
285
|
+
}
|
|
286
|
+
const shouldPreemptGeneration = discoveredPids.size > 0 && (generationActivity?.generationActive === true
|
|
287
|
+
|| snapshot?.currentPhase === "generating"
|
|
288
|
+
|| snapshot?.currentPhase === "scoring");
|
|
289
|
+
const preemption = shouldPreemptGeneration
|
|
290
|
+
? await requestPreemption({
|
|
291
|
+
paths: options.paths,
|
|
292
|
+
reason: options.reason,
|
|
293
|
+
timeoutMs: Math.min(shutdownGraceMs, 15_000),
|
|
294
|
+
}).catch(() => null)
|
|
295
|
+
: null;
|
|
296
|
+
try {
|
|
297
|
+
for (const pid of discoveredPids) {
|
|
298
|
+
if (await terminateMiningRuntimePid({
|
|
299
|
+
pid,
|
|
300
|
+
shutdownGraceMs,
|
|
301
|
+
sleepImpl: options.sleepImpl,
|
|
302
|
+
})) {
|
|
303
|
+
terminatedPids.push(pid);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
await preemption?.release().catch(() => undefined);
|
|
309
|
+
}
|
|
310
|
+
const controlLockCleared = options.clearControlLockFile === true
|
|
311
|
+
? await clearOrphanedFileLock(options.paths.miningControlLockPath, isProcessAlive).catch(() => false)
|
|
312
|
+
: false;
|
|
313
|
+
await rm(resolveMiningGenerationRequestPath(options.paths), { force: true }).catch(() => undefined);
|
|
314
|
+
await rm(resolveMiningGenerationActivityPath(options.paths), { force: true }).catch(() => undefined);
|
|
315
|
+
const walletRootId = snapshot?.walletRootId
|
|
316
|
+
?? (typeof controlLockMetadata?.walletRootId === "string" ? controlLockMetadata.walletRootId : null);
|
|
317
|
+
if (snapshot !== null || walletRootId !== null || terminatedPids.length > 0 || controlLockCleared) {
|
|
318
|
+
await saveMiningRuntimeStatus(options.paths.miningStatusPath, createStoppedMiningRuntimeSnapshotForTakeover({
|
|
319
|
+
snapshot,
|
|
320
|
+
walletRootId,
|
|
321
|
+
nowUnixMs: Date.now(),
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
controlLockCleared,
|
|
326
|
+
replaced: terminatedPids.length > 0,
|
|
327
|
+
snapshot,
|
|
328
|
+
terminatedPids,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
async function acquireMiningStartControlLock(options) {
|
|
332
|
+
while (true) {
|
|
333
|
+
try {
|
|
334
|
+
return await acquireFileLock(options.paths.miningControlLockPath, {
|
|
335
|
+
purpose: options.purpose,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
if (!(error instanceof FileLockBusyError)) {
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
if (error.existingMetadata?.processId === process.pid) {
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
const takeover = await takeOverMiningRuntime({
|
|
346
|
+
paths: options.paths,
|
|
347
|
+
reason: options.takeoverReason,
|
|
348
|
+
clearControlLockFile: true,
|
|
349
|
+
controlLockMetadata: error.existingMetadata,
|
|
350
|
+
requestMiningPreemption: options.requestMiningPreemption,
|
|
351
|
+
shutdownGraceMs: options.shutdownGraceMs,
|
|
352
|
+
sleepImpl: options.sleepImpl,
|
|
353
|
+
});
|
|
354
|
+
if (!takeover.replaced && !takeover.controlLockCleared) {
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
91
360
|
function writeStdout(stream, line) {
|
|
92
361
|
if (stream === undefined) {
|
|
93
362
|
return;
|
|
@@ -204,11 +473,41 @@ function createMiningLoopState() {
|
|
|
204
473
|
selectedCandidate: null,
|
|
205
474
|
ui: createEmptyMiningFollowVisualizerState(),
|
|
206
475
|
waitingNote: null,
|
|
476
|
+
bitcoinRecoveryFirstFailureAtUnixMs: null,
|
|
477
|
+
bitcoinRecoveryFirstUnreachableAtUnixMs: null,
|
|
478
|
+
bitcoinRecoveryLastRestartAttemptAtUnixMs: null,
|
|
479
|
+
bitcoinRecoveryServiceInstanceId: null,
|
|
480
|
+
bitcoinRecoveryProcessId: null,
|
|
481
|
+
reconnectSettledUntilUnixMs: null,
|
|
482
|
+
tipSettledUntilUnixMs: null,
|
|
207
483
|
};
|
|
208
484
|
}
|
|
209
485
|
export function createMiningLoopStateForTesting() {
|
|
210
486
|
return createMiningLoopState();
|
|
211
487
|
}
|
|
488
|
+
function expireMiningSettleWindows(loopState, nowUnixMs) {
|
|
489
|
+
if (loopState.reconnectSettledUntilUnixMs !== null
|
|
490
|
+
&& loopState.reconnectSettledUntilUnixMs <= nowUnixMs) {
|
|
491
|
+
loopState.reconnectSettledUntilUnixMs = null;
|
|
492
|
+
}
|
|
493
|
+
if (loopState.tipSettledUntilUnixMs !== null
|
|
494
|
+
&& loopState.tipSettledUntilUnixMs <= nowUnixMs) {
|
|
495
|
+
loopState.tipSettledUntilUnixMs = null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function setMiningReconnectSettleWindow(loopState, nowUnixMs) {
|
|
499
|
+
loopState.reconnectSettledUntilUnixMs = nowUnixMs + MINING_NETWORK_SETTLE_WINDOW_MS;
|
|
500
|
+
}
|
|
501
|
+
function setMiningTipSettleWindow(loopState, nowUnixMs) {
|
|
502
|
+
loopState.tipSettledUntilUnixMs = nowUnixMs + MINING_TIP_SETTLE_WINDOW_MS;
|
|
503
|
+
}
|
|
504
|
+
function buildMiningSettleWindowStatusOverrides(loopState, nowUnixMs) {
|
|
505
|
+
expireMiningSettleWindows(loopState, nowUnixMs);
|
|
506
|
+
return {
|
|
507
|
+
reconnectSettledUntilUnixMs: loopState.reconnectSettledUntilUnixMs,
|
|
508
|
+
tipSettledUntilUnixMs: loopState.tipSettledUntilUnixMs,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
212
511
|
function buildMiningTipKey(bestBlockHash, targetBlockHeight) {
|
|
213
512
|
if (bestBlockHash === null || targetBlockHeight === null) {
|
|
214
513
|
return null;
|
|
@@ -232,7 +531,7 @@ function fallbackSettledWinnerDomainName(domainId) {
|
|
|
232
531
|
return `domain-${domainId}`;
|
|
233
532
|
}
|
|
234
533
|
function resolveCurrentMinedBlockBoard(options) {
|
|
235
|
-
const settledBlockHeight = options.
|
|
534
|
+
const settledBlockHeight = options.snapshotTipHeight ?? null;
|
|
236
535
|
if (settledBlockHeight === null) {
|
|
237
536
|
return {
|
|
238
537
|
settledBlockHeight,
|
|
@@ -245,12 +544,6 @@ function resolveCurrentMinedBlockBoard(options) {
|
|
|
245
544
|
settledBoardEntries: [],
|
|
246
545
|
};
|
|
247
546
|
}
|
|
248
|
-
if (options.nodeBestHeight !== null && (options.snapshotTipHeight ?? -1) < options.nodeBestHeight) {
|
|
249
|
-
return {
|
|
250
|
-
settledBlockHeight,
|
|
251
|
-
settledBoardEntries: [],
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
547
|
const settledBoardEntries = (getBlockWinners(options.snapshotState, settledBlockHeight) ?? [])
|
|
255
548
|
.slice()
|
|
256
549
|
.sort((left, right) => left.rank - right.rank || left.txIndex - right.txIndex)
|
|
@@ -259,6 +552,11 @@ function resolveCurrentMinedBlockBoard(options) {
|
|
|
259
552
|
rank: winner.rank,
|
|
260
553
|
domainName: lookupDomainById(options.snapshotState, winner.domainId)?.name ?? fallbackSettledWinnerDomainName(winner.domainId),
|
|
261
554
|
sentence: winner.sentenceText ?? "[unavailable]",
|
|
555
|
+
requiredWords: resolveSettledWinnerRequiredWords({
|
|
556
|
+
domainId: winner.domainId,
|
|
557
|
+
bip39WordIndices: winner.bip39WordIndices,
|
|
558
|
+
snapshotTipPreviousHashHex: options.snapshotTipPreviousHashHex,
|
|
559
|
+
}),
|
|
262
560
|
}));
|
|
263
561
|
return {
|
|
264
562
|
settledBlockHeight,
|
|
@@ -266,17 +564,42 @@ function resolveCurrentMinedBlockBoard(options) {
|
|
|
266
564
|
};
|
|
267
565
|
}
|
|
268
566
|
export function resolveSettledBoardForTesting(options) {
|
|
269
|
-
return resolveCurrentMinedBlockBoard(
|
|
567
|
+
return resolveCurrentMinedBlockBoard({
|
|
568
|
+
...options,
|
|
569
|
+
snapshotTipPreviousHashHex: options.snapshotTipPreviousHashHex ?? null,
|
|
570
|
+
});
|
|
270
571
|
}
|
|
271
|
-
function syncMiningUiSettledBoard(loopState, snapshotState, snapshotTipHeight,
|
|
572
|
+
function syncMiningUiSettledBoard(loopState, snapshotState, snapshotTipHeight, snapshotTipPreviousHashHex) {
|
|
272
573
|
const settledBoard = resolveCurrentMinedBlockBoard({
|
|
273
574
|
snapshotState,
|
|
274
575
|
snapshotTipHeight,
|
|
275
|
-
|
|
576
|
+
snapshotTipPreviousHashHex,
|
|
577
|
+
nodeBestHeight: null,
|
|
276
578
|
});
|
|
277
579
|
loopState.ui.settledBlockHeight = settledBoard.settledBlockHeight;
|
|
278
580
|
loopState.ui.settledBoardEntries = settledBoard.settledBoardEntries;
|
|
279
581
|
}
|
|
582
|
+
function syncMiningUiForCurrentTip(options) {
|
|
583
|
+
const targetBlockHeight = options.nodeBestHeight === null
|
|
584
|
+
? null
|
|
585
|
+
: options.nodeBestHeight + 1;
|
|
586
|
+
const tipKey = buildMiningTipKey(options.nodeBestHash, targetBlockHeight);
|
|
587
|
+
const priorTipKey = options.loopState.currentTipKey;
|
|
588
|
+
const tipChanged = tipKey !== null && tipKey !== priorTipKey;
|
|
589
|
+
if (tipKey !== priorTipKey) {
|
|
590
|
+
options.loopState.currentTipKey = tipKey;
|
|
591
|
+
resetMiningUiForTip(options.loopState, targetBlockHeight);
|
|
592
|
+
if (options.recentWin !== null) {
|
|
593
|
+
options.loopState.ui.recentWin = options.recentWin;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
syncMiningUiSettledBoard(options.loopState, options.snapshotState, options.snapshotTipHeight, options.snapshotTipPreviousHashHex);
|
|
597
|
+
return {
|
|
598
|
+
targetBlockHeight,
|
|
599
|
+
tipKey,
|
|
600
|
+
tipChanged,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
280
603
|
function setMiningUiCandidate(loopState, candidate) {
|
|
281
604
|
loopState.ui.latestSentence = candidate.sentence;
|
|
282
605
|
loopState.ui.provisionalRequiredWords = [...candidate.bip39Words];
|
|
@@ -306,6 +629,123 @@ function clearSelectedCandidate(loopState) {
|
|
|
306
629
|
loopState.selectedCandidateTipKey = null;
|
|
307
630
|
loopState.selectedCandidate = null;
|
|
308
631
|
}
|
|
632
|
+
function clearMiningUiTransientCandidate(loopState) {
|
|
633
|
+
loopState.ui.provisionalRequiredWords = [];
|
|
634
|
+
loopState.ui.provisionalEntry = {
|
|
635
|
+
domainName: null,
|
|
636
|
+
sentence: null,
|
|
637
|
+
};
|
|
638
|
+
loopState.ui.latestSentence = null;
|
|
639
|
+
}
|
|
640
|
+
function discardMiningLoopTransientWork(loopState, walletRootId) {
|
|
641
|
+
clearMiningGateCache(walletRootId);
|
|
642
|
+
clearSelectedCandidate(loopState);
|
|
643
|
+
clearMiningUiTransientCandidate(loopState);
|
|
644
|
+
loopState.waitingNote = null;
|
|
645
|
+
}
|
|
646
|
+
function resolveMiningBitcoindRecoveryIdentity(value) {
|
|
647
|
+
const raw = (value ?? {});
|
|
648
|
+
return {
|
|
649
|
+
serviceInstanceId: raw.serviceInstanceId ?? null,
|
|
650
|
+
processId: raw.processId ?? raw.pid ?? null,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function miningBitcoindRecoveryIdentityMatches(left, right) {
|
|
654
|
+
if (left.serviceInstanceId !== null && right.serviceInstanceId !== null) {
|
|
655
|
+
return left.serviceInstanceId === right.serviceInstanceId;
|
|
656
|
+
}
|
|
657
|
+
if (left.processId !== null && right.processId !== null) {
|
|
658
|
+
return left.processId === right.processId;
|
|
659
|
+
}
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
function rememberMiningBitcoindRecoveryIdentity(loopState, value) {
|
|
663
|
+
const next = resolveMiningBitcoindRecoveryIdentity(value);
|
|
664
|
+
if (next.serviceInstanceId === null && next.processId === null) {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
const previous = {
|
|
668
|
+
serviceInstanceId: loopState.bitcoinRecoveryServiceInstanceId,
|
|
669
|
+
processId: loopState.bitcoinRecoveryProcessId,
|
|
670
|
+
};
|
|
671
|
+
const changed = (previous.serviceInstanceId !== null
|
|
672
|
+
|| previous.processId !== null) && !miningBitcoindRecoveryIdentityMatches(previous, next);
|
|
673
|
+
loopState.bitcoinRecoveryServiceInstanceId = next.serviceInstanceId ?? (next.processId !== null && previous.processId === next.processId
|
|
674
|
+
? previous.serviceInstanceId
|
|
675
|
+
: null);
|
|
676
|
+
loopState.bitcoinRecoveryProcessId = next.processId ?? (next.serviceInstanceId !== null && previous.serviceInstanceId === next.serviceInstanceId
|
|
677
|
+
? previous.processId
|
|
678
|
+
: null);
|
|
679
|
+
return changed;
|
|
680
|
+
}
|
|
681
|
+
function resetMiningBitcoindRecoveryState(loopState, value) {
|
|
682
|
+
const hadRecovery = loopState.bitcoinRecoveryFirstFailureAtUnixMs !== null;
|
|
683
|
+
loopState.bitcoinRecoveryFirstFailureAtUnixMs = null;
|
|
684
|
+
loopState.bitcoinRecoveryFirstUnreachableAtUnixMs = null;
|
|
685
|
+
loopState.bitcoinRecoveryLastRestartAttemptAtUnixMs = null;
|
|
686
|
+
if (value !== undefined) {
|
|
687
|
+
rememberMiningBitcoindRecoveryIdentity(loopState, value);
|
|
688
|
+
}
|
|
689
|
+
return hadRecovery;
|
|
690
|
+
}
|
|
691
|
+
function isMiningBitcoindRecoveryPidAlive(pid) {
|
|
692
|
+
if (pid === null || pid === undefined || !Number.isInteger(pid) || pid <= 0) {
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
try {
|
|
696
|
+
process.kill(pid, 0);
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
if (error instanceof Error && "code" in error && error.code === "EPERM") {
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function describeRecoverableMiningBitcoindError(error) {
|
|
707
|
+
return error instanceof Error ? error.message : String(error);
|
|
708
|
+
}
|
|
709
|
+
function isRecoverableMiningBitcoindError(error) {
|
|
710
|
+
if (isRetryableManagedRpcError(error)) {
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
if (!(error instanceof Error)) {
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
if ("code" in error) {
|
|
717
|
+
const code = error.code;
|
|
718
|
+
if (code === "ENOENT" || code === "ECONNREFUSED" || code === "ECONNRESET") {
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return error.message === "managed_bitcoind_service_start_timeout"
|
|
723
|
+
|| error.message === "bitcoind_cookie_timeout"
|
|
724
|
+
|| error.message.includes("cookie file is unavailable")
|
|
725
|
+
|| error.message.includes("cookie file could not be read")
|
|
726
|
+
|| error.message.includes("ECONNREFUSED")
|
|
727
|
+
|| error.message.includes("ECONNRESET")
|
|
728
|
+
|| error.message.includes("socket hang up");
|
|
729
|
+
}
|
|
730
|
+
async function attachManagedBitcoindForRecovery(options) {
|
|
731
|
+
try {
|
|
732
|
+
const service = await options.attachService({
|
|
733
|
+
dataDir: options.dataDir,
|
|
734
|
+
chain: "main",
|
|
735
|
+
startHeight: 0,
|
|
736
|
+
walletRootId: options.walletRootId,
|
|
737
|
+
});
|
|
738
|
+
const serviceStatus = await service.refreshServiceStatus?.().catch(() => null);
|
|
739
|
+
rememberMiningBitcoindRecoveryIdentity(options.loopState, serviceStatus ?? { pid: service.pid });
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
if (!isRecoverableMiningBitcoindError(error)) {
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
309
749
|
async function resolveFundingDisplaySats(state, rpc) {
|
|
310
750
|
const utxos = await rpc.listUnspent(state.managedCoreWallet.walletName, 0);
|
|
311
751
|
return utxos.reduce((sum, entry) => {
|
|
@@ -348,6 +788,22 @@ function syncMiningVisualizerBalances(loopState, readContext, balanceSats) {
|
|
|
348
788
|
: getBalance(readContext.snapshot.state, readContext.localState.state.funding.scriptPubKeyHex);
|
|
349
789
|
loopState.ui.balanceSats = balanceSats;
|
|
350
790
|
}
|
|
791
|
+
function createIndexedMiningFollowVisualizerState(readContext) {
|
|
792
|
+
const uiState = createEmptyMiningFollowVisualizerState();
|
|
793
|
+
const localState = readContext.localState;
|
|
794
|
+
const settledBoard = resolveCurrentMinedBlockBoard({
|
|
795
|
+
snapshotState: readContext.snapshot?.state ?? null,
|
|
796
|
+
snapshotTipHeight: readContext.snapshot?.tip?.height ?? readContext.indexer.snapshotTip?.height ?? null,
|
|
797
|
+
snapshotTipPreviousHashHex: readContext.snapshot?.tip?.previousHashHex ?? readContext.indexer.snapshotTip?.previousHashHex ?? null,
|
|
798
|
+
nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
|
|
799
|
+
});
|
|
800
|
+
uiState.settledBlockHeight = settledBoard.settledBlockHeight;
|
|
801
|
+
uiState.settledBoardEntries = settledBoard.settledBoardEntries;
|
|
802
|
+
if (readContext.snapshot !== null && localState.availability === "ready" && localState.state !== null) {
|
|
803
|
+
uiState.balanceCogtoshi = getBalance(readContext.snapshot.state, localState.state.funding.scriptPubKeyHex);
|
|
804
|
+
}
|
|
805
|
+
return uiState;
|
|
806
|
+
}
|
|
351
807
|
function syncMiningVisualizerBlockTimes(loopState, blockTimesByHeight) {
|
|
352
808
|
loopState.ui.visibleBlockTimesByHeight = { ...blockTimesByHeight };
|
|
353
809
|
}
|
|
@@ -642,28 +1098,73 @@ async function resolveOverlayAuthorizedMiningDomain(options) {
|
|
|
642
1098
|
function buildStatusSnapshot(view, overrides = {}) {
|
|
643
1099
|
return {
|
|
644
1100
|
...view.runtime,
|
|
645
|
-
runMode: overrides.runMode
|
|
646
|
-
backgroundWorkerPid: overrides.backgroundWorkerPid
|
|
647
|
-
backgroundWorkerRunId: overrides.backgroundWorkerRunId
|
|
648
|
-
backgroundWorkerHeartbeatAtUnixMs: overrides.backgroundWorkerHeartbeatAtUnixMs
|
|
649
|
-
currentPhase: overrides.currentPhase
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
1101
|
+
runMode: resolveSnapshotOverride(overrides.runMode, view.runtime.runMode),
|
|
1102
|
+
backgroundWorkerPid: resolveSnapshotOverride(overrides.backgroundWorkerPid, view.runtime.backgroundWorkerPid),
|
|
1103
|
+
backgroundWorkerRunId: resolveSnapshotOverride(overrides.backgroundWorkerRunId, view.runtime.backgroundWorkerRunId),
|
|
1104
|
+
backgroundWorkerHeartbeatAtUnixMs: resolveSnapshotOverride(overrides.backgroundWorkerHeartbeatAtUnixMs, view.runtime.backgroundWorkerHeartbeatAtUnixMs),
|
|
1105
|
+
currentPhase: resolveSnapshotOverride(overrides.currentPhase, view.runtime.currentPhase),
|
|
1106
|
+
currentPublishState: resolveSnapshotOverride(overrides.currentPublishState, view.runtime.currentPublishState),
|
|
1107
|
+
targetBlockHeight: resolveSnapshotOverride(overrides.targetBlockHeight, view.runtime.targetBlockHeight),
|
|
1108
|
+
referencedBlockHashDisplay: resolveSnapshotOverride(overrides.referencedBlockHashDisplay, view.runtime.referencedBlockHashDisplay),
|
|
1109
|
+
currentDomainId: resolveSnapshotOverride(overrides.currentDomainId, view.runtime.currentDomainId),
|
|
1110
|
+
currentDomainName: resolveSnapshotOverride(overrides.currentDomainName, view.runtime.currentDomainName),
|
|
1111
|
+
currentSentenceDisplay: resolveSnapshotOverride(overrides.currentSentenceDisplay, view.runtime.currentSentenceDisplay),
|
|
1112
|
+
currentCanonicalBlend: resolveSnapshotOverride(overrides.currentCanonicalBlend, view.runtime.currentCanonicalBlend),
|
|
1113
|
+
currentTxid: resolveSnapshotOverride(overrides.currentTxid, view.runtime.currentTxid),
|
|
1114
|
+
currentWtxid: resolveSnapshotOverride(overrides.currentWtxid, view.runtime.currentWtxid),
|
|
1115
|
+
currentFeeRateSatVb: resolveSnapshotOverride(overrides.currentFeeRateSatVb, view.runtime.currentFeeRateSatVb),
|
|
1116
|
+
currentAbsoluteFeeSats: resolveSnapshotOverride(overrides.currentAbsoluteFeeSats, view.runtime.currentAbsoluteFeeSats),
|
|
1117
|
+
currentBlockFeeSpentSats: resolveSnapshotOverride(overrides.currentBlockFeeSpentSats, view.runtime.currentBlockFeeSpentSats),
|
|
1118
|
+
lastSuspendDetectedAtUnixMs: resolveSnapshotOverride(overrides.lastSuspendDetectedAtUnixMs, view.runtime.lastSuspendDetectedAtUnixMs),
|
|
1119
|
+
reconnectSettledUntilUnixMs: resolveSnapshotOverride(overrides.reconnectSettledUntilUnixMs, view.runtime.reconnectSettledUntilUnixMs),
|
|
1120
|
+
tipSettledUntilUnixMs: resolveSnapshotOverride(overrides.tipSettledUntilUnixMs, view.runtime.tipSettledUntilUnixMs),
|
|
1121
|
+
providerState: resolveSnapshotOverride(overrides.providerState, view.runtime.providerState),
|
|
1122
|
+
corePublishState: resolveSnapshotOverride(overrides.corePublishState, view.runtime.corePublishState),
|
|
1123
|
+
currentPublishDecision: resolveSnapshotOverride(overrides.currentPublishDecision, view.runtime.currentPublishDecision),
|
|
1124
|
+
sameDomainCompetitorSuppressed: resolveSnapshotOverride(overrides.sameDomainCompetitorSuppressed, view.runtime.sameDomainCompetitorSuppressed),
|
|
1125
|
+
higherRankedCompetitorDomainCount: resolveSnapshotOverride(overrides.higherRankedCompetitorDomainCount, view.runtime.higherRankedCompetitorDomainCount),
|
|
1126
|
+
dedupedCompetitorDomainCount: resolveSnapshotOverride(overrides.dedupedCompetitorDomainCount, view.runtime.dedupedCompetitorDomainCount),
|
|
1127
|
+
competitivenessGateIndeterminate: resolveSnapshotOverride(overrides.competitivenessGateIndeterminate, view.runtime.competitivenessGateIndeterminate),
|
|
1128
|
+
mempoolSequenceCacheStatus: resolveSnapshotOverride(overrides.mempoolSequenceCacheStatus, view.runtime.mempoolSequenceCacheStatus),
|
|
1129
|
+
lastMempoolSequence: resolveSnapshotOverride(overrides.lastMempoolSequence, view.runtime.lastMempoolSequence),
|
|
1130
|
+
lastCompetitivenessGateAtUnixMs: resolveSnapshotOverride(overrides.lastCompetitivenessGateAtUnixMs, view.runtime.lastCompetitivenessGateAtUnixMs),
|
|
1131
|
+
lastError: resolveSnapshotOverride(overrides.lastError, view.runtime.lastError),
|
|
1132
|
+
note: resolveSnapshotOverride(overrides.note, view.runtime.note),
|
|
1133
|
+
livePublishInMempool: resolveSnapshotOverride(overrides.livePublishInMempool, view.runtime.livePublishInMempool),
|
|
664
1134
|
updatedAtUnixMs: Date.now(),
|
|
665
1135
|
};
|
|
666
1136
|
}
|
|
1137
|
+
function buildPrePublishStatusOverrides(options) {
|
|
1138
|
+
const replacing = options.state.miningState.currentTxid !== null;
|
|
1139
|
+
const replacingAcrossTips = replacing && !livePublishTargetsCandidateTip({
|
|
1140
|
+
liveState: options.state.miningState,
|
|
1141
|
+
candidate: options.candidate,
|
|
1142
|
+
});
|
|
1143
|
+
return {
|
|
1144
|
+
currentPhase: replacing ? "replacing" : "publishing",
|
|
1145
|
+
currentPublishDecision: replacing ? "replacing" : "publishing",
|
|
1146
|
+
targetBlockHeight: options.candidate.targetBlockHeight,
|
|
1147
|
+
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
1148
|
+
currentDomainId: options.candidate.domainId,
|
|
1149
|
+
currentDomainName: options.candidate.domainName,
|
|
1150
|
+
currentSentenceDisplay: options.candidate.sentence,
|
|
1151
|
+
currentCanonicalBlend: options.candidate.canonicalBlend.toString(),
|
|
1152
|
+
note: replacing
|
|
1153
|
+
? "Replacing the live mining transaction for the current tip."
|
|
1154
|
+
: "Broadcasting the best mining candidate for the current tip.",
|
|
1155
|
+
...(replacingAcrossTips
|
|
1156
|
+
? {
|
|
1157
|
+
currentPublishState: "none",
|
|
1158
|
+
currentTxid: null,
|
|
1159
|
+
currentWtxid: null,
|
|
1160
|
+
livePublishInMempool: false,
|
|
1161
|
+
currentFeeRateSatVb: null,
|
|
1162
|
+
currentAbsoluteFeeSats: null,
|
|
1163
|
+
currentBlockFeeSpentSats: "0",
|
|
1164
|
+
}
|
|
1165
|
+
: {}),
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
667
1168
|
async function refreshAndSaveStatus(options) {
|
|
668
1169
|
const view = await inspectMiningControlPlane({
|
|
669
1170
|
provider: options.provider,
|
|
@@ -682,6 +1183,93 @@ async function refreshAndSaveStatus(options) {
|
|
|
682
1183
|
async function appendEvent(paths, event) {
|
|
683
1184
|
await appendMiningEvent(paths.miningEventsPath, event);
|
|
684
1185
|
}
|
|
1186
|
+
async function handleRecoverableMiningBitcoindFailure(options) {
|
|
1187
|
+
const failureMessage = describeRecoverableMiningBitcoindError(options.error);
|
|
1188
|
+
const walletRootId = options.readContext.localState.walletRootId ?? undefined;
|
|
1189
|
+
if (options.loopState.bitcoinRecoveryFirstFailureAtUnixMs === null) {
|
|
1190
|
+
options.loopState.bitcoinRecoveryFirstFailureAtUnixMs = options.nowUnixMs;
|
|
1191
|
+
}
|
|
1192
|
+
let restartedService = false;
|
|
1193
|
+
const probe = await options.probeService({
|
|
1194
|
+
dataDir: options.dataDir,
|
|
1195
|
+
chain: "main",
|
|
1196
|
+
startHeight: 0,
|
|
1197
|
+
walletRootId,
|
|
1198
|
+
}).catch((probeError) => {
|
|
1199
|
+
if (!isRecoverableMiningBitcoindError(probeError)) {
|
|
1200
|
+
throw probeError;
|
|
1201
|
+
}
|
|
1202
|
+
return null;
|
|
1203
|
+
});
|
|
1204
|
+
if (probe !== null) {
|
|
1205
|
+
if (probe.compatibility === "compatible") {
|
|
1206
|
+
rememberMiningBitcoindRecoveryIdentity(options.loopState, probe.status);
|
|
1207
|
+
options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs = null;
|
|
1208
|
+
}
|
|
1209
|
+
else if (probe.compatibility === "unreachable") {
|
|
1210
|
+
const identityChanged = rememberMiningBitcoindRecoveryIdentity(options.loopState, probe.status);
|
|
1211
|
+
const livePid = isMiningBitcoindRecoveryPidAlive(probe.status?.processId ?? null);
|
|
1212
|
+
if (identityChanged || options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs === null) {
|
|
1213
|
+
options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs = options.nowUnixMs;
|
|
1214
|
+
}
|
|
1215
|
+
if (!livePid) {
|
|
1216
|
+
restartedService = await attachManagedBitcoindForRecovery({
|
|
1217
|
+
dataDir: options.dataDir,
|
|
1218
|
+
walletRootId,
|
|
1219
|
+
attachService: options.attachService,
|
|
1220
|
+
loopState: options.loopState,
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
else {
|
|
1224
|
+
const graceElapsed = (options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs !== null
|
|
1225
|
+
&& options.nowUnixMs - options.loopState.bitcoinRecoveryFirstUnreachableAtUnixMs
|
|
1226
|
+
>= MINING_BITCOIN_RECOVERY_GRACE_MS);
|
|
1227
|
+
const cooldownElapsed = (options.loopState.bitcoinRecoveryLastRestartAttemptAtUnixMs === null
|
|
1228
|
+
|| options.nowUnixMs - options.loopState.bitcoinRecoveryLastRestartAttemptAtUnixMs
|
|
1229
|
+
>= MINING_BITCOIN_RECOVERY_RESTART_COOLDOWN_MS);
|
|
1230
|
+
if (graceElapsed && cooldownElapsed) {
|
|
1231
|
+
options.loopState.bitcoinRecoveryLastRestartAttemptAtUnixMs = options.nowUnixMs;
|
|
1232
|
+
await options.stopService({
|
|
1233
|
+
dataDir: options.dataDir,
|
|
1234
|
+
walletRootId,
|
|
1235
|
+
}).catch((stopError) => {
|
|
1236
|
+
if (!isRecoverableMiningBitcoindError(stopError)) {
|
|
1237
|
+
throw stopError;
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
await attachManagedBitcoindForRecovery({
|
|
1241
|
+
dataDir: options.dataDir,
|
|
1242
|
+
walletRootId,
|
|
1243
|
+
attachService: options.attachService,
|
|
1244
|
+
loopState: options.loopState,
|
|
1245
|
+
});
|
|
1246
|
+
restartedService = true;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
else {
|
|
1251
|
+
throw new Error(probe.error ?? "managed_bitcoind_protocol_error");
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
if (restartedService) {
|
|
1255
|
+
discardMiningLoopTransientWork(options.loopState, walletRootId);
|
|
1256
|
+
setMiningReconnectSettleWindow(options.loopState, options.nowUnixMs);
|
|
1257
|
+
}
|
|
1258
|
+
await refreshAndSaveStatus({
|
|
1259
|
+
paths: options.paths,
|
|
1260
|
+
provider: options.provider,
|
|
1261
|
+
readContext: options.readContext,
|
|
1262
|
+
overrides: {
|
|
1263
|
+
runMode: options.runMode,
|
|
1264
|
+
currentPhase: "waiting-bitcoin-network",
|
|
1265
|
+
lastError: failureMessage,
|
|
1266
|
+
note: MINING_BITCOIN_RECOVERY_NOTE,
|
|
1267
|
+
...buildMiningSettleWindowStatusOverrides(options.loopState, options.nowUnixMs),
|
|
1268
|
+
},
|
|
1269
|
+
visualizer: options.visualizer,
|
|
1270
|
+
visualizerState: options.loopState.ui,
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
685
1273
|
async function handleDetectedMiningRuntimeResume(options) {
|
|
686
1274
|
const readContext = await options.openReadContext({
|
|
687
1275
|
dataDir: options.dataDir,
|
|
@@ -691,6 +1279,7 @@ async function handleDetectedMiningRuntimeResume(options) {
|
|
|
691
1279
|
});
|
|
692
1280
|
try {
|
|
693
1281
|
clearMiningGateCache(readContext.localState.walletRootId);
|
|
1282
|
+
setMiningReconnectSettleWindow(options.loopState, options.detectedAtUnixMs);
|
|
694
1283
|
await refreshAndSaveStatus({
|
|
695
1284
|
paths: options.paths,
|
|
696
1285
|
provider: options.provider,
|
|
@@ -703,8 +1292,10 @@ async function handleDetectedMiningRuntimeResume(options) {
|
|
|
703
1292
|
currentPhase: "resuming",
|
|
704
1293
|
lastSuspendDetectedAtUnixMs: options.detectedAtUnixMs,
|
|
705
1294
|
note: "Mining discarded stale in-flight work after a large local runtime gap and is rechecking health.",
|
|
1295
|
+
...buildMiningSettleWindowStatusOverrides(options.loopState, options.detectedAtUnixMs),
|
|
706
1296
|
},
|
|
707
1297
|
visualizer: options.visualizer,
|
|
1298
|
+
visualizerState: createIndexedMiningFollowVisualizerState(readContext),
|
|
708
1299
|
});
|
|
709
1300
|
}
|
|
710
1301
|
finally {
|
|
@@ -768,7 +1359,7 @@ function determineCorePublishState(info) {
|
|
|
768
1359
|
}
|
|
769
1360
|
function createMiningPlan(options) {
|
|
770
1361
|
const fundingUtxos = options.allUtxos.filter((entry) => entry.scriptPubKey === options.state.funding.scriptPubKeyHex
|
|
771
|
-
&& entry.confirmations >=
|
|
1362
|
+
&& entry.confirmations >= MINING_FUNDING_MIN_CONF
|
|
772
1363
|
&& entry.spendable !== false
|
|
773
1364
|
&& entry.safe !== false
|
|
774
1365
|
&& !(options.conflictOutpoint !== null
|
|
@@ -821,6 +1412,7 @@ async function buildMiningTransaction(options) {
|
|
|
821
1412
|
finalizeErrorCode: "wallet_mining_finalize_failed",
|
|
822
1413
|
mempoolRejectPrefix: "wallet_mining_mempool_rejected",
|
|
823
1414
|
feeRate: options.plan.feeRateSatVb,
|
|
1415
|
+
availableFundingMinConf: MINING_FUNDING_MIN_CONF,
|
|
824
1416
|
});
|
|
825
1417
|
}
|
|
826
1418
|
export function createMiningPlanForTesting(options) {
|
|
@@ -897,6 +1489,37 @@ function createStaleMiningCandidateWaitingNote() {
|
|
|
897
1489
|
function createRetryableMiningPublishWaitingNote() {
|
|
898
1490
|
return "Selected mining candidate did not reach mempool and will be retried on the current tip with refreshed wallet state.";
|
|
899
1491
|
}
|
|
1492
|
+
const MINING_FUNDING_MIN_CONF = 0;
|
|
1493
|
+
function createInsufficientFundsMiningPublishWaitingNote() {
|
|
1494
|
+
return "Mining is waiting for enough safe BTC funding that Bitcoin Core can use for the next publish.";
|
|
1495
|
+
}
|
|
1496
|
+
function createInsufficientFundsMiningPublishErrorMessage() {
|
|
1497
|
+
return "Bitcoin Core could not fund the next mining publish with safe BTC.";
|
|
1498
|
+
}
|
|
1499
|
+
function buildMiningGenerationRequest(options) {
|
|
1500
|
+
return {
|
|
1501
|
+
schemaVersion: 1,
|
|
1502
|
+
requestId: options.requestId ?? `mining-${options.targetBlockHeight}-${randomBytes(8).toString("hex")}`,
|
|
1503
|
+
targetBlockHeight: options.targetBlockHeight,
|
|
1504
|
+
referencedBlockHashDisplay: options.referencedBlockHashDisplay,
|
|
1505
|
+
generatedAtUnixMs: options.generatedAtUnixMs ?? Date.now(),
|
|
1506
|
+
extraPrompt: options.extraPrompt,
|
|
1507
|
+
limits: createMiningSentenceRequestLimits(),
|
|
1508
|
+
rootDomains: options.domains.map((domain) => ({
|
|
1509
|
+
domainId: domain.domainId,
|
|
1510
|
+
domainName: domain.domainName,
|
|
1511
|
+
requiredWords: domain.requiredWords,
|
|
1512
|
+
extraPrompt: options.domainExtraPrompts[domain.domainName.toLowerCase()] ?? null,
|
|
1513
|
+
})),
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
export function buildMiningGenerationRequestForTesting(options) {
|
|
1517
|
+
return buildMiningGenerationRequest({
|
|
1518
|
+
...options,
|
|
1519
|
+
domainExtraPrompts: options.domainExtraPrompts ?? {},
|
|
1520
|
+
extraPrompt: options.extraPrompt ?? null,
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
900
1523
|
async function generateCandidatesForDomains(options) {
|
|
901
1524
|
const bestBlockHash = options.readContext.nodeStatus?.nodeBestHashHex;
|
|
902
1525
|
if (bestBlockHash === null || bestBlockHash === undefined) {
|
|
@@ -908,6 +1531,10 @@ async function generateCandidatesForDomains(options) {
|
|
|
908
1531
|
...domain,
|
|
909
1532
|
requiredWords: getWords(domain.domainId, referencedBlockHashInternal),
|
|
910
1533
|
}));
|
|
1534
|
+
const clientConfig = await loadClientConfig({
|
|
1535
|
+
path: options.paths.clientConfigPath,
|
|
1536
|
+
provider: options.provider,
|
|
1537
|
+
}).catch(() => null);
|
|
911
1538
|
const abortController = new AbortController();
|
|
912
1539
|
let stale = false;
|
|
913
1540
|
let staleIndexerTruth = false;
|
|
@@ -946,20 +1573,13 @@ async function generateCandidatesForDomains(options) {
|
|
|
946
1573
|
runId: options.runId ?? null,
|
|
947
1574
|
pid: process.pid ?? null,
|
|
948
1575
|
});
|
|
949
|
-
const generationRequest = {
|
|
950
|
-
schemaVersion: 1,
|
|
951
|
-
requestId: `mining-${targetBlockHeight}-${randomBytes(8).toString("hex")}`,
|
|
1576
|
+
const generationRequest = buildMiningGenerationRequest({
|
|
952
1577
|
targetBlockHeight,
|
|
953
1578
|
referencedBlockHashDisplay: bestBlockHash,
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
domainId: domain.domainId,
|
|
959
|
-
domainName: domain.domainName,
|
|
960
|
-
requiredWords: domain.requiredWords,
|
|
961
|
-
})),
|
|
962
|
-
};
|
|
1579
|
+
domains: rootDomains,
|
|
1580
|
+
domainExtraPrompts: clientConfig?.mining.domainExtraPrompts ?? {},
|
|
1581
|
+
extraPrompt: clientConfig?.mining.builtIn?.extraPrompt ?? null,
|
|
1582
|
+
});
|
|
963
1583
|
let generated;
|
|
964
1584
|
try {
|
|
965
1585
|
generated = await generateMiningSentences(generationRequest, {
|
|
@@ -1103,6 +1723,7 @@ function toSentenceBoardEntries(entries) {
|
|
|
1103
1723
|
rank: entry.rank,
|
|
1104
1724
|
domainName: entry.domainName,
|
|
1105
1725
|
sentence: entry.sentence,
|
|
1726
|
+
requiredWords: resolveBip39WordsFromIndices(entry.bip39WordIndices),
|
|
1106
1727
|
}));
|
|
1107
1728
|
}
|
|
1108
1729
|
async function runCompetitivenessGate(options) {
|
|
@@ -1534,7 +2155,7 @@ async function publishCandidateOnce(options) {
|
|
|
1534
2155
|
nodeBestHeight: options.readContext.nodeStatus?.nodeBestHeight ?? null,
|
|
1535
2156
|
snapshotState: options.readContext.snapshot.state,
|
|
1536
2157
|
})).state;
|
|
1537
|
-
const allUtxos = await rpc.listUnspent(state.managedCoreWallet.walletName,
|
|
2158
|
+
const allUtxos = await rpc.listUnspent(state.managedCoreWallet.walletName, MINING_FUNDING_MIN_CONF);
|
|
1538
2159
|
const conflictOutpoint = resolveMiningConflictOutpoint({
|
|
1539
2160
|
state,
|
|
1540
2161
|
allUtxos,
|
|
@@ -1801,6 +2422,29 @@ async function publishCandidate(options) {
|
|
|
1801
2422
|
candidate: refreshedCandidate,
|
|
1802
2423
|
};
|
|
1803
2424
|
}
|
|
2425
|
+
if (isInsufficientFundsError(error)) {
|
|
2426
|
+
const note = createInsufficientFundsMiningPublishWaitingNote();
|
|
2427
|
+
const lastError = createInsufficientFundsMiningPublishErrorMessage();
|
|
2428
|
+
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.", {
|
|
2429
|
+
level: "warn",
|
|
2430
|
+
runId: options.runId,
|
|
2431
|
+
targetBlockHeight: refreshedCandidate.targetBlockHeight,
|
|
2432
|
+
referencedBlockHashDisplay: refreshedCandidate.referencedBlockHashDisplay,
|
|
2433
|
+
domainId: refreshedCandidate.domainId,
|
|
2434
|
+
domainName: refreshedCandidate.domainName,
|
|
2435
|
+
score: refreshedCandidate.canonicalBlend.toString(),
|
|
2436
|
+
reason: "insufficient-funds",
|
|
2437
|
+
}));
|
|
2438
|
+
return {
|
|
2439
|
+
state: readyReadContext.localState.state,
|
|
2440
|
+
txid: null,
|
|
2441
|
+
decision: "publish-paused-insufficient-funds",
|
|
2442
|
+
note,
|
|
2443
|
+
lastError,
|
|
2444
|
+
skipped: true,
|
|
2445
|
+
candidate: null,
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
1804
2448
|
throw error;
|
|
1805
2449
|
}
|
|
1806
2450
|
}
|
|
@@ -1830,6 +2474,7 @@ export async function ensureBuiltInMiningSetupIfNeeded(options) {
|
|
|
1830
2474
|
return true;
|
|
1831
2475
|
}
|
|
1832
2476
|
async function performMiningCycle(options) {
|
|
2477
|
+
const now = options.nowImpl ?? Date.now;
|
|
1833
2478
|
let readContext = await options.openReadContext({
|
|
1834
2479
|
dataDir: options.dataDir,
|
|
1835
2480
|
databasePath: options.databasePath,
|
|
@@ -1838,30 +2483,39 @@ async function performMiningCycle(options) {
|
|
|
1838
2483
|
});
|
|
1839
2484
|
let readContextClosed = false;
|
|
1840
2485
|
try {
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
},
|
|
1852
|
-
});
|
|
1853
|
-
if (readContext.localState.availability !== "ready" || readContext.localState.state === null) {
|
|
1854
|
-
await refreshAndSaveStatus({
|
|
2486
|
+
let clearRecoveredBitcoindError = false;
|
|
2487
|
+
const saveCycleStatus = async (readContext, overrides, includeVisualizer = true) => {
|
|
2488
|
+
const statusNowUnixMs = now();
|
|
2489
|
+
const resolvedOverrides = clearRecoveredBitcoindError && overrides.lastError === undefined
|
|
2490
|
+
? {
|
|
2491
|
+
...overrides,
|
|
2492
|
+
lastError: null,
|
|
2493
|
+
}
|
|
2494
|
+
: overrides;
|
|
2495
|
+
return await refreshAndSaveStatus({
|
|
1855
2496
|
paths: options.paths,
|
|
1856
2497
|
provider: options.provider,
|
|
1857
2498
|
readContext,
|
|
1858
2499
|
overrides: {
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
note: "Wallet state must be locally available for mining to continue.",
|
|
2500
|
+
...buildMiningSettleWindowStatusOverrides(options.loopState, statusNowUnixMs),
|
|
2501
|
+
...resolvedOverrides,
|
|
1862
2502
|
},
|
|
1863
|
-
visualizer: options.visualizer,
|
|
1864
|
-
visualizerState: options.loopState.ui,
|
|
2503
|
+
visualizer: includeVisualizer ? options.visualizer : undefined,
|
|
2504
|
+
visualizerState: includeVisualizer ? options.loopState.ui : undefined,
|
|
2505
|
+
});
|
|
2506
|
+
};
|
|
2507
|
+
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
2508
|
+
await saveCycleStatus(readContext, {
|
|
2509
|
+
runMode: options.runMode,
|
|
2510
|
+
backgroundWorkerPid: options.backgroundWorkerPid,
|
|
2511
|
+
backgroundWorkerRunId: options.backgroundWorkerRunId,
|
|
2512
|
+
backgroundWorkerHeartbeatAtUnixMs: options.runMode === "background" ? now() : null,
|
|
2513
|
+
}, false);
|
|
2514
|
+
if (readContext.localState.availability !== "ready" || readContext.localState.state === null) {
|
|
2515
|
+
await saveCycleStatus(readContext, {
|
|
2516
|
+
runMode: options.runMode,
|
|
2517
|
+
currentPhase: "waiting",
|
|
2518
|
+
note: "Wallet state must be locally available for mining to continue.",
|
|
1865
2519
|
});
|
|
1866
2520
|
return;
|
|
1867
2521
|
}
|
|
@@ -1911,18 +2565,25 @@ async function performMiningCycle(options) {
|
|
|
1911
2565
|
indexedTipHashHex: indexedTip?.blockHashHex ?? null,
|
|
1912
2566
|
}).catch(() => ({}));
|
|
1913
2567
|
syncMiningVisualizerBlockTimes(options.loopState, visibleBlockTimes);
|
|
2568
|
+
const { targetBlockHeight, tipKey, tipChanged } = syncMiningUiForCurrentTip({
|
|
2569
|
+
loopState: options.loopState,
|
|
2570
|
+
snapshotState: effectiveReadContext.snapshot?.state ?? null,
|
|
2571
|
+
snapshotTipHeight: effectiveReadContext.snapshot?.tip?.height ?? effectiveReadContext.indexer.snapshotTip?.height ?? null,
|
|
2572
|
+
snapshotTipPreviousHashHex: effectiveReadContext.snapshot?.tip?.previousHashHex ?? effectiveReadContext.indexer.snapshotTip?.previousHashHex ?? null,
|
|
2573
|
+
nodeBestHeight: effectiveReadContext.nodeStatus?.nodeBestHeight ?? null,
|
|
2574
|
+
nodeBestHash: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
2575
|
+
recentWin: reconciliation.recentWin,
|
|
2576
|
+
});
|
|
2577
|
+
if (tipChanged) {
|
|
2578
|
+
setMiningTipSettleWindow(options.loopState, now());
|
|
2579
|
+
}
|
|
2580
|
+
const displaySats = await resolveFundingDisplaySats(effectiveReadContext.localState.state, rpc).catch(() => null);
|
|
2581
|
+
syncMiningVisualizerBalances(options.loopState, effectiveReadContext, displaySats);
|
|
1914
2582
|
if (effectiveReadContext.localState.state.miningState.state === "repair-required") {
|
|
1915
|
-
await
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
overrides: {
|
|
1920
|
-
runMode: options.runMode,
|
|
1921
|
-
currentPhase: "waiting",
|
|
1922
|
-
note: "Mining is blocked until the current mining publish is repaired or reconciled.",
|
|
1923
|
-
},
|
|
1924
|
-
visualizer: options.visualizer,
|
|
1925
|
-
visualizerState: options.loopState.ui,
|
|
2583
|
+
await saveCycleStatus(effectiveReadContext, {
|
|
2584
|
+
runMode: options.runMode,
|
|
2585
|
+
currentPhase: "waiting",
|
|
2586
|
+
note: "Mining is blocked until the current mining publish is repaired or reconciled.",
|
|
1926
2587
|
});
|
|
1927
2588
|
return;
|
|
1928
2589
|
}
|
|
@@ -1944,17 +2605,10 @@ async function performMiningCycle(options) {
|
|
|
1944
2605
|
state: nextState,
|
|
1945
2606
|
},
|
|
1946
2607
|
};
|
|
1947
|
-
await
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
overrides: {
|
|
1952
|
-
runMode: options.runMode,
|
|
1953
|
-
currentPhase: "waiting",
|
|
1954
|
-
note: "Mining is paused while another wallet mutation is active.",
|
|
1955
|
-
},
|
|
1956
|
-
visualizer: options.visualizer,
|
|
1957
|
-
visualizerState: options.loopState.ui,
|
|
2608
|
+
await saveCycleStatus(effectiveReadContext, {
|
|
2609
|
+
runMode: options.runMode,
|
|
2610
|
+
currentPhase: "waiting",
|
|
2611
|
+
note: "Mining is paused while another wallet mutation is active.",
|
|
1958
2612
|
});
|
|
1959
2613
|
return;
|
|
1960
2614
|
}
|
|
@@ -1972,23 +2626,16 @@ async function performMiningCycle(options) {
|
|
|
1972
2626
|
provider: options.provider,
|
|
1973
2627
|
paths: options.paths,
|
|
1974
2628
|
});
|
|
1975
|
-
await
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
localState: {
|
|
1981
|
-
...effectiveReadContext.localState,
|
|
1982
|
-
state: nextState,
|
|
1983
|
-
},
|
|
1984
|
-
},
|
|
1985
|
-
overrides: {
|
|
1986
|
-
runMode: options.runMode,
|
|
1987
|
-
currentPhase: "waiting",
|
|
1988
|
-
note: "Mining is paused while another wallet command is preempting sentence generation.",
|
|
2629
|
+
await saveCycleStatus({
|
|
2630
|
+
...effectiveReadContext,
|
|
2631
|
+
localState: {
|
|
2632
|
+
...effectiveReadContext.localState,
|
|
2633
|
+
state: nextState,
|
|
1989
2634
|
},
|
|
1990
|
-
|
|
1991
|
-
|
|
2635
|
+
}, {
|
|
2636
|
+
runMode: options.runMode,
|
|
2637
|
+
currentPhase: "waiting",
|
|
2638
|
+
note: "Mining is paused while another wallet command is preempting sentence generation.",
|
|
1992
2639
|
});
|
|
1993
2640
|
return;
|
|
1994
2641
|
}
|
|
@@ -2003,53 +2650,36 @@ async function performMiningCycle(options) {
|
|
|
2003
2650
|
network: networkInfo,
|
|
2004
2651
|
mempool: mempoolInfo,
|
|
2005
2652
|
});
|
|
2653
|
+
clearRecoveredBitcoindError = resetMiningBitcoindRecoveryState(options.loopState, effectiveReadContext.nodeStatus?.serviceStatus ?? { pid: service.pid });
|
|
2006
2654
|
if (corePublishState !== "healthy") {
|
|
2007
|
-
await
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
runMode: options.runMode,
|
|
2013
|
-
currentPhase: "waiting-bitcoin-network",
|
|
2014
|
-
corePublishState,
|
|
2015
|
-
note: "Mining is waiting for the local Bitcoin node to become publishable.",
|
|
2016
|
-
},
|
|
2017
|
-
visualizer: options.visualizer,
|
|
2018
|
-
visualizerState: options.loopState.ui,
|
|
2655
|
+
await saveCycleStatus(effectiveReadContext, {
|
|
2656
|
+
runMode: options.runMode,
|
|
2657
|
+
currentPhase: "waiting-bitcoin-network",
|
|
2658
|
+
corePublishState,
|
|
2659
|
+
note: "Mining is waiting for the local Bitcoin node to become publishable.",
|
|
2019
2660
|
});
|
|
2020
2661
|
return;
|
|
2021
2662
|
}
|
|
2022
2663
|
if (effectiveReadContext.indexer.health !== "synced" || effectiveReadContext.nodeHealth !== "synced") {
|
|
2023
|
-
await
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
: "waiting-bitcoin-network",
|
|
2032
|
-
note: effectiveReadContext.indexer.health !== "synced"
|
|
2033
|
-
? "Mining is waiting for Bitcoin Core and the indexer to align."
|
|
2034
|
-
: "Mining is waiting for the local Bitcoin node to become publishable.",
|
|
2035
|
-
},
|
|
2036
|
-
visualizer: options.visualizer,
|
|
2037
|
-
visualizerState: options.loopState.ui,
|
|
2664
|
+
await saveCycleStatus(effectiveReadContext, {
|
|
2665
|
+
runMode: options.runMode,
|
|
2666
|
+
currentPhase: effectiveReadContext.indexer.health !== "synced"
|
|
2667
|
+
? "waiting-indexer"
|
|
2668
|
+
: "waiting-bitcoin-network",
|
|
2669
|
+
note: effectiveReadContext.indexer.health !== "synced"
|
|
2670
|
+
? "Mining is waiting for Bitcoin Core and the indexer to align."
|
|
2671
|
+
: "Mining is waiting for the local Bitcoin node to become publishable.",
|
|
2038
2672
|
});
|
|
2039
2673
|
return;
|
|
2040
2674
|
}
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
}
|
|
2675
|
+
if (targetBlockHeight === null) {
|
|
2676
|
+
await saveCycleStatus(effectiveReadContext, {
|
|
2677
|
+
runMode: options.runMode,
|
|
2678
|
+
currentPhase: "waiting-bitcoin-network",
|
|
2679
|
+
note: "Mining is waiting for the local Bitcoin node to become publishable.",
|
|
2680
|
+
});
|
|
2681
|
+
return;
|
|
2049
2682
|
}
|
|
2050
|
-
syncMiningUiSettledBoard(options.loopState, effectiveReadContext.snapshot?.state ?? null, effectiveReadContext.snapshot?.tip?.height ?? effectiveReadContext.indexer.snapshotTip?.height ?? null, effectiveReadContext.nodeStatus?.nodeBestHeight ?? null);
|
|
2051
|
-
const displaySats = await resolveFundingDisplaySats(effectiveReadContext.localState.state, rpc).catch(() => null);
|
|
2052
|
-
syncMiningVisualizerBalances(options.loopState, effectiveReadContext, displaySats);
|
|
2053
2683
|
if (getBlockRewardCogtoshi(targetBlockHeight) === 0n) {
|
|
2054
2684
|
const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
|
|
2055
2685
|
state: "paused",
|
|
@@ -2060,24 +2690,17 @@ async function performMiningCycle(options) {
|
|
|
2060
2690
|
provider: options.provider,
|
|
2061
2691
|
paths: options.paths,
|
|
2062
2692
|
});
|
|
2063
|
-
await
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
localState: {
|
|
2069
|
-
...effectiveReadContext.localState,
|
|
2070
|
-
state: nextState,
|
|
2071
|
-
},
|
|
2072
|
-
},
|
|
2073
|
-
overrides: {
|
|
2074
|
-
runMode: options.runMode,
|
|
2075
|
-
currentPhase: "idle",
|
|
2076
|
-
currentPublishDecision: "publish-skipped-zero-reward",
|
|
2077
|
-
note: "Mining is disabled because the target block reward is zero.",
|
|
2693
|
+
await saveCycleStatus({
|
|
2694
|
+
...effectiveReadContext,
|
|
2695
|
+
localState: {
|
|
2696
|
+
...effectiveReadContext.localState,
|
|
2697
|
+
state: nextState,
|
|
2078
2698
|
},
|
|
2079
|
-
|
|
2080
|
-
|
|
2699
|
+
}, {
|
|
2700
|
+
runMode: options.runMode,
|
|
2701
|
+
currentPhase: "idle",
|
|
2702
|
+
currentPublishDecision: "publish-skipped-zero-reward",
|
|
2703
|
+
note: "Mining is disabled because the target block reward is zero.",
|
|
2081
2704
|
});
|
|
2082
2705
|
await appendEvent(options.paths, createEvent("publish-skipped-zero-reward", "Skipped mining because the target block reward is zero.", {
|
|
2083
2706
|
targetBlockHeight,
|
|
@@ -2087,17 +2710,10 @@ async function performMiningCycle(options) {
|
|
|
2087
2710
|
return;
|
|
2088
2711
|
}
|
|
2089
2712
|
if (tipKey !== null && options.loopState.attemptedTipKey === tipKey) {
|
|
2090
|
-
await
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
overrides: {
|
|
2095
|
-
runMode: options.runMode,
|
|
2096
|
-
currentPhase: "waiting",
|
|
2097
|
-
note: options.loopState.waitingNote ?? "Waiting for the next block after the last mining attempt on this tip.",
|
|
2098
|
-
},
|
|
2099
|
-
visualizer: options.visualizer,
|
|
2100
|
-
visualizerState: options.loopState.ui,
|
|
2713
|
+
await saveCycleStatus(effectiveReadContext, {
|
|
2714
|
+
runMode: options.runMode,
|
|
2715
|
+
currentPhase: "waiting",
|
|
2716
|
+
note: options.loopState.waitingNote ?? "Waiting for the next block after the last mining attempt on this tip.",
|
|
2101
2717
|
});
|
|
2102
2718
|
return;
|
|
2103
2719
|
}
|
|
@@ -2135,31 +2751,17 @@ async function performMiningCycle(options) {
|
|
|
2135
2751
|
if (selectedCandidate === null) {
|
|
2136
2752
|
const domains = resolveEligibleAnchoredRoots(effectiveReadContext);
|
|
2137
2753
|
if (domains.length === 0) {
|
|
2138
|
-
await
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
overrides: {
|
|
2143
|
-
runMode: options.runMode,
|
|
2144
|
-
currentPhase: "idle",
|
|
2145
|
-
note: "No locally controlled anchored root domains are currently eligible to mine.",
|
|
2146
|
-
},
|
|
2147
|
-
visualizer: options.visualizer,
|
|
2148
|
-
visualizerState: options.loopState.ui,
|
|
2754
|
+
await saveCycleStatus(effectiveReadContext, {
|
|
2755
|
+
runMode: options.runMode,
|
|
2756
|
+
currentPhase: "idle",
|
|
2757
|
+
note: "No locally controlled anchored root domains are currently eligible to mine.",
|
|
2149
2758
|
});
|
|
2150
2759
|
return;
|
|
2151
2760
|
}
|
|
2152
|
-
await
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
overrides: {
|
|
2157
|
-
runMode: options.runMode,
|
|
2158
|
-
currentPhase: "generating",
|
|
2159
|
-
note: "Generating mining sentences for eligible root domains.",
|
|
2160
|
-
},
|
|
2161
|
-
visualizer: options.visualizer,
|
|
2162
|
-
visualizerState: options.loopState.ui,
|
|
2761
|
+
await saveCycleStatus(effectiveReadContext, {
|
|
2762
|
+
runMode: options.runMode,
|
|
2763
|
+
currentPhase: "generating",
|
|
2764
|
+
note: "Generating mining sentences for eligible root domains.",
|
|
2163
2765
|
});
|
|
2164
2766
|
await appendEvent(options.paths, createEvent("sentence-generation-start", "Started mining sentence generation.", {
|
|
2165
2767
|
targetBlockHeight,
|
|
@@ -2186,19 +2788,12 @@ async function performMiningCycle(options) {
|
|
|
2186
2788
|
options.loopState.attemptedTipKey = tipKey;
|
|
2187
2789
|
options.loopState.waitingNote = "Mining is waiting for the sentence provider to recover.";
|
|
2188
2790
|
}
|
|
2189
|
-
await
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
currentPhase: "waiting-provider",
|
|
2196
|
-
providerState: error.providerState,
|
|
2197
|
-
lastError: error.message,
|
|
2198
|
-
note: "Mining is waiting for the sentence provider to recover.",
|
|
2199
|
-
},
|
|
2200
|
-
visualizer: options.visualizer,
|
|
2201
|
-
visualizerState: options.loopState.ui,
|
|
2791
|
+
await saveCycleStatus(effectiveReadContext, {
|
|
2792
|
+
runMode: options.runMode,
|
|
2793
|
+
currentPhase: "waiting-provider",
|
|
2794
|
+
providerState: error.providerState,
|
|
2795
|
+
lastError: error.message,
|
|
2796
|
+
note: "Mining is waiting for the sentence provider to recover.",
|
|
2202
2797
|
});
|
|
2203
2798
|
await appendEvent(options.paths, createEvent("publish-paused-provider", error.message, {
|
|
2204
2799
|
level: "warn",
|
|
@@ -2241,19 +2836,12 @@ async function performMiningCycle(options) {
|
|
|
2241
2836
|
options.loopState.attemptedTipKey = tipKey;
|
|
2242
2837
|
options.loopState.waitingNote = "Mining sentence generation failed for the current tip.";
|
|
2243
2838
|
}
|
|
2244
|
-
await
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
currentPhase: "waiting-provider",
|
|
2251
|
-
providerState: "unavailable",
|
|
2252
|
-
lastError: failureMessage,
|
|
2253
|
-
note: "Mining sentence generation failed for the current tip.",
|
|
2254
|
-
},
|
|
2255
|
-
visualizer: options.visualizer,
|
|
2256
|
-
visualizerState: options.loopState.ui,
|
|
2839
|
+
await saveCycleStatus(effectiveReadContext, {
|
|
2840
|
+
runMode: options.runMode,
|
|
2841
|
+
currentPhase: "waiting-provider",
|
|
2842
|
+
providerState: "unavailable",
|
|
2843
|
+
lastError: failureMessage,
|
|
2844
|
+
note: "Mining sentence generation failed for the current tip.",
|
|
2257
2845
|
});
|
|
2258
2846
|
await appendEvent(options.paths, createEvent("sentence-generation-failed", failureMessage, {
|
|
2259
2847
|
level: "error",
|
|
@@ -2263,17 +2851,10 @@ async function performMiningCycle(options) {
|
|
|
2263
2851
|
}));
|
|
2264
2852
|
return;
|
|
2265
2853
|
}
|
|
2266
|
-
await
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
overrides: {
|
|
2271
|
-
runMode: options.runMode,
|
|
2272
|
-
currentPhase: "scoring",
|
|
2273
|
-
note: "Scoring mining candidates for the current tip.",
|
|
2274
|
-
},
|
|
2275
|
-
visualizer: options.visualizer,
|
|
2276
|
-
visualizerState: options.loopState.ui,
|
|
2854
|
+
await saveCycleStatus(effectiveReadContext, {
|
|
2855
|
+
runMode: options.runMode,
|
|
2856
|
+
currentPhase: "scoring",
|
|
2857
|
+
note: "Scoring mining candidates for the current tip.",
|
|
2277
2858
|
});
|
|
2278
2859
|
const best = await chooseBestLocalCandidate(candidates);
|
|
2279
2860
|
if (best === null) {
|
|
@@ -2282,18 +2863,11 @@ async function performMiningCycle(options) {
|
|
|
2282
2863
|
options.loopState.waitingNote = "No publishable mining candidate passed scoring gates for the current tip.";
|
|
2283
2864
|
}
|
|
2284
2865
|
clearSelectedCandidate(options.loopState);
|
|
2285
|
-
await
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
runMode: options.runMode,
|
|
2291
|
-
currentPhase: "idle",
|
|
2292
|
-
currentPublishDecision: "publish-skipped-no-candidate",
|
|
2293
|
-
note: "No publishable mining candidate passed scoring gates for the current tip.",
|
|
2294
|
-
},
|
|
2295
|
-
visualizer: options.visualizer,
|
|
2296
|
-
visualizerState: options.loopState.ui,
|
|
2866
|
+
await saveCycleStatus(effectiveReadContext, {
|
|
2867
|
+
runMode: options.runMode,
|
|
2868
|
+
currentPhase: "idle",
|
|
2869
|
+
currentPublishDecision: "publish-skipped-no-candidate",
|
|
2870
|
+
note: "No publishable mining candidate passed scoring gates for the current tip.",
|
|
2297
2871
|
});
|
|
2298
2872
|
await appendEvent(options.paths, createEvent("publish-skipped-no-candidate", "No publishable mining candidate passed scoring gates.", {
|
|
2299
2873
|
targetBlockHeight,
|
|
@@ -2340,25 +2914,18 @@ async function performMiningCycle(options) {
|
|
|
2340
2914
|
: gate.decision === "suppressed-top5-mempool"
|
|
2341
2915
|
? `Best local sentence found, but ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
|
|
2342
2916
|
: "Mining skipped this tick because the mempool competitiveness gate could not be verified safely.";
|
|
2343
|
-
await
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
|
|
2356
|
-
lastMempoolSequence: gate.lastMempoolSequence,
|
|
2357
|
-
lastCompetitivenessGateAtUnixMs: Date.now(),
|
|
2358
|
-
note: options.loopState.waitingNote,
|
|
2359
|
-
},
|
|
2360
|
-
visualizer: options.visualizer,
|
|
2361
|
-
visualizerState: options.loopState.ui,
|
|
2917
|
+
await saveCycleStatus(effectiveReadContext, {
|
|
2918
|
+
runMode: options.runMode,
|
|
2919
|
+
currentPhase: "waiting",
|
|
2920
|
+
currentPublishDecision: gate.decision,
|
|
2921
|
+
sameDomainCompetitorSuppressed: gate.sameDomainCompetitorSuppressed,
|
|
2922
|
+
higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
|
|
2923
|
+
dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
|
|
2924
|
+
competitivenessGateIndeterminate: gate.competitivenessGateIndeterminate,
|
|
2925
|
+
mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
|
|
2926
|
+
lastMempoolSequence: gate.lastMempoolSequence,
|
|
2927
|
+
lastCompetitivenessGateAtUnixMs: now(),
|
|
2928
|
+
note: options.loopState.waitingNote,
|
|
2362
2929
|
});
|
|
2363
2930
|
await appendEvent(options.paths, createEvent(gate.decision === "suppressed-same-domain-mempool"
|
|
2364
2931
|
? "publish-skipped-same-domain-mempool"
|
|
@@ -2387,21 +2954,12 @@ async function performMiningCycle(options) {
|
|
|
2387
2954
|
if (!await ensureCurrentIndexerTruthOrRestart()) {
|
|
2388
2955
|
return;
|
|
2389
2956
|
}
|
|
2390
|
-
await
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
currentPhase: effectiveReadContext.localState.state.miningState.currentTxid === null
|
|
2397
|
-
? "publishing"
|
|
2398
|
-
: "replacing",
|
|
2399
|
-
note: effectiveReadContext.localState.state.miningState.currentTxid === null
|
|
2400
|
-
? "Broadcasting the best mining candidate for the current tip."
|
|
2401
|
-
: "Replacing the live mining transaction for the current tip.",
|
|
2402
|
-
},
|
|
2403
|
-
visualizer: options.visualizer,
|
|
2404
|
-
visualizerState: options.loopState.ui,
|
|
2957
|
+
await saveCycleStatus(effectiveReadContext, {
|
|
2958
|
+
runMode: options.runMode,
|
|
2959
|
+
...buildPrePublishStatusOverrides({
|
|
2960
|
+
state: effectiveReadContext.localState.state,
|
|
2961
|
+
candidate: selectedCandidate,
|
|
2962
|
+
}),
|
|
2405
2963
|
});
|
|
2406
2964
|
const publishLock = await acquireFileLock(options.paths.walletControlLockPath, {
|
|
2407
2965
|
purpose: "wallet-mine",
|
|
@@ -2432,32 +2990,25 @@ async function performMiningCycle(options) {
|
|
|
2432
2990
|
if (published.retryable === true) {
|
|
2433
2991
|
cacheSelectedCandidateForTip(options.loopState, tipKey, published.candidate);
|
|
2434
2992
|
options.loopState.waitingNote = published.note;
|
|
2435
|
-
await
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
localState: {
|
|
2441
|
-
...effectiveReadContext.localState,
|
|
2442
|
-
state: published.state,
|
|
2443
|
-
},
|
|
2444
|
-
},
|
|
2445
|
-
overrides: {
|
|
2446
|
-
runMode: options.runMode,
|
|
2447
|
-
currentPhase: "waiting",
|
|
2448
|
-
currentPublishDecision: published.decision,
|
|
2449
|
-
sameDomainCompetitorSuppressed: false,
|
|
2450
|
-
higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
|
|
2451
|
-
dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
|
|
2452
|
-
competitivenessGateIndeterminate: false,
|
|
2453
|
-
mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
|
|
2454
|
-
lastMempoolSequence: gateSnapshot.lastMempoolSequence,
|
|
2455
|
-
lastCompetitivenessGateAtUnixMs: Date.now(),
|
|
2456
|
-
note: published.note,
|
|
2457
|
-
livePublishInMempool: published.state.miningState.livePublishInMempool,
|
|
2993
|
+
await saveCycleStatus({
|
|
2994
|
+
...effectiveReadContext,
|
|
2995
|
+
localState: {
|
|
2996
|
+
...effectiveReadContext.localState,
|
|
2997
|
+
state: published.state,
|
|
2458
2998
|
},
|
|
2459
|
-
|
|
2460
|
-
|
|
2999
|
+
}, {
|
|
3000
|
+
runMode: options.runMode,
|
|
3001
|
+
currentPhase: "waiting",
|
|
3002
|
+
currentPublishDecision: published.decision,
|
|
3003
|
+
sameDomainCompetitorSuppressed: false,
|
|
3004
|
+
higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
|
|
3005
|
+
dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
|
|
3006
|
+
competitivenessGateIndeterminate: false,
|
|
3007
|
+
mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
|
|
3008
|
+
lastMempoolSequence: gateSnapshot.lastMempoolSequence,
|
|
3009
|
+
lastCompetitivenessGateAtUnixMs: now(),
|
|
3010
|
+
note: published.note,
|
|
3011
|
+
livePublishInMempool: published.state.miningState.livePublishInMempool,
|
|
2461
3012
|
});
|
|
2462
3013
|
return;
|
|
2463
3014
|
}
|
|
@@ -2465,32 +3016,29 @@ async function performMiningCycle(options) {
|
|
|
2465
3016
|
clearSelectedCandidate(options.loopState);
|
|
2466
3017
|
setMiningUiCandidate(options.loopState, selectedCandidate);
|
|
2467
3018
|
options.loopState.waitingNote = published.note;
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
},
|
|
2477
|
-
},
|
|
2478
|
-
overrides: {
|
|
2479
|
-
runMode: options.runMode,
|
|
2480
|
-
currentPhase: "waiting",
|
|
2481
|
-
currentPublishDecision: published.decision,
|
|
2482
|
-
sameDomainCompetitorSuppressed: false,
|
|
2483
|
-
higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
|
|
2484
|
-
dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
|
|
2485
|
-
competitivenessGateIndeterminate: false,
|
|
2486
|
-
mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
|
|
2487
|
-
lastMempoolSequence: gateSnapshot.lastMempoolSequence,
|
|
2488
|
-
lastCompetitivenessGateAtUnixMs: Date.now(),
|
|
2489
|
-
note: published.note,
|
|
2490
|
-
livePublishInMempool: published.state.miningState.livePublishInMempool,
|
|
3019
|
+
const lastError = published.decision === "publish-paused-insufficient-funds"
|
|
3020
|
+
? published.lastError ?? createInsufficientFundsMiningPublishErrorMessage()
|
|
3021
|
+
: undefined;
|
|
3022
|
+
await saveCycleStatus({
|
|
3023
|
+
...effectiveReadContext,
|
|
3024
|
+
localState: {
|
|
3025
|
+
...effectiveReadContext.localState,
|
|
3026
|
+
state: published.state,
|
|
2491
3027
|
},
|
|
2492
|
-
|
|
2493
|
-
|
|
3028
|
+
}, {
|
|
3029
|
+
runMode: options.runMode,
|
|
3030
|
+
currentPhase: "waiting",
|
|
3031
|
+
currentPublishDecision: published.decision,
|
|
3032
|
+
sameDomainCompetitorSuppressed: false,
|
|
3033
|
+
higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
|
|
3034
|
+
dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
|
|
3035
|
+
competitivenessGateIndeterminate: false,
|
|
3036
|
+
mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
|
|
3037
|
+
lastMempoolSequence: gateSnapshot.lastMempoolSequence,
|
|
3038
|
+
lastCompetitivenessGateAtUnixMs: now(),
|
|
3039
|
+
lastError,
|
|
3040
|
+
note: published.note,
|
|
3041
|
+
livePublishInMempool: published.state.miningState.livePublishInMempool,
|
|
2494
3042
|
});
|
|
2495
3043
|
return;
|
|
2496
3044
|
}
|
|
@@ -2506,32 +3054,25 @@ async function performMiningCycle(options) {
|
|
|
2506
3054
|
: `Mining candidate ${published.decision === "replaced"
|
|
2507
3055
|
? "replaced"
|
|
2508
3056
|
: "broadcast"} as ${published.txid}. Waiting for the next block.`;
|
|
2509
|
-
await
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
localState: {
|
|
2515
|
-
...effectiveReadContext.localState,
|
|
2516
|
-
state: published.state,
|
|
2517
|
-
},
|
|
2518
|
-
},
|
|
2519
|
-
overrides: {
|
|
2520
|
-
runMode: options.runMode,
|
|
2521
|
-
currentPhase: "waiting",
|
|
2522
|
-
currentPublishDecision: published.decision,
|
|
2523
|
-
sameDomainCompetitorSuppressed: false,
|
|
2524
|
-
higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
|
|
2525
|
-
dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
|
|
2526
|
-
competitivenessGateIndeterminate: false,
|
|
2527
|
-
mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
|
|
2528
|
-
lastMempoolSequence: gateSnapshot.lastMempoolSequence,
|
|
2529
|
-
lastCompetitivenessGateAtUnixMs: Date.now(),
|
|
2530
|
-
note: options.loopState.waitingNote,
|
|
2531
|
-
livePublishInMempool: published.state.miningState.livePublishInMempool,
|
|
3057
|
+
await saveCycleStatus({
|
|
3058
|
+
...effectiveReadContext,
|
|
3059
|
+
localState: {
|
|
3060
|
+
...effectiveReadContext.localState,
|
|
3061
|
+
state: published.state,
|
|
2532
3062
|
},
|
|
2533
|
-
|
|
2534
|
-
|
|
3063
|
+
}, {
|
|
3064
|
+
runMode: options.runMode,
|
|
3065
|
+
currentPhase: "waiting",
|
|
3066
|
+
currentPublishDecision: published.decision,
|
|
3067
|
+
sameDomainCompetitorSuppressed: false,
|
|
3068
|
+
higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
|
|
3069
|
+
dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
|
|
3070
|
+
competitivenessGateIndeterminate: false,
|
|
3071
|
+
mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
|
|
3072
|
+
lastMempoolSequence: gateSnapshot.lastMempoolSequence,
|
|
3073
|
+
lastCompetitivenessGateAtUnixMs: now(),
|
|
3074
|
+
note: options.loopState.waitingNote,
|
|
3075
|
+
livePublishInMempool: published.state.miningState.livePublishInMempool,
|
|
2535
3076
|
});
|
|
2536
3077
|
}
|
|
2537
3078
|
finally {
|
|
@@ -2540,6 +3081,7 @@ async function performMiningCycle(options) {
|
|
|
2540
3081
|
}
|
|
2541
3082
|
catch (error) {
|
|
2542
3083
|
if (error instanceof MiningSuspendDetectedError) {
|
|
3084
|
+
discardMiningLoopTransientWork(options.loopState, readContext?.localState.walletRootId ?? undefined);
|
|
2543
3085
|
if (readContext !== null && !readContextClosed) {
|
|
2544
3086
|
await readContext.close();
|
|
2545
3087
|
readContextClosed = true;
|
|
@@ -2555,6 +3097,24 @@ async function performMiningCycle(options) {
|
|
|
2555
3097
|
detectedAtUnixMs: error.detectedAtUnixMs,
|
|
2556
3098
|
openReadContext: options.openReadContext,
|
|
2557
3099
|
visualizer: options.visualizer,
|
|
3100
|
+
loopState: options.loopState,
|
|
3101
|
+
});
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
if (readContext !== null && isRecoverableMiningBitcoindError(error)) {
|
|
3105
|
+
await handleRecoverableMiningBitcoindFailure({
|
|
3106
|
+
error,
|
|
3107
|
+
dataDir: options.dataDir,
|
|
3108
|
+
provider: options.provider,
|
|
3109
|
+
paths: options.paths,
|
|
3110
|
+
runMode: options.runMode,
|
|
3111
|
+
readContext,
|
|
3112
|
+
loopState: options.loopState,
|
|
3113
|
+
attachService: options.attachService,
|
|
3114
|
+
probeService: options.probeService,
|
|
3115
|
+
stopService: options.stopService,
|
|
3116
|
+
nowUnixMs: now(),
|
|
3117
|
+
visualizer: options.visualizer,
|
|
2558
3118
|
});
|
|
2559
3119
|
return;
|
|
2560
3120
|
}
|
|
@@ -2656,6 +3216,9 @@ async function attemptSaveMempool(rpc, paths, runId) {
|
|
|
2656
3216
|
async function runMiningLoop(options) {
|
|
2657
3217
|
const suspendDetector = createMiningSuspendDetector();
|
|
2658
3218
|
const loopState = createMiningLoopState();
|
|
3219
|
+
const probeService = options.probeService ?? probeManagedBitcoindService;
|
|
3220
|
+
const stopService = options.stopService ?? stopManagedBitcoindService;
|
|
3221
|
+
const sleepImpl = options.sleepImpl ?? sleep;
|
|
2659
3222
|
await appendEvent(options.paths, createEvent("runtime-start", `Started ${options.runMode} mining runtime.`, {
|
|
2660
3223
|
runId: options.backgroundWorkerRunId,
|
|
2661
3224
|
}));
|
|
@@ -2667,6 +3230,7 @@ async function runMiningLoop(options) {
|
|
|
2667
3230
|
if (!(error instanceof MiningSuspendDetectedError)) {
|
|
2668
3231
|
throw error;
|
|
2669
3232
|
}
|
|
3233
|
+
discardMiningLoopTransientWork(loopState, null);
|
|
2670
3234
|
await handleDetectedMiningRuntimeResume({
|
|
2671
3235
|
dataDir: options.dataDir,
|
|
2672
3236
|
databasePath: options.databasePath,
|
|
@@ -2678,6 +3242,7 @@ async function runMiningLoop(options) {
|
|
|
2678
3242
|
detectedAtUnixMs: error.detectedAtUnixMs,
|
|
2679
3243
|
openReadContext: options.openReadContext,
|
|
2680
3244
|
visualizer: options.visualizer,
|
|
3245
|
+
loopState,
|
|
2681
3246
|
});
|
|
2682
3247
|
continue;
|
|
2683
3248
|
}
|
|
@@ -2685,8 +3250,10 @@ async function runMiningLoop(options) {
|
|
|
2685
3250
|
...options,
|
|
2686
3251
|
suspendDetector,
|
|
2687
3252
|
loopState,
|
|
3253
|
+
probeService,
|
|
3254
|
+
stopService,
|
|
2688
3255
|
});
|
|
2689
|
-
await
|
|
3256
|
+
await sleepImpl(Math.min(MINING_LOOP_INTERVAL_MS, MINING_STATUS_HEARTBEAT_INTERVAL_MS), options.signal);
|
|
2690
3257
|
}
|
|
2691
3258
|
const service = await options.attachService({
|
|
2692
3259
|
dataDir: options.dataDir,
|
|
@@ -2723,36 +3290,52 @@ export async function runForegroundMining(options) {
|
|
|
2723
3290
|
const openReadContext = options.openReadContext ?? openWalletReadContext;
|
|
2724
3291
|
const attachService = options.attachService ?? attachOrStartManagedBitcoindService;
|
|
2725
3292
|
const rpcFactory = options.rpcFactory ?? createRpcClient;
|
|
2726
|
-
const
|
|
3293
|
+
const requestMiningPreemption = options.requestMiningPreemption ?? requestMiningGenerationPreemption;
|
|
3294
|
+
const runMiningLoopImpl = options.runMiningLoopImpl ?? runMiningLoop;
|
|
3295
|
+
const saveStopSnapshotImpl = options.saveStopSnapshotImpl ?? saveStopSnapshot;
|
|
3296
|
+
let visualizer = null;
|
|
3297
|
+
const setupReady = options.builtInSetupEnsured === true
|
|
3298
|
+
? true
|
|
3299
|
+
: await ensureBuiltInMiningSetupIfNeeded({
|
|
3300
|
+
provider,
|
|
3301
|
+
prompter: options.prompter,
|
|
3302
|
+
paths,
|
|
3303
|
+
});
|
|
3304
|
+
if (!setupReady) {
|
|
3305
|
+
throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
|
|
3306
|
+
}
|
|
3307
|
+
const controlLock = await acquireMiningStartControlLock({
|
|
3308
|
+
paths,
|
|
2727
3309
|
purpose: "mine-foreground",
|
|
3310
|
+
takeoverReason: "mine-foreground-replace",
|
|
3311
|
+
requestMiningPreemption,
|
|
3312
|
+
shutdownGraceMs: options.shutdownGraceMs,
|
|
3313
|
+
sleepImpl: options.sleepImpl,
|
|
2728
3314
|
});
|
|
2729
|
-
|
|
3315
|
+
const abortController = new AbortController();
|
|
3316
|
+
const abortListener = () => {
|
|
3317
|
+
abortController.abort();
|
|
3318
|
+
};
|
|
3319
|
+
const handleSigint = () => abortController.abort();
|
|
3320
|
+
const handleSigterm = () => abortController.abort();
|
|
2730
3321
|
try {
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
provider,
|
|
2739
|
-
prompter: options.prompter,
|
|
2740
|
-
paths,
|
|
2741
|
-
});
|
|
2742
|
-
if (!setupReady) {
|
|
2743
|
-
throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
|
|
2744
|
-
}
|
|
3322
|
+
await takeOverMiningRuntime({
|
|
3323
|
+
paths,
|
|
3324
|
+
reason: "mine-foreground-replace",
|
|
3325
|
+
requestMiningPreemption,
|
|
3326
|
+
shutdownGraceMs: options.shutdownGraceMs,
|
|
3327
|
+
sleepImpl: options.sleepImpl,
|
|
3328
|
+
});
|
|
2745
3329
|
visualizer = new MiningFollowVisualizer({
|
|
3330
|
+
clientVersion: options.clientVersion,
|
|
3331
|
+
updateAvailable: options.updateAvailable,
|
|
2746
3332
|
progressOutput: options.progressOutput ?? "auto",
|
|
2747
3333
|
stream: options.stderr,
|
|
2748
3334
|
});
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
process.on("SIGINT", () => abortController.abort());
|
|
2754
|
-
process.on("SIGTERM", () => abortController.abort());
|
|
2755
|
-
await runMiningLoop({
|
|
3335
|
+
options.signal?.addEventListener("abort", abortListener, { once: true });
|
|
3336
|
+
process.on("SIGINT", handleSigint);
|
|
3337
|
+
process.on("SIGTERM", handleSigterm);
|
|
3338
|
+
await runMiningLoopImpl({
|
|
2756
3339
|
dataDir: options.dataDir,
|
|
2757
3340
|
databasePath: options.databasePath,
|
|
2758
3341
|
provider,
|
|
@@ -2768,7 +3351,7 @@ export async function runForegroundMining(options) {
|
|
|
2768
3351
|
stdout: options.stdout,
|
|
2769
3352
|
visualizer,
|
|
2770
3353
|
});
|
|
2771
|
-
await
|
|
3354
|
+
await saveStopSnapshotImpl({
|
|
2772
3355
|
dataDir: options.dataDir,
|
|
2773
3356
|
databasePath: options.databasePath,
|
|
2774
3357
|
provider,
|
|
@@ -2780,6 +3363,9 @@ export async function runForegroundMining(options) {
|
|
|
2780
3363
|
});
|
|
2781
3364
|
}
|
|
2782
3365
|
finally {
|
|
3366
|
+
options.signal?.removeEventListener("abort", abortListener);
|
|
3367
|
+
process.off("SIGINT", handleSigint);
|
|
3368
|
+
process.off("SIGTERM", handleSigterm);
|
|
2783
3369
|
visualizer?.close();
|
|
2784
3370
|
await controlLock.release();
|
|
2785
3371
|
}
|
|
@@ -2787,35 +3373,50 @@ export async function runForegroundMining(options) {
|
|
|
2787
3373
|
export async function startBackgroundMining(options) {
|
|
2788
3374
|
const provider = options.provider ?? createDefaultWalletSecretProvider();
|
|
2789
3375
|
const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
|
|
2790
|
-
const
|
|
2791
|
-
|
|
2792
|
-
|
|
3376
|
+
const requestMiningPreemption = options.requestMiningPreemption ?? requestMiningGenerationPreemption;
|
|
3377
|
+
const spawnWorkerProcess = options.spawnWorkerProcess ?? spawn;
|
|
3378
|
+
const waitForBackgroundHealthyImpl = options.waitForBackgroundHealthyImpl ?? waitForBackgroundHealthy;
|
|
3379
|
+
const setupReady = options.builtInSetupEnsured === true
|
|
3380
|
+
? true
|
|
3381
|
+
: await ensureBuiltInMiningSetupIfNeeded({
|
|
3382
|
+
provider,
|
|
3383
|
+
prompter: options.prompter,
|
|
3384
|
+
paths,
|
|
3385
|
+
});
|
|
3386
|
+
if (!setupReady) {
|
|
3387
|
+
throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
|
|
3388
|
+
}
|
|
3389
|
+
let controlLock;
|
|
2793
3390
|
try {
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
3391
|
+
controlLock = await acquireMiningStartControlLock({
|
|
3392
|
+
paths,
|
|
3393
|
+
purpose: "mine-start",
|
|
3394
|
+
takeoverReason: "mine-start-replace",
|
|
3395
|
+
requestMiningPreemption,
|
|
3396
|
+
shutdownGraceMs: options.shutdownGraceMs,
|
|
3397
|
+
sleepImpl: options.sleepImpl,
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
catch (error) {
|
|
3401
|
+
if (error instanceof FileLockBusyError && error.existingMetadata?.processId === process.pid) {
|
|
2798
3402
|
return {
|
|
2799
3403
|
started: false,
|
|
2800
|
-
snapshot:
|
|
3404
|
+
snapshot: await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null),
|
|
2801
3405
|
};
|
|
2802
3406
|
}
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
:
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
if (!setupReady) {
|
|
2814
|
-
throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
|
|
2815
|
-
}
|
|
3407
|
+
throw error;
|
|
3408
|
+
}
|
|
3409
|
+
try {
|
|
3410
|
+
await takeOverMiningRuntime({
|
|
3411
|
+
paths,
|
|
3412
|
+
reason: "mine-start-replace",
|
|
3413
|
+
requestMiningPreemption,
|
|
3414
|
+
shutdownGraceMs: options.shutdownGraceMs,
|
|
3415
|
+
sleepImpl: options.sleepImpl,
|
|
3416
|
+
});
|
|
2816
3417
|
const runId = randomBytes(16).toString("hex");
|
|
2817
3418
|
const workerMainPath = fileURLToPath(new URL("./worker-main.js", import.meta.url));
|
|
2818
|
-
const child =
|
|
3419
|
+
const child = spawnWorkerProcess(process.execPath, [
|
|
2819
3420
|
workerMainPath,
|
|
2820
3421
|
`--data-dir=${options.dataDir}`,
|
|
2821
3422
|
`--database-path=${options.databasePath}`,
|
|
@@ -2825,7 +3426,7 @@ export async function startBackgroundMining(options) {
|
|
|
2825
3426
|
stdio: "ignore",
|
|
2826
3427
|
});
|
|
2827
3428
|
child.unref();
|
|
2828
|
-
const snapshot = await
|
|
3429
|
+
const snapshot = await waitForBackgroundHealthyImpl(paths);
|
|
2829
3430
|
return {
|
|
2830
3431
|
started: true,
|
|
2831
3432
|
snapshot,
|
|
@@ -2957,14 +3558,33 @@ export async function runBackgroundMiningWorker(options) {
|
|
|
2957
3558
|
});
|
|
2958
3559
|
}
|
|
2959
3560
|
export async function handleDetectedMiningRuntimeResumeForTesting(options) {
|
|
2960
|
-
await handleDetectedMiningRuntimeResume(
|
|
3561
|
+
await handleDetectedMiningRuntimeResume({
|
|
3562
|
+
...options,
|
|
3563
|
+
loopState: options.loopState ?? createMiningLoopState(),
|
|
3564
|
+
});
|
|
3565
|
+
}
|
|
3566
|
+
export async function takeOverMiningRuntimeForTesting(options) {
|
|
3567
|
+
return await takeOverMiningRuntime(options);
|
|
2961
3568
|
}
|
|
2962
3569
|
export async function performMiningCycleForTesting(options) {
|
|
2963
3570
|
await performMiningCycle({
|
|
2964
3571
|
...options,
|
|
3572
|
+
probeService: options.probeService ?? probeManagedBitcoindService,
|
|
3573
|
+
stopService: options.stopService ?? stopManagedBitcoindService,
|
|
2965
3574
|
loopState: options.loopState ?? createMiningLoopState(),
|
|
2966
3575
|
});
|
|
2967
3576
|
}
|
|
3577
|
+
export async function runMiningLoopForTesting(options) {
|
|
3578
|
+
await runMiningLoop({
|
|
3579
|
+
...options,
|
|
3580
|
+
});
|
|
3581
|
+
}
|
|
3582
|
+
export function buildPrePublishStatusOverridesForTesting(options) {
|
|
3583
|
+
return buildPrePublishStatusOverrides(options);
|
|
3584
|
+
}
|
|
3585
|
+
export function buildStatusSnapshotForTesting(view, overrides = {}) {
|
|
3586
|
+
return buildStatusSnapshot(view, overrides);
|
|
3587
|
+
}
|
|
2968
3588
|
export function shouldKeepCurrentTipLivePublishForTesting(options) {
|
|
2969
3589
|
return livePublishTargetsCandidateTip(options);
|
|
2970
3590
|
}
|