@cogcoin/client 0.5.9 → 0.5.11

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@0.5.9` 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.11` 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
 
@@ -107,7 +107,7 @@ The built-in managed-node integration:
107
107
  - uses RPC for durable reads and ZMQ `hashblock` notifications for tip following
108
108
  - launches a local full node with cookie auth
109
109
  - defaults to an assumeutxo-first mainnet bootstrap using `https://snapshots.cogcoin.org/utxo-910000.dat`
110
- - opportunistically loads the public getblock archive family from `https://snapshots.cogcoin.org/getblock-910000-latest.{json,dat}` to accelerate post-`910000` Bitcoin Core catch-up
110
+ - opportunistically loads the public getblock range family from `https://snapshots.cogcoin.org/getblock-manifest.json` plus immutable `getblock-<first>-<last>.dat` bands to accelerate post-`910000` Bitcoin Core catch-up
111
111
  - composes the existing SQLite-backed client rather than replacing it
112
112
 
113
113
  If `dataDir` is omitted, the managed node defaults to:
@@ -121,15 +121,16 @@ On a fresh mainnet managed sync, `syncToTip()` or `startFollowingTip()`:
121
121
  1. downloads the pinned Cogcoin UTXO snapshot with resume support
122
122
  2. validates its known size and SHA-256
123
123
  3. loads it with Bitcoin Core assumeutxo
124
- 4. opportunistically downloads and validates the public getblock archive for raw post-snapshot Bitcoin blocks
125
- 5. loads that archive into managed Bitcoin Core when available
126
- 6. continues Bitcoin sync and Cogcoin replay from the managed node until the live tip is caught up
124
+ 4. opportunistically checks for the next published 500-block getblock band at each post-snapshot boundary
125
+ 5. downloads, validates, and loads that range into managed Bitcoin Core when available
126
+ 6. syncs Cogcoin through that range, deletes the consumed local band cache, and repeats for the next boundary
127
+ 7. falls back to ordinary Bitcoin sync and Cogcoin replay once no further published range exists
127
128
 
128
- The public getblock archive provenance is tracked in the companion scraper repository:
129
+ The public getblock range provenance is tracked in the companion scraper repository:
129
130
 
