@cogcoin/client 0.5.5 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/bitcoind/bootstrap/chainstate.d.ts +2 -1
- package/dist/bitcoind/bootstrap/chainstate.js +4 -1
- package/dist/bitcoind/bootstrap/controller.d.ts +4 -1
- package/dist/bitcoind/bootstrap/controller.js +42 -5
- package/dist/bitcoind/bootstrap/getblock-archive.d.ts +39 -0
- package/dist/bitcoind/bootstrap/getblock-archive.js +548 -0
- package/dist/bitcoind/bootstrap/headers.d.ts +12 -0
- package/dist/bitcoind/bootstrap/headers.js +95 -10
- package/dist/bitcoind/bootstrap.d.ts +1 -0
- package/dist/bitcoind/bootstrap.js +1 -0
- package/dist/bitcoind/client/factory.js +91 -28
- package/dist/bitcoind/client/managed-client.d.ts +1 -1
- package/dist/bitcoind/client/managed-client.js +4 -3
- package/dist/bitcoind/client/sync-engine.js +55 -13
- package/dist/bitcoind/errors.js +18 -0
- package/dist/bitcoind/indexer-daemon-main.js +78 -0
- package/dist/bitcoind/indexer-daemon.d.ts +10 -1
- package/dist/bitcoind/indexer-daemon.js +44 -28
- package/dist/bitcoind/node.js +2 -0
- package/dist/bitcoind/processing-start-height.d.ts +7 -0
- package/dist/bitcoind/processing-start-height.js +9 -0
- package/dist/bitcoind/progress/constants.d.ts +1 -0
- package/dist/bitcoind/progress/constants.js +1 -0
- package/dist/bitcoind/progress/controller.d.ts +22 -0
- package/dist/bitcoind/progress/controller.js +49 -23
- package/dist/bitcoind/progress/formatting.js +29 -1
- package/dist/bitcoind/progress/render-policy.d.ts +35 -0
- package/dist/bitcoind/progress/render-policy.js +81 -0
- package/dist/bitcoind/retryable-rpc.d.ts +11 -0
- package/dist/bitcoind/retryable-rpc.js +30 -0
- package/dist/bitcoind/service-paths.js +2 -6
- package/dist/bitcoind/service.d.ts +21 -2
- package/dist/bitcoind/service.js +274 -122
- package/dist/bitcoind/testing.d.ts +2 -2
- package/dist/bitcoind/testing.js +2 -2
- package/dist/bitcoind/types.d.ts +36 -1
- package/dist/cli/commands/follow.js +11 -0
- package/dist/cli/commands/getblock-archive-restart.d.ts +5 -0
- package/dist/cli/commands/getblock-archive-restart.js +15 -0
- package/dist/cli/commands/mining-admin.js +4 -0
- package/dist/cli/commands/mining-read.js +8 -5
- package/dist/cli/commands/mining-runtime.js +4 -0
- package/dist/cli/commands/service-runtime.js +150 -134
- package/dist/cli/commands/status.js +2 -0
- package/dist/cli/commands/sync.js +11 -0
- package/dist/cli/commands/wallet-admin.js +106 -24
- package/dist/cli/commands/wallet-mutation.js +57 -4
- package/dist/cli/commands/wallet-read.js +2 -0
- package/dist/cli/context.js +8 -4
- package/dist/cli/mutation-command-groups.d.ts +2 -1
- package/dist/cli/mutation-command-groups.js +5 -0
- package/dist/cli/mutation-json.d.ts +18 -2
- package/dist/cli/mutation-json.js +49 -0
- package/dist/cli/mutation-success.d.ts +1 -0
- package/dist/cli/mutation-success.js +2 -2
- package/dist/cli/output.js +86 -1
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +133 -3
- package/dist/cli/preview-json.d.ts +10 -1
- package/dist/cli/preview-json.js +32 -0
- package/dist/cli/prompt.js +1 -1
- package/dist/cli/runner.js +4 -0
- package/dist/cli/types.d.ts +15 -5
- package/dist/cli/types.js +1 -1
- package/dist/cli/wallet-format.js +140 -14
- package/dist/wallet/lifecycle.d.ts +21 -1
- package/dist/wallet/lifecycle.js +252 -116
- package/dist/wallet/mining/visualizer.d.ts +11 -6
- package/dist/wallet/mining/visualizer.js +32 -15
- package/dist/wallet/read/context.js +10 -4
- package/dist/wallet/reset.d.ts +61 -2
- package/dist/wallet/reset.js +246 -89
- package/dist/wallet/root-resolution.d.ts +20 -0
- package/dist/wallet/root-resolution.js +37 -0
- package/dist/wallet/runtime.d.ts +13 -1
- package/dist/wallet/runtime.js +54 -11
- package/dist/wallet/state/crypto.d.ts +3 -0
- package/dist/wallet/state/crypto.js +3 -0
- package/dist/wallet/state/provider.d.ts +1 -0
- package/dist/wallet/state/provider.js +119 -3
- package/dist/wallet/state/seed-index.d.ts +43 -0
- package/dist/wallet/state/seed-index.js +151 -0
- package/dist/wallet/state/storage.d.ts +7 -1
- package/dist/wallet/state/storage.js +39 -0
- package/dist/wallet/tx/anchor.d.ts +22 -0
- package/dist/wallet/tx/anchor.js +215 -8
- package/dist/wallet/tx/index.d.ts +1 -1
- package/dist/wallet/tx/index.js +1 -1
- package/dist/wallet/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
1
2
|
import { formatManagedSyncErrorMessage } from "../errors.js";
|
|
3
|
+
import { MANAGED_RPC_RETRY_MESSAGE, consumeManagedRpcRetryDelayMs, createManagedRpcRetryState, describeManagedRpcRetryError, resetManagedRpcRetryState, isRetryableManagedRpcError, } from "../retryable-rpc.js";
|
|
2
4
|
import { DEFAULT_SNAPSHOT_METADATA, HEADER_NO_PEER_TIMEOUT_MS, HEADER_POLL_MS, } from "./constants.js";
|
|
5
|
+
const DEBUG_LOG_TAIL_BYTES = 64 * 1024;
|
|
6
|
+
const HEADER_SYNC_DEBUG_LINE_PATTERN = /Pre-synchronizing blockheaders,\s*height:\s*([\d,]+)\s*\(~(\d+(?:\.\d+)?)%\)/u;
|
|
3
7
|
function createAbortError(signal) {
|
|
4
8
|
const reason = signal?.reason;
|
|
5
9
|
if (reason instanceof Error) {
|
|
@@ -33,7 +37,52 @@ function resolvePeerCount(networkInfo) {
|
|
|
33
37
|
? networkInfo.connections
|
|
34
38
|
: (networkInfo.connections_in ?? 0) + (networkInfo.connections_out ?? 0);
|
|
35
39
|
}
|
|
36
|
-
function
|
|
40
|
+
async function readDebugLogTail(filePath, maxBytes = DEBUG_LOG_TAIL_BYTES) {
|
|
41
|
+
let handle = null;
|
|
42
|
+
try {
|
|
43
|
+
handle = await open(filePath, "r");
|
|
44
|
+
const stats = await handle.stat();
|
|
45
|
+
const bytesToRead = Math.min(maxBytes, Math.max(0, stats.size));
|
|
46
|
+
if (bytesToRead === 0) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
50
|
+
await handle.read(buffer, 0, bytesToRead, stats.size - bytesToRead);
|
|
51
|
+
return buffer.toString("utf8");
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await handle?.close().catch(() => { });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function readHeaderSyncProgressFromDebugLog(debugLogPath) {
|
|
64
|
+
const tail = await readDebugLogTail(debugLogPath);
|
|
65
|
+
if (tail === null) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const lines = tail.split(/\r?\n/u).reverse();
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
const match = HEADER_SYNC_DEBUG_LINE_PATTERN.exec(line);
|
|
71
|
+
if (match === null) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const height = Number(match[1].replaceAll(",", ""));
|
|
75
|
+
if (!Number.isFinite(height) || height < 0) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
height,
|
|
80
|
+
message: `Pre-synchronizing blockheaders, height: ${height.toLocaleString()} (~${match[2]}%)`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
function resolveHeaderWaitMessage(headers, peerCount, networkActive, rpcHeaders, headerSyncMessage) {
|
|
37
86
|
if (!networkActive) {
|
|
38
87
|
return "Bitcoin networking is inactive for the managed node.";
|
|
39
88
|
}
|
|
@@ -43,33 +92,69 @@ function resolveHeaderWaitMessage(headers, peerCount, networkActive) {
|
|
|
43
92
|
if (peerCount === 0) {
|
|
44
93
|
return `Waiting for peers to continue header sync (${headers.toLocaleString()} headers, 0 peers).`;
|
|
45
94
|
}
|
|
46
|
-
|
|
95
|
+
if (rpcHeaders > 0) {
|
|
96
|
+
return "Waiting for Bitcoin headers to reach the snapshot height.";
|
|
97
|
+
}
|
|
98
|
+
return headerSyncMessage ?? "Pre-synchronizing blockheaders.";
|
|
47
99
|
}
|
|
48
100
|
export async function waitForHeaders(rpc, snapshot, progress, options = {}) {
|
|
49
101
|
const now = options.now ?? Date.now;
|
|
50
102
|
const sleepImpl = options.sleep ?? sleep;
|
|
51
103
|
const noPeerTimeoutMs = options.noPeerTimeoutMs ?? HEADER_NO_PEER_TIMEOUT_MS;
|
|
52
104
|
const { signal } = options;
|
|
105
|
+
const retryState = options.retryState ?? createManagedRpcRetryState();
|
|
106
|
+
const readDebugLogProgress = options.readDebugLogProgress ?? readHeaderSyncProgressFromDebugLog;
|
|
53
107
|
let noPeerSince = null;
|
|
108
|
+
let lastBlocks = 0;
|
|
109
|
+
let lastHeaders = 0;
|
|
54
110
|
while (true) {
|
|
55
111
|
throwIfAborted(signal);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
112
|
+
let info;
|
|
113
|
+
let networkInfo;
|
|
114
|
+
try {
|
|
115
|
+
[info, networkInfo] = await Promise.all([
|
|
116
|
+
rpc.getBlockchainInfo(),
|
|
117
|
+
rpc.getNetworkInfo(),
|
|
118
|
+
]);
|
|
119
|
+
resetManagedRpcRetryState(retryState);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (!isRetryableManagedRpcError(error)) {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
await progress.setPhase("wait_headers_for_snapshot", {
|
|
126
|
+
headers: lastHeaders,
|
|
127
|
+
targetHeight: snapshot.height,
|
|
128
|
+
blocks: lastBlocks,
|
|
129
|
+
percent: (Math.min(lastHeaders, snapshot.height) / snapshot.height) * 100,
|
|
130
|
+
lastError: describeManagedRpcRetryError(error),
|
|
131
|
+
message: MANAGED_RPC_RETRY_MESSAGE,
|
|
132
|
+
});
|
|
133
|
+
await sleepImpl(consumeManagedRpcRetryDelayMs(retryState), signal);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
lastBlocks = info.blocks;
|
|
137
|
+
const debugLogProgress = info.headers === 0 && options.debugLogPath !== undefined
|
|
138
|
+
? await readDebugLogProgress(options.debugLogPath)
|
|
139
|
+
: null;
|
|
140
|
+
const observedHeaders = info.headers > 0
|
|
141
|
+
? info.headers
|
|
142
|
+
: Math.max(info.headers, debugLogProgress?.height ?? 0);
|
|
143
|
+
lastHeaders = observedHeaders;
|
|
60
144
|
const peerCount = resolvePeerCount(networkInfo);
|
|
61
|
-
const message = resolveHeaderWaitMessage(
|
|
145
|
+
const message = resolveHeaderWaitMessage(observedHeaders, peerCount, networkInfo.networkactive, info.headers, debugLogProgress?.message ?? null);
|
|
62
146
|
await progress.setPhase("wait_headers_for_snapshot", {
|
|
63
|
-
headers:
|
|
147
|
+
headers: observedHeaders,
|
|
64
148
|
targetHeight: snapshot.height,
|
|
65
149
|
blocks: info.blocks,
|
|
66
|
-
percent: (Math.min(
|
|
150
|
+
percent: (Math.min(observedHeaders, snapshot.height) / snapshot.height) * 100,
|
|
151
|
+
lastError: null,
|
|
67
152
|
message,
|
|
68
153
|
});
|
|
69
154
|
if (info.headers >= snapshot.height) {
|
|
70
155
|
return;
|
|
71
156
|
}
|
|
72
|
-
if (
|
|
157
|
+
if (observedHeaders === 0 && peerCount === 0) {
|
|
73
158
|
noPeerSince ??= now();
|
|
74
159
|
if (now() - noPeerSince >= noPeerTimeoutMs) {
|
|
75
160
|
throw new Error(formatManagedSyncErrorMessage("bitcoind_no_peers_for_header_sync_check_internet_or_firewall"));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { DEFAULT_SNAPSHOT_METADATA } from "./bootstrap/constants.js";
|
|
2
2
|
export { AssumeUtxoBootstrapController } from "./bootstrap/controller.js";
|
|
3
3
|
export { downloadSnapshotFileForTesting } from "./bootstrap/download.js";
|
|
4
|
+
export { prepareLatestGetblockArchive, prepareLatestGetblockArchiveForTesting, resolveGetblockArchivePathsForTesting, resolveReadyGetblockArchiveForTesting, waitForGetblockArchiveImport, waitForGetblockArchiveImportForTesting, } from "./bootstrap/getblock-archive.js";
|
|
4
5
|
export { waitForHeadersForTesting } from "./bootstrap/headers.js";
|
|
5
6
|
export { resolveBootstrapPathsForTesting } from "./bootstrap/paths.js";
|
|
6
7
|
export { loadBootstrapStateForTesting, saveBootstrapStateForTesting, createBootstrapStateForTesting, } from "./bootstrap/state.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { DEFAULT_SNAPSHOT_METADATA } from "./bootstrap/constants.js";
|
|
2
2
|
export { AssumeUtxoBootstrapController } from "./bootstrap/controller.js";
|
|
3
3
|
export { downloadSnapshotFileForTesting } from "./bootstrap/download.js";
|
|
4
|
+
export { prepareLatestGetblockArchive, prepareLatestGetblockArchiveForTesting, resolveGetblockArchivePathsForTesting, resolveReadyGetblockArchiveForTesting, waitForGetblockArchiveImport, waitForGetblockArchiveImportForTesting, } from "./bootstrap/getblock-archive.js";
|
|
4
5
|
export { waitForHeadersForTesting } from "./bootstrap/headers.js";
|
|
5
6
|
export { resolveBootstrapPathsForTesting } from "./bootstrap/paths.js";
|
|
6
7
|
export { loadBootstrapStateForTesting, saveBootstrapStateForTesting, createBootstrapStateForTesting, } from "./bootstrap/state.js";
|
|
@@ -1,47 +1,110 @@
|
|
|
1
1
|
import { loadBundledGenesisParameters } from "@cogcoin/indexer";
|
|
2
2
|
import { resolveDefaultBitcoindDataDirForTesting } from "../../app-paths.js";
|
|
3
3
|
import { openClient } from "../../client.js";
|
|
4
|
-
import { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, resolveBootstrapPathsForTesting } from "../bootstrap.js";
|
|
4
|
+
import { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, prepareLatestGetblockArchiveForTesting, resolveBootstrapPathsForTesting, } from "../bootstrap.js";
|
|
5
5
|
import { attachOrStartIndexerDaemon } from "../indexer-daemon.js";
|
|
6
6
|
import { createRpcClient } from "../node.js";
|
|
7
|
+
import { assertCogcoinProcessingStartHeight, resolveCogcoinProcessingStartHeight, } from "../processing-start-height.js";
|
|
7
8
|
import { ManagedProgressController } from "../progress.js";
|
|
8
|
-
import { attachOrStartManagedBitcoindService } from "../service.js";
|
|
9
|
+
import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, stopManagedBitcoindService, } from "../service.js";
|
|
9
10
|
import { DefaultManagedBitcoindClient } from "./managed-client.js";
|
|
10
11
|
const DEFAULT_SYNC_DEBOUNCE_MS = 250;
|
|
11
12
|
async function createManagedBitcoindClient(options) {
|
|
12
13
|
const genesisParameters = options.genesisParameters ?? await loadBundledGenesisParameters();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
assertCogcoinProcessingStartHeight({
|
|
15
|
+
chain: options.chain,
|
|
16
|
+
startHeight: options.startHeight,
|
|
17
|
+
genesisParameters,
|
|
17
18
|
});
|
|
18
|
-
const
|
|
19
|
+
const dataDir = options.dataDir ?? resolveDefaultBitcoindDataDirForTesting();
|
|
19
20
|
const progress = new ManagedProgressController({
|
|
20
21
|
onProgress: options.onProgress,
|
|
21
22
|
progressOutput: options.progressOutput,
|
|
22
|
-
quoteStatePath: resolveBootstrapPathsForTesting(
|
|
23
|
+
quoteStatePath: resolveBootstrapPathsForTesting(dataDir, DEFAULT_SNAPSHOT_METADATA).quoteStatePath,
|
|
23
24
|
snapshot: DEFAULT_SNAPSHOT_METADATA,
|
|
24
25
|
});
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
let progressStarted = false;
|
|
27
|
+
try {
|
|
28
|
+
await progress.start();
|
|
29
|
+
progressStarted = true;
|
|
30
|
+
let getblockArchive = options.chain === "main"
|
|
31
|
+
? await prepareLatestGetblockArchiveForTesting({
|
|
32
|
+
dataDir,
|
|
33
|
+
progress,
|
|
34
|
+
fetchImpl: options.fetchImpl,
|
|
35
|
+
})
|
|
36
|
+
: null;
|
|
37
|
+
if (options.chain === "main" && getblockArchive !== null) {
|
|
38
|
+
const existingProbe = await probeManagedBitcoindService({
|
|
39
|
+
...options,
|
|
40
|
+
dataDir,
|
|
41
|
+
});
|
|
42
|
+
if (existingProbe.compatibility === "compatible" && existingProbe.status !== null) {
|
|
43
|
+
const currentArchiveEndHeight = existingProbe.status.getblockArchiveEndHeight ?? null;
|
|
44
|
+
const currentArchiveSha256 = existingProbe.status.getblockArchiveSha256 ?? null;
|
|
45
|
+
const nextArchiveEndHeight = getblockArchive.manifest.endHeight;
|
|
46
|
+
const nextArchiveSha256 = getblockArchive.manifest.artifactSha256;
|
|
47
|
+
const needsRestart = currentArchiveEndHeight !== nextArchiveEndHeight
|
|
48
|
+
|| currentArchiveSha256 !== nextArchiveSha256;
|
|
49
|
+
if (needsRestart) {
|
|
50
|
+
const restartApproved = options.confirmGetblockArchiveRestart === undefined
|
|
51
|
+
? false
|
|
52
|
+
: await options.confirmGetblockArchiveRestart({
|
|
53
|
+
currentArchiveEndHeight,
|
|
54
|
+
nextArchiveEndHeight,
|
|
55
|
+
});
|
|
56
|
+
if (restartApproved) {
|
|
57
|
+
await stopManagedBitcoindService({
|
|
58
|
+
dataDir,
|
|
59
|
+
walletRootId: options.walletRootId,
|
|
60
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
getblockArchive = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const node = await attachOrStartManagedBitcoindService({
|
|
70
|
+
...options,
|
|
38
71
|
dataDir,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
|
|
72
|
+
getblockArchivePath: getblockArchive?.artifactPath ?? null,
|
|
73
|
+
getblockArchiveEndHeight: getblockArchive?.manifest.endHeight ?? null,
|
|
74
|
+
getblockArchiveSha256: getblockArchive?.manifest.artifactSha256 ?? null,
|
|
75
|
+
});
|
|
76
|
+
const rpc = createRpcClient(node.rpc);
|
|
77
|
+
const bootstrap = new AssumeUtxoBootstrapController({
|
|
78
|
+
rpc,
|
|
79
|
+
dataDir: node.dataDir,
|
|
80
|
+
progress,
|
|
81
|
+
snapshot: DEFAULT_SNAPSHOT_METADATA,
|
|
82
|
+
});
|
|
83
|
+
const client = await openClient({
|
|
84
|
+
store: options.store,
|
|
85
|
+
genesisParameters,
|
|
86
|
+
snapshotInterval: options.snapshotInterval,
|
|
87
|
+
});
|
|
88
|
+
const indexerDaemon = options.databasePath
|
|
89
|
+
? await attachOrStartIndexerDaemon({
|
|
90
|
+
dataDir,
|
|
91
|
+
databasePath: options.databasePath,
|
|
92
|
+
walletRootId: options.walletRootId,
|
|
93
|
+
startupTimeoutMs: options.startupTimeoutMs,
|
|
94
|
+
})
|
|
95
|
+
: null;
|
|
96
|
+
await indexerDaemon?.pauseBackgroundFollow();
|
|
97
|
+
// The persistent service may already exist from a non-processing attach path
|
|
98
|
+
// that used startHeight 0. Cogcoin replay still begins at the requested
|
|
99
|
+
// processing boundary for this managed client.
|
|
100
|
+
return new DefaultManagedBitcoindClient(client, options.store, node, rpc, progress, bootstrap, indexerDaemon, options.startHeight, options.syncDebounceMs ?? DEFAULT_SYNC_DEBOUNCE_MS);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (progressStarted) {
|
|
104
|
+
await progress.close().catch(() => undefined);
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
45
108
|
}
|
|
46
109
|
export async function openManagedBitcoindClient(options) {
|
|
47
110
|
const genesisParameters = options.genesisParameters ?? await loadBundledGenesisParameters();
|
|
@@ -49,7 +112,7 @@ export async function openManagedBitcoindClient(options) {
|
|
|
49
112
|
...options,
|
|
50
113
|
genesisParameters,
|
|
51
114
|
chain: "main",
|
|
52
|
-
startHeight: genesisParameters
|
|
115
|
+
startHeight: resolveCogcoinProcessingStartHeight(genesisParameters),
|
|
53
116
|
});
|
|
54
117
|
}
|
|
55
118
|
export async function openManagedBitcoindClientInternal(options) {
|
|
@@ -7,7 +7,7 @@ import type { BitcoinRpcClient } from "../rpc.js";
|
|
|
7
7
|
import type { ManagedBitcoindClient, ManagedBitcoindNodeHandle, ManagedBitcoindStatus, SyncResult } from "../types.js";
|
|
8
8
|
export declare class DefaultManagedBitcoindClient implements ManagedBitcoindClient {
|
|
9
9
|
#private;
|
|
10
|
-
constructor(client: Client, store: ClientStoreAdapter, node: ManagedBitcoindNodeHandle, rpc: BitcoinRpcClient, progress: ManagedProgressController, bootstrap: AssumeUtxoBootstrapController, indexerDaemon: IndexerDaemonClient | null, syncDebounceMs: number);
|
|
10
|
+
constructor(client: Client, store: ClientStoreAdapter, node: ManagedBitcoindNodeHandle, rpc: BitcoinRpcClient, progress: ManagedProgressController, bootstrap: AssumeUtxoBootstrapController, indexerDaemon: IndexerDaemonClient | null, startHeight: number, syncDebounceMs: number);
|
|
11
11
|
getTip(): Promise<import("../../types.js").ClientTip | null>;
|
|
12
12
|
getState(): Promise<import("@cogcoin/indexer/types").IndexerState>;
|
|
13
13
|
applyBlock(block: BitcoinBlock): Promise<import("../../types.js").ApplyBlockResult>;
|
|
@@ -22,7 +22,7 @@ export class DefaultManagedBitcoindClient {
|
|
|
22
22
|
#syncPromise = Promise.resolve(createInitialSyncResult());
|
|
23
23
|
#debounceTimer = null;
|
|
24
24
|
#syncAbortControllers = new Set();
|
|
25
|
-
constructor(client, store, node, rpc, progress, bootstrap, indexerDaemon, syncDebounceMs) {
|
|
25
|
+
constructor(client, store, node, rpc, progress, bootstrap, indexerDaemon, startHeight, syncDebounceMs) {
|
|
26
26
|
this.#client = client;
|
|
27
27
|
this.#store = store;
|
|
28
28
|
this.#node = node;
|
|
@@ -30,7 +30,7 @@ export class DefaultManagedBitcoindClient {
|
|
|
30
30
|
this.#progress = progress;
|
|
31
31
|
this.#bootstrap = bootstrap;
|
|
32
32
|
this.#indexerDaemon = indexerDaemon;
|
|
33
|
-
this.#startHeight =
|
|
33
|
+
this.#startHeight = startHeight;
|
|
34
34
|
this.#syncDebounceMs = syncDebounceMs;
|
|
35
35
|
}
|
|
36
36
|
async getTip() {
|
|
@@ -176,8 +176,9 @@ export class DefaultManagedBitcoindClient {
|
|
|
176
176
|
await this.#syncPromise.catch(() => undefined);
|
|
177
177
|
await this.#progress.close();
|
|
178
178
|
await this.#node.stop();
|
|
179
|
-
await this.#indexerDaemon?.close();
|
|
180
179
|
await this.#client.close();
|
|
180
|
+
await this.#indexerDaemon?.resumeBackgroundFollow().catch(() => undefined);
|
|
181
|
+
await this.#indexerDaemon?.close();
|
|
181
182
|
}
|
|
182
183
|
async playSyncCompletionScene() {
|
|
183
184
|
this.#assertOpen();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { waitForGetblockArchiveImport } from "../bootstrap.js";
|
|
1
2
|
import { formatManagedSyncErrorMessage } from "../errors.js";
|
|
2
3
|
import { normalizeRpcBlock } from "../normalize.js";
|
|
4
|
+
import { MANAGED_RPC_RETRY_MESSAGE, consumeManagedRpcRetryDelayMs, createManagedRpcRetryState, describeManagedRpcRetryError, isRetryableManagedRpcError, resetManagedRpcRetryState, } from "../retryable-rpc.js";
|
|
3
5
|
import { estimateEtaSeconds } from "./rate-tracker.js";
|
|
4
6
|
const DEFAULT_SYNC_CATCH_UP_POLL_MS = 2_000;
|
|
5
7
|
function createAbortError(signal) {
|
|
@@ -44,12 +46,42 @@ async function setBitcoinSyncProgress(dependencies, info) {
|
|
|
44
46
|
headers: info.headers,
|
|
45
47
|
targetHeight: info.headers,
|
|
46
48
|
etaSeconds,
|
|
49
|
+
lastError: null,
|
|
47
50
|
message: dependencies.node.expectedChain === "main"
|
|
48
51
|
? "Bitcoin Core is syncing blocks after assumeutxo bootstrap."
|
|
49
52
|
: "Reading blocks from the managed Bitcoin node.",
|
|
50
53
|
});
|
|
51
54
|
}
|
|
52
|
-
async function
|
|
55
|
+
async function setRetryingProgress(dependencies, error) {
|
|
56
|
+
const status = dependencies.progress.getStatusSnapshot();
|
|
57
|
+
const { phase: _phase, updatedAt: _updatedAt, ...progress } = status.bootstrapProgress;
|
|
58
|
+
await dependencies.progress.setPhase(status.bootstrapPhase, {
|
|
59
|
+
...progress,
|
|
60
|
+
lastError: describeManagedRpcRetryError(error),
|
|
61
|
+
message: MANAGED_RPC_RETRY_MESSAGE,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function runWithManagedRpcRetry(dependencies, retryState, operation) {
|
|
65
|
+
while (true) {
|
|
66
|
+
throwIfAborted(dependencies.abortSignal);
|
|
67
|
+
try {
|
|
68
|
+
const result = await operation();
|
|
69
|
+
resetManagedRpcRetryState(retryState);
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (isAbortError(error, dependencies.abortSignal)) {
|
|
74
|
+
throw createAbortError(dependencies.abortSignal);
|
|
75
|
+
}
|
|
76
|
+
if (!isRetryableManagedRpcError(error)) {
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
await setRetryingProgress(dependencies, error);
|
|
80
|
+
await sleep(consumeManagedRpcRetryDelayMs(retryState), dependencies.abortSignal);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function findCommonAncestor(dependencies, tip, bestHeight, runRpc) {
|
|
53
85
|
const startHeight = Math.min(tip.height, bestHeight);
|
|
54
86
|
for (let height = startHeight; height >= dependencies.startHeight; height -= 1) {
|
|
55
87
|
const localHashHex = height === tip.height
|
|
@@ -58,14 +90,14 @@ async function findCommonAncestor(dependencies, tip, bestHeight) {
|
|
|
58
90
|
if (localHashHex === null) {
|
|
59
91
|
continue;
|
|
60
92
|
}
|
|
61
|
-
const chainHashHex = await dependencies.rpc.getBlockHash(height);
|
|
93
|
+
const chainHashHex = await runRpc(() => dependencies.rpc.getBlockHash(height));
|
|
62
94
|
if (chainHashHex === localHashHex) {
|
|
63
95
|
return height;
|
|
64
96
|
}
|
|
65
97
|
}
|
|
66
98
|
return dependencies.startHeight - 1;
|
|
67
99
|
}
|
|
68
|
-
async function syncAgainstBestHeight(dependencies, bestHeight) {
|
|
100
|
+
async function syncAgainstBestHeight(dependencies, bestHeight, runRpc) {
|
|
69
101
|
if (bestHeight < dependencies.startHeight) {
|
|
70
102
|
return {
|
|
71
103
|
appliedBlocks: 0,
|
|
@@ -77,7 +109,7 @@ async function syncAgainstBestHeight(dependencies, bestHeight) {
|
|
|
77
109
|
let rewoundBlocks = 0;
|
|
78
110
|
let commonAncestorHeight = null;
|
|
79
111
|
if (startTip !== null) {
|
|
80
|
-
const rewindTarget = await findCommonAncestor(dependencies, startTip, bestHeight);
|
|
112
|
+
const rewindTarget = await findCommonAncestor(dependencies, startTip, bestHeight, runRpc);
|
|
81
113
|
if (rewindTarget < startTip.height) {
|
|
82
114
|
commonAncestorHeight = rewindTarget < dependencies.startHeight ? null : rewindTarget;
|
|
83
115
|
await dependencies.client.rewindToHeight(rewindTarget);
|
|
@@ -93,8 +125,8 @@ async function syncAgainstBestHeight(dependencies, bestHeight) {
|
|
|
93
125
|
await dependencies.progress.setCogcoinSync(nextHeight - 1, bestHeight, estimateEtaSeconds(dependencies.cogcoinRateTracker, nextHeight - 1, bestHeight));
|
|
94
126
|
}
|
|
95
127
|
for (let height = nextHeight; height <= bestHeight; height += 1) {
|
|
96
|
-
const blockHashHex = await dependencies.rpc.getBlockHash(height);
|
|
97
|
-
const rpcBlock = await dependencies.rpc.getBlock(blockHashHex);
|
|
128
|
+
const blockHashHex = await runRpc(() => dependencies.rpc.getBlockHash(height));
|
|
129
|
+
const rpcBlock = await runRpc(() => dependencies.rpc.getBlock(blockHashHex));
|
|
98
130
|
const normalizedBlock = normalizeRpcBlock(rpcBlock);
|
|
99
131
|
await dependencies.client.applyBlock(normalizedBlock);
|
|
100
132
|
if (typeof rpcBlock.time === "number") {
|
|
@@ -111,12 +143,21 @@ async function syncAgainstBestHeight(dependencies, bestHeight) {
|
|
|
111
143
|
}
|
|
112
144
|
export async function syncToTip(dependencies) {
|
|
113
145
|
try {
|
|
146
|
+
const retryState = createManagedRpcRetryState();
|
|
147
|
+
const runRpc = (operation) => runWithManagedRpcRetry(dependencies, retryState, operation);
|
|
114
148
|
throwIfAborted(dependencies.abortSignal);
|
|
115
|
-
await dependencies.node.validate();
|
|
149
|
+
await runRpc(() => dependencies.node.validate());
|
|
116
150
|
const indexedTipBeforeBootstrap = await dependencies.client.getTip();
|
|
117
|
-
await dependencies.bootstrap.ensureReady(indexedTipBeforeBootstrap, dependencies.node.expectedChain, {
|
|
151
|
+
await runRpc(() => dependencies.bootstrap.ensureReady(indexedTipBeforeBootstrap, dependencies.node.expectedChain, {
|
|
118
152
|
signal: dependencies.abortSignal,
|
|
119
|
-
|
|
153
|
+
retryState,
|
|
154
|
+
}));
|
|
155
|
+
if (dependencies.node.expectedChain === "main"
|
|
156
|
+
&& dependencies.node.getblockArchiveEndHeight !== null) {
|
|
157
|
+
await waitForGetblockArchiveImport({
|
|
158
|
+
getBlockchainInfo: () => runRpc(() => dependencies.rpc.getBlockchainInfo()),
|
|
159
|
+
}, dependencies.progress, dependencies.node.getblockArchiveEndHeight, dependencies.abortSignal);
|
|
160
|
+
}
|
|
120
161
|
const startTip = await dependencies.client.getTip();
|
|
121
162
|
const aggregate = {
|
|
122
163
|
appliedBlocks: 0,
|
|
@@ -129,9 +170,9 @@ export async function syncToTip(dependencies) {
|
|
|
129
170
|
};
|
|
130
171
|
while (true) {
|
|
131
172
|
throwIfAborted(dependencies.abortSignal);
|
|
132
|
-
const startInfo = await dependencies.rpc.getBlockchainInfo();
|
|
173
|
+
const startInfo = await runRpc(() => dependencies.rpc.getBlockchainInfo());
|
|
133
174
|
await setBitcoinSyncProgress(dependencies, startInfo);
|
|
134
|
-
const pass = await syncAgainstBestHeight(dependencies, startInfo.blocks);
|
|
175
|
+
const pass = await syncAgainstBestHeight(dependencies, startInfo.blocks, runRpc);
|
|
135
176
|
aggregate.appliedBlocks += pass.appliedBlocks;
|
|
136
177
|
aggregate.rewoundBlocks += pass.rewoundBlocks;
|
|
137
178
|
if (pass.commonAncestorHeight !== null) {
|
|
@@ -140,19 +181,20 @@ export async function syncToTip(dependencies) {
|
|
|
140
181
|
: Math.min(aggregate.commonAncestorHeight, pass.commonAncestorHeight);
|
|
141
182
|
}
|
|
142
183
|
const finalTip = await dependencies.client.getTip();
|
|
143
|
-
const endInfo = await dependencies.rpc.getBlockchainInfo();
|
|
184
|
+
const endInfo = await runRpc(() => dependencies.rpc.getBlockchainInfo());
|
|
144
185
|
const caughtUpCogcoin = endInfo.blocks < dependencies.startHeight || finalTip?.height === endInfo.blocks;
|
|
145
186
|
aggregate.endingHeight = finalTip?.height ?? null;
|
|
146
187
|
aggregate.bestHeight = endInfo.blocks;
|
|
147
188
|
aggregate.bestHashHex = endInfo.bestblockhash;
|
|
148
189
|
if (endInfo.blocks === endInfo.headers && caughtUpCogcoin) {
|
|
149
190
|
if (dependencies.isFollowing()) {
|
|
150
|
-
dependencies.progress.replaceFollowBlockTimes(await dependencies.loadVisibleFollowBlockTimes(finalTip));
|
|
191
|
+
dependencies.progress.replaceFollowBlockTimes(await runRpc(() => dependencies.loadVisibleFollowBlockTimes(finalTip)));
|
|
151
192
|
}
|
|
152
193
|
await dependencies.progress.setPhase(dependencies.isFollowing() ? "follow_tip" : "complete", {
|
|
153
194
|
blocks: endInfo.blocks,
|
|
154
195
|
headers: endInfo.headers,
|
|
155
196
|
targetHeight: endInfo.headers,
|
|
197
|
+
lastError: null,
|
|
156
198
|
message: dependencies.isFollowing()
|
|
157
199
|
? "Following the live Bitcoin tip."
|
|
158
200
|
: "Managed sync fully caught up to the live tip.",
|
package/dist/bitcoind/errors.js
CHANGED
|
@@ -5,6 +5,24 @@ function appendNextStep(message, nextStep) {
|
|
|
5
5
|
return `${message} Next: ${nextStep}`;
|
|
6
6
|
}
|
|
7
7
|
export function formatManagedSyncErrorMessage(message) {
|
|
8
|
+
if (message.startsWith("managed_getblock_archive_manifest_http_")) {
|
|
9
|
+
return appendNextStep(`Getblock archive manifest request failed (${message.replace("managed_getblock_archive_manifest_http_", "HTTP ")}).`, "Wait a moment, confirm the snapshot host is reachable, then rerun sync.");
|
|
10
|
+
}
|
|
11
|
+
if (message.startsWith("managed_getblock_archive_http_")) {
|
|
12
|
+
return appendNextStep(`Getblock archive request failed (${message.replace("managed_getblock_archive_http_", "HTTP ")}).`, "Wait a moment, confirm the snapshot host is reachable, then rerun sync.");
|
|
13
|
+
}
|
|
14
|
+
if (message === "managed_getblock_archive_response_body_missing") {
|
|
15
|
+
return appendNextStep("Getblock archive server returned an empty response body.", "Wait a moment, confirm the snapshot host is reachable, then rerun sync.");
|
|
16
|
+
}
|
|
17
|
+
if (message === "managed_getblock_archive_resume_requires_partial_content") {
|
|
18
|
+
return appendNextStep("Getblock archive server ignored the resume request for a partial download.", "Wait a moment and rerun sync. If this keeps happening, confirm the snapshot host supports HTTP range requests.");
|
|
19
|
+
}
|
|
20
|
+
if (message.startsWith("managed_getblock_archive_chunk_sha256_mismatch_")) {
|
|
21
|
+
return appendNextStep("A downloaded getblock archive chunk was corrupted and was rolled back to the last verified checkpoint.", "Wait a moment and rerun sync. If this keeps happening, check local disk health and the stability of the archive download.");
|
|
22
|
+
}
|
|
23
|
+
if (message === "managed_getblock_archive_sha256_mismatch" || message === "managed_getblock_archive_truncated") {
|
|
24
|
+
return appendNextStep("The downloaded getblock archive did not match the published manifest.", "Rerun sync so the archive can be downloaded again. If this keeps happening, check local disk health and the snapshot host.");
|
|
25
|
+
}
|
|
8
26
|
if (message === "bitcoind_no_peers_for_header_sync_check_internet_or_firewall") {
|
|
9
27
|
return appendNextStep("No Bitcoin peers were available for header sync.", "Check your internet access and firewall rules for outbound Bitcoin connections, then rerun sync.");
|
|
10
28
|
}
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import net from "node:net";
|
|
3
3
|
import { access, constants, mkdir, readFile, rm } from "node:fs/promises";
|
|
4
4
|
import { serializeIndexerState } from "@cogcoin/indexer";
|
|
5
|
+
import { openManagedBitcoindClientInternal } from "./client.js";
|
|
5
6
|
import { openClient } from "../client.js";
|
|
6
7
|
import { openSqliteStore } from "../sqlite/index.js";
|
|
7
8
|
import { writeRuntimeStatusFile } from "../wallet/fs/status-file.js";
|
|
@@ -29,6 +30,9 @@ async function readJsonFile(filePath) {
|
|
|
29
30
|
throw error;
|
|
30
31
|
}
|
|
31
32
|
}
|
|
33
|
+
async function readManagedBitcoindStatus(paths) {
|
|
34
|
+
return readJsonFile(paths.bitcoindStatusPath);
|
|
35
|
+
}
|
|
32
36
|
async function readPackageVersionFromDisk() {
|
|
33
37
|
try {
|
|
34
38
|
const raw = await readFile(new URL("../../package.json", import.meta.url), "utf8");
|
|
@@ -168,6 +172,9 @@ async function main() {
|
|
|
168
172
|
let lastAppliedAtUnixMs = null;
|
|
169
173
|
let lastError = null;
|
|
170
174
|
let hasSuccessfulCoreTipRefresh = false;
|
|
175
|
+
let backgroundStore = null;
|
|
176
|
+
let backgroundClient = null;
|
|
177
|
+
let backgroundResumePromise = null;
|
|
171
178
|
await mkdir(paths.indexerServiceRoot, { recursive: true });
|
|
172
179
|
await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
|
|
173
180
|
const observeAppliedTip = (appliedTip, now) => {
|
|
@@ -264,6 +271,58 @@ async function main() {
|
|
|
264
271
|
lastError = leaseState.lastError;
|
|
265
272
|
return writeStatus();
|
|
266
273
|
};
|
|
274
|
+
const pauseBackgroundFollow = async () => {
|
|
275
|
+
const pendingResume = backgroundResumePromise;
|
|
276
|
+
backgroundResumePromise = null;
|
|
277
|
+
await pendingResume?.catch(() => undefined);
|
|
278
|
+
const client = backgroundClient;
|
|
279
|
+
const store = backgroundStore;
|
|
280
|
+
backgroundClient = null;
|
|
281
|
+
backgroundStore = null;
|
|
282
|
+
await client?.close().catch(() => undefined);
|
|
283
|
+
await store?.close().catch(() => undefined);
|
|
284
|
+
};
|
|
285
|
+
const resumeBackgroundFollow = async () => {
|
|
286
|
+
if (backgroundClient !== null) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (backgroundResumePromise !== null) {
|
|
290
|
+
return backgroundResumePromise;
|
|
291
|
+
}
|
|
292
|
+
backgroundResumePromise = (async () => {
|
|
293
|
+
const bitcoindStatus = await readManagedBitcoindStatus(paths);
|
|
294
|
+
const store = await openSqliteStore({ filename: databasePath });
|
|
295
|
+
try {
|
|
296
|
+
const client = await openManagedBitcoindClientInternal({
|
|
297
|
+
store,
|
|
298
|
+
dataDir,
|
|
299
|
+
chain: bitcoindStatus?.chain ?? "main",
|
|
300
|
+
startHeight: bitcoindStatus?.startHeight ?? 0,
|
|
301
|
+
walletRootId,
|
|
302
|
+
progressOutput: "none",
|
|
303
|
+
});
|
|
304
|
+
try {
|
|
305
|
+
await client.startFollowingTip();
|
|
306
|
+
backgroundStore = store;
|
|
307
|
+
backgroundClient = client;
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
await client.close().catch(() => undefined);
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
await store.close().catch(() => undefined);
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
})();
|
|
319
|
+
try {
|
|
320
|
+
await backgroundResumePromise;
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
backgroundResumePromise = null;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
267
326
|
const heartbeat = setInterval(() => {
|
|
268
327
|
void refreshStatus().catch(() => undefined);
|
|
269
328
|
const now = Date.now();
|
|
@@ -427,6 +486,24 @@ async function main() {
|
|
|
427
486
|
});
|
|
428
487
|
return;
|
|
429
488
|
}
|
|
489
|
+
if (request.method === "PauseBackgroundFollow") {
|
|
490
|
+
await pauseBackgroundFollow();
|
|
491
|
+
writeResponse({
|
|
492
|
+
id: request.id,
|
|
493
|
+
ok: true,
|
|
494
|
+
result: null,
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (request.method === "ResumeBackgroundFollow") {
|
|
499
|
+
await resumeBackgroundFollow();
|
|
500
|
+
writeResponse({
|
|
501
|
+
id: request.id,
|
|
502
|
+
ok: true,
|
|
503
|
+
result: null,
|
|
504
|
+
});
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
430
507
|
throw new Error(`indexer_daemon_unknown_method_${request.method}`);
|
|
431
508
|
}
|
|
432
509
|
catch (error) {
|
|
@@ -443,6 +520,7 @@ async function main() {
|
|
|
443
520
|
});
|
|
444
521
|
const shutdown = async () => {
|
|
445
522
|
clearInterval(heartbeat);
|
|
523
|
+
await pauseBackgroundFollow().catch(() => undefined);
|
|
446
524
|
state = "stopping";
|
|
447
525
|
heartbeatAtUnixMs = Date.now();
|
|
448
526
|
updatedAtUnixMs = heartbeatAtUnixMs;
|