@cogcoin/client 0.5.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/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/app-paths.d.ts +38 -0
- package/dist/app-paths.js +121 -0
- package/dist/art/banner.txt +13 -0
- package/dist/art/scroll.txt +13 -0
- package/dist/art/train-car.txt +6 -0
- package/dist/art/train-smoke.txt +6 -0
- package/dist/art/train.txt +6 -0
- package/dist/bitcoind/bootstrap/chainstate.d.ts +4 -0
- package/dist/bitcoind/bootstrap/chainstate.js +13 -0
- package/dist/bitcoind/bootstrap/constants.d.ts +7 -0
- package/dist/bitcoind/bootstrap/constants.js +12 -0
- package/dist/bitcoind/bootstrap/controller.d.ts +29 -0
- package/dist/bitcoind/bootstrap/controller.js +101 -0
- package/dist/bitcoind/bootstrap/download.d.ts +2 -0
- package/dist/bitcoind/bootstrap/download.js +196 -0
- package/dist/bitcoind/bootstrap/headers.d.ts +13 -0
- package/dist/bitcoind/bootstrap/headers.js +61 -0
- package/dist/bitcoind/bootstrap/paths.d.ts +4 -0
- package/dist/bitcoind/bootstrap/paths.js +15 -0
- package/dist/bitcoind/bootstrap/snapshot-file.d.ts +7 -0
- package/dist/bitcoind/bootstrap/snapshot-file.js +42 -0
- package/dist/bitcoind/bootstrap/state.d.ts +40 -0
- package/dist/bitcoind/bootstrap/state.js +70 -0
- package/dist/bitcoind/bootstrap/types.d.ts +28 -0
- package/dist/bitcoind/bootstrap/types.js +1 -0
- package/dist/bitcoind/bootstrap.d.ts +8 -0
- package/dist/bitcoind/bootstrap.js +7 -0
- package/dist/bitcoind/client/factory.d.ts +3 -0
- package/dist/bitcoind/client/factory.js +57 -0
- package/dist/bitcoind/client/follow-block-times.d.ts +8 -0
- package/dist/bitcoind/client/follow-block-times.js +25 -0
- package/dist/bitcoind/client/follow-loop.d.ts +10 -0
- package/dist/bitcoind/client/follow-loop.js +57 -0
- package/dist/bitcoind/client/internal-types.d.ts +63 -0
- package/dist/bitcoind/client/internal-types.js +18 -0
- package/dist/bitcoind/client/managed-client.d.ts +20 -0
- package/dist/bitcoind/client/managed-client.js +197 -0
- package/dist/bitcoind/client/rate-tracker.d.ts +2 -0
- package/dist/bitcoind/client/rate-tracker.js +24 -0
- package/dist/bitcoind/client/sync-engine.d.ts +3 -0
- package/dist/bitcoind/client/sync-engine.js +143 -0
- package/dist/bitcoind/client.d.ts +1 -0
- package/dist/bitcoind/client.js +1 -0
- package/dist/bitcoind/errors.d.ts +1 -0
- package/dist/bitcoind/errors.js +49 -0
- package/dist/bitcoind/index.d.ts +2 -0
- package/dist/bitcoind/index.js +1 -0
- package/dist/bitcoind/indexer-daemon-main.d.ts +1 -0
- package/dist/bitcoind/indexer-daemon-main.js +472 -0
- package/dist/bitcoind/indexer-daemon.d.ts +107 -0
- package/dist/bitcoind/indexer-daemon.js +391 -0
- package/dist/bitcoind/node.d.ts +8 -0
- package/dist/bitcoind/node.js +219 -0
- package/dist/bitcoind/normalize.d.ts +3 -0
- package/dist/bitcoind/normalize.js +47 -0
- package/dist/bitcoind/progress/assets.d.ts +10 -0
- package/dist/bitcoind/progress/assets.js +90 -0
- package/dist/bitcoind/progress/constants.d.ts +48 -0
- package/dist/bitcoind/progress/constants.js +53 -0
- package/dist/bitcoind/progress/controller.d.ts +28 -0
- package/dist/bitcoind/progress/controller.js +188 -0
- package/dist/bitcoind/progress/follow-scene.d.ts +40 -0
- package/dist/bitcoind/progress/follow-scene.js +367 -0
- package/dist/bitcoind/progress/formatting.d.ts +23 -0
- package/dist/bitcoind/progress/formatting.js +227 -0
- package/dist/bitcoind/progress/quote-scene.d.ts +4 -0
- package/dist/bitcoind/progress/quote-scene.js +137 -0
- package/dist/bitcoind/progress/train-scene.d.ts +9 -0
- package/dist/bitcoind/progress/train-scene.js +92 -0
- package/dist/bitcoind/progress/tty-renderer.d.ts +18 -0
- package/dist/bitcoind/progress/tty-renderer.js +150 -0
- package/dist/bitcoind/progress.d.ts +7 -0
- package/dist/bitcoind/progress.js +7 -0
- package/dist/bitcoind/quotes.d.ts +24 -0
- package/dist/bitcoind/quotes.js +195 -0
- package/dist/bitcoind/rpc.d.ts +71 -0
- package/dist/bitcoind/rpc.js +322 -0
- package/dist/bitcoind/service-paths.d.ts +19 -0
- package/dist/bitcoind/service-paths.js +49 -0
- package/dist/bitcoind/service.d.ts +40 -0
- package/dist/bitcoind/service.js +735 -0
- package/dist/bitcoind/testing.d.ts +9 -0
- package/dist/bitcoind/testing.js +9 -0
- package/dist/bitcoind/types.d.ts +396 -0
- package/dist/bitcoind/types.js +3 -0
- package/dist/bytes.d.ts +9 -0
- package/dist/bytes.js +36 -0
- package/dist/cli/commands/follow.d.ts +2 -0
- package/dist/cli/commands/follow.js +43 -0
- package/dist/cli/commands/mining-admin.d.ts +2 -0
- package/dist/cli/commands/mining-admin.js +92 -0
- package/dist/cli/commands/mining-read.d.ts +2 -0
- package/dist/cli/commands/mining-read.js +173 -0
- package/dist/cli/commands/mining-runtime.d.ts +2 -0
- package/dist/cli/commands/mining-runtime.js +108 -0
- package/dist/cli/commands/status.d.ts +2 -0
- package/dist/cli/commands/status.js +31 -0
- package/dist/cli/commands/sync.d.ts +2 -0
- package/dist/cli/commands/sync.js +52 -0
- package/dist/cli/commands/wallet-admin.d.ts +2 -0
- package/dist/cli/commands/wallet-admin.js +175 -0
- package/dist/cli/commands/wallet-mutation.d.ts +2 -0
- package/dist/cli/commands/wallet-mutation.js +681 -0
- package/dist/cli/commands/wallet-read.d.ts +2 -0
- package/dist/cli/commands/wallet-read.js +265 -0
- package/dist/cli/context.d.ts +3 -0
- package/dist/cli/context.js +75 -0
- package/dist/cli/io.d.ts +3 -0
- package/dist/cli/io.js +12 -0
- package/dist/cli/mining-format.d.ts +5 -0
- package/dist/cli/mining-format.js +156 -0
- package/dist/cli/mining-json.d.ts +49 -0
- package/dist/cli/mining-json.js +89 -0
- package/dist/cli/mutation-command-groups.d.ts +15 -0
- package/dist/cli/mutation-command-groups.js +71 -0
- package/dist/cli/mutation-json.d.ts +430 -0
- package/dist/cli/mutation-json.js +311 -0
- package/dist/cli/mutation-resolved-json.d.ts +124 -0
- package/dist/cli/mutation-resolved-json.js +129 -0
- package/dist/cli/mutation-success.d.ts +20 -0
- package/dist/cli/mutation-success.js +47 -0
- package/dist/cli/mutation-text-format.d.ts +22 -0
- package/dist/cli/mutation-text-format.js +171 -0
- package/dist/cli/mutation-text-write.d.ts +13 -0
- package/dist/cli/mutation-text-write.js +16 -0
- package/dist/cli/output.d.ts +185 -0
- package/dist/cli/output.js +1085 -0
- package/dist/cli/parse.d.ts +3 -0
- package/dist/cli/parse.js +971 -0
- package/dist/cli/preview-json.d.ts +416 -0
- package/dist/cli/preview-json.js +293 -0
- package/dist/cli/prompt.d.ts +3 -0
- package/dist/cli/prompt.js +33 -0
- package/dist/cli/read-json.d.ts +187 -0
- package/dist/cli/read-json.js +675 -0
- package/dist/cli/runner.d.ts +2 -0
- package/dist/cli/runner.js +129 -0
- package/dist/cli/signals.d.ts +3 -0
- package/dist/cli/signals.js +63 -0
- package/dist/cli/status-format.d.ts +2 -0
- package/dist/cli/status-format.js +48 -0
- package/dist/cli/types.d.ts +148 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/wallet-format.d.ts +29 -0
- package/dist/cli/wallet-format.js +637 -0
- package/dist/cli/workflow-hints.d.ts +13 -0
- package/dist/cli/workflow-hints.js +94 -0
- package/dist/cli-runner.d.ts +3 -0
- package/dist/cli-runner.js +3 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +6 -0
- package/dist/client/default-client.d.ts +11 -0
- package/dist/client/default-client.js +118 -0
- package/dist/client/factory.d.ts +2 -0
- package/dist/client/factory.js +15 -0
- package/dist/client/initialization.d.ts +6 -0
- package/dist/client/initialization.js +30 -0
- package/dist/client/persistence.d.ts +5 -0
- package/dist/client/persistence.js +28 -0
- package/dist/client/store-adapter.d.ts +3 -0
- package/dist/client/store-adapter.js +20 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/passive-status.d.ts +36 -0
- package/dist/passive-status.js +100 -0
- package/dist/sqlite/better-sqlite3.d.ts +26 -0
- package/dist/sqlite/better-sqlite3.js +4 -0
- package/dist/sqlite/checkpoints.d.ts +11 -0
- package/dist/sqlite/checkpoints.js +27 -0
- package/dist/sqlite/driver.d.ts +17 -0
- package/dist/sqlite/driver.js +98 -0
- package/dist/sqlite/index.d.ts +4 -0
- package/dist/sqlite/index.js +9 -0
- package/dist/sqlite/migrate.d.ts +2 -0
- package/dist/sqlite/migrate.js +37 -0
- package/dist/sqlite/store.d.ts +3 -0
- package/dist/sqlite/store.js +122 -0
- package/dist/sqlite/tip-meta.d.ts +26 -0
- package/dist/sqlite/tip-meta.js +97 -0
- package/dist/sqlite/types.d.ts +10 -0
- package/dist/sqlite/types.js +1 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.js +1 -0
- package/dist/wallet/archive.d.ts +4 -0
- package/dist/wallet/archive.js +39 -0
- package/dist/wallet/cogop/constants.d.ts +32 -0
- package/dist/wallet/cogop/constants.js +32 -0
- package/dist/wallet/cogop/index.d.ts +32 -0
- package/dist/wallet/cogop/index.js +213 -0
- package/dist/wallet/cogop/numeric.d.ts +3 -0
- package/dist/wallet/cogop/numeric.js +24 -0
- package/dist/wallet/cogop/scriptpubkey.d.ts +2 -0
- package/dist/wallet/cogop/scriptpubkey.js +13 -0
- package/dist/wallet/cogop/validate-name.d.ts +2 -0
- package/dist/wallet/cogop/validate-name.js +18 -0
- package/dist/wallet/fs/atomic.d.ts +6 -0
- package/dist/wallet/fs/atomic.js +46 -0
- package/dist/wallet/fs/lock.d.ts +19 -0
- package/dist/wallet/fs/lock.js +61 -0
- package/dist/wallet/fs/status-file.d.ts +1 -0
- package/dist/wallet/fs/status-file.js +4 -0
- package/dist/wallet/lifecycle.d.ts +193 -0
- package/dist/wallet/lifecycle.js +1475 -0
- package/dist/wallet/material.d.ts +45 -0
- package/dist/wallet/material.js +118 -0
- package/dist/wallet/mining/config.d.ts +18 -0
- package/dist/wallet/mining/config.js +44 -0
- package/dist/wallet/mining/constants.d.ts +24 -0
- package/dist/wallet/mining/constants.js +24 -0
- package/dist/wallet/mining/control.d.ts +53 -0
- package/dist/wallet/mining/control.js +758 -0
- package/dist/wallet/mining/coordination.d.ts +40 -0
- package/dist/wallet/mining/coordination.js +121 -0
- package/dist/wallet/mining/hook-protocol.d.ts +47 -0
- package/dist/wallet/mining/hook-protocol.js +161 -0
- package/dist/wallet/mining/hook-runner.d.ts +1 -0
- package/dist/wallet/mining/hook-runner.js +52 -0
- package/dist/wallet/mining/hooks.d.ts +38 -0
- package/dist/wallet/mining/hooks.js +520 -0
- package/dist/wallet/mining/index.d.ts +8 -0
- package/dist/wallet/mining/index.js +6 -0
- package/dist/wallet/mining/runner.d.ts +155 -0
- package/dist/wallet/mining/runner.js +2574 -0
- package/dist/wallet/mining/runtime-artifacts.d.ts +17 -0
- package/dist/wallet/mining/runtime-artifacts.js +166 -0
- package/dist/wallet/mining/sentences.d.ts +23 -0
- package/dist/wallet/mining/sentences.js +281 -0
- package/dist/wallet/mining/state.d.ts +9 -0
- package/dist/wallet/mining/state.js +75 -0
- package/dist/wallet/mining/types.d.ts +141 -0
- package/dist/wallet/mining/types.js +1 -0
- package/dist/wallet/mining/visualizer.d.ts +19 -0
- package/dist/wallet/mining/visualizer.js +134 -0
- package/dist/wallet/mining/worker-main.d.ts +1 -0
- package/dist/wallet/mining/worker-main.js +17 -0
- package/dist/wallet/read/context.d.ts +20 -0
- package/dist/wallet/read/context.js +532 -0
- package/dist/wallet/read/filter.d.ts +9 -0
- package/dist/wallet/read/filter.js +42 -0
- package/dist/wallet/read/index.d.ts +4 -0
- package/dist/wallet/read/index.js +3 -0
- package/dist/wallet/read/project.d.ts +11 -0
- package/dist/wallet/read/project.js +300 -0
- package/dist/wallet/read/types.d.ts +144 -0
- package/dist/wallet/read/types.js +1 -0
- package/dist/wallet/runtime.d.ts +26 -0
- package/dist/wallet/runtime.js +28 -0
- package/dist/wallet/state/crypto.d.ts +31 -0
- package/dist/wallet/state/crypto.js +127 -0
- package/dist/wallet/state/provider.d.ts +37 -0
- package/dist/wallet/state/provider.js +312 -0
- package/dist/wallet/state/session.d.ts +12 -0
- package/dist/wallet/state/session.js +23 -0
- package/dist/wallet/state/storage.d.ts +19 -0
- package/dist/wallet/state/storage.js +55 -0
- package/dist/wallet/tx/anchor.d.ts +40 -0
- package/dist/wallet/tx/anchor.js +1210 -0
- package/dist/wallet/tx/cog.d.ts +92 -0
- package/dist/wallet/tx/cog.js +1055 -0
- package/dist/wallet/tx/common.d.ts +89 -0
- package/dist/wallet/tx/common.js +156 -0
- package/dist/wallet/tx/confirm.d.ts +15 -0
- package/dist/wallet/tx/confirm.js +24 -0
- package/dist/wallet/tx/domain-admin.d.ts +105 -0
- package/dist/wallet/tx/domain-admin.js +869 -0
- package/dist/wallet/tx/domain-market.d.ts +112 -0
- package/dist/wallet/tx/domain-market.js +1365 -0
- package/dist/wallet/tx/field.d.ts +101 -0
- package/dist/wallet/tx/field.js +1853 -0
- package/dist/wallet/tx/identity-selector.d.ts +12 -0
- package/dist/wallet/tx/identity-selector.js +52 -0
- package/dist/wallet/tx/index.d.ts +7 -0
- package/dist/wallet/tx/index.js +7 -0
- package/dist/wallet/tx/journal.d.ts +5 -0
- package/dist/wallet/tx/journal.js +31 -0
- package/dist/wallet/tx/register.d.ts +68 -0
- package/dist/wallet/tx/register.js +952 -0
- package/dist/wallet/tx/reputation.d.ts +72 -0
- package/dist/wallet/tx/reputation.js +693 -0
- package/dist/wallet/tx/targets.d.ts +7 -0
- package/dist/wallet/tx/targets.js +122 -0
- package/dist/wallet/types.d.ts +249 -0
- package/dist/wallet/types.js +1 -0
- package/dist/writing_quotes.json +1654 -0
- package/package.json +78 -0
|
@@ -0,0 +1,2574 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { lookupDomain, lookupDomainById, } from "@cogcoin/indexer/queries";
|
|
5
|
+
import { assaySentences, deriveBlendSeed, displayToInternalBlockhash, getWords, settleBlock, } from "@cogcoin/scoring";
|
|
6
|
+
import { probeIndexerDaemon } from "../../bitcoind/indexer-daemon.js";
|
|
7
|
+
import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
|
|
8
|
+
import { createRpcClient } from "../../bitcoind/node.js";
|
|
9
|
+
import { COG_OPCODES, COG_PREFIX } from "../cogop/constants.js";
|
|
10
|
+
import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
|
|
11
|
+
import { DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB, buildWalletMutationTransaction, isAlreadyAcceptedError, isBroadcastUnknownError, saveWalletStatePreservingUnlock, } from "../tx/common.js";
|
|
12
|
+
import { acquireFileLock } from "../fs/lock.js";
|
|
13
|
+
import { loadUnlockedWalletState } from "../lifecycle.js";
|
|
14
|
+
import { isMineableWalletDomain, openWalletReadContext, } from "../read/index.js";
|
|
15
|
+
import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
|
|
16
|
+
import { createDefaultWalletSecretProvider, } from "../state/provider.js";
|
|
17
|
+
import { serializeMine } from "../cogop/index.js";
|
|
18
|
+
import { appendMiningEvent, loadMiningRuntimeStatus, saveMiningRuntimeStatus, } from "./runtime-artifacts.js";
|
|
19
|
+
import { loadClientConfig } from "./config.js";
|
|
20
|
+
import { MINING_HOOK_COOLDOWN_MS, MINING_HOOK_FAILURE_THRESHOLD, 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
|
+
import { inspectMiningControlPlane, setupBuiltInMining } from "./control.js";
|
|
22
|
+
import { isMiningGenerationAbortRequested, markMiningGenerationActive, markMiningGenerationInactive, readMiningPreemptionRequest, requestMiningGenerationPreemption, } from "./coordination.js";
|
|
23
|
+
import { clearMiningFamilyState, miningFamilyMayStillExist, normalizeMiningPublishState, normalizeMiningStateRecord, } from "./state.js";
|
|
24
|
+
import { createGenerateSentencesHookLimits } from "./hook-protocol.js";
|
|
25
|
+
import { generateMiningSentences, MiningProviderRequestError } from "./sentences.js";
|
|
26
|
+
import { MiningFollowVisualizer } from "./visualizer.js";
|
|
27
|
+
const BEST_BLOCK_POLL_INTERVAL_MS = 500;
|
|
28
|
+
const BACKGROUND_START_TIMEOUT_MS = 15_000;
|
|
29
|
+
class MiningSuspendDetectedError extends Error {
|
|
30
|
+
detectedAtUnixMs;
|
|
31
|
+
constructor(detectedAtUnixMs) {
|
|
32
|
+
super("mining_runtime_resumed");
|
|
33
|
+
this.detectedAtUnixMs = detectedAtUnixMs;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const miningGateCache = new Map();
|
|
37
|
+
function createMiningSuspendDetector(monotonicNow = performance.now()) {
|
|
38
|
+
return {
|
|
39
|
+
lastMonotonicMs: monotonicNow,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function checkpointMiningSuspendDetector(detector, monotonicNow = performance.now()) {
|
|
43
|
+
if (detector === undefined) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const gapMs = monotonicNow - detector.lastMonotonicMs;
|
|
47
|
+
detector.lastMonotonicMs = monotonicNow;
|
|
48
|
+
if (gapMs > MINING_SUSPEND_GAP_THRESHOLD_MS) {
|
|
49
|
+
throw new MiningSuspendDetectedError(Date.now());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function clearMiningGateCache(walletRootId) {
|
|
53
|
+
if (walletRootId === null || walletRootId === undefined) {
|
|
54
|
+
miningGateCache.clear();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
miningGateCache.delete(walletRootId);
|
|
58
|
+
}
|
|
59
|
+
function sleep(ms, signal) {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const timer = setTimeout(resolve, ms);
|
|
62
|
+
signal?.addEventListener("abort", () => {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
resolve();
|
|
65
|
+
}, { once: true });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async function isProcessAlive(pid) {
|
|
69
|
+
if (pid === null) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
process.kill(pid, 0);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof Error && "code" in error && error.code === "ESRCH") {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function writeStdout(stream, line) {
|
|
84
|
+
if (stream === undefined) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
stream.write(`${line}\n`);
|
|
88
|
+
}
|
|
89
|
+
function createEvent(kind, message, options = {}) {
|
|
90
|
+
return {
|
|
91
|
+
schemaVersion: 1,
|
|
92
|
+
timestampUnixMs: options.timestampUnixMs ?? Date.now(),
|
|
93
|
+
level: options.level ?? "info",
|
|
94
|
+
kind,
|
|
95
|
+
message,
|
|
96
|
+
targetBlockHeight: options.targetBlockHeight ?? null,
|
|
97
|
+
referencedBlockHashDisplay: options.referencedBlockHashDisplay ?? null,
|
|
98
|
+
domainId: options.domainId ?? null,
|
|
99
|
+
domainName: options.domainName ?? null,
|
|
100
|
+
txid: options.txid ?? null,
|
|
101
|
+
feeRateSatVb: options.feeRateSatVb ?? null,
|
|
102
|
+
feeSats: options.feeSats ?? null,
|
|
103
|
+
score: options.score ?? null,
|
|
104
|
+
reason: options.reason ?? null,
|
|
105
|
+
runId: options.runId ?? null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function cloneMiningState(state) {
|
|
109
|
+
const normalized = normalizeMiningStateRecord(state);
|
|
110
|
+
return {
|
|
111
|
+
...normalized,
|
|
112
|
+
currentBip39WordIndices: normalized.currentBip39WordIndices === null ? null : [...normalized.currentBip39WordIndices],
|
|
113
|
+
sharedMiningConflictOutpoint: normalized.sharedMiningConflictOutpoint === null
|
|
114
|
+
? null
|
|
115
|
+
: { ...normalized.sharedMiningConflictOutpoint },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function hasBlockingMutation(state) {
|
|
119
|
+
return state.proactiveFamilies.some((family) => family.status === "draft"
|
|
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"
|
|
124
|
+
|| mutation.status === "broadcasting"
|
|
125
|
+
|| mutation.status === "broadcast-unknown"
|
|
126
|
+
|| mutation.status === "live"
|
|
127
|
+
|| mutation.status === "repair-required");
|
|
128
|
+
}
|
|
129
|
+
function rootDomain(name) {
|
|
130
|
+
return !name.includes("-");
|
|
131
|
+
}
|
|
132
|
+
function uint32BigEndian(value) {
|
|
133
|
+
const buffer = Buffer.alloc(4);
|
|
134
|
+
buffer.writeUInt32BE(value >>> 0, 0);
|
|
135
|
+
return buffer;
|
|
136
|
+
}
|
|
137
|
+
function getBlockRewardCogtoshi(height) {
|
|
138
|
+
const halvingEra = Math.floor(height / 210_000);
|
|
139
|
+
if (halvingEra >= 33) {
|
|
140
|
+
return 0n;
|
|
141
|
+
}
|
|
142
|
+
return 5000000000n >> BigInt(halvingEra);
|
|
143
|
+
}
|
|
144
|
+
function deriveMiningWordIndices(referencedBlockhash, miningDomainId) {
|
|
145
|
+
const seed = createHash("sha256")
|
|
146
|
+
.update(Buffer.from(referencedBlockhash))
|
|
147
|
+
.update(uint32BigEndian(miningDomainId))
|
|
148
|
+
.digest();
|
|
149
|
+
const indices = [];
|
|
150
|
+
for (let index = 0; index < 5; index += 1) {
|
|
151
|
+
const chunkOffset = index * 4;
|
|
152
|
+
let wordIndex = seed.readUInt32BE(chunkOffset) % 2048;
|
|
153
|
+
while (indices.includes(wordIndex)) {
|
|
154
|
+
wordIndex = (wordIndex + 1) % 2048;
|
|
155
|
+
}
|
|
156
|
+
indices.push(wordIndex);
|
|
157
|
+
}
|
|
158
|
+
return indices;
|
|
159
|
+
}
|
|
160
|
+
function outpointKey(outpoint) {
|
|
161
|
+
return outpoint === null ? null : `${outpoint.txid}:${outpoint.vout}`;
|
|
162
|
+
}
|
|
163
|
+
function numberToSats(value) {
|
|
164
|
+
const text = typeof value === "number" ? value.toFixed(8) : value;
|
|
165
|
+
const match = /^(-?)(\d+)(?:\.(\d{0,8}))?$/.exec(text.trim());
|
|
166
|
+
if (match == null) {
|
|
167
|
+
throw new Error(`mining_invalid_amount_${text}`);
|
|
168
|
+
}
|
|
169
|
+
const sign = match[1] === "-" ? -1n : 1n;
|
|
170
|
+
const whole = BigInt(match[2] ?? "0");
|
|
171
|
+
const fraction = BigInt((match[3] ?? "").padEnd(8, "0"));
|
|
172
|
+
return sign * ((whole * 100000000n) + fraction);
|
|
173
|
+
}
|
|
174
|
+
function satsToBtc(value) {
|
|
175
|
+
return Number(value) / 100_000_000;
|
|
176
|
+
}
|
|
177
|
+
function computeIntentFingerprint(state, candidate) {
|
|
178
|
+
return createHash("sha256")
|
|
179
|
+
.update([
|
|
180
|
+
"mine",
|
|
181
|
+
state.walletRootId,
|
|
182
|
+
candidate.domainId,
|
|
183
|
+
candidate.referencedBlockHashDisplay,
|
|
184
|
+
Buffer.from(candidate.encodedSentenceBytes).toString("hex"),
|
|
185
|
+
].join("\n"))
|
|
186
|
+
.digest("hex");
|
|
187
|
+
}
|
|
188
|
+
function defaultMiningStatePatch(state, patch) {
|
|
189
|
+
return {
|
|
190
|
+
...state,
|
|
191
|
+
miningState: {
|
|
192
|
+
...cloneMiningState(state.miningState),
|
|
193
|
+
...patch,
|
|
194
|
+
currentPublishState: normalizeMiningPublishState(patch.currentPublishState ?? state.miningState.currentPublishState),
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function decodeMinePayload(payload) {
|
|
199
|
+
if (payload.length < 68 || Buffer.from(payload.subarray(0, 3)).toString("utf8") !== "COG" || payload[3] !== 0x01) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
domainId: Buffer.from(payload).readUInt32BE(4),
|
|
204
|
+
referencedBlockPrefixHex: Buffer.from(payload.subarray(8, 12)).toString("hex"),
|
|
205
|
+
sentenceBytes: payload.subarray(12, 72),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function bytesToHex(value) {
|
|
209
|
+
return value == null ? null : Buffer.from(value).toString("hex");
|
|
210
|
+
}
|
|
211
|
+
function readU32BE(bytes, offset) {
|
|
212
|
+
if ((offset + 4) > bytes.length) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
return Buffer.from(bytes.subarray(offset, offset + 4)).readUInt32BE(0);
|
|
216
|
+
}
|
|
217
|
+
function readLenPrefixedScriptHex(bytes, offset) {
|
|
218
|
+
const length = bytes[offset];
|
|
219
|
+
if (length === undefined || (offset + 1 + length) > bytes.length) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
scriptHex: Buffer.from(bytes.subarray(offset + 1, offset + 1 + length)).toString("hex"),
|
|
224
|
+
nextOffset: offset + 1 + length,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function parseSupportedAncestorOperation(context) {
|
|
228
|
+
const payload = context.payload;
|
|
229
|
+
if (payload === null) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
if (payload.length < 4
|
|
233
|
+
|| payload[0] !== COG_PREFIX[0]
|
|
234
|
+
|| payload[1] !== COG_PREFIX[1]
|
|
235
|
+
|| payload[2] !== COG_PREFIX[2]) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const opcode = payload[3];
|
|
239
|
+
if (opcode === COG_OPCODES.DOMAIN_REG) {
|
|
240
|
+
const nameLength = payload[4];
|
|
241
|
+
if (nameLength === undefined || (5 + nameLength) !== payload.length) {
|
|
242
|
+
return "unsupported";
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
kind: "domain-reg",
|
|
246
|
+
name: Buffer.from(payload.subarray(5, 5 + nameLength)).toString("utf8"),
|
|
247
|
+
senderScriptHex: context.senderScriptHex,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
if (opcode === COG_OPCODES.DOMAIN_TRANSFER) {
|
|
251
|
+
const domainId = readU32BE(payload, 4);
|
|
252
|
+
const recipient = domainId === null ? null : readLenPrefixedScriptHex(payload, 8);
|
|
253
|
+
if (domainId === null || recipient === null || recipient.nextOffset !== payload.length) {
|
|
254
|
+
return "unsupported";
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
kind: "domain-transfer",
|
|
258
|
+
domainId,
|
|
259
|
+
recipientScriptHex: recipient.scriptHex,
|
|
260
|
+
senderScriptHex: context.senderScriptHex,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (opcode === COG_OPCODES.DOMAIN_ANCHOR) {
|
|
264
|
+
const domainId = readU32BE(payload, 4);
|
|
265
|
+
if (domainId === null) {
|
|
266
|
+
return "unsupported";
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
kind: "domain-anchor",
|
|
270
|
+
domainId,
|
|
271
|
+
senderScriptHex: context.senderScriptHex,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (opcode === COG_OPCODES.SET_DELEGATE || opcode === COG_OPCODES.SET_MINER) {
|
|
275
|
+
const domainId = readU32BE(payload, 4);
|
|
276
|
+
if (domainId === null) {
|
|
277
|
+
return "unsupported";
|
|
278
|
+
}
|
|
279
|
+
if (payload.length === 8) {
|
|
280
|
+
return opcode === COG_OPCODES.SET_DELEGATE
|
|
281
|
+
? { kind: "set-delegate", domainId, delegateScriptHex: null }
|
|
282
|
+
: { kind: "set-miner", domainId, minerScriptHex: null };
|
|
283
|
+
}
|
|
284
|
+
const target = readLenPrefixedScriptHex(payload, 8);
|
|
285
|
+
if (target === null || target.nextOffset !== payload.length) {
|
|
286
|
+
return "unsupported";
|
|
287
|
+
}
|
|
288
|
+
return opcode === COG_OPCODES.SET_DELEGATE
|
|
289
|
+
? { kind: "set-delegate", domainId, delegateScriptHex: target.scriptHex }
|
|
290
|
+
: { kind: "set-miner", domainId, minerScriptHex: target.scriptHex };
|
|
291
|
+
}
|
|
292
|
+
return "unsupported";
|
|
293
|
+
}
|
|
294
|
+
function getAncestorTxids(context, txContexts) {
|
|
295
|
+
return context.rawTransaction.vin
|
|
296
|
+
.map((vin) => vin.txid ?? null)
|
|
297
|
+
.filter((txid) => txid !== null && txContexts.has(txid));
|
|
298
|
+
}
|
|
299
|
+
function topologicallyOrderAncestorContexts(options) {
|
|
300
|
+
const visited = new Set();
|
|
301
|
+
const visiting = new Set();
|
|
302
|
+
const ordered = [];
|
|
303
|
+
const visit = (txid) => {
|
|
304
|
+
if (visited.has(txid)) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
if (visiting.has(txid)) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
const context = options.txContexts.get(txid);
|
|
311
|
+
if (context === undefined) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
visiting.add(txid);
|
|
315
|
+
for (const parentTxid of getAncestorTxids(context, options.txContexts)) {
|
|
316
|
+
if (!visit(parentTxid)) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
visiting.delete(txid);
|
|
321
|
+
visited.add(txid);
|
|
322
|
+
ordered.push(context);
|
|
323
|
+
return true;
|
|
324
|
+
};
|
|
325
|
+
const root = options.txContexts.get(options.txid);
|
|
326
|
+
if (root === undefined) {
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
for (const parentTxid of getAncestorTxids(root, options.txContexts)) {
|
|
330
|
+
if (!visit(parentTxid)) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return ordered;
|
|
335
|
+
}
|
|
336
|
+
function cloneOverlayDomainFromConfirmed(readContext, domainId) {
|
|
337
|
+
const domain = lookupDomainById(readContext.snapshot.state, domainId);
|
|
338
|
+
if (domain === null) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
domainId,
|
|
343
|
+
name: domain.name,
|
|
344
|
+
anchored: domain.anchored,
|
|
345
|
+
ownerScriptHex: bytesToHex(domain.ownerScriptPubKey),
|
|
346
|
+
delegateScriptHex: bytesToHex(domain.delegate),
|
|
347
|
+
minerScriptHex: bytesToHex(domain.miner),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function applySupportedAncestorOperation(options) {
|
|
351
|
+
const ensureDomain = (domainId) => {
|
|
352
|
+
const existing = options.overlay.get(domainId);
|
|
353
|
+
if (existing !== undefined) {
|
|
354
|
+
return existing;
|
|
355
|
+
}
|
|
356
|
+
const confirmed = cloneOverlayDomainFromConfirmed(options.readContext, domainId);
|
|
357
|
+
if (confirmed === null) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
options.overlay.set(domainId, confirmed);
|
|
361
|
+
return confirmed;
|
|
362
|
+
};
|
|
363
|
+
if (options.operation.kind === "domain-reg") {
|
|
364
|
+
if (!rootDomain(options.operation.name)) {
|
|
365
|
+
return { nextDomainId: options.nextDomainId, indeterminate: true };
|
|
366
|
+
}
|
|
367
|
+
if (lookupDomain(options.readContext.snapshot.state, options.operation.name) !== null) {
|
|
368
|
+
return { nextDomainId: options.nextDomainId, indeterminate: true };
|
|
369
|
+
}
|
|
370
|
+
options.overlay.set(options.nextDomainId, {
|
|
371
|
+
domainId: options.nextDomainId,
|
|
372
|
+
name: options.operation.name,
|
|
373
|
+
anchored: false,
|
|
374
|
+
ownerScriptHex: options.operation.senderScriptHex,
|
|
375
|
+
delegateScriptHex: null,
|
|
376
|
+
minerScriptHex: null,
|
|
377
|
+
});
|
|
378
|
+
return {
|
|
379
|
+
nextDomainId: options.nextDomainId + 1,
|
|
380
|
+
indeterminate: false,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const domain = ensureDomain(options.operation.domainId);
|
|
384
|
+
if (domain === null) {
|
|
385
|
+
return { nextDomainId: options.nextDomainId, indeterminate: true };
|
|
386
|
+
}
|
|
387
|
+
if (options.operation.kind === "domain-transfer") {
|
|
388
|
+
domain.ownerScriptHex = options.operation.recipientScriptHex;
|
|
389
|
+
options.overlay.set(domain.domainId, domain);
|
|
390
|
+
return { nextDomainId: options.nextDomainId, indeterminate: false };
|
|
391
|
+
}
|
|
392
|
+
if (options.operation.kind === "domain-anchor") {
|
|
393
|
+
domain.anchored = true;
|
|
394
|
+
if (options.operation.senderScriptHex !== null) {
|
|
395
|
+
domain.ownerScriptHex = options.operation.senderScriptHex;
|
|
396
|
+
}
|
|
397
|
+
options.overlay.set(domain.domainId, domain);
|
|
398
|
+
return { nextDomainId: options.nextDomainId, indeterminate: false };
|
|
399
|
+
}
|
|
400
|
+
if (options.operation.kind === "set-delegate") {
|
|
401
|
+
domain.delegateScriptHex = options.operation.delegateScriptHex;
|
|
402
|
+
options.overlay.set(domain.domainId, domain);
|
|
403
|
+
return { nextDomainId: options.nextDomainId, indeterminate: false };
|
|
404
|
+
}
|
|
405
|
+
domain.minerScriptHex = options.operation.minerScriptHex;
|
|
406
|
+
options.overlay.set(domain.domainId, domain);
|
|
407
|
+
return { nextDomainId: options.nextDomainId, indeterminate: false };
|
|
408
|
+
}
|
|
409
|
+
async function resolveOverlayAuthorizedMiningDomain(options) {
|
|
410
|
+
const orderedAncestors = topologicallyOrderAncestorContexts({
|
|
411
|
+
txid: options.txid,
|
|
412
|
+
txContexts: options.txContexts,
|
|
413
|
+
});
|
|
414
|
+
if (orderedAncestors === null) {
|
|
415
|
+
return "indeterminate";
|
|
416
|
+
}
|
|
417
|
+
const overlay = new Map();
|
|
418
|
+
let nextDomainId = options.readContext.snapshot.state.consensus.nextDomainId;
|
|
419
|
+
for (const ancestor of orderedAncestors) {
|
|
420
|
+
const parsed = parseSupportedAncestorOperation(ancestor);
|
|
421
|
+
if (parsed === "unsupported") {
|
|
422
|
+
return "indeterminate";
|
|
423
|
+
}
|
|
424
|
+
if (parsed === null) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const applied = applySupportedAncestorOperation({
|
|
428
|
+
readContext: options.readContext,
|
|
429
|
+
overlay,
|
|
430
|
+
nextDomainId,
|
|
431
|
+
operation: parsed,
|
|
432
|
+
});
|
|
433
|
+
nextDomainId = applied.nextDomainId;
|
|
434
|
+
if (applied.indeterminate) {
|
|
435
|
+
return "indeterminate";
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const domain = overlay.get(options.domainId) ?? cloneOverlayDomainFromConfirmed(options.readContext, options.domainId);
|
|
439
|
+
if (domain === null || domain.name === null || !rootDomain(domain.name) || !domain.anchored) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
const authorized = domain.ownerScriptHex === options.senderScriptHex
|
|
443
|
+
|| domain.delegateScriptHex === options.senderScriptHex
|
|
444
|
+
|| domain.minerScriptHex === options.senderScriptHex;
|
|
445
|
+
return authorized ? domain : null;
|
|
446
|
+
}
|
|
447
|
+
function buildStatusSnapshot(view, overrides = {}) {
|
|
448
|
+
return {
|
|
449
|
+
...view.runtime,
|
|
450
|
+
runMode: overrides.runMode ?? view.runtime.runMode,
|
|
451
|
+
backgroundWorkerPid: overrides.backgroundWorkerPid ?? view.runtime.backgroundWorkerPid,
|
|
452
|
+
backgroundWorkerRunId: overrides.backgroundWorkerRunId ?? view.runtime.backgroundWorkerRunId,
|
|
453
|
+
backgroundWorkerHeartbeatAtUnixMs: overrides.backgroundWorkerHeartbeatAtUnixMs ?? view.runtime.backgroundWorkerHeartbeatAtUnixMs,
|
|
454
|
+
currentPhase: overrides.currentPhase ?? view.runtime.currentPhase,
|
|
455
|
+
lastSuspendDetectedAtUnixMs: overrides.lastSuspendDetectedAtUnixMs ?? view.runtime.lastSuspendDetectedAtUnixMs,
|
|
456
|
+
providerState: overrides.providerState ?? view.runtime.providerState,
|
|
457
|
+
corePublishState: overrides.corePublishState ?? view.runtime.corePublishState,
|
|
458
|
+
currentPublishDecision: overrides.currentPublishDecision ?? view.runtime.currentPublishDecision,
|
|
459
|
+
sameDomainCompetitorSuppressed: overrides.sameDomainCompetitorSuppressed ?? view.runtime.sameDomainCompetitorSuppressed,
|
|
460
|
+
higherRankedCompetitorDomainCount: overrides.higherRankedCompetitorDomainCount ?? view.runtime.higherRankedCompetitorDomainCount,
|
|
461
|
+
dedupedCompetitorDomainCount: overrides.dedupedCompetitorDomainCount ?? view.runtime.dedupedCompetitorDomainCount,
|
|
462
|
+
competitivenessGateIndeterminate: overrides.competitivenessGateIndeterminate ?? view.runtime.competitivenessGateIndeterminate,
|
|
463
|
+
mempoolSequenceCacheStatus: overrides.mempoolSequenceCacheStatus ?? view.runtime.mempoolSequenceCacheStatus,
|
|
464
|
+
lastMempoolSequence: overrides.lastMempoolSequence ?? view.runtime.lastMempoolSequence,
|
|
465
|
+
lastCompetitivenessGateAtUnixMs: overrides.lastCompetitivenessGateAtUnixMs ?? view.runtime.lastCompetitivenessGateAtUnixMs,
|
|
466
|
+
lastError: overrides.lastError ?? view.runtime.lastError,
|
|
467
|
+
note: overrides.note ?? view.runtime.note,
|
|
468
|
+
liveMiningFamilyInMempool: overrides.liveMiningFamilyInMempool ?? view.runtime.liveMiningFamilyInMempool,
|
|
469
|
+
updatedAtUnixMs: Date.now(),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
async function refreshAndSaveStatus(options) {
|
|
473
|
+
const view = await inspectMiningControlPlane({
|
|
474
|
+
provider: options.provider,
|
|
475
|
+
localState: options.readContext.localState,
|
|
476
|
+
bitcoind: options.readContext.bitcoind,
|
|
477
|
+
nodeStatus: options.readContext.nodeStatus,
|
|
478
|
+
nodeHealth: options.readContext.nodeHealth,
|
|
479
|
+
indexer: options.readContext.indexer,
|
|
480
|
+
paths: options.paths,
|
|
481
|
+
});
|
|
482
|
+
const snapshot = buildStatusSnapshot(view, options.overrides);
|
|
483
|
+
await saveMiningRuntimeStatus(options.paths.miningStatusPath, snapshot);
|
|
484
|
+
options.visualizer?.update(snapshot);
|
|
485
|
+
return snapshot;
|
|
486
|
+
}
|
|
487
|
+
async function appendEvent(paths, event) {
|
|
488
|
+
await appendMiningEvent(paths.miningEventsPath, event);
|
|
489
|
+
}
|
|
490
|
+
async function handleDetectedMiningRuntimeResume(options) {
|
|
491
|
+
const readContext = await options.openReadContext({
|
|
492
|
+
dataDir: options.dataDir,
|
|
493
|
+
databasePath: options.databasePath,
|
|
494
|
+
secretProvider: options.provider,
|
|
495
|
+
paths: options.paths,
|
|
496
|
+
});
|
|
497
|
+
try {
|
|
498
|
+
clearMiningGateCache(readContext.localState.walletRootId);
|
|
499
|
+
await refreshAndSaveStatus({
|
|
500
|
+
paths: options.paths,
|
|
501
|
+
provider: options.provider,
|
|
502
|
+
readContext,
|
|
503
|
+
overrides: {
|
|
504
|
+
runMode: options.runMode,
|
|
505
|
+
backgroundWorkerPid: options.backgroundWorkerPid,
|
|
506
|
+
backgroundWorkerRunId: options.backgroundWorkerRunId,
|
|
507
|
+
backgroundWorkerHeartbeatAtUnixMs: options.runMode === "background" ? Date.now() : null,
|
|
508
|
+
currentPhase: "resuming",
|
|
509
|
+
lastSuspendDetectedAtUnixMs: options.detectedAtUnixMs,
|
|
510
|
+
note: "Mining discarded stale in-flight work after a large local runtime gap and is rechecking health.",
|
|
511
|
+
},
|
|
512
|
+
visualizer: options.visualizer,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
finally {
|
|
516
|
+
await readContext.close();
|
|
517
|
+
}
|
|
518
|
+
await appendEvent(options.paths, createEvent("system-resumed", "Detected a large local runtime gap, discarded stale in-flight mining work, and resumed health checks from scratch.", {
|
|
519
|
+
level: "warn",
|
|
520
|
+
runId: options.backgroundWorkerRunId,
|
|
521
|
+
timestampUnixMs: options.detectedAtUnixMs,
|
|
522
|
+
}));
|
|
523
|
+
}
|
|
524
|
+
function getIndexerTruthKey(readContext) {
|
|
525
|
+
if (readContext.snapshot.daemonInstanceId == null
|
|
526
|
+
|| readContext.snapshot.snapshotSeq == null) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
walletRootId: readContext.localState.state.walletRootId,
|
|
531
|
+
daemonInstanceId: readContext.snapshot.daemonInstanceId,
|
|
532
|
+
snapshotSeq: readContext.snapshot.snapshotSeq,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
async function indexerTruthIsCurrent(options) {
|
|
536
|
+
if (options.truthKey === null) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
const probe = await probeIndexerDaemon({
|
|
540
|
+
dataDir: options.dataDir,
|
|
541
|
+
walletRootId: options.truthKey.walletRootId,
|
|
542
|
+
});
|
|
543
|
+
try {
|
|
544
|
+
return probe.compatibility === "compatible"
|
|
545
|
+
&& probe.status !== null
|
|
546
|
+
&& probe.status.state === "synced"
|
|
547
|
+
&& probe.status.daemonInstanceId === options.truthKey.daemonInstanceId
|
|
548
|
+
&& probe.status.snapshotSeq === options.truthKey.snapshotSeq;
|
|
549
|
+
}
|
|
550
|
+
finally {
|
|
551
|
+
await probe.client?.close().catch(() => undefined);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async function ensureIndexerTruthIsCurrent(options) {
|
|
555
|
+
if (!await indexerTruthIsCurrent(options)) {
|
|
556
|
+
throw new Error("mining_generation_stale_indexer_truth");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function determineCorePublishState(info) {
|
|
560
|
+
if (info.network.networkactive === false) {
|
|
561
|
+
return "network-inactive";
|
|
562
|
+
}
|
|
563
|
+
if ((info.network.connections_out ?? 0) <= 0) {
|
|
564
|
+
return "no-outbound-peers";
|
|
565
|
+
}
|
|
566
|
+
if (info.blockchain.initialblockdownload === true) {
|
|
567
|
+
return "ibd";
|
|
568
|
+
}
|
|
569
|
+
if (info.mempool.loaded === false) {
|
|
570
|
+
return "mempool-loading";
|
|
571
|
+
}
|
|
572
|
+
return "healthy";
|
|
573
|
+
}
|
|
574
|
+
function createMiningPlan(options) {
|
|
575
|
+
const fundingUtxos = options.allUtxos.filter((entry) => entry.scriptPubKey === options.state.funding.scriptPubKeyHex
|
|
576
|
+
&& entry.confirmations >= 1
|
|
577
|
+
&& entry.spendable !== false
|
|
578
|
+
&& entry.safe !== false
|
|
579
|
+
&& !(entry.txid === options.conflictOutpoint.txid && entry.vout === options.conflictOutpoint.vout));
|
|
580
|
+
const opReturnData = serializeMine(options.candidate.domainId, options.candidate.referencedBlockHashInternal, options.candidate.encodedSentenceBytes).opReturnData;
|
|
581
|
+
const expectedOpReturnScriptHex = Buffer.concat([
|
|
582
|
+
Buffer.from([0x6a, opReturnData.length]),
|
|
583
|
+
Buffer.from(opReturnData),
|
|
584
|
+
]).toString("hex");
|
|
585
|
+
return {
|
|
586
|
+
sender: options.candidate.sender,
|
|
587
|
+
inputs: [
|
|
588
|
+
options.candidate.anchorOutpoint,
|
|
589
|
+
options.conflictOutpoint,
|
|
590
|
+
...fundingUtxos.map((entry) => ({ txid: entry.txid, vout: entry.vout })),
|
|
591
|
+
],
|
|
592
|
+
outputs: [
|
|
593
|
+
{ data: Buffer.from(opReturnData).toString("hex") },
|
|
594
|
+
{ [options.candidate.sender.address]: satsToBtc(BigInt(options.state.anchorValueSats)) },
|
|
595
|
+
],
|
|
596
|
+
changeAddress: options.state.funding.address,
|
|
597
|
+
changePosition: 2,
|
|
598
|
+
expectedOpReturnScriptHex,
|
|
599
|
+
expectedAnchorScriptHex: options.candidate.sender.scriptPubKeyHex,
|
|
600
|
+
expectedAnchorValueSats: BigInt(options.state.anchorValueSats),
|
|
601
|
+
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
602
|
+
expectedConflictOutpoint: options.conflictOutpoint,
|
|
603
|
+
feeRateSatVb: options.feeRateSatVb,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function validateMiningDraft(decoded, funded, plan) {
|
|
607
|
+
const inputs = decoded.tx.vin;
|
|
608
|
+
const outputs = decoded.tx.vout;
|
|
609
|
+
if (inputs.length < 2) {
|
|
610
|
+
throw new Error("wallet_mining_missing_inputs");
|
|
611
|
+
}
|
|
612
|
+
if (inputs[0]?.prevout?.scriptPubKey?.hex !== plan.sender.scriptPubKeyHex) {
|
|
613
|
+
throw new Error("wallet_mining_sender_input_mismatch");
|
|
614
|
+
}
|
|
615
|
+
if (inputs[1]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex) {
|
|
616
|
+
throw new Error("wallet_mining_conflict_input_mismatch");
|
|
617
|
+
}
|
|
618
|
+
for (let index = 2; index < inputs.length; index += 1) {
|
|
619
|
+
if (inputs[index]?.prevout?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex) {
|
|
620
|
+
throw new Error("wallet_mining_unexpected_funding_input");
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
624
|
+
throw new Error("wallet_mining_opreturn_mismatch");
|
|
625
|
+
}
|
|
626
|
+
if (outputs[1]?.scriptPubKey?.hex !== plan.expectedAnchorScriptHex) {
|
|
627
|
+
throw new Error("wallet_mining_anchor_output_mismatch");
|
|
628
|
+
}
|
|
629
|
+
if (numberToSats(outputs[1]?.value ?? 0) !== plan.expectedAnchorValueSats) {
|
|
630
|
+
throw new Error("wallet_mining_anchor_value_mismatch");
|
|
631
|
+
}
|
|
632
|
+
if (funded.changepos !== -1 && (funded.changepos !== plan.changePosition || outputs[funded.changepos]?.scriptPubKey?.hex !== plan.allowedFundingScriptPubKeyHex)) {
|
|
633
|
+
throw new Error("wallet_mining_change_output_mismatch");
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async function buildMiningTransaction(options) {
|
|
637
|
+
return buildWalletMutationTransaction({
|
|
638
|
+
rpc: options.rpc,
|
|
639
|
+
walletName: options.walletName,
|
|
640
|
+
plan: options.plan,
|
|
641
|
+
validateFundedDraft: validateMiningDraft,
|
|
642
|
+
finalizeErrorCode: "wallet_mining_finalize_failed",
|
|
643
|
+
mempoolRejectPrefix: "wallet_mining_mempool_rejected",
|
|
644
|
+
feeRate: options.plan.feeRateSatVb,
|
|
645
|
+
builderOptions: {
|
|
646
|
+
addInputs: true,
|
|
647
|
+
includeUnsafe: true,
|
|
648
|
+
minConf: 0,
|
|
649
|
+
lockUnspents: true,
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
function resolveEligibleAnchoredRoots(context) {
|
|
654
|
+
const state = context.localState.state;
|
|
655
|
+
const model = context.model;
|
|
656
|
+
const snapshot = context.snapshot;
|
|
657
|
+
if (state === null || model === null || snapshot === null) {
|
|
658
|
+
return [];
|
|
659
|
+
}
|
|
660
|
+
const domains = [];
|
|
661
|
+
for (const domain of model.domains) {
|
|
662
|
+
if (!isMineableWalletDomain(context, domain)) {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
const localRecord = state.domains.find((entry) => entry.name === domain.name);
|
|
666
|
+
const ownerIdentity = model.identities.find((identity) => identity.index === domain.ownerLocalIndex);
|
|
667
|
+
const domainId = domain.domainId;
|
|
668
|
+
if (domainId === null
|
|
669
|
+
|| domainId === undefined
|
|
670
|
+
|| localRecord?.currentCanonicalAnchorOutpoint === null
|
|
671
|
+
|| localRecord?.currentCanonicalAnchorOutpoint === undefined
|
|
672
|
+
|| ownerIdentity?.address == null
|
|
673
|
+
|| ownerIdentity.readOnly) {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
const chainDomain = lookupDomain(snapshot.state, domain.name);
|
|
677
|
+
if (chainDomain === null || !chainDomain.anchored) {
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
domains.push({
|
|
681
|
+
domainId,
|
|
682
|
+
domainName: domain.name,
|
|
683
|
+
localIndex: ownerIdentity.index,
|
|
684
|
+
sender: {
|
|
685
|
+
localIndex: ownerIdentity.index,
|
|
686
|
+
scriptPubKeyHex: ownerIdentity.scriptPubKeyHex,
|
|
687
|
+
address: ownerIdentity.address,
|
|
688
|
+
},
|
|
689
|
+
anchorOutpoint: {
|
|
690
|
+
txid: localRecord.currentCanonicalAnchorOutpoint.txid,
|
|
691
|
+
vout: localRecord.currentCanonicalAnchorOutpoint.vout,
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
return domains.sort((left, right) => left.domainId - right.domainId || left.domainName.localeCompare(right.domainName));
|
|
696
|
+
}
|
|
697
|
+
async function persistCustomHookRuntimeOutcome(options) {
|
|
698
|
+
const hookState = options.readContext.localState.state.hookClientState.mining;
|
|
699
|
+
if (hookState.mode !== "custom") {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
if (options.success) {
|
|
703
|
+
if ((hookState.consecutiveFailureCount ?? 0) === 0 && hookState.cooldownUntilUnixMs === null) {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
options.readContext.localState.state.hookClientState.mining = {
|
|
707
|
+
...hookState,
|
|
708
|
+
consecutiveFailureCount: 0,
|
|
709
|
+
cooldownUntilUnixMs: null,
|
|
710
|
+
};
|
|
711
|
+
await saveWalletStatePreservingUnlock({
|
|
712
|
+
state: options.readContext.localState.state,
|
|
713
|
+
provider: options.provider,
|
|
714
|
+
unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
|
|
715
|
+
nowUnixMs: options.nowUnixMs,
|
|
716
|
+
paths: options.paths,
|
|
717
|
+
});
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
const consecutiveFailureCount = (hookState.consecutiveFailureCount ?? 0) + 1;
|
|
721
|
+
const cooldownUntilUnixMs = consecutiveFailureCount >= MINING_HOOK_FAILURE_THRESHOLD
|
|
722
|
+
? options.nowUnixMs + MINING_HOOK_COOLDOWN_MS
|
|
723
|
+
: null;
|
|
724
|
+
options.readContext.localState.state.hookClientState.mining = {
|
|
725
|
+
...hookState,
|
|
726
|
+
consecutiveFailureCount,
|
|
727
|
+
cooldownUntilUnixMs,
|
|
728
|
+
};
|
|
729
|
+
await saveWalletStatePreservingUnlock({
|
|
730
|
+
state: options.readContext.localState.state,
|
|
731
|
+
provider: options.provider,
|
|
732
|
+
unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
|
|
733
|
+
nowUnixMs: options.nowUnixMs,
|
|
734
|
+
paths: options.paths,
|
|
735
|
+
});
|
|
736
|
+
return cooldownUntilUnixMs !== null && cooldownUntilUnixMs > options.nowUnixMs;
|
|
737
|
+
}
|
|
738
|
+
async function generateCandidatesForDomains(options) {
|
|
739
|
+
const bestBlockHash = options.readContext.nodeStatus?.nodeBestHashHex;
|
|
740
|
+
if (bestBlockHash === null || bestBlockHash === undefined) {
|
|
741
|
+
return [];
|
|
742
|
+
}
|
|
743
|
+
const targetBlockHeight = (options.readContext.nodeStatus?.nodeBestHeight ?? 0) + 1;
|
|
744
|
+
const referencedBlockHashInternal = Buffer.from(displayToInternalBlockhash(bestBlockHash), "hex");
|
|
745
|
+
const rootDomains = options.domains.map((domain) => ({
|
|
746
|
+
...domain,
|
|
747
|
+
requiredWords: getWords(domain.domainId, referencedBlockHashInternal),
|
|
748
|
+
}));
|
|
749
|
+
const abortController = new AbortController();
|
|
750
|
+
let stale = false;
|
|
751
|
+
let staleIndexerTruth = false;
|
|
752
|
+
let preempted = false;
|
|
753
|
+
const timer = setInterval(async () => {
|
|
754
|
+
try {
|
|
755
|
+
const [current, truthCurrent] = await Promise.all([
|
|
756
|
+
options.rpc.getBlockchainInfo(),
|
|
757
|
+
indexerTruthIsCurrent({
|
|
758
|
+
dataDir: options.readContext.dataDir,
|
|
759
|
+
truthKey: options.indexerTruthKey,
|
|
760
|
+
}),
|
|
761
|
+
]);
|
|
762
|
+
if (current.bestblockhash !== bestBlockHash) {
|
|
763
|
+
stale = true;
|
|
764
|
+
abortController.abort();
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (!truthCurrent) {
|
|
768
|
+
staleIndexerTruth = true;
|
|
769
|
+
abortController.abort();
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (await isMiningGenerationAbortRequested(options.paths)) {
|
|
773
|
+
preempted = true;
|
|
774
|
+
abortController.abort();
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
// Ignore transient polling failures and let the main cycle degrade on the next tick.
|
|
779
|
+
}
|
|
780
|
+
}, BEST_BLOCK_POLL_INTERVAL_MS);
|
|
781
|
+
try {
|
|
782
|
+
await markMiningGenerationActive({
|
|
783
|
+
paths: options.paths,
|
|
784
|
+
runId: options.runId ?? null,
|
|
785
|
+
pid: process.pid ?? null,
|
|
786
|
+
});
|
|
787
|
+
const generationRequest = {
|
|
788
|
+
schemaVersion: 1,
|
|
789
|
+
requestId: `mining-${targetBlockHeight}-${randomBytes(8).toString("hex")}`,
|
|
790
|
+
targetBlockHeight,
|
|
791
|
+
referencedBlockHashDisplay: bestBlockHash,
|
|
792
|
+
generatedAtUnixMs: Date.now(),
|
|
793
|
+
extraPrompt: null,
|
|
794
|
+
limits: createGenerateSentencesHookLimits(),
|
|
795
|
+
rootDomains: rootDomains.map((domain) => ({
|
|
796
|
+
domainId: domain.domainId,
|
|
797
|
+
domainName: domain.domainName,
|
|
798
|
+
requiredWords: domain.requiredWords,
|
|
799
|
+
})),
|
|
800
|
+
};
|
|
801
|
+
let generated;
|
|
802
|
+
try {
|
|
803
|
+
generated = await generateMiningSentences(generationRequest, {
|
|
804
|
+
paths: options.paths,
|
|
805
|
+
provider: options.provider,
|
|
806
|
+
hookState: options.readContext.localState.state.hookClientState.mining,
|
|
807
|
+
signal: abortController.signal,
|
|
808
|
+
fetchImpl: options.fetchImpl,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
if (stale) {
|
|
813
|
+
throw new Error("mining_generation_stale_tip");
|
|
814
|
+
}
|
|
815
|
+
if (staleIndexerTruth) {
|
|
816
|
+
throw new Error("mining_generation_stale_indexer_truth");
|
|
817
|
+
}
|
|
818
|
+
if (preempted) {
|
|
819
|
+
throw new Error("mining_generation_preempted");
|
|
820
|
+
}
|
|
821
|
+
throw error;
|
|
822
|
+
}
|
|
823
|
+
if (stale) {
|
|
824
|
+
throw new Error("mining_generation_stale_tip");
|
|
825
|
+
}
|
|
826
|
+
if (staleIndexerTruth) {
|
|
827
|
+
throw new Error("mining_generation_stale_indexer_truth");
|
|
828
|
+
}
|
|
829
|
+
if (preempted) {
|
|
830
|
+
throw new Error("mining_generation_preempted");
|
|
831
|
+
}
|
|
832
|
+
await ensureIndexerTruthIsCurrent({
|
|
833
|
+
dataDir: options.readContext.dataDir,
|
|
834
|
+
truthKey: options.indexerTruthKey,
|
|
835
|
+
});
|
|
836
|
+
if (generated.hookMode === "custom") {
|
|
837
|
+
await persistCustomHookRuntimeOutcome({
|
|
838
|
+
readContext: options.readContext,
|
|
839
|
+
provider: options.provider,
|
|
840
|
+
paths: options.paths,
|
|
841
|
+
nowUnixMs: Date.now(),
|
|
842
|
+
success: true,
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
const sentencesByDomain = new Map();
|
|
846
|
+
for (const candidate of generated.candidates) {
|
|
847
|
+
const existing = sentencesByDomain.get(candidate.domainId) ?? [];
|
|
848
|
+
existing.push(candidate.sentence);
|
|
849
|
+
sentencesByDomain.set(candidate.domainId, existing);
|
|
850
|
+
}
|
|
851
|
+
const candidates = [];
|
|
852
|
+
for (const domain of rootDomains) {
|
|
853
|
+
const domainSentences = sentencesByDomain.get(domain.domainId) ?? [];
|
|
854
|
+
if (domainSentences.length === 0) {
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
const assayed = await assaySentences(domain.domainId, referencedBlockHashInternal, domainSentences);
|
|
858
|
+
const best = assayed.find((entry) => entry.gatesPass && entry.encodedSentenceBytes !== null && entry.rank === 1);
|
|
859
|
+
if (best === undefined || best.encodedSentenceBytes === null || best.canonicalBlend === null) {
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
candidates.push({
|
|
863
|
+
domainId: domain.domainId,
|
|
864
|
+
domainName: domain.domainName,
|
|
865
|
+
localIndex: domain.localIndex,
|
|
866
|
+
sender: domain.sender,
|
|
867
|
+
anchorOutpoint: domain.anchorOutpoint,
|
|
868
|
+
sentence: best.sentence,
|
|
869
|
+
encodedSentenceBytes: best.encodedSentenceBytes,
|
|
870
|
+
bip39WordIndices: [...best.bip39WordIndices],
|
|
871
|
+
bip39Words: best.bip39Words,
|
|
872
|
+
canonicalBlend: best.canonicalBlend,
|
|
873
|
+
referencedBlockHashDisplay: bestBlockHash,
|
|
874
|
+
referencedBlockHashInternal,
|
|
875
|
+
targetBlockHeight,
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
return candidates;
|
|
879
|
+
}
|
|
880
|
+
finally {
|
|
881
|
+
clearInterval(timer);
|
|
882
|
+
await markMiningGenerationInactive({
|
|
883
|
+
paths: options.paths,
|
|
884
|
+
runId: options.runId ?? null,
|
|
885
|
+
pid: process.pid ?? null,
|
|
886
|
+
}).catch(() => undefined);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
async function chooseBestLocalCandidate(candidates) {
|
|
890
|
+
if (candidates.length === 0) {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
if (candidates.length === 1) {
|
|
894
|
+
return candidates[0];
|
|
895
|
+
}
|
|
896
|
+
const blendSeed = deriveBlendSeed(candidates[0].referencedBlockHashInternal);
|
|
897
|
+
const winners = await settleBlock({
|
|
898
|
+
blendSeed,
|
|
899
|
+
blockRewardCogtoshi: 100n,
|
|
900
|
+
submissions: candidates
|
|
901
|
+
.slice()
|
|
902
|
+
.sort((left, right) => left.domainId - right.domainId || left.domainName.localeCompare(right.domainName))
|
|
903
|
+
.map((candidate, index) => ({
|
|
904
|
+
miningDomainId: candidate.domainId,
|
|
905
|
+
rawSentenceBytes: candidate.encodedSentenceBytes,
|
|
906
|
+
recipientScriptPubKey: Buffer.from(candidate.sender.scriptPubKeyHex, "hex"),
|
|
907
|
+
bip39WordIndices: candidate.bip39WordIndices,
|
|
908
|
+
txIndex: index,
|
|
909
|
+
})),
|
|
910
|
+
});
|
|
911
|
+
const winner = winners[0];
|
|
912
|
+
if (winner === undefined) {
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
return candidates.find((candidate) => candidate.domainId === winner.miningDomainId) ?? null;
|
|
916
|
+
}
|
|
917
|
+
async function runCompetitivenessGate(options) {
|
|
918
|
+
const createDecision = (overrides) => ({
|
|
919
|
+
allowed: overrides.allowed ?? false,
|
|
920
|
+
decision: overrides.decision ?? "indeterminate-mempool-gate",
|
|
921
|
+
sameDomainCompetitorSuppressed: overrides.sameDomainCompetitorSuppressed ?? false,
|
|
922
|
+
higherRankedCompetitorDomainCount: overrides.higherRankedCompetitorDomainCount ?? 0,
|
|
923
|
+
dedupedCompetitorDomainCount: overrides.dedupedCompetitorDomainCount ?? 0,
|
|
924
|
+
competitivenessGateIndeterminate: overrides.competitivenessGateIndeterminate ?? false,
|
|
925
|
+
mempoolSequenceCacheStatus: overrides.mempoolSequenceCacheStatus ?? null,
|
|
926
|
+
lastMempoolSequence: overrides.lastMempoolSequence ?? null,
|
|
927
|
+
});
|
|
928
|
+
const walletRootId = options.readContext.localState.walletRootId ?? "uninitialized-wallet-root";
|
|
929
|
+
const indexerTruthKey = getIndexerTruthKey(options.readContext);
|
|
930
|
+
const localFeeTarget = DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB;
|
|
931
|
+
const excludedTxids = [options.currentTxid].filter((value) => value !== null).sort();
|
|
932
|
+
const localAssayTupleKey = [
|
|
933
|
+
options.candidate.domainId,
|
|
934
|
+
Buffer.from(options.candidate.encodedSentenceBytes).toString("hex"),
|
|
935
|
+
options.candidate.canonicalBlend.toString(),
|
|
936
|
+
options.candidate.sender.scriptPubKeyHex,
|
|
937
|
+
].join(":");
|
|
938
|
+
let mempoolVerbose;
|
|
939
|
+
try {
|
|
940
|
+
mempoolVerbose = await options.rpc.getRawMempoolVerbose();
|
|
941
|
+
}
|
|
942
|
+
catch {
|
|
943
|
+
return createDecision({
|
|
944
|
+
competitivenessGateIndeterminate: true,
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
const mempoolSequence = String(mempoolVerbose.mempool_sequence);
|
|
948
|
+
const cached = miningGateCache.get(walletRootId);
|
|
949
|
+
const cachedTruthMatches = cached !== undefined
|
|
950
|
+
&& indexerTruthKey !== null
|
|
951
|
+
&& cached.indexerDaemonInstanceId === indexerTruthKey.daemonInstanceId
|
|
952
|
+
&& cached.indexerSnapshotSeq === indexerTruthKey.snapshotSeq;
|
|
953
|
+
const cachedReferencedBlockMatches = cached !== undefined
|
|
954
|
+
&& cached.referencedBlockHashDisplay === options.candidate.referencedBlockHashDisplay;
|
|
955
|
+
if (cached !== undefined && (!cachedTruthMatches || !cachedReferencedBlockMatches)) {
|
|
956
|
+
clearMiningGateCache(walletRootId);
|
|
957
|
+
}
|
|
958
|
+
if (cached !== undefined
|
|
959
|
+
&& cachedTruthMatches
|
|
960
|
+
&& cachedReferencedBlockMatches
|
|
961
|
+
&& cached.localAssayTupleKey === localAssayTupleKey
|
|
962
|
+
&& cached.currentFeeTargetSatVb === localFeeTarget
|
|
963
|
+
&& cached.excludedTxidsKey === excludedTxids.join(",")
|
|
964
|
+
&& cached.mempoolSequence === mempoolSequence) {
|
|
965
|
+
return {
|
|
966
|
+
...cached.decision,
|
|
967
|
+
mempoolSequenceCacheStatus: "reused",
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
const referencedPrefix = Buffer.from(options.candidate.referencedBlockHashInternal.subarray(0, 4)).toString("hex");
|
|
971
|
+
const visibleTxids = mempoolVerbose.txids.filter((txid) => !excludedTxids.includes(txid));
|
|
972
|
+
const txContexts = cachedTruthMatches && cachedReferencedBlockMatches
|
|
973
|
+
? (cached?.txContexts ?? new Map())
|
|
974
|
+
: new Map();
|
|
975
|
+
for (const txid of [...txContexts.keys()]) {
|
|
976
|
+
if (!visibleTxids.includes(txid)) {
|
|
977
|
+
txContexts.delete(txid);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
for (const txid of visibleTxids) {
|
|
981
|
+
if (txContexts.has(txid)) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
const [tx, mempoolEntry] = await Promise.all([
|
|
985
|
+
options.rpc.getRawTransaction(txid, true).catch(() => null),
|
|
986
|
+
options.rpc.getMempoolEntry(txid).catch(() => null),
|
|
987
|
+
]);
|
|
988
|
+
if (tx === null || mempoolEntry === null) {
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
const effectiveFeeRate = Number([
|
|
992
|
+
mempoolEntry.vsize > 0 ? (numberToSats(mempoolEntry.fees.base) / BigInt(mempoolEntry.vsize)) : 0n,
|
|
993
|
+
(mempoolEntry.ancestorsize ?? 0) > 0 ? (numberToSats(mempoolEntry.fees.ancestor) / BigInt(mempoolEntry.ancestorsize ?? 1)) : 0n,
|
|
994
|
+
(mempoolEntry.descendantsize ?? 0) > 0 ? (numberToSats(mempoolEntry.fees.descendant) / BigInt(mempoolEntry.descendantsize ?? 1)) : 0n,
|
|
995
|
+
].reduce((best, candidate) => (candidate > best ? candidate : best), 0n));
|
|
996
|
+
const payloadHex = tx.vout.find((entry) => entry.scriptPubKey?.hex?.startsWith("6a") === true)?.scriptPubKey?.hex;
|
|
997
|
+
txContexts.set(txid, {
|
|
998
|
+
txid,
|
|
999
|
+
effectiveFeeRate,
|
|
1000
|
+
senderScriptHex: tx.vin[0]?.prevout?.scriptPubKey?.hex ?? null,
|
|
1001
|
+
rawTransaction: tx,
|
|
1002
|
+
payload: payloadHex === undefined ? null : extractOpReturnPayloadFromScriptHex(payloadHex),
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
const entries = new Map();
|
|
1006
|
+
for (const txid of visibleTxids) {
|
|
1007
|
+
const context = txContexts.get(txid);
|
|
1008
|
+
if (context === undefined || context.effectiveFeeRate < localFeeTarget || context.payload === null || context.senderScriptHex === null) {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
const decoded = decodeMinePayload(context.payload);
|
|
1012
|
+
if (decoded === null || decoded.referencedBlockPrefixHex !== referencedPrefix) {
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
const overlayDomain = await resolveOverlayAuthorizedMiningDomain({
|
|
1016
|
+
readContext: options.readContext,
|
|
1017
|
+
txid,
|
|
1018
|
+
txContexts,
|
|
1019
|
+
domainId: decoded.domainId,
|
|
1020
|
+
senderScriptHex: context.senderScriptHex,
|
|
1021
|
+
});
|
|
1022
|
+
if (overlayDomain === "indeterminate") {
|
|
1023
|
+
const decision = createDecision({
|
|
1024
|
+
competitivenessGateIndeterminate: true,
|
|
1025
|
+
decision: "indeterminate-mempool-gate",
|
|
1026
|
+
mempoolSequenceCacheStatus: "refreshed",
|
|
1027
|
+
lastMempoolSequence: mempoolSequence,
|
|
1028
|
+
});
|
|
1029
|
+
miningGateCache.set(walletRootId, {
|
|
1030
|
+
indexerDaemonInstanceId: indexerTruthKey?.daemonInstanceId ?? "none",
|
|
1031
|
+
indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
|
|
1032
|
+
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
1033
|
+
localAssayTupleKey,
|
|
1034
|
+
currentFeeTargetSatVb: localFeeTarget,
|
|
1035
|
+
excludedTxidsKey: excludedTxids.join(","),
|
|
1036
|
+
mempoolSequence,
|
|
1037
|
+
txids: [...visibleTxids],
|
|
1038
|
+
txContexts,
|
|
1039
|
+
decision,
|
|
1040
|
+
});
|
|
1041
|
+
return decision;
|
|
1042
|
+
}
|
|
1043
|
+
if (overlayDomain === null || overlayDomain.name === null || !rootDomain(overlayDomain.name)) {
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
const assayed = await assaySentences(decoded.domainId, options.candidate.referencedBlockHashInternal, [Buffer.from(decoded.sentenceBytes).toString("utf8")]).catch(() => []);
|
|
1047
|
+
const scored = assayed[0];
|
|
1048
|
+
if (scored === undefined || !scored.gatesPass || scored.encodedSentenceBytes === null || scored.canonicalBlend === null) {
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
entries.set(txid, {
|
|
1052
|
+
txid,
|
|
1053
|
+
effectiveFeeRate: context.effectiveFeeRate,
|
|
1054
|
+
domainId: decoded.domainId,
|
|
1055
|
+
senderScriptHex: context.senderScriptHex,
|
|
1056
|
+
encodedSentenceBytesHex: Buffer.from(scored.encodedSentenceBytes).toString("hex"),
|
|
1057
|
+
bip39WordIndices: [...scored.bip39WordIndices],
|
|
1058
|
+
canonicalBlend: scored.canonicalBlend,
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
const sameDomainCompetitors = [...entries.values()].filter((entry) => entry.domainId === options.candidate.domainId);
|
|
1062
|
+
const sameDomainCompetitorSuppressed = sameDomainCompetitors.some((competitor) => competitor.canonicalBlend > options.candidate.canonicalBlend
|
|
1063
|
+
|| competitor.canonicalBlend === options.candidate.canonicalBlend);
|
|
1064
|
+
let decision;
|
|
1065
|
+
const otherDomainBest = new Map();
|
|
1066
|
+
for (const entry of entries.values()) {
|
|
1067
|
+
if (entry.domainId === options.candidate.domainId) {
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
const best = otherDomainBest.get(entry.domainId);
|
|
1071
|
+
if (best === undefined
|
|
1072
|
+
|| entry.canonicalBlend > best.canonicalBlend
|
|
1073
|
+
|| (entry.canonicalBlend === best.canonicalBlend && entry.effectiveFeeRate > best.effectiveFeeRate)
|
|
1074
|
+
|| (entry.canonicalBlend === best.canonicalBlend && entry.effectiveFeeRate === best.effectiveFeeRate && entry.txid.localeCompare(best.txid) < 0)) {
|
|
1075
|
+
otherDomainBest.set(entry.domainId, entry);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
if (sameDomainCompetitorSuppressed) {
|
|
1079
|
+
decision = createDecision({
|
|
1080
|
+
allowed: false,
|
|
1081
|
+
decision: "suppressed-same-domain-mempool",
|
|
1082
|
+
sameDomainCompetitorSuppressed: true,
|
|
1083
|
+
higherRankedCompetitorDomainCount: 1,
|
|
1084
|
+
dedupedCompetitorDomainCount: otherDomainBest.size,
|
|
1085
|
+
competitivenessGateIndeterminate: false,
|
|
1086
|
+
mempoolSequenceCacheStatus: "refreshed",
|
|
1087
|
+
lastMempoolSequence: mempoolSequence,
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
else {
|
|
1091
|
+
try {
|
|
1092
|
+
const submissions = [
|
|
1093
|
+
{
|
|
1094
|
+
miningDomainId: options.candidate.domainId,
|
|
1095
|
+
rawSentenceBytes: options.candidate.encodedSentenceBytes,
|
|
1096
|
+
recipientScriptPubKey: Buffer.from(options.candidate.sender.scriptPubKeyHex, "hex"),
|
|
1097
|
+
bip39WordIndices: options.candidate.bip39WordIndices,
|
|
1098
|
+
txIndex: 0,
|
|
1099
|
+
},
|
|
1100
|
+
...[...otherDomainBest.values()]
|
|
1101
|
+
.sort((left, right) => left.domainId - right.domainId || left.txid.localeCompare(right.txid))
|
|
1102
|
+
.map((entry, index) => ({
|
|
1103
|
+
miningDomainId: entry.domainId,
|
|
1104
|
+
rawSentenceBytes: Buffer.from(entry.encodedSentenceBytesHex, "hex"),
|
|
1105
|
+
recipientScriptPubKey: Buffer.from(entry.senderScriptHex, "hex"),
|
|
1106
|
+
bip39WordIndices: entry.bip39WordIndices,
|
|
1107
|
+
txIndex: index + 1,
|
|
1108
|
+
})),
|
|
1109
|
+
];
|
|
1110
|
+
const winners = await settleBlock({
|
|
1111
|
+
blendSeed: deriveBlendSeed(options.candidate.referencedBlockHashInternal),
|
|
1112
|
+
blockRewardCogtoshi: 100n,
|
|
1113
|
+
submissions,
|
|
1114
|
+
});
|
|
1115
|
+
const localWinner = winners.find((winner) => winner.miningDomainId === options.candidate.domainId);
|
|
1116
|
+
const higherRankedCompetitorDomainCount = localWinner === undefined
|
|
1117
|
+
? Math.max(0, winners.length - 1)
|
|
1118
|
+
: Math.max(0, localWinner.rank - 1);
|
|
1119
|
+
if (higherRankedCompetitorDomainCount >= 5) {
|
|
1120
|
+
decision = createDecision({
|
|
1121
|
+
allowed: false,
|
|
1122
|
+
decision: "suppressed-top5-mempool",
|
|
1123
|
+
sameDomainCompetitorSuppressed: false,
|
|
1124
|
+
higherRankedCompetitorDomainCount,
|
|
1125
|
+
dedupedCompetitorDomainCount: otherDomainBest.size,
|
|
1126
|
+
competitivenessGateIndeterminate: false,
|
|
1127
|
+
mempoolSequenceCacheStatus: "refreshed",
|
|
1128
|
+
lastMempoolSequence: mempoolSequence,
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
else {
|
|
1132
|
+
decision = createDecision({
|
|
1133
|
+
allowed: true,
|
|
1134
|
+
decision: "publish",
|
|
1135
|
+
sameDomainCompetitorSuppressed: false,
|
|
1136
|
+
higherRankedCompetitorDomainCount,
|
|
1137
|
+
dedupedCompetitorDomainCount: otherDomainBest.size,
|
|
1138
|
+
competitivenessGateIndeterminate: false,
|
|
1139
|
+
mempoolSequenceCacheStatus: "refreshed",
|
|
1140
|
+
lastMempoolSequence: mempoolSequence,
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
catch {
|
|
1145
|
+
decision = createDecision({
|
|
1146
|
+
allowed: false,
|
|
1147
|
+
decision: "indeterminate-mempool-gate",
|
|
1148
|
+
sameDomainCompetitorSuppressed: false,
|
|
1149
|
+
higherRankedCompetitorDomainCount: 0,
|
|
1150
|
+
dedupedCompetitorDomainCount: otherDomainBest.size,
|
|
1151
|
+
competitivenessGateIndeterminate: true,
|
|
1152
|
+
mempoolSequenceCacheStatus: "refreshed",
|
|
1153
|
+
lastMempoolSequence: mempoolSequence,
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
miningGateCache.set(walletRootId, {
|
|
1158
|
+
indexerDaemonInstanceId: indexerTruthKey?.daemonInstanceId ?? "none",
|
|
1159
|
+
indexerSnapshotSeq: indexerTruthKey?.snapshotSeq ?? "none",
|
|
1160
|
+
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
1161
|
+
localAssayTupleKey,
|
|
1162
|
+
currentFeeTargetSatVb: localFeeTarget,
|
|
1163
|
+
excludedTxidsKey: excludedTxids.join(","),
|
|
1164
|
+
mempoolSequence,
|
|
1165
|
+
txids: [...visibleTxids],
|
|
1166
|
+
txContexts,
|
|
1167
|
+
decision,
|
|
1168
|
+
});
|
|
1169
|
+
return decision;
|
|
1170
|
+
}
|
|
1171
|
+
function candidateOutranksLive(options) {
|
|
1172
|
+
const liveState = normalizeMiningStateRecord(options.liveState);
|
|
1173
|
+
const nextSentenceHex = Buffer.from(options.candidate.encodedSentenceBytes).toString("hex");
|
|
1174
|
+
if (liveState.currentEncodedSentenceBytesHex === null) {
|
|
1175
|
+
return true;
|
|
1176
|
+
}
|
|
1177
|
+
if (liveState.currentDomainId === options.candidate.domainId) {
|
|
1178
|
+
if (liveState.currentEncodedSentenceBytesHex === nextSentenceHex) {
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
const currentScore = liveState.currentScore === null ? null : BigInt(liveState.currentScore);
|
|
1182
|
+
return currentScore === null || options.candidate.canonicalBlend > currentScore;
|
|
1183
|
+
}
|
|
1184
|
+
return true;
|
|
1185
|
+
}
|
|
1186
|
+
function candidateMatchesLiveFamily(options) {
|
|
1187
|
+
const liveState = normalizeMiningStateRecord(options.liveState);
|
|
1188
|
+
return liveState.currentDomainId === options.candidate.domainId
|
|
1189
|
+
&& liveState.currentEncodedSentenceBytesHex === Buffer.from(options.candidate.encodedSentenceBytes).toString("hex")
|
|
1190
|
+
&& liveState.currentSenderScriptPubKeyHex === options.candidate.sender.scriptPubKeyHex
|
|
1191
|
+
&& liveState.currentReferencedBlockHashDisplay === options.candidate.referencedBlockHashDisplay
|
|
1192
|
+
&& liveState.currentBlockTargetHeight === options.candidate.targetBlockHeight;
|
|
1193
|
+
}
|
|
1194
|
+
function candidateNeedsFeeMaintenance(options) {
|
|
1195
|
+
const liveState = normalizeMiningStateRecord(options.liveState);
|
|
1196
|
+
return candidateMatchesLiveFamily(options)
|
|
1197
|
+
&& liveState.currentTxid !== null
|
|
1198
|
+
&& liveState.currentFeeRateSatVb !== null
|
|
1199
|
+
&& liveState.currentPublishState === "in-mempool"
|
|
1200
|
+
&& liveState.liveMiningFamilyInMempool === true;
|
|
1201
|
+
}
|
|
1202
|
+
async function candidateWinsAgainstLive(options) {
|
|
1203
|
+
const liveState = normalizeMiningStateRecord(options.liveState);
|
|
1204
|
+
if (liveState.currentDomainId === null || liveState.currentEncodedSentenceBytesHex === null) {
|
|
1205
|
+
return true;
|
|
1206
|
+
}
|
|
1207
|
+
if (liveState.currentDomainId === options.candidate.domainId) {
|
|
1208
|
+
return candidateOutranksLive(options);
|
|
1209
|
+
}
|
|
1210
|
+
if (liveState.currentBip39WordIndices === null || liveState.currentSenderScriptPubKeyHex === null || liveState.currentBlendSeedHex === null) {
|
|
1211
|
+
return true;
|
|
1212
|
+
}
|
|
1213
|
+
const settled = await settleBlock({
|
|
1214
|
+
blendSeed: Buffer.from(liveState.currentBlendSeedHex, "hex"),
|
|
1215
|
+
blockRewardCogtoshi: 100n,
|
|
1216
|
+
submissions: [
|
|
1217
|
+
{
|
|
1218
|
+
miningDomainId: liveState.currentDomainId,
|
|
1219
|
+
rawSentenceBytes: Buffer.from(liveState.currentEncodedSentenceBytesHex, "hex"),
|
|
1220
|
+
recipientScriptPubKey: Buffer.from(liveState.currentSenderScriptPubKeyHex, "hex"),
|
|
1221
|
+
bip39WordIndices: liveState.currentBip39WordIndices,
|
|
1222
|
+
txIndex: 0,
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
miningDomainId: options.candidate.domainId,
|
|
1226
|
+
rawSentenceBytes: options.candidate.encodedSentenceBytes,
|
|
1227
|
+
recipientScriptPubKey: Buffer.from(options.candidate.sender.scriptPubKeyHex, "hex"),
|
|
1228
|
+
bip39WordIndices: options.candidate.bip39WordIndices,
|
|
1229
|
+
txIndex: 1,
|
|
1230
|
+
},
|
|
1231
|
+
],
|
|
1232
|
+
});
|
|
1233
|
+
const incumbent = settled.find((entry) => entry.miningDomainId === liveState.currentDomainId);
|
|
1234
|
+
const challenger = settled.find((entry) => entry.miningDomainId === options.candidate.domainId);
|
|
1235
|
+
return challenger !== undefined
|
|
1236
|
+
&& incumbent !== undefined
|
|
1237
|
+
&& challenger.rank < incumbent.rank;
|
|
1238
|
+
}
|
|
1239
|
+
function miningCandidateIsCurrent(options) {
|
|
1240
|
+
return options.state.currentReferencedBlockHashDisplay !== null
|
|
1241
|
+
&& options.nodeBestHash !== null
|
|
1242
|
+
&& options.state.currentReferencedBlockHashDisplay === options.nodeBestHash
|
|
1243
|
+
&& options.state.currentBlockTargetHeight !== null
|
|
1244
|
+
&& options.nodeBestHeight !== null
|
|
1245
|
+
&& options.state.currentBlockTargetHeight === (options.nodeBestHeight + 1);
|
|
1246
|
+
}
|
|
1247
|
+
async function rebuildPersistentAnchorLocks(options) {
|
|
1248
|
+
const walletName = options.state.managedCoreWallet.walletName;
|
|
1249
|
+
const [locked, spendable] = await Promise.all([
|
|
1250
|
+
options.rpc.listLockUnspent(walletName).catch(() => []),
|
|
1251
|
+
options.rpc.listUnspent(walletName, 0).catch(() => []),
|
|
1252
|
+
]);
|
|
1253
|
+
const spendableKeys = new Set(spendable.map((entry) => `${entry.txid}:${entry.vout}`));
|
|
1254
|
+
const expected = options.state.domains
|
|
1255
|
+
.map((domain) => domain.currentCanonicalAnchorOutpoint)
|
|
1256
|
+
.filter((outpoint) => outpoint !== null)
|
|
1257
|
+
.map((outpoint) => ({ txid: outpoint.txid, vout: outpoint.vout }))
|
|
1258
|
+
.filter((outpoint) => spendableKeys.has(`${outpoint.txid}:${outpoint.vout}`));
|
|
1259
|
+
const expectedKeys = new Set(expected.map((outpoint) => `${outpoint.txid}:${outpoint.vout}`));
|
|
1260
|
+
const lockedKeys = new Set(locked.map((outpoint) => `${outpoint.txid}:${outpoint.vout}`));
|
|
1261
|
+
const staleLocked = locked.filter((outpoint) => !expectedKeys.has(`${outpoint.txid}:${outpoint.vout}`)
|
|
1262
|
+
|| !spendableKeys.has(`${outpoint.txid}:${outpoint.vout}`));
|
|
1263
|
+
const missingLocked = expected.filter((outpoint) => !lockedKeys.has(`${outpoint.txid}:${outpoint.vout}`));
|
|
1264
|
+
if (staleLocked.length > 0) {
|
|
1265
|
+
await options.rpc.lockUnspent(walletName, true, staleLocked).catch(() => undefined);
|
|
1266
|
+
}
|
|
1267
|
+
if (missingLocked.length > 0) {
|
|
1268
|
+
await options.rpc.lockUnspent(walletName, false, missingLocked).catch(() => undefined);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
async function reconcileLiveMiningState(options) {
|
|
1272
|
+
let state = {
|
|
1273
|
+
...options.state,
|
|
1274
|
+
miningState: normalizeMiningStateRecord(options.state.miningState),
|
|
1275
|
+
};
|
|
1276
|
+
const currentTxid = state.miningState.currentTxid;
|
|
1277
|
+
if (currentTxid === null || !miningFamilyMayStillExist(state.miningState)) {
|
|
1278
|
+
await rebuildPersistentAnchorLocks({ state, rpc: options.rpc });
|
|
1279
|
+
return state;
|
|
1280
|
+
}
|
|
1281
|
+
const walletName = state.managedCoreWallet.walletName;
|
|
1282
|
+
const [mempoolVerbose, walletTx] = await Promise.all([
|
|
1283
|
+
options.rpc.getRawMempoolVerbose().catch(() => ({
|
|
1284
|
+
txids: [],
|
|
1285
|
+
mempool_sequence: "unknown",
|
|
1286
|
+
})),
|
|
1287
|
+
options.rpc.getTransaction(walletName, currentTxid).catch(() => null),
|
|
1288
|
+
]);
|
|
1289
|
+
const inMempool = mempoolVerbose.txids.includes(currentTxid);
|
|
1290
|
+
if (walletTx !== null && walletTx.confirmations > 0) {
|
|
1291
|
+
state = {
|
|
1292
|
+
...state,
|
|
1293
|
+
miningState: {
|
|
1294
|
+
...clearMiningFamilyState(state.miningState),
|
|
1295
|
+
currentPublishDecision: "tx-confirmed-while-down",
|
|
1296
|
+
},
|
|
1297
|
+
};
|
|
1298
|
+
await rebuildPersistentAnchorLocks({ state, rpc: options.rpc });
|
|
1299
|
+
return state;
|
|
1300
|
+
}
|
|
1301
|
+
if (inMempool) {
|
|
1302
|
+
const stale = !miningCandidateIsCurrent({
|
|
1303
|
+
state: state.miningState,
|
|
1304
|
+
nodeBestHash: options.nodeBestHash,
|
|
1305
|
+
nodeBestHeight: options.nodeBestHeight,
|
|
1306
|
+
});
|
|
1307
|
+
state = defaultMiningStatePatch(state, {
|
|
1308
|
+
liveMiningFamilyInMempool: true,
|
|
1309
|
+
currentPublishState: "in-mempool",
|
|
1310
|
+
state: stale
|
|
1311
|
+
? "paused-stale"
|
|
1312
|
+
: state.miningState.runMode === "stopped"
|
|
1313
|
+
? "paused"
|
|
1314
|
+
: "live",
|
|
1315
|
+
pauseReason: stale
|
|
1316
|
+
? "stale-block-context"
|
|
1317
|
+
: state.miningState.runMode === "stopped"
|
|
1318
|
+
? "user-stopped"
|
|
1319
|
+
: null,
|
|
1320
|
+
currentPublishDecision: stale ? "paused-stale-mempool" : "restored-live-family",
|
|
1321
|
+
});
|
|
1322
|
+
await rebuildPersistentAnchorLocks({ state, rpc: options.rpc });
|
|
1323
|
+
return state;
|
|
1324
|
+
}
|
|
1325
|
+
if ((walletTx?.walletconflicts?.length ?? 0) > 0) {
|
|
1326
|
+
state = defaultMiningStatePatch(state, {
|
|
1327
|
+
state: "repair-required",
|
|
1328
|
+
pauseReason: state.miningState.currentPublishState === "broadcast-unknown"
|
|
1329
|
+
? "broadcast-unknown-conflict"
|
|
1330
|
+
: "wallet-conflict-observed",
|
|
1331
|
+
liveMiningFamilyInMempool: false,
|
|
1332
|
+
currentPublishDecision: state.miningState.currentPublishState === "broadcast-unknown"
|
|
1333
|
+
? "repair-required-broadcast-conflict"
|
|
1334
|
+
: "repair-required-wallet-conflict",
|
|
1335
|
+
});
|
|
1336
|
+
await rebuildPersistentAnchorLocks({ state, rpc: options.rpc });
|
|
1337
|
+
return state;
|
|
1338
|
+
}
|
|
1339
|
+
state = defaultMiningStatePatch(state, {
|
|
1340
|
+
...clearMiningFamilyState(state.miningState),
|
|
1341
|
+
currentPublishDecision: state.miningState.currentPublishState === "broadcast-unknown"
|
|
1342
|
+
? "broadcast-unknown-not-seen"
|
|
1343
|
+
: "live-family-not-seen",
|
|
1344
|
+
});
|
|
1345
|
+
await rebuildPersistentAnchorLocks({ state, rpc: options.rpc });
|
|
1346
|
+
return state;
|
|
1347
|
+
}
|
|
1348
|
+
async function publishCandidate(options) {
|
|
1349
|
+
const service = await options.attachService({
|
|
1350
|
+
dataDir: options.dataDir,
|
|
1351
|
+
chain: "main",
|
|
1352
|
+
startHeight: 0,
|
|
1353
|
+
walletRootId: options.readContext.localState.state.walletRootId,
|
|
1354
|
+
});
|
|
1355
|
+
const rpc = options.rpcFactory(service.rpc);
|
|
1356
|
+
let state = await reconcileLiveMiningState({
|
|
1357
|
+
state: options.readContext.localState.state,
|
|
1358
|
+
rpc,
|
|
1359
|
+
nodeBestHash: options.readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1360
|
+
nodeBestHeight: options.readContext.nodeStatus?.nodeBestHeight ?? null,
|
|
1361
|
+
});
|
|
1362
|
+
const allUtxos = await rpc.listUnspent(state.managedCoreWallet.walletName, 0);
|
|
1363
|
+
const fundingConflict = state.miningState.sharedMiningConflictOutpoint
|
|
1364
|
+
?? allUtxos.find((entry) => entry.scriptPubKey === state.funding.scriptPubKeyHex
|
|
1365
|
+
&& entry.confirmations >= 1
|
|
1366
|
+
&& entry.spendable !== false
|
|
1367
|
+
&& entry.safe !== false
|
|
1368
|
+
&& !(entry.txid === options.candidate.anchorOutpoint.txid && entry.vout === options.candidate.anchorOutpoint.vout));
|
|
1369
|
+
if (fundingConflict === undefined || fundingConflict === null) {
|
|
1370
|
+
throw new Error("wallet_mining_missing_conflict_utxo");
|
|
1371
|
+
}
|
|
1372
|
+
const conflictOutpoint = "txid" in fundingConflict
|
|
1373
|
+
? { txid: fundingConflict.txid, vout: fundingConflict.vout }
|
|
1374
|
+
: fundingConflict;
|
|
1375
|
+
const priorMiningState = cloneMiningState(state.miningState);
|
|
1376
|
+
const nextFeeRate = state.miningState.currentFeeRateSatVb === null
|
|
1377
|
+
? DEFAULT_WALLET_MUTATION_FEE_RATE_SAT_VB
|
|
1378
|
+
: state.miningState.currentFeeRateSatVb + 1;
|
|
1379
|
+
const shouldFeeBump = candidateNeedsFeeMaintenance({
|
|
1380
|
+
liveState: state.miningState,
|
|
1381
|
+
candidate: options.candidate,
|
|
1382
|
+
});
|
|
1383
|
+
if (state.miningState.currentPublishState === "in-mempool"
|
|
1384
|
+
&& state.miningState.liveMiningFamilyInMempool === true
|
|
1385
|
+
&& !shouldFeeBump
|
|
1386
|
+
&& !await candidateWinsAgainstLive({
|
|
1387
|
+
liveState: state.miningState,
|
|
1388
|
+
candidate: options.candidate,
|
|
1389
|
+
})) {
|
|
1390
|
+
return {
|
|
1391
|
+
state: defaultMiningStatePatch(state, {
|
|
1392
|
+
currentPublishDecision: "kept-live-family",
|
|
1393
|
+
}),
|
|
1394
|
+
txid: state.miningState.currentTxid,
|
|
1395
|
+
decision: "kept-live-family",
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
const plan = createMiningPlan({
|
|
1399
|
+
state,
|
|
1400
|
+
candidate: options.candidate,
|
|
1401
|
+
conflictOutpoint,
|
|
1402
|
+
allUtxos,
|
|
1403
|
+
feeRateSatVb: nextFeeRate,
|
|
1404
|
+
});
|
|
1405
|
+
const built = await buildMiningTransaction({
|
|
1406
|
+
rpc,
|
|
1407
|
+
walletName: state.managedCoreWallet.walletName,
|
|
1408
|
+
plan,
|
|
1409
|
+
});
|
|
1410
|
+
const intentFingerprintHex = computeIntentFingerprint(state, options.candidate);
|
|
1411
|
+
state = defaultMiningStatePatch(state, {
|
|
1412
|
+
state: "live",
|
|
1413
|
+
currentPublishState: "broadcasting",
|
|
1414
|
+
currentDomain: options.candidate.domainName,
|
|
1415
|
+
currentDomainId: options.candidate.domainId,
|
|
1416
|
+
currentDomainIndex: options.candidate.localIndex,
|
|
1417
|
+
currentSenderScriptPubKeyHex: options.candidate.sender.scriptPubKeyHex,
|
|
1418
|
+
currentTxid: built.txid,
|
|
1419
|
+
currentWtxid: built.wtxid,
|
|
1420
|
+
currentFeeRateSatVb: nextFeeRate,
|
|
1421
|
+
currentAbsoluteFeeSats: numberToSats(built.funded.fee).toString() === "0" ? 0 : Number(numberToSats(built.funded.fee)),
|
|
1422
|
+
currentScore: options.candidate.canonicalBlend.toString(),
|
|
1423
|
+
currentSentence: options.candidate.sentence,
|
|
1424
|
+
currentEncodedSentenceBytesHex: Buffer.from(options.candidate.encodedSentenceBytes).toString("hex"),
|
|
1425
|
+
currentBip39WordIndices: [...options.candidate.bip39WordIndices],
|
|
1426
|
+
currentBlendSeedHex: Buffer.from(deriveBlendSeed(options.candidate.referencedBlockHashInternal)).toString("hex"),
|
|
1427
|
+
currentBlockTargetHeight: options.candidate.targetBlockHeight,
|
|
1428
|
+
currentReferencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
1429
|
+
currentIntentFingerprintHex: intentFingerprintHex,
|
|
1430
|
+
sharedMiningConflictOutpoint: conflictOutpoint,
|
|
1431
|
+
liveMiningFamilyInMempool: null,
|
|
1432
|
+
currentPublishDecision: priorMiningState.currentTxid === null
|
|
1433
|
+
? "publishing"
|
|
1434
|
+
: shouldFeeBump
|
|
1435
|
+
? "fee-bump"
|
|
1436
|
+
: "replacing",
|
|
1437
|
+
});
|
|
1438
|
+
await saveWalletStatePreservingUnlock({
|
|
1439
|
+
state,
|
|
1440
|
+
provider: options.provider,
|
|
1441
|
+
unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
|
|
1442
|
+
nowUnixMs: Date.now(),
|
|
1443
|
+
paths: options.paths,
|
|
1444
|
+
});
|
|
1445
|
+
try {
|
|
1446
|
+
await rpc.sendRawTransaction(built.rawHex);
|
|
1447
|
+
}
|
|
1448
|
+
catch (error) {
|
|
1449
|
+
if (isAlreadyAcceptedError(error)) {
|
|
1450
|
+
state = defaultMiningStatePatch(state, {
|
|
1451
|
+
currentPublishState: "in-mempool",
|
|
1452
|
+
liveMiningFamilyInMempool: true,
|
|
1453
|
+
});
|
|
1454
|
+
await saveWalletStatePreservingUnlock({
|
|
1455
|
+
state,
|
|
1456
|
+
provider: options.provider,
|
|
1457
|
+
unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
|
|
1458
|
+
nowUnixMs: Date.now(),
|
|
1459
|
+
paths: options.paths,
|
|
1460
|
+
});
|
|
1461
|
+
await appendEvent(options.paths, createEvent(state.miningState.currentPublishDecision === "replacing" ? "tx-replaced" : "tx-broadcast", `Mining transaction ${built.txid} is already accepted by the local node.`, {
|
|
1462
|
+
runId: options.runId,
|
|
1463
|
+
targetBlockHeight: options.candidate.targetBlockHeight,
|
|
1464
|
+
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
1465
|
+
domainId: options.candidate.domainId,
|
|
1466
|
+
domainName: options.candidate.domainName,
|
|
1467
|
+
txid: built.txid,
|
|
1468
|
+
feeRateSatVb: nextFeeRate,
|
|
1469
|
+
feeSats: numberToSats(built.funded.fee).toString(),
|
|
1470
|
+
score: options.candidate.canonicalBlend.toString(),
|
|
1471
|
+
}));
|
|
1472
|
+
return {
|
|
1473
|
+
state,
|
|
1474
|
+
txid: built.txid,
|
|
1475
|
+
decision: state.miningState.currentPublishDecision === "fee-bump"
|
|
1476
|
+
? "fee-bump"
|
|
1477
|
+
: state.miningState.currentPublishDecision === "replacing"
|
|
1478
|
+
? "replaced"
|
|
1479
|
+
: "broadcast",
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
if (isBroadcastUnknownError(error)) {
|
|
1483
|
+
state = defaultMiningStatePatch(state, {
|
|
1484
|
+
currentPublishState: "broadcast-unknown",
|
|
1485
|
+
currentPublishDecision: "broadcast-unknown",
|
|
1486
|
+
});
|
|
1487
|
+
await saveWalletStatePreservingUnlock({
|
|
1488
|
+
state,
|
|
1489
|
+
provider: options.provider,
|
|
1490
|
+
unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
|
|
1491
|
+
nowUnixMs: Date.now(),
|
|
1492
|
+
paths: options.paths,
|
|
1493
|
+
});
|
|
1494
|
+
await appendEvent(options.paths, createEvent("error", `Mining broadcast became uncertain for ${built.txid}.`, {
|
|
1495
|
+
level: "warn",
|
|
1496
|
+
runId: options.runId,
|
|
1497
|
+
targetBlockHeight: options.candidate.targetBlockHeight,
|
|
1498
|
+
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
1499
|
+
domainId: options.candidate.domainId,
|
|
1500
|
+
domainName: options.candidate.domainName,
|
|
1501
|
+
txid: built.txid,
|
|
1502
|
+
feeRateSatVb: nextFeeRate,
|
|
1503
|
+
feeSats: numberToSats(built.funded.fee).toString(),
|
|
1504
|
+
score: options.candidate.canonicalBlend.toString(),
|
|
1505
|
+
reason: "broadcast-unknown",
|
|
1506
|
+
}));
|
|
1507
|
+
return {
|
|
1508
|
+
state,
|
|
1509
|
+
txid: built.txid,
|
|
1510
|
+
decision: "broadcast-unknown",
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
throw error;
|
|
1514
|
+
}
|
|
1515
|
+
const absoluteFeeSats = numberToSats(built.funded.fee);
|
|
1516
|
+
const replacementCount = priorMiningState.currentTxid === null
|
|
1517
|
+
? priorMiningState.replacementCount
|
|
1518
|
+
: priorMiningState.replacementCount + 1;
|
|
1519
|
+
state = defaultMiningStatePatch(state, {
|
|
1520
|
+
currentPublishState: "in-mempool",
|
|
1521
|
+
liveMiningFamilyInMempool: true,
|
|
1522
|
+
currentPublishDecision: state.miningState.currentPublishDecision === "fee-bump"
|
|
1523
|
+
? "fee-bump"
|
|
1524
|
+
: state.miningState.currentPublishDecision === "replacing"
|
|
1525
|
+
? "replaced"
|
|
1526
|
+
: "broadcast",
|
|
1527
|
+
replacementCount,
|
|
1528
|
+
currentAbsoluteFeeSats: Number(absoluteFeeSats),
|
|
1529
|
+
currentBlockFeeSpentSats: (BigInt(state.miningState.currentBlockFeeSpentSats) + absoluteFeeSats).toString(),
|
|
1530
|
+
sessionFeeSpentSats: (BigInt(state.miningState.sessionFeeSpentSats) + absoluteFeeSats).toString(),
|
|
1531
|
+
lifetimeFeeSpentSats: (BigInt(state.miningState.lifetimeFeeSpentSats) + absoluteFeeSats).toString(),
|
|
1532
|
+
});
|
|
1533
|
+
await saveWalletStatePreservingUnlock({
|
|
1534
|
+
state,
|
|
1535
|
+
provider: options.provider,
|
|
1536
|
+
unlockUntilUnixMs: options.readContext.localState.unlockUntilUnixMs,
|
|
1537
|
+
nowUnixMs: Date.now(),
|
|
1538
|
+
paths: options.paths,
|
|
1539
|
+
});
|
|
1540
|
+
await appendEvent(options.paths, createEvent(state.miningState.currentPublishDecision === "replaced"
|
|
1541
|
+
? "tx-replaced"
|
|
1542
|
+
: state.miningState.currentPublishDecision === "fee-bump"
|
|
1543
|
+
? "tx-fee-bump"
|
|
1544
|
+
: "tx-broadcast", `${state.miningState.currentPublishDecision === "replaced"
|
|
1545
|
+
? "Replaced"
|
|
1546
|
+
: state.miningState.currentPublishDecision === "fee-bump"
|
|
1547
|
+
? "Fee-bumped"
|
|
1548
|
+
: "Broadcast"} mining transaction ${built.txid}.`, {
|
|
1549
|
+
runId: options.runId,
|
|
1550
|
+
targetBlockHeight: options.candidate.targetBlockHeight,
|
|
1551
|
+
referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
|
|
1552
|
+
domainId: options.candidate.domainId,
|
|
1553
|
+
domainName: options.candidate.domainName,
|
|
1554
|
+
txid: built.txid,
|
|
1555
|
+
feeRateSatVb: nextFeeRate,
|
|
1556
|
+
feeSats: absoluteFeeSats.toString(),
|
|
1557
|
+
score: options.candidate.canonicalBlend.toString(),
|
|
1558
|
+
}));
|
|
1559
|
+
return {
|
|
1560
|
+
state,
|
|
1561
|
+
txid: built.txid,
|
|
1562
|
+
decision: state.miningState.currentPublishDecision === "fee-bump"
|
|
1563
|
+
? "fee-bump"
|
|
1564
|
+
: state.miningState.currentPublishDecision === "replaced"
|
|
1565
|
+
? "replaced"
|
|
1566
|
+
: "broadcast",
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
async function ensureBuiltInSetupIfNeeded(options) {
|
|
1570
|
+
const unlocked = await loadUnlockedWalletState({
|
|
1571
|
+
provider: options.provider,
|
|
1572
|
+
paths: options.paths,
|
|
1573
|
+
});
|
|
1574
|
+
if (unlocked?.state.hookClientState.mining.mode === "custom") {
|
|
1575
|
+
return true;
|
|
1576
|
+
}
|
|
1577
|
+
const config = await loadClientConfig({
|
|
1578
|
+
path: options.paths.clientConfigPath,
|
|
1579
|
+
provider: options.provider,
|
|
1580
|
+
}).catch(() => null);
|
|
1581
|
+
if (config?.mining.builtIn !== null) {
|
|
1582
|
+
return true;
|
|
1583
|
+
}
|
|
1584
|
+
if (options.prompter.isInteractive === false) {
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
await setupBuiltInMining({
|
|
1588
|
+
provider: options.provider,
|
|
1589
|
+
prompter: options.prompter,
|
|
1590
|
+
paths: options.paths,
|
|
1591
|
+
});
|
|
1592
|
+
return true;
|
|
1593
|
+
}
|
|
1594
|
+
async function performMiningCycle(options) {
|
|
1595
|
+
let readContext = await options.openReadContext({
|
|
1596
|
+
dataDir: options.dataDir,
|
|
1597
|
+
databasePath: options.databasePath,
|
|
1598
|
+
secretProvider: options.provider,
|
|
1599
|
+
paths: options.paths,
|
|
1600
|
+
});
|
|
1601
|
+
let readContextClosed = false;
|
|
1602
|
+
try {
|
|
1603
|
+
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
1604
|
+
await refreshAndSaveStatus({
|
|
1605
|
+
paths: options.paths,
|
|
1606
|
+
provider: options.provider,
|
|
1607
|
+
readContext,
|
|
1608
|
+
overrides: {
|
|
1609
|
+
runMode: options.runMode,
|
|
1610
|
+
backgroundWorkerPid: options.backgroundWorkerPid,
|
|
1611
|
+
backgroundWorkerRunId: options.backgroundWorkerRunId,
|
|
1612
|
+
backgroundWorkerHeartbeatAtUnixMs: options.runMode === "background" ? Date.now() : null,
|
|
1613
|
+
},
|
|
1614
|
+
});
|
|
1615
|
+
if (readContext.localState.availability !== "ready" || readContext.localState.state === null || readContext.localState.unlockUntilUnixMs === null) {
|
|
1616
|
+
await refreshAndSaveStatus({
|
|
1617
|
+
paths: options.paths,
|
|
1618
|
+
provider: options.provider,
|
|
1619
|
+
readContext,
|
|
1620
|
+
overrides: {
|
|
1621
|
+
runMode: options.runMode,
|
|
1622
|
+
currentPhase: "waiting",
|
|
1623
|
+
note: "Wallet must stay unlocked for mining to continue.",
|
|
1624
|
+
},
|
|
1625
|
+
visualizer: options.visualizer,
|
|
1626
|
+
});
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
const service = await options.attachService({
|
|
1630
|
+
dataDir: options.dataDir,
|
|
1631
|
+
chain: "main",
|
|
1632
|
+
startHeight: 0,
|
|
1633
|
+
walletRootId: readContext.localState.state.walletRootId,
|
|
1634
|
+
});
|
|
1635
|
+
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
1636
|
+
const rpc = options.rpcFactory(service.rpc);
|
|
1637
|
+
const reconciledState = await reconcileLiveMiningState({
|
|
1638
|
+
state: readContext.localState.state,
|
|
1639
|
+
rpc,
|
|
1640
|
+
nodeBestHash: readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1641
|
+
nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
|
|
1642
|
+
});
|
|
1643
|
+
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
1644
|
+
let effectiveReadContext = readContext;
|
|
1645
|
+
if (JSON.stringify(reconciledState.miningState) !== JSON.stringify(readContext.localState.state.miningState)) {
|
|
1646
|
+
await saveWalletStatePreservingUnlock({
|
|
1647
|
+
state: reconciledState,
|
|
1648
|
+
provider: options.provider,
|
|
1649
|
+
unlockUntilUnixMs: readContext.localState.unlockUntilUnixMs,
|
|
1650
|
+
nowUnixMs: Date.now(),
|
|
1651
|
+
paths: options.paths,
|
|
1652
|
+
});
|
|
1653
|
+
effectiveReadContext = {
|
|
1654
|
+
...readContext,
|
|
1655
|
+
localState: {
|
|
1656
|
+
...readContext.localState,
|
|
1657
|
+
availability: "ready",
|
|
1658
|
+
unlockUntilUnixMs: readContext.localState.unlockUntilUnixMs,
|
|
1659
|
+
state: reconciledState,
|
|
1660
|
+
},
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
if (effectiveReadContext.localState.state.miningState.state === "repair-required") {
|
|
1664
|
+
await refreshAndSaveStatus({
|
|
1665
|
+
paths: options.paths,
|
|
1666
|
+
provider: options.provider,
|
|
1667
|
+
readContext: effectiveReadContext,
|
|
1668
|
+
overrides: {
|
|
1669
|
+
runMode: options.runMode,
|
|
1670
|
+
currentPhase: "waiting",
|
|
1671
|
+
note: "Mining is blocked until the current mining family is repaired or reconciled.",
|
|
1672
|
+
},
|
|
1673
|
+
visualizer: options.visualizer,
|
|
1674
|
+
});
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
if (hasBlockingMutation(effectiveReadContext.localState.state)) {
|
|
1678
|
+
const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
|
|
1679
|
+
state: "paused",
|
|
1680
|
+
pauseReason: "wallet-busy",
|
|
1681
|
+
});
|
|
1682
|
+
await saveWalletStatePreservingUnlock({
|
|
1683
|
+
state: nextState,
|
|
1684
|
+
provider: options.provider,
|
|
1685
|
+
unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
|
|
1686
|
+
nowUnixMs: Date.now(),
|
|
1687
|
+
paths: options.paths,
|
|
1688
|
+
});
|
|
1689
|
+
effectiveReadContext = {
|
|
1690
|
+
...effectiveReadContext,
|
|
1691
|
+
localState: {
|
|
1692
|
+
...effectiveReadContext.localState,
|
|
1693
|
+
availability: "ready",
|
|
1694
|
+
unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
|
|
1695
|
+
state: nextState,
|
|
1696
|
+
},
|
|
1697
|
+
};
|
|
1698
|
+
await refreshAndSaveStatus({
|
|
1699
|
+
paths: options.paths,
|
|
1700
|
+
provider: options.provider,
|
|
1701
|
+
readContext: effectiveReadContext,
|
|
1702
|
+
overrides: {
|
|
1703
|
+
runMode: options.runMode,
|
|
1704
|
+
currentPhase: "waiting",
|
|
1705
|
+
note: "Mining is paused while another wallet mutation family is active.",
|
|
1706
|
+
},
|
|
1707
|
+
visualizer: options.visualizer,
|
|
1708
|
+
});
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
const preemptionRequest = await readMiningPreemptionRequest(options.paths);
|
|
1712
|
+
if (preemptionRequest !== null) {
|
|
1713
|
+
const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
|
|
1714
|
+
state: effectiveReadContext.localState.state.miningState.liveMiningFamilyInMempool
|
|
1715
|
+
&& effectiveReadContext.localState.state.miningState.state === "paused-stale"
|
|
1716
|
+
? "paused-stale"
|
|
1717
|
+
: "paused",
|
|
1718
|
+
pauseReason: preemptionRequest.reason,
|
|
1719
|
+
});
|
|
1720
|
+
await saveWalletStatePreservingUnlock({
|
|
1721
|
+
state: nextState,
|
|
1722
|
+
provider: options.provider,
|
|
1723
|
+
unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
|
|
1724
|
+
nowUnixMs: Date.now(),
|
|
1725
|
+
paths: options.paths,
|
|
1726
|
+
});
|
|
1727
|
+
await refreshAndSaveStatus({
|
|
1728
|
+
paths: options.paths,
|
|
1729
|
+
provider: options.provider,
|
|
1730
|
+
readContext: {
|
|
1731
|
+
...effectiveReadContext,
|
|
1732
|
+
localState: {
|
|
1733
|
+
...effectiveReadContext.localState,
|
|
1734
|
+
state: nextState,
|
|
1735
|
+
},
|
|
1736
|
+
},
|
|
1737
|
+
overrides: {
|
|
1738
|
+
runMode: options.runMode,
|
|
1739
|
+
currentPhase: "waiting",
|
|
1740
|
+
note: "Mining is paused while another wallet command is preempting sentence generation.",
|
|
1741
|
+
},
|
|
1742
|
+
visualizer: options.visualizer,
|
|
1743
|
+
});
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
const [blockchainInfo, networkInfo, mempoolInfo] = await Promise.all([
|
|
1747
|
+
rpc.getBlockchainInfo(),
|
|
1748
|
+
rpc.getNetworkInfo(),
|
|
1749
|
+
rpc.getMempoolInfo(),
|
|
1750
|
+
]);
|
|
1751
|
+
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
1752
|
+
const corePublishState = determineCorePublishState({
|
|
1753
|
+
blockchain: blockchainInfo,
|
|
1754
|
+
network: networkInfo,
|
|
1755
|
+
mempool: mempoolInfo,
|
|
1756
|
+
});
|
|
1757
|
+
if (corePublishState !== "healthy") {
|
|
1758
|
+
await refreshAndSaveStatus({
|
|
1759
|
+
paths: options.paths,
|
|
1760
|
+
provider: options.provider,
|
|
1761
|
+
readContext: effectiveReadContext,
|
|
1762
|
+
overrides: {
|
|
1763
|
+
runMode: options.runMode,
|
|
1764
|
+
currentPhase: "waiting-bitcoin-network",
|
|
1765
|
+
corePublishState,
|
|
1766
|
+
note: "Mining is waiting for the local Bitcoin node to become publishable.",
|
|
1767
|
+
},
|
|
1768
|
+
visualizer: options.visualizer,
|
|
1769
|
+
});
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
if (effectiveReadContext.indexer.health !== "synced" || effectiveReadContext.nodeHealth !== "synced") {
|
|
1773
|
+
await refreshAndSaveStatus({
|
|
1774
|
+
paths: options.paths,
|
|
1775
|
+
provider: options.provider,
|
|
1776
|
+
readContext: effectiveReadContext,
|
|
1777
|
+
overrides: {
|
|
1778
|
+
runMode: options.runMode,
|
|
1779
|
+
currentPhase: effectiveReadContext.indexer.health !== "synced"
|
|
1780
|
+
? "waiting-indexer"
|
|
1781
|
+
: "waiting-bitcoin-network",
|
|
1782
|
+
note: effectiveReadContext.indexer.health !== "synced"
|
|
1783
|
+
? "Mining is waiting for Bitcoin Core and the indexer to align."
|
|
1784
|
+
: "Mining is waiting for the local Bitcoin node to become publishable.",
|
|
1785
|
+
},
|
|
1786
|
+
visualizer: options.visualizer,
|
|
1787
|
+
});
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
const targetBlockHeight = (effectiveReadContext.nodeStatus?.nodeBestHeight ?? 0) + 1;
|
|
1791
|
+
if (getBlockRewardCogtoshi(targetBlockHeight) === 0n) {
|
|
1792
|
+
const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
|
|
1793
|
+
state: "paused",
|
|
1794
|
+
pauseReason: "zero-reward",
|
|
1795
|
+
});
|
|
1796
|
+
await saveWalletStatePreservingUnlock({
|
|
1797
|
+
state: nextState,
|
|
1798
|
+
provider: options.provider,
|
|
1799
|
+
unlockUntilUnixMs: effectiveReadContext.localState.unlockUntilUnixMs,
|
|
1800
|
+
nowUnixMs: Date.now(),
|
|
1801
|
+
paths: options.paths,
|
|
1802
|
+
});
|
|
1803
|
+
await refreshAndSaveStatus({
|
|
1804
|
+
paths: options.paths,
|
|
1805
|
+
provider: options.provider,
|
|
1806
|
+
readContext: {
|
|
1807
|
+
...effectiveReadContext,
|
|
1808
|
+
localState: {
|
|
1809
|
+
...effectiveReadContext.localState,
|
|
1810
|
+
state: nextState,
|
|
1811
|
+
},
|
|
1812
|
+
},
|
|
1813
|
+
overrides: {
|
|
1814
|
+
runMode: options.runMode,
|
|
1815
|
+
currentPhase: "idle",
|
|
1816
|
+
currentPublishDecision: "publish-skipped-zero-reward",
|
|
1817
|
+
note: "Mining is disabled because the target block reward is zero.",
|
|
1818
|
+
},
|
|
1819
|
+
visualizer: options.visualizer,
|
|
1820
|
+
});
|
|
1821
|
+
await appendEvent(options.paths, createEvent("publish-skipped-zero-reward", "Skipped mining because the target block reward is zero.", {
|
|
1822
|
+
targetBlockHeight,
|
|
1823
|
+
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1824
|
+
runId: options.backgroundWorkerRunId,
|
|
1825
|
+
}));
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
const domains = resolveEligibleAnchoredRoots(effectiveReadContext);
|
|
1829
|
+
if (domains.length === 0) {
|
|
1830
|
+
await refreshAndSaveStatus({
|
|
1831
|
+
paths: options.paths,
|
|
1832
|
+
provider: options.provider,
|
|
1833
|
+
readContext: effectiveReadContext,
|
|
1834
|
+
overrides: {
|
|
1835
|
+
runMode: options.runMode,
|
|
1836
|
+
currentPhase: "idle",
|
|
1837
|
+
note: "No locally controlled anchored root domains are currently eligible to mine.",
|
|
1838
|
+
},
|
|
1839
|
+
visualizer: options.visualizer,
|
|
1840
|
+
});
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
const indexerTruthKey = getIndexerTruthKey(effectiveReadContext);
|
|
1844
|
+
const walletRootId = effectiveReadContext.localState.walletRootId;
|
|
1845
|
+
const ensureCurrentIndexerTruthOrRestart = async () => {
|
|
1846
|
+
try {
|
|
1847
|
+
await ensureIndexerTruthIsCurrent({
|
|
1848
|
+
dataDir: effectiveReadContext.dataDir,
|
|
1849
|
+
truthKey: indexerTruthKey,
|
|
1850
|
+
});
|
|
1851
|
+
return true;
|
|
1852
|
+
}
|
|
1853
|
+
catch (error) {
|
|
1854
|
+
if (!(error instanceof Error) || error.message !== "mining_generation_stale_indexer_truth") {
|
|
1855
|
+
throw error;
|
|
1856
|
+
}
|
|
1857
|
+
clearMiningGateCache(walletRootId);
|
|
1858
|
+
await appendEvent(options.paths, createEvent("generation-restarted-indexer-truth", "Detected updated coherent indexer truth during mining; restarting on the next tick.", {
|
|
1859
|
+
level: "warn",
|
|
1860
|
+
targetBlockHeight,
|
|
1861
|
+
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1862
|
+
runId: options.backgroundWorkerRunId,
|
|
1863
|
+
}));
|
|
1864
|
+
return false;
|
|
1865
|
+
}
|
|
1866
|
+
};
|
|
1867
|
+
await refreshAndSaveStatus({
|
|
1868
|
+
paths: options.paths,
|
|
1869
|
+
provider: options.provider,
|
|
1870
|
+
readContext: effectiveReadContext,
|
|
1871
|
+
overrides: {
|
|
1872
|
+
runMode: options.runMode,
|
|
1873
|
+
currentPhase: "generating",
|
|
1874
|
+
note: "Generating mining sentences for eligible root domains.",
|
|
1875
|
+
},
|
|
1876
|
+
visualizer: options.visualizer,
|
|
1877
|
+
});
|
|
1878
|
+
await appendEvent(options.paths, createEvent("hook-request-start", "Started mining sentence generation.", {
|
|
1879
|
+
targetBlockHeight,
|
|
1880
|
+
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1881
|
+
runId: options.backgroundWorkerRunId,
|
|
1882
|
+
}));
|
|
1883
|
+
let candidates;
|
|
1884
|
+
try {
|
|
1885
|
+
candidates = await generateCandidatesForDomains({
|
|
1886
|
+
rpc,
|
|
1887
|
+
readContext: effectiveReadContext,
|
|
1888
|
+
domains,
|
|
1889
|
+
provider: options.provider,
|
|
1890
|
+
paths: options.paths,
|
|
1891
|
+
indexerTruthKey,
|
|
1892
|
+
runId: options.backgroundWorkerRunId,
|
|
1893
|
+
fetchImpl: options.fetchImpl,
|
|
1894
|
+
});
|
|
1895
|
+
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
1896
|
+
}
|
|
1897
|
+
catch (error) {
|
|
1898
|
+
if (error instanceof MiningProviderRequestError) {
|
|
1899
|
+
await refreshAndSaveStatus({
|
|
1900
|
+
paths: options.paths,
|
|
1901
|
+
provider: options.provider,
|
|
1902
|
+
readContext: effectiveReadContext,
|
|
1903
|
+
overrides: {
|
|
1904
|
+
runMode: options.runMode,
|
|
1905
|
+
currentPhase: "waiting-provider",
|
|
1906
|
+
providerState: error.providerState,
|
|
1907
|
+
lastError: error.message,
|
|
1908
|
+
note: "Mining is waiting for the sentence provider to recover.",
|
|
1909
|
+
},
|
|
1910
|
+
visualizer: options.visualizer,
|
|
1911
|
+
});
|
|
1912
|
+
await appendEvent(options.paths, createEvent("publish-paused-provider", error.message, {
|
|
1913
|
+
level: "warn",
|
|
1914
|
+
targetBlockHeight,
|
|
1915
|
+
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1916
|
+
runId: options.backgroundWorkerRunId,
|
|
1917
|
+
}));
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
if (error instanceof Error && error.message === "mining_generation_stale_tip") {
|
|
1921
|
+
await appendEvent(options.paths, createEvent("generation-restarted-new-tip", "Detected a new best tip during sentence generation; restarting on the next tick.", {
|
|
1922
|
+
level: "warn",
|
|
1923
|
+
targetBlockHeight,
|
|
1924
|
+
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1925
|
+
runId: options.backgroundWorkerRunId,
|
|
1926
|
+
}));
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
if (error instanceof Error && error.message === "mining_generation_stale_indexer_truth") {
|
|
1930
|
+
clearMiningGateCache(walletRootId);
|
|
1931
|
+
await appendEvent(options.paths, createEvent("generation-restarted-indexer-truth", "Detected updated coherent indexer truth during sentence generation; restarting on the next tick.", {
|
|
1932
|
+
level: "warn",
|
|
1933
|
+
targetBlockHeight,
|
|
1934
|
+
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1935
|
+
runId: options.backgroundWorkerRunId,
|
|
1936
|
+
}));
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
if (error instanceof Error && error.message === "mining_generation_preempted") {
|
|
1940
|
+
await appendEvent(options.paths, createEvent("generation-paused-preempted", "Stopped sentence generation because another wallet command requested mining preemption.", {
|
|
1941
|
+
level: "warn",
|
|
1942
|
+
targetBlockHeight,
|
|
1943
|
+
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1944
|
+
runId: options.backgroundWorkerRunId,
|
|
1945
|
+
}));
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
const hookCooldownActive = await persistCustomHookRuntimeOutcome({
|
|
1949
|
+
readContext: effectiveReadContext,
|
|
1950
|
+
provider: options.provider,
|
|
1951
|
+
paths: options.paths,
|
|
1952
|
+
nowUnixMs: Date.now(),
|
|
1953
|
+
success: false,
|
|
1954
|
+
});
|
|
1955
|
+
const failureMessage = error instanceof Error ? error.message : String(error);
|
|
1956
|
+
await refreshAndSaveStatus({
|
|
1957
|
+
paths: options.paths,
|
|
1958
|
+
provider: options.provider,
|
|
1959
|
+
readContext: effectiveReadContext,
|
|
1960
|
+
overrides: {
|
|
1961
|
+
runMode: options.runMode,
|
|
1962
|
+
currentPhase: "waiting-provider",
|
|
1963
|
+
providerState: effectiveReadContext.localState.state?.hookClientState.mining.mode === "custom"
|
|
1964
|
+
? "hook-error"
|
|
1965
|
+
: undefined,
|
|
1966
|
+
lastError: failureMessage,
|
|
1967
|
+
note: effectiveReadContext.localState.state?.hookClientState.mining.mode === "custom"
|
|
1968
|
+
? (hookCooldownActive
|
|
1969
|
+
? "Custom mining hook launch is paused during the post-failure cooldown window."
|
|
1970
|
+
: "Custom mining hook failed during sentence generation. Fix it or rerun `cogcoin hooks enable mining`.")
|
|
1971
|
+
: "Mining sentence generation failed for the current tip.",
|
|
1972
|
+
},
|
|
1973
|
+
visualizer: options.visualizer,
|
|
1974
|
+
});
|
|
1975
|
+
await appendEvent(options.paths, createEvent("hook-request-failed", failureMessage, {
|
|
1976
|
+
level: "error",
|
|
1977
|
+
targetBlockHeight,
|
|
1978
|
+
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
1979
|
+
runId: options.backgroundWorkerRunId,
|
|
1980
|
+
}));
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
await refreshAndSaveStatus({
|
|
1984
|
+
paths: options.paths,
|
|
1985
|
+
provider: options.provider,
|
|
1986
|
+
readContext: effectiveReadContext,
|
|
1987
|
+
overrides: {
|
|
1988
|
+
runMode: options.runMode,
|
|
1989
|
+
currentPhase: "scoring",
|
|
1990
|
+
note: "Scoring mining candidates for the current tip.",
|
|
1991
|
+
},
|
|
1992
|
+
visualizer: options.visualizer,
|
|
1993
|
+
});
|
|
1994
|
+
const best = await chooseBestLocalCandidate(candidates);
|
|
1995
|
+
if (best === null) {
|
|
1996
|
+
await refreshAndSaveStatus({
|
|
1997
|
+
paths: options.paths,
|
|
1998
|
+
provider: options.provider,
|
|
1999
|
+
readContext: effectiveReadContext,
|
|
2000
|
+
overrides: {
|
|
2001
|
+
runMode: options.runMode,
|
|
2002
|
+
currentPhase: "idle",
|
|
2003
|
+
currentPublishDecision: "publish-skipped-no-candidate",
|
|
2004
|
+
note: "No publishable mining candidate passed scoring gates for the current tip.",
|
|
2005
|
+
},
|
|
2006
|
+
visualizer: options.visualizer,
|
|
2007
|
+
});
|
|
2008
|
+
await appendEvent(options.paths, createEvent("publish-skipped-no-candidate", "No publishable mining candidate passed scoring gates.", {
|
|
2009
|
+
targetBlockHeight,
|
|
2010
|
+
referencedBlockHashDisplay: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
2011
|
+
runId: options.backgroundWorkerRunId,
|
|
2012
|
+
}));
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
if (!await ensureCurrentIndexerTruthOrRestart()) {
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
writeStdout(options.stdout, `Selected ${best.domainName}: ${best.sentence} (${best.canonicalBlend.toString()})`);
|
|
2019
|
+
await appendEvent(options.paths, createEvent("candidate-selected", `Selected ${best.domainName} with score ${best.canonicalBlend.toString()}.`, {
|
|
2020
|
+
targetBlockHeight: best.targetBlockHeight,
|
|
2021
|
+
referencedBlockHashDisplay: best.referencedBlockHashDisplay,
|
|
2022
|
+
domainId: best.domainId,
|
|
2023
|
+
domainName: best.domainName,
|
|
2024
|
+
score: best.canonicalBlend.toString(),
|
|
2025
|
+
runId: options.backgroundWorkerRunId,
|
|
2026
|
+
}));
|
|
2027
|
+
const gate = await runCompetitivenessGate({
|
|
2028
|
+
rpc,
|
|
2029
|
+
readContext: effectiveReadContext,
|
|
2030
|
+
candidate: best,
|
|
2031
|
+
currentTxid: effectiveReadContext.localState.state.miningState.currentTxid,
|
|
2032
|
+
});
|
|
2033
|
+
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
2034
|
+
if (!gate.allowed) {
|
|
2035
|
+
await refreshAndSaveStatus({
|
|
2036
|
+
paths: options.paths,
|
|
2037
|
+
provider: options.provider,
|
|
2038
|
+
readContext: effectiveReadContext,
|
|
2039
|
+
overrides: {
|
|
2040
|
+
runMode: options.runMode,
|
|
2041
|
+
currentPhase: "waiting",
|
|
2042
|
+
currentPublishDecision: gate.decision,
|
|
2043
|
+
sameDomainCompetitorSuppressed: gate.sameDomainCompetitorSuppressed,
|
|
2044
|
+
higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
|
|
2045
|
+
dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
|
|
2046
|
+
competitivenessGateIndeterminate: gate.competitivenessGateIndeterminate,
|
|
2047
|
+
mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
|
|
2048
|
+
lastMempoolSequence: gate.lastMempoolSequence,
|
|
2049
|
+
lastCompetitivenessGateAtUnixMs: Date.now(),
|
|
2050
|
+
note: gate.decision === "suppressed-same-domain-mempool"
|
|
2051
|
+
? "Best local sentence found, but a same-domain mempool competitor already matches or beats it."
|
|
2052
|
+
: gate.decision === "suppressed-top5-mempool"
|
|
2053
|
+
? `Best local sentence found, but ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
|
|
2054
|
+
: "Mining skipped this tick because the mempool competitiveness gate could not be verified safely.",
|
|
2055
|
+
},
|
|
2056
|
+
visualizer: options.visualizer,
|
|
2057
|
+
});
|
|
2058
|
+
await appendEvent(options.paths, createEvent(gate.decision === "suppressed-same-domain-mempool"
|
|
2059
|
+
? "publish-skipped-same-domain-mempool"
|
|
2060
|
+
: gate.decision === "suppressed-top5-mempool"
|
|
2061
|
+
? "publish-skipped-top5-mempool"
|
|
2062
|
+
: "publish-skipped-gate-indeterminate", gate.decision === "suppressed-same-domain-mempool"
|
|
2063
|
+
? "Skipped publish because a same-domain mempool competitor already outranks the local candidate."
|
|
2064
|
+
: gate.decision === "suppressed-top5-mempool"
|
|
2065
|
+
? `Skipped publish because ${gate.higherRankedCompetitorDomainCount} stronger competitor root domains are already in mempool.`
|
|
2066
|
+
: "Skipped publish because the competitiveness gate could not be evaluated safely.", {
|
|
2067
|
+
targetBlockHeight: best.targetBlockHeight,
|
|
2068
|
+
referencedBlockHashDisplay: best.referencedBlockHashDisplay,
|
|
2069
|
+
domainId: best.domainId,
|
|
2070
|
+
domainName: best.domainName,
|
|
2071
|
+
score: best.canonicalBlend.toString(),
|
|
2072
|
+
runId: options.backgroundWorkerRunId,
|
|
2073
|
+
reason: gate.decision,
|
|
2074
|
+
}));
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
if (!await ensureCurrentIndexerTruthOrRestart()) {
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
await refreshAndSaveStatus({
|
|
2081
|
+
paths: options.paths,
|
|
2082
|
+
provider: options.provider,
|
|
2083
|
+
readContext: effectiveReadContext,
|
|
2084
|
+
overrides: {
|
|
2085
|
+
runMode: options.runMode,
|
|
2086
|
+
currentPhase: effectiveReadContext.localState.state.miningState.currentTxid === null
|
|
2087
|
+
? "publishing"
|
|
2088
|
+
: "replacing",
|
|
2089
|
+
note: effectiveReadContext.localState.state.miningState.currentTxid === null
|
|
2090
|
+
? "Broadcasting the best mining candidate for the current tip."
|
|
2091
|
+
: "Replacing the live mining transaction for the current tip.",
|
|
2092
|
+
},
|
|
2093
|
+
visualizer: options.visualizer,
|
|
2094
|
+
});
|
|
2095
|
+
const publishLock = await acquireFileLock(options.paths.walletControlLockPath, {
|
|
2096
|
+
purpose: "wallet-mine",
|
|
2097
|
+
walletRootId: effectiveReadContext.localState.state.walletRootId,
|
|
2098
|
+
});
|
|
2099
|
+
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
2100
|
+
try {
|
|
2101
|
+
if (!await ensureCurrentIndexerTruthOrRestart()) {
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
2105
|
+
const published = await publishCandidate({
|
|
2106
|
+
readContext: effectiveReadContext,
|
|
2107
|
+
candidate: best,
|
|
2108
|
+
dataDir: options.dataDir,
|
|
2109
|
+
provider: options.provider,
|
|
2110
|
+
paths: options.paths,
|
|
2111
|
+
attachService: options.attachService,
|
|
2112
|
+
rpcFactory: options.rpcFactory,
|
|
2113
|
+
runId: options.backgroundWorkerRunId,
|
|
2114
|
+
});
|
|
2115
|
+
checkpointMiningSuspendDetector(options.suspendDetector);
|
|
2116
|
+
await refreshAndSaveStatus({
|
|
2117
|
+
paths: options.paths,
|
|
2118
|
+
provider: options.provider,
|
|
2119
|
+
readContext: {
|
|
2120
|
+
...effectiveReadContext,
|
|
2121
|
+
localState: {
|
|
2122
|
+
...effectiveReadContext.localState,
|
|
2123
|
+
state: published.state,
|
|
2124
|
+
},
|
|
2125
|
+
},
|
|
2126
|
+
overrides: {
|
|
2127
|
+
runMode: options.runMode,
|
|
2128
|
+
currentPhase: "publishing",
|
|
2129
|
+
currentPublishDecision: published.decision,
|
|
2130
|
+
sameDomainCompetitorSuppressed: false,
|
|
2131
|
+
higherRankedCompetitorDomainCount: gate.higherRankedCompetitorDomainCount,
|
|
2132
|
+
dedupedCompetitorDomainCount: gate.dedupedCompetitorDomainCount,
|
|
2133
|
+
competitivenessGateIndeterminate: false,
|
|
2134
|
+
mempoolSequenceCacheStatus: gate.mempoolSequenceCacheStatus,
|
|
2135
|
+
lastMempoolSequence: gate.lastMempoolSequence,
|
|
2136
|
+
lastCompetitivenessGateAtUnixMs: Date.now(),
|
|
2137
|
+
note: published.txid === null
|
|
2138
|
+
? "Mining candidate was evaluated but the existing live family stayed in place."
|
|
2139
|
+
: `Mining candidate ${published.decision === "replaced"
|
|
2140
|
+
? "replaced"
|
|
2141
|
+
: published.decision === "fee-bump"
|
|
2142
|
+
? "fee-bumped"
|
|
2143
|
+
: "broadcast"} as ${published.txid}.`,
|
|
2144
|
+
liveMiningFamilyInMempool: published.state.miningState.liveMiningFamilyInMempool,
|
|
2145
|
+
},
|
|
2146
|
+
visualizer: options.visualizer,
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
finally {
|
|
2150
|
+
await publishLock.release();
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
catch (error) {
|
|
2154
|
+
if (error instanceof MiningSuspendDetectedError) {
|
|
2155
|
+
if (readContext !== null && !readContextClosed) {
|
|
2156
|
+
await readContext.close();
|
|
2157
|
+
readContextClosed = true;
|
|
2158
|
+
}
|
|
2159
|
+
await handleDetectedMiningRuntimeResume({
|
|
2160
|
+
dataDir: options.dataDir,
|
|
2161
|
+
databasePath: options.databasePath,
|
|
2162
|
+
provider: options.provider,
|
|
2163
|
+
paths: options.paths,
|
|
2164
|
+
runMode: options.runMode,
|
|
2165
|
+
backgroundWorkerPid: options.backgroundWorkerPid,
|
|
2166
|
+
backgroundWorkerRunId: options.backgroundWorkerRunId,
|
|
2167
|
+
detectedAtUnixMs: error.detectedAtUnixMs,
|
|
2168
|
+
openReadContext: options.openReadContext,
|
|
2169
|
+
visualizer: options.visualizer,
|
|
2170
|
+
});
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
throw error;
|
|
2174
|
+
}
|
|
2175
|
+
finally {
|
|
2176
|
+
if (readContext !== null && !readContextClosed) {
|
|
2177
|
+
await readContext.close();
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
async function saveStopSnapshot(options) {
|
|
2182
|
+
const readContext = await openWalletReadContext({
|
|
2183
|
+
dataDir: options.dataDir,
|
|
2184
|
+
databasePath: options.databasePath,
|
|
2185
|
+
secretProvider: options.provider,
|
|
2186
|
+
paths: options.paths,
|
|
2187
|
+
});
|
|
2188
|
+
try {
|
|
2189
|
+
let localState = readContext.localState;
|
|
2190
|
+
if (localState.availability === "ready" && localState.state !== null && localState.unlockUntilUnixMs !== null) {
|
|
2191
|
+
const service = await attachOrStartManagedBitcoindService({
|
|
2192
|
+
dataDir: options.dataDir,
|
|
2193
|
+
chain: "main",
|
|
2194
|
+
startHeight: 0,
|
|
2195
|
+
walletRootId: localState.state.walletRootId,
|
|
2196
|
+
}).catch(() => null);
|
|
2197
|
+
if (service !== null) {
|
|
2198
|
+
const rpc = createRpcClient(service.rpc);
|
|
2199
|
+
const reconciledState = await reconcileLiveMiningState({
|
|
2200
|
+
state: localState.state,
|
|
2201
|
+
rpc,
|
|
2202
|
+
nodeBestHash: readContext.nodeStatus?.nodeBestHashHex ?? null,
|
|
2203
|
+
nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
|
|
2204
|
+
});
|
|
2205
|
+
const stopState = defaultMiningStatePatch(reconciledState, {
|
|
2206
|
+
runMode: "stopped",
|
|
2207
|
+
state: reconciledState.miningState.liveMiningFamilyInMempool
|
|
2208
|
+
? reconciledState.miningState.state === "paused-stale"
|
|
2209
|
+
? "paused-stale"
|
|
2210
|
+
: "paused"
|
|
2211
|
+
: reconciledState.miningState.state === "repair-required"
|
|
2212
|
+
? "repair-required"
|
|
2213
|
+
: "idle",
|
|
2214
|
+
pauseReason: reconciledState.miningState.liveMiningFamilyInMempool
|
|
2215
|
+
? reconciledState.miningState.state === "paused-stale"
|
|
2216
|
+
? "stale-block-context"
|
|
2217
|
+
: "user-stopped"
|
|
2218
|
+
: reconciledState.miningState.state === "repair-required"
|
|
2219
|
+
? reconciledState.miningState.pauseReason
|
|
2220
|
+
: null,
|
|
2221
|
+
});
|
|
2222
|
+
await saveWalletStatePreservingUnlock({
|
|
2223
|
+
state: stopState,
|
|
2224
|
+
provider: options.provider,
|
|
2225
|
+
unlockUntilUnixMs: localState.unlockUntilUnixMs,
|
|
2226
|
+
nowUnixMs: Date.now(),
|
|
2227
|
+
paths: options.paths,
|
|
2228
|
+
});
|
|
2229
|
+
localState = {
|
|
2230
|
+
...localState,
|
|
2231
|
+
state: stopState,
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
await refreshAndSaveStatus({
|
|
2236
|
+
paths: options.paths,
|
|
2237
|
+
provider: options.provider,
|
|
2238
|
+
readContext: {
|
|
2239
|
+
...readContext,
|
|
2240
|
+
localState,
|
|
2241
|
+
},
|
|
2242
|
+
overrides: {
|
|
2243
|
+
runMode: "stopped",
|
|
2244
|
+
backgroundWorkerPid: options.runMode === "background" ? null : options.backgroundWorkerPid,
|
|
2245
|
+
backgroundWorkerRunId: options.runMode === "background" ? null : options.backgroundWorkerRunId,
|
|
2246
|
+
backgroundWorkerHeartbeatAtUnixMs: options.runMode === "background" ? null : Date.now(),
|
|
2247
|
+
currentPhase: "idle",
|
|
2248
|
+
note: options.note,
|
|
2249
|
+
},
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
finally {
|
|
2253
|
+
await readContext.close();
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
async function attemptSaveMempool(rpc, paths, runId) {
|
|
2257
|
+
try {
|
|
2258
|
+
await rpc.saveMempool?.();
|
|
2259
|
+
}
|
|
2260
|
+
catch {
|
|
2261
|
+
// ignore
|
|
2262
|
+
}
|
|
2263
|
+
finally {
|
|
2264
|
+
await appendEvent(paths, createEvent("savemempool-attempted", "Attempted to persist the local mempool before stopping mining.", {
|
|
2265
|
+
runId,
|
|
2266
|
+
}));
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
async function runMiningLoop(options) {
|
|
2270
|
+
const suspendDetector = createMiningSuspendDetector();
|
|
2271
|
+
await appendEvent(options.paths, createEvent("runtime-start", `Started ${options.runMode} mining runtime.`, {
|
|
2272
|
+
runId: options.backgroundWorkerRunId,
|
|
2273
|
+
}));
|
|
2274
|
+
while (!options.signal?.aborted) {
|
|
2275
|
+
try {
|
|
2276
|
+
checkpointMiningSuspendDetector(suspendDetector);
|
|
2277
|
+
}
|
|
2278
|
+
catch (error) {
|
|
2279
|
+
if (!(error instanceof MiningSuspendDetectedError)) {
|
|
2280
|
+
throw error;
|
|
2281
|
+
}
|
|
2282
|
+
await handleDetectedMiningRuntimeResume({
|
|
2283
|
+
dataDir: options.dataDir,
|
|
2284
|
+
databasePath: options.databasePath,
|
|
2285
|
+
provider: options.provider,
|
|
2286
|
+
paths: options.paths,
|
|
2287
|
+
runMode: options.runMode,
|
|
2288
|
+
backgroundWorkerPid: options.backgroundWorkerPid,
|
|
2289
|
+
backgroundWorkerRunId: options.backgroundWorkerRunId,
|
|
2290
|
+
detectedAtUnixMs: error.detectedAtUnixMs,
|
|
2291
|
+
openReadContext: options.openReadContext,
|
|
2292
|
+
visualizer: options.visualizer,
|
|
2293
|
+
});
|
|
2294
|
+
continue;
|
|
2295
|
+
}
|
|
2296
|
+
await performMiningCycle({
|
|
2297
|
+
...options,
|
|
2298
|
+
suspendDetector,
|
|
2299
|
+
});
|
|
2300
|
+
await sleep(Math.min(MINING_LOOP_INTERVAL_MS, MINING_STATUS_HEARTBEAT_INTERVAL_MS), options.signal);
|
|
2301
|
+
}
|
|
2302
|
+
const service = await options.attachService({
|
|
2303
|
+
dataDir: options.dataDir,
|
|
2304
|
+
chain: "main",
|
|
2305
|
+
startHeight: 0,
|
|
2306
|
+
walletRootId: undefined,
|
|
2307
|
+
}).catch(() => null);
|
|
2308
|
+
if (service !== null) {
|
|
2309
|
+
await attemptSaveMempool(options.rpcFactory(service.rpc), options.paths, options.backgroundWorkerRunId);
|
|
2310
|
+
}
|
|
2311
|
+
await appendEvent(options.paths, createEvent("runtime-stop", `Stopped ${options.runMode} mining runtime.`, {
|
|
2312
|
+
runId: options.backgroundWorkerRunId,
|
|
2313
|
+
}));
|
|
2314
|
+
}
|
|
2315
|
+
async function waitForBackgroundHealthy(paths) {
|
|
2316
|
+
const deadline = Date.now() + BACKGROUND_START_TIMEOUT_MS;
|
|
2317
|
+
while (Date.now() < deadline) {
|
|
2318
|
+
const snapshot = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
|
|
2319
|
+
if (snapshot !== null
|
|
2320
|
+
&& snapshot.runMode === "background"
|
|
2321
|
+
&& snapshot.backgroundWorkerHealth === "healthy") {
|
|
2322
|
+
return snapshot;
|
|
2323
|
+
}
|
|
2324
|
+
await sleep(250);
|
|
2325
|
+
}
|
|
2326
|
+
return loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
|
|
2327
|
+
}
|
|
2328
|
+
export async function runForegroundMining(options) {
|
|
2329
|
+
if (!options.prompter.isInteractive) {
|
|
2330
|
+
throw new Error("mine_requires_tty");
|
|
2331
|
+
}
|
|
2332
|
+
const provider = options.provider ?? createDefaultWalletSecretProvider();
|
|
2333
|
+
const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
|
|
2334
|
+
const openReadContext = options.openReadContext ?? openWalletReadContext;
|
|
2335
|
+
const attachService = options.attachService ?? attachOrStartManagedBitcoindService;
|
|
2336
|
+
const rpcFactory = options.rpcFactory ?? createRpcClient;
|
|
2337
|
+
const controlLock = await acquireFileLock(paths.miningControlLockPath, {
|
|
2338
|
+
purpose: "mine-foreground",
|
|
2339
|
+
});
|
|
2340
|
+
let visualizer = null;
|
|
2341
|
+
try {
|
|
2342
|
+
const existing = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
|
|
2343
|
+
if (existing?.runMode === "background") {
|
|
2344
|
+
throw new Error("Background mining is already active. Run `cogcoin mine stop` first.");
|
|
2345
|
+
}
|
|
2346
|
+
const setupReady = await ensureBuiltInSetupIfNeeded({
|
|
2347
|
+
provider,
|
|
2348
|
+
prompter: options.prompter,
|
|
2349
|
+
paths,
|
|
2350
|
+
});
|
|
2351
|
+
if (!setupReady) {
|
|
2352
|
+
throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
|
|
2353
|
+
}
|
|
2354
|
+
visualizer = new MiningFollowVisualizer({
|
|
2355
|
+
progressOutput: options.progressOutput ?? "auto",
|
|
2356
|
+
stream: options.stderr,
|
|
2357
|
+
});
|
|
2358
|
+
const abortController = new AbortController();
|
|
2359
|
+
options.signal?.addEventListener("abort", () => {
|
|
2360
|
+
abortController.abort();
|
|
2361
|
+
}, { once: true });
|
|
2362
|
+
process.on("SIGINT", () => abortController.abort());
|
|
2363
|
+
process.on("SIGTERM", () => abortController.abort());
|
|
2364
|
+
await runMiningLoop({
|
|
2365
|
+
dataDir: options.dataDir,
|
|
2366
|
+
databasePath: options.databasePath,
|
|
2367
|
+
provider,
|
|
2368
|
+
paths,
|
|
2369
|
+
runMode: "foreground",
|
|
2370
|
+
backgroundWorkerPid: null,
|
|
2371
|
+
backgroundWorkerRunId: null,
|
|
2372
|
+
signal: abortController.signal,
|
|
2373
|
+
fetchImpl: options.fetchImpl,
|
|
2374
|
+
openReadContext,
|
|
2375
|
+
attachService,
|
|
2376
|
+
rpcFactory,
|
|
2377
|
+
stdout: options.stdout,
|
|
2378
|
+
visualizer,
|
|
2379
|
+
});
|
|
2380
|
+
await saveStopSnapshot({
|
|
2381
|
+
dataDir: options.dataDir,
|
|
2382
|
+
databasePath: options.databasePath,
|
|
2383
|
+
provider,
|
|
2384
|
+
paths,
|
|
2385
|
+
runMode: "foreground",
|
|
2386
|
+
backgroundWorkerPid: null,
|
|
2387
|
+
backgroundWorkerRunId: null,
|
|
2388
|
+
note: "Foreground mining stopped cleanly.",
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
finally {
|
|
2392
|
+
visualizer?.close();
|
|
2393
|
+
await controlLock.release();
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
export async function startBackgroundMining(options) {
|
|
2397
|
+
const provider = options.provider ?? createDefaultWalletSecretProvider();
|
|
2398
|
+
const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
|
|
2399
|
+
const controlLock = await acquireFileLock(paths.miningControlLockPath, {
|
|
2400
|
+
purpose: "mine-start",
|
|
2401
|
+
});
|
|
2402
|
+
try {
|
|
2403
|
+
const existing = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
|
|
2404
|
+
if (existing?.runMode === "background"
|
|
2405
|
+
&& existing.backgroundWorkerPid !== null
|
|
2406
|
+
&& await isProcessAlive(existing.backgroundWorkerPid)) {
|
|
2407
|
+
return {
|
|
2408
|
+
started: false,
|
|
2409
|
+
snapshot: existing,
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
if (existing?.runMode === "foreground") {
|
|
2413
|
+
throw new Error("Foreground mining is already active. Interrupt that process directly.");
|
|
2414
|
+
}
|
|
2415
|
+
const setupReady = await ensureBuiltInSetupIfNeeded({
|
|
2416
|
+
provider,
|
|
2417
|
+
prompter: options.prompter,
|
|
2418
|
+
paths,
|
|
2419
|
+
});
|
|
2420
|
+
if (!setupReady) {
|
|
2421
|
+
throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
|
|
2422
|
+
}
|
|
2423
|
+
const runId = randomBytes(16).toString("hex");
|
|
2424
|
+
const workerMainPath = fileURLToPath(new URL("./worker-main.js", import.meta.url));
|
|
2425
|
+
const child = spawn(process.execPath, [
|
|
2426
|
+
workerMainPath,
|
|
2427
|
+
`--data-dir=${options.dataDir}`,
|
|
2428
|
+
`--database-path=${options.databasePath}`,
|
|
2429
|
+
`--run-id=${runId}`,
|
|
2430
|
+
], {
|
|
2431
|
+
detached: true,
|
|
2432
|
+
stdio: "ignore",
|
|
2433
|
+
});
|
|
2434
|
+
child.unref();
|
|
2435
|
+
const snapshot = await waitForBackgroundHealthy(paths);
|
|
2436
|
+
return {
|
|
2437
|
+
started: true,
|
|
2438
|
+
snapshot,
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
finally {
|
|
2442
|
+
await controlLock.release();
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
export async function stopBackgroundMining(options) {
|
|
2446
|
+
const provider = options.provider ?? createDefaultWalletSecretProvider();
|
|
2447
|
+
const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
|
|
2448
|
+
const controlLock = await acquireFileLock(paths.miningControlLockPath, {
|
|
2449
|
+
purpose: "mine-stop",
|
|
2450
|
+
});
|
|
2451
|
+
try {
|
|
2452
|
+
const snapshot = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
|
|
2453
|
+
if (snapshot === null || snapshot.runMode !== "background" || snapshot.backgroundWorkerPid === null) {
|
|
2454
|
+
return snapshot;
|
|
2455
|
+
}
|
|
2456
|
+
const preemption = await requestMiningGenerationPreemption({
|
|
2457
|
+
paths,
|
|
2458
|
+
reason: "mine-stop",
|
|
2459
|
+
timeoutMs: Math.min(MINING_SHUTDOWN_GRACE_MS, 15_000),
|
|
2460
|
+
}).catch(() => null);
|
|
2461
|
+
process.kill(snapshot.backgroundWorkerPid, "SIGTERM");
|
|
2462
|
+
const deadline = Date.now() + MINING_SHUTDOWN_GRACE_MS;
|
|
2463
|
+
while (Date.now() < deadline) {
|
|
2464
|
+
try {
|
|
2465
|
+
process.kill(snapshot.backgroundWorkerPid, 0);
|
|
2466
|
+
await sleep(250);
|
|
2467
|
+
}
|
|
2468
|
+
catch (error) {
|
|
2469
|
+
if (error instanceof Error && "code" in error && error.code === "ESRCH") {
|
|
2470
|
+
break;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
try {
|
|
2475
|
+
process.kill(snapshot.backgroundWorkerPid, "SIGKILL");
|
|
2476
|
+
}
|
|
2477
|
+
catch {
|
|
2478
|
+
// ignore
|
|
2479
|
+
}
|
|
2480
|
+
await saveStopSnapshot({
|
|
2481
|
+
dataDir: options.dataDir,
|
|
2482
|
+
databasePath: options.databasePath,
|
|
2483
|
+
provider,
|
|
2484
|
+
paths,
|
|
2485
|
+
runMode: "background",
|
|
2486
|
+
backgroundWorkerPid: snapshot.backgroundWorkerPid,
|
|
2487
|
+
backgroundWorkerRunId: snapshot.backgroundWorkerRunId,
|
|
2488
|
+
note: snapshot.liveMiningFamilyInMempool
|
|
2489
|
+
? "Background mining stopped. The last mining transaction may still confirm from mempool."
|
|
2490
|
+
: "Background mining stopped.",
|
|
2491
|
+
});
|
|
2492
|
+
await preemption?.release().catch(() => undefined);
|
|
2493
|
+
return loadMiningRuntimeStatus(paths.miningStatusPath);
|
|
2494
|
+
}
|
|
2495
|
+
finally {
|
|
2496
|
+
await controlLock.release();
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
export async function runBackgroundMiningWorker(options) {
|
|
2500
|
+
const provider = options.provider ?? createDefaultWalletSecretProvider();
|
|
2501
|
+
const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
|
|
2502
|
+
const openReadContext = options.openReadContext ?? openWalletReadContext;
|
|
2503
|
+
const attachService = options.attachService ?? attachOrStartManagedBitcoindService;
|
|
2504
|
+
const rpcFactory = options.rpcFactory ?? createRpcClient;
|
|
2505
|
+
const abortController = new AbortController();
|
|
2506
|
+
process.on("SIGINT", () => abortController.abort());
|
|
2507
|
+
process.on("SIGTERM", () => abortController.abort());
|
|
2508
|
+
const initialContext = await openReadContext({
|
|
2509
|
+
dataDir: options.dataDir,
|
|
2510
|
+
databasePath: options.databasePath,
|
|
2511
|
+
secretProvider: provider,
|
|
2512
|
+
paths,
|
|
2513
|
+
});
|
|
2514
|
+
try {
|
|
2515
|
+
const initialView = await inspectMiningControlPlane({
|
|
2516
|
+
provider,
|
|
2517
|
+
localState: initialContext.localState,
|
|
2518
|
+
bitcoind: initialContext.bitcoind,
|
|
2519
|
+
nodeStatus: initialContext.nodeStatus,
|
|
2520
|
+
nodeHealth: initialContext.nodeHealth,
|
|
2521
|
+
indexer: initialContext.indexer,
|
|
2522
|
+
paths,
|
|
2523
|
+
});
|
|
2524
|
+
await saveMiningRuntimeStatus(paths.miningStatusPath, {
|
|
2525
|
+
...initialView.runtime,
|
|
2526
|
+
walletRootId: initialContext.localState.walletRootId,
|
|
2527
|
+
workerApiVersion: MINING_WORKER_API_VERSION,
|
|
2528
|
+
workerBinaryVersion: process.version,
|
|
2529
|
+
workerBuildId: options.runId,
|
|
2530
|
+
runMode: "background",
|
|
2531
|
+
backgroundWorkerPid: process.pid,
|
|
2532
|
+
backgroundWorkerRunId: options.runId,
|
|
2533
|
+
backgroundWorkerHeartbeatAtUnixMs: Date.now(),
|
|
2534
|
+
currentPhase: "idle",
|
|
2535
|
+
updatedAtUnixMs: Date.now(),
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
finally {
|
|
2539
|
+
await initialContext.close();
|
|
2540
|
+
}
|
|
2541
|
+
await runMiningLoop({
|
|
2542
|
+
dataDir: options.dataDir,
|
|
2543
|
+
databasePath: options.databasePath,
|
|
2544
|
+
provider,
|
|
2545
|
+
paths,
|
|
2546
|
+
runMode: "background",
|
|
2547
|
+
backgroundWorkerPid: process.pid,
|
|
2548
|
+
backgroundWorkerRunId: options.runId,
|
|
2549
|
+
signal: abortController.signal,
|
|
2550
|
+
fetchImpl: options.fetchImpl,
|
|
2551
|
+
openReadContext,
|
|
2552
|
+
attachService,
|
|
2553
|
+
rpcFactory,
|
|
2554
|
+
});
|
|
2555
|
+
await saveStopSnapshot({
|
|
2556
|
+
dataDir: options.dataDir,
|
|
2557
|
+
databasePath: options.databasePath,
|
|
2558
|
+
provider,
|
|
2559
|
+
paths,
|
|
2560
|
+
runMode: "background",
|
|
2561
|
+
backgroundWorkerPid: process.pid,
|
|
2562
|
+
backgroundWorkerRunId: options.runId,
|
|
2563
|
+
note: "Background mining worker stopped cleanly.",
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
export async function handleDetectedMiningRuntimeResumeForTesting(options) {
|
|
2567
|
+
await handleDetectedMiningRuntimeResume(options);
|
|
2568
|
+
}
|
|
2569
|
+
export async function performMiningCycleForTesting(options) {
|
|
2570
|
+
await performMiningCycle(options);
|
|
2571
|
+
}
|
|
2572
|
+
export function shouldTreatCandidateAsFeeBumpForTesting(options) {
|
|
2573
|
+
return candidateNeedsFeeMaintenance(options);
|
|
2574
|
+
}
|