@cogcoin/client 1.1.16 → 1.2.1

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.
@@ -2,9 +2,9 @@ import { createHash } from "node:crypto";
2
2
  import { assaySentences, deriveBlendSeed } from "@cogcoin/scoring";
3
3
  import { lookupDomain, lookupDomainById } from "@cogcoin/indexer/queries";
4
4
  import { COG_OPCODES, COG_PREFIX } from "../cogop/constants.js";
5
- import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
6
5
  import { compareLexicographically, numberToSats, resolveBip39WordsFromIndices, rootDomain, tieBreakHash, } from "./engine-utils.js";
7
6
  import { getIndexerTruthKey } from "./candidate.js";
7
+ import { hydrateMiningMempoolIndex, } from "./mempool-index.js";
8
8
  const MINING_MEMPOOL_COOPERATIVE_YIELD_EVERY = 25;
9
9
  const MINING_MEMPOOL_RAW_TX_FETCH_CONCURRENCY = 8;
10
10
  const MINING_MEMPOOL_PROGRESS_REPORT_EVERY = 25;
@@ -52,6 +52,41 @@ function resolveEffectiveFeeRate(mempoolEntry) {
52
52
  : 0n,
53
53
  ].reduce((best, candidate) => (candidate > best ? candidate : best), 0n));
54
54
  }
