@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
|
@@ -1,7 +1,39 @@
|
|
|
1
|
+
import { AssumeUtxoBootstrapController, deleteGetblockArchiveRange, prepareGetblockArchiveRange, } from "../bootstrap.js";
|
|
2
|
+
import { createRpcClient } from "../node.js";
|
|
3
|
+
import { attachOrStartManagedBitcoindService, stopManagedBitcoindService, } from "../service.js";
|
|
1
4
|
import { closeFollowLoopResources, scheduleSync, startFollowingTipLoop } from "./follow-loop.js";
|
|
2
5
|
import { loadVisibleFollowBlockTimes } from "./follow-block-times.js";
|
|
3
6
|
import { createBlockRateTracker, createInitialSyncResult, } from "./internal-types.js";
|
|
4
7
|
import { syncToTip as runManagedSync } from "./sync-engine.js";
|
|
8
|
+
const GETBLOCK_RANGE_BASE_HEIGHT = 910_000;
|
|
9
|
+
const GETBLOCK_RANGE_SIZE = 500;
|
|
10
|
+
function isBoundaryHeight(height) {
|
|
11
|
+
return height >= GETBLOCK_RANGE_BASE_HEIGHT
|
|
12
|
+
&& (height - GETBLOCK_RANGE_BASE_HEIGHT) % GETBLOCK_RANGE_SIZE === 0;
|
|
13
|
+
}
|
|
14
|
+
function resolveNextBoundary(height) {
|
|
15
|
+
if (height < GETBLOCK_RANGE_BASE_HEIGHT) {
|
|
16
|
+
return GETBLOCK_RANGE_BASE_HEIGHT;
|
|
17
|
+
}
|
|
18
|
+
if (isBoundaryHeight(height)) {
|
|
19
|
+
return height;
|
|
20
|
+
}
|
|
21
|
+
return GETBLOCK_RANGE_BASE_HEIGHT
|
|
22
|
+
+ Math.ceil((height - GETBLOCK_RANGE_BASE_HEIGHT) / GETBLOCK_RANGE_SIZE) * GETBLOCK_RANGE_SIZE;
|
|
23
|
+
}
|
|
24
|
+
function mergeSyncResults(target, source) {
|
|
25
|
+
target.appliedBlocks += source.appliedBlocks;
|
|
26
|
+
target.rewoundBlocks += source.rewoundBlocks;
|
|
27
|
+
target.startingHeight = target.startingHeight ?? source.startingHeight;
|
|
28
|
+
target.endingHeight = source.endingHeight;
|
|
29
|
+
target.bestHeight = source.bestHeight;
|
|
30
|
+
target.bestHashHex = source.bestHashHex;
|
|
31
|
+
if (source.commonAncestorHeight !== null) {
|
|
32
|
+
target.commonAncestorHeight = target.commonAncestorHeight === null
|
|
33
|
+
? source.commonAncestorHeight
|
|
34
|
+
: Math.min(target.commonAncestorHeight, source.commonAncestorHeight);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
5
37
|
export class DefaultManagedBitcoindClient {
|
|
6
38
|
#client;
|
|
7
39
|
#store;
|
|
@@ -13,6 +45,11 @@ export class DefaultManagedBitcoindClient {
|
|
|
13
45
|
#reattachIndexerDaemon;
|
|
14
46
|
#startHeight;
|
|
15
47
|
#syncDebounceMs;
|
|
48
|
+
#dataDir;
|
|
49
|
+
#walletRootId;
|
|
50
|
+
#startupTimeoutMs;
|
|
51
|
+
#shutdownTimeoutMs;
|
|
52
|
+
#fetchImpl;
|
|
16
53
|
#following = false;
|
|
17
54
|
#closed = false;
|
|
18
55
|
#subscriber = null;
|
|
@@ -23,7 +60,7 @@ export class DefaultManagedBitcoindClient {
|
|
|
23
60
|
#syncPromise = Promise.resolve(createInitialSyncResult());
|
|
24
61
|
#debounceTimer = null;
|
|
25
62
|
#syncAbortControllers = new Set();
|
|
26
|
-
constructor(client, store, node, rpc, progress, bootstrap, indexerDaemon, reattachIndexerDaemon, startHeight, syncDebounceMs) {
|
|
63
|
+
constructor(client, store, node, rpc, progress, bootstrap, indexerDaemon, reattachIndexerDaemon, startHeight, syncDebounceMs, dataDir, walletRootId, startupTimeoutMs, shutdownTimeoutMs, fetchImpl) {
|
|
27
64
|
this.#client = client;
|
|
28
65
|
this.#store = store;
|
|
29
66
|
this.#node = node;
|
|
@@ -34,6 +71,11 @@ export class DefaultManagedBitcoindClient {
|
|
|
34
71
|
this.#reattachIndexerDaemon = reattachIndexerDaemon;
|
|
35
72
|
this.#startHeight = startHeight;
|
|
36
73
|
this.#syncDebounceMs = syncDebounceMs;
|
|
74
|
+
this.#dataDir = dataDir;
|
|
75
|
+
this.#walletRootId = walletRootId;
|
|
76
|
+
this.#startupTimeoutMs = startupTimeoutMs;
|
|
77
|
+
this.#shutdownTimeoutMs = shutdownTimeoutMs;
|
|
78
|
+
this.#fetchImpl = fetchImpl;
|
|
37
79
|
}
|
|
38
80
|
async getTip() {
|
|
39
81
|
return this.#client.getTip();
|
|
@@ -54,20 +96,10 @@ export class DefaultManagedBitcoindClient {
|
|
|
54
96
|
const abortController = new AbortController();
|
|
55
97
|
this.#syncAbortControllers.add(abortController);
|
|
56
98
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
rpc: this.#rpc,
|
|
62
|
-
progress: this.#progress,
|
|
63
|
-
bootstrap: this.#bootstrap,
|
|
64
|
-
startHeight: this.#startHeight,
|
|
65
|
-
bitcoinRateTracker: this.#bitcoinRateTracker,
|
|
66
|
-
cogcoinRateTracker: this.#cogcoinRateTracker,
|
|
67
|
-
abortSignal: abortController.signal,
|
|
68
|
-
isFollowing: () => this.#following,
|
|
69
|
-
loadVisibleFollowBlockTimes: (tip) => this.#loadVisibleFollowBlockTimes(tip),
|
|
70
|
-
});
|
|
99
|
+
if (this.#node.expectedChain !== "main") {
|
|
100
|
+
return await this.#runManagedSyncPass(null, abortController.signal);
|
|
101
|
+
}
|
|
102
|
+
return await this.#syncWithStagedRanges(abortController.signal);
|
|
71
103
|
}
|
|
72
104
|
finally {
|
|
73
105
|
this.#syncAbortControllers.delete(abortController);
|
|
@@ -186,6 +218,134 @@ export class DefaultManagedBitcoindClient {
|
|
|
186
218
|
this.#assertOpen();
|
|
187
219
|
await this.#progress.playCompletionScene();
|
|
188
220
|
}
|
|
221
|
+
async #syncWithStagedRanges(abortSignal) {
|
|
222
|
+
const aggregate = createInitialSyncResult();
|
|
223
|
+
let stagedModeEnabled = true;
|
|
224
|
+
await this.#ensureBootstrapReady(abortSignal);
|
|
225
|
+
while (stagedModeEnabled) {
|
|
226
|
+
const info = await this.#rpc.getBlockchainInfo();
|
|
227
|
+
if (info.blocks < GETBLOCK_RANGE_BASE_HEIGHT) {
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
const nextBoundary = resolveNextBoundary(info.blocks);
|
|
231
|
+
if (nextBoundary === null) {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
if (!isBoundaryHeight(info.blocks)) {
|
|
235
|
+
mergeSyncResults(aggregate, await this.#runManagedSyncPass(nextBoundary, abortSignal));
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const firstBlockHeight = info.blocks + 1;
|
|
239
|
+
const lastBlockHeight = info.blocks + GETBLOCK_RANGE_SIZE;
|
|
240
|
+
let readyRange;
|
|
241
|
+
try {
|
|
242
|
+
readyRange = await prepareGetblockArchiveRange({
|
|
243
|
+
dataDir: this.#dataDir,
|
|
244
|
+
progress: this.#progress,
|
|
245
|
+
firstBlockHeight,
|
|
246
|
+
lastBlockHeight,
|
|
247
|
+
fetchImpl: this.#fetchImpl,
|
|
248
|
+
signal: abortSignal,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
stagedModeEnabled = false;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
if (readyRange === null) {
|
|
256
|
+
stagedModeEnabled = false;
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
const stagedRestartActive = await this.#restartManagedNodeWithRange(readyRange, abortSignal);
|
|
260
|
+
mergeSyncResults(aggregate, await this.#runManagedSyncPass(lastBlockHeight, abortSignal));
|
|
261
|
+
if (stagedRestartActive) {
|
|
262
|
+
await deleteGetblockArchiveRange({
|
|
263
|
+
dataDir: this.#dataDir,
|
|
264
|
+
firstBlockHeight,
|
|
265
|
+
lastBlockHeight,
|
|
266
|
+
}).catch(() => undefined);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
stagedModeEnabled = false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
mergeSyncResults(aggregate, await this.#runManagedSyncPass(null, abortSignal));
|
|
273
|
+
return aggregate;
|
|
274
|
+
}
|
|
275
|
+
async #runManagedSyncPass(targetHeightCap, abortSignal) {
|
|
276
|
+
return runManagedSync({
|
|
277
|
+
client: this.#client,
|
|
278
|
+
store: this.#store,
|
|
279
|
+
node: this.#node,
|
|
280
|
+
rpc: this.#rpc,
|
|
281
|
+
progress: this.#progress,
|
|
282
|
+
bootstrap: this.#bootstrap,
|
|
283
|
+
startHeight: this.#startHeight,
|
|
284
|
+
targetHeightCap,
|
|
285
|
+
bitcoinRateTracker: this.#bitcoinRateTracker,
|
|
286
|
+
cogcoinRateTracker: this.#cogcoinRateTracker,
|
|
287
|
+
abortSignal,
|
|
288
|
+
isFollowing: () => this.#following,
|
|
289
|
+
loadVisibleFollowBlockTimes: (tip) => this.#loadVisibleFollowBlockTimes(tip),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
async #ensureBootstrapReady(signal) {
|
|
293
|
+
await this.#node.validate();
|
|
294
|
+
const indexedTipBeforeBootstrap = await this.#client.getTip();
|
|
295
|
+
await this.#bootstrap.ensureReady(indexedTipBeforeBootstrap, this.#node.expectedChain, { signal });
|
|
296
|
+
}
|
|
297
|
+
async #restartManagedNodeWithRange(readyRange, abortSignal) {
|
|
298
|
+
if (abortSignal.aborted) {
|
|
299
|
+
throw abortSignal.reason instanceof Error ? abortSignal.reason : new Error("managed_sync_aborted");
|
|
300
|
+
}
|
|
301
|
+
const baseOptions = {
|
|
302
|
+
chain: this.#node.expectedChain,
|
|
303
|
+
startHeight: this.#node.startHeight,
|
|
304
|
+
dataDir: this.#dataDir,
|
|
305
|
+
walletRootId: this.#walletRootId,
|
|
306
|
+
startupTimeoutMs: this.#startupTimeoutMs,
|
|
307
|
+
shutdownTimeoutMs: this.#shutdownTimeoutMs,
|
|
308
|
+
};
|
|
309
|
+
try {
|
|
310
|
+
await stopManagedBitcoindService({
|
|
311
|
+
dataDir: this.#dataDir,
|
|
312
|
+
walletRootId: this.#walletRootId,
|
|
313
|
+
shutdownTimeoutMs: this.#shutdownTimeoutMs,
|
|
314
|
+
});
|
|
315
|
+
const node = await attachOrStartManagedBitcoindService({
|
|
316
|
+
...baseOptions,
|
|
317
|
+
getblockArchivePath: readyRange.artifactPath,
|
|
318
|
+
getblockArchiveEndHeight: readyRange.manifest.lastBlockHeight,
|
|
319
|
+
getblockArchiveSha256: readyRange.manifest.artifactSha256,
|
|
320
|
+
});
|
|
321
|
+
await this.#replaceManagedBindings(node);
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
const node = await attachOrStartManagedBitcoindService({
|
|
326
|
+
...baseOptions,
|
|
327
|
+
getblockArchivePath: null,
|
|
328
|
+
getblockArchiveEndHeight: null,
|
|
329
|
+
getblockArchiveSha256: null,
|
|
330
|
+
});
|
|
331
|
+
await this.#replaceManagedBindings(node);
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async #replaceManagedBindings(node) {
|
|
336
|
+
this.#node = node;
|
|
337
|
+
this.#rpc = createRpcClient(node.rpc);
|
|
338
|
+
this.#bootstrap = new AssumeUtxoBootstrapController({
|
|
339
|
+
rpc: this.#rpc,
|
|
340
|
+
dataDir: node.dataDir,
|
|
341
|
+
progress: this.#progress,
|
|
342
|
+
snapshot: this.#progress.getStatusSnapshot().snapshot,
|
|
343
|
+
});
|
|
344
|
+
if (this.#subscriber !== null) {
|
|
345
|
+
this.#subscriber.connect(node.zmq.endpoint);
|
|
346
|
+
this.#subscriber.subscribe(node.zmq.topic);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
189
349
|
#scheduleSync() {
|
|
190
350
|
scheduleSync({
|
|
191
351
|
syncDebounceMs: this.#syncDebounceMs,
|
|
@@ -39,12 +39,13 @@ function sleep(ms, signal) {
|
|
|
39
39
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
-
async function setBitcoinSyncProgress(dependencies, info) {
|
|
43
|
-
const
|
|
42
|
+
async function setBitcoinSyncProgress(dependencies, info, targetHeightCap) {
|
|
43
|
+
const targetHeight = targetHeightCap === null ? info.headers : Math.min(info.headers, targetHeightCap);
|
|
44
|
+
const etaSeconds = estimateEtaSeconds(dependencies.bitcoinRateTracker, info.blocks, targetHeight);
|
|
44
45
|
await dependencies.progress.setPhase("bitcoin_sync", {
|
|
45
46
|
blocks: info.blocks,
|
|
46
47
|
headers: info.headers,
|
|
47
|
-
targetHeight
|
|
48
|
+
targetHeight,
|
|
48
49
|
etaSeconds,
|
|
49
50
|
lastError: null,
|
|
50
51
|
message: dependencies.node.expectedChain === "main"
|
|
@@ -171,8 +172,11 @@ export async function syncToTip(dependencies) {
|
|
|
171
172
|
while (true) {
|
|
172
173
|
throwIfAborted(dependencies.abortSignal);
|
|
173
174
|
const startInfo = await runRpc(() => dependencies.rpc.getBlockchainInfo());
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
const cappedBestHeight = dependencies.targetHeightCap === null || dependencies.targetHeightCap === undefined
|
|
176
|
+
? startInfo.blocks
|
|
177
|
+
: Math.min(startInfo.blocks, dependencies.targetHeightCap);
|
|
178
|
+
await setBitcoinSyncProgress(dependencies, startInfo, dependencies.targetHeightCap ?? null);
|
|
179
|
+
const pass = await syncAgainstBestHeight(dependencies, cappedBestHeight, runRpc);
|
|
176
180
|
aggregate.appliedBlocks += pass.appliedBlocks;
|
|
177
181
|
aggregate.rewoundBlocks += pass.rewoundBlocks;
|
|
178
182
|
if (pass.commonAncestorHeight !== null) {
|
|
@@ -182,18 +186,22 @@ export async function syncToTip(dependencies) {
|
|
|
182
186
|
}
|
|
183
187
|
const finalTip = await dependencies.client.getTip();
|
|
184
188
|
const endInfo = await runRpc(() => dependencies.rpc.getBlockchainInfo());
|
|
185
|
-
const
|
|
189
|
+
const endBestHeight = dependencies.targetHeightCap === null || dependencies.targetHeightCap === undefined
|
|
190
|
+
? endInfo.blocks
|
|
191
|
+
: Math.min(endInfo.blocks, dependencies.targetHeightCap);
|
|
192
|
+
const caughtUpCogcoin = endBestHeight < dependencies.startHeight || finalTip?.height === endBestHeight;
|
|
186
193
|
aggregate.endingHeight = finalTip?.height ?? null;
|
|
187
|
-
aggregate.bestHeight =
|
|
194
|
+
aggregate.bestHeight = endBestHeight;
|
|
188
195
|
aggregate.bestHashHex = endInfo.bestblockhash;
|
|
189
|
-
if (
|
|
196
|
+
if ((dependencies.targetHeightCap !== null && dependencies.targetHeightCap !== undefined && caughtUpCogcoin)
|
|
197
|
+
|| (endInfo.blocks === endInfo.headers && caughtUpCogcoin)) {
|
|
190
198
|
if (dependencies.isFollowing()) {
|
|
191
199
|
dependencies.progress.replaceFollowBlockTimes(await runRpc(() => dependencies.loadVisibleFollowBlockTimes(finalTip)));
|
|
192
200
|
}
|
|
193
201
|
await dependencies.progress.setPhase(dependencies.isFollowing() ? "follow_tip" : "complete", {
|
|
194
202
|
blocks: endInfo.blocks,
|
|
195
203
|
headers: endInfo.headers,
|
|
196
|
-
targetHeight: endInfo.headers,
|
|
204
|
+
targetHeight: dependencies.targetHeightCap ?? endInfo.headers,
|
|
197
205
|
lastError: null,
|
|
198
206
|
message: dependencies.isFollowing()
|
|
199
207
|
? "Following the live Bitcoin tip."
|
|
@@ -201,8 +209,8 @@ export async function syncToTip(dependencies) {
|
|
|
201
209
|
});
|
|
202
210
|
return aggregate;
|
|
203
211
|
}
|
|
204
|
-
await setBitcoinSyncProgress(dependencies, endInfo);
|
|
205
|
-
if (
|
|
212
|
+
await setBitcoinSyncProgress(dependencies, endInfo, dependencies.targetHeightCap ?? null);
|
|
213
|
+
if (endBestHeight >= dependencies.startHeight && finalTip?.height !== endBestHeight) {
|
|
206
214
|
continue;
|
|
207
215
|
}
|
|
208
216
|
await sleep(DEFAULT_SYNC_CATCH_UP_POLL_MS, dependencies.abortSignal);
|
package/dist/bitcoind/errors.js
CHANGED
|
@@ -6,22 +6,22 @@ function appendNextStep(message, nextStep) {
|
|
|
6
6
|
}
|
|
7
7
|
export function formatManagedSyncErrorMessage(message) {
|
|
8
8
|
if (message.startsWith("managed_getblock_archive_manifest_http_")) {
|
|
9
|
-
return appendNextStep(`Getblock
|
|
9
|
+
return appendNextStep(`Getblock range manifest request failed (${message.replace("managed_getblock_archive_manifest_http_", "HTTP ")}).`, "Wait a moment, confirm the snapshot host is reachable, then rerun sync.");
|
|
10
10
|
}
|
|
11
11
|
if (message.startsWith("managed_getblock_archive_http_")) {
|
|
12
|
-
return appendNextStep(`Getblock
|
|
12
|
+
return appendNextStep(`Getblock range request failed (${message.replace("managed_getblock_archive_http_", "HTTP ")}).`, "Wait a moment, confirm the snapshot host is reachable, then rerun sync.");
|
|
13
13
|
}
|
|
14
14
|
if (message === "managed_getblock_archive_response_body_missing") {
|
|
15
|
-
return appendNextStep("Getblock
|
|
15
|
+
return appendNextStep("Getblock range server returned an empty response body.", "Wait a moment, confirm the snapshot host is reachable, then rerun sync.");
|
|
16
16
|
}
|
|
17
17
|
if (message === "managed_getblock_archive_resume_requires_partial_content") {
|
|
18
|
-
return appendNextStep("Getblock
|
|
18
|
+
return appendNextStep("Getblock range server ignored the resume request for a partial download.", "Wait a moment and rerun sync. If this keeps happening, confirm the snapshot host supports HTTP range requests.");
|
|
19
19
|
}
|
|
20
20
|
if (message.startsWith("managed_getblock_archive_chunk_sha256_mismatch_")) {
|
|
21
|
-
return appendNextStep("A downloaded getblock
|
|
21
|
+
return appendNextStep("A downloaded getblock range chunk was corrupted and was rolled back to the last verified checkpoint.", "Wait a moment and rerun sync. If this keeps happening, check local disk health and the stability of the archive download.");
|
|
22
22
|
}
|
|
23
23
|
if (message === "managed_getblock_archive_sha256_mismatch" || message === "managed_getblock_archive_truncated") {
|
|
24
|
-
return appendNextStep("The downloaded getblock
|
|
24
|
+
return appendNextStep("The downloaded getblock range did not match the published manifest.", "Rerun sync so the archive can be downloaded again. If this keeps happening, check local disk health and the snapshot host.");
|
|
25
25
|
}
|
|
26
26
|
if (message === "bitcoind_no_peers_for_header_sync_check_internet_or_firewall") {
|
|
27
27
|
return appendNextStep("No Bitcoin peers were available for header sync.", "Check your internet access and firewall rules for outbound Bitcoin connections, then rerun sync.");
|
|
@@ -2,9 +2,9 @@ import { FIELD_LEFT, FIELD_WIDTH, PREPARING_SYNC_LINE, PROGRESS_TICK_MS, SCROLL_
|
|
|
2
2
|
export function createDefaultMessage(phase) {
|
|
3
3
|
switch (phase) {
|
|
4
4
|
case "getblock_archive_download":
|
|
5
|
-
return "Downloading getblock
|
|
5
|
+
return "Downloading getblock range.";
|
|
6
6
|
case "getblock_archive_import":
|
|
7
|
-
return "Bitcoin Core is importing getblock
|
|
7
|
+
return "Bitcoin Core is importing getblock range blocks.";
|
|
8
8
|
case "snapshot_download":
|
|
9
9
|
return "Downloading UTXO snapshot.";
|
|
10
10
|
case "wait_headers_for_snapshot":
|
|
@@ -126,9 +126,9 @@ function animateStatusEllipsis(now) {
|
|
|
126
126
|
export function resolveStatusFieldText(progress, snapshotHeight, now = 0) {
|
|
127
127
|
switch (progress.phase) {
|
|
128
128
|
case "getblock_archive_download":
|
|
129
|
-
return `Downloading getblock
|
|
129
|
+
return `Downloading getblock range${animateStatusEllipsis(now)}`;
|
|
130
130
|
case "getblock_archive_import":
|
|
131
|
-
return `Importing getblock
|
|
131
|
+
return `Importing getblock range${animateStatusEllipsis(now)}`;
|
|
132
132
|
case "paused":
|
|
133
133
|
case "snapshot_download":
|
|
134
134
|
return `Downloading snapshot to ${snapshotHeight}${animateStatusEllipsis(now)}`;
|
|
@@ -4,7 +4,7 @@ export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopInde
|
|
|
4
4
|
export { normalizeRpcBlock } from "./normalize.js";
|
|
5
5
|
export { BitcoinRpcClient } from "./rpc.js";
|
|
6
6
|
export { attachOrStartManagedBitcoindService, buildManagedServiceArgsForTesting, readManagedBitcoindServiceStatusForTesting, resolveManagedBitcoindDbcacheMiB, stopManagedBitcoindService, shutdownManagedBitcoindServiceForTesting, writeBitcoinConfForTesting, } from "./service.js";
|
|
7
|
-
export { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, createBootstrapStateForTesting, downloadSnapshotFileForTesting, loadBootstrapStateForTesting, prepareLatestGetblockArchiveForTesting, resolveBootstrapPathsForTesting, resolveGetblockArchivePathsForTesting, resolveReadyGetblockArchiveForTesting, saveBootstrapStateForTesting, validateSnapshotFileForTesting, waitForGetblockArchiveImportForTesting, waitForHeadersForTesting, } from "./bootstrap.js";
|
|
7
|
+
export { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, createBootstrapStateForTesting, deleteGetblockArchiveRangeForTesting, downloadSnapshotFileForTesting, loadBootstrapStateForTesting, prepareGetblockArchiveRangeForTesting, prepareLatestGetblockArchiveForTesting, resolveBootstrapPathsForTesting, resolveGetblockArchivePathsForTesting, resolveReadyGetblockArchiveForTesting, saveBootstrapStateForTesting, validateSnapshotFileForTesting, waitForGetblockArchiveImportForTesting, waitForHeadersForTesting, } from "./bootstrap.js";
|
|
8
8
|
export { buildBitcoindArgsForTesting, createRpcClient, launchManagedBitcoindNode, resolveDefaultBitcoindDataDirForTesting, validateNodeConfigForTesting, } from "./node.js";
|
|
9
9
|
export { ManagedProgressController, TtyProgressRenderer, advanceFollowSceneStateForTesting, createFollowSceneStateForTesting, createBootstrapProgressForTesting, formatCompactFollowAgeLabelForTesting, loadBannerArtForTesting, loadScrollArtForTesting, loadTrainCarArtForTesting, loadTrainArtForTesting, loadTrainSmokeArtForTesting, formatProgressLineForTesting, formatQuoteLineForTesting, renderArtFrameForTesting, renderCompletionFrameForTesting, renderFollowFrameForTesting, renderIntroFrameForTesting, resolveCompletionMessageForTesting, resolveIntroMessageForTesting, resolveStatusFieldTextForTesting, setFollowBlockTimeForTesting, setFollowBlockTimesForTesting, syncFollowSceneStateForTesting, } from "./progress.js";
|
|
10
10
|
export { WritingQuoteRotator, loadWritingQuotesForTesting, shuffleIndicesForTesting, } from "./quotes.js";
|
package/dist/bitcoind/testing.js
CHANGED
|
@@ -4,7 +4,7 @@ export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopInde
|
|
|
4
4
|
export { normalizeRpcBlock } from "./normalize.js";
|
|
5
5
|
export { BitcoinRpcClient } from "./rpc.js";
|
|
6
6
|
export { attachOrStartManagedBitcoindService, buildManagedServiceArgsForTesting, readManagedBitcoindServiceStatusForTesting, resolveManagedBitcoindDbcacheMiB, stopManagedBitcoindService, shutdownManagedBitcoindServiceForTesting, writeBitcoinConfForTesting, } from "./service.js";
|
|
7
|
-
export { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, createBootstrapStateForTesting, downloadSnapshotFileForTesting, loadBootstrapStateForTesting, prepareLatestGetblockArchiveForTesting, resolveBootstrapPathsForTesting, resolveGetblockArchivePathsForTesting, resolveReadyGetblockArchiveForTesting, saveBootstrapStateForTesting, validateSnapshotFileForTesting, waitForGetblockArchiveImportForTesting, waitForHeadersForTesting, } from "./bootstrap.js";
|
|
7
|
+
export { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, createBootstrapStateForTesting, deleteGetblockArchiveRangeForTesting, downloadSnapshotFileForTesting, loadBootstrapStateForTesting, prepareGetblockArchiveRangeForTesting, prepareLatestGetblockArchiveForTesting, resolveBootstrapPathsForTesting, resolveGetblockArchivePathsForTesting, resolveReadyGetblockArchiveForTesting, saveBootstrapStateForTesting, validateSnapshotFileForTesting, waitForGetblockArchiveImportForTesting, waitForHeadersForTesting, } from "./bootstrap.js";
|
|
8
8
|
export { buildBitcoindArgsForTesting, createRpcClient, launchManagedBitcoindNode, resolveDefaultBitcoindDataDirForTesting, validateNodeConfigForTesting, } from "./node.js";
|
|
9
9
|
export { ManagedProgressController, TtyProgressRenderer, advanceFollowSceneStateForTesting, createFollowSceneStateForTesting, createBootstrapProgressForTesting, formatCompactFollowAgeLabelForTesting, loadBannerArtForTesting, loadScrollArtForTesting, loadTrainCarArtForTesting, loadTrainArtForTesting, loadTrainSmokeArtForTesting, formatProgressLineForTesting, formatQuoteLineForTesting, renderArtFrameForTesting, renderCompletionFrameForTesting, renderFollowFrameForTesting, renderIntroFrameForTesting, resolveCompletionMessageForTesting, resolveIntroMessageForTesting, resolveStatusFieldTextForTesting, setFollowBlockTimeForTesting, setFollowBlockTimesForTesting, syncFollowSceneStateForTesting, } from "./progress.js";
|
|
10
10
|
export { WritingQuoteRotator, loadWritingQuotesForTesting, shuffleIndicesForTesting, } from "./quotes.js";
|
package/dist/bitcoind/types.d.ts
CHANGED
|
@@ -17,28 +17,27 @@ export interface SnapshotChunkManifest {
|
|
|
17
17
|
snapshotSha256: string;
|
|
18
18
|
chunkSha256s: string[];
|
|
19
19
|
}
|
|
20
|
-
export interface
|
|
21
|
-
height: number;
|
|
22
|
-
blockHash: string;
|
|
23
|
-
previousBlockHash: string;
|
|
24
|
-
recordOffset: number;
|
|
25
|
-
recordLength: number;
|
|
26
|
-
rawBlockSizeBytes: number;
|
|
27
|
-
}
|
|
28
|
-
export interface GetblockArchiveManifest {
|
|
20
|
+
export interface GetblockRangeManifestEntry {
|
|
29
21
|
formatVersion: number;
|
|
30
22
|
chain: "main";
|
|
31
23
|
baseSnapshotHeight: number;
|
|
32
24
|
firstBlockHeight: number;
|
|
33
|
-
|
|
34
|
-
blockCount: number;
|
|
25
|
+
lastBlockHeight: number;
|
|
35
26
|
artifactFilename: string;
|
|
36
27
|
artifactSizeBytes: number;
|
|
37
28
|
artifactSha256: string;
|
|
38
29
|
chunkSizeBytes: number;
|
|
39
30
|
chunkSha256s: string[];
|
|
40
|
-
blocks: GetblockArchiveManifestBlockRecord[];
|
|
41
31
|
}
|
|
32
|
+
export interface GetblockRangeManifest {
|
|
33
|
+
formatVersion: number;
|
|
34
|
+
chain: "main";
|
|
35
|
+
baseSnapshotHeight: number;
|
|
36
|
+
rangeSizeBlocks: number;
|
|
37
|
+
publishedThroughHeight: number;
|
|
38
|
+
ranges: GetblockRangeManifestEntry[];
|
|
39
|
+
}
|
|
40
|
+
export type GetblockArchiveManifest = GetblockRangeManifestEntry;
|
|
42
41
|
export interface WritingQuote {
|
|
43
42
|
quote: string;
|
|
44
43
|
author: string;
|
|
@@ -3,7 +3,6 @@ import { resolveWalletRootIdFromLocalArtifacts } from "../../wallet/root-resolut
|
|
|
3
3
|
import { usesTtyProgress, writeLine } from "../io.js";
|
|
4
4
|
import { classifyCliError } from "../output.js";
|
|
5
5
|
import { createStopSignalWatcher } from "../signals.js";
|
|
6
|
-
import { confirmGetblockArchiveRestart } from "./getblock-archive-restart.js";
|
|
7
6
|
export async function runFollowCommand(parsed, context) {
|
|
8
7
|
const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
|
|
9
8
|
const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
|
|
@@ -24,7 +23,6 @@ export async function runFollowCommand(parsed, context) {
|
|
|
24
23
|
dataDir,
|
|
25
24
|
walletRootId: walletRoot.walletRootId,
|
|
26
25
|
progressOutput: parsed.progressOutput,
|
|
27
|
-
confirmGetblockArchiveRestart: async (options) => confirmGetblockArchiveRestart(parsed, context, options),
|
|
28
26
|
});
|
|
29
27
|
storeOwned = false;
|
|
30
28
|
const stopWatcher = createStopSignalWatcher(context.signalSource, context.stderr, client, context.forceExit);
|
|
@@ -4,7 +4,6 @@ import { resolveWalletRootIdFromLocalArtifacts } from "../../wallet/root-resolut
|
|
|
4
4
|
import { writeLine } from "../io.js";
|
|
5
5
|
import { classifyCliError } from "../output.js";
|
|
6
6
|
import { createStopSignalWatcher, waitForCompletionOrStop } from "../signals.js";
|
|
7
|
-
import { confirmGetblockArchiveRestart } from "./getblock-archive-restart.js";
|
|
8
7
|
export async function runSyncCommand(parsed, context) {
|
|
9
8
|
const dbPath = parsed.dbPath ?? context.resolveDefaultClientDatabasePath();
|
|
10
9
|
const dataDir = parsed.dataDir ?? context.resolveDefaultBitcoindDataDir();
|
|
@@ -25,7 +24,6 @@ export async function runSyncCommand(parsed, context) {
|
|
|
25
24
|
dataDir,
|
|
26
25
|
walletRootId: walletRoot.walletRootId,
|
|
27
26
|
progressOutput: parsed.progressOutput,
|
|
28
|
-
confirmGetblockArchiveRestart: async (options) => confirmGetblockArchiveRestart(parsed, context, options),
|
|
29
27
|
});
|
|
30
28
|
storeOwned = false;
|
|
31
29
|
const stopWatcher = createStopSignalWatcher(context.signalSource, context.stderr, client, context.forceExit);
|
package/dist/cli/signals.js
CHANGED
|
@@ -19,10 +19,18 @@ export function createStopSignalWatcher(signalSource, stderr, client, forceExit)
|
|
|
19
19
|
};
|
|
20
20
|
const onFirstSignal = () => {
|
|
21
21
|
closing = true;
|
|
22
|
-
writeLine(stderr, "Detaching from managed Cogcoin client...");
|
|
22
|
+
writeLine(stderr, "Detaching from managed Cogcoin client and resuming background indexer follow...");
|
|
23
23
|
void client.close().then(() => {
|
|
24
|
+
if (resolved) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
writeLine(stderr, "Detached cleanly; background indexer follow resumed.");
|
|
24
28
|
settle(0);
|
|
25
29
|
}, () => {
|
|
30
|
+
if (resolved) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
writeLine(stderr, "Detach failed before background indexer follow was confirmed.");
|
|
26
34
|
settle(1);
|
|
27
35
|
});
|
|
28
36
|
};
|
|
@@ -103,5 +111,11 @@ export async function waitForCompletionOrStop(promise, stopWatcher) {
|
|
|
103
111
|
}
|
|
104
112
|
throw outcome.error;
|
|
105
113
|
}
|
|
114
|
+
if (stopWatcher.isStopping()) {
|
|
115
|
+
return {
|
|
116
|
+
kind: "stopped",
|
|
117
|
+
code: await stopWatcher.promise,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
106
120
|
return outcome;
|
|
107
121
|
}
|
package/dist/wallet/tx/anchor.js
CHANGED
|
@@ -227,6 +227,9 @@ function resolveAnchorOutpointForSender(state, senderIndex) {
|
|
|
227
227
|
vout: anchoredDomain.currentCanonicalAnchorOutpoint.vout,
|
|
228
228
|
};
|
|
229
229
|
}
|
|
230
|
+
function isFundingSender(state, sender) {
|
|
231
|
+
return sender.scriptPubKeyHex === state.funding.scriptPubKeyHex;
|
|
232
|
+
}
|
|
230
233
|
async function confirmAnchor(prompter, operation) {
|
|
231
234
|
prompter.writeLine(`You are anchoring "${operation.chainDomain.name}" onto dedicated index ${operation.targetIdentity.localIndex}.`);
|
|
232
235
|
prompter.writeLine("Anchoring is permanent chain state. This flow uses two transactions and is not rolled back automatically.");
|
|
@@ -274,10 +277,15 @@ function resolveAnchorOperation(context, domainName, foundingMessageText, foundi
|
|
|
274
277
|
if (senderIdentity.readOnly) {
|
|
275
278
|
throw new Error("wallet_anchor_owner_read_only");
|
|
276
279
|
}
|
|
277
|
-
const sourceAnchorOutpoint =
|
|
280
|
+
const sourceAnchorOutpoint = isFundingSender(context.localState.state, {
|
|
281
|
+
localIndex: senderIdentity.index,
|
|
282
|
+
scriptPubKeyHex: senderIdentity.scriptPubKeyHex,
|
|
283
|
+
address: senderIdentity.address,
|
|
284
|
+
})
|
|
278
285
|
? null
|
|
279
286
|
: resolveAnchorOutpointForSender(context.localState.state, senderIdentity.index);
|
|
280
|
-
if (
|
|
287
|
+
if (sourceAnchorOutpoint === null
|
|
288
|
+
&& senderIdentity.scriptPubKeyHex !== context.localState.state.funding.scriptPubKeyHex) {
|
|
281
289
|
throw new Error("wallet_anchor_owner_identity_not_supported");
|
|
282
290
|
}
|
|
283
291
|
const targetIdentity = selectNextDedicatedIdentityTarget(context.localState.state);
|
|
@@ -466,6 +474,8 @@ function buildTx1Plan(options) {
|
|
|
466
474
|
expectedReplacementAnchorScriptHex: null,
|
|
467
475
|
expectedReplacementAnchorValueSats: null,
|
|
468
476
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
477
|
+
requiredSenderOutpoint: null,
|
|
478
|
+
requiredProvisionalOutpoint: null,
|
|
469
479
|
errorPrefix: "wallet_anchor_tx1",
|
|
470
480
|
};
|
|
471
481
|
}
|
|
@@ -494,6 +504,8 @@ function buildTx1Plan(options) {
|
|
|
494
504
|
expectedReplacementAnchorScriptHex: options.operation.sourceSender.scriptPubKeyHex,
|
|
495
505
|
expectedReplacementAnchorValueSats: BigInt(options.state.anchorValueSats),
|
|
496
506
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
507
|
+
requiredSenderOutpoint: options.operation.sourceAnchorOutpoint,
|
|
508
|
+
requiredProvisionalOutpoint: null,
|
|
497
509
|
errorPrefix: "wallet_anchor_tx1",
|
|
498
510
|
};
|
|
499
511
|
}
|
|
@@ -538,19 +550,53 @@ function buildTx2Plan(options) {
|
|
|
538
550
|
expectedReplacementAnchorScriptHex: null,
|
|
539
551
|
expectedReplacementAnchorValueSats: null,
|
|
540
552
|
allowedFundingScriptPubKeyHex: options.state.funding.scriptPubKeyHex,
|
|
553
|
+
requiredSenderOutpoint: null,
|
|
554
|
+
requiredProvisionalOutpoint: {
|
|
555
|
+
txid: provisional.txid,
|
|
556
|
+
vout: provisional.vout,
|
|
557
|
+
},
|
|
541
558
|
errorPrefix: "wallet_anchor_tx2",
|
|
542
559
|
};
|
|
543
560
|
}
|
|
561
|
+
function getDecodedInputScriptPubKeyHex(input) {
|
|
562
|
+
return input.prevout?.scriptPubKey?.hex ?? null;
|
|
563
|
+
}
|
|
564
|
+
function getDecodedInputVout(input) {
|
|
565
|
+
const vout = input.vout;
|
|
566
|
+
return typeof vout === "number" ? vout : null;
|
|
567
|
+
}
|
|
568
|
+
function inputMatchesOutpoint(input, outpoint) {
|
|
569
|
+
return input.txid === outpoint.txid && getDecodedInputVout(input) === outpoint.vout;
|
|
570
|
+
}
|
|
571
|
+
function assertNoUnexpectedAnchorInputs(inputs, allowedScripts, unexpectedInputErrorCode) {
|
|
572
|
+
for (const input of inputs) {
|
|
573
|
+
const scriptPubKeyHex = getDecodedInputScriptPubKeyHex(input);
|
|
574
|
+
if (scriptPubKeyHex === null || !allowedScripts.has(scriptPubKeyHex)) {
|
|
575
|
+
throw new Error(unexpectedInputErrorCode);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
544
579
|
function validateTx1Draft(decoded, funded, plan) {
|
|
545
580
|
const inputs = decoded.tx.vin;
|
|
546
581
|
const outputs = decoded.tx.vout;
|
|
547
|
-
if (inputs.length === 0
|
|
582
|
+
if (inputs.length === 0) {
|
|
583
|
+
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
584
|
+
}
|
|
585
|
+
const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(inputs[0]);
|
|
586
|
+
if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex) {
|
|
548
587
|
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
549
588
|
}
|
|
550
|
-
|
|
551
|
-
if (inputs[
|
|
589
|
+
if (plan.requiredSenderOutpoint !== null) {
|
|
590
|
+
if (!inputMatchesOutpoint(inputs[0], plan.requiredSenderOutpoint)) {
|
|
591
|
+
throw new Error(`${plan.errorPrefix}_sender_input_mismatch`);
|
|
592
|
+
}
|
|
593
|
+
if (inputs.length < 2 || getDecodedInputScriptPubKeyHex(inputs[1]) !== plan.allowedFundingScriptPubKeyHex) {
|
|
552
594
|
throw new Error(`${plan.errorPrefix}_unexpected_funding_input`);
|
|
553
595
|
}
|
|
596
|
+
assertNoUnexpectedAnchorInputs(inputs.slice(2), new Set([plan.allowedFundingScriptPubKeyHex]), `${plan.errorPrefix}_unexpected_funding_input`);
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
assertNoUnexpectedAnchorInputs(inputs, new Set([plan.allowedFundingScriptPubKeyHex]), `${plan.errorPrefix}_unexpected_funding_input`);
|
|
554
600
|
}
|
|
555
601
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
556
602
|
throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
|
|
@@ -586,14 +632,18 @@ function validateTx1Draft(decoded, funded, plan) {
|
|
|
586
632
|
function validateTx2Draft(decoded, funded, plan) {
|
|
587
633
|
const inputs = decoded.tx.vin;
|
|
588
634
|
const outputs = decoded.tx.vout;
|
|
589
|
-
if (inputs.length === 0 ||
|
|
635
|
+
if (inputs.length === 0 || plan.requiredProvisionalOutpoint === null) {
|
|
590
636
|
throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
|
|
591
637
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
638
|
+
const firstInputScriptPubKeyHex = getDecodedInputScriptPubKeyHex(inputs[0]);
|
|
639
|
+
if (firstInputScriptPubKeyHex !== plan.sender.scriptPubKeyHex
|
|
640
|
+
|| !inputMatchesOutpoint(inputs[0], plan.requiredProvisionalOutpoint)) {
|
|
641
|
+
throw new Error(`${plan.errorPrefix}_provisional_input_mismatch`);
|
|
642
|
+
}
|
|
643
|
+
if (inputs.length < 2 || getDecodedInputScriptPubKeyHex(inputs[1]) !== plan.allowedFundingScriptPubKeyHex) {
|
|
644
|
+
throw new Error(`${plan.errorPrefix}_unexpected_funding_input`);
|
|
596
645
|
}
|
|
646
|
+
assertNoUnexpectedAnchorInputs(inputs.slice(2), new Set([plan.allowedFundingScriptPubKeyHex]), `${plan.errorPrefix}_unexpected_funding_input`);
|
|
597
647
|
if (outputs[0]?.scriptPubKey?.hex !== plan.expectedOpReturnScriptHex) {
|
|
598
648
|
throw new Error(`${plan.errorPrefix}_opreturn_mismatch`);
|
|
599
649
|
}
|
|
@@ -625,6 +675,9 @@ async function buildTx1(options) {
|
|
|
625
675
|
validateFundedDraft: validateTx1Draft,
|
|
626
676
|
finalizeErrorCode: "wallet_anchor_tx1_finalize_failed",
|
|
627
677
|
mempoolRejectPrefix: "wallet_anchor_tx1_mempool_rejected",
|
|
678
|
+
builderOptions: {
|
|
679
|
+
addInputs: false,
|
|
680
|
+
},
|
|
628
681
|
});
|
|
629
682
|
}
|
|
630
683
|
async function buildTx2(options) {
|
|
@@ -636,6 +689,7 @@ async function buildTx2(options) {
|
|
|
636
689
|
finalizeErrorCode: "wallet_anchor_tx2_finalize_failed",
|
|
637
690
|
mempoolRejectPrefix: "wallet_anchor_tx2_mempool_rejected",
|
|
638
691
|
builderOptions: {
|
|
692
|
+
addInputs: false,
|
|
639
693
|
includeUnsafe: true,
|
|
640
694
|
minConf: 0,
|
|
641
695
|
},
|
package/package.json
CHANGED