@cogcoin/client 0.5.8 → 0.5.10
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 +8 -7
- package/dist/bitcoind/bootstrap/getblock-archive.d.ts +17 -5
- package/dist/bitcoind/bootstrap/getblock-archive.js +139 -90
- package/dist/bitcoind/bootstrap.d.ts +1 -1
- package/dist/bitcoind/bootstrap.js +1 -1
- package/dist/bitcoind/client/factory.js +10 -45
- package/dist/bitcoind/client/follow-loop.js +1 -1
- package/dist/bitcoind/client/internal-types.d.ts +1 -0
- package/dist/bitcoind/client/managed-client.d.ts +2 -2
- package/dist/bitcoind/client/managed-client.js +175 -15
- package/dist/bitcoind/client/sync-engine.js +19 -11
- package/dist/bitcoind/errors.js +6 -6
- package/dist/bitcoind/progress/formatting.js +4 -4
- package/dist/bitcoind/testing.d.ts +1 -1
- package/dist/bitcoind/testing.js +1 -1
- package/dist/bitcoind/types.d.ts +11 -12
- package/dist/cli/commands/follow.js +0 -2
- package/dist/cli/commands/sync.js +0 -2
- package/dist/cli/signals.js +15 -1
- package/dist/wallet/tx/anchor.js +64 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@cogcoin/client`
|
|
2
2
|
|
|
3
|
-
`@cogcoin/client@0.5.
|
|
3
|
+
`@cogcoin/client@0.5.10` 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
|
|
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
|
|
125
|
-
5. loads that
|
|
126
|
-
6.
|
|
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
|
|
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
|
|
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
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const GETBLOCK_ARCHIVE_REMOTE_MANIFEST_URL =
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
39
|
-
|| typeof manifest.
|
|
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
|
-
||
|
|
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
|
|
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,
|
|
54
|
-
partialArtifactPath: join(directory, `${
|
|
55
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
215
|
-
if (
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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(`${
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
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
|
|
459
|
-
return
|
|
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.
|
|
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
|
|
496
|
-
|
|
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
|
|
522
|
+
remoteManifest = await fetchManifestRange(fetchImpl, options.firstBlockHeight, options.lastBlockHeight, options.signal);
|
|
504
523
|
}
|
|
505
524
|
catch {
|
|
506
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
520
|
-
catch
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
return
|
|
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
|
|
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,
|
|
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,
|
|
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:
|
|
73
|
-
getblockArchiveEndHeight:
|
|
74
|
-
getblockArchiveSha256:
|
|
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
|
|
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>;
|