55
+ function createContextFromRawTransaction(txid, tx) {
56
+ const payloadHex = tx.vout.find((entry) => entry.scriptPubKey?.hex?.startsWith("6a") === true)?.scriptPubKey?.hex;
57
+ const payload = payloadHex === undefined ? null : Buffer.from(payloadHex, "hex");
58
+ const decodedPayload = payload === null || payload.length < 2 || payload[0] !== 0x6a
59
+ ? null
60
+ : (() => {
61
+ const opcode = payload[1];
62
+ if (opcode === undefined) {
63
+ return null;
64
+ }
65
+ if (opcode <= 75) {
66
+ const end = 2 + opcode;
67
+ return end === payload.length ? payload.subarray(2, end) : null;
68
+ }
69
+ if (opcode === 0x4c && payload.length >= 3) {
70
+ const length = payload[2];
71
+ const end = 3 + length;
72
+ return end === payload.length ? payload.subarray(3, end) : null;
73
+ }
74
+ return null;
75
+ })();
76
+ if (decodedPayload === null
77
+ || decodedPayload.length < 3
78
+ || decodedPayload[0] !== COG_PREFIX[0]
79
+ || decodedPayload[1] !== COG_PREFIX[1]
80
+ || decodedPayload[2] !== COG_PREFIX[2]) {
81
+ return null;
82
+ }
83
+ return {
84
+ txid,
85
+ senderScriptHex: tx.vin[0]?.prevout?.scriptPubKey?.hex ?? null,
86
+ inputTxids: tx.vin.map((input) => input.txid).filter((inputTxid) => inputTxid !== undefined),
87
+ payload: decodedPayload,
88
+ };
89
+ }
55
90
  async function warmMissingRawTxContexts(options) {
56
91
  const missingTxids = options.visibleTxids.filter((txid) => !options.rawTxContexts.has(txid));
57
92
  if (missingTxids.length === 0) {
@@ -99,13 +134,10 @@ async function warmMissingRawTxContexts(options) {
99
134
  const tx = await options.rpc.getRawTransaction(txid, true).catch(() => null);
100
135
  options.throwIfStopping?.();
101
136
  if (tx !== null) {
102
- const payloadHex = tx.vout.find((entry) => entry.scriptPubKey?.hex?.startsWith("6a") === true)?.scriptPubKey?.hex;
103
- options.rawTxContexts.set(txid, {
104
- txid,
105
- senderScriptHex: tx.vin[0]?.prevout?.scriptPubKey?.hex ?? null,
106
- rawTransaction: tx,
107
- payload: payloadHex === undefined ? null : extractOpReturnPayloadFromScriptHex(payloadHex),
108
- });
137
+ const context = createContextFromRawTransaction(txid, tx);
138
+ if (context !== null) {
139
+ options.rawTxContexts.set(txid, context);
140
+ }
109
141
  }
110
142
  completed += 1;
111
143
  await reportProgress(completed, completed === missingTxids.length);
@@ -217,9 +249,7 @@ function parseSupportedAncestorOperation(context) {
217
249
  return "unsupported";
218
250
  }
219
251
  function getAncestorTxids(context, txContexts) {
220
- return context.rawTransaction.vin
221
- .map((vin) => vin.txid ?? null)
222
- .filter((txid) => txid !== null && txContexts.has(txid));
252
+ return context.inputTxids.filter((txid) => txContexts.has(txid));
223
253
  }
224
254
  function topologicallyOrderAncestorContexts(options) {
225
255
  const visited = new Map();
@@ -281,9 +311,17 @@ function topologicallyOrderAncestorContexts(options) {
281
311
  return ordered;
282
312
  }
283
313
  export function topologicallyOrderAncestorTxidsForTesting(options) {
314
+ const txContexts = new Map([...options.txContexts].map(([txid, context]) => [txid, {
315
+ txid: context.txid,
316
+ senderScriptHex: null,
317
+ inputTxids: context.rawTransaction.vin
318
+ .map((vin) => vin.txid)
319
+ .filter((inputTxid) => inputTxid !== undefined),
320
+ payload: null,
321
+ }]));
284
322
  const ordered = topologicallyOrderAncestorContexts({
285
323
  txid: options.txid,
286
- txContexts: options.txContexts,
324
+ txContexts,
287
325
  });
288
326
  return ordered?.map((context) => context.txid) ?? null;
289
327
  }
@@ -439,6 +477,35 @@ function toSentenceBoardEntries(entries) {
439
477
  requiredWords: resolveBip39WordsFromIndices(entry.bip39WordIndices),
440
478
  }));
441
479
  }
480
+ async function fetchIndexedMempoolEntries(options) {
481
+ const entries = {};
482
+ let nextIndex = 0;
483
+ const workerCount = Math.min(MINING_MEMPOOL_RAW_TX_FETCH_CONCURRENCY, options.txids.length);
484
+ const workers = Array.from({ length: workerCount }, async () => {
485
+ while (true) {
486
+ const index = nextIndex;
487
+ if (index >= options.txids.length) {
488
+ return;
489
+ }
490
+ nextIndex += 1;
491
+ options.throwIfStopping?.();
492
+ const txid = options.txids[index];
493
+ const entry = await options.rpc.getMempoolEntry(txid).catch(() => null);
494
+ options.throwIfStopping?.();
495
+ if (entry === null) {
496
+ throw new Error("mining_mempool_index_entry_unavailable");
497
+ }
498
+ entries[txid] = entry;
499
+ }
500
+ });
501
+ try {
502
+ await Promise.all(workers);
503
+ return entries;
504
+ }
505
+ catch {
506
+ return null;
507
+ }
508
+ }
442
509
  export async function runCompetitivenessGate(options) {
443
510
  const createDecision = (overrides) => ({
444
511
  allowed: overrides.allowed ?? false,
@@ -475,12 +542,8 @@ export async function runCompetitivenessGate(options) {
475
542
  };
476
543
  };
477
544
  let mempoolVerbose;
478
- let mempoolEntries;
479
545
  try {
480
- [mempoolVerbose, mempoolEntries] = await Promise.all([
481
- options.rpc.getRawMempoolVerbose(),
482
- options.rpc.getRawMempoolEntries(),
483
- ]);
546
+ mempoolVerbose = await options.rpc.getRawMempoolVerbose();
484
547
  options.throwIfStopping?.();
485
548
  }
486
549
  catch {
@@ -512,19 +575,95 @@ export async function runCompetitivenessGate(options) {
512
575
  }
513
576
  const referencedPrefix = Buffer.from(options.candidate.referencedBlockHashInternal.subarray(0, 4)).toString("hex");
514
577
  const visibleTxids = mempoolVerbose.txids.filter((txid) => !excludedTxids.includes(txid));
515
- pruneRawTxContextsToVisibleTxids({
516
- rawTxContexts: cacheState.rawTxContexts,
517
- visibleTxids,
518
- });
519
- await warmMissingRawTxContexts({
520
- rpc: options.rpc,
521
- rawTxContexts: cacheState.rawTxContexts,
522
- visibleTxids,
523
- cooperativeYield: options.cooperativeYield,
524
- cooperativeYieldEvery: options.cooperativeYieldEvery,
525
- throwIfStopping: options.throwIfStopping,
526
- onWarmupProgress: options.onWarmupProgress,
527
- });
578
+ let rawTxContexts;
579
+ let mempoolEntries;
580
+ let gateCacheStatus = "refreshed";
581
+ if (options.mempoolIndex?.rawTxSupported === true) {
582
+ const indexed = await hydrateMiningMempoolIndex({
583
+ walletRootId,
584
+ serviceIdentity: options.mempoolIndex.serviceIdentity,
585
+ cachePath: options.mempoolIndex.cachePath,
586
+ rpc: options.rpc,
587
+ visibleTxids,
588
+ cooperativeYield: options.cooperativeYield,
589
+ cooperativeYieldEvery: options.cooperativeYieldEvery,
590
+ throwIfStopping: options.throwIfStopping,
591
+ onWarmupProgress: options.onWarmupProgress,
592
+ }).catch(() => null);
593
+ if (indexed !== null) {
594
+ rawTxContexts = indexed.contexts;
595
+ gateCacheStatus = indexed.cacheStatus;
596
+ const indexedTxids = visibleTxids.filter((txid) => rawTxContexts.has(txid));
597
+ const indexedEntries = await fetchIndexedMempoolEntries({
598
+ rpc: options.rpc,
599
+ txids: indexedTxids,
600
+ throwIfStopping: options.throwIfStopping,
601
+ });
602
+ if (indexedEntries === null) {
603
+ rawTxContexts = new Map();
604
+ mempoolEntries = {};
605
+ gateCacheStatus = "fallback-scan";
606
+ }
607
+ else {
608
+ mempoolEntries = indexedEntries;
609
+ }
610
+ }
611
+ else {
612
+ return createDecision({
613
+ competitivenessGateIndeterminate: true,
614
+ decision: "indeterminate-mempool-gate",
615
+ mempoolSequenceCacheStatus: "index-warming",
616
+ lastMempoolSequence: mempoolSequence,
617
+ });
618
+ }
619
+ }
620
+ else {
621
+ rawTxContexts = cacheState.rawTxContexts;
622
+ gateCacheStatus = options.mempoolIndex?.rawTxSupported === false ? "fallback-scan" : "refreshed";
623
+ const fallbackEntries = await options.rpc.getRawMempoolEntries().catch(() => null);
624
+ if (fallbackEntries === null) {
625
+ return createDecision({
626
+ competitivenessGateIndeterminate: true,
627
+ });
628
+ }
629
+ mempoolEntries = fallbackEntries;
630
+ pruneRawTxContextsToVisibleTxids({
631
+ rawTxContexts,
632
+ visibleTxids,
633
+ });
634
+ await warmMissingRawTxContexts({
635
+ rpc: options.rpc,
636
+ rawTxContexts,
637
+ visibleTxids,
638
+ cooperativeYield: options.cooperativeYield,
639
+ cooperativeYieldEvery: options.cooperativeYieldEvery,
640
+ throwIfStopping: options.throwIfStopping,
641
+ onWarmupProgress: options.onWarmupProgress,
642
+ });
643
+ }
644
+ if (gateCacheStatus === "fallback-scan" && rawTxContexts.size === 0 && Object.keys(mempoolEntries).length === 0) {
645
+ const fallbackEntries = await options.rpc.getRawMempoolEntries().catch(() => null);
646
+ if (fallbackEntries === null) {
647
+ return createDecision({
648
+ competitivenessGateIndeterminate: true,
649
+ });
650
+ }
651
+ mempoolEntries = fallbackEntries;
652
+ rawTxContexts = cacheState.rawTxContexts;
653
+ pruneRawTxContextsToVisibleTxids({
654
+ rawTxContexts,
655
+ visibleTxids,
656
+ });
657
+ await warmMissingRawTxContexts({
658
+ rpc: options.rpc,
659
+ rawTxContexts,
660
+ visibleTxids,
661
+ cooperativeYield: options.cooperativeYield,
662
+ cooperativeYieldEvery: options.cooperativeYieldEvery,
663
+ throwIfStopping: options.throwIfStopping,
664
+ onWarmupProgress: options.onWarmupProgress,
665
+ });
666
+ }
528
667
  const entries = new Map();
529
668
  for (let index = 0; index < visibleTxids.length; index += 1) {
530
669
  await maybeYieldDuringMempoolScan({
@@ -555,7 +694,7 @@ export async function runCompetitivenessGate(options) {
555
694
  const decision = createDecision({
556
695
  competitivenessGateIndeterminate: true,
557
696
  decision: "indeterminate-mempool-gate",
558
- mempoolSequenceCacheStatus: "refreshed",
697
+ mempoolSequenceCacheStatus: gateCacheStatus,
559
698
  lastMempoolSequence: mempoolSequence,
560
699
  });
561
700
  setDecisionReuse(decision);
@@ -625,7 +764,7 @@ export async function runCompetitivenessGate(options) {
625
764
  higherRankedCompetitorDomainCount: 1,
626
765
  dedupedCompetitorDomainCount: otherDomainBest.size,
627
766
  competitivenessGateIndeterminate: false,
628
- mempoolSequenceCacheStatus: "refreshed",
767
+ mempoolSequenceCacheStatus: gateCacheStatus,
629
768
  lastMempoolSequence: mempoolSequence,
630
769
  visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
631
770
  });
@@ -669,7 +808,7 @@ export async function runCompetitivenessGate(options) {
669
808
  higherRankedCompetitorDomainCount,
670
809
  dedupedCompetitorDomainCount: otherDomainBest.size,
671
810
  competitivenessGateIndeterminate: false,
672
- mempoolSequenceCacheStatus: "refreshed",
811
+ mempoolSequenceCacheStatus: gateCacheStatus,
673
812
  lastMempoolSequence: mempoolSequence,
674
813
  visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
675
814
  candidateRank,
@@ -683,7 +822,7 @@ export async function runCompetitivenessGate(options) {
683
822
  higherRankedCompetitorDomainCount,
684
823
  dedupedCompetitorDomainCount: otherDomainBest.size,
685
824
  competitivenessGateIndeterminate: false,
686
- mempoolSequenceCacheStatus: "refreshed",
825
+ mempoolSequenceCacheStatus: gateCacheStatus,
687
826
  lastMempoolSequence: mempoolSequence,
688
827
  visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
689
828
  candidateRank,
@@ -698,7 +837,7 @@ export async function runCompetitivenessGate(options) {
698
837
  higherRankedCompetitorDomainCount: 0,
699
838
  dedupedCompetitorDomainCount: otherDomainBest.size,
700
839
  competitivenessGateIndeterminate: true,
701
- mempoolSequenceCacheStatus: "refreshed",
840
+ mempoolSequenceCacheStatus: gateCacheStatus,
702
841
  lastMempoolSequence: mempoolSequence,
703
842
  visibleBoardEntries: toSentenceBoardEntries(visibleRankedEntries),
704
843
  });
@@ -10,6 +10,7 @@ import { type MiningRuntimeLoopState } from "./engine-state.js";
10
10
  import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
11
11
  import { createRpcClient } from "../../bitcoind/node.js";
12
12
  import { type MiningRuntimeStatusOverrides } from "./projection.js";
13
+ import type { MiningMempoolIndexGateOptions } from "./mempool-index.js";
13
14
  export declare function runMiningPhaseMachine(options: {
14
15
  dataDir: string;
15
16
  databasePath: string;
@@ -33,6 +34,7 @@ export declare function runMiningPhaseMachine(options: {
33
34
  assaySentencesImpl?: typeof assaySentences;
34
35
  cooperativeYieldImpl?: MiningCooperativeYield;
35
36
  cooperativeYieldEvery?: number;
37
+ mempoolIndex?: MiningMempoolIndexGateOptions;
36
38
  nowImpl?: () => number;
37
39
  saveCycleStatus: (readContext: WalletReadContext, overrides: MiningRuntimeStatusOverrides) => Promise<MiningRuntimeStatusV1>;
38
40
  appendEvent: (event: MiningEventRecord) => Promise<void>;
@@ -350,6 +350,7 @@ export async function runMiningPhaseMachine(options) {
350
350
  assaySentencesImpl: options.assaySentencesImpl,
351
351
  cooperativeYield: options.cooperativeYieldImpl,
352
352
  cooperativeYieldEvery: options.cooperativeYieldEvery,
353
+ mempoolIndex: options.mempoolIndex,
353
354
  throwIfStopping: options.throwIfStopping,
354
355
  onWarmupProgress: async (progress) => {
355
356
  if (progress.total <= 0) {
@@ -0,0 +1,65 @@
1
+ import type { WalletRuntimePaths } from "../runtime.js";
2
+ import type { MiningCooperativeYield, MiningRpcClient } from "./engine-types.js";
3
+ export interface MiningMempoolTxContext {
4
+ txid: string;
5
+ senderScriptHex: string | null;
6
+ inputTxids: string[];
7
+ payload: Uint8Array | null;
8
+ }
9
+ export interface MiningMempoolIndexHydrationResult {
10
+ contexts: Map<string, MiningMempoolTxContext>;
11
+ cacheStatus: "indexed" | "index-warming";
12
+ hydratedCount: number;
13
+ }
14
+ export interface MiningMempoolIndexGateOptions {
15
+ rawTxSupported: boolean;
16
+ cachePath: string;
17
+ serviceIdentity: string;
18
+ }
19
+ interface RawTxSubscriberLike extends AsyncIterable<unknown> {
20
+ close(): void;
21
+ connect(endpoint: string): void;
22
+ subscribe(topic: string): void;
23
+ }
24
+ interface ZeroMqModuleLike {
25
+ Subscriber: new () => RawTxSubscriberLike;
26
+ }
27
+ interface ParsedRawTransactionIndexContext {
28
+ txid: string;
29
+ inputTxids: string[];
30
+ payload: Uint8Array | null;
31
+ }
32
+ export declare function resolveMiningMempoolIndexCachePath(paths: Pick<WalletRuntimePaths, "miningRoot">): string;
33
+ export declare function resolveMiningMempoolServiceIdentity(options: {
34
+ dataDir: string;
35
+ pid: number | null;
36
+ zmqEndpoint: string;
37
+ rawTxTopic?: "rawtx";
38
+ }): string;
39
+ export declare function hydrateMiningMempoolIndex(options: {
40
+ walletRootId: string;
41
+ serviceIdentity: string;
42
+ cachePath: string;
43
+ rpc: Pick<MiningRpcClient, "getRawTransaction">;
44
+ visibleTxids: readonly string[];
45
+ cooperativeYield?: MiningCooperativeYield;
46
+ cooperativeYieldEvery?: number;
47
+ throwIfStopping?: () => void;
48
+ onWarmupProgress?: (progress: {
49
+ processed: number;
50
+ total: number;
51
+ }) => Promise<void> | void;
52
+ }): Promise<MiningMempoolIndexHydrationResult>;
53
+ declare function parseRawTransactionForIndex(rawHex: string): ParsedRawTransactionIndexContext | null;
54
+ export declare function ensureMiningMempoolRawTxSubscriber(options: {
55
+ walletRootId: string;
56
+ serviceIdentity: string;
57
+ cachePath: string;
58
+ zmqEndpoint: string;
59
+ rawTxTopic?: "rawtx";
60
+ loadZeroMq?: () => Promise<ZeroMqModuleLike>;
61
+ }): Promise<boolean>;
62
+ export declare function closeMiningMempoolIndexSubscribersForTesting(): Promise<void>;
63
+ export declare function clearMiningMempoolIndexCacheForTesting(): void;
64
+ export declare const parseRawTransactionForMiningMempoolIndexTesting: typeof parseRawTransactionForIndex;
65
+ export {};