@cogcoin/client 0.5.5 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +11 -2
  2. package/dist/bitcoind/bootstrap/chainstate.d.ts +2 -1
  3. package/dist/bitcoind/bootstrap/chainstate.js +4 -1
  4. package/dist/bitcoind/bootstrap/controller.d.ts +4 -1
  5. package/dist/bitcoind/bootstrap/controller.js +42 -5
  6. package/dist/bitcoind/bootstrap/getblock-archive.d.ts +39 -0
  7. package/dist/bitcoind/bootstrap/getblock-archive.js +548 -0
  8. package/dist/bitcoind/bootstrap/headers.d.ts +12 -0
  9. package/dist/bitcoind/bootstrap/headers.js +95 -10
  10. package/dist/bitcoind/bootstrap.d.ts +1 -0
  11. package/dist/bitcoind/bootstrap.js +1 -0
  12. package/dist/bitcoind/client/factory.js +91 -28
  13. package/dist/bitcoind/client/managed-client.d.ts +1 -1
  14. package/dist/bitcoind/client/managed-client.js +4 -3
  15. package/dist/bitcoind/client/sync-engine.js +55 -13
  16. package/dist/bitcoind/errors.js +18 -0
  17. package/dist/bitcoind/indexer-daemon-main.js +78 -0
  18. package/dist/bitcoind/indexer-daemon.d.ts +10 -1
  19. package/dist/bitcoind/indexer-daemon.js +44 -28
  20. package/dist/bitcoind/node.js +2 -0
  21. package/dist/bitcoind/processing-start-height.d.ts +7 -0
  22. package/dist/bitcoind/processing-start-height.js +9 -0
  23. package/dist/bitcoind/progress/constants.d.ts +1 -0
  24. package/dist/bitcoind/progress/constants.js +1 -0
  25. package/dist/bitcoind/progress/controller.d.ts +22 -0
  26. package/dist/bitcoind/progress/controller.js +49 -23
  27. package/dist/bitcoind/progress/formatting.js +29 -1
  28. package/dist/bitcoind/progress/render-policy.d.ts +35 -0
  29. package/dist/bitcoind/progress/render-policy.js +81 -0
  30. package/dist/bitcoind/retryable-rpc.d.ts +11 -0
  31. package/dist/bitcoind/retryable-rpc.js +30 -0
  32. package/dist/bitcoind/service-paths.js +2 -6
  33. package/dist/bitcoind/service.d.ts +21 -2
  34. package/dist/bitcoind/service.js +274 -122
  35. package/dist/bitcoind/testing.d.ts +2 -2
  36. package/dist/bitcoind/testing.js +2 -2
  37. package/dist/bitcoind/types.d.ts +36 -1
  38. package/dist/cli/commands/follow.js +11 -0
  39. package/dist/cli/commands/getblock-archive-restart.d.ts +5 -0
  40. package/dist/cli/commands/getblock-archive-restart.js +15 -0
  41. package/dist/cli/commands/mining-admin.js +4 -0
  42. package/dist/cli/commands/mining-read.js +8 -5
  43. package/dist/cli/commands/mining-runtime.js +4 -0
  44. package/dist/cli/commands/service-runtime.js +150 -134
  45. package/dist/cli/commands/status.js +2 -0
  46. package/dist/cli/commands/sync.js +11 -0
  47. package/dist/cli/commands/wallet-admin.js +106 -24
  48. package/dist/cli/commands/wallet-mutation.js +57 -4
  49. package/dist/cli/commands/wallet-read.js +2 -0
  50. package/dist/cli/context.js +8 -4
  51. package/dist/cli/mutation-command-groups.d.ts +2 -1
  52. package/dist/cli/mutation-command-groups.js +5 -0
  53. package/dist/cli/mutation-json.d.ts +18 -2
  54. package/dist/cli/mutation-json.js +49 -0
  55. package/dist/cli/mutation-success.d.ts +1 -0
  56. package/dist/cli/mutation-success.js +2 -2
  57. package/dist/cli/output.js +86 -1
  58. package/dist/cli/parse.d.ts +1 -1
  59. package/dist/cli/parse.js +133 -3
  60. package/dist/cli/preview-json.d.ts +10 -1
  61. package/dist/cli/preview-json.js +32 -0
  62. package/dist/cli/prompt.js +1 -1
  63. package/dist/cli/runner.js +4 -0
  64. package/dist/cli/types.d.ts +15 -5
  65. package/dist/cli/types.js +1 -1
  66. package/dist/cli/wallet-format.js +140 -14
  67. package/dist/wallet/lifecycle.d.ts +21 -1
  68. package/dist/wallet/lifecycle.js +252 -116
  69. package/dist/wallet/mining/visualizer.d.ts +11 -6
  70. package/dist/wallet/mining/visualizer.js +32 -15
  71. package/dist/wallet/read/context.js +10 -4
  72. package/dist/wallet/reset.d.ts +61 -2
  73. package/dist/wallet/reset.js +246 -89
  74. package/dist/wallet/root-resolution.d.ts +20 -0
  75. package/dist/wallet/root-resolution.js +37 -0
  76. package/dist/wallet/runtime.d.ts +13 -1
  77. package/dist/wallet/runtime.js +54 -11
  78. package/dist/wallet/state/crypto.d.ts +3 -0
  79. package/dist/wallet/state/crypto.js +3 -0
  80. package/dist/wallet/state/provider.d.ts +1 -0
  81. package/dist/wallet/state/provider.js +119 -3
  82. package/dist/wallet/state/seed-index.d.ts +43 -0
  83. package/dist/wallet/state/seed-index.js +151 -0
  84. package/dist/wallet/state/storage.d.ts +7 -1
  85. package/dist/wallet/state/storage.js +39 -0
  86. package/dist/wallet/tx/anchor.d.ts +22 -0
  87. package/dist/wallet/tx/anchor.js +215 -8
  88. package/dist/wallet/tx/index.d.ts +1 -1
  89. package/dist/wallet/tx/index.js +1 -1
  90. package/dist/wallet/types.d.ts +1 -0
  91. package/package.json +1 -1
