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