@cogcoin/client 0.5.15 → 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 +6 -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 -12
- package/dist/wallet/coin-control.js +100 -428
- 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 +116 -13
- package/dist/wallet/mining/runner.js +885 -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 -1250
- 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 +61 -8
- package/dist/wallet/tx/common.js +266 -146
- 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 +83 -924
- 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 (getDecodedInputScriptPubKeyHex(decoded, 1) !== 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
|
-
decoded,
|
|
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
|
}
|
|
@@ -668,15 +841,11 @@ function resolveEligibleAnchoredRoots(context) {
|
|
|
668
841
|
if (!isMineableWalletDomain(context, domain)) {
|
|
669
842
|
continue;
|
|
670
843
|
}
|
|
671
|
-
const localRecord = state.domains.find((entry) => entry.name === domain.name);
|
|
672
|
-
const ownerIdentity = model.identities.find((identity) => identity.index === domain.ownerLocalIndex);
|
|
673
844
|
const domainId = domain.domainId;
|
|
674
845
|
if (domainId === null
|
|
675
846
|
|| domainId === undefined
|
|
676
|
-
||
|
|
677
|
-
||
|
|
678
|
-
|| ownerIdentity?.address == null
|
|
679
|
-
|| ownerIdentity.readOnly) {
|
|
847
|
+
|| domain.ownerAddress == null
|
|
848
|
+
|| domain.ownerScriptPubKeyHex !== model.walletScriptPubKeyHex) {
|
|
680
849
|
continue;
|
|
681
850
|
}
|
|
682
851
|
const chainDomain = lookupDomain(snapshot.state, domain.name);
|
|
@@ -686,60 +855,47 @@ function resolveEligibleAnchoredRoots(context) {
|
|
|
686
855
|
domains.push({
|
|
687
856
|
domainId,
|
|
688
857
|
domainName: domain.name,
|
|
689
|
-
localIndex:
|
|
858
|
+
localIndex: 0,
|
|
690
859
|
sender: {
|
|
691
|
-
localIndex:
|
|
692
|
-
scriptPubKeyHex:
|
|
693
|
-
address:
|
|
694
|
-
},
|
|
695
|
-
anchorOutpoint: {
|
|
696
|
-
txid: localRecord.currentCanonicalAnchorOutpoint.txid,
|
|
697
|
-
vout: localRecord.currentCanonicalAnchorOutpoint.vout,
|
|
860
|
+
localIndex: 0,
|
|
861
|
+
scriptPubKeyHex: model.walletScriptPubKeyHex,
|
|
862
|
+
address: domain.ownerAddress,
|
|
698
863
|
},
|
|
699
864
|
});
|
|
700
865
|
}
|
|
701
866
|
return domains.sort((left, right) => left.domainId - right.domainId || left.domainName.localeCompare(right.domainName));
|
|
702
867
|
}
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
if (
|
|
706
|
-
return
|
|
707
|
-
}
|
|
708
|
-
if (options.success) {
|
|
709
|
-
if ((hookState.consecutiveFailureCount ?? 0) === 0 && hookState.cooldownUntilUnixMs === null) {
|
|
710
|
-
return false;
|
|
711
|
-
}
|
|
712
|
-
options.readContext.localState.state.hookClientState.mining = {
|
|
713
|
-
...hookState,
|
|
714
|
-
consecutiveFailureCount: 0,
|
|
715
|
-
cooldownUntilUnixMs: null,
|
|
716
|
-
};
|
|
717
|
-
await saveWalletStatePreservingUnlock({
|
|
718
|
-
state: options.readContext.localState.state,
|
|
719
|
-
provider: options.provider,
|
|
720
|
-
unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
|
|
721
|
-
nowUnixMs: options.nowUnixMs,
|
|
722
|
-
paths: options.paths,
|
|
723
|
-
});
|
|
724
|
-
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;
|
|
725
872
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
:
|
|
730
|
-
|
|
731
|
-
...hookState,
|
|
732
|
-
consecutiveFailureCount,
|
|
733
|
-
cooldownUntilUnixMs,
|
|
873
|
+
return {
|
|
874
|
+
...candidate,
|
|
875
|
+
domainName: refreshed.domainName,
|
|
876
|
+
localIndex: refreshed.localIndex,
|
|
877
|
+
sender: refreshed.sender,
|
|
734
878
|
};
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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.";
|
|
743
899
|
}
|
|
744
900
|
async function generateCandidatesForDomains(options) {
|
|
745
901
|
const bestBlockHash = options.readContext.nodeStatus?.nodeBestHashHex;
|
|
@@ -797,7 +953,7 @@ async function generateCandidatesForDomains(options) {
|
|
|
797
953
|
referencedBlockHashDisplay: bestBlockHash,
|
|
798
954
|
generatedAtUnixMs: Date.now(),
|
|
799
955
|
extraPrompt: null,
|
|
800
|
-
limits:
|
|
956
|
+
limits: createMiningSentenceRequestLimits(),
|
|
801
957
|
rootDomains: rootDomains.map((domain) => ({
|
|
802
958
|
domainId: domain.domainId,
|
|
803
959
|
domainName: domain.domainName,
|
|
@@ -809,7 +965,6 @@ async function generateCandidatesForDomains(options) {
|
|
|
809
965
|
generated = await generateMiningSentences(generationRequest, {
|
|
810
966
|
paths: options.paths,
|
|
811
967
|
provider: options.provider,
|
|
812
|
-
hookState: options.readContext.localState.state.hookClientState.mining,
|
|
813
968
|
signal: abortController.signal,
|
|
814
969
|
fetchImpl: options.fetchImpl,
|
|
815
970
|
});
|
|
@@ -839,15 +994,6 @@ async function generateCandidatesForDomains(options) {
|
|
|
839
994
|
dataDir: options.readContext.dataDir,
|
|
840
995
|
truthKey: options.indexerTruthKey,
|
|
841
996
|
});
|
|
842
|
-
if (generated.hookMode === "custom") {
|
|
843
|
-
await persistCustomHookRuntimeOutcome({
|
|
844
|
-
readContext: options.readContext,
|
|
845
|
-
provider: options.provider,
|
|
846
|
-
paths: options.paths,
|
|
847
|
-
nowUnixMs: Date.now(),
|
|
848
|
-
success: true,
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
997
|
const sentencesByDomain = new Map();
|
|
852
998
|
for (const candidate of generated.candidates) {
|
|
853
999
|
const existing = sentencesByDomain.get(candidate.domainId) ?? [];
|
|
@@ -870,7 +1016,6 @@ async function generateCandidatesForDomains(options) {
|
|
|
870
1016
|
domainName: domain.domainName,
|
|
871
1017
|
localIndex: domain.localIndex,
|
|
872
1018
|
sender: domain.sender,
|
|
873
|
-
anchorOutpoint: domain.anchorOutpoint,
|
|
874
1019
|
sentence: best.sentence,
|
|
875
1020
|
encodedSentenceBytes: best.encodedSentenceBytes,
|
|
876
1021
|
bip39WordIndices: [...best.bip39WordIndices],
|
|
@@ -920,6 +1065,46 @@ async function chooseBestLocalCandidate(candidates) {
|
|
|
920
1065
|
}
|
|
921
1066
|
return candidates.find((candidate) => candidate.domainId === winner.miningDomainId) ?? null;
|
|
922
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
|
+
}
|
|
923
1108
|
async function runCompetitivenessGate(options) {
|
|
924
1109
|
const createDecision = (overrides) => ({
|
|
925
1110
|
allowed: overrides.allowed ?? false,
|
|
@@ -930,10 +1115,11 @@ async function runCompetitivenessGate(options) {
|
|
|
930
1115
|
competitivenessGateIndeterminate: overrides.competitivenessGateIndeterminate ?? false,
|
|
931
1116
|
mempoolSequenceCacheStatus: overrides.mempoolSequenceCacheStatus ?? null,
|
|
932
1117
|
lastMempoolSequence: overrides.lastMempoolSequence ?? null,
|
|
1118
|
+
visibleBoardEntries: overrides.visibleBoardEntries ?? [],
|
|
1119
|
+
candidateRank: overrides.candidateRank ?? null,
|
|
933
1120
|
});
|
|
934
1121
|
const walletRootId = options.readContext.localState.walletRootId ?? "uninitialized-wallet-root";
|
|
935
1122
|
const indexerTruthKey = getIndexerTruthKey(options.readContext);
|
|
936
|
-
const localFeeTarget = DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB;
|
|
937
1123
|
const excludedTxids = [options.currentTxid].filter((value) => value !== null).sort();
|
|
938
1124
|
const localAssayTupleKey = [
|
|
939
1125
|
options.candidate.domainId,
|
|
@@ -965,7 +1151,6 @@ async function runCompetitivenessGate(options) {
|
|
|
965
1151
|
&& cachedTruthMatches
|
|
966
1152
|
&& cachedReferencedBlockMatches
|
|
967
1153
|
&& cached.localAssayTupleKey === localAssayTupleKey
|
|
968
|
-
&& cached.currentFeeTargetSatVb === localFeeTarget
|
|
969
1154
|
&& cached.excludedTxidsKey === excludedTxids.join(",")
|
|
970
1155
|
&& cached.mempoolSequence === mempoolSequence) {
|
|
971
1156
|
return {
|
|
@@ -1011,7 +1196,7 @@ async function runCompetitivenessGate(options) {
|
|
|
1011
1196
|
const entries = new Map();
|
|
1012
1197
|
for (const txid of visibleTxids) {
|
|
1013
1198
|
const context = txContexts.get(txid);
|
|
1014
|
-
if (context === undefined || context.
|
|
1199
|
+
if (context === undefined || context.payload === null || context.senderScriptHex === null) {
|
|
1015
1200
|
continue;
|
|
1016
1201
|
}
|
|
1017
1202
|
const decoded = decodeMinePayload(context.payload);
|
|
@@ -1037,7 +1222,6 @@ async function runCompetitivenessGate(options) {
|
|
|
1037
1222
|
indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
|
|
1038
1223
|
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
1039
1224
|
localAssayTupleKey,
|
|
1040
|
-
currentFeeTargetSatVb: localFeeTarget,
|
|
1041
1225
|
excludedTxidsKey: excludedTxids.join(","),
|
|
1042
1226
|
mempoolSequence,
|
|
1043
1227
|
txids: [...visibleTxids],
|
|
@@ -1058,26 +1242,46 @@ async function runCompetitivenessGate(options) {
|
|
|
1058
1242
|
txid,
|
|
1059
1243
|
effectiveFeeRate: context.effectiveFeeRate,
|
|
1060
1244
|
domainId: decoded.domainId,
|
|
1245
|
+
domainName: overlayDomain.name,
|
|
1246
|
+
sentence: Buffer.from(decoded.sentenceBytes).toString("utf8"),
|
|
1061
1247
|
senderScriptHex: context.senderScriptHex,
|
|
1062
1248
|
encodedSentenceBytesHex: Buffer.from(scored.encodedSentenceBytes).toString("hex"),
|
|
1063
1249
|
bip39WordIndices: [...scored.bip39WordIndices],
|
|
1064
1250
|
canonicalBlend: scored.canonicalBlend,
|
|
1065
1251
|
});
|
|
1066
1252
|
}
|
|
1067
|
-
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);
|
|
1068
1275
|
const sameDomainCompetitorSuppressed = sameDomainCompetitors.some((competitor) => competitor.canonicalBlend > options.candidate.canonicalBlend
|
|
1069
1276
|
|| competitor.canonicalBlend === options.candidate.canonicalBlend);
|
|
1070
1277
|
let decision;
|
|
1071
1278
|
const otherDomainBest = new Map();
|
|
1072
|
-
for (const entry of
|
|
1279
|
+
for (const entry of visibleBestByDomain.values()) {
|
|
1073
1280
|
if (entry.domainId === options.candidate.domainId) {
|
|
1074
1281
|
continue;
|
|
1075
1282
|
}
|
|
1076
1283
|
const best = otherDomainBest.get(entry.domainId);
|
|
1077
|
-
if (best
|
|
1078
|
-
|| entry.canonicalBlend > best.canonicalBlend
|
|
1079
|
-
|| (entry.canonicalBlend === best.canonicalBlend && entry.effectiveFeeRate > best.effectiveFeeRate)
|
|
1080
|
-
|| (entry.canonicalBlend === best.canonicalBlend && entry.effectiveFeeRate === best.effectiveFeeRate && entry.txid.localeCompare(best.txid) < 0)) {
|
|
1284
|
+
if (isBetterVisibleCompetitor(entry, best)) {
|
|
1081
1285
|
otherDomainBest.set(entry.domainId, entry);
|
|
1082
1286
|
}
|
|
1083
1287
|
}
|
|
@@ -1091,38 +1295,41 @@ async function runCompetitivenessGate(options) {
|
|
|
1091
1295
|
competitivenessGateIndeterminate: false,
|
|
1092
1296
|
mempoolSequenceCacheStatus: "refreshed",
|
|
1093
1297
|
lastMempoolSequence: mempoolSequence,
|
|
1298
|
+
visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
|
|
1094
1299
|
});
|
|
1095
1300
|
}
|
|
1096
1301
|
else {
|
|
1097
1302
|
try {
|
|
1098
|
-
const
|
|
1303
|
+
const candidateRankedEntries = rankMiningSentenceEntries([
|
|
1099
1304
|
{
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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"),
|
|
1103
1311
|
bip39WordIndices: options.candidate.bip39WordIndices,
|
|
1312
|
+
txid: null,
|
|
1104
1313
|
txIndex: 0,
|
|
1105
1314
|
},
|
|
1106
1315
|
...[...otherDomainBest.values()]
|
|
1107
1316
|
.sort((left, right) => left.domainId - right.domainId || left.txid.localeCompare(right.txid))
|
|
1108
1317
|
.map((entry, index) => ({
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1318
|
+
domainId: entry.domainId,
|
|
1319
|
+
domainName: entry.domainName,
|
|
1320
|
+
sentence: entry.sentence,
|
|
1321
|
+
canonicalBlend: entry.canonicalBlend,
|
|
1322
|
+
senderScriptHex: entry.senderScriptHex,
|
|
1323
|
+
encodedSentenceBytesHex: entry.encodedSentenceBytesHex,
|
|
1112
1324
|
bip39WordIndices: entry.bip39WordIndices,
|
|
1325
|
+
txid: entry.txid,
|
|
1113
1326
|
txIndex: index + 1,
|
|
1114
1327
|
})),
|
|
1115
|
-
];
|
|
1116
|
-
const
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
});
|
|
1121
|
-
const localWinner = winners.find((winner) => winner.miningDomainId === options.candidate.domainId);
|
|
1122
|
-
const higherRankedCompetitorDomainCount = localWinner === undefined
|
|
1123
|
-
? Math.max(0, winners.length - 1)
|
|
1124
|
-
: Math.max(0, localWinner.rank - 1);
|
|
1125
|
-
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) {
|
|
1126
1333
|
decision = createDecision({
|
|
1127
1334
|
allowed: false,
|
|
1128
1335
|
decision: "suppressed-top5-mempool",
|
|
@@ -1132,11 +1339,13 @@ async function runCompetitivenessGate(options) {
|
|
|
1132
1339
|
competitivenessGateIndeterminate: false,
|
|
1133
1340
|
mempoolSequenceCacheStatus: "refreshed",
|
|
1134
1341
|
lastMempoolSequence: mempoolSequence,
|
|
1342
|
+
visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
|
|
1343
|
+
candidateRank,
|
|
1135
1344
|
});
|
|
1136
1345
|
}
|
|
1137
1346
|
else {
|
|
1138
1347
|
decision = createDecision({
|
|
1139
|
-
allowed:
|
|
1348
|
+
allowed: candidateRank !== null,
|
|
1140
1349
|
decision: "publish",
|
|
1141
1350
|
sameDomainCompetitorSuppressed: false,
|
|
1142
1351
|
higherRankedCompetitorDomainCount,
|
|
@@ -1144,6 +1353,8 @@ async function runCompetitivenessGate(options) {
|
|
|
1144
1353
|
competitivenessGateIndeterminate: false,
|
|
1145
1354
|
mempoolSequenceCacheStatus: "refreshed",
|
|
1146
1355
|
lastMempoolSequence: mempoolSequence,
|
|
1356
|
+
visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
|
|
1357
|
+
candidateRank,
|
|
1147
1358
|
});
|
|
1148
1359
|
}
|
|
1149
1360
|
}
|
|
@@ -1157,6 +1368,7 @@ async function runCompetitivenessGate(options) {
|
|
|
1157
1368
|
competitivenessGateIndeterminate: true,
|
|
1158
1369
|
mempoolSequenceCacheStatus: "refreshed",
|
|
1159
1370
|
lastMempoolSequence: mempoolSequence,
|
|
1371
|
+
visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
|
|
1160
1372
|
});
|
|
1161
1373
|
}
|
|
1162
1374
|
}
|
|
@@ -1165,7 +1377,6 @@ async function runCompetitivenessGate(options) {
|
|
|
1165
1377
|
indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
|
|
1166
1378
|
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
1167
1379
|
localAssayTupleKey,
|
|
1168
|
-
currentFeeTargetSatVb: localFeeTarget,
|
|
1169
1380
|
excludedTxidsKey: excludedTxids.join(","),
|
|
1170
1381
|
mempoolSequence,
|
|
1171
1382
|
txids: [...visibleTxids],
|
|
@@ -1174,74 +1385,14 @@ async function runCompetitivenessGate(options) {
|
|
|
1174
1385
|
});
|
|
1175
1386
|
return decision;
|
|
1176
1387
|
}
|
|
1177
|
-
function
|
|
1388
|
+
function livePublishTargetsCandidateTip(options) {
|
|
1178
1389
|
const liveState = normalizeMiningStateRecord(options.liveState);
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
}
|
|
1183
|
-
if (liveState.currentDomainId === options.candidate.domainId) {
|
|
1184
|
-
if (liveState.currentEncodedSentenceBytesHex === nextSentenceHex) {
|
|
1185
|
-
return false;
|
|
1186
|
-
}
|
|
1187
|
-
const currentScore = liveState.currentScore === null ? null : BigInt(liveState.currentScore);
|
|
1188
|
-
return currentScore === null || options.candidate.canonicalBlend > currentScore;
|
|
1189
|
-
}
|
|
1190
|
-
return true;
|
|
1191
|
-
}
|
|
1192
|
-
function candidateMatchesLiveFamily(options) {
|
|
1193
|
-
const liveState = normalizeMiningStateRecord(options.liveState);
|
|
1194
|
-
return liveState.currentDomainId === options.candidate.domainId
|
|
1195
|
-
&& liveState.currentEncodedSentenceBytesHex === Buffer.from(options.candidate.encodedSentenceBytes).toString("hex")
|
|
1196
|
-
&& liveState.currentSenderScriptPubKeyHex === options.candidate.sender.scriptPubKeyHex
|
|
1390
|
+
return liveState.currentTxid !== null
|
|
1391
|
+
&& liveState.currentPublishState === "in-mempool"
|
|
1392
|
+
&& liveState.livePublishInMempool === true
|
|
1197
1393
|
&& liveState.currentReferencedBlockHashDisplay === options.candidate.referencedBlockHashDisplay
|
|
1198
1394
|
&& liveState.currentBlockTargetHeight === options.candidate.targetBlockHeight;
|
|
1199
1395
|
}
|
|
1200
|
-
function candidateNeedsFeeMaintenance(options) {
|
|
1201
|
-
const liveState = normalizeMiningStateRecord(options.liveState);
|
|
1202
|
-
return candidateMatchesLiveFamily(options)
|
|
1203
|
-
&& liveState.currentTxid !== null
|
|
1204
|
-
&& liveState.currentFeeRateSatVb !== null
|
|
1205
|
-
&& liveState.currentPublishState === "in-mempool"
|
|
1206
|
-
&& liveState.liveMiningFamilyInMempool === true;
|
|
1207
|
-
}
|
|
1208
|
-
async function candidateWinsAgainstLive(options) {
|
|
1209
|
-
const liveState = normalizeMiningStateRecord(options.liveState);
|
|
1210
|
-
if (liveState.currentDomainId === null || liveState.currentEncodedSentenceBytesHex === null) {
|
|
1211
|
-
return true;
|
|
1212
|
-
}
|
|
1213
|
-
if (liveState.currentDomainId === options.candidate.domainId) {
|
|
1214
|
-
return candidateOutranksLive(options);
|
|
1215
|
-
}
|
|
1216
|
-
if (liveState.currentBip39WordIndices === null || liveState.currentSenderScriptPubKeyHex === null || liveState.currentBlendSeedHex === null) {
|
|
1217
|
-
return true;
|
|
1218
|
-
}
|
|
1219
|
-
const settled = await settleBlock({
|
|
1220
|
-
blendSeed: Buffer.from(liveState.currentBlendSeedHex, "hex"),
|
|
1221
|
-
blockRewardCogtoshi: 100n,
|
|
1222
|
-
submissions: [
|
|
1223
|
-
{
|
|
1224
|
-
miningDomainId: liveState.currentDomainId,
|
|
1225
|
-
rawSentenceBytes: Buffer.from(liveState.currentEncodedSentenceBytesHex, "hex"),
|
|
1226
|
-
recipientScriptPubKey: Buffer.from(liveState.currentSenderScriptPubKeyHex, "hex"),
|
|
1227
|
-
bip39WordIndices: liveState.currentBip39WordIndices,
|
|
1228
|
-
txIndex: 0,
|
|
1229
|
-
},
|
|
1230
|
-
{
|
|
1231
|
-
miningDomainId: options.candidate.domainId,
|
|
1232
|
-
rawSentenceBytes: options.candidate.encodedSentenceBytes,
|
|
1233
|
-
recipientScriptPubKey: Buffer.from(options.candidate.sender.scriptPubKeyHex, "hex"),
|
|
1234
|
-
bip39WordIndices: options.candidate.bip39WordIndices,
|
|
1235
|
-
txIndex: 1,
|
|
1236
|
-
},
|
|
1237
|
-
],
|
|
1238
|
-
});
|
|
1239
|
-
const incumbent = settled.find((entry) => entry.miningDomainId === liveState.currentDomainId);
|
|
1240
|
-
const challenger = settled.find((entry) => entry.miningDomainId === options.candidate.domainId);
|
|
1241
|
-
return challenger !== undefined
|
|
1242
|
-
&& incumbent !== undefined
|
|
1243
|
-
&& challenger.rank < incumbent.rank;
|
|
1244
|
-
}
|
|
1245
1396
|
function miningCandidateIsCurrent(options) {
|
|
1246
1397
|
return options.state.currentReferencedBlockHashDisplay !== null
|
|
1247
1398
|
&& options.nodeBestHash !== null
|
|
@@ -1256,14 +1407,17 @@ async function reconcileLiveMiningState(options) {
|
|
|
1256
1407
|
miningState: normalizeMiningStateRecord(options.state.miningState),
|
|
1257
1408
|
};
|
|
1258
1409
|
const currentTxid = state.miningState.currentTxid;
|
|
1259
|
-
if (currentTxid === null || !
|
|
1410
|
+
if (currentTxid === null || !miningPublishMayStillExist(state.miningState)) {
|
|
1260
1411
|
await reconcilePersistentPolicyLocks({
|
|
1261
1412
|
rpc: options.rpc,
|
|
1262
1413
|
walletName: state.managedCoreWallet.walletName,
|
|
1263
1414
|
state,
|
|
1264
1415
|
fixedInputs: [],
|
|
1265
1416
|
});
|
|
1266
|
-
return
|
|
1417
|
+
return {
|
|
1418
|
+
state,
|
|
1419
|
+
recentWin: null,
|
|
1420
|
+
};
|
|
1267
1421
|
}
|
|
1268
1422
|
const walletName = state.managedCoreWallet.walletName;
|
|
1269
1423
|
const [mempoolVerbose, walletTx] = await Promise.all([
|
|
@@ -1275,10 +1429,11 @@ async function reconcileLiveMiningState(options) {
|
|
|
1275
1429
|
]);
|
|
1276
1430
|
const inMempool = mempoolVerbose.txids.includes(currentTxid);
|
|
1277
1431
|
if (walletTx !== null && walletTx.confirmations > 0) {
|
|
1432
|
+
const recentWin = findRecentMiningWin(options.snapshotState ?? null, currentTxid, state.miningState.currentBlockTargetHeight);
|
|
1278
1433
|
state = {
|
|
1279
1434
|
...state,
|
|
1280
1435
|
miningState: {
|
|
1281
|
-
...
|
|
1436
|
+
...clearMiningPublishState(state.miningState),
|
|
1282
1437
|
currentPublishDecision: "tx-confirmed-while-down",
|
|
1283
1438
|
},
|
|
1284
1439
|
};
|
|
@@ -1288,7 +1443,10 @@ async function reconcileLiveMiningState(options) {
|
|
|
1288
1443
|
state,
|
|
1289
1444
|
fixedInputs: [],
|
|
1290
1445
|
});
|
|
1291
|
-
return
|
|
1446
|
+
return {
|
|
1447
|
+
state,
|
|
1448
|
+
recentWin,
|
|
1449
|
+
};
|
|
1292
1450
|
}
|
|
1293
1451
|
if (inMempool) {
|
|
1294
1452
|
const stale = !miningCandidateIsCurrent({
|
|
@@ -1297,7 +1455,7 @@ async function reconcileLiveMiningState(options) {
|
|
|
1297
1455
|
nodeBestHeight: options.nodeBestHeight,
|
|
1298
1456
|
});
|
|
1299
1457
|
state = defaultMiningStatePatch(state, {
|
|
1300
|
-
|
|
1458
|
+
livePublishInMempool: true,
|
|
1301
1459
|
currentPublishState: "in-mempool",
|
|
1302
1460
|
state: stale
|
|
1303
1461
|
? "paused-stale"
|
|
@@ -1309,7 +1467,7 @@ async function reconcileLiveMiningState(options) {
|
|
|
1309
1467
|
: state.miningState.runMode === "stopped"
|
|
1310
1468
|
? "user-stopped"
|
|
1311
1469
|
: null,
|
|
1312
|
-
currentPublishDecision: stale ? "paused-stale-mempool" : "restored-live-
|
|
1470
|
+
currentPublishDecision: stale ? "paused-stale-mempool" : "restored-live-publish",
|
|
1313
1471
|
});
|
|
1314
1472
|
await reconcilePersistentPolicyLocks({
|
|
1315
1473
|
rpc: options.rpc,
|
|
@@ -1317,7 +1475,10 @@ async function reconcileLiveMiningState(options) {
|
|
|
1317
1475
|
state,
|
|
1318
1476
|
fixedInputs: [],
|
|
1319
1477
|
});
|
|
1320
|
-
return
|
|
1478
|
+
return {
|
|
1479
|
+
state,
|
|
1480
|
+
recentWin: null,
|
|
1481
|
+
};
|
|
1321
1482
|
}
|
|
1322
1483
|
if ((walletTx?.walletconflicts?.length ?? 0) > 0) {
|
|
1323
1484
|
state = defaultMiningStatePatch(state, {
|
|
@@ -1325,7 +1486,7 @@ async function reconcileLiveMiningState(options) {
|
|
|
1325
1486
|
pauseReason: state.miningState.currentPublishState === "broadcast-unknown"
|
|
1326
1487
|
? "broadcast-unknown-conflict"
|
|
1327
1488
|
: "wallet-conflict-observed",
|
|
1328
|
-
|
|
1489
|
+
livePublishInMempool: false,
|
|
1329
1490
|
currentPublishDecision: state.miningState.currentPublishState === "broadcast-unknown"
|
|
1330
1491
|
? "repair-required-broadcast-conflict"
|
|
1331
1492
|
: "repair-required-wallet-conflict",
|
|
@@ -1336,13 +1497,16 @@ async function reconcileLiveMiningState(options) {
|
|
|
1336
1497
|
state,
|
|
1337
1498
|
fixedInputs: [],
|
|
1338
1499
|
});
|
|
1339
|
-
return
|
|
1500
|
+
return {
|
|
1501
|
+
state,
|
|
1502
|
+
recentWin: null,
|
|
1503
|
+
};
|
|
1340
1504
|
}
|
|
1341
1505
|
state = defaultMiningStatePatch(state, {
|
|
1342
|
-
...
|
|
1506
|
+
...clearMiningPublishState(state.miningState),
|
|
1343
1507
|
currentPublishDecision: state.miningState.currentPublishState === "broadcast-unknown"
|
|
1344
1508
|
? "broadcast-unknown-not-seen"
|
|
1345
|
-
: "live-
|
|
1509
|
+
: "live-publish-not-seen",
|
|
1346
1510
|
});
|
|
1347
1511
|
await reconcilePersistentPolicyLocks({
|
|
1348
1512
|
rpc: options.rpc,
|
|
@@ -1350,9 +1514,12 @@ async function reconcileLiveMiningState(options) {
|
|
|
1350
1514
|
state,
|
|
1351
1515
|
fixedInputs: [],
|
|
1352
1516
|
});
|
|
1353
|
-
return
|
|
1517
|
+
return {
|
|
1518
|
+
state,
|
|
1519
|
+
recentWin: null,
|
|
1520
|
+
};
|
|
1354
1521
|
}
|
|
1355
|
-
async function
|
|
1522
|
+
async function publishCandidateOnce(options) {
|
|
1356
1523
|
const service = await options.attachService({
|
|
1357
1524
|
dataDir: options.dataDir,
|
|
1358
1525
|
chain: "main",
|
|
@@ -1360,48 +1527,35 @@ async function publishCandidate(options) {
|
|
|
1360
1527
|
walletRootId: options.readContext.localState.state.walletRootId,
|
|
1361
1528
|
});
|
|
1362
1529
|
const rpc = options.rpcFactory(service.rpc);
|
|
1363
|
-
let state = await reconcileLiveMiningState({
|
|
1530
|
+
let state = (await reconcileLiveMiningState({
|
|
1364
1531
|
state: options.readContext.localState.state,
|
|
1365
1532
|
rpc,
|
|
1366
1533
|
nodeBestHash: options.readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1367
1534
|
nodeBestHeight: options.readContext.nodeStatus?.nodeBestHeight ?? null,
|
|
1368
|
-
|
|
1535
|
+
snapshotState: options.readContext.snapshot.state,
|
|
1536
|
+
})).state;
|
|
1369
1537
|
const allUtxos = await rpc.listUnspent(state.managedCoreWallet.walletName, 0);
|
|
1370
|
-
const
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
&& entry.safe !== false
|
|
1375
|
-
&& !(entry.txid === options.candidate.anchorOutpoint.txid && entry.vout === options.candidate.anchorOutpoint.vout));
|
|
1376
|
-
if (fundingConflict === undefined || fundingConflict === null) {
|
|
1377
|
-
throw new Error("wallet_mining_missing_conflict_utxo");
|
|
1378
|
-
}
|
|
1379
|
-
const conflictOutpoint = "txid" in fundingConflict
|
|
1380
|
-
? { txid: fundingConflict.txid, vout: fundingConflict.vout }
|
|
1381
|
-
: fundingConflict;
|
|
1538
|
+
const conflictOutpoint = resolveMiningConflictOutpoint({
|
|
1539
|
+
state,
|
|
1540
|
+
allUtxos,
|
|
1541
|
+
});
|
|
1382
1542
|
const priorMiningState = cloneMiningState(state.miningState);
|
|
1383
|
-
|
|
1384
|
-
? DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB
|
|
1385
|
-
: state.miningState.currentFeeRateSatVb + 1;
|
|
1386
|
-
const shouldFeeBump = candidateNeedsFeeMaintenance({
|
|
1543
|
+
if (livePublishTargetsCandidateTip({
|
|
1387
1544
|
liveState: state.miningState,
|
|
1388
1545
|
candidate: options.candidate,
|
|
1389
|
-
})
|
|
1390
|
-
if (state.miningState.currentPublishState === "in-mempool"
|
|
1391
|
-
&& state.miningState.liveMiningFamilyInMempool === true
|
|
1392
|
-
&& !shouldFeeBump
|
|
1393
|
-
&& !await candidateWinsAgainstLive({
|
|
1394
|
-
liveState: state.miningState,
|
|
1395
|
-
candidate: options.candidate,
|
|
1396
|
-
})) {
|
|
1546
|
+
})) {
|
|
1397
1547
|
return {
|
|
1398
1548
|
state: defaultMiningStatePatch(state, {
|
|
1399
|
-
currentPublishDecision: "kept-live-
|
|
1549
|
+
currentPublishDecision: "kept-live-publish",
|
|
1400
1550
|
}),
|
|
1401
1551
|
txid: state.miningState.currentTxid,
|
|
1402
|
-
decision: "kept-live-
|
|
1552
|
+
decision: "kept-live-publish",
|
|
1403
1553
|
};
|
|
1404
1554
|
}
|
|
1555
|
+
const feeSelection = await resolveWalletMutationFeeSelection({
|
|
1556
|
+
rpc,
|
|
1557
|
+
});
|
|
1558
|
+
const nextFeeRate = feeSelection.feeRateSatVb;
|
|
1405
1559
|
const plan = createMiningPlan({
|
|
1406
1560
|
state,
|
|
1407
1561
|
candidate: options.candidate,
|
|
@@ -1436,18 +1590,14 @@ async function publishCandidate(options) {
|
|
|
1436
1590
|
currentReferencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
1437
1591
|
currentIntentFingerprintHex: intentFingerprintHex,
|
|
1438
1592
|
sharedMiningConflictOutpoint: conflictOutpoint,
|
|
1439
|
-
|
|
1593
|
+
livePublishInMempool: null,
|
|
1440
1594
|
currentPublishDecision: priorMiningState.currentTxid === null
|
|
1441
1595
|
? "publishing"
|
|
1442
|
-
:
|
|
1443
|
-
? "fee-bump"
|
|
1444
|
-
: "replacing",
|
|
1596
|
+
: "replacing",
|
|
1445
1597
|
});
|
|
1446
1598
|
await saveWalletStatePreservingUnlock({
|
|
1447
1599
|
state,
|
|
1448
1600
|
provider: options.provider,
|
|
1449
|
-
unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
|
|
1450
|
-
nowUnixMs: Date.now(),
|
|
1451
1601
|
paths: options.paths,
|
|
1452
1602
|
});
|
|
1453
1603
|
try {
|
|
@@ -1457,13 +1607,11 @@ async function publishCandidate(options) {
|
|
|
1457
1607
|
if (isAlreadyAcceptedError(error)) {
|
|
1458
1608
|
state = defaultMiningStatePatch(state, {
|
|
1459
1609
|
currentPublishState: "in-mempool",
|
|
1460
|
-
|
|
1610
|
+
livePublishInMempool: true,
|
|
1461
1611
|
});
|
|
1462
1612
|
await saveWalletStatePreservingUnlock({
|
|
1463
1613
|
state,
|
|
1464
1614
|
provider: options.provider,
|
|
1465
|
-
unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
|
|
1466
|
-
nowUnixMs: Date.now(),
|
|
1467
1615
|
paths: options.paths,
|
|
1468
1616
|
});
|
|
1469
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.`, {
|
|
@@ -1480,11 +1628,9 @@ async function publishCandidate(options) {
|
|
|
1480
1628
|
return {
|
|
1481
1629
|
state,
|
|
1482
1630
|
txid: built.txid,
|
|
1483
|
-
decision: state.miningState.currentPublishDecision === "
|
|
1484
|
-
? "
|
|
1485
|
-
:
|
|
1486
|
-
? "replaced"
|
|
1487
|
-
: "broadcast",
|
|
1631
|
+
decision: state.miningState.currentPublishDecision === "replacing"
|
|
1632
|
+
? "replaced"
|
|
1633
|
+
: "broadcast",
|
|
1488
1634
|
};
|
|
1489
1635
|
}
|
|
1490
1636
|
if (isBroadcastUnknownError(error)) {
|
|
@@ -1495,8 +1641,6 @@ async function publishCandidate(options) {
|
|
|
1495
1641
|
await saveWalletStatePreservingUnlock({
|
|
1496
1642
|
state,
|
|
1497
1643
|
provider: options.provider,
|
|
1498
|
-
unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
|
|
1499
|
-
nowUnixMs: Date.now(),
|
|
1500
1644
|
paths: options.paths,
|
|
1501
1645
|
});
|
|
1502
1646
|
await appendEvent(options.paths, createEvent("error", `Mining broadcast became uncertain for ${built.txid}.`, {
|
|
@@ -1518,7 +1662,16 @@ async function publishCandidate(options) {
|
|
|
1518
1662
|
decision: "broadcast-unknown",
|
|
1519
1663
|
};
|
|
1520
1664
|
}
|
|
1521
|
-
|
|
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);
|
|
1522
1675
|
}
|
|
1523
1676
|
const absoluteFeeSats = numberToSats(built.funded.fee);
|
|
1524
1677
|
const replacementCount = priorMiningState.currentTxid === null
|
|
@@ -1526,12 +1679,10 @@ async function publishCandidate(options) {
|
|
|
1526
1679
|
: priorMiningState.replacementCount + 1;
|
|
1527
1680
|
state = defaultMiningStatePatch(state, {
|
|
1528
1681
|
currentPublishState: "in-mempool",
|
|
1529
|
-
|
|
1530
|
-
currentPublishDecision: state.miningState.currentPublishDecision === "
|
|
1531
|
-
? "
|
|
1532
|
-
:
|
|
1533
|
-
? "replaced"
|
|
1534
|
-
: "broadcast",
|
|
1682
|
+
livePublishInMempool: true,
|
|
1683
|
+
currentPublishDecision: state.miningState.currentPublishDecision === "replacing"
|
|
1684
|
+
? "replaced"
|
|
1685
|
+
: "broadcast",
|
|
1535
1686
|
replacementCount,
|
|
1536
1687
|
currentAbsoluteFeeSats: Number(absoluteFeeSats),
|
|
1537
1688
|
currentBlockFeeSpentSats: (BigInt(state.miningState.currentBlockFeeSpentSats) + absoluteFeeSats).toString(),
|
|
@@ -1541,19 +1692,13 @@ async function publishCandidate(options) {
|
|
|
1541
1692
|
await saveWalletStatePreservingUnlock({
|
|
1542
1693
|
state,
|
|
1543
1694
|
provider: options.provider,
|
|
1544
|
-
unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
|
|
1545
|
-
nowUnixMs: Date.now(),
|
|
1546
1695
|
paths: options.paths,
|
|
1547
1696
|
});
|
|
1548
1697
|
await appendEvent(options.paths, createEvent(state.miningState.currentPublishDecision === "replaced"
|
|
1549
1698
|
? "tx-replaced"
|
|
1550
|
-
: state.miningState.currentPublishDecision === "
|
|
1551
|
-
? "tx-fee-bump"
|
|
1552
|
-
: "tx-broadcast", `${state.miningState.currentPublishDecision === "replaced"
|
|
1699
|
+
: "tx-broadcast", `${state.miningState.currentPublishDecision === "replaced"
|
|
1553
1700
|
? "Replaced"
|
|
1554
|
-
:
|
|
1555
|
-
? "Fee-bumped"
|
|
1556
|
-
: "Broadcast"} mining transaction ${built.txid}.`, {
|
|
1701
|
+
: "Broadcast"} mining transaction ${built.txid}.`, {
|
|
1557
1702
|
runId: options.runId,
|
|
1558
1703
|
targetBlockHeight: options.candidate.targetBlockHeight,
|
|
1559
1704
|
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
@@ -1567,21 +1712,106 @@ async function publishCandidate(options) {
|
|
|
1567
1712
|
return {
|
|
1568
1713
|
state,
|
|
1569
1714
|
txid: built.txid,
|
|
1570
|
-
decision: state.miningState.currentPublishDecision === "
|
|
1571
|
-
? "
|
|
1572
|
-
:
|
|
1573
|
-
? "replaced"
|
|
1574
|
-
: "broadcast",
|
|
1715
|
+
decision: state.miningState.currentPublishDecision === "replaced"
|
|
1716
|
+
? "replaced"
|
|
1717
|
+
: "broadcast",
|
|
1575
1718
|
};
|
|
1576
1719
|
}
|
|
1577
|
-
async function
|
|
1578
|
-
const
|
|
1579
|
-
|
|
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,
|
|
1580
1749
|
paths: options.paths,
|
|
1581
1750
|
});
|
|
1582
|
-
|
|
1583
|
-
|
|
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
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
finally {
|
|
1808
|
+
await lockedReadContext.close();
|
|
1584
1809
|
}
|
|
1810
|
+
}
|
|
1811
|
+
export async function publishCandidateForTesting(options) {
|
|
1812
|
+
return await publishCandidate(options);
|
|
1813
|
+
}
|
|
1814
|
+
async function ensureBuiltInSetupIfNeeded(options) {
|
|
1585
1815
|
const config = await loadClientConfig({
|
|
1586
1816
|
path: options.paths.clientConfigPath,
|
|
1587
1817
|
provider: options.provider,
|
|
@@ -1620,7 +1850,7 @@ async function performMiningCycle(options) {
|
|
|
1620
1850
|
backgroundWorkerHeartbeatAtUnixMs: options.runMode === "background" ? Date.now() : null,
|
|
1621
1851
|
},
|
|
1622
1852
|
});
|
|
1623
|
-
if (readContext.localState.availability !== "ready" || readContext.localState.state === null
|
|
1853
|
+
if (readContext.localState.availability !== "ready" || readContext.localState.state === null) {
|
|
1624
1854
|
await refreshAndSaveStatus({
|
|
1625
1855
|
paths: options.paths,
|
|
1626
1856
|
provider: options.provider,
|
|
@@ -1628,9 +1858,10 @@ async function performMiningCycle(options) {
|
|
|
1628
1858
|
overrides: {
|
|
1629
1859
|
runMode: options.runMode,
|
|
1630
1860
|
currentPhase: "waiting",
|
|
1631
|
-
note: "Wallet must
|
|
1861
|
+
note: "Wallet state must be locally available for mining to continue.",
|
|
1632
1862
|
},
|
|
1633
1863
|
visualizer: options.visualizer,
|
|
1864
|
+
visualizerState: options.loopState.ui,
|
|
1634
1865
|
});
|
|
1635
1866
|
return;
|
|
1636
1867
|
}
|
|
@@ -1642,20 +1873,20 @@ async function performMiningCycle(options) {
|
|
|
1642
1873
|
});
|
|
1643
1874
|
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
1644
1875
|
const rpc = options.rpcFactory(service.rpc);
|
|
1645
|
-
const
|
|
1876
|
+
const reconciliation = await reconcileLiveMiningState({
|
|
1646
1877
|
state: readContext.localState.state,
|
|
1647
1878
|
rpc,
|
|
1648
1879
|
nodeBestHash: readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1649
1880
|
nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
|
|
1881
|
+
snapshotState: readContext.snapshot?.state ?? null,
|
|
1650
1882
|
});
|
|
1883
|
+
const reconciledState = reconciliation.state;
|
|
1651
1884
|
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
1652
1885
|
let effectiveReadContext = readContext;
|
|
1653
1886
|
if (JSON.stringify(reconciledState.miningState) !== JSON.stringify(readContext.localState.state.miningState)) {
|
|
1654
1887
|
await saveWalletStatePreservingUnlock({
|
|
1655
1888
|
state: reconciledState,
|
|
1656
1889
|
provider: options.provider,
|
|
1657
|
-
unlockUntilUnixMs: readContext.localState.unlockUntilUnixMs,
|
|
1658
|
-
nowUnixMs: Date.now(),
|
|
1659
1890
|
paths: options.paths,
|
|
1660
1891
|
});
|
|
1661
1892
|
effectiveReadContext = {
|
|
@@ -1663,11 +1894,23 @@ async function performMiningCycle(options) {
|
|
|
1663
1894
|
localState: {
|
|
1664
1895
|
...readContext.localState,
|
|
1665
1896
|
availability: "ready",
|
|
1666
|
-
unlockUntilUnixMs: readContext.localState.unlockUntilUnixMs,
|
|
1667
1897
|
state: reconciledState,
|
|
1668
1898
|
},
|
|
1669
1899
|
};
|
|
1670
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);
|
|
1671
1914
|
if (effectiveReadContext.localState.state.miningState.state === "repair-required") {
|
|
1672
1915
|
await refreshAndSaveStatus({
|
|
1673
1916
|
paths: options.paths,
|
|
@@ -1676,9 +1919,10 @@ async function performMiningCycle(options) {
|
|
|
1676
1919
|
overrides: {
|
|
1677
1920
|
runMode: options.runMode,
|
|
1678
1921
|
currentPhase: "waiting",
|
|
1679
|
-
note: "Mining is blocked until the current mining
|
|
1922
|
+
note: "Mining is blocked until the current mining publish is repaired or reconciled.",
|
|
1680
1923
|
},
|
|
1681
1924
|
visualizer: options.visualizer,
|
|
1925
|
+
visualizerState: options.loopState.ui,
|
|
1682
1926
|
});
|
|
1683
1927
|
return;
|
|
1684
1928
|
}
|
|
@@ -1690,8 +1934,6 @@ async function performMiningCycle(options) {
|
|
|
1690
1934
|
await saveWalletStatePreservingUnlock({
|
|
1691
1935
|
state: nextState,
|
|
1692
1936
|
provider: options.provider,
|
|
1693
|
-
unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
|
|
1694
|
-
nowUnixMs: Date.now(),
|
|
1695
1937
|
paths: options.paths,
|
|
1696
1938
|
});
|
|
1697
1939
|
effectiveReadContext = {
|
|
@@ -1699,7 +1941,6 @@ async function performMiningCycle(options) {
|
|
|
1699
1941
|
localState: {
|
|
1700
1942
|
...effectiveReadContext.localState,
|
|
1701
1943
|
availability: "ready",
|
|
1702
|
-
unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
|
|
1703
1944
|
state: nextState,
|
|
1704
1945
|
},
|
|
1705
1946
|
};
|
|
@@ -1710,16 +1951,17 @@ async function performMiningCycle(options) {
|
|
|
1710
1951
|
overrides: {
|
|
1711
1952
|
runMode: options.runMode,
|
|
1712
1953
|
currentPhase: "waiting",
|
|
1713
|
-
note: "Mining is paused while another wallet mutation
|
|
1954
|
+
note: "Mining is paused while another wallet mutation is active.",
|
|
1714
1955
|
},
|
|
1715
1956
|
visualizer: options.visualizer,
|
|
1957
|
+
visualizerState: options.loopState.ui,
|
|
1716
1958
|
});
|
|
1717
1959
|
return;
|
|
1718
1960
|
}
|
|
1719
1961
|
const preemptionRequest = await readMiningPreemptionRequest(options.paths);
|
|
1720
1962
|
if (preemptionRequest !== null) {
|
|
1721
1963
|
const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
|
|
1722
|
-
state: effectiveReadContext.localState.state.miningState.
|
|
1964
|
+
state: effectiveReadContext.localState.state.miningState.livePublishInMempool
|
|
1723
1965
|
&& effectiveReadContext.localState.state.miningState.state === "paused-stale"
|
|
1724
1966
|
? "paused-stale"
|
|
1725
1967
|
: "paused",
|
|
@@ -1728,8 +1970,6 @@ async function performMiningCycle(options) {
|
|
|
1728
1970
|
await saveWalletStatePreservingUnlock({
|
|
1729
1971
|
state: nextState,
|
|
1730
1972
|
provider: options.provider,
|
|
1731
|
-
unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
|
|
1732
|
-
nowUnixMs: Date.now(),
|
|
1733
1973
|
paths: options.paths,
|
|
1734
1974
|
});
|
|
1735
1975
|
await refreshAndSaveStatus({
|
|
@@ -1748,6 +1988,7 @@ async function performMiningCycle(options) {
|
|
|
1748
1988
|
note: "Mining is paused while another wallet command is preempting sentence generation.",
|
|
1749
1989
|
},
|
|
1750
1990
|
visualizer: options.visualizer,
|
|
1991
|
+
visualizerState: options.loopState.ui,
|
|
1751
1992
|
});
|
|
1752
1993
|
return;
|
|
1753
1994
|
}
|
|
@@ -1774,6 +2015,7 @@ async function performMiningCycle(options) {
|
|
|
1774
2015
|
note: "Mining is waiting for the local Bitcoin node to become publishable.",
|
|
1775
2016
|
},
|
|
1776
2017
|
visualizer: options.visualizer,
|
|
2018
|
+
visualizerState: options.loopState.ui,
|
|
1777
2019
|
});
|
|
1778
2020
|
return;
|
|
1779
2021
|
}
|
|
@@ -1792,10 +2034,22 @@ async function performMiningCycle(options) {
|
|
|
1792
2034
|
: "Mining is waiting for the local Bitcoin node to become publishable.",
|
|
1793
2035
|
},
|
|
1794
2036
|
visualizer: options.visualizer,
|
|
2037
|
+
visualizerState: options.loopState.ui,
|
|
1795
2038
|
});
|
|
1796
2039
|
return;
|
|
1797
2040
|
}
|
|
1798
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);
|
|
1799
2053
|
if (getBlockRewardCogtoshi(targetBlockHeight) === 0n) {
|
|
1800
2054
|
const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
|
|
1801
2055
|
state: "paused",
|
|
@@ -1804,8 +2058,6 @@ async function performMiningCycle(options) {
|
|
|
1804
2058
|
await saveWalletStatePreservingUnlock({
|
|
1805
2059
|
state: nextState,
|
|
1806
2060
|
provider: options.provider,
|
|
1807
|
-
unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
|
|
1808
|
-
nowUnixMs: Date.now(),
|
|
1809
2061
|
paths: options.paths,
|
|
1810
2062
|
});
|
|
1811
2063
|
await refreshAndSaveStatus({
|
|
@@ -1825,6 +2077,7 @@ async function performMiningCycle(options) {
|
|
|
1825
2077
|
note: "Mining is disabled because the target block reward is zero.",
|
|
1826
2078
|
},
|
|
1827
2079
|
visualizer: options.visualizer,
|
|
2080
|
+
visualizerState: options.loopState.ui,
|
|
1828
2081
|
});
|
|
1829
2082
|
await appendEvent(options.paths, createEvent("publish-skipped-zero-reward", "Skipped mining because the target block reward is zero.", {
|
|
1830
2083
|
targetBlockHeight,
|
|
@@ -1833,18 +2086,18 @@ async function performMiningCycle(options) {
|
|
|
1833
2086
|
}));
|
|
1834
2087
|
return;
|
|
1835
2088
|
}
|
|
1836
|
-
|
|
1837
|
-
if (domains.length === 0) {
|
|
2089
|
+
if (tipKey !== null && options.loopState.attemptedTipKey === tipKey) {
|
|
1838
2090
|
await refreshAndSaveStatus({
|
|
1839
2091
|
paths: options.paths,
|
|
1840
2092
|
provider: options.provider,
|
|
1841
2093
|
readContext: effectiveReadContext,
|
|
1842
2094
|
overrides: {
|
|
1843
2095
|
runMode: options.runMode,
|
|
1844
|
-
currentPhase: "
|
|
1845
|
-
note: "
|
|
2096
|
+
currentPhase: "waiting",
|
|
2097
|
+
note: options.loopState.waitingNote ?? "Waiting for the next block after the last mining attempt on this tip.",
|
|
1846
2098
|
},
|
|
1847
2099
|
visualizer: options.visualizer,
|
|
2100
|
+
visualizerState: options.loopState.ui,
|
|
1848
2101
|
});
|
|
1849
2102
|
return;
|
|
1850
2103
|
}
|
|
@@ -1872,215 +2125,264 @@ async function performMiningCycle(options) {
|
|
|
1872
2125
|
return false;
|
|
1873
2126
|
}
|
|
1874
2127
|
};
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
});
|
|
1886
|
-
await appendEvent(options.paths, createEvent("hook-request-start", "Started mining sentence generation.", {
|
|
1887
|
-
targetBlockHeight,
|
|
1888
|
-
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1889
|
-
runId: options.backgroundWorkerRunId,
|
|
1890
|
-
}));
|
|
1891
|
-
let candidates;
|
|
1892
|
-
try {
|
|
1893
|
-
candidates = await generateCandidatesForDomains({
|
|
1894
|
-
rpc,
|
|
1895
|
-
readContext: effectiveReadContext,
|
|
1896
|
-
domains,
|
|
1897
|
-
provider: options.provider,
|
|
1898
|
-
paths: options.paths,
|
|
1899
|
-
indexerTruthKey,
|
|
1900
|
-
runId: options.backgroundWorkerRunId,
|
|
1901
|
-
fetchImpl: options.fetchImpl,
|
|
1902
|
-
});
|
|
1903
|
-
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
1904
|
-
}
|
|
1905
|
-
catch (error) {
|
|
1906
|
-
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) {
|
|
1907
2138
|
await refreshAndSaveStatus({
|
|
1908
2139
|
paths: options.paths,
|
|
1909
2140
|
provider: options.provider,
|
|
1910
2141
|
readContext: effectiveReadContext,
|
|
1911
2142
|
overrides: {
|
|
1912
2143
|
runMode: options.runMode,
|
|
1913
|
-
currentPhase: "
|
|
1914
|
-
|
|
1915
|
-
lastError: error.message,
|
|
1916
|
-
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.",
|
|
1917
2146
|
},
|
|
1918
2147
|
visualizer: options.visualizer,
|
|
2148
|
+
visualizerState: options.loopState.ui,
|
|
1919
2149
|
});
|
|
1920
|
-
await appendEvent(options.paths, createEvent("publish-paused-provider", error.message, {
|
|
1921
|
-
level: "warn",
|
|
1922
|
-
targetBlockHeight,
|
|
1923
|
-
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1924
|
-
runId: options.backgroundWorkerRunId,
|
|
1925
|
-
}));
|
|
1926
|
-
return;
|
|
1927
|
-
}
|
|
1928
|
-
if (error instanceof Error && error.message === "mining_generation_stale_tip") {
|
|
1929
|
-
await appendEvent(options.paths, createEvent("generation-restarted-new-tip", "Detected a new best tip during sentence generation; restarting on the next tick.", {
|
|
1930
|
-
level: "warn",
|
|
1931
|
-
targetBlockHeight,
|
|
1932
|
-
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1933
|
-
runId: options.backgroundWorkerRunId,
|
|
1934
|
-
}));
|
|
1935
|
-
return;
|
|
1936
|
-
}
|
|
1937
|
-
if (error instanceof Error && error.message === "mining_generation_stale_indexer_truth") {
|
|
1938
|
-
clearMiningGateCache(walletRootId);
|
|
1939
|
-
await appendEvent(options.paths, createEvent("generation-restarted-indexer-truth", "Detected updated coherent indexer truth during sentence generation; restarting on the next tick.", {
|
|
1940
|
-
level: "warn",
|
|
1941
|
-
targetBlockHeight,
|
|
1942
|
-
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1943
|
-
runId: options.backgroundWorkerRunId,
|
|
1944
|
-
}));
|
|
1945
|
-
return;
|
|
1946
|
-
}
|
|
1947
|
-
if (error instanceof Error && error.message === "mining_generation_preempted") {
|
|
1948
|
-
await appendEvent(options.paths, createEvent("generation-paused-preempted", "Stopped sentence generation because another wallet command requested mining preemption.", {
|
|
1949
|
-
level: "warn",
|
|
1950
|
-
targetBlockHeight,
|
|
1951
|
-
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1952
|
-
runId: options.backgroundWorkerRunId,
|
|
1953
|
-
}));
|
|
1954
2150
|
return;
|
|
1955
2151
|
}
|
|
1956
|
-
const hookCooldownActive = await persistCustomHookRuntimeOutcome({
|
|
1957
|
-
readContext: effectiveReadContext,
|
|
1958
|
-
provider: options.provider,
|
|
1959
|
-
paths: options.paths,
|
|
1960
|
-
nowUnixMs: Date.now(),
|
|
1961
|
-
success: false,
|
|
1962
|
-
});
|
|
1963
|
-
const failureMessage = error instanceof Error ? error.message : String(error);
|
|
1964
|
-
await refreshAndSaveStatus({
|
|
1965
|
-
paths: options.paths,
|
|
1966
|
-
provider: options.provider,
|
|
1967
|
-
readContext: effectiveReadContext,
|
|
1968
|
-
overrides: {
|
|
1969
|
-
runMode: options.runMode,
|
|
1970
|
-
currentPhase: "waiting-provider",
|
|
1971
|
-
providerState: effectiveReadContext.localState.state?.hookClientState.mining.mode === "custom"
|
|
1972
|
-
? "hook-error"
|
|
1973
|
-
: undefined,
|
|
1974
|
-
lastError: failureMessage,
|
|
1975
|
-
note: effectiveReadContext.localState.state?.hookClientState.mining.mode === "custom"
|
|
1976
|
-
? (hookCooldownActive
|
|
1977
|
-
? "Custom mining hook launch is paused during the post-failure cooldown window."
|
|
1978
|
-
: "Custom mining hook failed during sentence generation. Fix it or rerun `cogcoin hooks enable mining`.")
|
|
1979
|
-
: "Mining sentence generation failed for the current tip.",
|
|
1980
|
-
},
|
|
1981
|
-
visualizer: options.visualizer,
|
|
1982
|
-
});
|
|
1983
|
-
await appendEvent(options.paths, createEvent("hook-request-failed", failureMessage, {
|
|
1984
|
-
level: "error",
|
|
1985
|
-
targetBlockHeight,
|
|
1986
|
-
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1987
|
-
runId: options.backgroundWorkerRunId,
|
|
1988
|
-
}));
|
|
1989
|
-
return;
|
|
1990
|
-
}
|
|
1991
|
-
await refreshAndSaveStatus({
|
|
1992
|
-
paths: options.paths,
|
|
1993
|
-
provider: options.provider,
|
|
1994
|
-
readContext: effectiveReadContext,
|
|
1995
|
-
overrides: {
|
|
1996
|
-
runMode: options.runMode,
|
|
1997
|
-
currentPhase: "scoring",
|
|
1998
|
-
note: "Scoring mining candidates for the current tip.",
|
|
1999
|
-
},
|
|
2000
|
-
visualizer: options.visualizer,
|
|
2001
|
-
});
|
|
2002
|
-
const best = await chooseBestLocalCandidate(candidates);
|
|
2003
|
-
if (best === null) {
|
|
2004
2152
|
await refreshAndSaveStatus({
|
|
2005
2153
|
paths: options.paths,
|
|
2006
2154
|
provider: options.provider,
|
|
2007
2155
|
readContext: effectiveReadContext,
|
|
2008
2156
|
overrides: {
|
|
2009
2157
|
runMode: options.runMode,
|
|
2010
|
-
currentPhase: "
|
|
2011
|
-
|
|
2012
|
-
note: "No publishable mining candidate passed scoring gates for the current tip.",
|
|
2158
|
+
currentPhase: "generating",
|
|
2159
|
+
note: "Generating mining sentences for eligible root domains.",
|
|
2013
2160
|
},
|
|
2014
2161
|
visualizer: options.visualizer,
|
|
2162
|
+
visualizerState: options.loopState.ui,
|
|
2015
2163
|
});
|
|
2016
|
-
await appendEvent(options.paths, createEvent("
|
|
2164
|
+
await appendEvent(options.paths, createEvent("sentence-generation-start", "Started mining sentence generation.", {
|
|
2017
2165
|
targetBlockHeight,
|
|
2018
2166
|
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
2019
2167
|
runId: options.backgroundWorkerRunId,
|
|
2020
2168
|
}));
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
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
|
+
}
|
|
2043
2266
|
await refreshAndSaveStatus({
|
|
2044
2267
|
paths: options.paths,
|
|
2045
2268
|
provider: options.provider,
|
|
2046
2269
|
readContext: effectiveReadContext,
|
|
2047
2270
|
overrides: {
|
|
2048
2271
|
runMode: options.runMode,
|
|
2049
|
-
currentPhase: "
|
|
2050
|
-
|
|
2051
|
-
sameDomainCompetitorSuppressed: gate.sameDomainCompetitorSuppressed,
|
|
2052
|
-
higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
|
|
2053
|
-
dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
|
|
2054
|
-
competitivenessGateIndeterminate: gate.competitivenessGateIndeterminate,
|
|
2055
|
-
mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
|
|
2056
|
-
lastMempoolSequence: gate.lastMempoolSequence,
|
|
2057
|
-
lastCompetitivenessGateAtUnixMs: Date.now(),
|
|
2058
|
-
note: gate.decision === "suppressed-same-domain-mempool"
|
|
2059
|
-
? "Best local sentence found, but a same-domain mempool competitor already matches or beats it."
|
|
2060
|
-
: gate.decision === "suppressed-top5-mempool"
|
|
2061
|
-
? `Best local sentence found, but ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
|
|
2062
|
-
: "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.",
|
|
2063
2274
|
},
|
|
2064
2275
|
visualizer: options.visualizer,
|
|
2276
|
+
visualizerState: options.loopState.ui,
|
|
2065
2277
|
});
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
:
|
|
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()}.`, {
|
|
2075
2312
|
targetBlockHeight: best.targetBlockHeight,
|
|
2076
2313
|
referencedBlockHashDisplay: best.referencedBlockHashDisplay,
|
|
2077
2314
|
domainId: best.domainId,
|
|
2078
2315
|
domainName: best.domainName,
|
|
2079
2316
|
score: best.canonicalBlend.toString(),
|
|
2080
2317
|
runId: options.backgroundWorkerRunId,
|
|
2081
|
-
reason: gate.decision,
|
|
2082
2318
|
}));
|
|
2083
|
-
|
|
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);
|
|
2084
2386
|
}
|
|
2085
2387
|
if (!await ensureCurrentIndexerTruthOrRestart()) {
|
|
2086
2388
|
return;
|
|
@@ -2099,6 +2401,7 @@ async function performMiningCycle(options) {
|
|
|
2099
2401
|
: "Replacing the live mining transaction for the current tip.",
|
|
2100
2402
|
},
|
|
2101
2403
|
visualizer: options.visualizer,
|
|
2404
|
+
visualizerState: options.loopState.ui,
|
|
2102
2405
|
});
|
|
2103
2406
|
const publishLock = await acquireFileLock(options.paths.walletControlLockPath, {
|
|
2104
2407
|
purpose: "wallet-mine",
|
|
@@ -2111,16 +2414,98 @@ async function performMiningCycle(options) {
|
|
|
2111
2414
|
}
|
|
2112
2415
|
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
2113
2416
|
const published = await publishCandidate({
|
|
2114
|
-
readContext: effectiveReadContext,
|
|
2115
|
-
candidate: best,
|
|
2116
2417
|
dataDir: options.dataDir,
|
|
2418
|
+
databasePath: options.databasePath,
|
|
2117
2419
|
provider: options.provider,
|
|
2118
2420
|
paths: options.paths,
|
|
2421
|
+
fallbackState: effectiveReadContext.localState.state,
|
|
2422
|
+
openReadContext: options.openReadContext,
|
|
2119
2423
|
attachService: options.attachService,
|
|
2120
2424
|
rpcFactory: options.rpcFactory,
|
|
2425
|
+
candidate: selectedCandidate,
|
|
2121
2426
|
runId: options.backgroundWorkerRunId,
|
|
2122
2427
|
});
|
|
2123
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.`;
|
|
2124
2509
|
await refreshAndSaveStatus({
|
|
2125
2510
|
paths: options.paths,
|
|
2126
2511
|
provider: options.provider,
|
|
@@ -2133,25 +2518,20 @@ async function performMiningCycle(options) {
|
|
|
2133
2518
|
},
|
|
2134
2519
|
overrides: {
|
|
2135
2520
|
runMode: options.runMode,
|
|
2136
|
-
currentPhase: "
|
|
2521
|
+
currentPhase: "waiting",
|
|
2137
2522
|
currentPublishDecision: published.decision,
|
|
2138
2523
|
sameDomainCompetitorSuppressed: false,
|
|
2139
|
-
higherRankedCompetitorDomainCount:
|
|
2140
|
-
dedupedCompetitorDomainCount:
|
|
2524
|
+
higherRankedCompetitorDomainCount: gateSnapshot.higherRankedCompetitorDomainCount,
|
|
2525
|
+
dedupedCompetitorDomainCount: gateSnapshot.dedupedCompetitorDomainCount,
|
|
2141
2526
|
competitivenessGateIndeterminate: false,
|
|
2142
|
-
mempoolSequenceCacheStatus:
|
|
2143
|
-
lastMempoolSequence:
|
|
2527
|
+
mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
|
|
2528
|
+
lastMempoolSequence: gateSnapshot.lastMempoolSequence,
|
|
2144
2529
|
lastCompetitivenessGateAtUnixMs: Date.now(),
|
|
2145
|
-
note:
|
|
2146
|
-
|
|
2147
|
-
: `Mining candidate ${published.decision === "replaced"
|
|
2148
|
-
? "replaced"
|
|
2149
|
-
: published.decision === "fee-bump"
|
|
2150
|
-
? "fee-bumped"
|
|
2151
|
-
: "broadcast"} as ${published.txid}.`,
|
|
2152
|
-
liveMiningFamilyInMempool: published.state.miningState.liveMiningFamilyInMempool,
|
|
2530
|
+
note: options.loopState.waitingNote,
|
|
2531
|
+
livePublishInMempool: published.state.miningState.livePublishInMempool,
|
|
2153
2532
|
},
|
|
2154
2533
|
visualizer: options.visualizer,
|
|
2534
|
+
visualizerState: options.loopState.ui,
|
|
2155
2535
|
});
|
|
2156
2536
|
}
|
|
2157
2537
|
finally {
|
|
@@ -2195,7 +2575,7 @@ async function saveStopSnapshot(options) {
|
|
|
2195
2575
|
});
|
|
2196
2576
|
try {
|
|
2197
2577
|
let localState = readContext.localState;
|
|
2198
|
-
if (localState.availability === "ready" && localState.state !== null
|
|
2578
|
+
if (localState.availability === "ready" && localState.state !== null) {
|
|
2199
2579
|
const service = await attachOrStartManagedBitcoindService({
|
|
2200
2580
|
dataDir: options.dataDir,
|
|
2201
2581
|
chain: "main",
|
|
@@ -2204,22 +2584,23 @@ async function saveStopSnapshot(options) {
|
|
|
2204
2584
|
}).catch(() => null);
|
|
2205
2585
|
if (service !== null) {
|
|
2206
2586
|
const rpc = createRpcClient(service.rpc);
|
|
2207
|
-
const reconciledState = await reconcileLiveMiningState({
|
|
2587
|
+
const reconciledState = (await reconcileLiveMiningState({
|
|
2208
2588
|
state: localState.state,
|
|
2209
2589
|
rpc,
|
|
2210
2590
|
nodeBestHash: readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
2211
2591
|
nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
|
|
2212
|
-
|
|
2592
|
+
snapshotState: readContext.snapshot?.state ?? null,
|
|
2593
|
+
})).state;
|
|
2213
2594
|
const stopState = defaultMiningStatePatch(reconciledState, {
|
|
2214
2595
|
runMode: "stopped",
|
|
2215
|
-
state: reconciledState.miningState.
|
|
2596
|
+
state: reconciledState.miningState.livePublishInMempool
|
|
2216
2597
|
? reconciledState.miningState.state === "paused-stale"
|
|
2217
2598
|
? "paused-stale"
|
|
2218
2599
|
: "paused"
|
|
2219
2600
|
: reconciledState.miningState.state === "repair-required"
|
|
2220
2601
|
? "repair-required"
|
|
2221
2602
|
: "idle",
|
|
2222
|
-
pauseReason: reconciledState.miningState.
|
|
2603
|
+
pauseReason: reconciledState.miningState.livePublishInMempool
|
|
2223
2604
|
? reconciledState.miningState.state === "paused-stale"
|
|
2224
2605
|
? "stale-block-context"
|
|
2225
2606
|
: "user-stopped"
|
|
@@ -2230,8 +2611,6 @@ async function saveStopSnapshot(options) {
|
|
|
2230
2611
|
await saveWalletStatePreservingUnlock({
|
|
2231
2612
|
state: stopState,
|
|
2232
2613
|
provider: options.provider,
|
|
2233
|
-
unlockUntilUnixMs: localState.unlockUntilUnixMs,
|
|
2234
|
-
nowUnixMs: Date.now(),
|
|
2235
2614
|
paths: options.paths,
|
|
2236
2615
|
});
|
|
2237
2616
|
localState = {
|
|
@@ -2276,6 +2655,7 @@ async function attemptSaveMempool(rpc, paths, runId) {
|
|
|
2276
2655
|
}
|
|
2277
2656
|
async function runMiningLoop(options) {
|
|
2278
2657
|
const suspendDetector = createMiningSuspendDetector();
|
|
2658
|
+
const loopState = createMiningLoopState();
|
|
2279
2659
|
await appendEvent(options.paths, createEvent("runtime-start", `Started ${options.runMode} mining runtime.`, {
|
|
2280
2660
|
runId: options.backgroundWorkerRunId,
|
|
2281
2661
|
}));
|
|
@@ -2304,6 +2684,7 @@ async function runMiningLoop(options) {
|
|
|
2304
2684
|
await performMiningCycle({
|
|
2305
2685
|
...options,
|
|
2306
2686
|
suspendDetector,
|
|
2687
|
+
loopState,
|
|
2307
2688
|
});
|
|
2308
2689
|
await sleep(Math.min(MINING_LOOP_INTERVAL_MS, MINING_STATUS_HEARTBEAT_INTERVAL_MS), options.signal);
|
|
2309
2690
|
}
|
|
@@ -2493,7 +2874,7 @@ export async function stopBackgroundMining(options) {
|
|
|
2493
2874
|
runMode: "background",
|
|
2494
2875
|
backgroundWorkerPid: snapshot.backgroundWorkerPid,
|
|
2495
2876
|
backgroundWorkerRunId: snapshot.backgroundWorkerRunId,
|
|
2496
|
-
note: snapshot.
|
|
2877
|
+
note: snapshot.livePublishInMempool
|
|
2497
2878
|
? "Background mining stopped. The last mining transaction may still confirm from mempool."
|
|
2498
2879
|
: "Background mining stopped.",
|
|
2499
2880
|
});
|
|
@@ -2575,8 +2956,11 @@ export async function handleDetectedMiningRuntimeResumeForTesting(options) {
|
|
|
2575
2956
|
await handleDetectedMiningRuntimeResume(options);
|
|
2576
2957
|
}
|
|
2577
2958
|
export async function performMiningCycleForTesting(options) {
|
|
2578
|
-
await performMiningCycle(
|
|
2959
|
+
await performMiningCycle({
|
|
2960
|
+
...options,
|
|
2961
|
+
loopState: options.loopState ?? createMiningLoopState(),
|
|
2962
|
+
});
|
|
2579
2963
|
}
|
|
2580
|
-
export function
|
|
2581
|
-
return
|
|
2964
|
+
export function shouldKeepCurrentTipLivePublishForTesting(options) {
|
|
2965
|
+
return livePublishTargetsCandidateTip(options);
|
|
2582
2966
|
}
|