@cogcoin/client 0.5.14 → 1.0.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 +80 -25
- package/dist/app-paths.d.ts +5 -6
- package/dist/app-paths.js +8 -16
- package/dist/art/balance.txt +10 -0
- package/dist/art/welcome.txt +16 -0
- package/dist/bitcoind/bootstrap/controller.d.ts +1 -0
- package/dist/bitcoind/bootstrap/controller.js +53 -1
- package/dist/bitcoind/client/follow-block-times.d.ts +1 -0
- package/dist/bitcoind/client/follow-block-times.js +1 -1
- package/dist/bitcoind/client/internal-types.d.ts +7 -3
- package/dist/bitcoind/client/managed-client.d.ts +4 -2
- package/dist/bitcoind/client/managed-client.js +14 -0
- package/dist/bitcoind/client/sync-engine.js +72 -11
- package/dist/bitcoind/hash-order.d.ts +4 -0
- package/dist/bitcoind/hash-order.js +13 -0
- package/dist/bitcoind/indexer-daemon-main.js +11 -3
- package/dist/bitcoind/normalize.js +3 -2
- package/dist/bitcoind/processing-start-height.d.ts +5 -0
- package/dist/bitcoind/processing-start-height.js +7 -0
- package/dist/bitcoind/progress/constants.d.ts +4 -0
- package/dist/bitcoind/progress/constants.js +4 -0
- package/dist/bitcoind/progress/controller.d.ts +2 -1
- package/dist/bitcoind/progress/controller.js +3 -3
- package/dist/bitcoind/progress/follow-scene.d.ts +6 -2
- package/dist/bitcoind/progress/follow-scene.js +29 -6
- package/dist/bitcoind/progress/formatting.d.ts +1 -0
- package/dist/bitcoind/progress/formatting.js +6 -0
- package/dist/bitcoind/progress/train-scene.js +37 -18
- package/dist/bitcoind/progress/tty-renderer.d.ts +6 -1
- package/dist/bitcoind/progress/tty-renderer.js +8 -4
- package/dist/bitcoind/rpc.d.ts +2 -1
- package/dist/bitcoind/rpc.js +3 -0
- package/dist/bitcoind/types.d.ts +16 -0
- package/dist/bytes.d.ts +1 -0
- package/dist/bytes.js +3 -0
- package/dist/cli/art.d.ts +2 -0
- package/dist/cli/art.js +37 -0
- package/dist/cli/commands/client-admin.d.ts +2 -0
- package/dist/cli/commands/client-admin.js +91 -0
- package/dist/cli/commands/follow.js +0 -2
- package/dist/cli/commands/mining-admin.js +6 -47
- package/dist/cli/commands/mining-read.js +11 -50
- package/dist/cli/commands/mining-runtime.js +38 -3
- package/dist/cli/commands/service-runtime.js +0 -2
- package/dist/cli/commands/status.js +8 -2
- package/dist/cli/commands/sync.js +51 -4
- package/dist/cli/commands/wallet-admin.js +142 -136
- package/dist/cli/commands/wallet-mutation.js +91 -79
- package/dist/cli/commands/wallet-read.js +15 -18
- package/dist/cli/context.js +4 -14
- package/dist/cli/mining-format.d.ts +0 -1
- package/dist/cli/mining-format.js +5 -37
- package/dist/cli/mining-json.d.ts +0 -18
- package/dist/cli/mining-json.js +0 -35
- package/dist/cli/mutation-command-groups.d.ts +1 -2
- package/dist/cli/mutation-command-groups.js +0 -5
- package/dist/cli/mutation-json.d.ts +24 -145
- package/dist/cli/mutation-json.js +30 -136
- package/dist/cli/mutation-resolved-json.d.ts +0 -7
- package/dist/cli/mutation-resolved-json.js +4 -10
- package/dist/cli/mutation-success.d.ts +2 -0
- package/dist/cli/mutation-success.js +11 -1
- package/dist/cli/mutation-text-format.js +1 -3
- package/dist/cli/output.d.ts +1 -1
- package/dist/cli/output.js +254 -231
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +93 -122
- package/dist/cli/preview-json.d.ts +17 -120
- package/dist/cli/preview-json.js +14 -97
- package/dist/cli/prompt.js +8 -13
- package/dist/cli/read-json.d.ts +15 -37
- package/dist/cli/read-json.js +44 -140
- package/dist/cli/runner.js +10 -13
- package/dist/cli/types.d.ts +8 -17
- package/dist/cli/types.js +0 -2
- package/dist/cli/wallet-format.d.ts +1 -0
- package/dist/cli/wallet-format.js +205 -144
- package/dist/cli/workflow-hints.d.ts +3 -3
- package/dist/cli/workflow-hints.js +11 -8
- package/dist/client/default-client.d.ts +3 -1
- package/dist/client/default-client.js +45 -2
- package/dist/client/factory.js +1 -1
- package/dist/client/initialization.js +23 -0
- package/dist/client/persistence.js +5 -5
- package/dist/client/store-adapter.js +1 -0
- package/dist/sqlite/checkpoints.d.ts +1 -0
- package/dist/sqlite/checkpoints.js +7 -0
- package/dist/sqlite/store.js +14 -1
- package/dist/types.d.ts +1 -0
- package/dist/wallet/coin-control.d.ts +41 -11
- package/dist/wallet/coin-control.js +100 -357
- package/dist/wallet/descriptor-normalization.d.ts +1 -3
- package/dist/wallet/descriptor-normalization.js +0 -16
- package/dist/wallet/lifecycle.d.ts +7 -99
- package/dist/wallet/lifecycle.js +513 -968
- package/dist/wallet/managed-core-wallet.d.ts +13 -0
- package/dist/wallet/managed-core-wallet.js +20 -0
- package/dist/wallet/mining/constants.d.ts +5 -12
- package/dist/wallet/mining/constants.js +5 -12
- package/dist/wallet/mining/control.d.ts +1 -13
- package/dist/wallet/mining/control.js +45 -349
- package/dist/wallet/mining/index.d.ts +3 -4
- package/dist/wallet/mining/index.js +1 -2
- package/dist/wallet/mining/runner.d.ts +179 -6
- package/dist/wallet/mining/runner.js +891 -501
- package/dist/wallet/mining/runtime-artifacts.js +23 -3
- package/dist/wallet/mining/sentence-protocol.d.ts +44 -0
- package/dist/wallet/mining/sentence-protocol.js +123 -0
- package/dist/wallet/mining/sentences.d.ts +4 -8
- package/dist/wallet/mining/sentences.js +3 -52
- package/dist/wallet/mining/state.d.ts +11 -6
- package/dist/wallet/mining/state.js +7 -6
- package/dist/wallet/mining/types.d.ts +2 -30
- package/dist/wallet/mining/visualizer.d.ts +31 -3
- package/dist/wallet/mining/visualizer.js +135 -13
- package/dist/wallet/read/context.d.ts +0 -2
- package/dist/wallet/read/context.js +119 -140
- package/dist/wallet/read/filter.js +2 -11
- package/dist/wallet/read/index.d.ts +1 -1
- package/dist/wallet/read/project.js +24 -77
- package/dist/wallet/read/types.d.ts +10 -25
- package/dist/wallet/reset.d.ts +0 -1
- package/dist/wallet/reset.js +60 -138
- package/dist/wallet/root-resolution.d.ts +1 -5
- package/dist/wallet/root-resolution.js +0 -18
- package/dist/wallet/runtime.d.ts +0 -6
- package/dist/wallet/runtime.js +0 -8
- package/dist/wallet/state/client-password-agent.js +208 -0
- package/dist/wallet/state/client-password.d.ts +65 -0
- package/dist/wallet/state/client-password.js +952 -0
- package/dist/wallet/state/crypto.d.ts +1 -20
- package/dist/wallet/state/crypto.js +0 -63
- package/dist/wallet/state/provider.d.ts +23 -11
- package/dist/wallet/state/provider.js +248 -290
- package/dist/wallet/state/storage.d.ts +2 -2
- package/dist/wallet/state/storage.js +48 -16
- package/dist/wallet/tx/anchor.d.ts +3 -28
- package/dist/wallet/tx/anchor.js +349 -1240
- package/dist/wallet/tx/bitcoin-transfer.d.ts +35 -0
- package/dist/wallet/tx/bitcoin-transfer.js +200 -0
- package/dist/wallet/tx/cog.d.ts +5 -1
- package/dist/wallet/tx/cog.js +149 -185
- package/dist/wallet/tx/common.d.ts +74 -10
- package/dist/wallet/tx/common.js +315 -138
- package/dist/wallet/tx/domain-admin.d.ts +3 -1
- package/dist/wallet/tx/domain-admin.js +61 -99
- package/dist/wallet/tx/domain-market.d.ts +5 -1
- package/dist/wallet/tx/domain-market.js +221 -228
- package/dist/wallet/tx/field.d.ts +4 -10
- package/dist/wallet/tx/field.js +84 -914
- package/dist/wallet/tx/identity-selector.d.ts +9 -3
- package/dist/wallet/tx/identity-selector.js +17 -35
- package/dist/wallet/tx/index.d.ts +3 -1
- package/dist/wallet/tx/index.js +2 -1
- package/dist/wallet/tx/register.d.ts +3 -1
- package/dist/wallet/tx/register.js +62 -220
- package/dist/wallet/tx/reputation.d.ts +3 -1
- package/dist/wallet/tx/reputation.js +58 -95
- package/dist/wallet/types.d.ts +8 -122
- package/package.json +5 -5
- package/dist/wallet/archive.d.ts +0 -4
- package/dist/wallet/archive.js +0 -41
- package/dist/wallet/mining/hook-protocol.d.ts +0 -47
- package/dist/wallet/mining/hook-protocol.js +0 -161
- package/dist/wallet/mining/hook-runner.js +0 -52
- package/dist/wallet/mining/hooks.d.ts +0 -38
- package/dist/wallet/mining/hooks.js +0 -520
- package/dist/wallet/state/explicit-lock.d.ts +0 -4
- package/dist/wallet/state/explicit-lock.js +0 -19
- package/dist/wallet/state/session.d.ts +0 -12
- package/dist/wallet/state/session.js +0 -23
- /package/dist/wallet/{mining/hook-runner.d.ts → state/client-password-agent.d.ts} +0 -0
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { lookupDomain, lookupDomainById, } from "@cogcoin/indexer/queries";
|
|
4
|
+
import { getBalance, getBlockWinners, lookupDomain, lookupDomainById, } from "@cogcoin/indexer/queries";
|
|
5
5
|
import { assaySentences, deriveBlendSeed, displayToInternalBlockhash, getWords, settleBlock, } from "@cogcoin/scoring";
|
|
6
6
|
import { probeIndexerDaemon } from "../../bitcoind/indexer-daemon.js";
|
|
7
|
+
import { FOLLOW_VISIBLE_PRIOR_BLOCKS } from "../../bitcoind/client/follow-block-times.js";
|
|
7
8
|
import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
|
|
8
9
|
import { createRpcClient } from "../../bitcoind/node.js";
|
|
9
10
|
import { COG_OPCODES, COG_PREFIX } from "../cogop/constants.js";
|
|
10
11
|
import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
|
|
11
|
-
import {
|
|
12
|
+
import { assertFixedInputPrefixMatches, buildWalletMutationTransaction, outpointKey as walletMutationOutpointKey, isAlreadyAcceptedError, isBroadcastUnknownError, reconcilePersistentPolicyLocks, resolveWalletMutationFeeSelection, saveWalletStatePreservingUnlock, } from "../tx/common.js";
|
|
12
13
|
import { acquireFileLock } from "../fs/lock.js";
|
|
13
|
-
import { loadOrAutoUnlockWalletState } from "../lifecycle.js";
|
|
14
14
|
import { isMineableWalletDomain, openWalletReadContext, } from "../read/index.js";
|
|
15
15
|
import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
16
16
|
import { createDefaultWalletSecretProvider, } from "../state/provider.js";
|
|
17
17
|
import { serializeMine } from "../cogop/index.js";
|
|
18
18
|
import { appendMiningEvent, loadMiningRuntimeStatus, saveMiningRuntimeStatus, } from "./runtime-artifacts.js";
|
|
19
19
|
import { loadClientConfig } from "./config.js";
|
|
20
|
-
import {
|
|
20
|
+
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
21
|
import { inspectMiningControlPlane, setupBuiltInMining } from "./control.js";
|
|
22
22
|
import { isMiningGenerationAbortRequested, markMiningGenerationActive, markMiningGenerationInactive, readMiningPreemptionRequest, requestMiningGenerationPreemption, } from "./coordination.js";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
23
|
+
import { clearMiningPublishState, miningPublishIsInMempool, miningPublishMayStillExist, normalizeMiningPublishState, normalizeMiningStateRecord, } from "./state.js";
|
|
24
|
+
import { createMiningSentenceRequestLimits } from "./sentence-protocol.js";
|
|
25
25
|
import { generateMiningSentences, MiningProviderRequestError } from "./sentences.js";
|
|
26
|
-
import { MiningFollowVisualizer } from "./visualizer.js";
|
|
26
|
+
import { createEmptyMiningFollowVisualizerState, MiningFollowVisualizer, } from "./visualizer.js";
|
|
27
27
|
const BEST_BLOCK_POLL_INTERVAL_MS = 500;
|
|
28
28
|
const BACKGROUND_START_TIMEOUT_MS = 15_000;
|
|
29
29
|
class MiningSuspendDetectedError extends Error {
|
|
@@ -33,6 +33,14 @@ class MiningSuspendDetectedError extends Error {
|
|
|
33
33
|
this.detectedAtUnixMs = detectedAtUnixMs;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
class MiningPublishRejectedError extends Error {
|
|
37
|
+
revertedState;
|
|
38
|
+
constructor(message, revertedState) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "MiningPublishRejectedError";
|
|
41
|
+
this.revertedState = revertedState;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
36
44
|
const miningGateCache = new Map();
|
|
37
45
|
function createMiningSuspendDetector(monotonicNow = performance.now()) {
|
|
38
46
|
return {
|
|
@@ -116,11 +124,7 @@ function cloneMiningState(state) {
|
|
|
116
124
|
};
|
|
117
125
|
}
|
|
118
126
|
function hasBlockingMutation(state) {
|
|
119
|
-
return state.
|
|
120
|
-
|| family.status === "broadcasting"
|
|
121
|
-
|| family.status === "broadcast-unknown"
|
|
122
|
-
|| family.status === "live"
|
|
123
|
-
|| family.status === "repair-required") || (state.pendingMutations ?? []).some((mutation) => mutation.status === "draft"
|
|
127
|
+
return (state.pendingMutations ?? []).some((mutation) => mutation.status === "draft"
|
|
124
128
|
|| mutation.status === "broadcasting"
|
|
125
129
|
|| mutation.status === "broadcast-unknown"
|
|
126
130
|
|| mutation.status === "live"
|
|
@@ -174,6 +178,197 @@ function numberToSats(value) {
|
|
|
174
178
|
function satsToBtc(value) {
|
|
175
179
|
return Number(value) / 100_000_000;
|
|
176
180
|
}
|
|
181
|
+
function compareLexicographically(left, right) {
|
|
182
|
+
const length = Math.min(left.length, right.length);
|
|
183
|
+
for (let index = 0; index < length; index += 1) {
|
|
184
|
+
if (left[index] !== right[index]) {
|
|
185
|
+
return left[index] < right[index] ? -1 : 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (left.length === right.length) {
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
return left.length < right.length ? -1 : 1;
|
|
192
|
+
}
|
|
193
|
+
function tieBreakHash(blendSeed, miningDomainId) {
|
|
194
|
+
return createHash("sha256")
|
|
195
|
+
.update(Buffer.from(blendSeed))
|
|
196
|
+
.update(uint32BigEndian(miningDomainId))
|
|
197
|
+
.digest();
|
|
198
|
+
}
|
|
199
|
+
function createMiningLoopState() {
|
|
200
|
+
return {
|
|
201
|
+
attemptedTipKey: null,
|
|
202
|
+
currentTipKey: null,
|
|
203
|
+
selectedCandidateTipKey: null,
|
|
204
|
+
selectedCandidate: null,
|
|
205
|
+
ui: createEmptyMiningFollowVisualizerState(),
|
|
206
|
+
waitingNote: null,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
export function createMiningLoopStateForTesting() {
|
|
210
|
+
return createMiningLoopState();
|
|
211
|
+
}
|
|
212
|
+
function buildMiningTipKey(bestBlockHash, targetBlockHeight) {
|
|
213
|
+
if (bestBlockHash === null || targetBlockHeight === null) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return `${bestBlockHash}:${targetBlockHeight}`;
|
|
217
|
+
}
|
|
218
|
+
function resetMiningUiForTip(loopState, targetBlockHeight) {
|
|
219
|
+
const preservedTxid = loopState.ui.latestTxid;
|
|
220
|
+
loopState.ui = {
|
|
221
|
+
...createEmptyMiningFollowVisualizerState(),
|
|
222
|
+
latestTxid: preservedTxid,
|
|
223
|
+
};
|
|
224
|
+
loopState.selectedCandidateTipKey = null;
|
|
225
|
+
loopState.selectedCandidate = null;
|
|
226
|
+
loopState.waitingNote = null;
|
|
227
|
+
}
|
|
228
|
+
export function resetMiningUiForTipForTesting(loopState, targetBlockHeight) {
|
|
229
|
+
resetMiningUiForTip(loopState, targetBlockHeight);
|
|
230
|
+
}
|
|
231
|
+
function fallbackSettledWinnerDomainName(domainId) {
|
|
232
|
+
return `domain-${domainId}`;
|
|
233
|
+
}
|
|
234
|
+
function resolveCurrentMinedBlockBoard(options) {
|
|
235
|
+
const settledBlockHeight = options.nodeBestHeight ?? options.snapshotTipHeight ?? null;
|
|
236
|
+
if (settledBlockHeight === null) {
|
|
237
|
+
return {
|
|
238
|
+
settledBlockHeight,
|
|
239
|
+
settledBoardEntries: [],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (options.snapshotState === null || options.snapshotState === undefined) {
|
|
243
|
+
return {
|
|
244
|
+
settledBlockHeight,
|
|
245
|
+
settledBoardEntries: [],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (options.nodeBestHeight !== null && (options.snapshotTipHeight ?? -1) < options.nodeBestHeight) {
|
|
249
|
+
return {
|
|
250
|
+
settledBlockHeight,
|
|
251
|
+
settledBoardEntries: [],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const settledBoardEntries = (getBlockWinners(options.snapshotState, settledBlockHeight) ?? [])
|
|
255
|
+
.slice()
|
|
256
|
+
.sort((left, right) => left.rank - right.rank || left.txIndex - right.txIndex)
|
|
257
|
+
.slice(0, 5)
|
|
258
|
+
.map((winner) => ({
|
|
259
|
+
rank: winner.rank,
|
|
260
|
+
domainName: lookupDomainById(options.snapshotState, winner.domainId)?.name ?? fallbackSettledWinnerDomainName(winner.domainId),
|
|
261
|
+
sentence: winner.sentenceText ?? "[unavailable]",
|
|
262
|
+
}));
|
|
263
|
+
return {
|
|
264
|
+
settledBlockHeight,
|
|
265
|
+
settledBoardEntries,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
export function resolveSettledBoardForTesting(options) {
|
|
269
|
+
return resolveCurrentMinedBlockBoard(options);
|
|
270
|
+
}
|
|
271
|
+
function syncMiningUiSettledBoard(loopState, snapshotState, snapshotTipHeight, nodeBestHeight) {
|
|
272
|
+
const settledBoard = resolveCurrentMinedBlockBoard({
|
|
273
|
+
snapshotState,
|
|
274
|
+
snapshotTipHeight,
|
|
275
|
+
nodeBestHeight,
|
|
276
|
+
});
|
|
277
|
+
loopState.ui.settledBlockHeight = settledBoard.settledBlockHeight;
|
|
278
|
+
loopState.ui.settledBoardEntries = settledBoard.settledBoardEntries;
|
|
279
|
+
}
|
|
280
|
+
function setMiningUiCandidate(loopState, candidate) {
|
|
281
|
+
loopState.ui.latestSentence = candidate.sentence;
|
|
282
|
+
loopState.ui.provisionalRequiredWords = [...candidate.bip39Words];
|
|
283
|
+
loopState.ui.provisionalEntry = {
|
|
284
|
+
domainName: candidate.domainName,
|
|
285
|
+
sentence: candidate.sentence,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function getSelectedCandidateForTip(loopState, tipKey) {
|
|
289
|
+
if (tipKey === null || loopState.selectedCandidateTipKey !== tipKey) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return loopState.selectedCandidate;
|
|
293
|
+
}
|
|
294
|
+
export function getSelectedCandidateForTipForTesting(loopState, tipKey) {
|
|
295
|
+
return getSelectedCandidateForTip(loopState, tipKey);
|
|
296
|
+
}
|
|
297
|
+
function cacheSelectedCandidateForTip(loopState, tipKey, candidate) {
|
|
298
|
+
loopState.selectedCandidateTipKey = tipKey;
|
|
299
|
+
loopState.selectedCandidate = candidate;
|
|
300
|
+
setMiningUiCandidate(loopState, candidate);
|
|
301
|
+
}
|
|
302
|
+
export function cacheSelectedCandidateForTipForTesting(loopState, tipKey, candidate) {
|
|
303
|
+
cacheSelectedCandidateForTip(loopState, tipKey, candidate);
|
|
304
|
+
}
|
|
305
|
+
function clearSelectedCandidate(loopState) {
|
|
306
|
+
loopState.selectedCandidateTipKey = null;
|
|
307
|
+
loopState.selectedCandidate = null;
|
|
308
|
+
}
|
|
309
|
+
async function resolveFundingDisplaySats(state, rpc) {
|
|
310
|
+
const utxos = await rpc.listUnspent(state.managedCoreWallet.walletName, 0);
|
|
311
|
+
return utxos.reduce((sum, entry) => {
|
|
312
|
+
if (entry.scriptPubKey !== state.funding.scriptPubKeyHex
|
|
313
|
+
|| entry.spendable === false) {
|
|
314
|
+
return sum;
|
|
315
|
+
}
|
|
316
|
+
return sum + numberToSats(entry.amount);
|
|
317
|
+
}, 0n);
|
|
318
|
+
}
|
|
319
|
+
export async function resolveFundingDisplaySatsForTesting(state, rpc) {
|
|
320
|
+
return resolveFundingDisplaySats(state, rpc);
|
|
321
|
+
}
|
|
322
|
+
async function loadMiningVisibleFollowBlockTimes(options) {
|
|
323
|
+
if (options.indexedTipHeight === null || options.indexedTipHashHex === null) {
|
|
324
|
+
return {};
|
|
325
|
+
}
|
|
326
|
+
const blockTimesByHeight = {};
|
|
327
|
+
let currentHeight = options.indexedTipHeight;
|
|
328
|
+
let currentHashHex = options.indexedTipHashHex;
|
|
329
|
+
for (let offset = 0; offset <= FOLLOW_VISIBLE_PRIOR_BLOCKS; offset += 1) {
|
|
330
|
+
if (currentHeight < 0 || currentHashHex === null) {
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
const block = await options.rpc.getBlock(currentHashHex);
|
|
334
|
+
if (typeof block.time === "number") {
|
|
335
|
+
blockTimesByHeight[currentHeight] = block.time;
|
|
336
|
+
}
|
|
337
|
+
currentHashHex = block.previousblockhash ?? null;
|
|
338
|
+
currentHeight -= 1;
|
|
339
|
+
}
|
|
340
|
+
return blockTimesByHeight;
|
|
341
|
+
}
|
|
342
|
+
export async function loadMiningVisibleFollowBlockTimesForTesting(options) {
|
|
343
|
+
return loadMiningVisibleFollowBlockTimes(options);
|
|
344
|
+
}
|
|
345
|
+
function syncMiningVisualizerBalances(loopState, readContext, balanceSats) {
|
|
346
|
+
loopState.ui.balanceCogtoshi = readContext.snapshot === null
|
|
347
|
+
? null
|
|
348
|
+
: getBalance(readContext.snapshot.state, readContext.localState.state.funding.scriptPubKeyHex);
|
|
349
|
+
loopState.ui.balanceSats = balanceSats;
|
|
350
|
+
}
|
|
351
|
+
function syncMiningVisualizerBlockTimes(loopState, blockTimesByHeight) {
|
|
352
|
+
loopState.ui.visibleBlockTimesByHeight = { ...blockTimesByHeight };
|
|
353
|
+
}
|
|
354
|
+
export function syncMiningVisualizerBlockTimesForTesting(loopState, blockTimesByHeight) {
|
|
355
|
+
syncMiningVisualizerBlockTimes(loopState, blockTimesByHeight);
|
|
356
|
+
}
|
|
357
|
+
function findRecentMiningWin(snapshotState, txid, targetBlockHeight) {
|
|
358
|
+
if (snapshotState === null || snapshotState === undefined || txid === null || targetBlockHeight === null) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const winners = getBlockWinners(snapshotState, targetBlockHeight) ?? [];
|
|
362
|
+
const winner = winners.find((entry) => entry.txidHex === txid) ?? null;
|
|
363
|
+
if (winner === null) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
rank: winner.rank,
|
|
368
|
+
rewardCogtoshi: winner.rewardCogtoshi,
|
|
369
|
+
blockHeight: winner.height,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
177
372
|
function computeIntentFingerprint(state, candidate) {
|
|
178
373
|
return createHash("sha256")
|
|
179
374
|
.update([
|
|
@@ -465,7 +660,7 @@ function buildStatusSnapshot(view, overrides = {}) {
|
|
|
465
660
|
lastCompetitivenessGateAtUnixMs: overrides.lastCompetitivenessGateAtUnixMs ?? view.runtime.lastCompetitivenessGateAtUnixMs,
|
|
466
661
|
lastError: overrides.lastError ?? view.runtime.lastError,
|
|
467
662
|
note: overrides.note ?? view.runtime.note,
|
|
468
|
-
|
|
663
|
+
livePublishInMempool: overrides.livePublishInMempool ?? view.runtime.livePublishInMempool,
|
|
469
664
|
updatedAtUnixMs: Date.now(),
|
|
470
665
|
};
|
|
471
666
|
}
|
|
@@ -481,7 +676,7 @@ async function refreshAndSaveStatus(options) {
|
|
|
481
676
|
});
|
|
482
677
|
const snapshot = buildStatusSnapshot(view, options.overrides);
|
|
483
678
|
await saveMiningRuntimeStatus(options.paths.miningStatusPath, snapshot);
|
|
484
|
-
options.visualizer?.update(snapshot);
|
|
679
|
+
options.visualizer?.update(snapshot, options.visualizerState);
|
|
485
680
|
return snapshot;
|
|
486
681
|
}
|
|
487
682
|
async function appendEvent(paths, event) {
|
|
@@ -576,7 +771,9 @@ function createMiningPlan(options) {
|
|
|
576
771
|
&& entry.confirmations >= 1
|
|
577
772
|
&& entry.spendable !== false
|
|
578
773
|
&& entry.safe !== false
|
|
579
|
-
&& !(
|
|
774
|
+
&& !(options.conflictOutpoint !== null
|
|
775
|
+
&& entry.txid === options.conflictOutpoint.txid
|
|
776
|
+
&& entry.vout === options.conflictOutpoint.vout));
|
|
580
777
|
const opReturnData = serializeMine(options.candidate.domainId, options.candidate.referencedBlockHashInternal, options.candidate.encodedSentenceBytes).opReturnData;
|
|
581
778
|
const expectedOpReturnScriptHex = Buffer.concat([
|
|
582
779
|
Buffer.from([0x6a, opReturnData.length]),
|
|
@@ -584,19 +781,11 @@ function createMiningPlan(options) {
|
|
|
584
781
|
]).toString("hex");
|
|
585
782
|
return {
|
|
586
783
|
sender: options.candidate.sender,
|
|
587
|
-
fixedInputs: [
|
|
588
|
-
|
|
589
|
-
options.conflictOutpoint,
|
|
590
|
-
],
|
|
591
|
-
outputs: [
|
|
592
|
-
{ data: Buffer.from(opReturnData).toString("hex") },
|
|
593
|
-
{ [options.candidate.sender.address]: satsToBtc(BigInt(options.state.anchorValueSats)) },
|
|
594
|
-
],
|
|
784
|
+
fixedInputs: options.conflictOutpoint === null ? [] : [options.conflictOutpoint],
|
|
785
|
+
outputs: [{ data: Buffer.from(opReturnData).toString("hex") }],
|
|
595
786
|
changeAddress: options.state.funding.address,
|
|
596
|
-
changePosition:
|
|
787
|
+
changePosition: 1,
|
|
597
788
|
expectedOpReturnScriptHex,
|
|
598
|
-
expectedAnchorScriptHex: options.candidate.sender.scriptPubKeyHex,
|
|
599
|
-
expectedAnchorValueSats: BigInt(options.state.anchorValueSats),
|
|
600
789
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
601
790
|
eligibleFundingOutpointKeys: new Set(fundingUtxos.map((entry) => walletMutationOutpointKey({ txid: entry.txid, vout: entry.vout }))),
|
|
602
791
|
expectedConflictOutpoint: options.conflictOutpoint,
|
|
@@ -606,34 +795,18 @@ function createMiningPlan(options) {
|
|
|
606
795
|
function validateMiningDraft(decoded, funded, plan) {
|
|
607
796
|
const inputs = decoded.tx.vin;
|
|
608
797
|
const outputs = decoded.tx.vout;
|
|
609
|
-
if (inputs.length
|
|
798
|
+
if (inputs.length === 0) {
|
|
610
799
|
throw new Error("wallet_mining_missing_inputs");
|
|
611
800
|
}
|
|
612
801
|
assertFixedInputPrefixMatches(inputs, plan.fixedInputs, "wallet_mining_missing_inputs");
|
|
613
|
-
if (
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if (inputs[1]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex
|
|
617
|
-
|| inputs[1]?.txid !== plan.expectedConflictOutpoint.txid
|
|
618
|
-
|| inputs[1].vout !== plan.expectedConflictOutpoint.vout) {
|
|
802
|
+
if (plan.expectedConflictOutpoint !== null
|
|
803
|
+
&& (inputs[0]?.txid !== plan.expectedConflictOutpoint.txid
|
|
804
|
+
|| inputs[0]?.vout !== plan.expectedConflictOutpoint.vout)) {
|
|
619
805
|
throw new Error("wallet_mining_conflict_input_mismatch");
|
|
620
806
|
}
|
|
621
|
-
assertFundingInputsAfterFixedPrefix({
|
|
622
|
-
inputs,
|
|
623
|
-
fixedInputs: plan.fixedInputs,
|
|
624
|
-
allowedFundingScriptPubKeyHex: plan.allowedFundingScriptPubKeyHex,
|
|
625
|
-
eligibleFundingOutpointKeys: plan.eligibleFundingOutpointKeys,
|
|
626
|
-
errorCode: "wallet_mining_unexpected_funding_input",
|
|
627
|
-
});
|
|
628
807
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
629
808
|
throw new Error("wallet_mining_opreturn_mismatch");
|
|
630
809
|
}
|
|
631
|
-
if (outputs[1]?.scriptPubKey?.hex !== plan.expectedAnchorScriptHex) {
|
|
632
|
-
throw new Error("wallet_mining_anchor_output_mismatch");
|
|
633
|
-
}
|
|
634
|
-
if (numberToSats(outputs[1]?.value ?? 0) !== plan.expectedAnchorValueSats) {
|
|
635
|
-
throw new Error("wallet_mining_anchor_value_mismatch");
|
|
636
|
-
}
|
|
637
810
|
if (funded.changepos !== -1 && (funded.changepos !== plan.changePosition || outputs[funded.changepos]?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex)) {
|
|
638
811
|
throw new Error("wallet_mining_change_output_mismatch");
|
|
639
812
|
}
|
|
@@ -650,6 +823,12 @@ async function buildMiningTransaction(options) {
|
|
|
650
823
|
feeRate: options.plan.feeRateSatVb,
|
|
651
824
|
});
|
|
652
825
|
}
|
|
826
|
+
export function createMiningPlanForTesting(options) {
|
|
827
|
+
return createMiningPlan(options);
|
|
828
|
+
}
|
|
829
|
+
export function validateMiningDraftForTesting(decoded, funded, plan) {
|
|
830
|
+
validateMiningDraft(decoded, funded, plan);
|
|
831
|
+
}
|
|
653
832
|
function resolveEligibleAnchoredRoots(context) {
|
|
654
833
|
const state = context.localState.state;
|
|
655
834
|
const model = context.model;
|
|
@@ -662,15 +841,11 @@ function resolveEligibleAnchoredRoots(context) {
|
|
|
662
841
|
if (!isMineableWalletDomain(context, domain)) {
|
|
663
842
|
continue;
|
|
664
843
|
}
|
|
665
|
-
const localRecord = state.domains.find((entry) => entry.name === domain.name);
|
|
666
|
-
const ownerIdentity = model.identities.find((identity) => identity.index === domain.ownerLocalIndex);
|
|
667
844
|
const domainId = domain.domainId;
|
|
668
845
|
if (domainId === null
|
|
669
846
|
|| domainId === undefined
|
|
670
|
-
||
|
|
671
|
-
||
|
|
672
|
-
|| ownerIdentity?.address == null
|
|
673
|
-
|| ownerIdentity.readOnly) {
|
|
847
|
+
|| domain.ownerAddress == null
|
|
848
|
+
|| domain.ownerScriptPubKeyHex !== model.walletScriptPubKeyHex) {
|
|
674
849
|
continue;
|
|
675
850
|
}
|
|
676
851
|
const chainDomain = lookupDomain(snapshot.state, domain.name);
|
|
@@ -680,60 +855,47 @@ function resolveEligibleAnchoredRoots(context) {
|
|
|
680
855
|
domains.push({
|
|
681
856
|
domainId,
|
|
682
857
|
domainName: domain.name,
|
|
683
|
-
localIndex:
|
|
858
|
+
localIndex: 0,
|
|
684
859
|
sender: {
|
|
685
|
-
localIndex:
|
|
686
|
-
scriptPubKeyHex:
|
|
687
|
-
address:
|
|
688
|
-
},
|
|
689
|
-
anchorOutpoint: {
|
|
690
|
-
txid: localRecord.currentCanonicalAnchorOutpoint.txid,
|
|
691
|
-
vout: localRecord.currentCanonicalAnchorOutpoint.vout,
|
|
860
|
+
localIndex: 0,
|
|
861
|
+
scriptPubKeyHex: model.walletScriptPubKeyHex,
|
|
862
|
+
address: domain.ownerAddress,
|
|
692
863
|
},
|
|
693
864
|
});
|
|
694
865
|
}
|
|
695
866
|
return domains.sort((left, right) => left.domainId - right.domainId || left.domainName.localeCompare(right.domainName));
|
|
696
867
|
}
|
|
697
|
-
|
|
698
|
-
const
|
|
699
|
-
if (
|
|
700
|
-
return
|
|
701
|
-
}
|
|
702
|
-
if (options.success) {
|
|
703
|
-
if ((hookState.consecutiveFailureCount ?? 0) === 0 && hookState.cooldownUntilUnixMs === null) {
|
|
704
|
-
return false;
|
|
705
|
-
}
|
|
706
|
-
options.readContext.localState.state.hookClientState.mining = {
|
|
707
|
-
...hookState,
|
|
708
|
-
consecutiveFailureCount: 0,
|
|
709
|
-
cooldownUntilUnixMs: null,
|
|
710
|
-
};
|
|
711
|
-
await saveWalletStatePreservingUnlock({
|
|
712
|
-
state: options.readContext.localState.state,
|
|
713
|
-
provider: options.provider,
|
|
714
|
-
unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
|
|
715
|
-
nowUnixMs: options.nowUnixMs,
|
|
716
|
-
paths: options.paths,
|
|
717
|
-
});
|
|
718
|
-
return false;
|
|
868
|
+
function refreshMiningCandidateFromCurrentState(context, candidate) {
|
|
869
|
+
const refreshed = resolveEligibleAnchoredRoots(context).find((domain) => domain.domainId === candidate.domainId);
|
|
870
|
+
if (refreshed === undefined) {
|
|
871
|
+
return null;
|
|
719
872
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
:
|
|
724
|
-
|
|
725
|
-
...hookState,
|
|
726
|
-
consecutiveFailureCount,
|
|
727
|
-
cooldownUntilUnixMs,
|
|
873
|
+
return {
|
|
874
|
+
...candidate,
|
|
875
|
+
domainName: refreshed.domainName,
|
|
876
|
+
localIndex: refreshed.localIndex,
|
|
877
|
+
sender: refreshed.sender,
|
|
728
878
|
};
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
879
|
+
}
|
|
880
|
+
export function refreshMiningCandidateFromCurrentStateForTesting(context, candidate) {
|
|
881
|
+
return refreshMiningCandidateFromCurrentState(context, candidate);
|
|
882
|
+
}
|
|
883
|
+
function resolveMiningConflictOutpoint(options) {
|
|
884
|
+
const normalizedMiningState = normalizeMiningStateRecord(options.state.miningState);
|
|
885
|
+
if (miningPublishIsInMempool(normalizedMiningState) && normalizedMiningState.sharedMiningConflictOutpoint !== null) {
|
|
886
|
+
return { ...normalizedMiningState.sharedMiningConflictOutpoint };
|
|
887
|
+
}
|
|
888
|
+
void options.allUtxos;
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
export function resolveMiningConflictOutpointForTesting(options) {
|
|
892
|
+
return resolveMiningConflictOutpoint(options);
|
|
893
|
+
}
|
|
894
|
+
function createStaleMiningCandidateWaitingNote() {
|
|
895
|
+
return "Mining candidate changed before broadcast: the selected root domain is no longer locally mineable. Skipping this tip and waiting for the next block.";
|
|
896
|
+
}
|
|
897
|
+
function createRetryableMiningPublishWaitingNote() {
|
|
898
|
+
return "Selected mining candidate did not reach mempool and will be retried on the current tip with refreshed wallet state.";
|
|
737
899
|
}
|
|
738
900
|
async function generateCandidatesForDomains(options) {
|
|
739
901
|
const bestBlockHash = options.readContext.nodeStatus?.nodeBestHashHex;
|
|
@@ -791,7 +953,7 @@ async function generateCandidatesForDomains(options) {
|
|
|
791
953
|
referencedBlockHashDisplay: bestBlockHash,
|
|
792
954
|
generatedAtUnixMs: Date.now(),
|
|
793
955
|
extraPrompt: null,
|
|
794
|
-
limits:
|
|
956
|
+
limits: createMiningSentenceRequestLimits(),
|
|
795
957
|
rootDomains: rootDomains.map((domain) => ({
|
|
796
958
|
domainId: domain.domainId,
|
|
797
959
|
domainName: domain.domainName,
|
|
@@ -803,7 +965,6 @@ async function generateCandidatesForDomains(options) {
|
|
|
803
965
|
generated = await generateMiningSentences(generationRequest, {
|
|
804
966
|
paths: options.paths,
|
|
805
967
|
provider: options.provider,
|
|
806
|
-
hookState: options.readContext.localState.state.hookClientState.mining,
|
|
807
968
|
signal: abortController.signal,
|
|
808
969
|
fetchImpl: options.fetchImpl,
|
|
809
970
|
});
|
|
@@ -833,15 +994,6 @@ async function generateCandidatesForDomains(options) {
|
|
|
833
994
|
dataDir: options.readContext.dataDir,
|
|
834
995
|
truthKey: options.indexerTruthKey,
|
|
835
996
|
});
|
|
836
|
-
if (generated.hookMode === "custom") {
|
|
837
|
-
await persistCustomHookRuntimeOutcome({
|
|
838
|
-
readContext: options.readContext,
|
|
839
|
-
provider: options.provider,
|
|
840
|
-
paths: options.paths,
|
|
841
|
-
nowUnixMs: Date.now(),
|
|
842
|
-
success: true,
|
|
843
|
-
});
|
|
844
|
-
}
|
|
845
997
|
const sentencesByDomain = new Map();
|
|
846
998
|
for (const candidate of generated.candidates) {
|
|
847
999
|
const existing = sentencesByDomain.get(candidate.domainId) ?? [];
|
|
@@ -864,7 +1016,6 @@ async function generateCandidatesForDomains(options) {
|
|
|
864
1016
|
domainName: domain.domainName,
|
|
865
1017
|
localIndex: domain.localIndex,
|
|
866
1018
|
sender: domain.sender,
|
|
867
|
-
anchorOutpoint: domain.anchorOutpoint,
|
|
868
1019
|
sentence: best.sentence,
|
|
869
1020
|
encodedSentenceBytes: best.encodedSentenceBytes,
|
|
870
1021
|
bip39WordIndices: [...best.bip39WordIndices],
|
|
@@ -914,6 +1065,46 @@ async function chooseBestLocalCandidate(candidates) {
|
|
|
914
1065
|
}
|
|
915
1066
|
return candidates.find((candidate) => candidate.domainId === winner.miningDomainId) ?? null;
|
|
916
1067
|
}
|
|
1068
|
+
function isBetterVisibleCompetitor(candidate, current) {
|
|
1069
|
+
if (current === undefined) {
|
|
1070
|
+
return true;
|
|
1071
|
+
}
|
|
1072
|
+
if (candidate.canonicalBlend !== current.canonicalBlend) {
|
|
1073
|
+
return candidate.canonicalBlend > current.canonicalBlend;
|
|
1074
|
+
}
|
|
1075
|
+
if (candidate.effectiveFeeRate !== current.effectiveFeeRate) {
|
|
1076
|
+
return candidate.effectiveFeeRate > current.effectiveFeeRate;
|
|
1077
|
+
}
|
|
1078
|
+
return candidate.txid.localeCompare(current.txid) < 0;
|
|
1079
|
+
}
|
|
1080
|
+
function rankMiningSentenceEntries(entries, blendSeed) {
|
|
1081
|
+
return entries
|
|
1082
|
+
.map((entry) => ({
|
|
1083
|
+
...entry,
|
|
1084
|
+
tieBreak: tieBreakHash(blendSeed, entry.domainId),
|
|
1085
|
+
}))
|
|
1086
|
+
.sort((left, right) => {
|
|
1087
|
+
if (left.canonicalBlend !== right.canonicalBlend) {
|
|
1088
|
+
return left.canonicalBlend > right.canonicalBlend ? -1 : 1;
|
|
1089
|
+
}
|
|
1090
|
+
const tieBreakOrder = compareLexicographically(left.tieBreak, right.tieBreak);
|
|
1091
|
+
if (tieBreakOrder !== 0) {
|
|
1092
|
+
return tieBreakOrder;
|
|
1093
|
+
}
|
|
1094
|
+
return left.txIndex - right.txIndex;
|
|
1095
|
+
})
|
|
1096
|
+
.map((entry, index) => ({
|
|
1097
|
+
...entry,
|
|
1098
|
+
rank: index + 1,
|
|
1099
|
+
}));
|
|
1100
|
+
}
|
|
1101
|
+
function toSentenceBoardEntries(entries) {
|
|
1102
|
+
return entries.slice(0, 5).map((entry) => ({
|
|
1103
|
+
rank: entry.rank,
|
|
1104
|
+
domainName: entry.domainName,
|
|
1105
|
+
sentence: entry.sentence,
|
|
1106
|
+
}));
|
|
1107
|
+
}
|
|
917
1108
|
async function runCompetitivenessGate(options) {
|
|
918
1109
|
const createDecision = (overrides) => ({
|
|
919
1110
|
allowed: overrides.allowed ?? false,
|
|
@@ -924,10 +1115,11 @@ async function runCompetitivenessGate(options) {
|
|
|
924
1115
|
competitivenessGateIndeterminate: overrides.competitivenessGateIndeterminate ?? false,
|
|
925
1116
|
mempoolSequenceCacheStatus: overrides.mempoolSequenceCacheStatus ?? null,
|
|
926
1117
|
lastMempoolSequence: overrides.lastMempoolSequence ?? null,
|
|
1118
|
+
visibleBoardEntries: overrides.visibleBoardEntries ?? [],
|
|
1119
|
+
candidateRank: overrides.candidateRank ?? null,
|
|
927
1120
|
});
|
|
928
1121
|
const walletRootId = options.readContext.localState.walletRootId ?? "uninitialized-wallet-root";
|
|
929
1122
|
const indexerTruthKey = getIndexerTruthKey(options.readContext);
|
|
930
|
-
const localFeeTarget = DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB;
|
|
931
1123
|
const excludedTxids = [options.currentTxid].filter((value) => value !== null).sort();
|
|
932
1124
|
const localAssayTupleKey = [
|
|
933
1125
|
options.candidate.domainId,
|
|
@@ -959,7 +1151,6 @@ async function runCompetitivenessGate(options) {
|
|
|
959
1151
|
&& cachedTruthMatches
|
|
960
1152
|
&& cachedReferencedBlockMatches
|
|
961
1153
|
&& cached.localAssayTupleKey === localAssayTupleKey
|
|
962
|
-
&& cached.currentFeeTargetSatVb === localFeeTarget
|
|
963
1154
|
&& cached.excludedTxidsKey === excludedTxids.join(",")
|
|
964
1155
|
&& cached.mempoolSequence === mempoolSequence) {
|
|
965
1156
|
return {
|
|
@@ -1005,7 +1196,7 @@ async function runCompetitivenessGate(options) {
|
|
|
1005
1196
|
const entries = new Map();
|
|
1006
1197
|
for (const txid of visibleTxids) {
|
|
1007
1198
|
const context = txContexts.get(txid);
|
|
1008
|
-
if (context === undefined || context.
|
|
1199
|
+
if (context === undefined || context.payload === null || context.senderScriptHex === null) {
|
|
1009
1200
|
continue;
|
|
1010
1201
|
}
|
|
1011
1202
|
const decoded = decodeMinePayload(context.payload);
|
|
@@ -1031,7 +1222,6 @@ async function runCompetitivenessGate(options) {
|
|
|
1031
1222
|
indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
|
|
1032
1223
|
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
1033
1224
|
localAssayTupleKey,
|
|
1034
|
-
currentFeeTargetSatVb: localFeeTarget,
|
|
1035
1225
|
excludedTxidsKey: excludedTxids.join(","),
|
|
1036
1226
|
mempoolSequence,
|
|
1037
1227
|
txids: [...visibleTxids],
|
|
@@ -1052,26 +1242,46 @@ async function runCompetitivenessGate(options) {
|
|
|
1052
1242
|
txid,
|
|
1053
1243
|
effectiveFeeRate: context.effectiveFeeRate,
|
|
1054
1244
|
domainId: decoded.domainId,
|
|
1245
|
+
domainName: overlayDomain.name,
|
|
1246
|
+
sentence: Buffer.from(decoded.sentenceBytes).toString("utf8"),
|
|
1055
1247
|
senderScriptHex: context.senderScriptHex,
|
|
1056
1248
|
encodedSentenceBytesHex: Buffer.from(scored.encodedSentenceBytes).toString("hex"),
|
|
1057
1249
|
bip39WordIndices: [...scored.bip39WordIndices],
|
|
1058
1250
|
canonicalBlend: scored.canonicalBlend,
|
|
1059
1251
|
});
|
|
1060
1252
|
}
|
|
1061
|
-
const
|
|
1253
|
+
const blendSeed = deriveBlendSeed(options.candidate.referencedBlockHashInternal);
|
|
1254
|
+
const visibleBestByDomain = new Map();
|
|
1255
|
+
for (const entry of entries.values()) {
|
|
1256
|
+
const current = visibleBestByDomain.get(entry.domainId);
|
|
1257
|
+
if (isBetterVisibleCompetitor(entry, current)) {
|
|
1258
|
+
visibleBestByDomain.set(entry.domainId, entry);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
const visibleRankedEntries = rankMiningSentenceEntries([...visibleBestByDomain.values()]
|
|
1262
|
+
.sort((left, right) => left.domainId - right.domainId || left.txid.localeCompare(right.txid))
|
|
1263
|
+
.map((entry, index) => ({
|
|
1264
|
+
domainId: entry.domainId,
|
|
1265
|
+
domainName: entry.domainName,
|
|
1266
|
+
sentence: entry.sentence,
|
|
1267
|
+
canonicalBlend: entry.canonicalBlend,
|
|
1268
|
+
senderScriptHex: entry.senderScriptHex,
|
|
1269
|
+
encodedSentenceBytesHex: entry.encodedSentenceBytesHex,
|
|
1270
|
+
bip39WordIndices: entry.bip39WordIndices,
|
|
1271
|
+
txid: entry.txid,
|
|
1272
|
+
txIndex: index,
|
|
1273
|
+
})), blendSeed);
|
|
1274
|
+
const sameDomainCompetitors = [...visibleBestByDomain.values()].filter((entry) => entry.domainId === options.candidate.domainId);
|
|
1062
1275
|
const sameDomainCompetitorSuppressed = sameDomainCompetitors.some((competitor) => competitor.canonicalBlend > options.candidate.canonicalBlend
|
|
1063
1276
|
|| competitor.canonicalBlend === options.candidate.canonicalBlend);
|
|
1064
1277
|
let decision;
|
|
1065
1278
|
const otherDomainBest = new Map();
|
|
1066
|
-
for (const entry of
|
|
1279
|
+
for (const entry of visibleBestByDomain.values()) {
|
|
1067
1280
|
if (entry.domainId === options.candidate.domainId) {
|
|
1068
1281
|
continue;
|
|
1069
1282
|
}
|
|
1070
1283
|
const best = otherDomainBest.get(entry.domainId);
|
|
1071
|
-
if (best
|
|
1072
|
-
|| entry.canonicalBlend > best.canonicalBlend
|
|
1073
|
-
|| (entry.canonicalBlend === best.canonicalBlend && entry.effectiveFeeRate > best.effectiveFeeRate)
|
|
1074
|
-
|| (entry.canonicalBlend === best.canonicalBlend && entry.effectiveFeeRate === best.effectiveFeeRate && entry.txid.localeCompare(best.txid) < 0)) {
|
|
1284
|
+
if (isBetterVisibleCompetitor(entry, best)) {
|
|
1075
1285
|
otherDomainBest.set(entry.domainId, entry);
|
|
1076
1286
|
}
|
|
1077
1287
|
}
|
|
@@ -1085,38 +1295,41 @@ async function runCompetitivenessGate(options) {
|
|
|
1085
1295
|
competitivenessGateIndeterminate: false,
|
|
1086
1296
|
mempoolSequenceCacheStatus: "refreshed",
|
|
1087
1297
|
lastMempoolSequence: mempoolSequence,
|
|
1298
|
+
visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
|
|
1088
1299
|
});
|
|
1089
1300
|
}
|
|
1090
1301
|
else {
|
|
1091
1302
|
try {
|
|
1092
|
-
const
|
|
1303
|
+
const candidateRankedEntries = rankMiningSentenceEntries([
|
|
1093
1304
|
{
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1305
|
+
domainId: options.candidate.domainId,
|
|
1306
|
+
domainName: options.candidate.domainName,
|
|
1307
|
+
sentence: options.candidate.sentence,
|
|
1308
|
+
canonicalBlend: options.candidate.canonicalBlend,
|
|
1309
|
+
senderScriptHex: options.candidate.sender.scriptPubKeyHex,
|
|
1310
|
+
encodedSentenceBytesHex: Buffer.from(options.candidate.encodedSentenceBytes).toString("hex"),
|
|
1097
1311
|
bip39WordIndices: options.candidate.bip39WordIndices,
|
|
1312
|
+
txid: null,
|
|
1098
1313
|
txIndex: 0,
|
|
1099
1314
|
},
|
|
1100
1315
|
...[...otherDomainBest.values()]
|
|
1101
1316
|
.sort((left, right) => left.domainId - right.domainId || left.txid.localeCompare(right.txid))
|
|
1102
1317
|
.map((entry, index) => ({
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1318
|
+
domainId: entry.domainId,
|
|
1319
|
+
domainName: entry.domainName,
|
|
1320
|
+
sentence: entry.sentence,
|
|
1321
|
+
canonicalBlend: entry.canonicalBlend,
|
|
1322
|
+
senderScriptHex: entry.senderScriptHex,
|
|
1323
|
+
encodedSentenceBytesHex: entry.encodedSentenceBytesHex,
|
|
1106
1324
|
bip39WordIndices: entry.bip39WordIndices,
|
|
1325
|
+
txid: entry.txid,
|
|
1107
1326
|
txIndex: index + 1,
|
|
1108
1327
|
})),
|
|
1109
|
-
];
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
});
|
|
1115
|
-
const localWinner = winners.find((winner) => winner.miningDomainId === options.candidate.domainId);
|
|
1116
|
-
const higherRankedCompetitorDomainCount = localWinner === undefined
|
|
1117
|
-
? Math.max(0, winners.length - 1)
|
|
1118
|
-
: Math.max(0, localWinner.rank - 1);
|
|
1119
|
-
if (higherRankedCompetitorDomainCount >= 5) {
|
|
1328
|
+
], blendSeed);
|
|
1329
|
+
const localEntry = candidateRankedEntries.find((entry) => entry.txid === null) ?? null;
|
|
1330
|
+
const candidateRank = localEntry?.rank ?? null;
|
|
1331
|
+
const higherRankedCompetitorDomainCount = candidateRank === null ? 0 : Math.max(0, candidateRank - 1);
|
|
1332
|
+
if (candidateRank !== null && candidateRank > 5) {
|
|
1120
1333
|
decision = createDecision({
|
|
1121
1334
|
allowed: false,
|
|
1122
1335
|
decision: "suppressed-top5-mempool",
|
|
@@ -1126,11 +1339,13 @@ async function runCompetitivenessGate(options) {
|
|
|
1126
1339
|
competitivenessGateIndeterminate: false,
|
|
1127
1340
|
mempoolSequenceCacheStatus: "refreshed",
|
|
1128
1341
|
lastMempoolSequence: mempoolSequence,
|
|
1342
|
+
visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
|
|
1343
|
+
candidateRank,
|
|
1129
1344
|
});
|
|
1130
1345
|
}
|
|
1131
1346
|
else {
|
|
1132
1347
|
decision = createDecision({
|
|
1133
|
-
allowed:
|
|
1348
|
+
allowed: candidateRank !== null,
|
|
1134
1349
|
decision: "publish",
|
|
1135
1350
|
sameDomainCompetitorSuppressed: false,
|
|
1136
1351
|
higherRankedCompetitorDomainCount,
|
|
@@ -1138,6 +1353,8 @@ async function runCompetitivenessGate(options) {
|
|
|
1138
1353
|
competitivenessGateIndeterminate: false,
|
|
1139
1354
|
mempoolSequenceCacheStatus: "refreshed",
|
|
1140
1355
|
lastMempoolSequence: mempoolSequence,
|
|
1356
|
+
visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
|
|
1357
|
+
candidateRank,
|
|
1141
1358
|
});
|
|
1142
1359
|
}
|
|
1143
1360
|
}
|
|
@@ -1151,6 +1368,7 @@ async function runCompetitivenessGate(options) {
|
|
|
1151
1368
|
competitivenessGateIndeterminate: true,
|
|
1152
1369
|
mempoolSequenceCacheStatus: "refreshed",
|
|
1153
1370
|
lastMempoolSequence: mempoolSequence,
|
|
1371
|
+
visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
|
|
1154
1372
|
});
|
|
1155
1373
|
}
|
|
1156
1374
|
}
|
|
@@ -1159,7 +1377,6 @@ async function runCompetitivenessGate(options) {
|
|
|
1159
1377
|
indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
|
|
1160
1378
|
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
1161
1379
|
localAssayTupleKey,
|
|
1162
|
-
currentFeeTargetSatVb: localFeeTarget,
|
|
1163
1380
|
excludedTxidsKey: excludedTxids.join(","),
|
|
1164
1381
|
mempoolSequence,
|
|
1165
1382
|
txids: [...visibleTxids],
|
|
@@ -1168,74 +1385,14 @@ async function runCompetitivenessGate(options) {
|
|
|
1168
1385
|
});
|
|
1169
1386
|
return decision;
|
|
1170
1387
|
}
|
|
1171
|
-
function
|
|
1172
|
-
const liveState = normalizeMiningStateRecord(options.liveState);
|
|
1173
|
-
const nextSentenceHex = Buffer.from(options.candidate.encodedSentenceBytes).toString("hex");
|
|
1174
|
-
if (liveState.currentEncodedSentenceBytesHex === null) {
|
|
1175
|
-
return true;
|
|
1176
|
-
}
|
|
1177
|
-
if (liveState.currentDomainId === options.candidate.domainId) {
|
|
1178
|
-
if (liveState.currentEncodedSentenceBytesHex === nextSentenceHex) {
|
|
1179
|
-
return false;
|
|
1180
|
-
}
|
|
1181
|
-
const currentScore = liveState.currentScore === null ? null : BigInt(liveState.currentScore);
|
|
1182
|
-
return currentScore === null || options.candidate.canonicalBlend > currentScore;
|
|
1183
|
-
}
|
|
1184
|
-
return true;
|
|
1185
|
-
}
|
|
1186
|
-
function candidateMatchesLiveFamily(options) {
|
|
1388
|
+
function livePublishTargetsCandidateTip(options) {
|
|
1187
1389
|
const liveState = normalizeMiningStateRecord(options.liveState);
|
|
1188
|
-
return liveState.
|
|
1189
|
-
&& liveState.
|
|
1190
|
-
&& liveState.
|
|
1390
|
+
return liveState.currentTxid !== null
|
|
1391
|
+
&& liveState.currentPublishState === "in-mempool"
|
|
1392
|
+
&& liveState.livePublishInMempool === true
|
|
1191
1393
|
&& liveState.currentReferencedBlockHashDisplay === options.candidate.referencedBlockHashDisplay
|
|
1192
1394
|
&& liveState.currentBlockTargetHeight === options.candidate.targetBlockHeight;
|
|
1193
1395
|
}
|
|
1194
|
-
function candidateNeedsFeeMaintenance(options) {
|
|
1195
|
-
const liveState = normalizeMiningStateRecord(options.liveState);
|
|
1196
|
-
return candidateMatchesLiveFamily(options)
|
|
1197
|
-
&& liveState.currentTxid !== null
|
|
1198
|
-
&& liveState.currentFeeRateSatVb !== null
|
|
1199
|
-
&& liveState.currentPublishState === "in-mempool"
|
|
1200
|
-
&& liveState.liveMiningFamilyInMempool === true;
|
|
1201
|
-
}
|
|
1202
|
-
async function candidateWinsAgainstLive(options) {
|
|
1203
|
-
const liveState = normalizeMiningStateRecord(options.liveState);
|
|
1204
|
-
if (liveState.currentDomainId === null || liveState.currentEncodedSentenceBytesHex === null) {
|
|
1205
|
-
return true;
|
|
1206
|
-
}
|
|
1207
|
-
if (liveState.currentDomainId === options.candidate.domainId) {
|
|
1208
|
-
return candidateOutranksLive(options);
|
|
1209
|
-
}
|
|
1210
|
-
if (liveState.currentBip39WordIndices === null || liveState.currentSenderScriptPubKeyHex === null || liveState.currentBlendSeedHex === null) {
|
|
1211
|
-
return true;
|
|
1212
|
-
}
|
|
1213
|
-
const settled = await settleBlock({
|
|
1214
|
-
blendSeed: Buffer.from(liveState.currentBlendSeedHex, "hex"),
|
|
1215
|
-
blockRewardCogtoshi: 100n,
|
|
1216
|
-
submissions: [
|
|
1217
|
-
{
|
|
1218
|
-
miningDomainId: liveState.currentDomainId,
|
|
1219
|
-
rawSentenceBytes: Buffer.from(liveState.currentEncodedSentenceBytesHex, "hex"),
|
|
1220
|
-
recipientScriptPubKey: Buffer.from(liveState.currentSenderScriptPubKeyHex, "hex"),
|
|
1221
|
-
bip39WordIndices: liveState.currentBip39WordIndices,
|
|
1222
|
-
txIndex: 0,
|
|
1223
|
-
},
|
|
1224
|
-
{
|
|
1225
|
-
miningDomainId: options.candidate.domainId,
|
|
1226
|
-
rawSentenceBytes: options.candidate.encodedSentenceBytes,
|
|
1227
|
-
recipientScriptPubKey: Buffer.from(options.candidate.sender.scriptPubKeyHex, "hex"),
|
|
1228
|
-
bip39WordIndices: options.candidate.bip39WordIndices,
|
|
1229
|
-
txIndex: 1,
|
|
1230
|
-
},
|
|
1231
|
-
],
|
|
1232
|
-
});
|
|
1233
|
-
const incumbent = settled.find((entry) => entry.miningDomainId === liveState.currentDomainId);
|
|
1234
|
-
const challenger = settled.find((entry) => entry.miningDomainId === options.candidate.domainId);
|
|
1235
|
-
return challenger !== undefined
|
|
1236
|
-
&& incumbent !== undefined
|
|
1237
|
-
&& challenger.rank < incumbent.rank;
|
|
1238
|
-
}
|
|
1239
1396
|
function miningCandidateIsCurrent(options) {
|
|
1240
1397
|
return options.state.currentReferencedBlockHashDisplay !== null
|
|
1241
1398
|
&& options.nodeBestHash !== null
|
|
@@ -1250,14 +1407,17 @@ async function reconcileLiveMiningState(options) {
|
|
|
1250
1407
|
miningState: normalizeMiningStateRecord(options.state.miningState),
|
|
1251
1408
|
};
|
|
1252
1409
|
const currentTxid = state.miningState.currentTxid;
|
|
1253
|
-
if (currentTxid === null || !
|
|
1410
|
+
if (currentTxid === null || !miningPublishMayStillExist(state.miningState)) {
|
|
1254
1411
|
await reconcilePersistentPolicyLocks({
|
|
1255
1412
|
rpc: options.rpc,
|
|
1256
1413
|
walletName: state.managedCoreWallet.walletName,
|
|
1257
1414
|
state,
|
|
1258
1415
|
fixedInputs: [],
|
|
1259
1416
|
});
|
|
1260
|
-
return
|
|
1417
|
+
return {
|
|
1418
|
+
state,
|
|
1419
|
+
recentWin: null,
|
|
1420
|
+
};
|
|
1261
1421
|
}
|
|
1262
1422
|
const walletName = state.managedCoreWallet.walletName;
|
|
1263
1423
|
const [mempoolVerbose, walletTx] = await Promise.all([
|
|
@@ -1269,10 +1429,11 @@ async function reconcileLiveMiningState(options) {
|
|
|
1269
1429
|
]);
|
|
1270
1430
|
const inMempool = mempoolVerbose.txids.includes(currentTxid);
|
|
1271
1431
|
if (walletTx !== null && walletTx.confirmations > 0) {
|
|
1432
|
+
const recentWin = findRecentMiningWin(options.snapshotState ?? null, currentTxid, state.miningState.currentBlockTargetHeight);
|
|
1272
1433
|
state = {
|
|
1273
1434
|
...state,
|
|
1274
1435
|
miningState: {
|
|
1275
|
-
...
|
|
1436
|
+
...clearMiningPublishState(state.miningState),
|
|
1276
1437
|
currentPublishDecision: "tx-confirmed-while-down",
|
|
1277
1438
|
},
|
|
1278
1439
|
};
|
|
@@ -1282,7 +1443,10 @@ async function reconcileLiveMiningState(options) {
|
|
|
1282
1443
|
state,
|
|
1283
1444
|
fixedInputs: [],
|
|
1284
1445
|
});
|
|
1285
|
-
return
|
|
1446
|
+
return {
|
|
1447
|
+
state,
|
|
1448
|
+
recentWin,
|
|
1449
|
+
};
|
|
1286
1450
|
}
|
|
1287
1451
|
if (inMempool) {
|
|
1288
1452
|
const stale = !miningCandidateIsCurrent({
|
|
@@ -1291,7 +1455,7 @@ async function reconcileLiveMiningState(options) {
|
|
|
1291
1455
|
nodeBestHeight: options.nodeBestHeight,
|
|
1292
1456
|
});
|
|
1293
1457
|
state = defaultMiningStatePatch(state, {
|
|
1294
|
-
|
|
1458
|
+
livePublishInMempool: true,
|
|
1295
1459
|
currentPublishState: "in-mempool",
|
|
1296
1460
|
state: stale
|
|
1297
1461
|
? "paused-stale"
|
|
@@ -1303,7 +1467,7 @@ async function reconcileLiveMiningState(options) {
|
|
|
1303
1467
|
: state.miningState.runMode === "stopped"
|
|
1304
1468
|
? "user-stopped"
|
|
1305
1469
|
: null,
|
|
1306
|
-
currentPublishDecision: stale ? "paused-stale-mempool" : "restored-live-
|
|
1470
|
+
currentPublishDecision: stale ? "paused-stale-mempool" : "restored-live-publish",
|
|
1307
1471
|
});
|
|
1308
1472
|
await reconcilePersistentPolicyLocks({
|
|
1309
1473
|
rpc: options.rpc,
|
|
@@ -1311,7 +1475,10 @@ async function reconcileLiveMiningState(options) {
|
|
|
1311
1475
|
state,
|
|
1312
1476
|
fixedInputs: [],
|
|
1313
1477
|
});
|
|
1314
|
-
return
|
|
1478
|
+
return {
|
|
1479
|
+
state,
|
|
1480
|
+
recentWin: null,
|
|
1481
|
+
};
|
|
1315
1482
|
}
|
|
1316
1483
|
if ((walletTx?.walletconflicts?.length ?? 0) > 0) {
|
|
1317
1484
|
state = defaultMiningStatePatch(state, {
|
|
@@ -1319,7 +1486,7 @@ async function reconcileLiveMiningState(options) {
|
|
|
1319
1486
|
pauseReason: state.miningState.currentPublishState === "broadcast-unknown"
|
|
1320
1487
|
? "broadcast-unknown-conflict"
|
|
1321
1488
|
: "wallet-conflict-observed",
|
|
1322
|
-
|
|
1489
|
+
livePublishInMempool: false,
|
|
1323
1490
|
currentPublishDecision: state.miningState.currentPublishState === "broadcast-unknown"
|
|
1324
1491
|
? "repair-required-broadcast-conflict"
|
|
1325
1492
|
: "repair-required-wallet-conflict",
|
|
@@ -1330,13 +1497,16 @@ async function reconcileLiveMiningState(options) {
|
|
|
1330
1497
|
state,
|
|
1331
1498
|
fixedInputs: [],
|
|
1332
1499
|
});
|
|
1333
|
-
return
|
|
1500
|
+
return {
|
|
1501
|
+
state,
|
|
1502
|
+
recentWin: null,
|
|
1503
|
+
};
|
|
1334
1504
|
}
|
|
1335
1505
|
state = defaultMiningStatePatch(state, {
|
|
1336
|
-
...
|
|
1506
|
+
...clearMiningPublishState(state.miningState),
|
|
1337
1507
|
currentPublishDecision: state.miningState.currentPublishState === "broadcast-unknown"
|
|
1338
1508
|
? "broadcast-unknown-not-seen"
|
|
1339
|
-
: "live-
|
|
1509
|
+
: "live-publish-not-seen",
|
|
1340
1510
|
});
|
|
1341
1511
|
await reconcilePersistentPolicyLocks({
|
|
1342
1512
|
rpc: options.rpc,
|
|
@@ -1344,9 +1514,12 @@ async function reconcileLiveMiningState(options) {
|
|
|
1344
1514
|
state,
|
|
1345
1515
|
fixedInputs: [],
|
|
1346
1516
|
});
|
|
1347
|
-
return
|
|
1517
|
+
return {
|
|
1518
|
+
state,
|
|
1519
|
+
recentWin: null,
|
|
1520
|
+
};
|
|
1348
1521
|
}
|
|
1349
|
-
async function
|
|
1522
|
+
async function publishCandidateOnce(options) {
|
|
1350
1523
|
const service = await options.attachService({
|
|
1351
1524
|
dataDir: options.dataDir,
|
|
1352
1525
|
chain: "main",
|
|
@@ -1354,48 +1527,35 @@ async function publishCandidate(options) {
|
|
|
1354
1527
|
walletRootId: options.readContext.localState.state.walletRootId,
|
|
1355
1528
|
});
|
|
1356
1529
|
const rpc = options.rpcFactory(service.rpc);
|
|
1357
|
-
let state = await reconcileLiveMiningState({
|
|
1530
|
+
let state = (await reconcileLiveMiningState({
|
|
1358
1531
|
state: options.readContext.localState.state,
|
|
1359
1532
|
rpc,
|
|
1360
1533
|
nodeBestHash: options.readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1361
1534
|
nodeBestHeight: options.readContext.nodeStatus?.nodeBestHeight ?? null,
|
|
1362
|
-
|
|
1535
|
+
snapshotState: options.readContext.snapshot.state,
|
|
1536
|
+
})).state;
|
|
1363
1537
|
const allUtxos = await rpc.listUnspent(state.managedCoreWallet.walletName, 0);
|
|
1364
|
-
const
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
&& entry.safe !== false
|
|
1369
|
-
&& !(entry.txid === options.candidate.anchorOutpoint.txid && entry.vout === options.candidate.anchorOutpoint.vout));
|
|
1370
|
-
if (fundingConflict === undefined || fundingConflict === null) {
|
|
1371
|
-
throw new Error("wallet_mining_missing_conflict_utxo");
|
|
1372
|
-
}
|
|
1373
|
-
const conflictOutpoint = "txid" in fundingConflict
|
|
1374
|
-
? { txid: fundingConflict.txid, vout: fundingConflict.vout }
|
|
1375
|
-
: fundingConflict;
|
|
1538
|
+
const conflictOutpoint = resolveMiningConflictOutpoint({
|
|
1539
|
+
state,
|
|
1540
|
+
allUtxos,
|
|
1541
|
+
});
|
|
1376
1542
|
const priorMiningState = cloneMiningState(state.miningState);
|
|
1377
|
-
|
|
1378
|
-
? DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB
|
|
1379
|
-
: state.miningState.currentFeeRateSatVb + 1;
|
|
1380
|
-
const shouldFeeBump = candidateNeedsFeeMaintenance({
|
|
1543
|
+
if (livePublishTargetsCandidateTip({
|
|
1381
1544
|
liveState: state.miningState,
|
|
1382
1545
|
candidate: options.candidate,
|
|
1383
|
-
})
|
|
1384
|
-
if (state.miningState.currentPublishState === "in-mempool"
|
|
1385
|
-
&& state.miningState.liveMiningFamilyInMempool === true
|
|
1386
|
-
&& !shouldFeeBump
|
|
1387
|
-
&& !await candidateWinsAgainstLive({
|
|
1388
|
-
liveState: state.miningState,
|
|
1389
|
-
candidate: options.candidate,
|
|
1390
|
-
})) {
|
|
1546
|
+
})) {
|
|
1391
1547
|
return {
|
|
1392
1548
|
state: defaultMiningStatePatch(state, {
|
|
1393
|
-
currentPublishDecision: "kept-live-
|
|
1549
|
+
currentPublishDecision: "kept-live-publish",
|
|
1394
1550
|
}),
|
|
1395
1551
|
txid: state.miningState.currentTxid,
|
|
1396
|
-
decision: "kept-live-
|
|
1552
|
+
decision: "kept-live-publish",
|
|
1397
1553
|
};
|
|
1398
1554
|
}
|
|
1555
|
+
const feeSelection = await resolveWalletMutationFeeSelection({
|
|
1556
|
+
rpc,
|
|
1557
|
+
});
|
|
1558
|
+
const nextFeeRate = feeSelection.feeRateSatVb;
|
|
1399
1559
|
const plan = createMiningPlan({
|
|
1400
1560
|
state,
|
|
1401
1561
|
candidate: options.candidate,
|
|
@@ -1430,18 +1590,14 @@ async function publishCandidate(options) {
|
|
|
1430
1590
|
currentReferencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
1431
1591
|
currentIntentFingerprintHex: intentFingerprintHex,
|
|
1432
1592
|
sharedMiningConflictOutpoint: conflictOutpoint,
|
|
1433
|
-
|
|
1593
|
+
livePublishInMempool: null,
|
|
1434
1594
|
currentPublishDecision: priorMiningState.currentTxid === null
|
|
1435
1595
|
? "publishing"
|
|
1436
|
-
:
|
|
1437
|
-
? "fee-bump"
|
|
1438
|
-
: "replacing",
|
|
1596
|
+
: "replacing",
|
|
1439
1597
|
});
|
|
1440
1598
|
await saveWalletStatePreservingUnlock({
|
|
1441
1599
|
state,
|
|
1442
1600
|
provider: options.provider,
|
|
1443
|
-
unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
|
|
1444
|
-
nowUnixMs: Date.now(),
|
|
1445
1601
|
paths: options.paths,
|
|
1446
1602
|
});
|
|
1447
1603
|
try {
|
|
@@ -1451,13 +1607,11 @@ async function publishCandidate(options) {
|
|
|
1451
1607
|
if (isAlreadyAcceptedError(error)) {
|
|
1452
1608
|
state = defaultMiningStatePatch(state, {
|
|
1453
1609
|
currentPublishState: "in-mempool",
|
|
1454
|
-
|
|
1610
|
+
livePublishInMempool: true,
|
|
1455
1611
|
});
|
|
1456
1612
|
await saveWalletStatePreservingUnlock({
|
|
1457
1613
|
state,
|
|
1458
1614
|
provider: options.provider,
|
|
1459
|
-
unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
|
|
1460
|
-
nowUnixMs: Date.now(),
|
|
1461
1615
|
paths: options.paths,
|
|
1462
1616
|
});
|
|
1463
1617
|
await appendEvent(options.paths, createEvent(state.miningState.currentPublishDecision === "replacing" ? "tx-replaced" : "tx-broadcast", `Mining transaction ${built.txid} is already accepted by the local node.`, {
|
|
@@ -1474,11 +1628,9 @@ async function publishCandidate(options) {
|
|
|
1474
1628
|
return {
|
|
1475
1629
|
state,
|
|
1476
1630
|
txid: built.txid,
|
|
1477
|
-
decision: state.miningState.currentPublishDecision === "
|
|
1478
|
-
? "
|
|
1479
|
-
:
|
|
1480
|
-
? "replaced"
|
|
1481
|
-
: "broadcast",
|
|
1631
|
+
decision: state.miningState.currentPublishDecision === "replacing"
|
|
1632
|
+
? "replaced"
|
|
1633
|
+
: "broadcast",
|
|
1482
1634
|
};
|
|
1483
1635
|
}
|
|
1484
1636
|
if (isBroadcastUnknownError(error)) {
|
|
@@ -1489,8 +1641,6 @@ async function publishCandidate(options) {
|
|
|
1489
1641
|
await saveWalletStatePreservingUnlock({
|
|
1490
1642
|
state,
|
|
1491
1643
|
provider: options.provider,
|
|
1492
|
-
unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
|
|
1493
|
-
nowUnixMs: Date.now(),
|
|
1494
1644
|
paths: options.paths,
|
|
1495
1645
|
});
|
|
1496
1646
|
await appendEvent(options.paths, createEvent("error", `Mining broadcast became uncertain for ${built.txid}.`, {
|
|
@@ -1512,7 +1662,16 @@ async function publishCandidate(options) {
|
|
|
1512
1662
|
decision: "broadcast-unknown",
|
|
1513
1663
|
};
|
|
1514
1664
|
}
|
|
1515
|
-
|
|
1665
|
+
state = {
|
|
1666
|
+
...state,
|
|
1667
|
+
miningState: cloneMiningState(priorMiningState),
|
|
1668
|
+
};
|
|
1669
|
+
await saveWalletStatePreservingUnlock({
|
|
1670
|
+
state,
|
|
1671
|
+
provider: options.provider,
|
|
1672
|
+
paths: options.paths,
|
|
1673
|
+
});
|
|
1674
|
+
throw new MiningPublishRejectedError(error instanceof Error ? error.message : String(error), state);
|
|
1516
1675
|
}
|
|
1517
1676
|
const absoluteFeeSats = numberToSats(built.funded.fee);
|
|
1518
1677
|
const replacementCount = priorMiningState.currentTxid === null
|
|
@@ -1520,12 +1679,10 @@ async function publishCandidate(options) {
|
|
|
1520
1679
|
: priorMiningState.replacementCount + 1;
|
|
1521
1680
|
state = defaultMiningStatePatch(state, {
|
|
1522
1681
|
currentPublishState: "in-mempool",
|
|
1523
|
-
|
|
1524
|
-
currentPublishDecision: state.miningState.currentPublishDecision === "
|
|
1525
|
-
? "
|
|
1526
|
-
:
|
|
1527
|
-
? "replaced"
|
|
1528
|
-
: "broadcast",
|
|
1682
|
+
livePublishInMempool: true,
|
|
1683
|
+
currentPublishDecision: state.miningState.currentPublishDecision === "replacing"
|
|
1684
|
+
? "replaced"
|
|
1685
|
+
: "broadcast",
|
|
1529
1686
|
replacementCount,
|
|
1530
1687
|
currentAbsoluteFeeSats: Number(absoluteFeeSats),
|
|
1531
1688
|
currentBlockFeeSpentSats: (BigInt(state.miningState.currentBlockFeeSpentSats) + absoluteFeeSats).toString(),
|
|
@@ -1535,19 +1692,13 @@ async function publishCandidate(options) {
|
|
|
1535
1692
|
await saveWalletStatePreservingUnlock({
|
|
1536
1693
|
state,
|
|
1537
1694
|
provider: options.provider,
|
|
1538
|
-
unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
|
|
1539
|
-
nowUnixMs: Date.now(),
|
|
1540
1695
|
paths: options.paths,
|
|
1541
1696
|
});
|
|
1542
1697
|
await appendEvent(options.paths, createEvent(state.miningState.currentPublishDecision === "replaced"
|
|
1543
1698
|
? "tx-replaced"
|
|
1544
|
-
: state.miningState.currentPublishDecision === "
|
|
1545
|
-
? "tx-fee-bump"
|
|
1546
|
-
: "tx-broadcast", `${state.miningState.currentPublishDecision === "replaced"
|
|
1699
|
+
: "tx-broadcast", `${state.miningState.currentPublishDecision === "replaced"
|
|
1547
1700
|
? "Replaced"
|
|
1548
|
-
:
|
|
1549
|
-
? "Fee-bumped"
|
|
1550
|
-
: "Broadcast"} mining transaction ${built.txid}.`, {
|
|
1701
|
+
: "Broadcast"} mining transaction ${built.txid}.`, {
|
|
1551
1702
|
runId: options.runId,
|
|
1552
1703
|
targetBlockHeight: options.candidate.targetBlockHeight,
|
|
1553
1704
|
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
@@ -1561,21 +1712,106 @@ async function publishCandidate(options) {
|
|
|
1561
1712
|
return {
|
|
1562
1713
|
state,
|
|
1563
1714
|
txid: built.txid,
|
|
1564
|
-
decision: state.miningState.currentPublishDecision === "
|
|
1565
|
-
? "
|
|
1566
|
-
:
|
|
1567
|
-
? "replaced"
|
|
1568
|
-
: "broadcast",
|
|
1715
|
+
decision: state.miningState.currentPublishDecision === "replaced"
|
|
1716
|
+
? "replaced"
|
|
1717
|
+
: "broadcast",
|
|
1569
1718
|
};
|
|
1570
1719
|
}
|
|
1571
|
-
async function
|
|
1572
|
-
const
|
|
1573
|
-
|
|
1720
|
+
async function publishCandidate(options) {
|
|
1721
|
+
const publishAttempt = options.publishAttempt ?? publishCandidateOnce;
|
|
1722
|
+
const appendEventFn = options.appendEventFn ?? appendEvent;
|
|
1723
|
+
const createStaleCandidateSkipResult = async (state) => {
|
|
1724
|
+
const note = createStaleMiningCandidateWaitingNote();
|
|
1725
|
+
await appendEventFn(options.paths, createEvent("publish-skipped-stale-candidate", "Skipped mining publish for the current tip because the selected root domain is no longer locally mineable.", {
|
|
1726
|
+
level: "warn",
|
|
1727
|
+
runId: options.runId,
|
|
1728
|
+
targetBlockHeight: options.candidate.targetBlockHeight,
|
|
1729
|
+
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
1730
|
+
domainId: options.candidate.domainId,
|
|
1731
|
+
domainName: options.candidate.domainName,
|
|
1732
|
+
score: options.candidate.canonicalBlend.toString(),
|
|
1733
|
+
reason: "candidate-unavailable",
|
|
1734
|
+
}));
|
|
1735
|
+
return {
|
|
1736
|
+
state,
|
|
1737
|
+
txid: null,
|
|
1738
|
+
decision: "publish-skipped-stale-candidate",
|
|
1739
|
+
note,
|
|
1740
|
+
skipped: true,
|
|
1741
|
+
candidate: null,
|
|
1742
|
+
};
|
|
1743
|
+
};
|
|
1744
|
+
const lockedReadContext = await options.openReadContext({
|
|
1745
|
+
dataDir: options.dataDir,
|
|
1746
|
+
databasePath: options.databasePath,
|
|
1747
|
+
secretProvider: options.provider,
|
|
1748
|
+
walletControlLockHeld: true,
|
|
1574
1749
|
paths: options.paths,
|
|
1575
1750
|
});
|
|
1576
|
-
|
|
1577
|
-
|
|
1751
|
+
try {
|
|
1752
|
+
if (lockedReadContext.localState.availability !== "ready"
|
|
1753
|
+
|| lockedReadContext.localState.state === null
|
|
1754
|
+
|| lockedReadContext.snapshot === null
|
|
1755
|
+
|| lockedReadContext.model === null) {
|
|
1756
|
+
return await createStaleCandidateSkipResult(options.fallbackState);
|
|
1757
|
+
}
|
|
1758
|
+
const readyReadContext = lockedReadContext;
|
|
1759
|
+
const refreshedCandidate = refreshMiningCandidateFromCurrentState(readyReadContext, options.candidate);
|
|
1760
|
+
if (refreshedCandidate === null) {
|
|
1761
|
+
return await createStaleCandidateSkipResult(readyReadContext.localState.state);
|
|
1762
|
+
}
|
|
1763
|
+
try {
|
|
1764
|
+
const published = await publishAttempt({
|
|
1765
|
+
readContext: readyReadContext,
|
|
1766
|
+
candidate: refreshedCandidate,
|
|
1767
|
+
dataDir: options.dataDir,
|
|
1768
|
+
provider: options.provider,
|
|
1769
|
+
paths: options.paths,
|
|
1770
|
+
attachService: options.attachService,
|
|
1771
|
+
rpcFactory: options.rpcFactory,
|
|
1772
|
+
runId: options.runId,
|
|
1773
|
+
});
|
|
1774
|
+
return {
|
|
1775
|
+
...published,
|
|
1776
|
+
candidate: refreshedCandidate,
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
catch (error) {
|
|
1780
|
+
if (error instanceof Error && error.message === "wallet_mining_mempool_rejected_missing-inputs") {
|
|
1781
|
+
const note = createRetryableMiningPublishWaitingNote();
|
|
1782
|
+
const revertedState = error instanceof MiningPublishRejectedError
|
|
1783
|
+
? error.revertedState
|
|
1784
|
+
: readyReadContext.localState.state;
|
|
1785
|
+
await appendEventFn(options.paths, createEvent("publish-retry-pending", "Selected mining candidate did not reach mempool and will be retried on the current tip with refreshed wallet state.", {
|
|
1786
|
+
level: "warn",
|
|
1787
|
+
runId: options.runId,
|
|
1788
|
+
targetBlockHeight: refreshedCandidate.targetBlockHeight,
|
|
1789
|
+
referencedBlockHashDisplay: refreshedCandidate.referencedBlockHashDisplay,
|
|
1790
|
+
domainId: refreshedCandidate.domainId,
|
|
1791
|
+
domainName: refreshedCandidate.domainName,
|
|
1792
|
+
score: refreshedCandidate.canonicalBlend.toString(),
|
|
1793
|
+
reason: "missing-inputs",
|
|
1794
|
+
}));
|
|
1795
|
+
return {
|
|
1796
|
+
state: revertedState,
|
|
1797
|
+
txid: null,
|
|
1798
|
+
decision: "publish-retry-pending",
|
|
1799
|
+
note,
|
|
1800
|
+
retryable: true,
|
|
1801
|
+
candidate: refreshedCandidate,
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
throw error;
|
|
1805
|
+
}
|
|
1578
1806
|
}
|
|
1807
|
+
finally {
|
|
1808
|
+
await lockedReadContext.close();
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
export async function publishCandidateForTesting(options) {
|
|
1812
|
+
return await publishCandidate(options);
|
|
1813
|
+
}
|
|
1814
|
+
async function ensureBuiltInSetupIfNeeded(options) {
|
|
1579
1815
|
const config = await loadClientConfig({
|
|
1580
1816
|
path: options.paths.clientConfigPath,
|
|
1581
1817
|
provider: options.provider,
|
|
@@ -1614,7 +1850,7 @@ async function performMiningCycle(options) {
|
|
|
1614
1850
|
backgroundWorkerHeartbeatAtUnixMs: options.runMode === "background" ? Date.now() : null,
|
|
1615
1851
|
},
|
|
1616
1852
|
});
|
|
1617
|
-
if (readContext.localState.availability !== "ready" || readContext.localState.state === null
|
|
1853
|
+
if (readContext.localState.availability !== "ready" || readContext.localState.state === null) {
|
|
1618
1854
|
await refreshAndSaveStatus({
|
|
1619
1855
|
paths: options.paths,
|
|
1620
1856
|
provider: options.provider,
|
|
@@ -1622,9 +1858,10 @@ async function performMiningCycle(options) {
|
|
|
1622
1858
|
overrides: {
|
|
1623
1859
|
runMode: options.runMode,
|
|
1624
1860
|
currentPhase: "waiting",
|
|
1625
|
-
note: "Wallet must
|
|
1861
|
+
note: "Wallet state must be locally available for mining to continue.",
|
|
1626
1862
|
},
|
|
1627
1863
|
visualizer: options.visualizer,
|
|
1864
|
+
visualizerState: options.loopState.ui,
|
|
1628
1865
|
});
|
|
1629
1866
|
return;
|
|
1630
1867
|
}
|
|
@@ -1636,20 +1873,20 @@ async function performMiningCycle(options) {
|
|
|
1636
1873
|
});
|
|
1637
1874
|
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
1638
1875
|
const rpc = options.rpcFactory(service.rpc);
|
|
1639
|
-
const
|
|
1876
|
+
const reconciliation = await reconcileLiveMiningState({
|
|
1640
1877
|
state: readContext.localState.state,
|
|
1641
1878
|
rpc,
|
|
1642
1879
|
nodeBestHash: readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1643
1880
|
nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
|
|
1881
|
+
snapshotState: readContext.snapshot?.state ?? null,
|
|
1644
1882
|
});
|
|
1883
|
+
const reconciledState = reconciliation.state;
|
|
1645
1884
|
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
1646
1885
|
let effectiveReadContext = readContext;
|
|
1647
1886
|
if (JSON.stringify(reconciledState.miningState) !== JSON.stringify(readContext.localState.state.miningState)) {
|
|
1648
1887
|
await saveWalletStatePreservingUnlock({
|
|
1649
1888
|
state: reconciledState,
|
|
1650
1889
|
provider: options.provider,
|
|
1651
|
-
unlockUntilUnixMs: readContext.localState.unlockUntilUnixMs,
|
|
1652
|
-
nowUnixMs: Date.now(),
|
|
1653
1890
|
paths: options.paths,
|
|
1654
1891
|
});
|
|
1655
1892
|
effectiveReadContext = {
|
|
@@ -1657,11 +1894,23 @@ async function performMiningCycle(options) {
|
|
|
1657
1894
|
localState: {
|
|
1658
1895
|
...readContext.localState,
|
|
1659
1896
|
availability: "ready",
|
|
1660
|
-
unlockUntilUnixMs: readContext.localState.unlockUntilUnixMs,
|
|
1661
1897
|
state: reconciledState,
|
|
1662
1898
|
},
|
|
1663
1899
|
};
|
|
1664
1900
|
}
|
|
1901
|
+
if (reconciliation.recentWin !== null) {
|
|
1902
|
+
options.loopState.ui.recentWin = reconciliation.recentWin;
|
|
1903
|
+
}
|
|
1904
|
+
if (effectiveReadContext.localState.state.miningState.currentTxid !== null) {
|
|
1905
|
+
options.loopState.ui.latestTxid = effectiveReadContext.localState.state.miningState.currentTxid;
|
|
1906
|
+
}
|
|
1907
|
+
const indexedTip = effectiveReadContext.snapshot?.tip ?? effectiveReadContext.indexer.snapshotTip ?? null;
|
|
1908
|
+
const visibleBlockTimes = await loadMiningVisibleFollowBlockTimes({
|
|
1909
|
+
rpc,
|
|
1910
|
+
indexedTipHeight: indexedTip?.height ?? null,
|
|
1911
|
+
indexedTipHashHex: indexedTip?.blockHashHex ?? null,
|
|
1912
|
+
}).catch(() => ({}));
|
|
1913
|
+
syncMiningVisualizerBlockTimes(options.loopState, visibleBlockTimes);
|
|
1665
1914
|
if (effectiveReadContext.localState.state.miningState.state === "repair-required") {
|
|
1666
1915
|
await refreshAndSaveStatus({
|
|
1667
1916
|
paths: options.paths,
|
|
@@ -1670,9 +1919,10 @@ async function performMiningCycle(options) {
|
|
|
1670
1919
|
overrides: {
|
|
1671
1920
|
runMode: options.runMode,
|
|
1672
1921
|
currentPhase: "waiting",
|
|
1673
|
-
note: "Mining is blocked until the current mining
|
|
1922
|
+
note: "Mining is blocked until the current mining publish is repaired or reconciled.",
|
|
1674
1923
|
},
|
|
1675
1924
|
visualizer: options.visualizer,
|
|
1925
|
+
visualizerState: options.loopState.ui,
|
|
1676
1926
|
});
|
|
1677
1927
|
return;
|
|
1678
1928
|
}
|
|
@@ -1684,8 +1934,6 @@ async function performMiningCycle(options) {
|
|
|
1684
1934
|
await saveWalletStatePreservingUnlock({
|
|
1685
1935
|
state: nextState,
|
|
1686
1936
|
provider: options.provider,
|
|
1687
|
-
unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
|
|
1688
|
-
nowUnixMs: Date.now(),
|
|
1689
1937
|
paths: options.paths,
|
|
1690
1938
|
});
|
|
1691
1939
|
effectiveReadContext = {
|
|
@@ -1693,7 +1941,6 @@ async function performMiningCycle(options) {
|
|
|
1693
1941
|
localState: {
|
|
1694
1942
|
...effectiveReadContext.localState,
|
|
1695
1943
|
availability: "ready",
|
|
1696
|
-
unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
|
|
1697
1944
|
state: nextState,
|
|
1698
1945
|
},
|
|
1699
1946
|
};
|
|
@@ -1704,16 +1951,17 @@ async function performMiningCycle(options) {
|
|
|
1704
1951
|
overrides: {
|
|
1705
1952
|
runMode: options.runMode,
|
|
1706
1953
|
currentPhase: "waiting",
|
|
1707
|
-
note: "Mining is paused while another wallet mutation
|
|
1954
|
+
note: "Mining is paused while another wallet mutation is active.",
|
|
1708
1955
|
},
|
|
1709
1956
|
visualizer: options.visualizer,
|
|
1957
|
+
visualizerState: options.loopState.ui,
|
|
1710
1958
|
});
|
|
1711
1959
|
return;
|
|
1712
1960
|
}
|
|
1713
1961
|
const preemptionRequest = await readMiningPreemptionRequest(options.paths);
|
|
1714
1962
|
if (preemptionRequest !== null) {
|
|
1715
1963
|
const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
|
|
1716
|
-
state: effectiveReadContext.localState.state.miningState.
|
|
1964
|
+
state: effectiveReadContext.localState.state.miningState.livePublishInMempool
|
|
1717
1965
|
&& effectiveReadContext.localState.state.miningState.state === "paused-stale"
|
|
1718
1966
|
? "paused-stale"
|
|
1719
1967
|
: "paused",
|
|
@@ -1722,8 +1970,6 @@ async function performMiningCycle(options) {
|
|
|
1722
1970
|
await saveWalletStatePreservingUnlock({
|
|
1723
1971
|
state: nextState,
|
|
1724
1972
|
provider: options.provider,
|
|
1725
|
-
unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
|
|
1726
|
-
nowUnixMs: Date.now(),
|
|
1727
1973
|
paths: options.paths,
|
|
1728
1974
|
});
|
|
1729
1975
|
await refreshAndSaveStatus({
|
|
@@ -1742,6 +1988,7 @@ async function performMiningCycle(options) {
|
|
|
1742
1988
|
note: "Mining is paused while another wallet command is preempting sentence generation.",
|
|
1743
1989
|
},
|
|
1744
1990
|
visualizer: options.visualizer,
|
|
1991
|
+
visualizerState: options.loopState.ui,
|
|
1745
1992
|
});
|
|
1746
1993
|
return;
|
|
1747
1994
|
}
|
|
@@ -1768,6 +2015,7 @@ async function performMiningCycle(options) {
|
|
|
1768
2015
|
note: "Mining is waiting for the local Bitcoin node to become publishable.",
|
|
1769
2016
|
},
|
|
1770
2017
|
visualizer: options.visualizer,
|
|
2018
|
+
visualizerState: options.loopState.ui,
|
|
1771
2019
|
});
|
|
1772
2020
|
return;
|
|
1773
2021
|
}
|
|
@@ -1786,10 +2034,22 @@ async function performMiningCycle(options) {
|
|
|
1786
2034
|
: "Mining is waiting for the local Bitcoin node to become publishable.",
|
|
1787
2035
|
},
|
|
1788
2036
|
visualizer: options.visualizer,
|
|
2037
|
+
visualizerState: options.loopState.ui,
|
|
1789
2038
|
});
|
|
1790
2039
|
return;
|
|
1791
2040
|
}
|
|
1792
2041
|
const targetBlockHeight = (effectiveReadContext.nodeStatus?.nodeBestHeight ?? 0) + 1;
|
|
2042
|
+
const tipKey = buildMiningTipKey(effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null, targetBlockHeight);
|
|
2043
|
+
if (tipKey !== options.loopState.currentTipKey) {
|
|
2044
|
+
options.loopState.currentTipKey = tipKey;
|
|
2045
|
+
resetMiningUiForTip(options.loopState, targetBlockHeight);
|
|
2046
|
+
if (reconciliation.recentWin !== null) {
|
|
2047
|
+
options.loopState.ui.recentWin = reconciliation.recentWin;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
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);
|
|
1793
2053
|
if (getBlockRewardCogtoshi(targetBlockHeight) === 0n) {
|
|
1794
2054
|
const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
|
|
1795
2055
|
state: "paused",
|
|
@@ -1798,8 +2058,6 @@ async function performMiningCycle(options) {
|
|
|
1798
2058
|
await saveWalletStatePreservingUnlock({
|
|
1799
2059
|
state: nextState,
|
|
1800
2060
|
provider: options.provider,
|
|
1801
|
-
unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
|
|
1802
|
-
nowUnixMs: Date.now(),
|
|
1803
2061
|
paths: options.paths,
|
|
1804
2062
|
});
|
|
1805
2063
|
await refreshAndSaveStatus({
|
|
@@ -1819,6 +2077,7 @@ async function performMiningCycle(options) {
|
|
|
1819
2077
|
note: "Mining is disabled because the target block reward is zero.",
|
|
1820
2078
|
},
|
|
1821
2079
|
visualizer: options.visualizer,
|
|
2080
|
+
visualizerState: options.loopState.ui,
|
|
1822
2081
|
});
|
|
1823
2082
|
await appendEvent(options.paths, createEvent("publish-skipped-zero-reward", "Skipped mining because the target block reward is zero.", {
|
|
1824
2083
|
targetBlockHeight,
|
|
@@ -1827,18 +2086,18 @@ async function performMiningCycle(options) {
|
|
|
1827
2086
|
}));
|
|
1828
2087
|
return;
|
|
1829
2088
|
}
|
|
1830
|
-
|
|
1831
|
-
if (domains.length === 0) {
|
|
2089
|
+
if (tipKey !== null && options.loopState.attemptedTipKey === tipKey) {
|
|
1832
2090
|
await refreshAndSaveStatus({
|
|
1833
2091
|
paths: options.paths,
|
|
1834
2092
|
provider: options.provider,
|
|
1835
2093
|
readContext: effectiveReadContext,
|
|
1836
2094
|
overrides: {
|
|
1837
2095
|
runMode: options.runMode,
|
|
1838
|
-
currentPhase: "
|
|
1839
|
-
note: "
|
|
2096
|
+
currentPhase: "waiting",
|
|
2097
|
+
note: options.loopState.waitingNote ?? "Waiting for the next block after the last mining attempt on this tip.",
|
|
1840
2098
|
},
|
|
1841
2099
|
visualizer: options.visualizer,
|
|
2100
|
+
visualizerState: options.loopState.ui,
|
|
1842
2101
|
});
|
|
1843
2102
|
return;
|
|
1844
2103
|
}
|
|
@@ -1866,215 +2125,264 @@ async function performMiningCycle(options) {
|
|
|
1866
2125
|
return false;
|
|
1867
2126
|
}
|
|
1868
2127
|
};
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
});
|
|
1880
|
-
await appendEvent(options.paths, createEvent("hook-request-start", "Started mining sentence generation.", {
|
|
1881
|
-
targetBlockHeight,
|
|
1882
|
-
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1883
|
-
runId: options.backgroundWorkerRunId,
|
|
1884
|
-
}));
|
|
1885
|
-
let candidates;
|
|
1886
|
-
try {
|
|
1887
|
-
candidates = await generateCandidatesForDomains({
|
|
1888
|
-
rpc,
|
|
1889
|
-
readContext: effectiveReadContext,
|
|
1890
|
-
domains,
|
|
1891
|
-
provider: options.provider,
|
|
1892
|
-
paths: options.paths,
|
|
1893
|
-
indexerTruthKey,
|
|
1894
|
-
runId: options.backgroundWorkerRunId,
|
|
1895
|
-
fetchImpl: options.fetchImpl,
|
|
1896
|
-
});
|
|
1897
|
-
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
1898
|
-
}
|
|
1899
|
-
catch (error) {
|
|
1900
|
-
if (error instanceof MiningProviderRequestError) {
|
|
2128
|
+
let selectedCandidate = getSelectedCandidateForTip(options.loopState, tipKey);
|
|
2129
|
+
let gateSnapshot = {
|
|
2130
|
+
higherRankedCompetitorDomainCount: 0,
|
|
2131
|
+
dedupedCompetitorDomainCount: 0,
|
|
2132
|
+
mempoolSequenceCacheStatus: null,
|
|
2133
|
+
lastMempoolSequence: null,
|
|
2134
|
+
};
|
|
2135
|
+
if (selectedCandidate === null) {
|
|
2136
|
+
const domains = resolveEligibleAnchoredRoots(effectiveReadContext);
|
|
2137
|
+
if (domains.length === 0) {
|
|
1901
2138
|
await refreshAndSaveStatus({
|
|
1902
2139
|
paths: options.paths,
|
|
1903
2140
|
provider: options.provider,
|
|
1904
2141
|
readContext: effectiveReadContext,
|
|
1905
2142
|
overrides: {
|
|
1906
2143
|
runMode: options.runMode,
|
|
1907
|
-
currentPhase: "
|
|
1908
|
-
|
|
1909
|
-
lastError: error.message,
|
|
1910
|
-
note: "Mining is waiting for the sentence provider to recover.",
|
|
2144
|
+
currentPhase: "idle",
|
|
2145
|
+
note: "No locally controlled anchored root domains are currently eligible to mine.",
|
|
1911
2146
|
},
|
|
1912
2147
|
visualizer: options.visualizer,
|
|
2148
|
+
visualizerState: options.loopState.ui,
|
|
1913
2149
|
});
|
|
1914
|
-
await appendEvent(options.paths, createEvent("publish-paused-provider", error.message, {
|
|
1915
|
-
level: "warn",
|
|
1916
|
-
targetBlockHeight,
|
|
1917
|
-
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1918
|
-
runId: options.backgroundWorkerRunId,
|
|
1919
|
-
}));
|
|
1920
2150
|
return;
|
|
1921
2151
|
}
|
|
1922
|
-
if (error instanceof Error && error.message === "mining_generation_stale_tip") {
|
|
1923
|
-
await appendEvent(options.paths, createEvent("generation-restarted-new-tip", "Detected a new best tip during sentence generation; restarting on the next tick.", {
|
|
1924
|
-
level: "warn",
|
|
1925
|
-
targetBlockHeight,
|
|
1926
|
-
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1927
|
-
runId: options.backgroundWorkerRunId,
|
|
1928
|
-
}));
|
|
1929
|
-
return;
|
|
1930
|
-
}
|
|
1931
|
-
if (error instanceof Error && error.message === "mining_generation_stale_indexer_truth") {
|
|
1932
|
-
clearMiningGateCache(walletRootId);
|
|
1933
|
-
await appendEvent(options.paths, createEvent("generation-restarted-indexer-truth", "Detected updated coherent indexer truth during sentence generation; restarting on the next tick.", {
|
|
1934
|
-
level: "warn",
|
|
1935
|
-
targetBlockHeight,
|
|
1936
|
-
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1937
|
-
runId: options.backgroundWorkerRunId,
|
|
1938
|
-
}));
|
|
1939
|
-
return;
|
|
1940
|
-
}
|
|
1941
|
-
if (error instanceof Error && error.message === "mining_generation_preempted") {
|
|
1942
|
-
await appendEvent(options.paths, createEvent("generation-paused-preempted", "Stopped sentence generation because another wallet command requested mining preemption.", {
|
|
1943
|
-
level: "warn",
|
|
1944
|
-
targetBlockHeight,
|
|
1945
|
-
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1946
|
-
runId: options.backgroundWorkerRunId,
|
|
1947
|
-
}));
|
|
1948
|
-
return;
|
|
1949
|
-
}
|
|
1950
|
-
const hookCooldownActive = await persistCustomHookRuntimeOutcome({
|
|
1951
|
-
readContext: effectiveReadContext,
|
|
1952
|
-
provider: options.provider,
|
|
1953
|
-
paths: options.paths,
|
|
1954
|
-
nowUnixMs: Date.now(),
|
|
1955
|
-
success: false,
|
|
1956
|
-
});
|
|
1957
|
-
const failureMessage = error instanceof Error ? error.message : String(error);
|
|
1958
|
-
await refreshAndSaveStatus({
|
|
1959
|
-
paths: options.paths,
|
|
1960
|
-
provider: options.provider,
|
|
1961
|
-
readContext: effectiveReadContext,
|
|
1962
|
-
overrides: {
|
|
1963
|
-
runMode: options.runMode,
|
|
1964
|
-
currentPhase: "waiting-provider",
|
|
1965
|
-
providerState: effectiveReadContext.localState.state?.hookClientState.mining.mode === "custom"
|
|
1966
|
-
? "hook-error"
|
|
1967
|
-
: undefined,
|
|
1968
|
-
lastError: failureMessage,
|
|
1969
|
-
note: effectiveReadContext.localState.state?.hookClientState.mining.mode === "custom"
|
|
1970
|
-
? (hookCooldownActive
|
|
1971
|
-
? "Custom mining hook launch is paused during the post-failure cooldown window."
|
|
1972
|
-
: "Custom mining hook failed during sentence generation. Fix it or rerun `cogcoin hooks enable mining`.")
|
|
1973
|
-
: "Mining sentence generation failed for the current tip.",
|
|
1974
|
-
},
|
|
1975
|
-
visualizer: options.visualizer,
|
|
1976
|
-
});
|
|
1977
|
-
await appendEvent(options.paths, createEvent("hook-request-failed", failureMessage, {
|
|
1978
|
-
level: "error",
|
|
1979
|
-
targetBlockHeight,
|
|
1980
|
-
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1981
|
-
runId: options.backgroundWorkerRunId,
|
|
1982
|
-
}));
|
|
1983
|
-
return;
|
|
1984
|
-
}
|
|
1985
|
-
await refreshAndSaveStatus({
|
|
1986
|
-
paths: options.paths,
|
|
1987
|
-
provider: options.provider,
|
|
1988
|
-
readContext: effectiveReadContext,
|
|
1989
|
-
overrides: {
|
|
1990
|
-
runMode: options.runMode,
|
|
1991
|
-
currentPhase: "scoring",
|
|
1992
|
-
note: "Scoring mining candidates for the current tip.",
|
|
1993
|
-
},
|
|
1994
|
-
visualizer: options.visualizer,
|
|
1995
|
-
});
|
|
1996
|
-
const best = await chooseBestLocalCandidate(candidates);
|
|
1997
|
-
if (best === null) {
|
|
1998
2152
|
await refreshAndSaveStatus({
|
|
1999
2153
|
paths: options.paths,
|
|
2000
2154
|
provider: options.provider,
|
|
2001
2155
|
readContext: effectiveReadContext,
|
|
2002
2156
|
overrides: {
|
|
2003
2157
|
runMode: options.runMode,
|
|
2004
|
-
currentPhase: "
|
|
2005
|
-
|
|
2006
|
-
note: "No publishable mining candidate passed scoring gates for the current tip.",
|
|
2158
|
+
currentPhase: "generating",
|
|
2159
|
+
note: "Generating mining sentences for eligible root domains.",
|
|
2007
2160
|
},
|
|
2008
2161
|
visualizer: options.visualizer,
|
|
2162
|
+
visualizerState: options.loopState.ui,
|
|
2009
2163
|
});
|
|
2010
|
-
await appendEvent(options.paths, createEvent("
|
|
2164
|
+
await appendEvent(options.paths, createEvent("sentence-generation-start", "Started mining sentence generation.", {
|
|
2011
2165
|
targetBlockHeight,
|
|
2012
2166
|
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
2013
2167
|
runId: options.backgroundWorkerRunId,
|
|
2014
2168
|
}));
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2169
|
+
let candidates;
|
|
2170
|
+
try {
|
|
2171
|
+
candidates = await generateCandidatesForDomains({
|
|
2172
|
+
rpc,
|
|
2173
|
+
readContext: effectiveReadContext,
|
|
2174
|
+
domains,
|
|
2175
|
+
provider: options.provider,
|
|
2176
|
+
paths: options.paths,
|
|
2177
|
+
indexerTruthKey,
|
|
2178
|
+
runId: options.backgroundWorkerRunId,
|
|
2179
|
+
fetchImpl: options.fetchImpl,
|
|
2180
|
+
});
|
|
2181
|
+
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
2182
|
+
}
|
|
2183
|
+
catch (error) {
|
|
2184
|
+
if (error instanceof MiningProviderRequestError) {
|
|
2185
|
+
if (tipKey !== null) {
|
|
2186
|
+
options.loopState.attemptedTipKey = tipKey;
|
|
2187
|
+
options.loopState.waitingNote = "Mining is waiting for the sentence provider to recover.";
|
|
2188
|
+
}
|
|
2189
|
+
await refreshAndSaveStatus({
|
|
2190
|
+
paths: options.paths,
|
|
2191
|
+
provider: options.provider,
|
|
2192
|
+
readContext: effectiveReadContext,
|
|
2193
|
+
overrides: {
|
|
2194
|
+
runMode: options.runMode,
|
|
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,
|
|
2202
|
+
});
|
|
2203
|
+
await appendEvent(options.paths, createEvent("publish-paused-provider", error.message, {
|
|
2204
|
+
level: "warn",
|
|
2205
|
+
targetBlockHeight,
|
|
2206
|
+
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
2207
|
+
runId: options.backgroundWorkerRunId,
|
|
2208
|
+
}));
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
if (error instanceof Error && error.message === "mining_generation_stale_tip") {
|
|
2212
|
+
await appendEvent(options.paths, createEvent("generation-restarted-new-tip", "Detected a new best tip during sentence generation; restarting on the next tick.", {
|
|
2213
|
+
level: "warn",
|
|
2214
|
+
targetBlockHeight,
|
|
2215
|
+
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
2216
|
+
runId: options.backgroundWorkerRunId,
|
|
2217
|
+
}));
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
if (error instanceof Error && error.message === "mining_generation_stale_indexer_truth") {
|
|
2221
|
+
clearMiningGateCache(walletRootId);
|
|
2222
|
+
await appendEvent(options.paths, createEvent("generation-restarted-indexer-truth", "Detected updated coherent indexer truth during mining; restarting on the next tick.", {
|
|
2223
|
+
level: "warn",
|
|
2224
|
+
targetBlockHeight,
|
|
2225
|
+
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
2226
|
+
runId: options.backgroundWorkerRunId,
|
|
2227
|
+
}));
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
if (error instanceof Error && error.message === "mining_generation_preempted") {
|
|
2231
|
+
await appendEvent(options.paths, createEvent("generation-paused-preempted", "Stopped sentence generation because another wallet command requested mining preemption.", {
|
|
2232
|
+
level: "warn",
|
|
2233
|
+
targetBlockHeight,
|
|
2234
|
+
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
2235
|
+
runId: options.backgroundWorkerRunId,
|
|
2236
|
+
}));
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
const failureMessage = error instanceof Error ? error.message : String(error);
|
|
2240
|
+
if (tipKey !== null) {
|
|
2241
|
+
options.loopState.attemptedTipKey = tipKey;
|
|
2242
|
+
options.loopState.waitingNote = "Mining sentence generation failed for the current tip.";
|
|
2243
|
+
}
|
|
2244
|
+
await refreshAndSaveStatus({
|
|
2245
|
+
paths: options.paths,
|
|
2246
|
+
provider: options.provider,
|
|
2247
|
+
readContext: effectiveReadContext,
|
|
2248
|
+
overrides: {
|
|
2249
|
+
runMode: options.runMode,
|
|
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,
|
|
2257
|
+
});
|
|
2258
|
+
await appendEvent(options.paths, createEvent("sentence-generation-failed", failureMessage, {
|
|
2259
|
+
level: "error",
|
|
2260
|
+
targetBlockHeight,
|
|
2261
|
+
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
2262
|
+
runId: options.backgroundWorkerRunId,
|
|
2263
|
+
}));
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2037
2266
|
await refreshAndSaveStatus({
|
|
2038
2267
|
paths: options.paths,
|
|
2039
2268
|
provider: options.provider,
|
|
2040
2269
|
readContext: effectiveReadContext,
|
|
2041
2270
|
overrides: {
|
|
2042
2271
|
runMode: options.runMode,
|
|
2043
|
-
currentPhase: "
|
|
2044
|
-
|
|
2045
|
-
sameDomainCompetitorSuppressed: gate.sameDomainCompetitorSuppressed,
|
|
2046
|
-
higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
|
|
2047
|
-
dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
|
|
2048
|
-
competitivenessGateIndeterminate: gate.competitivenessGateIndeterminate,
|
|
2049
|
-
mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
|
|
2050
|
-
lastMempoolSequence: gate.lastMempoolSequence,
|
|
2051
|
-
lastCompetitivenessGateAtUnixMs: Date.now(),
|
|
2052
|
-
note: gate.decision === "suppressed-same-domain-mempool"
|
|
2053
|
-
? "Best local sentence found, but a same-domain mempool competitor already matches or beats it."
|
|
2054
|
-
: gate.decision === "suppressed-top5-mempool"
|
|
2055
|
-
? `Best local sentence found, but ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
|
|
2056
|
-
: "Mining skipped this tick because the mempool competitiveness gate could not be verified safely.",
|
|
2272
|
+
currentPhase: "scoring",
|
|
2273
|
+
note: "Scoring mining candidates for the current tip.",
|
|
2057
2274
|
},
|
|
2058
2275
|
visualizer: options.visualizer,
|
|
2276
|
+
visualizerState: options.loopState.ui,
|
|
2059
2277
|
});
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
:
|
|
2278
|
+
const best = await chooseBestLocalCandidate(candidates);
|
|
2279
|
+
if (best === null) {
|
|
2280
|
+
if (tipKey !== null) {
|
|
2281
|
+
options.loopState.attemptedTipKey = tipKey;
|
|
2282
|
+
options.loopState.waitingNote = "No publishable mining candidate passed scoring gates for the current tip.";
|
|
2283
|
+
}
|
|
2284
|
+
clearSelectedCandidate(options.loopState);
|
|
2285
|
+
await refreshAndSaveStatus({
|
|
2286
|
+
paths: options.paths,
|
|
2287
|
+
provider: options.provider,
|
|
2288
|
+
readContext: effectiveReadContext,
|
|
2289
|
+
overrides: {
|
|
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,
|
|
2297
|
+
});
|
|
2298
|
+
await appendEvent(options.paths, createEvent("publish-skipped-no-candidate", "No publishable mining candidate passed scoring gates.", {
|
|
2299
|
+
targetBlockHeight,
|
|
2300
|
+
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
2301
|
+
runId: options.backgroundWorkerRunId,
|
|
2302
|
+
}));
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
if (!await ensureCurrentIndexerTruthOrRestart()) {
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
options.loopState.ui.recentWin = null;
|
|
2309
|
+
cacheSelectedCandidateForTip(options.loopState, tipKey, best);
|
|
2310
|
+
selectedCandidate = best;
|
|
2311
|
+
await appendEvent(options.paths, createEvent("candidate-selected", `Selected ${best.domainName} with score ${best.canonicalBlend.toString()}.`, {
|
|
2069
2312
|
targetBlockHeight: best.targetBlockHeight,
|
|
2070
2313
|
referencedBlockHashDisplay: best.referencedBlockHashDisplay,
|
|
2071
2314
|
domainId: best.domainId,
|
|
2072
2315
|
domainName: best.domainName,
|
|
2073
2316
|
score: best.canonicalBlend.toString(),
|
|
2074
2317
|
runId: options.backgroundWorkerRunId,
|
|
2075
|
-
reason: gate.decision,
|
|
2076
2318
|
}));
|
|
2077
|
-
|
|
2319
|
+
const gate = await runCompetitivenessGate({
|
|
2320
|
+
rpc,
|
|
2321
|
+
readContext: effectiveReadContext,
|
|
2322
|
+
candidate: best,
|
|
2323
|
+
currentTxid: effectiveReadContext.localState.state.miningState.currentTxid,
|
|
2324
|
+
});
|
|
2325
|
+
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
2326
|
+
gateSnapshot = {
|
|
2327
|
+
higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
|
|
2328
|
+
dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
|
|
2329
|
+
mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
|
|
2330
|
+
lastMempoolSequence: gate.lastMempoolSequence,
|
|
2331
|
+
};
|
|
2332
|
+
if (!gate.allowed) {
|
|
2333
|
+
if (tipKey !== null) {
|
|
2334
|
+
options.loopState.attemptedTipKey = tipKey;
|
|
2335
|
+
}
|
|
2336
|
+
clearSelectedCandidate(options.loopState);
|
|
2337
|
+
setMiningUiCandidate(options.loopState, best);
|
|
2338
|
+
options.loopState.waitingNote = gate.decision === "suppressed-same-domain-mempool"
|
|
2339
|
+
? "Best local sentence found, but a same-domain mempool competitor already matches or beats it."
|
|
2340
|
+
: gate.decision === "suppressed-top5-mempool"
|
|
2341
|
+
? `Best local sentence found, but ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
|
|
2342
|
+
: "Mining skipped this tick because the mempool competitiveness gate could not be verified safely.";
|
|
2343
|
+
await refreshAndSaveStatus({
|
|
2344
|
+
paths: options.paths,
|
|
2345
|
+
provider: options.provider,
|
|
2346
|
+
readContext: effectiveReadContext,
|
|
2347
|
+
overrides: {
|
|
2348
|
+
runMode: options.runMode,
|
|
2349
|
+
currentPhase: "waiting",
|
|
2350
|
+
currentPublishDecision: gate.decision,
|
|
2351
|
+
sameDomainCompetitorSuppressed: gate.sameDomainCompetitorSuppressed,
|
|
2352
|
+
higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
|
|
2353
|
+
dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
|
|
2354
|
+
competitivenessGateIndeterminate: gate.competitivenessGateIndeterminate,
|
|
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,
|
|
2362
|
+
});
|
|
2363
|
+
await appendEvent(options.paths, createEvent(gate.decision === "suppressed-same-domain-mempool"
|
|
2364
|
+
? "publish-skipped-same-domain-mempool"
|
|
2365
|
+
: gate.decision === "suppressed-top5-mempool"
|
|
2366
|
+
? "publish-skipped-top5-mempool"
|
|
2367
|
+
: "publish-skipped-gate-indeterminate", gate.decision === "suppressed-same-domain-mempool"
|
|
2368
|
+
? "Skipped publish because a same-domain mempool competitor already outranks the local candidate."
|
|
2369
|
+
: gate.decision === "suppressed-top5-mempool"
|
|
2370
|
+
? `Skipped publish because ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
|
|
2371
|
+
: "Skipped publish because the competitiveness gate could not be evaluated safely.", {
|
|
2372
|
+
targetBlockHeight: best.targetBlockHeight,
|
|
2373
|
+
referencedBlockHashDisplay: best.referencedBlockHashDisplay,
|
|
2374
|
+
domainId: best.domainId,
|
|
2375
|
+
domainName: best.domainName,
|
|
2376
|
+
score: best.canonicalBlend.toString(),
|
|
2377
|
+
runId: options.backgroundWorkerRunId,
|
|
2378
|
+
reason: gate.decision,
|
|
2379
|
+
}));
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
else {
|
|
2384
|
+
options.loopState.ui.recentWin = null;
|
|
2385
|
+
setMiningUiCandidate(options.loopState, selectedCandidate);
|
|
2078
2386
|
}
|
|
2079
2387
|
if (!await ensureCurrentIndexerTruthOrRestart()) {
|
|
2080
2388
|
return;
|
|
@@ -2093,6 +2401,7 @@ async function performMiningCycle(options) {
|
|
|
2093
2401
|
: "Replacing the live mining transaction for the current tip.",
|
|
2094
2402
|
},
|
|
2095
2403
|
visualizer: options.visualizer,
|
|
2404
|
+
visualizerState: options.loopState.ui,
|
|
2096
2405
|
});
|
|
2097
2406
|
const publishLock = await acquireFileLock(options.paths.walletControlLockPath, {
|
|
2098
2407
|
purpose: "wallet-mine",
|
|
@@ -2105,16 +2414,98 @@ async function performMiningCycle(options) {
|
|
|
2105
2414
|
}
|
|
2106
2415
|
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
2107
2416
|
const published = await publishCandidate({
|
|
2108
|
-
readContext: effectiveReadContext,
|
|
2109
|
-
candidate: best,
|
|
2110
2417
|
dataDir: options.dataDir,
|
|
2418
|
+
databasePath: options.databasePath,
|
|
2111
2419
|
provider: options.provider,
|
|
2112
2420
|
paths: options.paths,
|
|
2421
|
+
fallbackState: effectiveReadContext.localState.state,
|
|
2422
|
+
openReadContext: options.openReadContext,
|
|
2113
2423
|
attachService: options.attachService,
|
|
2114
2424
|
rpcFactory: options.rpcFactory,
|
|
2425
|
+
candidate: selectedCandidate,
|
|
2115
2426
|
runId: options.backgroundWorkerRunId,
|
|
2116
2427
|
});
|
|
2117
2428
|
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
2429
|
+
if (tipKey !== null && published.retryable !== true) {
|
|
2430
|
+
options.loopState.attemptedTipKey = tipKey;
|
|
2431
|
+
}
|
|
2432
|
+
if (published.retryable === true) {
|
|
2433
|
+
cacheSelectedCandidateForTip(options.loopState, tipKey, published.candidate);
|
|
2434
|
+
options.loopState.waitingNote = published.note;
|
|
2435
|
+
await refreshAndSaveStatus({
|
|
2436
|
+
paths: options.paths,
|
|
2437
|
+
provider: options.provider,
|
|
2438
|
+
readContext: {
|
|
2439
|
+
...effectiveReadContext,
|
|
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,
|
|
2458
|
+
},
|
|
2459
|
+
visualizer: options.visualizer,
|
|
2460
|
+
visualizerState: options.loopState.ui,
|
|
2461
|
+
});
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
if (published.skipped === true) {
|
|
2465
|
+
clearSelectedCandidate(options.loopState);
|
|
2466
|
+
setMiningUiCandidate(options.loopState, selectedCandidate);
|
|
2467
|
+
options.loopState.waitingNote = published.note;
|
|
2468
|
+
await refreshAndSaveStatus({
|
|
2469
|
+
paths: options.paths,
|
|
2470
|
+
provider: options.provider,
|
|
2471
|
+
readContext: {
|
|
2472
|
+
...effectiveReadContext,
|
|
2473
|
+
localState: {
|
|
2474
|
+
...effectiveReadContext.localState,
|
|
2475
|
+
state: published.state,
|
|
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,
|
|
2491
|
+
},
|
|
2492
|
+
visualizer: options.visualizer,
|
|
2493
|
+
visualizerState: options.loopState.ui,
|
|
2494
|
+
});
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
clearSelectedCandidate(options.loopState);
|
|
2498
|
+
if (published.txid !== null) {
|
|
2499
|
+
options.loopState.ui.latestTxid = published.txid;
|
|
2500
|
+
}
|
|
2501
|
+
setMiningUiCandidate(options.loopState, published.candidate);
|
|
2502
|
+
options.loopState.waitingNote = published.decision === "kept-live-publish"
|
|
2503
|
+
? "Existing live mining publish already covers this block attempt. Waiting for the next block."
|
|
2504
|
+
: published.txid === null
|
|
2505
|
+
? "Mining candidate was evaluated but the existing live publish stayed in place."
|
|
2506
|
+
: `Mining candidate ${published.decision === "replaced"
|
|
2507
|
+
? "replaced"
|
|
2508
|
+
: "broadcast"} as ${published.txid}. Waiting for the next block.`;
|
|
2118
2509
|
await refreshAndSaveStatus({
|
|
2119
2510
|
paths: options.paths,
|
|
2120
2511
|
provider: options.provider,
|
|
@@ -2127,25 +2518,20 @@ async function performMiningCycle(options) {
|
|
|
2127
2518
|
},
|
|
2128
2519
|
overrides: {
|
|
2129
2520
|
runMode: options.runMode,
|
|
2130
|
-
currentPhase: "
|
|
2521
|
+
currentPhase: "waiting",
|
|
2131
2522
|
currentPublishDecision: published.decision,
|
|
2132
2523
|
sameDomainCompetitorSuppressed: false,
|
|
2133
|
-
higherRankedCompetitorDomainCount:
|
|
2134
|
-
dedupedCompetitorDomainCount:
|
|
2524
|
+
higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
|
|
2525
|
+
dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
|
|
2135
2526
|
competitivenessGateIndeterminate: false,
|
|
2136
|
-
mempoolSequenceCacheStatus:
|
|
2137
|
-
lastMempoolSequence:
|
|
2527
|
+
mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
|
|
2528
|
+
lastMempoolSequence: gateSnapshot.lastMempoolSequence,
|
|
2138
2529
|
lastCompetitivenessGateAtUnixMs: Date.now(),
|
|
2139
|
-
note:
|
|
2140
|
-
|
|
2141
|
-
: `Mining candidate ${published.decision === "replaced"
|
|
2142
|
-
? "replaced"
|
|
2143
|
-
: published.decision === "fee-bump"
|
|
2144
|
-
? "fee-bumped"
|
|
2145
|
-
: "broadcast"} as ${published.txid}.`,
|
|
2146
|
-
liveMiningFamilyInMempool: published.state.miningState.liveMiningFamilyInMempool,
|
|
2530
|
+
note: options.loopState.waitingNote,
|
|
2531
|
+
livePublishInMempool: published.state.miningState.livePublishInMempool,
|
|
2147
2532
|
},
|
|
2148
2533
|
visualizer: options.visualizer,
|
|
2534
|
+
visualizerState: options.loopState.ui,
|
|
2149
2535
|
});
|
|
2150
2536
|
}
|
|
2151
2537
|
finally {
|
|
@@ -2189,7 +2575,7 @@ async function saveStopSnapshot(options) {
|
|
|
2189
2575
|
});
|
|
2190
2576
|
try {
|
|
2191
2577
|
let localState = readContext.localState;
|
|
2192
|
-
if (localState.availability === "ready" && localState.state !== null
|
|
2578
|
+
if (localState.availability === "ready" && localState.state !== null) {
|
|
2193
2579
|
const service = await attachOrStartManagedBitcoindService({
|
|
2194
2580
|
dataDir: options.dataDir,
|
|
2195
2581
|
chain: "main",
|
|
@@ -2198,22 +2584,23 @@ async function saveStopSnapshot(options) {
|
|
|
2198
2584
|
}).catch(() => null);
|
|
2199
2585
|
if (service !== null) {
|
|
2200
2586
|
const rpc = createRpcClient(service.rpc);
|
|
2201
|
-
const reconciledState = await reconcileLiveMiningState({
|
|
2587
|
+
const reconciledState = (await reconcileLiveMiningState({
|
|
2202
2588
|
state: localState.state,
|
|
2203
2589
|
rpc,
|
|
2204
2590
|
nodeBestHash: readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
2205
2591
|
nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
|
|
2206
|
-
|
|
2592
|
+
snapshotState: readContext.snapshot?.state ?? null,
|
|
2593
|
+
})).state;
|
|
2207
2594
|
const stopState = defaultMiningStatePatch(reconciledState, {
|
|
2208
2595
|
runMode: "stopped",
|
|
2209
|
-
state: reconciledState.miningState.
|
|
2596
|
+
state: reconciledState.miningState.livePublishInMempool
|
|
2210
2597
|
? reconciledState.miningState.state === "paused-stale"
|
|
2211
2598
|
? "paused-stale"
|
|
2212
2599
|
: "paused"
|
|
2213
2600
|
: reconciledState.miningState.state === "repair-required"
|
|
2214
2601
|
? "repair-required"
|
|
2215
2602
|
: "idle",
|
|
2216
|
-
pauseReason: reconciledState.miningState.
|
|
2603
|
+
pauseReason: reconciledState.miningState.livePublishInMempool
|
|
2217
2604
|
? reconciledState.miningState.state === "paused-stale"
|
|
2218
2605
|
? "stale-block-context"
|
|
2219
2606
|
: "user-stopped"
|
|
@@ -2224,8 +2611,6 @@ async function saveStopSnapshot(options) {
|
|
|
2224
2611
|
await saveWalletStatePreservingUnlock({
|
|
2225
2612
|
state: stopState,
|
|
2226
2613
|
provider: options.provider,
|
|
2227
|
-
unlockUntilUnixMs: localState.unlockUntilUnixMs,
|
|
2228
|
-
nowUnixMs: Date.now(),
|
|
2229
2614
|
paths: options.paths,
|
|
2230
2615
|
});
|
|
2231
2616
|
localState = {
|
|
@@ -2270,6 +2655,7 @@ async function attemptSaveMempool(rpc, paths, runId) {
|
|
|
2270
2655
|
}
|
|
2271
2656
|
async function runMiningLoop(options) {
|
|
2272
2657
|
const suspendDetector = createMiningSuspendDetector();
|
|
2658
|
+
const loopState = createMiningLoopState();
|
|
2273
2659
|
await appendEvent(options.paths, createEvent("runtime-start", `Started ${options.runMode} mining runtime.`, {
|
|
2274
2660
|
runId: options.backgroundWorkerRunId,
|
|
2275
2661
|
}));
|
|
@@ -2298,6 +2684,7 @@ async function runMiningLoop(options) {
|
|
|
2298
2684
|
await performMiningCycle({
|
|
2299
2685
|
...options,
|
|
2300
2686
|
suspendDetector,
|
|
2687
|
+
loopState,
|
|
2301
2688
|
});
|
|
2302
2689
|
await sleep(Math.min(MINING_LOOP_INTERVAL_MS, MINING_STATUS_HEARTBEAT_INTERVAL_MS), options.signal);
|
|
2303
2690
|
}
|
|
@@ -2487,7 +2874,7 @@ export async function stopBackgroundMining(options) {
|
|
|
2487
2874
|
runMode: "background",
|
|
2488
2875
|
backgroundWorkerPid: snapshot.backgroundWorkerPid,
|
|
2489
2876
|
backgroundWorkerRunId: snapshot.backgroundWorkerRunId,
|
|
2490
|
-
note: snapshot.
|
|
2877
|
+
note: snapshot.livePublishInMempool
|
|
2491
2878
|
? "Background mining stopped. The last mining transaction may still confirm from mempool."
|
|
2492
2879
|
: "Background mining stopped.",
|
|
2493
2880
|
});
|
|
@@ -2569,8 +2956,11 @@ export async function handleDetectedMiningRuntimeResumeForTesting(options) {
|
|
|
2569
2956
|
await handleDetectedMiningRuntimeResume(options);
|
|
2570
2957
|
}
|
|
2571
2958
|
export async function performMiningCycleForTesting(options) {
|
|
2572
|
-
await performMiningCycle(
|
|
2959
|
+
await performMiningCycle({
|
|
2960
|
+
...options,
|
|
2961
|
+
loopState: options.loopState ?? createMiningLoopState(),
|
|
2962
|
+
});
|
|
2573
2963
|
}
|
|
2574
|
-
export function
|
|
2575
|
-
return
|
|
2964
|
+
export function shouldKeepCurrentTipLivePublishForTesting(options) {
|
|
2965
|
+
return livePublishTargetsCandidateTip(options);
|
|
2576
2966
|
}
|