@@ -2,12 +2,14 @@ import { randomBytes } from "node:crypto";
2
2
  import { execFile, spawn } from "node:child_process";
3
3
  import { access, constants, mkdir, readFile, readdir, rm } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
5
+ import { totalmem } from "node:os";
5
6
  import { promisify } from "node:util";
6
7
  import net from "node:net";
7
8
  import { getBitcoindPath } from "@cogcoin/bitcoin";
8
9
  import { acquireFileLock, FileLockBusyError } from "../wallet/fs/lock.js";
9
10
  import { writeFileAtomic } from "../wallet/fs/atomic.js";
10
11
  import { writeRuntimeStatusFile } from "../wallet/fs/status-file.js";
12
+ import { stopIndexerDaemonServiceWithLockHeld } from "./indexer-daemon.js";
11
13
  import { createRpcClient, validateNodeConfigForTesting } from "./node.js";
12
14
  import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
13
15
  import { MANAGED_BITCOIND_SERVICE_API_VERSION as MANAGED_BITCOIND_SERVICE_API_VERSION_VALUE } from "./types.js";
@@ -16,6 +18,32 @@ const LOCAL_HOST = "127.0.0.1";
16
18
  const SUPPORTED_BITCOIND_VERSION = "30.2.0";
17
19
  const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
18
20
  const DEFAULT_SHUTDOWN_TIMEOUT_MS = 15_000;
