@cogcoin/client 1.1.4 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/dist/bitcoind/progress/tty-renderer.js +3 -2
- package/dist/bitcoind/service.js +1 -1
- package/dist/cli/command-registry.d.ts +39 -0
- package/dist/cli/command-registry.js +1132 -0
- package/dist/cli/commands/client-admin.js +6 -56
- package/dist/cli/commands/mining-admin.js +9 -32
- package/dist/cli/commands/mining-read.js +15 -56
- package/dist/cli/commands/mining-runtime.js +258 -57
- package/dist/cli/commands/service-runtime.js +1 -64
- package/dist/cli/commands/status.js +2 -15
- package/dist/cli/commands/update.js +6 -21
- package/dist/cli/commands/wallet-admin.js +18 -120
- package/dist/cli/commands/wallet-mutation.js +4 -7
- package/dist/cli/commands/wallet-read.js +31 -138
- package/dist/cli/context.js +2 -4
- package/dist/cli/mining-format.js +8 -2
- package/dist/cli/mutation-command-groups.d.ts +11 -11
- package/dist/cli/mutation-command-groups.js +9 -18
- package/dist/cli/mutation-json.d.ts +1 -17
- package/dist/cli/mutation-json.js +1 -28
- package/dist/cli/mutation-success.d.ts +0 -1
- package/dist/cli/mutation-success.js +0 -19
- package/dist/cli/output.d.ts +1 -10
- package/dist/cli/output.js +52 -481
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +38 -695
- package/dist/cli/runner.js +28 -113
- package/dist/cli/types.d.ts +7 -8
- package/dist/cli/update-notifier.js +1 -1
- package/dist/cli/wallet-format.js +1 -1
- package/dist/wallet/lifecycle/managed-core.d.ts +23 -0
- package/dist/wallet/lifecycle/managed-core.js +257 -0
- package/dist/wallet/lifecycle/repair-mining.d.ts +49 -0
- package/dist/wallet/lifecycle/repair-mining.js +304 -0
- package/dist/wallet/lifecycle/repair-runtime.d.ts +36 -0
- package/dist/wallet/lifecycle/repair-runtime.js +206 -0
- package/dist/wallet/lifecycle/repair.d.ts +11 -0
- package/dist/wallet/lifecycle/repair.js +368 -0
- package/dist/wallet/lifecycle/setup.d.ts +16 -0
- package/dist/wallet/lifecycle/setup.js +430 -0
- package/dist/wallet/lifecycle/types.d.ts +125 -0
- package/dist/wallet/lifecycle/types.js +1 -0
- package/dist/wallet/lifecycle.d.ts +4 -165
- package/dist/wallet/lifecycle.js +3 -1656
- package/dist/wallet/mining/candidate.d.ts +60 -0
- package/dist/wallet/mining/candidate.js +290 -0
- package/dist/wallet/mining/competitiveness.d.ts +22 -0
- package/dist/wallet/mining/competitiveness.js +640 -0
- package/dist/wallet/mining/control.js +7 -251
- package/dist/wallet/mining/cycle.d.ts +39 -0
- package/dist/wallet/mining/cycle.js +542 -0
- package/dist/wallet/mining/engine-state.d.ts +66 -0
- package/dist/wallet/mining/engine-state.js +211 -0
- package/dist/wallet/mining/engine-types.d.ts +173 -0
- package/dist/wallet/mining/engine-types.js +1 -0
- package/dist/wallet/mining/engine-utils.d.ts +7 -0
- package/dist/wallet/mining/engine-utils.js +75 -0
- package/dist/wallet/mining/events.d.ts +2 -0
- package/dist/wallet/mining/events.js +19 -0
- package/dist/wallet/mining/lifecycle.d.ts +71 -0
- package/dist/wallet/mining/lifecycle.js +355 -0
- package/dist/wallet/mining/projection.d.ts +61 -0
- package/dist/wallet/mining/projection.js +319 -0
- package/dist/wallet/mining/publish.d.ts +79 -0
- package/dist/wallet/mining/publish.js +614 -0
- package/dist/wallet/mining/runner.d.ts +12 -418
- package/dist/wallet/mining/runner.js +274 -3433
- package/dist/wallet/mining/supervisor.d.ts +134 -0
- package/dist/wallet/mining/supervisor.js +558 -0
- package/dist/wallet/mining/visualizer-sync.d.ts +42 -0
- package/dist/wallet/mining/visualizer-sync.js +166 -0
- package/dist/wallet/mining/visualizer.d.ts +1 -0
- package/dist/wallet/mining/visualizer.js +33 -18
- package/dist/wallet/reset.d.ts +1 -1
- package/dist/wallet/reset.js +35 -11
- package/dist/wallet/runtime.d.ts +0 -6
- package/dist/wallet/runtime.js +2 -38
- package/dist/wallet/tx/common.d.ts +18 -0
- package/dist/wallet/tx/common.js +40 -26
- package/package.json +1 -1
- package/dist/wallet/state/seed-index.d.ts +0 -43
- package/dist/wallet/state/seed-index.js +0 -151
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import { assaySentences } from "@cogcoin/scoring";
|
|
2
|
+
import { acquireFileLock } from "../fs/lock.js";
|
|
3
|
+
import { openWalletReadContext } from "../read/index.js";
|
|
4
|
+
import { createMiningEventRecord } from "./events.js";
|
|
5
|
+
import { ensureIndexerTruthIsCurrent, generateCandidatesForDomains, getIndexerTruthKey, resolveEligibleAnchoredRoots, chooseBestLocalCandidate, } from "./candidate.js";
|
|
6
|
+
import { clearMiningGateCache, runCompetitivenessGate } from "./competitiveness.js";
|
|
7
|
+
import { createInsufficientFundsMiningPublishErrorMessage, createInsufficientFundsMiningPublishWaitingNote, publishCandidate, probeMiningFundingAvailability, } from "./publish.js";
|
|
8
|
+
import { cacheSelectedCandidateForTip, clearMiningProviderWait, clearSelectedCandidate, getSelectedCandidateForTip, isTransientMiningProviderError, recordTerminalMiningProviderWait, recordTransientMiningProviderWait, setMiningUiCandidate, } from "./engine-state.js";
|
|
9
|
+
import { MiningProviderRequestError } from "./sentences.js";
|
|
10
|
+
import { isInsufficientFundsError } from "../tx/common.js";
|
|
11
|
+
import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
|
|
12
|
+
import { createRpcClient } from "../../bitcoind/node.js";
|
|
13
|
+
import { buildPrePublishStatusOverrides, } from "./projection.js";
|
|
14
|
+
function createInitialState(options) {
|
|
15
|
+
return {
|
|
16
|
+
phase: "idle",
|
|
17
|
+
targetBlockHeight: options.targetBlockHeight,
|
|
18
|
+
tipKey: options.tipKey,
|
|
19
|
+
selectedCandidate: getSelectedCandidateForTip(options.loopState, options.tipKey),
|
|
20
|
+
generatedCandidates: null,
|
|
21
|
+
gateSnapshot: {
|
|
22
|
+
higherRankedCompetitorDomainCount: 0,
|
|
23
|
+
dedupedCompetitorDomainCount: 0,
|
|
24
|
+
mempoolSequenceCacheStatus: null,
|
|
25
|
+
lastMempoolSequence: null,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export async function runMiningPhaseMachine(options) {
|
|
30
|
+
const now = options.nowImpl ?? Date.now;
|
|
31
|
+
const generateCandidatesImpl = options.generateCandidatesForDomainsImpl ?? generateCandidatesForDomains;
|
|
32
|
+
const runGateImpl = options.runCompetitivenessGateImpl ?? runCompetitivenessGate;
|
|
33
|
+
const state = createInitialState({
|
|
34
|
+
targetBlockHeight: options.targetBlockHeight,
|
|
35
|
+
tipKey: options.tipKey,
|
|
36
|
+
loopState: options.loopState,
|
|
37
|
+
});
|
|
38
|
+
const indexerTruthKey = getIndexerTruthKey(options.readContext);
|
|
39
|
+
const walletRootId = options.readContext.localState.walletRootId;
|
|
40
|
+
const ensureCurrentIndexerTruthOrRestart = async () => {
|
|
41
|
+
try {
|
|
42
|
+
await ensureIndexerTruthIsCurrent({
|
|
43
|
+
dataDir: options.readContext.dataDir,
|
|
44
|
+
truthKey: indexerTruthKey,
|
|
45
|
+
});
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (!(error instanceof Error) || error.message !== "mining_generation_stale_indexer_truth") {
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
clearMiningGateCache(walletRootId);
|
|
53
|
+
await options.appendEvent(createMiningEventRecord("generation-restarted-indexer-truth", "Detected updated coherent indexer truth during mining; restarting on the next tick.", {
|
|
54
|
+
level: "warn",
|
|
55
|
+
targetBlockHeight: state.targetBlockHeight,
|
|
56
|
+
referencedBlockHashDisplay: options.readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
57
|
+
runId: options.backgroundWorkerRunId,
|
|
58
|
+
}));
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
while (true) {
|
|
63
|
+
switch (state.phase) {
|
|
64
|
+
case "idle": {
|
|
65
|
+
if (options.corePublishState !== "healthy") {
|
|
66
|
+
clearMiningProviderWait(options.loopState);
|
|
67
|
+
await options.saveCycleStatus(options.readContext, {
|
|
68
|
+
runMode: options.runMode,
|
|
69
|
+
currentPhase: "waiting-bitcoin-network",
|
|
70
|
+
corePublishState: options.corePublishState,
|
|
71
|
+
note: "Mining is waiting for the local Bitcoin node to become publishable.",
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (options.readContext.indexer.health !== "synced" || options.readContext.nodeHealth !== "synced") {
|
|
76
|
+
clearMiningProviderWait(options.loopState);
|
|
77
|
+
await options.saveCycleStatus(options.readContext, {
|
|
78
|
+
runMode: options.runMode,
|
|
79
|
+
currentPhase: options.readContext.indexer.health !== "synced"
|
|
80
|
+
? "waiting-indexer"
|
|
81
|
+
: "waiting-bitcoin-network",
|
|
82
|
+
note: options.readContext.indexer.health !== "synced"
|
|
83
|
+
? "Mining is waiting for Bitcoin Core and the indexer to align."
|
|
84
|
+
: "Mining is waiting for the local Bitcoin node to become publishable.",
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (state.targetBlockHeight === null) {
|
|
89
|
+
clearMiningProviderWait(options.loopState);
|
|
90
|
+
await options.saveCycleStatus(options.readContext, {
|
|
91
|
+
runMode: options.runMode,
|
|
92
|
+
currentPhase: "waiting-bitcoin-network",
|
|
93
|
+
note: "Mining is waiting for the local Bitcoin node to become publishable.",
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const eligibleDomains = resolveEligibleAnchoredRoots(options.readContext);
|
|
98
|
+
if (state.selectedCandidate === null) {
|
|
99
|
+
if (eligibleDomains.length === 0) {
|
|
100
|
+
clearMiningProviderWait(options.loopState);
|
|
101
|
+
await options.saveCycleStatus(options.readContext, {
|
|
102
|
+
runMode: options.runMode,
|
|
103
|
+
currentPhase: "idle",
|
|
104
|
+
currentPublishDecision: null,
|
|
105
|
+
lastError: null,
|
|
106
|
+
note: "No locally controlled anchored root domains are currently eligible to mine.",
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
await probeMiningFundingAvailability({
|
|
112
|
+
rpc: options.rpc,
|
|
113
|
+
walletName: options.readContext.localState.state.managedCoreWallet.walletName,
|
|
114
|
+
state: options.readContext.localState.state,
|
|
115
|
+
domains: eligibleDomains,
|
|
116
|
+
referencedBlockHashDisplay: options.readContext.nodeStatus?.nodeBestHashHex ?? "00".repeat(32),
|
|
117
|
+
targetBlockHeight: state.targetBlockHeight,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (isInsufficientFundsError(error)) {
|
|
122
|
+
clearMiningProviderWait(options.loopState);
|
|
123
|
+
clearSelectedCandidate(options.loopState);
|
|
124
|
+
options.loopState.waitingNote = createInsufficientFundsMiningPublishWaitingNote();
|
|
125
|
+
await options.saveCycleStatus(options.readContext, {
|
|
126
|
+
runMode: options.runMode,
|
|
127
|
+
currentPhase: "waiting",
|
|
128
|
+
currentPublishDecision: "publish-paused-insufficient-funds",
|
|
129
|
+
lastError: createInsufficientFundsMiningPublishErrorMessage(),
|
|
130
|
+
note: createInsufficientFundsMiningPublishWaitingNote(),
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (options.loopState.providerWaitState !== null
|
|
138
|
+
&& options.loopState.providerWaitLastError !== null) {
|
|
139
|
+
if (options.loopState.providerWaitNextRetryAtUnixMs !== null
|
|
140
|
+
&& now() < options.loopState.providerWaitNextRetryAtUnixMs) {
|
|
141
|
+
await options.saveCycleStatus(options.readContext, {
|
|
142
|
+
runMode: options.runMode,
|
|
143
|
+
currentPhase: "waiting-provider",
|
|
144
|
+
currentPublishDecision: null,
|
|
145
|
+
providerState: options.loopState.providerWaitState,
|
|
146
|
+
lastError: options.loopState.providerWaitLastError,
|
|
147
|
+
note: "Mining is waiting for the sentence provider to recover.",
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (options.loopState.providerWaitNextRetryAtUnixMs === null
|
|
152
|
+
&& state.tipKey !== null
|
|
153
|
+
&& options.loopState.attemptedTipKey === state.tipKey) {
|
|
154
|
+
await options.saveCycleStatus(options.readContext, {
|
|
155
|
+
runMode: options.runMode,
|
|
156
|
+
currentPhase: "waiting-provider",
|
|
157
|
+
currentPublishDecision: null,
|
|
158
|
+
providerState: options.loopState.providerWaitState,
|
|
159
|
+
lastError: options.loopState.providerWaitLastError,
|
|
160
|
+
note: "Mining is waiting for the sentence provider to recover.",
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
clearMiningProviderWait(options.loopState, options.loopState.providerWaitNextRetryAtUnixMs === null);
|
|
165
|
+
}
|
|
166
|
+
if (state.tipKey !== null && options.loopState.attemptedTipKey === state.tipKey) {
|
|
167
|
+
await options.saveCycleStatus(options.readContext, {
|
|
168
|
+
runMode: options.runMode,
|
|
169
|
+
currentPhase: "waiting",
|
|
170
|
+
lastError: null,
|
|
171
|
+
note: options.loopState.waitingNote ?? "Waiting for the next block after the last mining attempt on this tip.",
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
state.phase = state.selectedCandidate === null ? "generating" : "publishing";
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
case "generating": {
|
|
179
|
+
await options.saveCycleStatus(options.readContext, {
|
|
180
|
+
runMode: options.runMode,
|
|
181
|
+
currentPhase: "generating",
|
|
182
|
+
currentPublishDecision: null,
|
|
183
|
+
lastError: null,
|
|
184
|
+
note: "Generating mining sentences for eligible root domains.",
|
|
185
|
+
});
|
|
186
|
+
await options.appendEvent(createMiningEventRecord("sentence-generation-start", "Started mining sentence generation.", {
|
|
187
|
+
targetBlockHeight: state.targetBlockHeight,
|
|
188
|
+
referencedBlockHashDisplay: options.readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
189
|
+
runId: options.backgroundWorkerRunId,
|
|
190
|
+
}));
|
|
191
|
+
try {
|
|
192
|
+
state.generatedCandidates = await generateCandidatesImpl({
|
|
193
|
+
rpc: options.rpc,
|
|
194
|
+
readContext: options.readContext,
|
|
195
|
+
domains: resolveEligibleAnchoredRoots(options.readContext),
|
|
196
|
+
provider: options.provider,
|
|
197
|
+
paths: options.paths,
|
|
198
|
+
indexerTruthKey,
|
|
199
|
+
runId: options.backgroundWorkerRunId,
|
|
200
|
+
fetchImpl: options.fetchImpl,
|
|
201
|
+
});
|
|
202
|
+
options.throwIfSuspendDetected?.();
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error instanceof MiningProviderRequestError) {
|
|
206
|
+
if (isTransientMiningProviderError(error)) {
|
|
207
|
+
recordTransientMiningProviderWait({
|
|
208
|
+
loopState: options.loopState,
|
|
209
|
+
error,
|
|
210
|
+
nowUnixMs: now(),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
recordTerminalMiningProviderWait({
|
|
215
|
+
loopState: options.loopState,
|
|
216
|
+
error,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (!isTransientMiningProviderError(error) && state.tipKey !== null) {
|
|
220
|
+
options.loopState.attemptedTipKey = state.tipKey;
|
|
221
|
+
}
|
|
222
|
+
await options.saveCycleStatus(options.readContext, {
|
|
223
|
+
runMode: options.runMode,
|
|
224
|
+
currentPhase: "waiting-provider",
|
|
225
|
+
currentPublishDecision: null,
|
|
226
|
+
providerState: options.loopState.providerWaitState ?? error.providerState,
|
|
227
|
+
lastError: error.message,
|
|
228
|
+
note: "Mining is waiting for the sentence provider to recover.",
|
|
229
|
+
});
|
|
230
|
+
await options.appendEvent(createMiningEventRecord("publish-paused-provider", error.message, {
|
|
231
|
+
level: "warn",
|
|
232
|
+
targetBlockHeight: state.targetBlockHeight,
|
|
233
|
+
referencedBlockHashDisplay: options.readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
234
|
+
runId: options.backgroundWorkerRunId,
|
|
235
|
+
}));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (error instanceof Error && error.message === "mining_generation_stale_tip") {
|
|
239
|
+
await options.appendEvent(createMiningEventRecord("generation-restarted-new-tip", "Detected a new best tip during sentence generation; restarting on the next tick.", {
|
|
240
|
+
level: "warn",
|
|
241
|
+
targetBlockHeight: state.targetBlockHeight,
|
|
242
|
+
referencedBlockHashDisplay: options.readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
243
|
+
runId: options.backgroundWorkerRunId,
|
|
244
|
+
}));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (error instanceof Error && error.message === "mining_generation_stale_indexer_truth") {
|
|
248
|
+
clearMiningProviderWait(options.loopState);
|
|
249
|
+
clearMiningGateCache(walletRootId);
|
|
250
|
+
await options.appendEvent(createMiningEventRecord("generation-restarted-indexer-truth", "Detected updated coherent indexer truth during mining; restarting on the next tick.", {
|
|
251
|
+
level: "warn",
|
|
252
|
+
targetBlockHeight: state.targetBlockHeight,
|
|
253
|
+
referencedBlockHashDisplay: options.readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
254
|
+
runId: options.backgroundWorkerRunId,
|
|
255
|
+
}));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (error instanceof Error && error.message === "mining_generation_preempted") {
|
|
259
|
+
clearMiningProviderWait(options.loopState);
|
|
260
|
+
await options.appendEvent(createMiningEventRecord("generation-paused-preempted", "Stopped sentence generation because another wallet command requested mining preemption.", {
|
|
261
|
+
level: "warn",
|
|
262
|
+
targetBlockHeight: state.targetBlockHeight,
|
|
263
|
+
referencedBlockHashDisplay: options.readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
264
|
+
runId: options.backgroundWorkerRunId,
|
|
265
|
+
}));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
clearMiningProviderWait(options.loopState);
|
|
269
|
+
const failureMessage = error instanceof Error ? error.message : String(error);
|
|
270
|
+
if (state.tipKey !== null) {
|
|
271
|
+
options.loopState.attemptedTipKey = state.tipKey;
|
|
272
|
+
options.loopState.waitingNote = "Mining sentence generation failed for the current tip.";
|
|
273
|
+
}
|
|
274
|
+
await options.saveCycleStatus(options.readContext, {
|
|
275
|
+
runMode: options.runMode,
|
|
276
|
+
currentPhase: "waiting-provider",
|
|
277
|
+
currentPublishDecision: null,
|
|
278
|
+
providerState: "unavailable",
|
|
279
|
+
lastError: failureMessage,
|
|
280
|
+
note: "Mining sentence generation failed for the current tip.",
|
|
281
|
+
});
|
|
282
|
+
await options.appendEvent(createMiningEventRecord("sentence-generation-failed", failureMessage, {
|
|
283
|
+
level: "error",
|
|
284
|
+
targetBlockHeight: state.targetBlockHeight,
|
|
285
|
+
referencedBlockHashDisplay: options.readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
286
|
+
runId: options.backgroundWorkerRunId,
|
|
287
|
+
}));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
state.phase = "scoring";
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
case "scoring": {
|
|
294
|
+
clearMiningProviderWait(options.loopState);
|
|
295
|
+
await options.saveCycleStatus(options.readContext, {
|
|
296
|
+
runMode: options.runMode,
|
|
297
|
+
currentPhase: "scoring",
|
|
298
|
+
currentPublishDecision: null,
|
|
299
|
+
lastError: null,
|
|
300
|
+
note: "Scoring mining candidates for the current tip.",
|
|
301
|
+
});
|
|
302
|
+
const best = await chooseBestLocalCandidate(state.generatedCandidates ?? []);
|
|
303
|
+
if (best === null) {
|
|
304
|
+
if (state.tipKey !== null) {
|
|
305
|
+
options.loopState.attemptedTipKey = state.tipKey;
|
|
306
|
+
options.loopState.waitingNote = "No publishable mining candidate passed scoring gates for the current tip.";
|
|
307
|
+
}
|
|
308
|
+
clearSelectedCandidate(options.loopState);
|
|
309
|
+
await options.saveCycleStatus(options.readContext, {
|
|
310
|
+
runMode: options.runMode,
|
|
311
|
+
currentPhase: "idle",
|
|
312
|
+
currentPublishDecision: "publish-skipped-no-candidate",
|
|
313
|
+
note: "No publishable mining candidate passed scoring gates for the current tip.",
|
|
314
|
+
});
|
|
315
|
+
await options.appendEvent(createMiningEventRecord("publish-skipped-no-candidate", "No publishable mining candidate passed scoring gates.", {
|
|
316
|
+
targetBlockHeight: state.targetBlockHeight,
|
|
317
|
+
referencedBlockHashDisplay: options.readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
318
|
+
runId: options.backgroundWorkerRunId,
|
|
319
|
+
}));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (!await ensureCurrentIndexerTruthOrRestart()) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
options.loopState.ui.recentWin = null;
|
|
326
|
+
cacheSelectedCandidateForTip(options.loopState, state.tipKey, best, options.readContext.localState.state.miningState);
|
|
327
|
+
state.selectedCandidate = best;
|
|
328
|
+
await options.appendEvent(createMiningEventRecord("candidate-selected", `Selected ${best.domainName} with score ${best.canonicalBlend.toString()}.`, {
|
|
329
|
+
targetBlockHeight: best.targetBlockHeight,
|
|
330
|
+
referencedBlockHashDisplay: best.referencedBlockHashDisplay,
|
|
331
|
+
domainId: best.domainId,
|
|
332
|
+
domainName: best.domainName,
|
|
333
|
+
score: best.canonicalBlend.toString(),
|
|
334
|
+
runId: options.backgroundWorkerRunId,
|
|
335
|
+
}));
|
|
336
|
+
const gate = await runGateImpl({
|
|
337
|
+
rpc: options.rpc,
|
|
338
|
+
readContext: options.readContext,
|
|
339
|
+
candidate: best,
|
|
340
|
+
currentTxid: options.readContext.localState.state.miningState.currentTxid,
|
|
341
|
+
assaySentencesImpl: options.assaySentencesImpl,
|
|
342
|
+
cooperativeYield: options.cooperativeYieldImpl,
|
|
343
|
+
cooperativeYieldEvery: options.cooperativeYieldEvery,
|
|
344
|
+
});
|
|
345
|
+
options.throwIfSuspendDetected?.();
|
|
346
|
+
state.gateSnapshot = {
|
|
347
|
+
higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
|
|
348
|
+
dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
|
|
349
|
+
mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
|
|
350
|
+
lastMempoolSequence: gate.lastMempoolSequence,
|
|
351
|
+
};
|
|
352
|
+
if (!gate.allowed) {
|
|
353
|
+
if (state.tipKey !== null) {
|
|
354
|
+
options.loopState.attemptedTipKey = state.tipKey;
|
|
355
|
+
}
|
|
356
|
+
clearSelectedCandidate(options.loopState);
|
|
357
|
+
setMiningUiCandidate(options.loopState, best, options.readContext.localState.state.miningState);
|
|
358
|
+
options.loopState.waitingNote = gate.decision === "suppressed-same-domain-mempool"
|
|
359
|
+
? "Best local sentence found, but a same-domain mempool competitor already matches or beats it."
|
|
360
|
+
: gate.decision === "suppressed-top5-mempool"
|
|
361
|
+
? `Best local sentence found, but ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
|
|
362
|
+
: "Mining skipped this tick because the mempool competitiveness gate could not be verified safely.";
|
|
363
|
+
await options.saveCycleStatus(options.readContext, {
|
|
364
|
+
runMode: options.runMode,
|
|
365
|
+
currentPhase: "waiting",
|
|
366
|
+
currentPublishDecision: gate.decision,
|
|
367
|
+
sameDomainCompetitorSuppressed: gate.sameDomainCompetitorSuppressed,
|
|
368
|
+
higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
|
|
369
|
+
dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
|
|
370
|
+
competitivenessGateIndeterminate: gate.competitivenessGateIndeterminate,
|
|
371
|
+
mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
|
|
372
|
+
lastMempoolSequence: gate.lastMempoolSequence,
|
|
373
|
+
lastCompetitivenessGateAtUnixMs: now(),
|
|
374
|
+
note: options.loopState.waitingNote,
|
|
375
|
+
});
|
|
376
|
+
await options.appendEvent(createMiningEventRecord(gate.decision === "suppressed-same-domain-mempool"
|
|
377
|
+
? "publish-skipped-same-domain-mempool"
|
|
378
|
+
: gate.decision === "suppressed-top5-mempool"
|
|
379
|
+
? "publish-skipped-top5-mempool"
|
|
380
|
+
: "publish-skipped-gate-indeterminate", gate.decision === "suppressed-same-domain-mempool"
|
|
381
|
+
? "Skipped publish because a same-domain mempool competitor already outranks the local candidate."
|
|
382
|
+
: gate.decision === "suppressed-top5-mempool"
|
|
383
|
+
? `Skipped publish because ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
|
|
384
|
+
: "Skipped publish because the competitiveness gate could not be evaluated safely.", {
|
|
385
|
+
targetBlockHeight: best.targetBlockHeight,
|
|
386
|
+
referencedBlockHashDisplay: best.referencedBlockHashDisplay,
|
|
387
|
+
domainId: best.domainId,
|
|
388
|
+
domainName: best.domainName,
|
|
389
|
+
score: best.canonicalBlend.toString(),
|
|
390
|
+
runId: options.backgroundWorkerRunId,
|
|
391
|
+
reason: gate.decision,
|
|
392
|
+
}));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
state.phase = "publishing";
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
case "publishing":
|
|
399
|
+
case "replacing": {
|
|
400
|
+
const selectedCandidate = state.selectedCandidate;
|
|
401
|
+
if (selectedCandidate === null) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
options.loopState.ui.recentWin = null;
|
|
405
|
+
setMiningUiCandidate(options.loopState, selectedCandidate, options.readContext.localState.state.miningState);
|
|
406
|
+
if (!await ensureCurrentIndexerTruthOrRestart()) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
await options.saveCycleStatus(options.readContext, {
|
|
410
|
+
runMode: options.runMode,
|
|
411
|
+
...buildPrePublishStatusOverrides({
|
|
412
|
+
state: options.readContext.localState.state,
|
|
413
|
+
candidate: selectedCandidate,
|
|
414
|
+
}),
|
|
415
|
+
});
|
|
416
|
+
const publishLock = await acquireFileLock(options.paths.walletControlLockPath, {
|
|
417
|
+
purpose: "wallet-mine",
|
|
418
|
+
walletRootId: options.readContext.localState.state.walletRootId,
|
|
419
|
+
});
|
|
420
|
+
try {
|
|
421
|
+
if (!await ensureCurrentIndexerTruthOrRestart()) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
options.throwIfSuspendDetected?.();
|
|
425
|
+
const published = await publishCandidate({
|
|
426
|
+
dataDir: options.dataDir,
|
|
427
|
+
databasePath: options.databasePath,
|
|
428
|
+
provider: options.provider,
|
|
429
|
+
paths: options.paths,
|
|
430
|
+
fallbackState: options.readContext.localState.state,
|
|
431
|
+
openReadContext: options.openReadContext,
|
|
432
|
+
attachService: options.attachService,
|
|
433
|
+
rpcFactory: options.rpcFactory,
|
|
434
|
+
candidate: selectedCandidate,
|
|
435
|
+
runId: options.backgroundWorkerRunId,
|
|
436
|
+
appendEventFn: async (_paths, event) => {
|
|
437
|
+
await options.appendEvent(event);
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
if (state.tipKey !== null
|
|
441
|
+
&& published.retryable !== true
|
|
442
|
+
&& published.decision !== "publish-paused-insufficient-funds") {
|
|
443
|
+
options.loopState.attemptedTipKey = state.tipKey;
|
|
444
|
+
}
|
|
445
|
+
if (published.retryable === true) {
|
|
446
|
+
cacheSelectedCandidateForTip(options.loopState, state.tipKey, published.candidate, published.state.miningState);
|
|
447
|
+
options.loopState.waitingNote = published.note;
|
|
448
|
+
await options.saveCycleStatus({
|
|
449
|
+
...options.readContext,
|
|
450
|
+
localState: {
|
|
451
|
+
...options.readContext.localState,
|
|
452
|
+
state: published.state,
|
|
453
|
+
},
|
|
454
|
+
}, {
|
|
455
|
+
runMode: options.runMode,
|
|
456
|
+
currentPhase: "waiting",
|
|
457
|
+
currentPublishDecision: published.decision,
|
|
458
|
+
sameDomainCompetitorSuppressed: false,
|
|
459
|
+
higherRankedCompetitorDomainCount: state.gateSnapshot.higherRankedCompetitorDomainCount,
|
|
460
|
+
dedupedCompetitorDomainCount: state.gateSnapshot.dedupedCompetitorDomainCount,
|
|
461
|
+
competitivenessGateIndeterminate: false,
|
|
462
|
+
mempoolSequenceCacheStatus: state.gateSnapshot.mempoolSequenceCacheStatus,
|
|
463
|
+
lastMempoolSequence: state.gateSnapshot.lastMempoolSequence,
|
|
464
|
+
lastCompetitivenessGateAtUnixMs: now(),
|
|
465
|
+
note: published.note,
|
|
466
|
+
livePublishInMempool: published.state.miningState.livePublishInMempool,
|
|
467
|
+
});
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (published.skipped === true) {
|
|
471
|
+
clearSelectedCandidate(options.loopState);
|
|
472
|
+
setMiningUiCandidate(options.loopState, selectedCandidate, published.state.miningState);
|
|
473
|
+
options.loopState.waitingNote = published.note;
|
|
474
|
+
const lastError = published.decision === "publish-paused-insufficient-funds"
|
|
475
|
+
? published.lastError ?? createInsufficientFundsMiningPublishErrorMessage()
|
|
476
|
+
: undefined;
|
|
477
|
+
await options.saveCycleStatus({
|
|
478
|
+
...options.readContext,
|
|
479
|
+
localState: {
|
|
480
|
+
...options.readContext.localState,
|
|
481
|
+
state: published.state,
|
|
482
|
+
},
|
|
483
|
+
}, {
|
|
484
|
+
runMode: options.runMode,
|
|
485
|
+
currentPhase: "waiting",
|
|
486
|
+
currentPublishDecision: published.decision,
|
|
487
|
+
sameDomainCompetitorSuppressed: false,
|
|
488
|
+
higherRankedCompetitorDomainCount: state.gateSnapshot.higherRankedCompetitorDomainCount,
|
|
489
|
+
dedupedCompetitorDomainCount: state.gateSnapshot.dedupedCompetitorDomainCount,
|
|
490
|
+
competitivenessGateIndeterminate: false,
|
|
491
|
+
mempoolSequenceCacheStatus: state.gateSnapshot.mempoolSequenceCacheStatus,
|
|
492
|
+
lastMempoolSequence: state.gateSnapshot.lastMempoolSequence,
|
|
493
|
+
lastCompetitivenessGateAtUnixMs: now(),
|
|
494
|
+
lastError,
|
|
495
|
+
note: published.note,
|
|
496
|
+
livePublishInMempool: published.state.miningState.livePublishInMempool,
|
|
497
|
+
});
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
clearSelectedCandidate(options.loopState);
|
|
501
|
+
if (published.txid !== null) {
|
|
502
|
+
options.loopState.ui.latestTxid = published.txid;
|
|
503
|
+
}
|
|
504
|
+
setMiningUiCandidate(options.loopState, published.candidate, published.state.miningState);
|
|
505
|
+
options.loopState.waitingNote = published.decision === "kept-live-publish"
|
|
506
|
+
? "Existing live mining publish already covers this block attempt. Waiting for the next block."
|
|
507
|
+
: published.txid === null
|
|
508
|
+
? "Mining candidate was evaluated but the existing live publish stayed in place."
|
|
509
|
+
: `Mining candidate ${published.decision === "replaced"
|
|
510
|
+
? "replaced"
|
|
511
|
+
: "broadcast"} as ${published.txid}. Waiting for the next block.`;
|
|
512
|
+
await options.saveCycleStatus({
|
|
513
|
+
...options.readContext,
|
|
514
|
+
localState: {
|
|
515
|
+
...options.readContext.localState,
|
|
516
|
+
state: published.state,
|
|
517
|
+
},
|
|
518
|
+
}, {
|
|
519
|
+
runMode: options.runMode,
|
|
520
|
+
currentPhase: "waiting",
|
|
521
|
+
currentPublishDecision: published.decision,
|
|
522
|
+
sameDomainCompetitorSuppressed: false,
|
|
523
|
+
higherRankedCompetitorDomainCount: state.gateSnapshot.higherRankedCompetitorDomainCount,
|
|
524
|
+
dedupedCompetitorDomainCount: state.gateSnapshot.dedupedCompetitorDomainCount,
|
|
525
|
+
competitivenessGateIndeterminate: false,
|
|
526
|
+
mempoolSequenceCacheStatus: state.gateSnapshot.mempoolSequenceCacheStatus,
|
|
527
|
+
lastMempoolSequence: state.gateSnapshot.lastMempoolSequence,
|
|
528
|
+
lastCompetitivenessGateAtUnixMs: now(),
|
|
529
|
+
note: options.loopState.waitingNote,
|
|
530
|
+
livePublishInMempool: published.state.miningState.livePublishInMempool,
|
|
531
|
+
});
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
finally {
|
|
535
|
+
await publishLock.release();
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
default:
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { MiningStateRecord, WalletStateV1 } from "../types.js";
|
|
2
|
+
import type { MiningCandidate } from "./engine-types.js";
|
|
3
|
+
import type { MiningFollowVisualizerState } from "./visualizer.js";
|
|
4
|
+
import { MiningProviderRequestError } from "./sentences.js";
|
|
5
|
+
export interface MiningRuntimeLoopState {
|
|
6
|
+
attemptedTipKey: string | null;
|
|
7
|
+
currentTipKey: string | null;
|
|
8
|
+
selectedCandidateTipKey: string | null;
|
|
9
|
+
selectedCandidate: MiningCandidate | null;
|
|
10
|
+
ui: MiningFollowVisualizerState;
|
|
11
|
+
waitingNote: string | null;
|
|
12
|
+
providerWaitState: "backoff" | "rate-limited" | "auth-error" | "not-found" | null;
|
|
13
|
+
providerWaitLastError: string | null;
|
|
14
|
+
providerWaitNextRetryAtUnixMs: number | null;
|
|
15
|
+
providerTransientFailureCount: number;
|
|
16
|
+
bitcoinRecoveryFirstFailureAtUnixMs: number | null;
|
|
17
|
+
bitcoinRecoveryFirstUnreachableAtUnixMs: number | null;
|
|
18
|
+
bitcoinRecoveryLastRestartAttemptAtUnixMs: number | null;
|
|
19
|
+
bitcoinRecoveryServiceInstanceId: string | null;
|
|
20
|
+
bitcoinRecoveryProcessId: number | null;
|
|
21
|
+
reconnectSettledUntilUnixMs: number | null;
|
|
22
|
+
tipSettledUntilUnixMs: number | null;
|
|
23
|
+
}
|
|
24
|
+
export declare function createMiningRuntimeLoopState(): MiningRuntimeLoopState;
|
|
25
|
+
export declare function cloneMiningState(state: MiningStateRecord): MiningStateRecord;
|
|
26
|
+
export declare function defaultMiningStatePatch(state: WalletStateV1, patch: Partial<MiningStateRecord>): WalletStateV1;
|
|
27
|
+
export declare function hasBlockingMutation(state: WalletStateV1): boolean;
|
|
28
|
+
export declare function livePublishTargetsCandidateTip(options: {
|
|
29
|
+
liveState: MiningStateRecord;
|
|
30
|
+
candidate: MiningCandidate;
|
|
31
|
+
}): boolean;
|
|
32
|
+
export declare function miningCandidateIsCurrent(options: {
|
|
33
|
+
state: MiningStateRecord;
|
|
34
|
+
nodeBestHash: string | null;
|
|
35
|
+
nodeBestHeight: number | null;
|
|
36
|
+
}): boolean;
|
|
37
|
+
export declare function resolveSharedMiningConflictOutpoint(state: MiningStateRecord): {
|
|
38
|
+
txid: string;
|
|
39
|
+
vout: number;
|
|
40
|
+
} | null;
|
|
41
|
+
export declare function clearMiningProviderWait(loopState: MiningRuntimeLoopState, resetTransientFailureCount?: boolean): void;
|
|
42
|
+
export declare function recordTransientMiningProviderWait(options: {
|
|
43
|
+
loopState: MiningRuntimeLoopState;
|
|
44
|
+
error: MiningProviderRequestError;
|
|
45
|
+
nowUnixMs: number;
|
|
46
|
+
}): void;
|
|
47
|
+
export declare function recordTerminalMiningProviderWait(options: {
|
|
48
|
+
loopState: MiningRuntimeLoopState;
|
|
49
|
+
error: MiningProviderRequestError;
|
|
50
|
+
}): void;
|
|
51
|
+
export declare function isTransientMiningProviderError(error: MiningProviderRequestError): boolean;
|
|
52
|
+
export declare function expireMiningSettleWindows(loopState: MiningRuntimeLoopState, nowUnixMs: number): void;
|
|
53
|
+
export declare function setMiningReconnectSettleWindow(loopState: MiningRuntimeLoopState, nowUnixMs: number): void;
|
|
54
|
+
export declare function setMiningTipSettleWindow(loopState: MiningRuntimeLoopState, nowUnixMs: number): void;
|
|
55
|
+
export declare function buildMiningSettleWindowStatusOverrides(loopState: MiningRuntimeLoopState, nowUnixMs: number): {
|
|
56
|
+
reconnectSettledUntilUnixMs: number | null;
|
|
57
|
+
tipSettledUntilUnixMs: number | null;
|
|
58
|
+
};
|
|
59
|
+
export declare function buildMiningTipKey(bestBlockHash: string | null, targetBlockHeight: number | null): string | null;
|
|
60
|
+
export declare function resetMiningUiForTip(loopState: MiningRuntimeLoopState, _targetBlockHeight: number | null): void;
|
|
61
|
+
export declare function setMiningUiCandidate(loopState: MiningRuntimeLoopState, candidate: MiningCandidate, liveState?: MiningStateRecord | null): void;
|
|
62
|
+
export declare function getSelectedCandidateForTip(loopState: MiningRuntimeLoopState, tipKey: string | null): MiningCandidate | null;
|
|
63
|
+
export declare function cacheSelectedCandidateForTip(loopState: MiningRuntimeLoopState, tipKey: string | null, candidate: MiningCandidate, liveState?: MiningStateRecord | null): void;
|
|
64
|
+
export declare function clearSelectedCandidate(loopState: MiningRuntimeLoopState): void;
|
|
65
|
+
export declare function clearMiningUiTransientCandidate(loopState: MiningRuntimeLoopState): void;
|
|
66
|
+
export declare function discardMiningLoopTransientWork(loopState: MiningRuntimeLoopState, walletRootId: string | null | undefined): void;
|