@cogcoin/client 0.5.4 → 0.5.6
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 +1 -1
- package/dist/app-paths.d.ts +2 -0
- package/dist/app-paths.js +4 -0
- package/dist/art/wallet.txt +9 -9
- package/dist/bitcoind/bootstrap/chainstate.d.ts +2 -1
- package/dist/bitcoind/bootstrap/chainstate.js +4 -1
- package/dist/bitcoind/bootstrap/chunk-manifest.d.ts +14 -0
- package/dist/bitcoind/bootstrap/chunk-manifest.js +85 -0
- package/dist/bitcoind/bootstrap/chunk-recovery.d.ts +4 -0
- package/dist/bitcoind/bootstrap/chunk-recovery.js +122 -0
- package/dist/bitcoind/bootstrap/constants.d.ts +3 -1
- package/dist/bitcoind/bootstrap/constants.js +3 -1
- package/dist/bitcoind/bootstrap/controller.d.ts +10 -2
- package/dist/bitcoind/bootstrap/controller.js +56 -12
- package/dist/bitcoind/bootstrap/default-snapshot-chunk-manifest.d.ts +2 -0
- package/dist/bitcoind/bootstrap/default-snapshot-chunk-manifest.js +2309 -0
- package/dist/bitcoind/bootstrap/download.js +177 -83
- package/dist/bitcoind/bootstrap/headers.d.ts +16 -2
- package/dist/bitcoind/bootstrap/headers.js +124 -14
- package/dist/bitcoind/bootstrap/state.d.ts +11 -1
- package/dist/bitcoind/bootstrap/state.js +50 -23
- package/dist/bitcoind/bootstrap/types.d.ts +12 -1
- package/dist/bitcoind/client/factory.js +11 -2
- package/dist/bitcoind/client/internal-types.d.ts +1 -0
- package/dist/bitcoind/client/managed-client.d.ts +1 -1
- package/dist/bitcoind/client/managed-client.js +29 -15
- package/dist/bitcoind/client/sync-engine.js +88 -16
- package/dist/bitcoind/errors.js +9 -0
- package/dist/bitcoind/indexer-daemon.d.ts +7 -0
- package/dist/bitcoind/indexer-daemon.js +31 -22
- package/dist/bitcoind/processing-start-height.d.ts +7 -0
- package/dist/bitcoind/processing-start-height.js +9 -0
- package/dist/bitcoind/progress/controller.js +1 -0
- package/dist/bitcoind/progress/formatting.js +4 -1
- package/dist/bitcoind/retryable-rpc.d.ts +11 -0
- package/dist/bitcoind/retryable-rpc.js +30 -0
- package/dist/bitcoind/service.d.ts +16 -1
- package/dist/bitcoind/service.js +228 -115
- package/dist/bitcoind/testing.d.ts +1 -1
- package/dist/bitcoind/testing.js +1 -1
- package/dist/bitcoind/types.d.ts +10 -0
- package/dist/cli/commands/follow.js +9 -0
- package/dist/cli/commands/service-runtime.js +150 -134
- package/dist/cli/commands/sync.js +9 -0
- package/dist/cli/commands/wallet-admin.js +77 -21
- package/dist/cli/context.js +4 -2
- package/dist/cli/mutation-json.js +2 -0
- package/dist/cli/output.js +3 -1
- package/dist/cli/parse.d.ts +1 -1
- package/dist/cli/parse.js +6 -0
- package/dist/cli/preview-json.js +2 -0
- package/dist/cli/runner.js +1 -0
- package/dist/cli/types.d.ts +6 -3
- package/dist/cli/types.js +1 -1
- package/dist/cli/wallet-format.js +134 -14
- package/dist/wallet/lifecycle.d.ts +6 -0
- package/dist/wallet/lifecycle.js +168 -37
- package/dist/wallet/read/context.js +10 -4
- package/dist/wallet/reset.d.ts +61 -2
- package/dist/wallet/reset.js +208 -63
- package/dist/wallet/root-resolution.d.ts +20 -0
- package/dist/wallet/root-resolution.js +37 -0
- package/dist/wallet/runtime.d.ts +3 -0
- package/dist/wallet/runtime.js +3 -0
- package/dist/wallet/state/crypto.d.ts +3 -0
- package/dist/wallet/state/crypto.js +3 -0
- package/dist/wallet/state/pending-init.d.ts +24 -0
- package/dist/wallet/state/pending-init.js +59 -0
- package/dist/wallet/state/provider.d.ts +1 -0
- package/dist/wallet/state/provider.js +7 -1
- package/dist/wallet/state/storage.d.ts +7 -1
- package/dist/wallet/state/storage.js +39 -0
- package/dist/wallet/types.d.ts +9 -0
- package/package.json +4 -2
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { BOOTSTRAP_STATE_VERSION, DEFAULT_SNAPSHOT_METADATA, } from "./constants.js";
|
|
4
4
|
function createInitialBootstrapState(snapshot) {
|
|
5
5
|
return {
|
|
6
|
-
metadataVersion:
|
|
6
|
+
metadataVersion: BOOTSTRAP_STATE_VERSION,
|
|
7
7
|
snapshot,
|
|
8
8
|
phase: "snapshot_download",
|
|
9
|
+
integrityVersion: 0,
|
|
10
|
+
chunkSizeBytes: 0,
|
|
11
|
+
verifiedChunkCount: 0,
|
|
9
12
|
downloadedBytes: 0,
|
|
10
13
|
validated: false,
|
|
11
14
|
loadTxOutSetComplete: false,
|
|
@@ -21,30 +24,48 @@ async function writeJsonAtomic(path, payload) {
|
|
|
21
24
|
await writeFile(tempPath, JSON.stringify(payload, null, 2));
|
|
22
25
|
await rename(tempPath, path);
|
|
23
26
|
}
|
|
24
|
-
|
|
27
|
+
function snapshotIdentityMatches(parsed, snapshot) {
|
|
28
|
+
return parsed.snapshot?.sha256 === snapshot.sha256
|
|
29
|
+
&& parsed.snapshot?.sizeBytes === snapshot.sizeBytes
|
|
30
|
+
&& parsed.snapshot?.height === snapshot.height
|
|
31
|
+
&& parsed.snapshot?.filename === snapshot.filename;
|
|
32
|
+
}
|
|
33
|
+
function normalizeLoadedBootstrapState(parsed, snapshot) {
|
|
34
|
+
if (typeof parsed.downloadedBytes !== "number"
|
|
35
|
+
|| typeof parsed.validated !== "boolean"
|
|
36
|
+
|| typeof parsed.loadTxOutSetComplete !== "boolean") {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
metadataVersion: BOOTSTRAP_STATE_VERSION,
|
|
41
|
+
snapshot,
|
|
42
|
+
phase: parsed.phase ?? "snapshot_download",
|
|
43
|
+
integrityVersion: typeof parsed.integrityVersion === "number" ? parsed.integrityVersion : 0,
|
|
44
|
+
chunkSizeBytes: typeof parsed.chunkSizeBytes === "number" ? parsed.chunkSizeBytes : 0,
|
|
45
|
+
verifiedChunkCount: typeof parsed.verifiedChunkCount === "number" ? parsed.verifiedChunkCount : 0,
|
|
46
|
+
downloadedBytes: parsed.downloadedBytes,
|
|
47
|
+
validated: parsed.validated,
|
|
48
|
+
loadTxOutSetComplete: parsed.loadTxOutSetComplete,
|
|
49
|
+
baseHeight: parsed.baseHeight ?? null,
|
|
50
|
+
tipHashHex: parsed.tipHashHex ?? null,
|
|
51
|
+
lastError: parsed.lastError ?? null,
|
|
52
|
+
updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export async function loadBootstrapStateRecord(paths, snapshot) {
|
|
25
56
|
try {
|
|
26
57
|
const raw = await readFile(paths.statePath, "utf8");
|
|
27
58
|
const parsed = JSON.parse(raw);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
&& typeof parsed.validated === "boolean"
|
|
36
|
-
&& typeof parsed.loadTxOutSetComplete === "boolean") {
|
|
59
|
+
const snapshotIdentity = parsed.snapshot === undefined
|
|
60
|
+
? "unknown"
|
|
61
|
+
: snapshotIdentityMatches(parsed, snapshot)
|
|
62
|
+
? "current"
|
|
63
|
+
: "different";
|
|
64
|
+
const normalized = normalizeLoadedBootstrapState(parsed, snapshot);
|
|
65
|
+
if (normalized !== null) {
|
|
37
66
|
return {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
phase: parsed.phase ?? "snapshot_download",
|
|
41
|
-
downloadedBytes: parsed.downloadedBytes,
|
|
42
|
-
validated: parsed.validated,
|
|
43
|
-
loadTxOutSetComplete: parsed.loadTxOutSetComplete,
|
|
44
|
-
baseHeight: parsed.baseHeight ?? null,
|
|
45
|
-
tipHashHex: parsed.tipHashHex ?? null,
|
|
46
|
-
lastError: parsed.lastError ?? null,
|
|
47
|
-
updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
|
|
67
|
+
state: normalized,
|
|
68
|
+
snapshotIdentity,
|
|
48
69
|
};
|
|
49
70
|
}
|
|
50
71
|
}
|
|
@@ -53,7 +74,13 @@ export async function loadBootstrapState(paths, snapshot) {
|
|
|
53
74
|
}
|
|
54
75
|
const state = createInitialBootstrapState(snapshot);
|
|
55
76
|
await writeJsonAtomic(paths.statePath, state);
|
|
56
|
-
return
|
|
77
|
+
return {
|
|
78
|
+
state,
|
|
79
|
+
snapshotIdentity: "unknown",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export async function loadBootstrapState(paths, snapshot) {
|
|
83
|
+
return (await loadBootstrapStateRecord(paths, snapshot)).state;
|
|
57
84
|
}
|
|
58
85
|
export async function saveBootstrapState(paths, state) {
|
|
59
86
|
state.updatedAt = Date.now();
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ManagedProgressController } from "../progress.js";
|
|
2
|
-
import type { BootstrapPhase, SnapshotMetadata } from "../types.js";
|
|
2
|
+
import type { BootstrapPhase, SnapshotChunkManifest, SnapshotMetadata } from "../types.js";
|
|
3
3
|
export interface BootstrapPersistentState {
|
|
4
4
|
metadataVersion: number;
|
|
5
5
|
snapshot: SnapshotMetadata;
|
|
6
6
|
phase: BootstrapPhase;
|
|
7
|
+
integrityVersion: number;
|
|
8
|
+
chunkSizeBytes: number;
|
|
9
|
+
verifiedChunkCount: number;
|
|
7
10
|
downloadedBytes: number;
|
|
8
11
|
validated: boolean;
|
|
9
12
|
loadTxOutSetComplete: boolean;
|
|
@@ -12,6 +15,11 @@ export interface BootstrapPersistentState {
|
|
|
12
15
|
lastError: string | null;
|
|
13
16
|
updatedAt: number;
|
|
14
17
|
}
|
|
18
|
+
export type BootstrapStateSnapshotIdentity = "current" | "different" | "unknown";
|
|
19
|
+
export interface LoadedBootstrapState {
|
|
20
|
+
state: BootstrapPersistentState;
|
|
21
|
+
snapshotIdentity: BootstrapStateSnapshotIdentity;
|
|
22
|
+
}
|
|
15
23
|
export interface BootstrapPaths {
|
|
16
24
|
directory: string;
|
|
17
25
|
snapshotPath: string;
|
|
@@ -22,7 +30,10 @@ export interface BootstrapPaths {
|
|
|
22
30
|
export interface DownloadSnapshotOptions {
|
|
23
31
|
fetchImpl?: typeof fetch;
|
|
24
32
|
metadata: SnapshotMetadata;
|
|
33
|
+
manifest?: SnapshotChunkManifest;
|
|
25
34
|
paths: BootstrapPaths;
|
|
26
35
|
progress: Pick<ManagedProgressController, "setPhase">;
|
|
27
36
|
state: BootstrapPersistentState;
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
snapshotIdentity?: BootstrapStateSnapshotIdentity;
|
|
28
39
|
}
|
|
@@ -4,12 +4,18 @@ import { openClient } from "../../client.js";
|
|
|
4
4
|
import { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, 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
9
|
import { attachOrStartManagedBitcoindService } 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();
|
|
14
|
+
assertCogcoinProcessingStartHeight({
|
|
15
|
+
chain: options.chain,
|
|
16
|
+
startHeight: options.startHeight,
|
|
17
|
+
genesisParameters,
|
|
18
|
+
});
|
|
13
19
|
const dataDir = options.dataDir ?? resolveDefaultBitcoindDataDirForTesting();
|
|
14
20
|
const node = await attachOrStartManagedBitcoindService({
|
|
15
21
|
...options,
|
|
@@ -41,7 +47,10 @@ async function createManagedBitcoindClient(options) {
|
|
|
41
47
|
startupTimeoutMs: options.startupTimeoutMs,
|
|
42
48
|
})
|
|
43
49
|
: null;
|
|
44
|
-
|
|
50
|
+
// The persistent service may already exist from a non-processing attach path
|
|
51
|
+
// that used startHeight 0. Cogcoin replay still begins at the requested
|
|
52
|
+
// processing boundary for this managed client.
|
|
53
|
+
return new DefaultManagedBitcoindClient(client, options.store, node, rpc, progress, bootstrap, indexerDaemon, options.startHeight, options.syncDebounceMs ?? DEFAULT_SYNC_DEBOUNCE_MS);
|
|
45
54
|
}
|
|
46
55
|
export async function openManagedBitcoindClient(options) {
|
|
47
56
|
const genesisParameters = options.genesisParameters ?? await loadBundledGenesisParameters();
|
|
@@ -49,7 +58,7 @@ export async function openManagedBitcoindClient(options) {
|
|
|
49
58
|
...options,
|
|
50
59
|
genesisParameters,
|
|
51
60
|
chain: "main",
|
|
52
|
-
startHeight: genesisParameters
|
|
61
|
+
startHeight: resolveCogcoinProcessingStartHeight(genesisParameters),
|
|
53
62
|
});
|
|
54
63
|
}
|
|
55
64
|
export async function openManagedBitcoindClientInternal(options) {
|
|
@@ -23,6 +23,7 @@ export interface SyncEngineDependencies {
|
|
|
23
23
|
startHeight: number;
|
|
24
24
|
bitcoinRateTracker: BlockRateTracker;
|
|
25
25
|
cogcoinRateTracker: BlockRateTracker;
|
|
26
|
+
abortSignal?: AbortSignal;
|
|
26
27
|
isFollowing(): boolean;
|
|
27
28
|
loadVisibleFollowBlockTimes(tip: Awaited<ReturnType<Client["getTip"]>>): Promise<Record<number, number>>;
|
|
28
29
|
}
|
|
@@ -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>;
|
|
@@ -21,7 +21,8 @@ export class DefaultManagedBitcoindClient {
|
|
|
21
21
|
#cogcoinRateTracker = createBlockRateTracker();
|
|
22
22
|
#syncPromise = Promise.resolve(createInitialSyncResult());
|
|
23
23
|
#debounceTimer = null;
|
|
24
|
-
|
|
24
|
+
#syncAbortControllers = new Set();
|
|
25
|
+
constructor(client, store, node, rpc, progress, bootstrap, indexerDaemon, startHeight, syncDebounceMs) {
|
|
25
26
|
this.#client = client;
|
|
26
27
|
this.#store = store;
|
|
27
28
|
this.#node = node;
|
|
@@ -29,7 +30,7 @@ export class DefaultManagedBitcoindClient {
|
|
|
29
30
|
this.#progress = progress;
|
|
30
31
|
this.#bootstrap = bootstrap;
|
|
31
32
|
this.#indexerDaemon = indexerDaemon;
|
|
32
|
-
this.#startHeight =
|
|
33
|
+
this.#startHeight = startHeight;
|
|
33
34
|
this.#syncDebounceMs = syncDebounceMs;
|
|
34
35
|
}
|
|
35
36
|
async getTip() {
|
|
@@ -47,19 +48,29 @@ export class DefaultManagedBitcoindClient {
|
|
|
47
48
|
async syncToTip() {
|
|
48
49
|
this.#assertOpen();
|
|
49
50
|
await this.#progress.start();
|
|
50
|
-
const run = async () =>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
const run = async () => {
|
|
52
|
+
const abortController = new AbortController();
|
|
53
|
+
this.#syncAbortControllers.add(abortController);
|
|
54
|
+
try {
|
|
55
|
+
return await runManagedSync({
|
|
56
|
+
client: this.#client,
|
|
57
|
+
store: this.#store,
|
|
58
|
+
node: this.#node,
|
|
59
|
+
rpc: this.#rpc,
|
|
60
|
+
progress: this.#progress,
|
|
61
|
+
bootstrap: this.#bootstrap,
|
|
62
|
+
startHeight: this.#startHeight,
|
|
63
|
+
bitcoinRateTracker: this.#bitcoinRateTracker,
|
|
64
|
+
cogcoinRateTracker: this.#cogcoinRateTracker,
|
|
65
|
+
abortSignal: abortController.signal,
|
|
66
|
+
isFollowing: () => this.#following,
|
|
67
|
+
loadVisibleFollowBlockTimes: (tip) => this.#loadVisibleFollowBlockTimes(tip),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
this.#syncAbortControllers.delete(abortController);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
63
74
|
const nextPromise = this.#syncPromise.then(run, run);
|
|
64
75
|
this.#syncPromise = nextPromise;
|
|
65
76
|
return nextPromise;
|
|
@@ -159,6 +170,9 @@ export class DefaultManagedBitcoindClient {
|
|
|
159
170
|
this.#subscriber = null;
|
|
160
171
|
this.#followLoop = null;
|
|
161
172
|
this.#pollTimer = null;
|
|
173
|
+
for (const abortController of this.#syncAbortControllers) {
|
|
174
|
+
abortController.abort(new Error("managed_sync_aborted"));
|
|
175
|
+
}
|
|
162
176
|
await this.#syncPromise.catch(() => undefined);
|
|
163
177
|
await this.#progress.close();
|
|
164
178
|
await this.#node.stop();
|
|
@@ -1,10 +1,41 @@
|
|
|
1
1
|
import { formatManagedSyncErrorMessage } from "../errors.js";
|
|
2
2
|
import { normalizeRpcBlock } from "../normalize.js";
|
|
3
|
+
import { MANAGED_RPC_RETRY_MESSAGE, consumeManagedRpcRetryDelayMs, createManagedRpcRetryState, describeManagedRpcRetryError, isRetryableManagedRpcError, resetManagedRpcRetryState, } from "../retryable-rpc.js";
|
|
3
4
|
import { estimateEtaSeconds } from "./rate-tracker.js";
|
|
4
5
|
const DEFAULT_SYNC_CATCH_UP_POLL_MS = 2_000;
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
function createAbortError(signal) {
|
|
7
|
+
const reason = signal?.reason;
|
|
8
|
+
if (reason instanceof Error) {
|
|
9
|
+
return reason;
|
|
10
|
+
}
|
|
11
|
+
const error = new Error("managed_sync_aborted");
|
|
12
|
+
error.name = "AbortError";
|
|
13
|
+
return error;
|
|
14
|
+
}
|
|
15
|
+
function isAbortError(error, signal) {
|
|
16
|
+
if (signal?.aborted) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return error instanceof Error
|
|
20
|
+
&& (error.name === "AbortError" || error.message === "managed_sync_aborted");
|
|
21
|
+
}
|
|
22
|
+
function throwIfAborted(signal) {
|
|
23
|
+
if (signal?.aborted) {
|
|
24
|
+
throw createAbortError(signal);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function sleep(ms, signal) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const timer = setTimeout(() => {
|
|
30
|
+
signal?.removeEventListener("abort", onAbort);
|
|
31
|
+
resolve();
|
|
32
|
+
}, ms);
|
|
33
|
+
const onAbort = () => {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
signal?.removeEventListener("abort", onAbort);
|
|
36
|
+
reject(createAbortError(signal));
|
|
37
|
+
};
|
|
38
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
8
39
|
});
|
|
9
40
|
}
|
|
10
41
|
async function setBitcoinSyncProgress(dependencies, info) {
|
|
@@ -14,12 +45,42 @@ async function setBitcoinSyncProgress(dependencies, info) {
|
|
|
14
45
|
headers: info.headers,
|
|
15
46
|
targetHeight: info.headers,
|
|
16
47
|
etaSeconds,
|
|
48
|
+
lastError: null,
|
|
17
49
|
message: dependencies.node.expectedChain === "main"
|
|
18
50
|
? "Bitcoin Core is syncing blocks after assumeutxo bootstrap."
|
|
19
51
|
: "Reading blocks from the managed Bitcoin node.",
|
|
20
52
|
});
|
|
21
53
|
}
|
|
22
|
-
async function
|
|
54
|
+
async function setRetryingProgress(dependencies, error) {
|
|
55
|
+
const status = dependencies.progress.getStatusSnapshot();
|
|
56
|
+
const { phase: _phase, updatedAt: _updatedAt, ...progress } = status.bootstrapProgress;
|
|
57
|
+
await dependencies.progress.setPhase(status.bootstrapPhase, {
|
|
58
|
+
...progress,
|
|
59
|
+
lastError: describeManagedRpcRetryError(error),
|
|
60
|
+
message: MANAGED_RPC_RETRY_MESSAGE,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function runWithManagedRpcRetry(dependencies, retryState, operation) {
|
|
64
|
+
while (true) {
|
|
65
|
+
throwIfAborted(dependencies.abortSignal);
|
|
66
|
+
try {
|
|
67
|
+
const result = await operation();
|
|
68
|
+
resetManagedRpcRetryState(retryState);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (isAbortError(error, dependencies.abortSignal)) {
|
|
73
|
+
throw createAbortError(dependencies.abortSignal);
|
|
74
|
+
}
|
|
75
|
+
if (!isRetryableManagedRpcError(error)) {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
await setRetryingProgress(dependencies, error);
|
|
79
|
+
await sleep(consumeManagedRpcRetryDelayMs(retryState), dependencies.abortSignal);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function findCommonAncestor(dependencies, tip, bestHeight, runRpc) {
|
|
23
84
|
const startHeight = Math.min(tip.height, bestHeight);
|
|
24
85
|
for (let height = startHeight; height >= dependencies.startHeight; height -= 1) {
|
|
25
86
|
const localHashHex = height === tip.height
|
|
@@ -28,14 +89,14 @@ async function findCommonAncestor(dependencies, tip, bestHeight) {
|
|
|
28
89
|
if (localHashHex === null) {
|
|
29
90
|
continue;
|
|
30
91
|
}
|
|
31
|
-
const chainHashHex = await dependencies.rpc.getBlockHash(height);
|
|
92
|
+
const chainHashHex = await runRpc(() => dependencies.rpc.getBlockHash(height));
|
|
32
93
|
if (chainHashHex === localHashHex) {
|
|
33
94
|
return height;
|
|
34
95
|
}
|
|
35
96
|
}
|
|
36
97
|
return dependencies.startHeight - 1;
|
|
37
98
|
}
|
|
38
|
-
async function syncAgainstBestHeight(dependencies, bestHeight) {
|
|
99
|
+
async function syncAgainstBestHeight(dependencies, bestHeight, runRpc) {
|
|
39
100
|
if (bestHeight < dependencies.startHeight) {
|
|
40
101
|
return {
|
|
41
102
|
appliedBlocks: 0,
|
|
@@ -47,7 +108,7 @@ async function syncAgainstBestHeight(dependencies, bestHeight) {
|
|
|
47
108
|
let rewoundBlocks = 0;
|
|
48
109
|
let commonAncestorHeight = null;
|
|
49
110
|
if (startTip !== null) {
|
|
50
|
-
const rewindTarget = await findCommonAncestor(dependencies, startTip, bestHeight);
|
|
111
|
+
const rewindTarget = await findCommonAncestor(dependencies, startTip, bestHeight, runRpc);
|
|
51
112
|
if (rewindTarget < startTip.height) {
|
|
52
113
|
commonAncestorHeight = rewindTarget < dependencies.startHeight ? null : rewindTarget;
|
|
53
114
|
await dependencies.client.rewindToHeight(rewindTarget);
|
|
@@ -63,8 +124,8 @@ async function syncAgainstBestHeight(dependencies, bestHeight) {
|
|
|
63
124
|
await dependencies.progress.setCogcoinSync(nextHeight - 1, bestHeight, estimateEtaSeconds(dependencies.cogcoinRateTracker, nextHeight - 1, bestHeight));
|
|
64
125
|
}
|
|
65
126
|
for (let height = nextHeight; height <= bestHeight; height += 1) {
|
|
66
|
-
const blockHashHex = await dependencies.rpc.getBlockHash(height);
|
|
67
|
-
const rpcBlock = await dependencies.rpc.getBlock(blockHashHex);
|
|
127
|
+
const blockHashHex = await runRpc(() => dependencies.rpc.getBlockHash(height));
|
|
128
|
+
const rpcBlock = await runRpc(() => dependencies.rpc.getBlock(blockHashHex));
|
|
68
129
|
const normalizedBlock = normalizeRpcBlock(rpcBlock);
|
|
69
130
|
await dependencies.client.applyBlock(normalizedBlock);
|
|
70
131
|
if (typeof rpcBlock.time === "number") {
|
|
@@ -81,9 +142,15 @@ async function syncAgainstBestHeight(dependencies, bestHeight) {
|
|
|
81
142
|
}
|
|
82
143
|
export async function syncToTip(dependencies) {
|
|
83
144
|
try {
|
|
84
|
-
|
|
145
|
+
const retryState = createManagedRpcRetryState();
|
|
146
|
+
const runRpc = (operation) => runWithManagedRpcRetry(dependencies, retryState, operation);
|
|
147
|
+
throwIfAborted(dependencies.abortSignal);
|
|
148
|
+
await runRpc(() => dependencies.node.validate());
|
|
85
149
|
const indexedTipBeforeBootstrap = await dependencies.client.getTip();
|
|
86
|
-
await dependencies.bootstrap.ensureReady(indexedTipBeforeBootstrap, dependencies.node.expectedChain
|
|
150
|
+
await runRpc(() => dependencies.bootstrap.ensureReady(indexedTipBeforeBootstrap, dependencies.node.expectedChain, {
|
|
151
|
+
signal: dependencies.abortSignal,
|
|
152
|
+
retryState,
|
|
153
|
+
}));
|
|
87
154
|
const startTip = await dependencies.client.getTip();
|
|
88
155
|
const aggregate = {
|
|
89
156
|
appliedBlocks: 0,
|
|
@@ -95,9 +162,10 @@ export async function syncToTip(dependencies) {
|
|
|
95
162
|
bestHashHex: "",
|
|
96
163
|
};
|
|
97
164
|
while (true) {
|
|
98
|
-
|
|
165
|
+
throwIfAborted(dependencies.abortSignal);
|
|
166
|
+
const startInfo = await runRpc(() => dependencies.rpc.getBlockchainInfo());
|
|
99
167
|
await setBitcoinSyncProgress(dependencies, startInfo);
|
|
100
|
-
const pass = await syncAgainstBestHeight(dependencies, startInfo.blocks);
|
|
168
|
+
const pass = await syncAgainstBestHeight(dependencies, startInfo.blocks, runRpc);
|
|
101
169
|
aggregate.appliedBlocks += pass.appliedBlocks;
|
|
102
170
|
aggregate.rewoundBlocks += pass.rewoundBlocks;
|
|
103
171
|
if (pass.commonAncestorHeight !== null) {
|
|
@@ -106,19 +174,20 @@ export async function syncToTip(dependencies) {
|
|
|
106
174
|
: Math.min(aggregate.commonAncestorHeight, pass.commonAncestorHeight);
|
|
107
175
|
}
|
|
108
176
|
const finalTip = await dependencies.client.getTip();
|
|
109
|
-
const endInfo = await dependencies.rpc.getBlockchainInfo();
|
|
177
|
+
const endInfo = await runRpc(() => dependencies.rpc.getBlockchainInfo());
|
|
110
178
|
const caughtUpCogcoin = endInfo.blocks < dependencies.startHeight || finalTip?.height === endInfo.blocks;
|
|
111
179
|
aggregate.endingHeight = finalTip?.height ?? null;
|
|
112
180
|
aggregate.bestHeight = endInfo.blocks;
|
|
113
181
|
aggregate.bestHashHex = endInfo.bestblockhash;
|
|
114
182
|
if (endInfo.blocks === endInfo.headers && caughtUpCogcoin) {
|
|
115
183
|
if (dependencies.isFollowing()) {
|
|
116
|
-
dependencies.progress.replaceFollowBlockTimes(await dependencies.loadVisibleFollowBlockTimes(finalTip));
|
|
184
|
+
dependencies.progress.replaceFollowBlockTimes(await runRpc(() => dependencies.loadVisibleFollowBlockTimes(finalTip)));
|
|
117
185
|
}
|
|
118
186
|
await dependencies.progress.setPhase(dependencies.isFollowing() ? "follow_tip" : "complete", {
|
|
119
187
|
blocks: endInfo.blocks,
|
|
120
188
|
headers: endInfo.headers,
|
|
121
189
|
targetHeight: endInfo.headers,
|
|
190
|
+
lastError: null,
|
|
122
191
|
message: dependencies.isFollowing()
|
|
123
192
|
? "Following the live Bitcoin tip."
|
|
124
193
|
: "Managed sync fully caught up to the live tip.",
|
|
@@ -129,10 +198,13 @@ export async function syncToTip(dependencies) {
|
|
|
129
198
|
if (endInfo.blocks >= dependencies.startHeight && finalTip?.height !== endInfo.blocks) {
|
|
130
199
|
continue;
|
|
131
200
|
}
|
|
132
|
-
await sleep(DEFAULT_SYNC_CATCH_UP_POLL_MS);
|
|
201
|
+
await sleep(DEFAULT_SYNC_CATCH_UP_POLL_MS, dependencies.abortSignal);
|
|
133
202
|
}
|
|
134
203
|
}
|
|
135
204
|
catch (error) {
|
|
205
|
+
if (isAbortError(error, dependencies.abortSignal)) {
|
|
206
|
+
throw createAbortError(dependencies.abortSignal);
|
|
207
|
+
}
|
|
136
208
|
const message = formatManagedSyncErrorMessage(error instanceof Error ? error.message : String(error));
|
|
137
209
|
await dependencies.progress.setPhase("error", {
|
|
138
210
|
lastError: message,
|
package/dist/bitcoind/errors.js
CHANGED
|
@@ -18,6 +18,15 @@ export function formatManagedSyncErrorMessage(message) {
|
|
|
18
18
|
if (message === "snapshot_response_body_missing") {
|
|
19
19
|
return appendNextStep("Snapshot server returned an empty response body.", "Wait a moment, confirm the snapshot host is reachable, then rerun sync.");
|
|
20
20
|
}
|
|
21
|
+
if (message === "snapshot_resume_requires_partial_content") {
|
|
22
|
+
return appendNextStep("Snapshot 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.");
|
|
23
|
+
}
|
|
24
|
+
if (message.startsWith("snapshot_chunk_sha256_mismatch_")) {
|
|
25
|
+
return appendNextStep("A downloaded snapshot 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 snapshot connection.");
|
|
26
|
+
}
|
|
27
|
+
if (message.startsWith("snapshot_download_incomplete_")) {
|
|
28
|
+
return appendNextStep("Snapshot download ended before the expected file size was reached.", "Wait a moment and rerun sync. The downloader will resume from the last verified checkpoint.");
|
|
29
|
+
}
|
|
21
30
|
if (message === "bitcoind_cookie_timeout") {
|
|
22
31
|
return appendNextStep("The managed Bitcoin node did not finish starting in time.", "Check the node logs and local permissions for the Bitcoin data directory, then rerun sync.");
|
|
23
32
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ManagedIndexerDaemonObservedStatus, type ManagedIndexerDaemonStatus } from "./types.js";
|
|
2
|
+
import { resolveManagedServicePaths } from "./service-paths.js";
|
|
2
3
|
interface DaemonRequest {
|
|
3
4
|
id: string;
|
|
4
5
|
method: "GetStatus" | "OpenSnapshot" | "ReadSnapshot" | "CloseSnapshot";
|
|
@@ -81,6 +82,12 @@ export interface CoherentIndexerSnapshotLease {
|
|
|
81
82
|
payload: IndexerSnapshotPayload;
|
|
82
83
|
status: ManagedIndexerDaemonStatus;
|
|
83
84
|
}
|
|
85
|
+
export declare function stopIndexerDaemonServiceWithLockHeld(options: {
|
|
86
|
+
dataDir: string;
|
|
87
|
+
walletRootId?: string;
|
|
88
|
+
shutdownTimeoutMs?: number;
|
|
89
|
+
paths?: ReturnType<typeof resolveManagedServicePaths>;
|
|
90
|
+
}): Promise<IndexerDaemonStopResult>;
|
|
84
91
|
export declare function probeIndexerDaemon(options: {
|
|
85
92
|
dataDir: string;
|
|
86
93
|
walletRootId?: string;
|
|
@@ -53,6 +53,33 @@ async function clearIndexerDaemonRuntimeArtifacts(paths) {
|
|
|
53
53
|
await rm(paths.indexerDaemonStatusPath, { force: true }).catch(() => undefined);
|
|
54
54
|
await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
|
|
55
55
|
}
|
|
56
|
+
export async function stopIndexerDaemonServiceWithLockHeld(options) {
|
|
57
|
+
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
58
|
+
const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
59
|
+
const status = await readJsonFile(paths.indexerDaemonStatusPath);
|
|
60
|
+
const processId = status?.processId ?? null;
|
|
61
|
+
if (status === null || processId === null || !await isProcessAlive(processId)) {
|
|
62
|
+
await clearIndexerDaemonRuntimeArtifacts(paths);
|
|
63
|
+
return {
|
|
64
|
+
status: "not-running",
|
|
65
|
+
walletRootId,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
process.kill(processId, "SIGTERM");
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
await waitForProcessExit(processId, options.shutdownTimeoutMs ?? 5_000, "indexer_daemon_stop_timeout");
|
|
77
|
+
await clearIndexerDaemonRuntimeArtifacts(paths);
|
|
78
|
+
return {
|
|
79
|
+
status: "stopped",
|
|
80
|
+
walletRootId,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
56
83
|
function createIndexerDaemonClient(socketPath) {
|
|
57
84
|
async function sendRequest(request) {
|
|
58
85
|
return new Promise((resolve, reject) => {
|
|
@@ -380,29 +407,11 @@ export async function stopIndexerDaemonService(options) {
|
|
|
380
407
|
dataDir: options.dataDir,
|
|
381
408
|
});
|
|
382
409
|
try {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (status === null || processId === null || !await isProcessAlive(processId)) {
|
|
386
|
-
await clearIndexerDaemonRuntimeArtifacts(paths);
|
|
387
|
-
return {
|
|
388
|
-
status: "not-running",
|
|
389
|
-
walletRootId,
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
try {
|
|
393
|
-
process.kill(processId, "SIGTERM");
|
|
394
|
-
}
|
|
395
|
-
catch (error) {
|
|
396
|
-
if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
|
|
397
|
-
throw error;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
await waitForProcessExit(processId, options.shutdownTimeoutMs ?? 5_000, "indexer_daemon_stop_timeout");
|
|
401
|
-
await clearIndexerDaemonRuntimeArtifacts(paths);
|
|
402
|
-
return {
|
|
403
|
-
status: "stopped",
|
|
410
|
+
return await stopIndexerDaemonServiceWithLockHeld({
|
|
411
|
+
...options,
|
|
404
412
|
walletRootId,
|
|
405
|
-
|
|
413
|
+
paths,
|
|
414
|
+
});
|
|
406
415
|
}
|
|
407
416
|
finally {
|
|
408
417
|
await lock.release();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GenesisParameters } from "@cogcoin/indexer/types";
|
|
2
|
+
export declare function resolveCogcoinProcessingStartHeight(genesisParameters: GenesisParameters): number;
|
|
3
|
+
export declare function assertCogcoinProcessingStartHeight(options: {
|
|
4
|
+
chain: "main" | "regtest";
|
|
5
|
+
startHeight: number;
|
|
6
|
+
genesisParameters: GenesisParameters;
|
|
7
|
+
}): void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function resolveCogcoinProcessingStartHeight(genesisParameters) {
|
|
2
|
+
return genesisParameters.genesisBlock;
|
|
3
|
+
}
|
|
4
|
+
export function assertCogcoinProcessingStartHeight(options) {
|
|
5
|
+
const processingStartHeight = resolveCogcoinProcessingStartHeight(options.genesisParameters);
|
|
6
|
+
if (options.chain === "main" && options.startHeight < processingStartHeight) {
|
|
7
|
+
throw new Error("cogcoin_processing_start_height_before_genesis");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -4,7 +4,7 @@ export function createDefaultMessage(phase) {
|
|
|
4
4
|
case "snapshot_download":
|
|
5
5
|
return "Downloading UTXO snapshot.";
|
|
6
6
|
case "wait_headers_for_snapshot":
|
|
7
|
-
return "
|
|
7
|
+
return "Pre-synchronizing blockheaders.";
|
|
8
8
|
case "load_snapshot":
|
|
9
9
|
return "Loading the UTXO snapshot into bitcoind.";
|
|
10
10
|
case "bitcoin_sync":
|
|
@@ -125,6 +125,9 @@ export function resolveStatusFieldText(progress, snapshotHeight, now = 0) {
|
|
|
125
125
|
case "snapshot_download":
|
|
126
126
|
return `Downloading snapshot to ${snapshotHeight}${animateStatusEllipsis(now)}`;
|
|
127
127
|
case "wait_headers_for_snapshot":
|
|
128
|
+
return progress.message === "Waiting for Bitcoin headers to reach the snapshot height."
|
|
129
|
+
? `Waiting for Bitcoin headers to reach the snapshot height${animateStatusEllipsis(now)}`
|
|
130
|
+
: `Pre-synchronizing blockheaders${animateStatusEllipsis(now)}`;
|
|
128
131
|
case "load_snapshot":
|
|
129
132
|
case "bitcoin_sync":
|
|
130
133
|
return `Syncing Bitcoin Blocks${animateStatusEllipsis(now)}`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const MANAGED_RPC_RETRY_BASE_MS = 1000;
|
|
2
|
+
export declare const MANAGED_RPC_RETRY_MAX_MS = 15000;
|
|
3
|
+
export declare const MANAGED_RPC_RETRY_MESSAGE = "Managed Bitcoin RPC temporarily unavailable; retrying until canceled.";
|
|
4
|
+
export interface ManagedRpcRetryState {
|
|
5
|
+
nextDelayMs: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function createManagedRpcRetryState(): ManagedRpcRetryState;
|
|
8
|
+
export declare function resetManagedRpcRetryState(state: ManagedRpcRetryState): void;
|
|
9
|
+
export declare function consumeManagedRpcRetryDelayMs(state: ManagedRpcRetryState): number;
|
|
10
|
+
export declare function isRetryableManagedRpcError(error: unknown): boolean;
|
|
11
|
+
export declare function describeManagedRpcRetryError(error: unknown): string;
|