130
131
  - [`github.com/cogcoin/bitcoin-scrape`](https://github.com/cogcoin/bitcoin-scrape)
131
132
 
132
- That repo documents how the `getblock-910000-latest.dat` and `getblock-910000-latest.json` artifacts are assembled from `bitcoin-cli getblockhash` plus `bitcoin-cli getblock <hash> 0`, including the blk-style file layout, manifest format, durability guarantees, and height-based cache-busting rules.
133
+ That repo documents how `getblock-manifest.json` and immutable files such as `getblock-910001-910500.dat` and `getblock-910501-911000.dat` are assembled from `bitcoin-cli getblockhash` plus `bitcoin-cli getblock <hash> 0`, including the blk-style file layout, range manifest format, durability guarantees, and publish order.
133
134
 
134
135
  The managed `bitcoind` client also exposes:
135
136
 
@@ -4,17 +4,27 @@ interface GetblockArchivePaths {
4
4
  directory: string;
5
5
  artifactPath: string;
6
6
  partialArtifactPath: string;
7
- manifestPath: string;
8
- partialManifestPath: string;
9
7
  statePath: string;
10
8
  }
11
9
  export interface ReadyGetblockArchive {
12
10
  manifest: GetblockArchiveManifest;
13
11
  artifactPath: string;
14
- manifestPath: string;
15
12
  }
16
- export declare function resolveGetblockArchivePathsForTesting(dataDir: string): GetblockArchivePaths;
17
- export declare function resolveReadyGetblockArchiveForTesting(dataDir: string): Promise<ReadyGetblockArchive | null>;
13
+ export declare function resolveGetblockArchivePathsForTesting(dataDir: string, firstBlockHeight?: number, lastBlockHeight?: number): GetblockArchivePaths;
14
+ export declare function resolveReadyGetblockArchiveForTesting(dataDir: string, manifest: GetblockArchiveManifest): Promise<ReadyGetblockArchive | null>;
15
+ export declare function prepareGetblockArchiveRange(options: {
16
+ dataDir: string;
17
+ progress: Pick<ManagedProgressController, "setPhase">;
18
+ firstBlockHeight: number;
19
+ lastBlockHeight: number;
20
+ fetchImpl?: typeof fetch;
21
+ signal?: AbortSignal;
22
+ }): Promise<ReadyGetblockArchive | null>;
23
+ export declare function deleteGetblockArchiveRange(options: {
24
+ dataDir: string;
25
+ firstBlockHeight: number;
26
+ lastBlockHeight: number;
27
+ }): Promise<void>;
18
28
  export declare function prepareLatestGetblockArchive(options: {
19
29
  dataDir: string;
20
30
  progress: Pick<ManagedProgressController, "setPhase">;
@@ -22,6 +32,8 @@ export declare function prepareLatestGetblockArchive(options: {
22
32
  signal?: AbortSignal;
23
33
  }): Promise<ReadyGetblockArchive | null>;
24
34
  export declare const prepareLatestGetblockArchiveForTesting: typeof prepareLatestGetblockArchive;
35
+ export declare const prepareGetblockArchiveRangeForTesting: typeof prepareGetblockArchiveRange;
36
+ export declare const deleteGetblockArchiveRangeForTesting: typeof deleteGetblockArchiveRange;
25
37
  export declare function waitForGetblockArchiveImportForTesting(rpc: Pick<{
26
38
  getBlockchainInfo(): Promise<{
27
39
  blocks: number;
@@ -4,18 +4,22 @@ import { dirname, join } from "node:path";
4
4
  import { DOWNLOAD_RETRY_BASE_MS, DOWNLOAD_RETRY_MAX_MS } from "./constants.js";
5
5
  const GETBLOCK_ARCHIVE_STATE_VERSION = 1;
6
6
  const GETBLOCK_ARCHIVE_BASE_HEIGHT = 910_000;
7
- const GETBLOCK_ARCHIVE_FILENAME = "getblock-910000-latest.dat";
8
- const GETBLOCK_ARCHIVE_MANIFEST_FILENAME = "getblock-910000-latest.json";
9
- const GETBLOCK_ARCHIVE_REMOTE_DATA_URL = "https://snapshots.cogcoin.org/getblock-910000-latest.dat";
10
- const GETBLOCK_ARCHIVE_REMOTE_MANIFEST_URL = "https://snapshots.cogcoin.org/getblock-910000-latest.json";
7
+ const GETBLOCK_ARCHIVE_RANGE_SIZE = 500;
8
+ const GETBLOCK_ARCHIVE_FIRST_HEIGHT = GETBLOCK_ARCHIVE_BASE_HEIGHT + 1;
9
+ const GETBLOCK_ARCHIVE_REMOTE_BASE_URL = "https://snapshots.cogcoin.org/";
10
+ const GETBLOCK_ARCHIVE_REMOTE_MANIFEST_URL = `${GETBLOCK_ARCHIVE_REMOTE_BASE_URL}getblock-manifest.json`;
11
11
  const TRUSTED_FRONTIER_REVERIFY_CHUNKS = 2;
12
12
  const HASH_READ_BUFFER_BYTES = 1024 * 1024;
13
13
  const IMPORT_POLL_MS = 2_000;
14
+ function buildRangeFilename(firstBlockHeight, lastBlockHeight) {
15
+ return `getblock-${firstBlockHeight}-${lastBlockHeight}.dat`;
16
+ }
14
17
  function createInitialState() {
15
18
  return {
16
19
  metadataVersion: GETBLOCK_ARCHIVE_STATE_VERSION,
17
20
  formatVersion: 0,
18
- endHeight: null,
21
+ firstBlockHeight: null,
22
+ lastBlockHeight: null,
19
23
  artifactSizeBytes: 0,
20
24
  artifactSha256: null,
21
25
  chunkSizeBytes: 0,
@@ -26,35 +30,74 @@ function createInitialState() {
26
30
  updatedAt: Date.now(),
27
31
  };
28
32
  }
29
- function assertManifestShape(parsed) {
33
+ function assertRangeManifestShape(parsed) {
30
34
  if (typeof parsed !== "object" || parsed === null) {
31
35
  throw new Error("managed_getblock_archive_manifest_invalid");
32
36
  }
33
37
  const manifest = parsed;
34
- if (manifest.chain !== "main"
38
+ if (manifest.formatVersion !== 1
39
+ || manifest.chain !== "main"
35
40
  || manifest.baseSnapshotHeight !== GETBLOCK_ARCHIVE_BASE_HEIGHT
36
- || manifest.artifactFilename !== GETBLOCK_ARCHIVE_FILENAME
37
41
  || typeof manifest.firstBlockHeight !== "number"
38
- || typeof manifest.endHeight !== "number"
39
- || typeof manifest.blockCount !== "number"
42
+ || typeof manifest.lastBlockHeight !== "number"
43
+ || typeof manifest.artifactFilename !== "string"
40
44
  || typeof manifest.artifactSizeBytes !== "number"
41
45
  || typeof manifest.artifactSha256 !== "string"
42
46
  || typeof manifest.chunkSizeBytes !== "number"
43
47
  || !Array.isArray(manifest.chunkSha256s)
44
- || !Array.isArray(manifest.blocks)) {
48
+ || manifest.chunkSha256s.some((hash) => typeof hash !== "string")) {
49
+ throw new Error("managed_getblock_archive_manifest_invalid");
50
+ }
51
+ if (manifest.lastBlockHeight - manifest.firstBlockHeight + 1 !== GETBLOCK_ARCHIVE_RANGE_SIZE
52
+ || manifest.artifactFilename !== buildRangeFilename(manifest.firstBlockHeight, manifest.lastBlockHeight)) {
45
53
  throw new Error("managed_getblock_archive_manifest_invalid");
46
54
  }
47
55
  return manifest;
48
56
  }
49
- function resolvePaths(dataDir) {
57
+ function assertAggregateManifestShape(parsed) {
58
+ if (typeof parsed !== "object" || parsed === null) {
59
+ throw new Error("managed_getblock_archive_manifest_invalid");
60
+ }
61
+ const manifest = parsed;
62
+ if (manifest.formatVersion !== 1
63
+ || manifest.chain !== "main"
64
+ || manifest.baseSnapshotHeight !== GETBLOCK_ARCHIVE_BASE_HEIGHT
65
+ || manifest.rangeSizeBlocks !== GETBLOCK_ARCHIVE_RANGE_SIZE
66
+ || typeof manifest.publishedThroughHeight !== "number"
67
+ || !Array.isArray(manifest.ranges)) {
68
+ throw new Error("managed_getblock_archive_manifest_invalid");
69
+ }
70
+ const ranges = manifest.ranges.map((entry) => assertRangeManifestShape(entry));
71
+ let expectedFirstBlockHeight = GETBLOCK_ARCHIVE_FIRST_HEIGHT;
72
+ for (const range of ranges) {
73
+ if (range.firstBlockHeight !== expectedFirstBlockHeight) {
74
+ throw new Error("managed_getblock_archive_manifest_invalid");
75
+ }
76
+ expectedFirstBlockHeight = range.lastBlockHeight + 1;
77
+ }
78
+ const expectedPublishedThrough = ranges.length === 0
79
+ ? GETBLOCK_ARCHIVE_BASE_HEIGHT
80
+ : ranges[ranges.length - 1].lastBlockHeight;
81
+ if (manifest.publishedThroughHeight !== expectedPublishedThrough) {
82
+ throw new Error("managed_getblock_archive_manifest_invalid");
83
+ }
84
+ return {
85
+ formatVersion: manifest.formatVersion,
86
+ chain: manifest.chain,
87
+ baseSnapshotHeight: manifest.baseSnapshotHeight,
88
+ rangeSizeBlocks: manifest.rangeSizeBlocks,
89
+ publishedThroughHeight: manifest.publishedThroughHeight,
90
+ ranges,
91
+ };
92
+ }
93
+ function resolvePaths(dataDir, firstBlockHeight, lastBlockHeight) {
50
94
  const directory = join(dataDir, "bootstrap", "getblock");
95
+ const artifactFilename = buildRangeFilename(firstBlockHeight, lastBlockHeight);
51
96
  return {
52
97
  directory,
53
- artifactPath: join(directory, GETBLOCK_ARCHIVE_FILENAME),
54
- partialArtifactPath: join(directory, `${GETBLOCK_ARCHIVE_FILENAME}.part`),
55
- manifestPath: join(directory, GETBLOCK_ARCHIVE_MANIFEST_FILENAME),
56
- partialManifestPath: join(directory, `${GETBLOCK_ARCHIVE_MANIFEST_FILENAME}.part`),
57
- statePath: join(directory, "state.json"),
98
+ artifactPath: join(directory, artifactFilename),
99
+ partialArtifactPath: join(directory, `${artifactFilename}.part`),
100
+ statePath: join(directory, `getblock-${firstBlockHeight}-${lastBlockHeight}.state.json`),
58
101
  };
59
102
  }
60
103
  async function statOrNull(path) {
@@ -74,17 +117,6 @@ async function writeJsonAtomic(path, payload) {
74
117
  await writeFile(tempPath, JSON.stringify(payload, null, 2));
75
118
  await rename(tempPath, path);
76
119
  }
77
- async function readManifest(path) {
78
- try {
79
- return assertManifestShape(JSON.parse(await readFile(path, "utf8")));
80
- }
81
- catch (error) {
82
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
83
- return null;
84
- }
85
- throw error;
86
- }
87
- }
88
120
  async function loadState(paths) {
89
121
  try {
90
122
  const parsed = JSON.parse(await readFile(paths.statePath, "utf8"));
@@ -94,7 +126,8 @@ async function loadState(paths) {
94
126
  return {
95
127
  metadataVersion: GETBLOCK_ARCHIVE_STATE_VERSION,
96
128
  formatVersion: typeof parsed.formatVersion === "number" ? parsed.formatVersion : 0,
97
- endHeight: typeof parsed.endHeight === "number" ? parsed.endHeight : null,
129
+ firstBlockHeight: typeof parsed.firstBlockHeight === "number" ? parsed.firstBlockHeight : null,
130
+ lastBlockHeight: typeof parsed.lastBlockHeight === "number" ? parsed.lastBlockHeight : null,
98
131
  artifactSizeBytes: typeof parsed.artifactSizeBytes === "number" ? parsed.artifactSizeBytes : 0,
99
132
  artifactSha256: typeof parsed.artifactSha256 === "string" ? parsed.artifactSha256 : null,
100
133
  chunkSizeBytes: typeof parsed.chunkSizeBytes === "number" ? parsed.chunkSizeBytes : 0,
@@ -145,7 +178,8 @@ function resolveVerifiedChunkCountFromBytes(manifest, bytes) {
145
178
  return verifiedChunkCount;
146
179
  }
147
180
  function stateMatchesManifest(state, manifest) {
148
- return state.endHeight === manifest.endHeight
181
+ return state.firstBlockHeight === manifest.firstBlockHeight
182
+ && state.lastBlockHeight === manifest.lastBlockHeight
149
183
  && state.artifactSha256 === manifest.artifactSha256
150
184
  && state.artifactSizeBytes === manifest.artifactSizeBytes
151
185
  && state.chunkSizeBytes === manifest.chunkSizeBytes
@@ -211,25 +245,15 @@ async function truncateFile(path, size) {
211
245
  }
212
246
  }
213
247
  async function reconcilePartialDownloadArtifacts(paths, manifest, state) {
214
- const partialManifest = await readManifest(paths.partialManifestPath).catch(() => null);
215
- if (partialManifest === null || partialManifest.endHeight !== manifest.endHeight || partialManifest.artifactSha256 !== manifest.artifactSha256) {
248
+ const partialInfo = await statOrNull(paths.partialArtifactPath);
249
+ if (partialInfo === null || !stateMatchesManifest(state, manifest)) {
216
250
  await rm(paths.partialArtifactPath, { force: true }).catch(() => undefined);
217
- await rm(paths.partialManifestPath, { force: true }).catch(() => undefined);
218
251
  state.formatVersion = manifest.formatVersion;
219
- state.endHeight = manifest.endHeight;
252
+ state.firstBlockHeight = manifest.firstBlockHeight;
253
+ state.lastBlockHeight = manifest.lastBlockHeight;
220
254
  state.artifactSizeBytes = manifest.artifactSizeBytes;
221
255
  state.artifactSha256 = manifest.artifactSha256;
222
256
  state.chunkSizeBytes = manifest.chunkSizeBytes;
223
- state.verifiedChunkCount = 0;
224
- state.downloadedBytes = 0;
225
- state.validated = false;
226
- state.lastError = null;
227
- await writeJsonAtomic(paths.partialManifestPath, manifest);
228
- await saveState(paths, state);
229
- return;
230
- }
231
- const partialInfo = await statOrNull(paths.partialArtifactPath);
232
- if (partialInfo === null) {
233
257
  state.verifiedChunkCount = 0;
234
258
  state.downloadedBytes = 0;
235
259
  state.validated = false;
@@ -246,7 +270,8 @@ async function reconcilePartialDownloadArtifacts(paths, manifest, state) {
246
270
  await truncateFile(paths.partialArtifactPath, verifiedBytes);
247
271
  }
248
272
  state.formatVersion = manifest.formatVersion;
249
- state.endHeight = manifest.endHeight;
273
+ state.firstBlockHeight = manifest.firstBlockHeight;
274
+ state.lastBlockHeight = manifest.lastBlockHeight;
250
275
  state.artifactSizeBytes = manifest.artifactSizeBytes;
251
276
  state.artifactSha256 = manifest.artifactSha256;
252
277
  state.chunkSizeBytes = manifest.chunkSizeBytes;
@@ -304,14 +329,14 @@ async function updateDownloadProgress(progress, manifest, state, startedAtUnixMs
304
329
  const bytesPerSecond = Math.max(0, (downloadedBytes - attemptStartBytes) / elapsedSeconds);
305
330
  const remaining = Math.max(0, manifest.artifactSizeBytes - downloadedBytes);
306
331
  await progress.setPhase("getblock_archive_download", {
307
- message: "Downloading getblock archive.",
332
+ message: "Downloading getblock range.",
308
333
  resumed: downloadedBytes > 0,
309
334
  downloadedBytes,
310
335
  totalBytes: manifest.artifactSizeBytes,
311
336
  percent: manifest.artifactSizeBytes > 0 ? (downloadedBytes / manifest.artifactSizeBytes) * 100 : 0,
312
337
  bytesPerSecond,
313
338
  etaSeconds: bytesPerSecond > 0 ? remaining / bytesPerSecond : null,
314
- targetHeight: manifest.endHeight,
339
+ targetHeight: manifest.lastBlockHeight,
315
340
  lastError: state.lastError,
316
341
  });
317
342
  }
@@ -320,7 +345,6 @@ async function downloadRemoteArchive(paths, manifest, state, progress, fetchImpl
320
345
  if (state.downloadedBytes >= manifest.artifactSizeBytes) {
321
346
  await validateWholeFile(paths.partialArtifactPath, manifest);
322
347
  await rename(paths.partialArtifactPath, paths.artifactPath);
323
- await rename(paths.partialManifestPath, paths.manifestPath);
324
348
  state.validated = true;
325
349
  state.verifiedChunkCount = manifest.chunkSha256s.length;
326
350
  state.downloadedBytes = manifest.artifactSizeBytes;
@@ -332,7 +356,7 @@ async function downloadRemoteArchive(paths, manifest, state, progress, fetchImpl
332
356
  while (true) {
333
357
  const startOffset = state.downloadedBytes;
334
358
  try {
335
- const response = await fetchImpl(`${GETBLOCK_ARCHIVE_REMOTE_DATA_URL}?end=${manifest.endHeight}`, {
359
+ const response = await fetchImpl(`${GETBLOCK_ARCHIVE_REMOTE_BASE_URL}${manifest.artifactFilename}`, {
336
360
  headers: startOffset > 0 ? { Range: `bytes=${startOffset}-` } : undefined,
337
361
  signal,
338
362
  });
@@ -401,21 +425,20 @@ async function downloadRemoteArchive(paths, manifest, state, progress, fetchImpl
401
425
  }
402
426
  await validateWholeFile(paths.partialArtifactPath, manifest);
403
427
  await rename(paths.partialArtifactPath, paths.artifactPath);
404
- await rename(paths.partialManifestPath, paths.manifestPath);
405
428
  state.validated = true;
406
429
  state.verifiedChunkCount = manifest.chunkSha256s.length;
407
430
  state.downloadedBytes = manifest.artifactSizeBytes;
408
431
  state.lastError = null;
409
432
  await saveState(paths, state);
410
433
  await progress.setPhase("getblock_archive_download", {
411
- message: "Downloading getblock archive.",
434
+ message: "Downloading getblock range.",
412
435
  resumed: startOffset > 0,
413
436
  downloadedBytes: manifest.artifactSizeBytes,
414
437
  totalBytes: manifest.artifactSizeBytes,
415
438
  percent: 100,
416
439
  bytesPerSecond: null,
417
440
  etaSeconds: 0,
418
- targetHeight: manifest.endHeight,
441
+ targetHeight: manifest.lastBlockHeight,
419
442
  lastError: null,
420
443
  });
421
444
  return;
@@ -427,14 +450,14 @@ async function downloadRemoteArchive(paths, manifest, state, progress, fetchImpl
427
450
  throw error;
428
451
  }
429
452
  await progress.setPhase("getblock_archive_download", {
430
- message: "Downloading getblock archive.",
453
+ message: "Downloading getblock range.",
431
454
  resumed: startOffset > 0,
432
455
  downloadedBytes: state.downloadedBytes,
433
456
  totalBytes: manifest.artifactSizeBytes,
434
457
  percent: manifest.artifactSizeBytes > 0 ? (state.downloadedBytes / manifest.artifactSizeBytes) * 100 : 0,
435
458
  bytesPerSecond: null,
436
459
  etaSeconds: null,
437
- targetHeight: manifest.endHeight,
460
+ targetHeight: manifest.lastBlockHeight,
438
461
  lastError: state.lastError,
439
462
  });
440
463
  await sleep(retryDelayMs, signal);
@@ -442,27 +465,21 @@ async function downloadRemoteArchive(paths, manifest, state, progress, fetchImpl
442
465
  }
443
466
  }
444
467
  }
445
- async function fetchLatestManifest(fetchImpl, cacheBustEnd, signal) {
446
- const url = cacheBustEnd === null
447
- ? GETBLOCK_ARCHIVE_REMOTE_MANIFEST_URL
448
- : `${GETBLOCK_ARCHIVE_REMOTE_MANIFEST_URL}?end=${cacheBustEnd}`;
449
- const response = await fetchImpl(url, { signal });
468
+ async function fetchManifestRange(fetchImpl, firstBlockHeight, lastBlockHeight, signal) {
469
+ const response = await fetchImpl(GETBLOCK_ARCHIVE_REMOTE_MANIFEST_URL, { signal });
470
+ if (response.status === 404) {
471
+ return null;
472
+ }
450
473
  if (!response.ok) {
451
474
  throw new Error(`managed_getblock_archive_manifest_http_${response.status}`);
452
475
  }
453
- return assertManifestShape(await response.json());
454
- }
455
- export function resolveGetblockArchivePathsForTesting(dataDir) {
456
- return resolvePaths(dataDir);
476
+ const manifest = assertAggregateManifestShape(await response.json());
477
+ return manifest.ranges.find((range) => range.firstBlockHeight === firstBlockHeight && range.lastBlockHeight === lastBlockHeight) ?? null;
457
478
  }
458
- export async function resolveReadyGetblockArchiveForTesting(dataDir) {
459
- return resolveReadyLocalGetblockArchive(resolvePaths(dataDir));
479
+ export function resolveGetblockArchivePathsForTesting(dataDir, firstBlockHeight = GETBLOCK_ARCHIVE_FIRST_HEIGHT, lastBlockHeight = GETBLOCK_ARCHIVE_BASE_HEIGHT + GETBLOCK_ARCHIVE_RANGE_SIZE) {
480
+ return resolvePaths(dataDir, firstBlockHeight, lastBlockHeight);
460
481
  }
461
- async function resolveReadyLocalGetblockArchive(paths, state = null) {
462
- const manifest = await readManifest(paths.manifestPath).catch(() => null);
463
- if (manifest === null) {
464
- return null;
465
- }
482
+ async function resolveReadyLocalGetblockArchive(paths, manifest, state = null) {
466
483
  const info = await statOrNull(paths.artifactPath);
467
484
  if (info === null || info.size !== manifest.artifactSizeBytes) {
468
485
  return null;
@@ -472,7 +489,8 @@ async function resolveReadyLocalGetblockArchive(paths, state = null) {
472
489
  try {
473
490
  await validateWholeFile(paths.artifactPath, manifest);
474
491
  loadedState.formatVersion = manifest.formatVersion;
475
- loadedState.endHeight = manifest.endHeight;
492
+ loadedState.firstBlockHeight = manifest.firstBlockHeight;
493
+ loadedState.lastBlockHeight = manifest.lastBlockHeight;
476
494
  loadedState.artifactSizeBytes = manifest.artifactSizeBytes;
477
495
  loadedState.artifactSha256 = manifest.artifactSha256;
478
496
  loadedState.chunkSizeBytes = manifest.chunkSizeBytes;
@@ -489,40 +507,71 @@ async function resolveReadyLocalGetblockArchive(paths, state = null) {
489
507
  return {
490
508
  manifest,
491
509
  artifactPath: paths.artifactPath,
492
- manifestPath: paths.manifestPath,
493
510
  };
494
511
  }
495
- export async function prepareLatestGetblockArchive(options) {
496
- const paths = resolvePaths(options.dataDir);
512
+ export async function resolveReadyGetblockArchiveForTesting(dataDir, manifest) {
513
+ return resolveReadyLocalGetblockArchive(resolvePaths(dataDir, manifest.firstBlockHeight, manifest.lastBlockHeight), manifest);
514
+ }
515
+ export async function prepareGetblockArchiveRange(options) {
516
+ const paths = resolvePaths(options.dataDir, options.firstBlockHeight, options.lastBlockHeight);
497
517
  await mkdir(paths.directory, { recursive: true });
498
518
  const state = await loadState(paths);
499
- const readyLocal = await resolveReadyLocalGetblockArchive(paths, state);
500
519
  const fetchImpl = options.fetchImpl ?? fetch;
501
520
  let remoteManifest;
502
521
  try {
503
- remoteManifest = await fetchLatestManifest(fetchImpl, readyLocal?.manifest.endHeight ?? null, options.signal);
522
+ remoteManifest = await fetchManifestRange(fetchImpl, options.firstBlockHeight, options.lastBlockHeight, options.signal);
504
523
  }
505
524
  catch {
506
- return readyLocal;
525
+ const readyLocal = {
526
+ formatVersion: state.formatVersion,
527
+ chain: "main",
528
+ baseSnapshotHeight: GETBLOCK_ARCHIVE_BASE_HEIGHT,
529
+ firstBlockHeight: state.firstBlockHeight ?? options.firstBlockHeight,
530
+ lastBlockHeight: state.lastBlockHeight ?? options.lastBlockHeight,
531
+ artifactFilename: buildRangeFilename(state.firstBlockHeight ?? options.firstBlockHeight, state.lastBlockHeight ?? options.lastBlockHeight),
532
+ artifactSizeBytes: state.artifactSizeBytes,
533
+ artifactSha256: state.artifactSha256 ?? "",
534
+ chunkSizeBytes: state.chunkSizeBytes,
535
+ chunkSha256s: [],
536
+ };
537
+ if (state.validated
538
+ && state.firstBlockHeight === options.firstBlockHeight
539
+ && state.lastBlockHeight === options.lastBlockHeight
540
+ && state.artifactSha256 !== null) {
541
+ const resolved = await resolveReadyLocalGetblockArchive(paths, readyLocal, state).catch(() => null);
542
+ if (resolved !== null) {
543
+ return resolved;
544
+ }
545
+ }
546
+ throw new Error("managed_getblock_archive_manifest_refresh_failed");
547
+ }
548
+ if (remoteManifest === null) {
549
+ return null;
507
550
  }
551
+ const readyLocal = await resolveReadyLocalGetblockArchive(paths, remoteManifest, state);
508
552
  if (readyLocal !== null
509
- && readyLocal.manifest.endHeight === remoteManifest.endHeight
510
553
  && readyLocal.manifest.artifactSha256 === remoteManifest.artifactSha256) {
511
554
  return readyLocal;
512
555
  }
513
- if (readyLocal !== null && readyLocal.manifest.endHeight > remoteManifest.endHeight) {
514
- return readyLocal;
515
- }
516
- await writeJsonAtomic(paths.partialManifestPath, remoteManifest);
517
- try {
518
- await downloadRemoteArchive(paths, remoteManifest, state, options.progress, fetchImpl, options.signal);
519
- }
520
- catch {
521
- return readyLocal;
522
- }
523
- return resolveReadyLocalGetblockArchive(paths, state);
556
+ await downloadRemoteArchive(paths, remoteManifest, state, options.progress, fetchImpl, options.signal);
557
+ return resolveReadyLocalGetblockArchive(paths, remoteManifest, state);
558
+ }
559
+ export async function deleteGetblockArchiveRange(options) {
560
+ const paths = resolvePaths(options.dataDir, options.firstBlockHeight, options.lastBlockHeight);
561
+ await rm(paths.artifactPath, { force: true }).catch(() => undefined);
562
+ await rm(paths.partialArtifactPath, { force: true }).catch(() => undefined);
563
+ await rm(paths.statePath, { force: true }).catch(() => undefined);
564
+ }
565
+ export async function prepareLatestGetblockArchive(options) {
566
+ return prepareGetblockArchiveRange({
567
+ ...options,
568
+ firstBlockHeight: GETBLOCK_ARCHIVE_FIRST_HEIGHT,
569
+ lastBlockHeight: GETBLOCK_ARCHIVE_BASE_HEIGHT + GETBLOCK_ARCHIVE_RANGE_SIZE,
570
+ });
524
571
  }
525
572
  export const prepareLatestGetblockArchiveForTesting = prepareLatestGetblockArchive;
573
+ export const prepareGetblockArchiveRangeForTesting = prepareGetblockArchiveRange;
574
+ export const deleteGetblockArchiveRangeForTesting = deleteGetblockArchiveRange;
526
575
  export async function waitForGetblockArchiveImportForTesting(rpc, progress, targetEndHeight, signal) {
527
576
  await waitForGetblockArchiveImport(rpc, progress, targetEndHeight, signal);
528
577
  }
@@ -533,7 +582,7 @@ export async function waitForGetblockArchiveImport(rpc, progress, targetEndHeigh
533
582
  }
534
583
  const info = await rpc.getBlockchainInfo();
535
584
  await progress.setPhase("getblock_archive_import", {
536
- message: "Bitcoin Core is importing getblock archive blocks.",
585
+ message: "Bitcoin Core is importing getblock range blocks.",
537
586
  blocks: info.blocks,
538
587
  headers: info.headers,
539
588
  targetHeight: targetEndHeight,
@@ -1,7 +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
+ export { deleteGetblockArchiveRange, deleteGetblockArchiveRangeForTesting, prepareGetblockArchiveRange, prepareGetblockArchiveRangeForTesting, prepareLatestGetblockArchive, prepareLatestGetblockArchiveForTesting, resolveGetblockArchivePathsForTesting, resolveReadyGetblockArchiveForTesting, waitForGetblockArchiveImport, waitForGetblockArchiveImportForTesting, } from "./bootstrap/getblock-archive.js";
5
5
  export { waitForHeadersForTesting } from "./bootstrap/headers.js";
6
6
  export { resolveBootstrapPathsForTesting } from "./bootstrap/paths.js";
7
7
  export { loadBootstrapStateForTesting, saveBootstrapStateForTesting, createBootstrapStateForTesting, } from "./bootstrap/state.js";
@@ -1,7 +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
+ export { deleteGetblockArchiveRange, deleteGetblockArchiveRangeForTesting, prepareGetblockArchiveRange, prepareGetblockArchiveRangeForTesting, prepareLatestGetblockArchive, prepareLatestGetblockArchiveForTesting, resolveGetblockArchivePathsForTesting, resolveReadyGetblockArchiveForTesting, waitForGetblockArchiveImport, waitForGetblockArchiveImportForTesting, } from "./bootstrap/getblock-archive.js";
5
5
  export { waitForHeadersForTesting } from "./bootstrap/headers.js";
6
6
  export { resolveBootstrapPathsForTesting } from "./bootstrap/paths.js";
7
7
  export { loadBootstrapStateForTesting, saveBootstrapStateForTesting, createBootstrapStateForTesting, } from "./bootstrap/state.js";
@@ -1,12 +1,12 @@
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, prepareLatestGetblockArchiveForTesting, resolveBootstrapPathsForTesting, } from "../bootstrap.js";
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
7
  import { assertCogcoinProcessingStartHeight, resolveCogcoinProcessingStartHeight, } from "../processing-start-height.js";
8
8
  import { ManagedProgressController } from "../progress.js";
9
- import { attachOrStartManagedBitcoindService, probeManagedBitcoindService, stopManagedBitcoindService, } from "../service.js";
9
+ import { attachOrStartManagedBitcoindService, } from "../service.js";
10
10
  import { DefaultManagedBitcoindClient } from "./managed-client.js";
11
11
  const DEFAULT_SYNC_DEBOUNCE_MS = 250;
12
12
  async function createManagedBitcoindClient(options) {
@@ -27,52 +27,17 @@ async function createManagedBitcoindClient(options) {
27
27
  try {
28
28
  await progress.start();
29
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
30
  const node = await attachOrStartManagedBitcoindService({
70
31
  ...options,
71
32
  dataDir,
72
- getblockArchivePath: getblockArchive?.artifactPath ?? null,
73
- getblockArchiveEndHeight: getblockArchive?.manifest.endHeight ?? null,
74
- getblockArchiveSha256: getblockArchive?.manifest.artifactSha256 ?? null,
33
+ getblockArchivePath: null,
34
+ getblockArchiveEndHeight: null,
35
+ getblockArchiveSha256: null,
75
36
  });
37
+ const walletRootId = options.walletRootId ?? node.walletRootId;
38
+ if (walletRootId === undefined) {
39
+ throw new Error("managed_bitcoind_wallet_root_unavailable");
40
+ }
76
41
  const rpc = createRpcClient(node.rpc);
77
42
  const bootstrap = new AssumeUtxoBootstrapController({
78
43
  rpc,
@@ -105,7 +70,7 @@ async function createManagedBitcoindClient(options) {
105
70
  walletRootId: options.walletRootId,
106
71
  startupTimeoutMs: options.startupTimeoutMs,
107
72
  })
108
- : null, options.startHeight, options.syncDebounceMs ?? DEFAULT_SYNC_DEBOUNCE_MS);
73
+ : null, options.startHeight, options.syncDebounceMs ?? DEFAULT_SYNC_DEBOUNCE_MS, dataDir, walletRootId, options.startupTimeoutMs, options.shutdownTimeoutMs, options.fetchImpl);
109
74
  }
110
75
  catch (error) {
111
76
  if (progressStarted) {
@@ -54,7 +54,7 @@ export function scheduleSync(dependencies) {
54
54
  }
55
55
  const timer = setTimeout(() => {
56
56
  dependencies.setDebounceTimer(null);
57
- void dependencies.syncToTip();
57
+ void dependencies.syncToTip().catch(() => undefined);
58
58
  }, dependencies.syncDebounceMs);
59
59
  dependencies.setDebounceTimer(timer);
60
60
  }
@@ -21,6 +21,7 @@ export interface SyncEngineDependencies {
21
21
  progress: ManagedProgressController;
22
22
  bootstrap: AssumeUtxoBootstrapController;
23
23
  startHeight: number;
24
+ targetHeightCap?: number | null;
24
25
  bitcoinRateTracker: BlockRateTracker;
25
26
  cogcoinRateTracker: BlockRateTracker;
26
27
  abortSignal?: AbortSignal;
@@ -1,13 +1,13 @@
1
1
  import type { BitcoinBlock } from "@cogcoin/indexer/types";
2
2
  import type { Client, ClientStoreAdapter } from "../../types.js";
3
- import type { AssumeUtxoBootstrapController } from "../bootstrap.js";
3
+ import { AssumeUtxoBootstrapController } from "../bootstrap.js";
4
4
  import type { IndexerDaemonClient } from "../indexer-daemon.js";
5
5
  import type { ManagedProgressController } from "../progress.js";
6
6
  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, reattachIndexerDaemon: (() => Promise<IndexerDaemonClient | null>) | null, startHeight: number, syncDebounceMs: number);
10
+ constructor(client: Client, store: ClientStoreAdapter, node: ManagedBitcoindNodeHandle, rpc: BitcoinRpcClient, progress: ManagedProgressController, bootstrap: AssumeUtxoBootstrapController, indexerDaemon: IndexerDaemonClient | null, reattachIndexerDaemon: (() => Promise<IndexerDaemonClient | null>) | null, startHeight: number, syncDebounceMs: number, dataDir: string, walletRootId: string, startupTimeoutMs: number | undefined, shutdownTimeoutMs: number | undefined, fetchImpl: typeof fetch | undefined);
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>;