21
+ const DEFAULT_DBCACHE_MIB = 450;
22
+ const claimedUninitializedRuntimeKeys = new Set();
23
+ const GIB = 1024 ** 3;
24
+ export function resolveManagedBitcoindDbcacheMiB(totalRamBytes) {
25
+ if (!Number.isFinite(totalRamBytes) || totalRamBytes <= 0) {
26
+ return DEFAULT_DBCACHE_MIB;
27
+ }
28
+ if (totalRamBytes < 8 * GIB) {
29
+ return 450;
30
+ }
31
+ if (totalRamBytes < 16 * GIB) {
32
+ return 768;
33
+ }
34
+ if (totalRamBytes < 32 * GIB) {
35
+ return 1024;
36
+ }
37
+ return 2048;
38
+ }
39
+ function detectManagedBitcoindDbcacheMiB() {
40
+ try {
41
+ return resolveManagedBitcoindDbcacheMiB(totalmem());
42
+ }
43
+ catch {
44
+ return DEFAULT_DBCACHE_MIB;
45
+ }
46
+ }
19
47
  function sleep(ms) {
20
48
  return new Promise((resolve) => {
21
49
  setTimeout(resolve, ms);
@@ -31,6 +59,20 @@ async function waitForProcessExit(pid, timeoutMs, errorCode) {
31
59
  }
32
60
  throw new Error(errorCode);
33
61
  }
62
+ async function acquireFileLockWithRetry(lockPath, metadata, timeoutMs) {
63
+ const deadline = Date.now() + timeoutMs;
64
+ while (true) {
65
+ try {
66
+ return await acquireFileLock(lockPath, metadata);
67
+ }
68
+ catch (error) {
69
+ if (!(error instanceof FileLockBusyError) || Date.now() >= deadline) {
70
+ throw error;
71
+ }
72
+ await sleep(250);
73
+ }
74
+ }
75
+ }
34
76
  function getWalletReplicaName(walletRootId) {
35
77
  return `cogcoin-${walletRootId}`.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 63);
36
78
  }
@@ -184,15 +226,14 @@ async function waitForRpcReady(rpc, cookieFile, expectedChain, timeoutMs) {
184
226
  throw lastError instanceof Error ? lastError : new Error("bitcoind_rpc_timeout");
185
227
  }
186
228
  function validateManagedBitcoindStatus(status, options, runtimeRoot) {
229
+ const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
230
+ const legacyRuntimeRoot = join(resolveManagedServicePaths(options.dataDir ?? "", walletRootId).runtimeRoot, walletRootId);
187
231
  if (status.serviceApiVersion !== MANAGED_BITCOIND_SERVICE_API_VERSION_VALUE) {
188
232
  throw new Error("managed_bitcoind_service_version_mismatch");
189
233
  }
190
- if (status.walletRootId !== (options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID)) {
191
- throw new Error("managed_bitcoind_wallet_root_mismatch");
192
- }
193
234
  if (status.chain !== options.chain
194
235
  || status.dataDir !== (options.dataDir ?? "")
195
- || status.runtimeRoot !== runtimeRoot) {
236
+ || (status.runtimeRoot !== runtimeRoot && status.runtimeRoot !== legacyRuntimeRoot)) {
196
237
  throw new Error("managed_bitcoind_runtime_mismatch");
197
238
  }
198
239
  }
@@ -233,6 +274,8 @@ function createBitcoindServiceStatus(options) {
233
274
  rpc: options.rpc,
234
275
  zmq: options.zmq,
235
276
  p2pPort: options.p2pPort,
277
+ getblockArchiveEndHeight: options.getblockArchiveEndHeight,
278
+ getblockArchiveSha256: options.getblockArchiveSha256,
236
279
  walletReplica: options.walletReplica,
237
280
  startedAtUnixMs: options.startedAtUnixMs,
238
281
  heartbeatAtUnixMs: options.heartbeatAtUnixMs,
@@ -245,9 +288,7 @@ function mapManagedBitcoindValidationError(error) {
245
288
  compatibility: error instanceof Error
246
289
  ? error.message === "managed_bitcoind_service_version_mismatch"
247
290
  ? "service-version-mismatch"
248
- : error.message === "managed_bitcoind_wallet_root_mismatch"
249
- ? "wallet-root-mismatch"
250
- : "runtime-mismatch"
291
+ : "runtime-mismatch"
251
292
  : "protocol-error",
252
293
  status: null,
253
294
  error: error instanceof Error ? error.message : "managed_bitcoind_protocol_error",
@@ -323,6 +364,9 @@ async function resolveRuntimeConfig(statusPath, configPath, options) {
323
364
  },
324
365
  zmqPort,
325
366
  p2pPort,
367
+ dbcacheMiB: detectManagedBitcoindDbcacheMiB(),
368
+ getblockArchiveEndHeight: options.getblockArchiveEndHeight ?? null,
369
+ getblockArchiveSha256: options.getblockArchiveSha256 ?? null,
326
370
  };
327
371
  }
328
372
  async function writeBitcoinConf(filePath, options, runtimeConfig) {
@@ -334,6 +378,7 @@ async function writeBitcoinConf(filePath, options, runtimeConfig) {
334
378
  "prune=0",
335
379
  "dnsseed=1",
336
380
  "listen=0",
381
+ `dbcache=${runtimeConfig.dbcacheMiB}`,
337
382
  `rpcbind=${LOCAL_HOST}`,
338
383
  `rpcallowip=${LOCAL_HOST}`,
339
384
  `rpcport=${runtimeConfig.rpc.port}`,
@@ -358,12 +403,22 @@ function buildManagedServiceArgs(options, runtimeConfig) {
358
403
  "-prune=0",
359
404
  "-dnsseed=1",
360
405
  "-listen=0",
406
+ `-dbcache=${runtimeConfig.dbcacheMiB}`,
361
407
  ];
362
408
  if (options.chain === "regtest") {
363
409
  args.push("-chain=regtest");
364
410
  }
411
+ if (options.getblockArchivePath !== undefined && options.getblockArchivePath !== null) {
412
+ args.push(`-loadblock=${options.getblockArchivePath}`);
413
+ }
365
414
  return args;
366
415
  }
416
+ export async function writeBitcoinConfForTesting(filePath, options, runtimeConfig) {
417
+ await writeBitcoinConf(filePath, options, runtimeConfig);
418
+ }
419
+ export function buildManagedServiceArgsForTesting(options, runtimeConfig) {
420
+ return buildManagedServiceArgs(options, runtimeConfig);
421
+ }
367
422
  function isMissingWalletError(message) {
368
423
  return message.includes("bitcoind_rpc_loadwallet_-18_")
369
424
  || message.includes("Path does not exist")
@@ -496,6 +551,8 @@ async function writeBitcoindStatus(paths, status) {
496
551
  rpc: status.rpc,
497
552
  zmqPort: status.zmq.port,
498
553
  p2pPort: status.p2pPort,
554
+ getblockArchiveEndHeight: status.getblockArchiveEndHeight,
555
+ getblockArchiveSha256: status.getblockArchiveSha256,
499
556
  });
500
557
  }
501
558
  async function clearManagedBitcoindRuntimeArtifacts(paths) {
@@ -504,15 +561,102 @@ async function clearManagedBitcoindRuntimeArtifacts(paths) {
504
561
  await rm(paths.bitcoindReadyPath, { force: true }).catch(() => undefined);
505
562
  await rm(paths.bitcoindWalletStatusPath, { force: true }).catch(() => undefined);
506
563
  }
564
+ export async function stopManagedBitcoindServiceWithLockHeld(options) {
565
+ const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
566
+ const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, walletRootId);
567
+ const status = await readJsonFile(paths.bitcoindStatusPath);
568
+ const processId = status?.processId ?? null;
569
+ if (status === null || processId === null || !await isProcessAlive(processId)) {
570
+ await clearManagedBitcoindRuntimeArtifacts(paths);
571
+ return {
572
+ status: "not-running",
573
+ walletRootId,
574
+ };
575
+ }
576
+ const rpc = createRpcClient(status.rpc);
577
+ try {
578
+ await rpc.stop();
579
+ }
580
+ catch {
581
+ try {
582
+ process.kill(processId, "SIGTERM");
583
+ }
584
+ catch (error) {
585
+ if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
586
+ throw error;
587
+ }
588
+ }
589
+ }
590
+ await waitForProcessExit(processId, options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS, "managed_bitcoind_service_stop_timeout");
591
+ await clearManagedBitcoindRuntimeArtifacts(paths);
592
+ return {
593
+ status: "stopped",
594
+ walletRootId,
595
+ };
596
+ }
597
+ export async function withClaimedUninitializedManagedRuntime(options, callback) {
598
+ const targetWalletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
599
+ const targetPaths = resolveManagedServicePaths(options.dataDir, targetWalletRootId);
600
+ const uninitializedPaths = resolveManagedServicePaths(options.dataDir, UNINITIALIZED_WALLET_ROOT_ID);
601
+ if (targetPaths.walletRuntimeRoot === uninitializedPaths.walletRuntimeRoot) {
602
+ return callback();
603
+ }
604
+ if (targetWalletRootId === UNINITIALIZED_WALLET_ROOT_ID) {
605
+ return callback();
606
+ }
607
+ const claimKey = `${options.dataDir}\n${targetWalletRootId}`;
608
+ if (claimedUninitializedRuntimeKeys.has(claimKey)) {
609
+ return callback();
610
+ }
611
+ claimedUninitializedRuntimeKeys.add(claimKey);
612
+ const lockTimeoutMs = options.shutdownTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
613
+ const bitcoindLock = await acquireFileLockWithRetry(uninitializedPaths.bitcoindLockPath, {
614
+ purpose: "managed-bitcoind-claim-uninitialized",
615
+ walletRootId: UNINITIALIZED_WALLET_ROOT_ID,
616
+ dataDir: options.dataDir,
617
+ }, lockTimeoutMs);
618
+ try {
619
+ const indexerLock = await acquireFileLockWithRetry(uninitializedPaths.indexerDaemonLockPath, {
620
+ purpose: "managed-indexer-claim-uninitialized",
621
+ walletRootId: UNINITIALIZED_WALLET_ROOT_ID,
622
+ dataDir: options.dataDir,
623
+ }, lockTimeoutMs);
624
+ try {
625
+ await stopIndexerDaemonServiceWithLockHeld({
626
+ dataDir: options.dataDir,
627
+ walletRootId: UNINITIALIZED_WALLET_ROOT_ID,
628
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
629
+ paths: uninitializedPaths,
630
+ });
631
+ await stopManagedBitcoindServiceWithLockHeld({
632
+ dataDir: options.dataDir,
633
+ walletRootId: UNINITIALIZED_WALLET_ROOT_ID,
634
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
635
+ paths: uninitializedPaths,
636
+ });
637
+ return await callback();
638
+ }
639
+ finally {
640
+ await indexerLock.release();
641
+ }
642
+ }
643
+ finally {
644
+ claimedUninitializedRuntimeKeys.delete(claimKey);
645
+ await bitcoindLock.release();
646
+ }
647
+ }
507
648
  async function refreshManagedBitcoindStatus(status, paths, options) {
508
649
  const nowUnixMs = Date.now();
509
650
  const rpc = createRpcClient(status.rpc);
651
+ const targetWalletRootId = options.walletRootId ?? status.walletRootId;
510
652
  try {
511
653
  await waitForRpcReady(rpc, status.rpc.cookieFile, status.chain, options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS);
512
654
  await validateNodeConfigForTesting(rpc, status.chain, status.zmq.endpoint);
513
- const walletReplica = await loadManagedWalletReplicaIfPresent(rpc, status.walletRootId, status.dataDir);
655
+ const walletReplica = await loadManagedWalletReplicaIfPresent(rpc, targetWalletRootId, status.dataDir);
514
656
  const nextStatus = {
515
657
  ...status,
658
+ walletRootId: targetWalletRootId,
659
+ runtimeRoot: paths.walletRuntimeRoot,
516
660
  state: "ready",
517
661
  processId: await isProcessAlive(status.processId) ? status.processId : null,
518
662
  walletReplica,
@@ -526,6 +670,8 @@ async function refreshManagedBitcoindStatus(status, paths, options) {
526
670
  catch (error) {
527
671
  const nextStatus = {
528
672
  ...status,
673
+ walletRootId: targetWalletRootId,
674
+ runtimeRoot: paths.walletRuntimeRoot,
529
675
  state: "failed",
530
676
  processId: await isProcessAlive(status.processId) ? status.processId : null,
531
677
  heartbeatAtUnixMs: nowUnixMs,
@@ -546,6 +692,8 @@ function createNodeHandle(status, paths, options) {
546
692
  expectedChain: currentStatus.chain,
547
693
  startHeight: currentStatus.startHeight,
548
694
  dataDir: currentStatus.dataDir,
695
+ getblockArchiveEndHeight: currentStatus.getblockArchiveEndHeight ?? null,
696
+ getblockArchiveSha256: currentStatus.getblockArchiveSha256 ?? null,
549
697
  walletRootId: currentStatus.walletRootId,
550
698
  runtimeRoot: paths.walletRuntimeRoot,
551
699
  async validate() {
@@ -553,6 +701,9 @@ function createNodeHandle(status, paths, options) {
553
701
  },
554
702
  async refreshServiceStatus() {
555
703
  currentStatus = await refreshManagedBitcoindStatus(currentStatus, paths, options);
704
+ this.getblockArchiveEndHeight = currentStatus.getblockArchiveEndHeight ?? null;
705
+ this.getblockArchiveSha256 = currentStatus.getblockArchiveSha256 ?? null;
706
+ this.walletRootId = currentStatus.walletRootId;
556
707
  return currentStatus;
557
708
  },
558
709
  async stop() {
@@ -617,103 +768,128 @@ export async function attachOrStartManagedBitcoindService(options) {
617
768
  dataDir: options.dataDir,
618
769
  walletRootId: options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID,
619
770
  };
620
- const existingProbe = await probeManagedBitcoindService(resolvedOptions);
621
- if (existingProbe.compatibility === "compatible") {
622
- const existing = await tryAttachExistingManagedBitcoindService(resolvedOptions);
623
- if (existing !== null) {
624
- return existing;
625
- }
626
- }
627
- if (existingProbe.compatibility !== "unreachable") {
628
- throw new Error(existingProbe.error ?? "managed_bitcoind_protocol_error");
629
- }
630
- const paths = resolveManagedServicePaths(resolvedOptions.dataDir ?? "", resolvedOptions.walletRootId);
631
771
  const startupTimeoutMs = resolvedOptions.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
632
- try {
633
- const lock = await acquireFileLock(paths.bitcoindLockPath, {
634
- purpose: "managed-bitcoind-start",
635
- walletRootId: resolvedOptions.walletRootId,
636
- dataDir: resolvedOptions.dataDir,
637
- });
638
- try {
639
- const liveProbe = await probeManagedBitcoindService(resolvedOptions);
640
- if (liveProbe.compatibility === "compatible") {
641
- const reattached = await tryAttachExistingManagedBitcoindService(resolvedOptions);
642
- if (reattached !== null) {
643
- return reattached;
644
- }
772
+ return withClaimedUninitializedManagedRuntime({
773
+ dataDir: resolvedOptions.dataDir ?? "",
774
+ walletRootId: resolvedOptions.walletRootId,
775
+ shutdownTimeoutMs: resolvedOptions.shutdownTimeoutMs,
776
+ }, async () => {
777
+ const existingProbe = await probeManagedBitcoindService(resolvedOptions);
778
+ if (existingProbe.compatibility === "compatible") {
779
+ const existing = await tryAttachExistingManagedBitcoindService(resolvedOptions);
780
+ if (existing !== null) {
781
+ return existing;
645
782
  }
646
- if (liveProbe.compatibility !== "unreachable") {
647
- throw new Error(liveProbe.error ?? "managed_bitcoind_protocol_error");
648
- }
649
- const bitcoindPath = await getBitcoindPath();
650
- await verifyBitcoindVersion(bitcoindPath);
651
- const binaryVersion = SUPPORTED_BITCOIND_VERSION;
652
- await mkdir(resolvedOptions.dataDir ?? "", { recursive: true });
653
- const runtimeConfig = await resolveRuntimeConfig(paths.bitcoindStatusPath, paths.bitcoindRuntimeConfigPath, resolvedOptions);
654
- await writeBitcoinConf(paths.bitcoinConfPath, resolvedOptions, runtimeConfig);
655
- const rpcConfig = runtimeConfig.rpc;
656
- const zmqConfig = {
657
- endpoint: `tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
658
- topic: "hashblock",
659
- port: runtimeConfig.zmqPort,
660
- pollIntervalMs: resolvedOptions.pollIntervalMs ?? 15_000,
661
- };
662
- const child = spawn(bitcoindPath, buildManagedServiceArgs(resolvedOptions, runtimeConfig), {
663
- detached: true,
664
- stdio: "ignore",
783
+ }
784
+ if (existingProbe.compatibility !== "unreachable") {
785
+ throw new Error(existingProbe.error ?? "managed_bitcoind_protocol_error");
786
+ }
787
+ const paths = resolveManagedServicePaths(resolvedOptions.dataDir ?? "", resolvedOptions.walletRootId);
788
+ try {
789
+ const lock = await acquireFileLock(paths.bitcoindLockPath, {
790
+ purpose: "managed-bitcoind-start",
791
+ walletRootId: resolvedOptions.walletRootId,
792
+ dataDir: resolvedOptions.dataDir,
665
793
  });
666
- child.unref();
667
- const rpc = createRpcClient(rpcConfig);
668
794
  try {
669
- await waitForRpcReady(rpc, rpcConfig.cookieFile, resolvedOptions.chain, startupTimeoutMs);
670
- await validateNodeConfigForTesting(rpc, resolvedOptions.chain, zmqConfig.endpoint);
671
- }
672
- catch (error) {
673
- if (child.pid !== undefined) {
795
+ const liveProbe = await probeManagedBitcoindService(resolvedOptions);
796
+ if (liveProbe.compatibility === "compatible") {
797
+ const reattached = await tryAttachExistingManagedBitcoindService(resolvedOptions);
798
+ if (reattached !== null) {
799
+ return reattached;
800
+ }
801
+ }
802
+ if (liveProbe.compatibility !== "unreachable") {
803
+ throw new Error(liveProbe.error ?? "managed_bitcoind_protocol_error");
804
+ }
805
+ const bitcoindPath = await getBitcoindPath();
806
+ await verifyBitcoindVersion(bitcoindPath);
807
+ const binaryVersion = SUPPORTED_BITCOIND_VERSION;
808
+ await mkdir(resolvedOptions.dataDir ?? "", { recursive: true });
809
+ const startManagedProcess = async (startOptions) => {
810
+ const runtimeConfig = await resolveRuntimeConfig(paths.bitcoindStatusPath, paths.bitcoindRuntimeConfigPath, startOptions);
811
+ await writeBitcoinConf(paths.bitcoinConfPath, startOptions, runtimeConfig);
812
+ const rpcConfig = runtimeConfig.rpc;
813
+ const zmqConfig = {
814
+ endpoint: `tcp://${LOCAL_HOST}:${runtimeConfig.zmqPort}`,
815
+ topic: "hashblock",
816
+ port: runtimeConfig.zmqPort,
817
+ pollIntervalMs: startOptions.pollIntervalMs ?? 15_000,
818
+ };
819
+ const child = spawn(bitcoindPath, buildManagedServiceArgs(startOptions, runtimeConfig), {
820
+ detached: true,
821
+ stdio: "ignore",
822
+ });
823
+ child.unref();
824
+ const rpc = createRpcClient(rpcConfig);
674
825
  try {
675
- process.kill(child.pid, "SIGTERM");
826
+ await waitForRpcReady(rpc, rpcConfig.cookieFile, startOptions.chain, startupTimeoutMs);
827
+ await validateNodeConfigForTesting(rpc, startOptions.chain, zmqConfig.endpoint);
828
+ }
829
+ catch (error) {
830
+ if (child.pid !== undefined) {
831
+ try {
832
+ process.kill(child.pid, "SIGTERM");
833
+ }
834
+ catch {
835
+ // ignore kill failures during startup cleanup
836
+ }
837
+ }
838
+ throw error;
676
839
  }
677
- catch {
678
- // ignore kill failures during startup cleanup
840
+ const nowUnixMs = Date.now();
841
+ const walletRootId = startOptions.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
842
+ const walletReplica = await loadManagedWalletReplicaIfPresent(rpc, walletRootId, startOptions.dataDir ?? "");
843
+ return createBitcoindServiceStatus({
844
+ binaryVersion,
845
+ serviceInstanceId: randomBytes(16).toString("hex"),
846
+ state: "ready",
847
+ processId: child.pid ?? null,
848
+ walletRootId,
849
+ chain: startOptions.chain,
850
+ dataDir: startOptions.dataDir ?? "",
851
+ runtimeRoot: paths.walletRuntimeRoot,
852
+ startHeight: startOptions.startHeight,
853
+ rpc: rpcConfig,
854
+ zmq: zmqConfig,
855
+ p2pPort: runtimeConfig.p2pPort,
856
+ getblockArchiveEndHeight: runtimeConfig.getblockArchiveEndHeight ?? null,
857
+ getblockArchiveSha256: runtimeConfig.getblockArchiveSha256 ?? null,
858
+ walletReplica,
859
+ startedAtUnixMs: nowUnixMs,
860
+ heartbeatAtUnixMs: nowUnixMs,
861
+ lastError: walletReplica.message ?? null,
862
+ });
863
+ };
864
+ let status;
865
+ try {
866
+ status = await startManagedProcess(resolvedOptions);
867
+ }
868
+ catch (error) {
869
+ if (resolvedOptions.getblockArchivePath === undefined || resolvedOptions.getblockArchivePath === null) {
870
+ throw error;
679
871
  }
872
+ status = await startManagedProcess({
873
+ ...resolvedOptions,
874
+ getblockArchivePath: null,
875
+ getblockArchiveEndHeight: null,
876
+ getblockArchiveSha256: null,
877
+ });
680
878
  }
681
- throw error;
879
+ await writeBitcoindStatus(paths, status);
880
+ return createNodeHandle(status, paths, resolvedOptions);
881
+ }
882
+ finally {
883
+ await lock.release();
682
884
  }
683
- const nowUnixMs = Date.now();
684
- const walletRootId = resolvedOptions.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
685
- const walletReplica = await loadManagedWalletReplicaIfPresent(rpc, walletRootId, resolvedOptions.dataDir ?? "");
686
- const status = createBitcoindServiceStatus({
687
- binaryVersion,
688
- serviceInstanceId: randomBytes(16).toString("hex"),
689
- state: "ready",
690
- processId: child.pid ?? null,
691
- walletRootId,
692
- chain: resolvedOptions.chain,
693
- dataDir: resolvedOptions.dataDir ?? "",
694
- runtimeRoot: paths.walletRuntimeRoot,
695
- startHeight: resolvedOptions.startHeight,
696
- rpc: rpcConfig,
697
- zmq: zmqConfig,
698
- p2pPort: runtimeConfig.p2pPort,
699
- walletReplica,
700
- startedAtUnixMs: nowUnixMs,
701
- heartbeatAtUnixMs: nowUnixMs,
702
- lastError: walletReplica.message ?? null,
703
- });
704
- await writeBitcoindStatus(paths, status);
705
- return createNodeHandle(status, paths, resolvedOptions);
706
- }
707
- finally {
708
- await lock.release();
709
885
  }
710
- }
711
- catch (error) {
712
- if (error instanceof FileLockBusyError) {
713
- return waitForManagedBitcoindService(resolvedOptions, startupTimeoutMs);
886
+ catch (error) {
887
+ if (error instanceof FileLockBusyError) {
888
+ return waitForManagedBitcoindService(resolvedOptions, startupTimeoutMs);
889
+ }
890
+ throw error;
714
891
  }
715
- throw error;
716
- }
892
+ });
717
893
  }
718
894
  export async function stopManagedBitcoindService(options) {
719
895
  const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
@@ -724,35 +900,11 @@ export async function stopManagedBitcoindService(options) {
724
900
  dataDir: options.dataDir,
725
901
  });
726
902
  try {
727
- const status = await readJsonFile(paths.bitcoindStatusPath);
728
- const processId = status?.processId ?? null;
729
- if (status === null || processId === null || !await isProcessAlive(processId)) {
730
- await clearManagedBitcoindRuntimeArtifacts(paths);
731
- return {
732
- status: "not-running",
733
- walletRootId,
734
- };
735
- }
736
- const rpc = createRpcClient(status.rpc);
737
- try {
738
- await rpc.stop();
739
- }
740
- catch {
741
- try {
742
- process.kill(processId, "SIGTERM");
743
- }
744
- catch (error) {
745
- if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
746
- throw error;
747
- }
748
- }
749
- }
750
- await waitForProcessExit(processId, options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS, "managed_bitcoind_service_stop_timeout");
751
- await clearManagedBitcoindRuntimeArtifacts(paths);
752
- return {
753
- status: "stopped",
903
+ return stopManagedBitcoindServiceWithLockHeld({
904
+ ...options,
754
905
  walletRootId,
755
- };
906
+ paths,
907
+ });
756
908
  }
757
909
  finally {
758
910
  await lock.release();
@@ -2,8 +2,8 @@ export { openManagedBitcoindClientInternal } from "./client.js";
2
2
  export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopIndexerDaemonService, shutdownIndexerDaemonForTesting, } from "./indexer-daemon.js";
3
3
  export { normalizeRpcBlock } from "./normalize.js";
4
4
  export { BitcoinRpcClient } from "./rpc.js";
5
- export { attachOrStartManagedBitcoindService, readManagedBitcoindServiceStatusForTesting, stopManagedBitcoindService, shutdownManagedBitcoindServiceForTesting, } from "./service.js";
6
- export { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, createBootstrapStateForTesting, downloadSnapshotFileForTesting, loadBootstrapStateForTesting, resolveBootstrapPathsForTesting, saveBootstrapStateForTesting, validateSnapshotFileForTesting, waitForHeadersForTesting, } from "./bootstrap.js";
5
+ export { attachOrStartManagedBitcoindService, buildManagedServiceArgsForTesting, readManagedBitcoindServiceStatusForTesting, resolveManagedBitcoindDbcacheMiB, stopManagedBitcoindService, shutdownManagedBitcoindServiceForTesting, writeBitcoinConfForTesting, } from "./service.js";
6
+ export { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, createBootstrapStateForTesting, downloadSnapshotFileForTesting, loadBootstrapStateForTesting, prepareLatestGetblockArchiveForTesting, resolveBootstrapPathsForTesting, resolveGetblockArchivePathsForTesting, resolveReadyGetblockArchiveForTesting, saveBootstrapStateForTesting, validateSnapshotFileForTesting, waitForGetblockArchiveImportForTesting, waitForHeadersForTesting, } from "./bootstrap.js";
7
7
  export { buildBitcoindArgsForTesting, createRpcClient, launchManagedBitcoindNode, resolveDefaultBitcoindDataDirForTesting, validateNodeConfigForTesting, } from "./node.js";
8
8
  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";
9
9
  export { WritingQuoteRotator, loadWritingQuotesForTesting, shuffleIndicesForTesting, } from "./quotes.js";
@@ -2,8 +2,8 @@ export { openManagedBitcoindClientInternal } from "./client.js";
2
2
  export { attachOrStartIndexerDaemon, readIndexerDaemonStatusForTesting, stopIndexerDaemonService, shutdownIndexerDaemonForTesting, } from "./indexer-daemon.js";
3
3
  export { normalizeRpcBlock } from "./normalize.js";
4
4
  export { BitcoinRpcClient } from "./rpc.js";
5
- export { attachOrStartManagedBitcoindService, readManagedBitcoindServiceStatusForTesting, stopManagedBitcoindService, shutdownManagedBitcoindServiceForTesting, } from "./service.js";
6
- export { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, createBootstrapStateForTesting, downloadSnapshotFileForTesting, loadBootstrapStateForTesting, resolveBootstrapPathsForTesting, saveBootstrapStateForTesting, validateSnapshotFileForTesting, waitForHeadersForTesting, } from "./bootstrap.js";
5
+ export { attachOrStartManagedBitcoindService, buildManagedServiceArgsForTesting, readManagedBitcoindServiceStatusForTesting, resolveManagedBitcoindDbcacheMiB, stopManagedBitcoindService, shutdownManagedBitcoindServiceForTesting, writeBitcoinConfForTesting, } from "./service.js";
6
+ export { AssumeUtxoBootstrapController, DEFAULT_SNAPSHOT_METADATA, createBootstrapStateForTesting, downloadSnapshotFileForTesting, loadBootstrapStateForTesting, prepareLatestGetblockArchiveForTesting, resolveBootstrapPathsForTesting, resolveGetblockArchivePathsForTesting, resolveReadyGetblockArchiveForTesting, saveBootstrapStateForTesting, validateSnapshotFileForTesting, waitForGetblockArchiveImportForTesting, waitForHeadersForTesting, } from "./bootstrap.js";
7
7
  export { buildBitcoindArgsForTesting, createRpcClient, launchManagedBitcoindNode, resolveDefaultBitcoindDataDirForTesting, validateNodeConfigForTesting, } from "./node.js";
8
8
  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";
9
9
  export { WritingQuoteRotator, loadWritingQuotesForTesting, shuffleIndicesForTesting, } from "./quotes.js";
@@ -1,5 +1,5 @@
1
1
  import type { BitcoinBlock, Client, ClientOptions, ClientTip } from "../types.js";
2
- export type BootstrapPhase = "snapshot_download" | "wait_headers_for_snapshot" | "load_snapshot" | "bitcoin_sync" | "cogcoin_sync" | "follow_tip" | "paused" | "error" | "complete";
2
+ export type BootstrapPhase = "getblock_archive_download" | "getblock_archive_import" | "snapshot_download" | "wait_headers_for_snapshot" | "load_snapshot" | "bitcoin_sync" | "cogcoin_sync" | "follow_tip" | "paused" | "error" | "complete";
3
3
  export type ProgressOutputMode = "auto" | "tty" | "none";
4
4
  export interface SnapshotMetadata {
5
5
  url: string;
@@ -17,6 +17,28 @@ export interface SnapshotChunkManifest {
17
17
  snapshotSha256: string;
18
18
  chunkSha256s: string[];
19
19
  }
20
+ export interface GetblockArchiveManifestBlockRecord {
21
+ height: number;
22
+ blockHash: string;
23
+ previousBlockHash: string;
24
+ recordOffset: number;
25
+ recordLength: number;
26
+ rawBlockSizeBytes: number;
27
+ }
28
+ export interface GetblockArchiveManifest {
29
+ formatVersion: number;
30
+ chain: "main";
31
+ baseSnapshotHeight: number;
32
+ firstBlockHeight: number;
33
+ endHeight: number;
34
+ blockCount: number;
35
+ artifactFilename: string;
36
+ artifactSizeBytes: number;
37
+ artifactSha256: string;
38
+ chunkSizeBytes: number;
39
+ chunkSha256s: string[];
40
+ blocks: GetblockArchiveManifestBlockRecord[];
41
+ }
20
42
  export interface WritingQuote {
21
43
  quote: string;
22
44
  author: string;
@@ -62,6 +84,9 @@ export interface ManagedBitcoindRuntimeConfig {
62
84
  rpc: BitcoindRpcConfig;
63
85
  zmqPort: number;
64
86
  p2pPort: number;
87
+ dbcacheMiB: number;
88
+ getblockArchiveEndHeight?: number | null;
89
+ getblockArchiveSha256?: string | null;
65
90
  }
66
91
  export declare const MANAGED_BITCOIND_SERVICE_API_VERSION = "cogcoin/bitcoind-service/v1";
67
92
  export type ManagedBitcoindServiceState = "starting" | "ready" | "stopping" | "failed";
@@ -80,6 +105,8 @@ export interface ManagedBitcoindServiceStatus {
80
105
  rpc: BitcoindRpcConfig;
81
106
  zmq: BitcoindZmqConfig;
82
107
  p2pPort: number;
108
+ getblockArchiveEndHeight: number | null;
109
+ getblockArchiveSha256: string | null;
83
110
  walletReplica: ManagedCoreWalletReplicaStatus | null;
84
111
  startedAtUnixMs: number;
85
112
  heartbeatAtUnixMs: number;
@@ -122,6 +149,10 @@ export interface ManagedBitcoindStatus {
122
149
  serviceStatus?: ManagedBitcoindObservedStatus | null;
123
150
  indexerDaemon?: ManagedIndexerDaemonObservedStatus | null;
124
151
  }
152
+ export interface ManagedGetblockArchiveRestartRequest {
153
+ currentArchiveEndHeight: number | null;
154
+ nextArchiveEndHeight: number;
155
+ }
125
156
  export interface ManagedBitcoindOptions extends ClientOptions {
126
157
  dataDir?: string;
127
158
  databasePath?: string;
@@ -136,6 +167,8 @@ export interface ManagedBitcoindOptions extends ClientOptions {
136
167
  managedWalletPassphrase?: string;
137
168
  onProgress?: (event: ManagedBitcoindProgressEvent) => void;
138
169
  progressOutput?: ProgressOutputMode;
170
+ fetchImpl?: typeof fetch;
171
+ confirmGetblockArchiveRestart?: (request: ManagedGetblockArchiveRestartRequest) => Promise<boolean>;
139
172
  }
140
173
  export interface ManagedBitcoindClient extends Client {
141
174
  syncToTip(): Promise<SyncResult>;
@@ -396,6 +429,8 @@ export interface ManagedBitcoindNodeHandle {
396
429
  expectedChain: "main" | "regtest";
397
430
  startHeight: number;
398
431
  dataDir: string;
432
+ getblockArchiveEndHeight: number | null;
433
+ getblockArchiveSha256: string | null;
399
434
  walletRootId?: string;
400
435
  runtimeRoot?: string;
401
436
  validate(): Promise<void>;