@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.
Files changed (74) hide show
  1. package/README.md +1 -1
  2. package/dist/app-paths.d.ts +2 -0
  3. package/dist/app-paths.js +4 -0
  4. package/dist/art/wallet.txt +9 -9
  5. package/dist/bitcoind/bootstrap/chainstate.d.ts +2 -1
  6. package/dist/bitcoind/bootstrap/chainstate.js +4 -1
  7. package/dist/bitcoind/bootstrap/chunk-manifest.d.ts +14 -0
  8. package/dist/bitcoind/bootstrap/chunk-manifest.js +85 -0
  9. package/dist/bitcoind/bootstrap/chunk-recovery.d.ts +4 -0
  10. package/dist/bitcoind/bootstrap/chunk-recovery.js +122 -0
  11. package/dist/bitcoind/bootstrap/constants.d.ts +3 -1
  12. package/dist/bitcoind/bootstrap/constants.js +3 -1
  13. package/dist/bitcoind/bootstrap/controller.d.ts +10 -2
  14. package/dist/bitcoind/bootstrap/controller.js +56 -12
  15. package/dist/bitcoind/bootstrap/default-snapshot-chunk-manifest.d.ts +2 -0
  16. package/dist/bitcoind/bootstrap/default-snapshot-chunk-manifest.js +2309 -0
  17. package/dist/bitcoind/bootstrap/download.js +177 -83
  18. package/dist/bitcoind/bootstrap/headers.d.ts +16 -2
  19. package/dist/bitcoind/bootstrap/headers.js +124 -14
  20. package/dist/bitcoind/bootstrap/state.d.ts +11 -1
  21. package/dist/bitcoind/bootstrap/state.js +50 -23
  22. package/dist/bitcoind/bootstrap/types.d.ts +12 -1
  23. package/dist/bitcoind/client/factory.js +11 -2
  24. package/dist/bitcoind/client/internal-types.d.ts +1 -0
  25. package/dist/bitcoind/client/managed-client.d.ts +1 -1
  26. package/dist/bitcoind/client/managed-client.js +29 -15
  27. package/dist/bitcoind/client/sync-engine.js +88 -16
  28. package/dist/bitcoind/errors.js +9 -0
  29. package/dist/bitcoind/indexer-daemon.d.ts +7 -0
  30. package/dist/bitcoind/indexer-daemon.js +31 -22
  31. package/dist/bitcoind/processing-start-height.d.ts +7 -0
  32. package/dist/bitcoind/processing-start-height.js +9 -0
  33. package/dist/bitcoind/progress/controller.js +1 -0
  34. package/dist/bitcoind/progress/formatting.js +4 -1
  35. package/dist/bitcoind/retryable-rpc.d.ts +11 -0
  36. package/dist/bitcoind/retryable-rpc.js +30 -0
  37. package/dist/bitcoind/service.d.ts +16 -1
  38. package/dist/bitcoind/service.js +228 -115
  39. package/dist/bitcoind/testing.d.ts +1 -1
  40. package/dist/bitcoind/testing.js +1 -1
  41. package/dist/bitcoind/types.d.ts +10 -0
  42. package/dist/cli/commands/follow.js +9 -0
  43. package/dist/cli/commands/service-runtime.js +150 -134
  44. package/dist/cli/commands/sync.js +9 -0
  45. package/dist/cli/commands/wallet-admin.js +77 -21
  46. package/dist/cli/context.js +4 -2
  47. package/dist/cli/mutation-json.js +2 -0
  48. package/dist/cli/output.js +3 -1
  49. package/dist/cli/parse.d.ts +1 -1
  50. package/dist/cli/parse.js +6 -0
  51. package/dist/cli/preview-json.js +2 -0
  52. package/dist/cli/runner.js +1 -0
  53. package/dist/cli/types.d.ts +6 -3
  54. package/dist/cli/types.js +1 -1
  55. package/dist/cli/wallet-format.js +134 -14
  56. package/dist/wallet/lifecycle.d.ts +6 -0
  57. package/dist/wallet/lifecycle.js +168 -37
  58. package/dist/wallet/read/context.js +10 -4
  59. package/dist/wallet/reset.d.ts +61 -2
  60. package/dist/wallet/reset.js +208 -63
  61. package/dist/wallet/root-resolution.d.ts +20 -0
  62. package/dist/wallet/root-resolution.js +37 -0
  63. package/dist/wallet/runtime.d.ts +3 -0
  64. package/dist/wallet/runtime.js +3 -0
  65. package/dist/wallet/state/crypto.d.ts +3 -0
  66. package/dist/wallet/state/crypto.js +3 -0
  67. package/dist/wallet/state/pending-init.d.ts +24 -0
  68. package/dist/wallet/state/pending-init.js +59 -0
  69. package/dist/wallet/state/provider.d.ts +1 -0
  70. package/dist/wallet/state/provider.js +7 -1
  71. package/dist/wallet/state/storage.d.ts +7 -1
  72. package/dist/wallet/state/storage.js +39 -0
  73. package/dist/wallet/types.d.ts +9 -0
  74. package/package.json +4 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@0.5.4` is the store-backed Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
3
+ `@cogcoin/client@0.5.6` is the store-backed Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
4
4
 
5
5
  Use Node 22 or newer.
6
6
 
@@ -17,6 +17,8 @@ export interface CogcoinResolvedPaths {
17
17
  walletStateDirectory: string;
18
18
  walletStatePath: string;
19
19
  walletStateBackupPath: string;
20
+ walletInitPendingPath: string;
21
+ walletInitPendingBackupPath: string;
20
22
  walletUnlockSessionPath: string;
21
23
  walletExplicitLockPath: string;
22
24
  walletControlLockPath: string;
package/dist/app-paths.js CHANGED
@@ -63,6 +63,8 @@ export function resolveCogcoinPathsForTesting(resolution = {}) {
63
63
  const walletStateDirectory = stateRoot;
64
64
  const walletStatePath = joinForPlatform(platform, walletStateDirectory, "wallet-state.enc");
65
65
  const walletStateBackupPath = joinForPlatform(platform, walletStateDirectory, "wallet-state.enc.bak");
66
+ const walletInitPendingPath = joinForPlatform(platform, walletStateDirectory, "wallet-init-pending.enc");
67
+ const walletInitPendingBackupPath = joinForPlatform(platform, walletStateDirectory, "wallet-init-pending.enc.bak");
66
68
  const walletUnlockSessionPath = joinForPlatform(platform, runtimeRoot, "wallet-unlock-session.enc");
67
69
  const walletExplicitLockPath = joinForPlatform(platform, runtimeRoot, "wallet-explicit-lock.json");
68
70
  const walletControlLockPath = joinForPlatform(platform, runtimeRoot, "wallet-control.lock");
@@ -91,6 +93,8 @@ export function resolveCogcoinPathsForTesting(resolution = {}) {
91
93
  walletStateDirectory,
92
94
  walletStatePath,
93
95
  walletStateBackupPath,
96
+ walletInitPendingPath,
97
+ walletInitPendingBackupPath,
94
98
  walletUnlockSessionPath,
95
99
  walletExplicitLockPath,
96
100
  walletControlLockPath,
@@ -1,10 +1,10 @@
1
1
  ⛭ Write this down. Keep it secret, keep it safe. ⛭
2
- ▐▀▀▀▀▀▀▚ ╔──────────────────────────────────────────────────────────────────╗
3
- ▛▞▀▀▀▀▀▀▚ │ │
4
- ▌▝▀▀▀▀▀▀▀▌ 1.achieved 6.achieved 11.achieved 16.achieved 21.achieved │
5
- ▗▙▙ 2.achieved 7.achieved 12.achieved 17.achieved 22.achieved │
6
- ▐ ▌ ▌ │ 3.achieved 8.achieved 13.achieved 18.achieved 23.achieved │
7
- ▐▀▀▚ ▌ │ 4.achieved 9.achieved 14.achieved 19.achieved 24.achieved │
8
- ▐▄▄▞ ▌ │ 5.achieved 10.achieved 15.achieved 20.achieved │
9
- ▘▘ ▌ │
10
- ▝▀▀▀▀▀▀▀▀▘ ╚──────────────────────────────────────────────────────────────────╝
2
+ ▐▀▀▀▀▀▀▀▀▚ ╔──────────────────────────────────────────────────────────────────╗
3
+ ▛▞▀▀▀▀▀▀▀▀▚ │ │
4
+ ▌▝▀▀▀▀▀▀▀▀▀▌│ 1.achieved 6.achieved 11.achieved 16.achieved 21.achieved │
5
+ ▗▙▙ ▌│ 2.achieved 7.achieved 12.achieved 17.achieved 22.achieved │
6
+ ▐ ▌ ▌│ 3.achieved 8.achieved 13.achieved 18.achieved 23.achieved │
7
+ ▐▀▀▚ ▌│ 4.achieved 9.achieved 14.achieved 19.achieved 24.achieved │
8
+ ▐▄▄▞ ▌│ 5.achieved 10.achieved 15.achieved 20.achieved │
9
+ ▘▘ ▌│
10
+ ▝▀▀▀▀▀▀▀▀▀▀▘╚──────────────────────────────────────────────────────────────────╝
@@ -1,4 +1,5 @@
1
1
  import type { BitcoinRpcClient } from "../rpc.js";
2
- import type { SnapshotMetadata } from "../types.js";
2
+ import type { RpcChainState, SnapshotMetadata } from "../types.js";
3
3
  import type { BootstrapPersistentState } from "./types.js";
4
4
  export declare function isSnapshotAlreadyLoaded(rpc: Pick<BitcoinRpcClient, "getChainStates">, snapshot: SnapshotMetadata, state: BootstrapPersistentState): Promise<boolean>;
5
+ export declare function findLoadedSnapshotChainState(rpc: Pick<BitcoinRpcClient, "getChainStates">, snapshot: SnapshotMetadata, state: BootstrapPersistentState): Promise<RpcChainState | null>;
@@ -8,6 +8,9 @@ function chainStateMatches(chainState, snapshot, baseHeight, tipHashHex) {
8
8
  return chainState.blocks === snapshot.height && chainState.validated === false;
9
9
  }
10
10
  export async function isSnapshotAlreadyLoaded(rpc, snapshot, state) {
11
+ return (await findLoadedSnapshotChainState(rpc, snapshot, state)) !== null;
12
+ }
13
+ export async function findLoadedSnapshotChainState(rpc, snapshot, state) {
11
14
  const chainStates = await rpc.getChainStates();
12
- return chainStates.chainstates.some((chainState) => chainStateMatches(chainState, snapshot, state.baseHeight, state.tipHashHex));
15
+ return chainStates.chainstates.find((chainState) => chainStateMatches(chainState, snapshot, state.baseHeight, state.tipHashHex)) ?? null;
13
16
  }
@@ -0,0 +1,14 @@
1
+ import type { BootstrapPersistentState } from "./types.js";
2
+ import type { SnapshotChunkManifest, SnapshotMetadata } from "../types.js";
3
+ export declare function resolveBundledSnapshotChunkManifest(snapshot?: SnapshotMetadata): SnapshotChunkManifest;
4
+ export declare function resolveSnapshotChunkCount(manifest: SnapshotChunkManifest): number;
5
+ export declare function resolveSnapshotChunkSize(manifest: SnapshotChunkManifest, chunkIndex: number): number;
6
+ export declare function resolveVerifiedChunkBytes(manifest: SnapshotChunkManifest, verifiedChunkCount: number): number;
7
+ export declare function resolveVerifiedChunkCountFromBytes(manifest: SnapshotChunkManifest, bytes: number): number;
8
+ export declare function clampVerifiedChunkCount(manifest: SnapshotChunkManifest, verifiedChunkCount: number): number;
9
+ export declare function stateHasTrustedIntegrityFrontier(state: BootstrapPersistentState, manifest: SnapshotChunkManifest): boolean;
10
+ export declare function createSnapshotChunkManifestRecord(options: {
11
+ snapshot: SnapshotMetadata;
12
+ chunkSha256s: string[];
13
+ chunkSizeBytes?: number;
14
+ }): SnapshotChunkManifest;
@@ -0,0 +1,85 @@
1
+ import { DEFAULT_SNAPSHOT_CHUNK_SIZE_BYTES, SNAPSHOT_CHUNK_MANIFEST_VERSION, } from "./constants.js";
2
+ import { DEFAULT_SNAPSHOT_CHUNK_MANIFEST } from "./default-snapshot-chunk-manifest.js";
3
+ import { DEFAULT_SNAPSHOT_METADATA } from "./constants.js";
4
+ function snapshotMatchesManifest(manifest, snapshot) {
5
+ return manifest.snapshotFilename === snapshot.filename
6
+ && manifest.snapshotHeight === snapshot.height
7
+ && manifest.snapshotSizeBytes === snapshot.sizeBytes
8
+ && manifest.snapshotSha256 === snapshot.sha256;
9
+ }
10
+ function snapshotMatchesDefault(snapshot) {
11
+ return snapshot.filename === DEFAULT_SNAPSHOT_METADATA.filename
12
+ && snapshot.height === DEFAULT_SNAPSHOT_METADATA.height
13
+ && snapshot.sizeBytes === DEFAULT_SNAPSHOT_METADATA.sizeBytes
14
+ && snapshot.sha256 === DEFAULT_SNAPSHOT_METADATA.sha256;
15
+ }
16
+ export function resolveBundledSnapshotChunkManifest(snapshot = DEFAULT_SNAPSHOT_METADATA) {
17
+ if (!snapshotMatchesDefault(snapshot)) {
18
+ throw new Error(`snapshot_chunk_manifest_unavailable_${snapshot.filename}`);
19
+ }
20
+ if (!snapshotMatchesManifest(DEFAULT_SNAPSHOT_CHUNK_MANIFEST, snapshot)) {
21
+ throw new Error(`snapshot_chunk_manifest_mismatch_${snapshot.filename}`);
22
+ }
23
+ return DEFAULT_SNAPSHOT_CHUNK_MANIFEST;
24
+ }
25
+ export function resolveSnapshotChunkCount(manifest) {
26
+ return manifest.chunkSha256s.length;
27
+ }
28
+ export function resolveSnapshotChunkSize(manifest, chunkIndex) {
29
+ if (chunkIndex < 0 || chunkIndex >= manifest.chunkSha256s.length) {
30
+ throw new Error(`snapshot_chunk_index_out_of_range_${chunkIndex}`);
31
+ }
32
+ const lastChunkIndex = manifest.chunkSha256s.length - 1;
33
+ if (chunkIndex < lastChunkIndex) {
34
+ return manifest.chunkSizeBytes;
35
+ }
36
+ const trailingBytes = manifest.snapshotSizeBytes % manifest.chunkSizeBytes;
37
+ return trailingBytes === 0 ? manifest.chunkSizeBytes : trailingBytes;
38
+ }
39
+ export function resolveVerifiedChunkBytes(manifest, verifiedChunkCount) {
40
+ const chunkCount = clampVerifiedChunkCount(manifest, verifiedChunkCount);
41
+ if (chunkCount <= 0) {
42
+ return 0;
43
+ }
44
+ if (chunkCount >= manifest.chunkSha256s.length) {
45
+ return manifest.snapshotSizeBytes;
46
+ }
47
+ return chunkCount * manifest.chunkSizeBytes;
48
+ }
49
+ export function resolveVerifiedChunkCountFromBytes(manifest, bytes) {
50
+ if (bytes >= manifest.snapshotSizeBytes) {
51
+ return manifest.chunkSha256s.length;
52
+ }
53
+ if (bytes <= 0) {
54
+ return 0;
55
+ }
56
+ return Math.floor(bytes / manifest.chunkSizeBytes);
57
+ }
58
+ export function clampVerifiedChunkCount(manifest, verifiedChunkCount) {
59
+ if (!Number.isFinite(verifiedChunkCount) || verifiedChunkCount <= 0) {
60
+ return 0;
61
+ }
62
+ return Math.min(Math.trunc(verifiedChunkCount), manifest.chunkSha256s.length);
63
+ }
64
+ export function stateHasTrustedIntegrityFrontier(state, manifest) {
65
+ if (state.integrityVersion !== SNAPSHOT_CHUNK_MANIFEST_VERSION) {
66
+ return false;
67
+ }
68
+ if (state.chunkSizeBytes !== manifest.chunkSizeBytes) {
69
+ return false;
70
+ }
71
+ const verifiedChunkCount = clampVerifiedChunkCount(manifest, state.verifiedChunkCount);
72
+ const verifiedBytes = resolveVerifiedChunkBytes(manifest, verifiedChunkCount);
73
+ return state.downloadedBytes === verifiedBytes;
74
+ }
75
+ export function createSnapshotChunkManifestRecord(options) {
76
+ return {
77
+ formatVersion: SNAPSHOT_CHUNK_MANIFEST_VERSION,
78
+ chunkSizeBytes: options.chunkSizeBytes ?? DEFAULT_SNAPSHOT_CHUNK_SIZE_BYTES,
79
+ snapshotFilename: options.snapshot.filename,
80
+ snapshotHeight: options.snapshot.height,
81
+ snapshotSizeBytes: options.snapshot.sizeBytes,
82
+ snapshotSha256: options.snapshot.sha256,
83
+ chunkSha256s: options.chunkSha256s,
84
+ };
85
+ }
@@ -0,0 +1,4 @@
1
+ import type { BootstrapPaths, BootstrapPersistentState, BootstrapStateSnapshotIdentity } from "./types.js";
2
+ import type { SnapshotChunkManifest } from "../types.js";
3
+ export declare function applyVerifiedFrontierState(state: BootstrapPersistentState, manifest: SnapshotChunkManifest, verifiedChunkCount: number): void;
4
+ export declare function reconcileSnapshotDownloadArtifacts(paths: BootstrapPaths, state: BootstrapPersistentState, manifest: SnapshotChunkManifest, snapshotIdentity: BootstrapStateSnapshotIdentity): Promise<void>;
@@ -0,0 +1,122 @@
1
+ import { createHash } from "node:crypto";
2
+ import { open, rename, rm } from "node:fs/promises";
3
+ import { clampVerifiedChunkCount, resolveSnapshotChunkCount, resolveSnapshotChunkSize, resolveVerifiedChunkBytes, resolveVerifiedChunkCountFromBytes, stateHasTrustedIntegrityFrontier, } from "./chunk-manifest.js";
4
+ import { resetSnapshotFiles, statOrNull } from "./snapshot-file.js";
5
+ const TRUSTED_FRONTIER_REVERIFY_CHUNKS = 2;
6
+ const HASH_READ_BUFFER_BYTES = 1024 * 1024;
7
+ async function moveSnapshotPathToPartial(paths) {
8
+ const partInfo = await statOrNull(paths.partialSnapshotPath);
9
+ const fullInfo = await statOrNull(paths.snapshotPath);
10
+ if (fullInfo === null) {
11
+ return;
12
+ }
13
+ if (partInfo !== null && partInfo.size > fullInfo.size) {
14
+ await rm(paths.snapshotPath, { force: true });
15
+ return;
16
+ }
17
+ if (partInfo !== null) {
18
+ await rm(paths.partialSnapshotPath, { force: true });
19
+ }
20
+ await rename(paths.snapshotPath, paths.partialSnapshotPath);
21
+ }
22
+ async function hashChunkRange(path, manifest, chunkIndex) {
23
+ const file = await open(path, "r");
24
+ const chunkSizeBytes = resolveSnapshotChunkSize(manifest, chunkIndex);
25
+ const buffer = Buffer.allocUnsafe(Math.min(HASH_READ_BUFFER_BYTES, chunkSizeBytes));
26
+ const hash = createHash("sha256");
27
+ let remainingBytes = chunkSizeBytes;
28
+ let position = resolveVerifiedChunkBytes(manifest, chunkIndex);
29
+ try {
30
+ while (remainingBytes > 0) {
31
+ const readLength = Math.min(buffer.length, remainingBytes);
32
+ const { bytesRead } = await file.read(buffer, 0, readLength, position);
33
+ if (bytesRead === 0) {
34
+ return null;
35
+ }
36
+ hash.update(buffer.subarray(0, bytesRead));
37
+ remainingBytes -= bytesRead;
38
+ position += bytesRead;
39
+ if (bytesRead < readLength) {
40
+ return remainingBytes === 0 ? hash.digest("hex") : null;
41
+ }
42
+ }
43
+ }
44
+ finally {
45
+ await file.close();
46
+ }
47
+ return hash.digest("hex");
48
+ }
49
+ async function scanVerifiedPrefix(path, manifest, fileSize) {
50
+ const chunkCount = resolveSnapshotChunkCount(manifest);
51
+ for (let chunkIndex = 0; chunkIndex < chunkCount; chunkIndex += 1) {
52
+ const expectedChunkEnd = resolveVerifiedChunkBytes(manifest, chunkIndex + 1);
53
+ if (fileSize < expectedChunkEnd) {
54
+ return chunkIndex;
55
+ }
56
+ const actualSha256 = await hashChunkRange(path, manifest, chunkIndex);
57
+ if (actualSha256 !== manifest.chunkSha256s[chunkIndex]) {
58
+ return chunkIndex;
59
+ }
60
+ }
61
+ return chunkCount;
62
+ }
63
+ async function reverifyTrustedFrontier(path, manifest, verifiedChunkCount, fileSize) {
64
+ const maxCompleteChunkCount = resolveVerifiedChunkCountFromBytes(manifest, Math.min(fileSize, manifest.snapshotSizeBytes));
65
+ const tentativeVerifiedChunkCount = Math.min(clampVerifiedChunkCount(manifest, verifiedChunkCount), maxCompleteChunkCount);
66
+ const startChunk = Math.max(0, tentativeVerifiedChunkCount - TRUSTED_FRONTIER_REVERIFY_CHUNKS);
67
+ for (let chunkIndex = startChunk; chunkIndex < tentativeVerifiedChunkCount; chunkIndex += 1) {
68
+ const actualSha256 = await hashChunkRange(path, manifest, chunkIndex);
69
+ if (actualSha256 !== manifest.chunkSha256s[chunkIndex]) {
70
+ return chunkIndex;
71
+ }
72
+ }
73
+ return tentativeVerifiedChunkCount;
74
+ }
75
+ async function truncatePartialSnapshot(paths, verifiedBytes) {
76
+ const file = await open(paths.partialSnapshotPath, "a+");
77
+ try {
78
+ await file.truncate(verifiedBytes);
79
+ await file.sync();
80
+ }
81
+ finally {
82
+ await file.close();
83
+ }
84
+ }
85
+ export function applyVerifiedFrontierState(state, manifest, verifiedChunkCount) {
86
+ const clampedVerifiedChunkCount = clampVerifiedChunkCount(manifest, verifiedChunkCount);
87
+ state.integrityVersion = manifest.formatVersion;
88
+ state.chunkSizeBytes = manifest.chunkSizeBytes;
89
+ state.verifiedChunkCount = clampedVerifiedChunkCount;
90
+ state.downloadedBytes = resolveVerifiedChunkBytes(manifest, clampedVerifiedChunkCount);
91
+ }
92
+ export async function reconcileSnapshotDownloadArtifacts(paths, state, manifest, snapshotIdentity) {
93
+ if (snapshotIdentity === "different") {
94
+ await resetSnapshotFiles(paths);
95
+ state.phase = "snapshot_download";
96
+ state.loadTxOutSetComplete = false;
97
+ state.baseHeight = null;
98
+ state.tipHashHex = null;
99
+ state.lastError = null;
100
+ applyVerifiedFrontierState(state, manifest, 0);
101
+ state.validated = false;
102
+ return;
103
+ }
104
+ if (!state.validated) {
105
+ await moveSnapshotPathToPartial(paths);
106
+ }
107
+ const partialInfo = await statOrNull(paths.partialSnapshotPath);
108
+ if (partialInfo === null) {
109
+ applyVerifiedFrontierState(state, manifest, 0);
110
+ state.validated = false;
111
+ return;
112
+ }
113
+ const verifiedChunkCount = stateHasTrustedIntegrityFrontier(state, manifest)
114
+ ? await reverifyTrustedFrontier(paths.partialSnapshotPath, manifest, state.verifiedChunkCount, partialInfo.size)
115
+ : await scanVerifiedPrefix(paths.partialSnapshotPath, manifest, partialInfo.size);
116
+ const verifiedBytes = resolveVerifiedChunkBytes(manifest, verifiedChunkCount);
117
+ if (partialInfo.size !== verifiedBytes) {
118
+ await truncatePartialSnapshot(paths, verifiedBytes);
119
+ }
120
+ applyVerifiedFrontierState(state, manifest, verifiedChunkCount);
121
+ state.validated = false;
122
+ }
@@ -1,5 +1,7 @@
1
1
  import type { SnapshotMetadata } from "../types.js";
2
- export declare const SNAPSHOT_METADATA_VERSION = 1;
2
+ export declare const BOOTSTRAP_STATE_VERSION = 2;
3
+ export declare const SNAPSHOT_CHUNK_MANIFEST_VERSION = 1;
4
+ export declare const DEFAULT_SNAPSHOT_CHUNK_SIZE_BYTES: number;
3
5
  export declare const DOWNLOAD_RETRY_BASE_MS = 1000;
4
6
  export declare const DOWNLOAD_RETRY_MAX_MS = 30000;
5
7
  export declare const HEADER_POLL_MS = 2000;
@@ -1,4 +1,6 @@
1
- export const SNAPSHOT_METADATA_VERSION = 1;
1
+ export const BOOTSTRAP_STATE_VERSION = 2;
2
+ export const SNAPSHOT_CHUNK_MANIFEST_VERSION = 1;
3
+ export const DEFAULT_SNAPSHOT_CHUNK_SIZE_BYTES = 4 * 1024 * 1024;
2
4
  export const DOWNLOAD_RETRY_BASE_MS = 1_000;
3
5
  export const DOWNLOAD_RETRY_MAX_MS = 30_000;
4
6
  export const HEADER_POLL_MS = 2_000;
@@ -1,7 +1,8 @@
1
1
  import type { ClientTip } from "../../types.js";
2
2
  import { ManagedProgressController } from "../progress.js";
3
3
  import { BitcoinRpcClient } from "../rpc.js";
4
- import type { BootstrapPhase, SnapshotMetadata } from "../types.js";
4
+ import { type ManagedRpcRetryState } from "../retryable-rpc.js";
5
+ import type { BootstrapPhase, SnapshotMetadata, SnapshotChunkManifest } from "../types.js";
5
6
  export declare class AssumeUtxoBootstrapController {
6
7
  #private;
7
8
  constructor(options: {
@@ -9,15 +10,22 @@ export declare class AssumeUtxoBootstrapController {
9
10
  dataDir: string;
10
11
  progress: ManagedProgressController;
11
12
  snapshot?: SnapshotMetadata;
13
+ manifest?: SnapshotChunkManifest;
12
14
  fetchImpl?: typeof fetch;
13
15
  });
14
16
  get quoteStatePath(): string;
15
17
  get snapshot(): SnapshotMetadata;
16
- ensureReady(indexedTip: ClientTip | null, expectedChain: "main" | "regtest"): Promise<void>;
18
+ ensureReady(indexedTip: ClientTip | null, expectedChain: "main" | "regtest", options?: {
19
+ signal?: AbortSignal;
20
+ retryState?: ManagedRpcRetryState;
21
+ }): Promise<void>;
17
22
  getStateForTesting(): Promise<{
18
23
  metadataVersion: number;
19
24
  snapshot: SnapshotMetadata;
20
25
  phase: BootstrapPhase;
26
+ integrityVersion: number;
27
+ chunkSizeBytes: number;
28
+ verifiedChunkCount: number;
21
29
  downloadedBytes: number;
22
30
  validated: boolean;
23
31
  loadTxOutSetComplete: boolean;
@@ -1,11 +1,13 @@
1
+ import { join } from "node:path";
1
2
  import { createBootstrapProgressForTesting, ManagedProgressController } from "../progress.js";
2
3
  import { BitcoinRpcClient } from "../rpc.js";
3
4
  import { DEFAULT_SNAPSHOT_METADATA } from "./constants.js";
4
5
  import { downloadSnapshotFileForTesting } from "./download.js";
5
6
  import { waitForHeaders } from "./headers.js";
6
7
  import { resolveBootstrapPaths } from "./paths.js";
7
- import { loadBootstrapState, saveBootstrapState } from "./state.js";
8
- import { isSnapshotAlreadyLoaded } from "./chainstate.js";
8
+ import { loadBootstrapStateRecord, saveBootstrapState } from "./state.js";
9
+ import { findLoadedSnapshotChainState, isSnapshotAlreadyLoaded } from "./chainstate.js";
10
+ import { describeManagedRpcRetryError, isRetryableManagedRpcError, } from "../retryable-rpc.js";
9
11
  async function loadSnapshotIntoNode(rpc, snapshotPath) {
10
12
  return rpc.loadTxOutSet(snapshotPath);
11
13
  }
@@ -14,13 +16,17 @@ export class AssumeUtxoBootstrapController {
14
16
  #paths;
15
17
  #progress;
16
18
  #snapshot;
19
+ #debugLogPath;
20
+ #manifest;
17
21
  #fetchImpl;
18
- #statePromise = null;
22
+ #stateRecordPromise = null;
19
23
  constructor(options) {
20
24
  this.#rpc = options.rpc;
21
25
  this.#progress = options.progress;
22
26
  this.#snapshot = options.snapshot ?? DEFAULT_SNAPSHOT_METADATA;
27
+ this.#manifest = options.manifest;
23
28
  this.#paths = resolveBootstrapPaths(options.dataDir, this.#snapshot);
29
+ this.#debugLogPath = join(options.dataDir, "debug.log");
24
30
  this.#fetchImpl = options.fetchImpl;
25
31
  }
26
32
  get quoteStatePath() {
@@ -29,7 +35,7 @@ export class AssumeUtxoBootstrapController {
29
35
  get snapshot() {
30
36
  return this.#snapshot;
31
37
  }
32
- async ensureReady(indexedTip, expectedChain) {
38
+ async ensureReady(indexedTip, expectedChain, options = {}) {
33
39
  if (expectedChain !== "main") {
34
40
  await this.#progress.setPhase("paused", {
35
41
  ...createBootstrapProgressForTesting("paused", this.#snapshot),
@@ -45,42 +51,77 @@ export class AssumeUtxoBootstrapController {
45
51
  });
46
52
  return;
47
53
  }
48
- const state = await this.#loadState();
54
+ const { state, snapshotIdentity } = await this.#loadStateRecord();
49
55
  if (state.loadTxOutSetComplete && await isSnapshotAlreadyLoaded(this.#rpc, this.#snapshot, state)) {
56
+ if (state.lastError !== null) {
57
+ state.lastError = null;
58
+ await saveBootstrapState(this.#paths, state);
59
+ }
50
60
  await this.#progress.setPhase("bitcoin_sync", {
51
61
  blocks: state.baseHeight,
52
62
  targetHeight: state.baseHeight ?? this.#snapshot.height,
53
63
  baseHeight: state.baseHeight,
54
64
  tipHashHex: state.tipHashHex,
55
65
  message: "Using the previously loaded assumeutxo chainstate.",
56
- lastError: state.lastError,
66
+ lastError: null,
57
67
  });
58
68
  return;
59
69
  }
60
70
  await downloadSnapshotFileForTesting({
61
71
  fetchImpl: this.#fetchImpl,
72
+ manifest: this.#manifest,
62
73
  metadata: this.#snapshot,
63
74
  paths: this.#paths,
64
75
  progress: this.#progress,
65
76
  state,
77
+ signal: options.signal,
78
+ snapshotIdentity,
66
79
  });
67
80
  if (!await isSnapshotAlreadyLoaded(this.#rpc, this.#snapshot, state)) {
68
- await waitForHeaders(this.#rpc, this.#snapshot, this.#progress);
81
+ await waitForHeaders(this.#rpc, this.#snapshot, this.#progress, {
82
+ signal: options.signal,
83
+ retryState: options.retryState,
84
+ debugLogPath: this.#debugLogPath,
85
+ });
69
86
  await this.#progress.setPhase("load_snapshot", {
70
87
  downloadedBytes: this.#snapshot.sizeBytes,
71
88
  totalBytes: this.#snapshot.sizeBytes,
72
89
  percent: 100,
73
90
  message: "Loading the UTXO snapshot into bitcoind.",
91
+ lastError: null,
74
92
  });
75
- const loadResult = await loadSnapshotIntoNode(this.#rpc, this.#paths.snapshotPath);
93
+ let loadResult;
94
+ try {
95
+ loadResult = await loadSnapshotIntoNode(this.#rpc, this.#paths.snapshotPath);
96
+ }
97
+ catch (error) {
98
+ if (!isRetryableManagedRpcError(error)) {
99
+ throw error;
100
+ }
101
+ state.lastError = describeManagedRpcRetryError(error);
102
+ await saveBootstrapState(this.#paths, state);
103
+ const loadedChainState = await findLoadedSnapshotChainState(this.#rpc, this.#snapshot, state);
104
+ if (loadedChainState === null) {
105
+ throw error;
106
+ }
107
+ loadResult = {
108
+ base_height: loadedChainState.blocks ?? this.#snapshot.height,
109
+ coins_loaded: 0,
110
+ tip_hash: loadedChainState.snapshot_blockhash ?? state.tipHashHex ?? "",
111
+ };
112
+ }
76
113
  state.loadTxOutSetComplete = true;
77
114
  state.baseHeight = loadResult.base_height;
78
- state.tipHashHex = loadResult.tip_hash;
115
+ state.tipHashHex = loadResult.tip_hash === "" ? state.tipHashHex : loadResult.tip_hash;
79
116
  state.phase = "bitcoin_sync";
80
117
  state.lastError = null;
81
118
  await saveBootstrapState(this.#paths, state);
82
119
  }
83
120
  const info = await this.#rpc.getBlockchainInfo();
121
+ if (state.lastError !== null) {
122
+ state.lastError = null;
123
+ await saveBootstrapState(this.#paths, state);
124
+ }
84
125
  await this.#progress.setPhase("bitcoin_sync", {
85
126
  blocks: info.blocks,
86
127
  headers: info.headers,
@@ -88,14 +129,17 @@ export class AssumeUtxoBootstrapController {
88
129
  baseHeight: state.baseHeight,
89
130
  tipHashHex: state.tipHashHex,
90
131
  message: "Bitcoin Core is syncing blocks after assumeutxo bootstrap.",
91
- lastError: state.lastError,
132
+ lastError: null,
92
133
  });
93
134
  }
94
135
  async getStateForTesting() {
95
136
  return { ...(await this.#loadState()) };
96
137
  }
97
138
  async #loadState() {
98
- this.#statePromise ??= loadBootstrapState(this.#paths, this.#snapshot);
99
- return this.#statePromise;
139
+ return (await this.#loadStateRecord()).state;
140
+ }
141
+ async #loadStateRecord() {
142
+ this.#stateRecordPromise ??= loadBootstrapStateRecord(this.#paths, this.#snapshot);
143
+ return this.#stateRecordPromise;
100
144
  }
101
145
  }
@@ -0,0 +1,2 @@
1
+ import type { SnapshotChunkManifest } from "../types.js";
2
+ export declare const DEFAULT_SNAPSHOT_CHUNK_MANIFEST: SnapshotChunkManifest;