@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.
- package/README.md +28 -11
- package/dist/bitcoind/indexer-daemon/lifecycle.js +98 -1
- package/dist/bitcoind/managed-bitcoind-service-config.js +2 -0
- package/dist/bitcoind/managed-bitcoind-service-lifecycle.js +4 -1
- package/dist/bitcoind/managed-runtime/bitcoind-policy.js +10 -0
- package/dist/bitcoind/managed-runtime/types.d.ts +1 -1
- package/dist/bitcoind/node.d.ts +3 -1
- package/dist/bitcoind/node.js +15 -3
- package/dist/bitcoind/types.d.ts +1 -0
- package/dist/cli/output/rules/services.js +7 -0
- package/dist/sqlite/reindex-requirement.d.ts +8 -0
- package/dist/sqlite/reindex-requirement.js +100 -0
- package/dist/wallet/lifecycle/repair-bitcoind.js +21 -11
- package/dist/wallet/lifecycle/repair-runtime.js +3 -0
- package/dist/wallet/lifecycle/types.d.ts +2 -2
- package/dist/wallet/mining/competitiveness.d.ts +5 -0
- package/dist/wallet/mining/competitiveness.js +174 -35
- package/dist/wallet/mining/cycle.d.ts +2 -0
- package/dist/wallet/mining/cycle.js +1 -0
- package/dist/wallet/mining/mempool-index.d.ts +65 -0
- package/dist/wallet/mining/mempool-index.js +451 -0
- package/dist/wallet/mining/runner.js +24 -0
- package/dist/wallet/mining/types.d.ts +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { COG_PREFIX } from "../cogop/constants.js";
|
|
5
|
+
import { writeJsonFileAtomic } from "../fs/atomic.js";
|
|
6
|
+
import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
|
|
7
|
+
const MINING_MEMPOOL_INDEX_SCHEMA_VERSION = 1;
|
|
8
|
+
const MINING_MEMPOOL_INDEX_RAW_TX_FETCH_CONCURRENCY = 8;
|
|
9
|
+
const MINING_MEMPOOL_INDEX_PROGRESS_REPORT_EVERY = 25;
|
|
10
|
+
const indexStates = new Map();
|
|
11
|
+
const rawTxSubscribers = new Map();
|
|
12
|
+
export function resolveMiningMempoolIndexCachePath(paths) {
|
|
13
|
+
return join(paths.miningRoot, "mempool-index.json");
|
|
14
|
+
}
|
|
15
|
+
export function resolveMiningMempoolServiceIdentity(options) {
|
|
16
|
+
return [
|
|
17
|
+
options.dataDir,
|
|
18
|
+
options.pid ?? "unknown-pid",
|
|
19
|
+
options.zmqEndpoint,
|
|
20
|
+
options.rawTxTopic ?? "no-rawtx",
|
|
21
|
+
].join("|");
|
|
22
|
+
}
|
|
23
|
+
function cacheKey(walletRootId, serviceIdentity, cachePath) {
|
|
24
|
+
return `${walletRootId}\n${serviceIdentity}\n${cachePath}`;
|
|
25
|
+
}
|
|
26
|
+
function isCogPayload(payload) {
|
|
27
|
+
return payload !== null
|
|
28
|
+
&& payload.length >= 3
|
|
29
|
+
&& payload[0] === COG_PREFIX[0]
|
|
30
|
+
&& payload[1] === COG_PREFIX[1]
|
|
31
|
+
&& payload[2] === COG_PREFIX[2];
|
|
32
|
+
}
|
|
33
|
+
function serializeContext(context) {
|
|
34
|
+
return {
|
|
35
|
+
txid: context.txid,
|
|
36
|
+
senderScriptHex: context.senderScriptHex,
|
|
37
|
+
inputTxids: [...context.inputTxids],
|
|
38
|
+
payloadHex: context.payload === null ? null : Buffer.from(context.payload).toString("hex"),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function deserializeContext(context) {
|
|
42
|
+
if (typeof context.txid !== "string"
|
|
43
|
+
|| (context.senderScriptHex !== null && typeof context.senderScriptHex !== "string")
|
|
44
|
+
|| !Array.isArray(context.inputTxids)
|
|
45
|
+
|| (context.payloadHex !== null && typeof context.payloadHex !== "string")) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
txid: context.txid,
|
|
50
|
+
senderScriptHex: context.senderScriptHex,
|
|
51
|
+
inputTxids: context.inputTxids.filter((txid) => typeof txid === "string"),
|
|
52
|
+
payload: context.payloadHex === null ? null : Buffer.from(context.payloadHex, "hex"),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function loadStateFromDisk(state) {
|
|
56
|
+
if (state.loaded) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
state.loaded = true;
|
|
60
|
+
const raw = await readFile(state.cachePath, "utf8").catch(() => null);
|
|
61
|
+
if (raw === null) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(raw);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (parsed.schemaVersion !== MINING_MEMPOOL_INDEX_SCHEMA_VERSION
|
|
72
|
+
|| parsed.walletRootId !== state.walletRootId
|
|
73
|
+
|| parsed.serviceIdentity !== state.serviceIdentity
|
|
74
|
+
|| !Array.isArray(parsed.contexts)
|
|
75
|
+
|| !Array.isArray(parsed.negativeTxids)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
for (const persistedContext of parsed.contexts) {
|
|
79
|
+
const context = deserializeContext(persistedContext);
|
|
80
|
+
if (context !== null && isCogPayload(context.payload)) {
|
|
81
|
+
state.contexts.set(context.txid, context);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const txid of parsed.negativeTxids) {
|
|
85
|
+
if (typeof txid === "string") {
|
|
86
|
+
state.negativeTxids.add(txid);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function getOrCreateState(options) {
|
|
91
|
+
const key = cacheKey(options.walletRootId, options.serviceIdentity, options.cachePath);
|
|
92
|
+
const existing = indexStates.get(key);
|
|
93
|
+
if (existing !== undefined) {
|
|
94
|
+
return existing;
|
|
95
|
+
}
|
|
96
|
+
const created = {
|
|
97
|
+
walletRootId: options.walletRootId,
|
|
98
|
+
serviceIdentity: options.serviceIdentity,
|
|
99
|
+
cachePath: options.cachePath,
|
|
100
|
+
contexts: new Map(),
|
|
101
|
+
negativeTxids: new Set(),
|
|
102
|
+
loaded: false,
|
|
103
|
+
savePromise: Promise.resolve(),
|
|
104
|
+
};
|
|
105
|
+
indexStates.set(key, created);
|
|
106
|
+
return created;
|
|
107
|
+
}
|
|
108
|
+
async function saveState(state) {
|
|
109
|
+
const payload = {
|
|
110
|
+
schemaVersion: MINING_MEMPOOL_INDEX_SCHEMA_VERSION,
|
|
111
|
+
walletRootId: state.walletRootId,
|
|
112
|
+
serviceIdentity: state.serviceIdentity,
|
|
113
|
+
contexts: [...state.contexts.values()]
|
|
114
|
+
.sort((left, right) => left.txid.localeCompare(right.txid))
|
|
115
|
+
.map(serializeContext),
|
|
116
|
+
negativeTxids: [...state.negativeTxids].sort(),
|
|
117
|
+
};
|
|
118
|
+
state.savePromise = state.savePromise.catch(() => undefined).then(async () => {
|
|
119
|
+
await mkdir(dirname(state.cachePath), { recursive: true });
|
|
120
|
+
await writeJsonFileAtomic(state.cachePath, payload, { mode: 0o600 });
|
|
121
|
+
});
|
|
122
|
+
await state.savePromise;
|
|
123
|
+
}
|
|
124
|
+
function pruneStateToVisibleTxids(state, visibleTxids) {
|
|
125
|
+
const visibleSet = new Set(visibleTxids);
|
|
126
|
+
let changed = false;
|
|
127
|
+
for (const txid of [...state.contexts.keys()]) {
|
|
128
|
+
if (!visibleSet.has(txid)) {
|
|
129
|
+
state.contexts.delete(txid);
|
|
130
|
+
changed = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (const txid of [...state.negativeTxids]) {
|
|
134
|
+
if (!visibleSet.has(txid)) {
|
|
135
|
+
state.negativeTxids.delete(txid);
|
|
136
|
+
changed = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return changed;
|
|
140
|
+
}
|
|
141
|
+
function createContextFromRpcTransaction(txid, tx) {
|
|
142
|
+
const payloadHex = tx.vout.find((entry) => entry.scriptPubKey?.hex?.startsWith("6a") === true)?.scriptPubKey?.hex;
|
|
143
|
+
const payload = payloadHex === undefined ? null : extractOpReturnPayloadFromScriptHex(payloadHex);
|
|
144
|
+
if (!isCogPayload(payload)) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
txid,
|
|
149
|
+
senderScriptHex: tx.vin[0]?.prevout?.scriptPubKey?.hex ?? null,
|
|
150
|
+
inputTxids: tx.vin.map((input) => input.txid).filter((inputTxid) => inputTxid !== undefined),
|
|
151
|
+
payload,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async function maybeYieldDuringIndexHydration(options) {
|
|
155
|
+
const yieldEvery = options.cooperativeYieldEvery ?? MINING_MEMPOOL_INDEX_PROGRESS_REPORT_EVERY;
|
|
156
|
+
if (yieldEvery <= 0 || options.iteration === 0 || (options.iteration % yieldEvery) !== 0) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
await (options.cooperativeYield ?? (() => new Promise((resolve) => {
|
|
160
|
+
setImmediate(resolve);
|
|
161
|
+
})))();
|
|
162
|
+
}
|
|
163
|
+
async function hydrateUnknownTxids(options) {
|
|
164
|
+
if (options.unknownTxids.length === 0) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
let completed = 0;
|
|
168
|
+
let nextIndex = 0;
|
|
169
|
+
let lastReportedProcessed = -1;
|
|
170
|
+
let reportPromise = Promise.resolve();
|
|
171
|
+
const reportProgress = async (processed, force = false) => {
|
|
172
|
+
if (options.onWarmupProgress === undefined) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (!force && processed !== options.unknownTxids.length && (processed % MINING_MEMPOOL_INDEX_PROGRESS_REPORT_EVERY) !== 0) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (processed === lastReportedProcessed) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
lastReportedProcessed = processed;
|
|
182
|
+
reportPromise = reportPromise.then(async () => {
|
|
183
|
+
await options.onWarmupProgress?.({
|
|
184
|
+
processed,
|
|
185
|
+
total: options.unknownTxids.length,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
await reportPromise;
|
|
189
|
+
};
|
|
190
|
+
await reportProgress(0, true);
|
|
191
|
+
let failed = false;
|
|
192
|
+
const workerCount = Math.min(MINING_MEMPOOL_INDEX_RAW_TX_FETCH_CONCURRENCY, options.unknownTxids.length);
|
|
193
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
194
|
+
while (true) {
|
|
195
|
+
const iteration = nextIndex;
|
|
196
|
+
if (iteration >= options.unknownTxids.length) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
nextIndex += 1;
|
|
200
|
+
await maybeYieldDuringIndexHydration({
|
|
201
|
+
iteration,
|
|
202
|
+
cooperativeYield: options.cooperativeYield,
|
|
203
|
+
cooperativeYieldEvery: options.cooperativeYieldEvery,
|
|
204
|
+
});
|
|
205
|
+
options.throwIfStopping?.();
|
|
206
|
+
const txid = options.unknownTxids[iteration];
|
|
207
|
+
const tx = await options.rpc.getRawTransaction(txid, true).catch(() => null);
|
|
208
|
+
options.throwIfStopping?.();
|
|
209
|
+
if (tx === null) {
|
|
210
|
+
failed = true;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const context = createContextFromRpcTransaction(txid, tx);
|
|
214
|
+
if (context === null) {
|
|
215
|
+
options.state.negativeTxids.add(txid);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
options.state.contexts.set(txid, context);
|
|
219
|
+
options.state.negativeTxids.delete(txid);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
completed += 1;
|
|
223
|
+
await reportProgress(completed, completed === options.unknownTxids.length);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
await Promise.all(workers);
|
|
227
|
+
if (failed) {
|
|
228
|
+
throw new Error("mining_mempool_index_hydration_incomplete");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
export async function hydrateMiningMempoolIndex(options) {
|
|
232
|
+
const state = getOrCreateState({
|
|
233
|
+
walletRootId: options.walletRootId,
|
|
234
|
+
serviceIdentity: options.serviceIdentity,
|
|
235
|
+
cachePath: options.cachePath,
|
|
236
|
+
});
|
|
237
|
+
await loadStateFromDisk(state);
|
|
238
|
+
let changed = pruneStateToVisibleTxids(state, options.visibleTxids);
|
|
239
|
+
const unknownTxids = options.visibleTxids.filter((txid) => !state.contexts.has(txid) && !state.negativeTxids.has(txid));
|
|
240
|
+
if (unknownTxids.length > 0) {
|
|
241
|
+
await hydrateUnknownTxids({
|
|
242
|
+
state,
|
|
243
|
+
rpc: options.rpc,
|
|
244
|
+
unknownTxids,
|
|
245
|
+
cooperativeYield: options.cooperativeYield,
|
|
246
|
+
cooperativeYieldEvery: options.cooperativeYieldEvery,
|
|
247
|
+
throwIfStopping: options.throwIfStopping,
|
|
248
|
+
onWarmupProgress: options.onWarmupProgress,
|
|
249
|
+
});
|
|
250
|
+
changed = true;
|
|
251
|
+
}
|
|
252
|
+
if (changed) {
|
|
253
|
+
await saveState(state);
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
contexts: new Map(state.contexts),
|
|
257
|
+
cacheStatus: unknownTxids.length === 0 ? "indexed" : "index-warming",
|
|
258
|
+
hydratedCount: unknownTxids.length,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function readVarInt(bytes, offset) {
|
|
262
|
+
const first = bytes[offset];
|
|
263
|
+
if (first === undefined) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
if (first < 0xfd) {
|
|
267
|
+
return { value: first, nextOffset: offset + 1, raw: bytes.subarray(offset, offset + 1) };
|
|
268
|
+
}
|
|
269
|
+
if (first === 0xfd && (offset + 3) <= bytes.length) {
|
|
270
|
+
return {
|
|
271
|
+
value: bytes.readUInt16LE(offset + 1),
|
|
272
|
+
nextOffset: offset + 3,
|
|
273
|
+
raw: bytes.subarray(offset, offset + 3),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (first === 0xfe && (offset + 5) <= bytes.length) {
|
|
277
|
+
return {
|
|
278
|
+
value: bytes.readUInt32LE(offset + 1),
|
|
279
|
+
nextOffset: offset + 5,
|
|
280
|
+
raw: bytes.subarray(offset, offset + 5),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (first === 0xff && (offset + 9) <= bytes.length) {
|
|
284
|
+
const value = bytes.readBigUInt64LE(offset + 1);
|
|
285
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
value: Number(value),
|
|
290
|
+
nextOffset: offset + 9,
|
|
291
|
+
raw: bytes.subarray(offset, offset + 9),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
function doubleSha256(bytes) {
|
|
297
|
+
return createHash("sha256").update(createHash("sha256").update(bytes).digest()).digest();
|
|
298
|
+
}
|
|
299
|
+
function parseRawTransactionForIndex(rawHex) {
|
|
300
|
+
const bytes = Buffer.from(rawHex, "hex");
|
|
301
|
+
if (bytes.length < 10) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
let offset = 4;
|
|
305
|
+
const txidParts = [bytes.subarray(0, 4)];
|
|
306
|
+
const segwit = bytes[offset] === 0x00 && bytes[offset + 1] !== undefined && bytes[offset + 1] !== 0x00;
|
|
307
|
+
if (segwit) {
|
|
308
|
+
offset += 2;
|
|
309
|
+
}
|
|
310
|
+
const inputCount = readVarInt(bytes, offset);
|
|
311
|
+
if (inputCount === null) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
txidParts.push(inputCount.raw);
|
|
315
|
+
offset = inputCount.nextOffset;
|
|
316
|
+
const inputTxids = [];
|
|
317
|
+
for (let index = 0; index < inputCount.value; index += 1) {
|
|
318
|
+
if ((offset + 36) > bytes.length) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
const prevoutHash = bytes.subarray(offset, offset + 32);
|
|
322
|
+
inputTxids.push(Buffer.from(prevoutHash).reverse().toString("hex"));
|
|
323
|
+
const inputFixed = bytes.subarray(offset, offset + 36);
|
|
324
|
+
txidParts.push(inputFixed);
|
|
325
|
+
offset += 36;
|
|
326
|
+
const scriptLength = readVarInt(bytes, offset);
|
|
327
|
+
if (scriptLength === null || (scriptLength.nextOffset + scriptLength.value + 4) > bytes.length) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
txidParts.push(scriptLength.raw);
|
|
331
|
+
offset = scriptLength.nextOffset;
|
|
332
|
+
txidParts.push(bytes.subarray(offset, offset + scriptLength.value + 4));
|
|
333
|
+
offset += scriptLength.value + 4;
|
|
334
|
+
}
|
|
335
|
+
const outputCount = readVarInt(bytes, offset);
|
|
336
|
+
if (outputCount === null) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
txidParts.push(outputCount.raw);
|
|
340
|
+
offset = outputCount.nextOffset;
|
|
341
|
+
let payload = null;
|
|
342
|
+
for (let index = 0; index < outputCount.value; index += 1) {
|
|
343
|
+
if ((offset + 8) > bytes.length) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
txidParts.push(bytes.subarray(offset, offset + 8));
|
|
347
|
+
offset += 8;
|
|
348
|
+
const scriptLength = readVarInt(bytes, offset);
|
|
349
|
+
if (scriptLength === null || (scriptLength.nextOffset + scriptLength.value) > bytes.length) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
txidParts.push(scriptLength.raw);
|
|
353
|
+
offset = scriptLength.nextOffset;
|
|
354
|
+
const script = bytes.subarray(offset, offset + scriptLength.value);
|
|
355
|
+
txidParts.push(script);
|
|
356
|
+
payload ??= extractOpReturnPayloadFromScriptHex(script.toString("hex"));
|
|
357
|
+
offset += scriptLength.value;
|
|
358
|
+
}
|
|
359
|
+
if (segwit) {
|
|
360
|
+
for (let index = 0; index < inputCount.value; index += 1) {
|
|
361
|
+
const witnessCount = readVarInt(bytes, offset);
|
|
362
|
+
if (witnessCount === null) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
offset = witnessCount.nextOffset;
|
|
366
|
+
for (let witnessIndex = 0; witnessIndex < witnessCount.value; witnessIndex += 1) {
|
|
367
|
+
const witnessLength = readVarInt(bytes, offset);
|
|
368
|
+
if (witnessLength === null || (witnessLength.nextOffset + witnessLength.value) > bytes.length) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
offset = witnessLength.nextOffset + witnessLength.value;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if ((offset + 4) !== bytes.length) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
txidParts.push(bytes.subarray(offset, offset + 4));
|
|
379
|
+
return {
|
|
380
|
+
txid: Buffer.from(doubleSha256(Buffer.concat(txidParts))).reverse().toString("hex"),
|
|
381
|
+
inputTxids,
|
|
382
|
+
payload,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function normalizeZmqFrames(frames) {
|
|
386
|
+
if (Array.isArray(frames)) {
|
|
387
|
+
return frames.filter(Buffer.isBuffer);
|
|
388
|
+
}
|
|
389
|
+
return Buffer.isBuffer(frames) ? [frames] : [];
|
|
390
|
+
}
|
|
391
|
+
async function loadZeroMqModule() {
|
|
392
|
+
return await import("zeromq");
|
|
393
|
+
}
|
|
394
|
+
export async function ensureMiningMempoolRawTxSubscriber(options) {
|
|
395
|
+
if (options.rawTxTopic !== "rawtx") {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
const key = cacheKey(options.walletRootId, options.serviceIdentity, options.cachePath);
|
|
399
|
+
if (rawTxSubscribers.has(key)) {
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
const state = getOrCreateState({
|
|
403
|
+
walletRootId: options.walletRootId,
|
|
404
|
+
serviceIdentity: options.serviceIdentity,
|
|
405
|
+
cachePath: options.cachePath,
|
|
406
|
+
});
|
|
407
|
+
await loadStateFromDisk(state);
|
|
408
|
+
let subscriber;
|
|
409
|
+
try {
|
|
410
|
+
const zeroMq = options.loadZeroMq === undefined ? await loadZeroMqModule() : await options.loadZeroMq();
|
|
411
|
+
subscriber = new zeroMq.Subscriber();
|
|
412
|
+
subscriber.connect(options.zmqEndpoint);
|
|
413
|
+
subscriber.subscribe(options.rawTxTopic);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
const loop = (async () => {
|
|
419
|
+
try {
|
|
420
|
+
for await (const frames of subscriber) {
|
|
421
|
+
const normalized = normalizeZmqFrames(frames);
|
|
422
|
+
if (normalized.length < 2 || normalized[0]?.toString() !== options.rawTxTopic) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const parsed = parseRawTransactionForIndex(normalized[1].toString("hex"));
|
|
426
|
+
if (parsed === null || isCogPayload(parsed.payload)) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
state.negativeTxids.add(parsed.txid);
|
|
430
|
+
void saveState(state).catch(() => undefined);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
// The index remains conservative; the gate hydrates unknown txids by RPC.
|
|
435
|
+
}
|
|
436
|
+
})();
|
|
437
|
+
rawTxSubscribers.set(key, { key, subscriber, loop });
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
export async function closeMiningMempoolIndexSubscribersForTesting() {
|
|
441
|
+
const subscribers = [...rawTxSubscribers.values()];
|
|
442
|
+
rawTxSubscribers.clear();
|
|
443
|
+
for (const state of subscribers) {
|
|
444
|
+
state.subscriber.close();
|
|
445
|
+
await state.loop.catch(() => undefined);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
export function clearMiningMempoolIndexCacheForTesting() {
|
|
449
|
+
indexStates.clear();
|
|
450
|
+
}
|
|
451
|
+
export const parseRawTransactionForMiningMempoolIndexTesting = parseRawTransactionForIndex;
|
|
@@ -36,6 +36,7 @@ import { createMiningSentenceRequestLimits } from "./sentence-protocol.js";
|
|
|
36
36
|
import { generateMiningSentences, MiningProviderRequestError } from "./sentences.js";
|
|
37
37
|
import { MiningFollowVisualizer, } from "./visualizer.js";
|
|
38
38
|
import { createIndexedMiningFollowVisualizerState, findRecentMiningWin, loadMiningVisibleFollowBlockTimes, resolveFundingDisplaySats, resolveSettledBoard, syncMiningUiForCurrentTip, syncMiningVisualizerBalances, syncMiningVisualizerBlockTimes, } from "./visualizer-sync.js";
|
|
39
|
+
import { ensureMiningMempoolRawTxSubscriber, resolveMiningMempoolIndexCachePath, resolveMiningMempoolServiceIdentity, } from "./mempool-index.js";
|
|
39
40
|
const BEST_BLOCK_POLL_INTERVAL_MS = 500;
|
|
40
41
|
const MINING_SUSPEND_HEARTBEAT_INTERVAL_MS = 1_000;
|
|
41
42
|
class MiningSuspendDetectedError extends Error {
|
|
@@ -237,6 +238,24 @@ async function performMiningCycle(options) {
|
|
|
237
238
|
throwIfStopping();
|
|
238
239
|
throwIfMiningSuspendDetected(options.suspendDetector);
|
|
239
240
|
const rpc = options.rpcFactory(service.rpc);
|
|
241
|
+
const serviceZmq = service.zmq;
|
|
242
|
+
const mempoolIndexCachePath = resolveMiningMempoolIndexCachePath(options.paths);
|
|
243
|
+
const mempoolIndexServiceIdentity = resolveMiningMempoolServiceIdentity({
|
|
244
|
+
dataDir: service.dataDir ?? options.dataDir,
|
|
245
|
+
pid: service.pid,
|
|
246
|
+
zmqEndpoint: serviceZmq?.endpoint ?? "unknown-zmq-endpoint",
|
|
247
|
+
rawTxTopic: serviceZmq?.rawTxTopic,
|
|
248
|
+
});
|
|
249
|
+
const mempoolIndexRawTxSupported = serviceZmq?.rawTxTopic === "rawtx";
|
|
250
|
+
if (mempoolIndexRawTxSupported && serviceZmq?.endpoint !== undefined) {
|
|
251
|
+
await ensureMiningMempoolRawTxSubscriber({
|
|
252
|
+
walletRootId: readContext.localState.state.walletRootId,
|
|
253
|
+
serviceIdentity: mempoolIndexServiceIdentity,
|
|
254
|
+
cachePath: mempoolIndexCachePath,
|
|
255
|
+
zmqEndpoint: serviceZmq.endpoint,
|
|
256
|
+
rawTxTopic: serviceZmq.rawTxTopic,
|
|
257
|
+
}).catch(() => false);
|
|
258
|
+
}
|
|
240
259
|
const reconciliation = await reconcileLiveMiningState({
|
|
241
260
|
state: readContext.localState.state,
|
|
242
261
|
rpc,
|
|
@@ -437,6 +456,11 @@ async function performMiningCycle(options) {
|
|
|
437
456
|
assaySentencesImpl: options.assaySentencesImpl,
|
|
438
457
|
cooperativeYieldImpl: options.cooperativeYieldImpl,
|
|
439
458
|
cooperativeYieldEvery: options.cooperativeYieldEvery,
|
|
459
|
+
mempoolIndex: {
|
|
460
|
+
rawTxSupported: mempoolIndexRawTxSupported,
|
|
461
|
+
cachePath: mempoolIndexCachePath,
|
|
462
|
+
serviceIdentity: mempoolIndexServiceIdentity,
|
|
463
|
+
},
|
|
440
464
|
nowImpl: now,
|
|
441
465
|
saveCycleStatus: async (context, overrides) => await saveCycleStatus(context, overrides),
|
|
442
466
|
appendEvent: async (event) => await appendEvent(options.paths, event),
|
|
@@ -109,7 +109,7 @@ export interface MiningRuntimeStatusV1 {
|
|
|
109
109
|
higherRankedCompetitorDomainCount: number | null;
|
|
110
110
|
dedupedCompetitorDomainCount: number | null;
|
|
111
111
|
competitivenessGateIndeterminate: boolean | null;
|
|
112
|
-
mempoolSequenceCacheStatus: "reused" | "refreshed" | null;
|
|
112
|
+
mempoolSequenceCacheStatus: "reused" | "refreshed" | "indexed" | "index-warming" | "fallback-scan" | null;
|
|
113
113
|
currentPublishDecision: string | null;
|
|
114
114
|
lastMempoolSequence: string | null;
|
|
115
115
|
lastCompetitivenessGateAtUnixMs: number | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cogcoin/client",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Store-backed Cogcoin client with wallet flows, SQLite persistence, and managed Bitcoin Core integration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@cogcoin/bitcoin": "30.2.0",
|
|
67
67
|
"@cogcoin/genesis": "1.0.0",
|
|
68
|
-
"@cogcoin/indexer": "1.0.
|
|
68
|
+
"@cogcoin/indexer": "1.0.2",
|
|
69
69
|
"@cogcoin/scoring": "1.0.0",
|
|
70
70
|
"@noble/hashes": "2.0.1",
|
|
71
71
|
"@scure/base": "^2.0.0",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"zeromq": "6.5.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
-
"@cogcoin/vectors": "1.0.
|
|
78
|
+
"@cogcoin/vectors": "1.0.1",
|
|
79
79
|
"@types/node": "^25.5.0",
|
|
80
80
|
"typescript": "^6.0.2"
|
|
81
81
|
}
|