@bsv/overlay 2.0.3 → 2.0.4

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 (86) hide show
  1. package/README.md +226 -106
  2. package/dist/cjs/mod.js +6 -1
  3. package/dist/cjs/mod.js.map +1 -1
  4. package/dist/cjs/package.json +13 -11
  5. package/dist/cjs/src/BASM.js +92 -0
  6. package/dist/cjs/src/BASM.js.map +1 -0
  7. package/dist/cjs/src/BASMRemote.js +43 -0
  8. package/dist/cjs/src/BASMRemote.js.map +1 -0
  9. package/dist/cjs/src/Engine.js +649 -47
  10. package/dist/cjs/src/Engine.js.map +1 -1
  11. package/dist/cjs/src/GASP/OverlayGASPRemote.js +2 -2
  12. package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
  13. package/dist/cjs/src/GASP/OverlayGASPStorage.js +18 -25
  14. package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
  15. package/dist/cjs/src/storage/knex/KnexStorage.js +329 -12
  16. package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
  17. package/dist/cjs/src/storage/knex/all-migrations.js +3 -1
  18. package/dist/cjs/src/storage/knex/all-migrations.js.map +1 -1
  19. package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.js +2 -24
  20. package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.js.map +1 -1
  21. package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js +114 -0
  22. package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js.map +1 -0
  23. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  24. package/dist/esm/mod.js +1 -0
  25. package/dist/esm/mod.js.map +1 -1
  26. package/dist/esm/src/BASM.js +85 -0
  27. package/dist/esm/src/BASM.js.map +1 -0
  28. package/dist/esm/src/BASMRemote.js +42 -0
  29. package/dist/esm/src/BASMRemote.js.map +1 -0
  30. package/dist/esm/src/Engine.js +637 -46
  31. package/dist/esm/src/Engine.js.map +1 -1
  32. package/dist/esm/src/GASP/OverlayGASPRemote.js +2 -2
  33. package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
  34. package/dist/esm/src/GASP/OverlayGASPStorage.js +18 -25
  35. package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
  36. package/dist/esm/src/storage/knex/KnexStorage.js +323 -12
  37. package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
  38. package/dist/esm/src/storage/knex/all-migrations.js +3 -1
  39. package/dist/esm/src/storage/knex/all-migrations.js.map +1 -1
  40. package/dist/esm/src/storage/knex/migrations/2025-05-28-001-enlarge.js +2 -24
  41. package/dist/esm/src/storage/knex/migrations/2025-05-28-001-enlarge.js.map +1 -1
  42. package/dist/esm/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js +109 -0
  43. package/dist/esm/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js.map +1 -0
  44. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  45. package/dist/types/mod.d.ts +2 -0
  46. package/dist/types/mod.d.ts.map +1 -1
  47. package/dist/types/src/BASM.d.ts +110 -0
  48. package/dist/types/src/BASM.d.ts.map +1 -0
  49. package/dist/types/src/BASMRemote.d.ts +14 -0
  50. package/dist/types/src/BASMRemote.d.ts.map +1 -0
  51. package/dist/types/src/Engine.d.ts +78 -1
  52. package/dist/types/src/Engine.d.ts.map +1 -1
  53. package/dist/types/src/GASP/OverlayGASPStorage.d.ts +1 -1
  54. package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
  55. package/dist/types/src/LookupService.d.ts.map +1 -1
  56. package/dist/types/src/storage/Storage.d.ts +118 -0
  57. package/dist/types/src/storage/Storage.d.ts.map +1 -1
  58. package/dist/types/src/storage/knex/KnexStorage.d.ts +38 -6
  59. package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
  60. package/dist/types/src/storage/knex/all-migrations.d.ts.map +1 -1
  61. package/dist/types/src/storage/knex/migrations/2025-05-28-001-enlarge.d.ts.map +1 -1
  62. package/dist/types/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts +4 -0
  63. package/dist/types/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts.map +1 -0
  64. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  65. package/docs/API.md +72 -580
  66. package/docs/BRC-136-BASM.md +285 -0
  67. package/docs/README.md +2 -0
  68. package/docs/Synchronization.md +10 -1
  69. package/docs/examples/README.md +2 -2
  70. package/docs/examples/gs-wip.md +68 -76
  71. package/mod.ts +25 -1
  72. package/package.json +25 -23
  73. package/src/BASM.ts +208 -0
  74. package/src/BASMRemote.ts +54 -0
  75. package/src/Engine.ts +762 -47
  76. package/src/GASP/OverlayGASPRemote.ts +2 -2
  77. package/src/GASP/OverlayGASPStorage.ts +17 -22
  78. package/src/LookupService.ts +2 -1
  79. package/src/__tests/BASM.test.ts +44 -0
  80. package/src/__tests/BASMChain.test.ts +290 -0
  81. package/src/__tests/Engine.test.ts +3 -4
  82. package/src/storage/Storage.ts +127 -0
  83. package/src/storage/knex/KnexStorage.ts +376 -14
  84. package/src/storage/knex/all-migrations.ts +3 -1
  85. package/src/storage/knex/migrations/2025-05-28-001-enlarge.ts +2 -22
  86. package/src/storage/knex/migrations/2026-05-29-001-brc136-basm.ts +123 -0
@@ -5,7 +5,10 @@ const sdk_1 = require("@bsv/sdk");
5
5
  const gasp_1 = require("@bsv/gasp");
6
6
  const OverlayGASPRemote_js_1 = require("./GASP/OverlayGASPRemote.js");
7
7
  const OverlayGASPStorage_js_1 = require("./GASP/OverlayGASPStorage.js");
8
+ const BASM_js_1 = require("./BASM.js");
9
+ const BASMRemote_js_1 = require("./BASMRemote.js");
8
10
  const DEFAULT_GASP_SYNC_LIMIT = 10000;
11
+ const DEFAULT_BASM_RANGE_LIMIT = 1024;
9
12
  /**
10
13
  * An engine for running BSV Overlay Services (topic managers and lookup services).
11
14
  */
@@ -28,8 +31,13 @@ class Engine {
28
31
  * @param {OverlayBroadcastFacilitator} overlayBroadcastFacilitator - Facilitator for propagation to other Overlay Services.
29
32
  * @param {typeof console} logger - The place where log entries are written.
30
33
  * @param {boolean} suppressDefaultSyncAdvertisements - Whether to suppress the default (SHIP/SLAP) sync advertisements.
34
+ * @param {TopicAnchorHeaderResolver} topicAnchorHeaderResolver - Resolves block hashes for BASM anchors.
35
+ * @param {boolean} basmSyncEnabled - Whether BASM sync should run automatically.
36
+ * @param {number} unprovenEvictionBlocks - Default block age for opt-in unproven state eviction.
31
37
  */
32
- constructor(managers, lookupServices, storage, chainTracker, hostingURL, shipTrackers, slapTrackers, broadcaster, advertiser, syncConfiguration, logTime = false, logPrefix = '[OVERLAY_ENGINE] ', throwOnBroadcastFailure = false, overlayBroadcastFacilitator = new sdk_1.HTTPSOverlayBroadcastFacilitator(), logger = console, suppressDefaultSyncAdvertisements = true) {
38
+ constructor(managers, lookupServices, storage, chainTracker, hostingURL, shipTrackers, slapTrackers, broadcaster, advertiser, syncConfiguration, logTime = false, logPrefix = '[OVERLAY_ENGINE] ', throwOnBroadcastFailure = false, overlayBroadcastFacilitator = new sdk_1.HTTPSOverlayBroadcastFacilitator(), logger = console, suppressDefaultSyncAdvertisements = true, topicAnchorHeaderResolver, basmSyncEnabled = false, unprovenEvictionBlocks = 144) {
39
+ var _a;
40
+ var _b;
33
41
  this.managers = managers;
34
42
  this.lookupServices = lookupServices;
35
43
  this.storage = storage;
@@ -46,6 +54,9 @@ class Engine {
46
54
  this.overlayBroadcastFacilitator = overlayBroadcastFacilitator;
47
55
  this.logger = logger;
48
56
  this.suppressDefaultSyncAdvertisements = suppressDefaultSyncAdvertisements;
57
+ this.topicAnchorHeaderResolver = topicAnchorHeaderResolver;
58
+ this.basmSyncEnabled = basmSyncEnabled;
59
+ this.unprovenEvictionBlocks = unprovenEvictionBlocks;
49
60
  // To encourage synchronization of overlay services, the SHIP sync strategy is used by default for all overlay topics, except for 'tm_ship' and 'tm_slap'.
50
61
  // For these two topics, any existing trackers are combined with the provided shipTrackers and slapTrackers omitting any duplicates.
51
62
  if (syncConfiguration === undefined) {
@@ -73,9 +84,7 @@ class Engine {
73
84
  }
74
85
  else {
75
86
  // Set undefined managers to 'SHIP' by default
76
- if (this.syncConfiguration[managerName] === undefined) {
77
- this.syncConfiguration[managerName] = 'SHIP';
78
- }
87
+ (_a = (_b = this.syncConfiguration)[managerName]) !== null && _a !== void 0 ? _a : (_b[managerName] = 'SHIP');
79
88
  }
80
89
  }
81
90
  }
@@ -90,6 +99,282 @@ class Engine {
90
99
  this.logger.timeEnd(`${this.logPrefix} ${label}`);
91
100
  }
92
101
  }
102
+ async currentHeightOrUndefined() {
103
+ if (this.chainTracker === 'scripts only') {
104
+ return undefined;
105
+ }
106
+ try {
107
+ return await this.chainTracker.currentHeight();
108
+ }
109
+ catch (error) {
110
+ this.logger.warn(`Unable to resolve current chain height for overlay metadata: ${error instanceof Error ? error.message : String(error)}`);
111
+ return undefined;
112
+ }
113
+ }
114
+ async resolveBlockHash(blockHeight, merkleRoot) {
115
+ var _a;
116
+ try {
117
+ const header = await ((_a = this.topicAnchorHeaderResolver) === null || _a === void 0 ? void 0 : _a.call(this, blockHeight));
118
+ if (header === undefined) {
119
+ return undefined;
120
+ }
121
+ if (header.merkleRoot !== undefined && merkleRoot !== undefined && header.merkleRoot !== merkleRoot) {
122
+ throw new Error(`Header merkle root ${header.merkleRoot} does not match proof root ${merkleRoot} at height ${blockHeight}`);
123
+ }
124
+ return header.blockHash;
125
+ }
126
+ catch (error) {
127
+ this.logger.warn(`Unable to resolve BASM block hash for height ${blockHeight}: ${error instanceof Error ? error.message : String(error)}`);
128
+ return undefined;
129
+ }
130
+ }
131
+ compactBEEFForStorage(tx, originalBEEF) {
132
+ return tx.merklePath === undefined ? originalBEEF : tx.toAtomicBEEF();
133
+ }
134
+ async recordTransactionData(tx, beef, blockHash) {
135
+ var _a;
136
+ if (typeof this.storage.upsertTransactionRecord !== 'function') {
137
+ return;
138
+ }
139
+ const txid = tx.id('hex');
140
+ const metadata = (0, BASM_js_1.extractMerkleProofMetadata)(txid, tx.merklePath);
141
+ await this.storage.upsertTransactionRecord({
142
+ txid,
143
+ beef: this.compactBEEFForStorage(tx, beef),
144
+ rawTx: Array.from(tx.toBinary()),
145
+ merklePath: (_a = tx.merklePath) === null || _a === void 0 ? void 0 : _a.toBinary(),
146
+ blockHeight: metadata === null || metadata === void 0 ? void 0 : metadata.blockHeight,
147
+ blockHash,
148
+ blockIndex: metadata === null || metadata === void 0 ? void 0 : metadata.blockIndex,
149
+ merkleRoot: metadata === null || metadata === void 0 ? void 0 : metadata.merkleRoot
150
+ });
151
+ }
152
+ async buildAppliedTransactionRecord(tx) {
153
+ const txid = tx.id('hex');
154
+ const metadata = (0, BASM_js_1.extractMerkleProofMetadata)(txid, tx.merklePath);
155
+ const [firstSeenHeight, blockHash] = await Promise.all([
156
+ this.currentHeightOrUndefined(),
157
+ metadata === undefined ? undefined : this.resolveBlockHash(metadata.blockHeight, metadata.merkleRoot)
158
+ ]);
159
+ return {
160
+ blockHeight: metadata === null || metadata === void 0 ? void 0 : metadata.blockHeight,
161
+ blockHash,
162
+ blockIndex: metadata === null || metadata === void 0 ? void 0 : metadata.blockIndex,
163
+ merkleRoot: metadata === null || metadata === void 0 ? void 0 : metadata.merkleRoot,
164
+ firstSeenHeight: firstSeenHeight !== null && firstSeenHeight !== void 0 ? firstSeenHeight : metadata === null || metadata === void 0 ? void 0 : metadata.blockHeight,
165
+ proven: metadata !== undefined
166
+ };
167
+ }
168
+ async recomputeTopicBlockAnchor(topic, blockHeight, blockHash) {
169
+ var _a, _b, _c;
170
+ if (typeof this.storage.findAdmittedTransactionsForBlock !== 'function' ||
171
+ typeof this.storage.upsertTopicBlockAnchor !== 'function' ||
172
+ typeof this.storage.findTopicBlockAnchor !== 'function') {
173
+ return undefined;
174
+ }
175
+ const anchorBlockHash = blockHash !== null && blockHash !== void 0 ? blockHash : (_a = (await this.storage.findTopicBlockAnchor(topic, blockHeight))) === null || _a === void 0 ? void 0 : _a.blockHash;
176
+ if (anchorBlockHash === undefined) {
177
+ return undefined;
178
+ }
179
+ // BRC-136 per-block completeness: establish the chain's genesis at the first
180
+ // admitted height, then keep every height from there to the tip contiguous so
181
+ // the cumulative TAC never resets across blocks with no admitted transactions.
182
+ // We rebuild [fromHeight, toHeight] rather than only the touched height so that
183
+ // an out-of-order proof (older height arriving after a newer one) can never
184
+ // leave a gap that silently breaks the chain.
185
+ const tip = await ((_c = (_b = this.storage).findTopicAnchorTip) === null || _c === void 0 ? void 0 : _c.call(_b, topic));
186
+ const tipHeight = tip !== undefined && tip.blockHeight >= 0 ? tip.blockHeight : undefined;
187
+ const fromHeight = tipHeight === undefined ? blockHeight : Math.min(blockHeight, tipHeight + 1);
188
+ const toHeight = tipHeight === undefined ? blockHeight : Math.max(blockHeight, tipHeight);
189
+ await this.rebuildTopicAnchorChain(topic, fromHeight, toHeight, new Map([[blockHeight, anchorBlockHash]]));
190
+ return await this.storage.findTopicBlockAnchor(topic, blockHeight);
191
+ }
192
+ /**
193
+ * Extends every configured topic's anchor chain forward with empty Topic Block
194
+ * Anchors (basmRoot = zero hash, admittedCount = 0) up to `toHeight`, so the
195
+ * cumulative TAC advances on every block even when a topic admits nothing —
196
+ * this is what lets a peer authoritatively confirm "this block contained no
197
+ * transactions for this topic". Chains with no first admission yet are left
198
+ * unstarted (genesis is the topic's first admitted height).
199
+ */
200
+ async advanceTopicAnchorChains(toHeight) {
201
+ if (typeof this.storage.findTopicAnchorTip !== 'function' ||
202
+ typeof this.storage.upsertTopicBlockAnchor !== 'function') {
203
+ return;
204
+ }
205
+ const targetHeight = toHeight !== null && toHeight !== void 0 ? toHeight : await this.currentHeightOrUndefined();
206
+ if (targetHeight === undefined) {
207
+ return;
208
+ }
209
+ for (const topic of Object.keys(this.managers)) {
210
+ const tip = await this.storage.findTopicAnchorTip(topic);
211
+ if (tip === undefined || tip.blockHeight < 0 || tip.blockHeight >= targetHeight) {
212
+ continue;
213
+ }
214
+ await this.rebuildTopicAnchorChain(topic, tip.blockHeight + 1, targetHeight);
215
+ }
216
+ }
217
+ /**
218
+ * Rebuilds a contiguous slice of a topic's anchor chain over [fromHeight,
219
+ * toHeight]. Each height uses its admitted transactions (empty -> zero basmRoot)
220
+ * and chains the cumulative TAC from the prior height. Missing heights are
221
+ * filled rather than skipped, so the chain stays gap-free. If a block hash
222
+ * cannot be resolved for some height the extension halts there to preserve
223
+ * contiguity instead of leaving a hole.
224
+ */
225
+ async rebuildTopicAnchorChain(topic, fromHeight, toHeight, blockHashHints = new Map(), forceResolve = false) {
226
+ var _a, _b, _c;
227
+ if (typeof this.storage.findAdmittedTransactionsForBlock !== 'function' ||
228
+ typeof this.storage.upsertTopicBlockAnchor !== 'function' ||
229
+ typeof this.storage.findTopicBlockAnchor !== 'function' ||
230
+ toHeight < fromHeight) {
231
+ return;
232
+ }
233
+ if (toHeight - fromHeight + 1 > DEFAULT_BASM_RANGE_LIMIT) {
234
+ // Bound the work per pass; the next trigger resumes from the new tip.
235
+ this.logger.warn(`[BASM] capping anchor chain extension for "${topic}" at ${DEFAULT_BASM_RANGE_LIMIT} blocks (requested ${fromHeight}..${toHeight}); will continue on the next pass`);
236
+ toHeight = fromHeight + DEFAULT_BASM_RANGE_LIMIT - 1;
237
+ }
238
+ const previousAnchor = fromHeight > 0
239
+ ? await this.storage.findTopicBlockAnchor(topic, fromHeight - 1)
240
+ : undefined;
241
+ let prevTac = (_a = previousAnchor === null || previousAnchor === void 0 ? void 0 : previousAnchor.tac) !== null && _a !== void 0 ? _a : BASM_js_1.BASM_ZERO_HASH;
242
+ for (let height = fromHeight; height <= toHeight; height++) {
243
+ const admitted = await this.storage.findAdmittedTransactionsForBlock(topic, height);
244
+ const existing = await this.storage.findTopicBlockAnchor(topic, height);
245
+ // On a reorg rebuild the existing anchor's block hash is stale, so force
246
+ // canonical re-resolution from the header resolver instead of reusing it.
247
+ const blockHash = (_c = (_b = blockHashHints.get(height)) !== null && _b !== void 0 ? _b : (forceResolve ? undefined : existing === null || existing === void 0 ? void 0 : existing.blockHash)) !== null && _c !== void 0 ? _c : await this.resolveBlockHash(height);
248
+ if (blockHash === undefined) {
249
+ this.logger.warn(`[BASM] unable to resolve block hash for "${topic}" at height ${height}; halting chain extension`);
250
+ return;
251
+ }
252
+ const basmRoot = (0, BASM_js_1.computeBasmRoot)(admitted);
253
+ const tac = (0, BASM_js_1.computeTac)(prevTac, blockHash, basmRoot);
254
+ await this.storage.upsertTopicBlockAnchor({
255
+ topic,
256
+ blockHeight: height,
257
+ blockHash,
258
+ basmRoot,
259
+ admittedCount: admitted.length,
260
+ tac
261
+ });
262
+ prevTac = tac;
263
+ }
264
+ }
265
+ /**
266
+ * Reconciles BASM anchors with a blockchain reorganization reported by the
267
+ * chain tracker (e.g. go-chaintracks `/v2/reorg/stream`). Proven topic
268
+ * transactions whose block was orphaned are demoted to unproven so they leave
269
+ * the admitted set, then every topic anchor chain intersecting the affected
270
+ * height range is rebuilt over the canonical block hashes. A reorg changes the
271
+ * canonical block hash for the affected heights, so topics with no demoted
272
+ * transaction are rebuilt too. Idempotent: a clean window demotes nothing and
273
+ * reproduces an identical TAC, so this is safe to invoke on every reorg event,
274
+ * SSE reconnect, and poll.
275
+ */
276
+ async handleReorg(input) {
277
+ var _a, _b;
278
+ const report = { perTopic: [] };
279
+ if (typeof this.storage.findProvenAppliedTransactionsByBlockHash !== 'function' ||
280
+ typeof this.storage.demoteAppliedTransactionToUnproven !== 'function' ||
281
+ typeof this.storage.findTopicBlockAnchors !== 'function' ||
282
+ typeof this.storage.upsertTopicBlockAnchor !== 'function') {
283
+ return report;
284
+ }
285
+ // 1) Demote proven admissions whose block was orphaned. Hashes are
286
+ // normalized to lower-case display hex to match stored block hashes
287
+ // (go-sdk chainhash.Hash marshals as reversed display hex).
288
+ const demotedByTopic = new Map();
289
+ for (const rawHash of input.orphanedBlockHashes) {
290
+ const blockHash = rawHash.toLowerCase();
291
+ const rows = await this.storage.findProvenAppliedTransactionsByBlockHash(blockHash);
292
+ for (const row of rows) {
293
+ await this.storage.demoteAppliedTransactionToUnproven(row.txid, row.topic);
294
+ const list = (_a = demotedByTopic.get(row.topic)) !== null && _a !== void 0 ? _a : [];
295
+ list.push(row.txid);
296
+ demotedByTopic.set(row.topic, list);
297
+ }
298
+ }
299
+ // 2) Rebuild every topic anchor chain that intersects the reorged range,
300
+ // forcing canonical block-hash re-resolution so stale hashes are replaced.
301
+ for (const topic of Object.keys(this.managers)) {
302
+ const existing = await this.storage.findTopicBlockAnchors(topic, input.rebuildFromHeight, input.newTipHeight);
303
+ if (existing.length === 0) {
304
+ continue;
305
+ }
306
+ const startHeight = Math.min(...existing.map(anchor => anchor.blockHeight));
307
+ await this.rebuildTopicAnchorChain(topic, startHeight, input.newTipHeight, new Map(), true);
308
+ report.perTopic.push({
309
+ topic,
310
+ demotedTxids: (_b = demotedByTopic.get(topic)) !== null && _b !== void 0 ? _b : [],
311
+ rebuiltFrom: startHeight,
312
+ rebuiltTo: input.newTipHeight
313
+ });
314
+ }
315
+ return report;
316
+ }
317
+ /**
318
+ * Revalidation sweep: the reorg fallback for chain trackers without a reorg
319
+ * event stream, and the catch-up step on every reorg-SSE (re)connect (the
320
+ * go-chaintracks reorg stream carries no event ids, so a reconnect cannot
321
+ * replay events missed while disconnected). Scans proven applied transactions
322
+ * in `[tip - depth + 1, tip]`; any whose proof root no longer validates against
323
+ * the chain tracker, or whose block hash diverges from the canonical header, is
324
+ * treated as orphaned and reconciled via {@link handleReorg}.
325
+ */
326
+ async isProvenAnchorStale(row, chainTracker) {
327
+ var _a;
328
+ if (row.merkleRoot !== undefined) {
329
+ try {
330
+ if (!(await chainTracker.isValidRootForHeight(row.merkleRoot, row.blockHeight))) {
331
+ return true;
332
+ }
333
+ }
334
+ catch (error) {
335
+ this.logger.warn(`[BASM] root validation failed for ${row.txid} at height ${row.blockHeight}: ${error instanceof Error ? error.message : String(error)}`);
336
+ return undefined;
337
+ }
338
+ }
339
+ const canonical = await this.resolveBlockHash(row.blockHeight);
340
+ return canonical !== undefined && canonical.toLowerCase() !== ((_a = row.blockHash) === null || _a === void 0 ? void 0 : _a.toLowerCase());
341
+ }
342
+ async revalidateRecentAnchors(depth = 3) {
343
+ const chainTracker = this.chainTracker;
344
+ if (chainTracker === 'scripts only') {
345
+ this.logger.warn('[BASM] revalidation sweep requires a ChainTracker; skipping');
346
+ return undefined;
347
+ }
348
+ if (typeof this.storage.findProvenAppliedTransactionsInRange !== 'function') {
349
+ return undefined;
350
+ }
351
+ const tip = await this.currentHeightOrUndefined();
352
+ if (tip === undefined) {
353
+ return undefined;
354
+ }
355
+ const fromHeight = Math.max(0, tip - depth + 1);
356
+ const rows = await this.storage.findProvenAppliedTransactionsInRange(fromHeight, tip);
357
+ const orphaned = new Set();
358
+ let minAffected = Number.POSITIVE_INFINITY;
359
+ for (const row of rows) {
360
+ if (row.blockHash === undefined) {
361
+ continue;
362
+ }
363
+ const stale = await this.isProvenAnchorStale(row, chainTracker);
364
+ if (stale === true) {
365
+ orphaned.add(row.blockHash.toLowerCase());
366
+ minAffected = Math.min(minAffected, row.blockHeight);
367
+ }
368
+ }
369
+ if (orphaned.size === 0) {
370
+ return { perTopic: [] };
371
+ }
372
+ return await this.handleReorg({
373
+ orphanedBlockHashes: Array.from(orphaned),
374
+ rebuildFromHeight: minAffected,
375
+ newTipHeight: tip
376
+ });
377
+ }
93
378
  /**
94
379
  * Submits a transaction for processing by Overlay Services.
95
380
  * @param {TaggedBEEF} taggedBEEF - The transaction to process
@@ -144,8 +429,8 @@ class Engine {
144
429
  // Identify previous coins admitted to this specific topic
145
430
  const previousCoins = [];
146
431
  const outputPromises = tx.inputs.map(async (input, i) => {
147
- var _a;
148
- const previousTXID = input.sourceTXID !== undefined ? input.sourceTXID : (_a = input.sourceTransaction) === null || _a === void 0 ? void 0 : _a.id('hex');
432
+ var _a, _b;
433
+ const previousTXID = (_a = input.sourceTXID) !== null && _a !== void 0 ? _a : (_b = input.sourceTransaction) === null || _b === void 0 ? void 0 : _b.id('hex');
149
434
  if (previousTXID !== undefined) {
150
435
  // Check if the previous output was admitted to this specific topic
151
436
  const output = await this.storage.findOutput(previousTXID, input.sourceOutputIndex, topic);
@@ -334,17 +619,17 @@ class Engine {
334
619
  if (typeof previousTXID !== 'string')
335
620
  continue;
336
621
  const previousOutputIndex = tx.inputs[inputIndex].sourceOutputIndex;
337
- if (!admissibleOutputs.coinsToRetain.includes(inputIndex)) {
338
- outputsToMarkStale.push({
622
+ if (admissibleOutputs.coinsToRetain.includes(inputIndex)) {
623
+ outputsConsumed.push({
339
624
  txid: previousTXID,
340
- previousOutputIndex,
341
- inputIndex
625
+ outputIndex: previousOutputIndex
342
626
  });
343
627
  }
344
628
  else {
345
- outputsConsumed.push({
629
+ outputsToMarkStale.push({
346
630
  txid: previousTXID,
347
- outputIndex: previousOutputIndex
631
+ previousOutputIndex,
632
+ inputIndex
348
633
  });
349
634
  }
350
635
  }
@@ -362,6 +647,7 @@ class Engine {
362
647
  // Handle admittance and notification of incoming UTXOs
363
648
  const newUTXOs = [];
364
649
  await Promise.all(outputsToAdmit.map(async (outputIndex) => {
650
+ var _a;
365
651
  if (typeof tx.outputs[outputIndex].satoshis !== 'number')
366
652
  return;
367
653
  this.startTime(`insertNewOutput_${txid.substring(0, 10)}`);
@@ -372,10 +658,11 @@ class Engine {
372
658
  satoshis: tx.outputs[outputIndex].satoshis,
373
659
  topic,
374
660
  spent: false,
375
- beef: taggedBEEF.beef,
661
+ beef: this.compactBEEFForStorage(tx, taggedBEEF.beef),
376
662
  consumedBy: [],
377
663
  outputsConsumed,
378
- score: Date.now()
664
+ score: Date.now(),
665
+ blockHeight: (_a = (0, BASM_js_1.extractMerkleProofMetadata)(txid, tx.merklePath)) === null || _a === void 0 ? void 0 : _a.blockHeight
379
666
  });
380
667
  this.endTime(`insertNewOutput_${txid.substring(0, 10)}`);
381
668
  newUTXOs.push({ txid, outputIndex });
@@ -415,6 +702,8 @@ class Engine {
415
702
  }));
416
703
  this.startTime(`outputConsumed_${txid.substring(0, 10)}`);
417
704
  // Update each output consumed to know who consumed it and insert applied transaction in parallel
705
+ const appliedRecord = await this.buildAppliedTransactionRecord(tx);
706
+ await this.recordTransactionData(tx, taggedBEEF.beef, appliedRecord.blockHash);
418
707
  await Promise.all([
419
708
  ...outputsConsumed.map(async (output) => {
420
709
  const outputToUpdate = await this.storage.findOutput(output.txid, output.outputIndex, topic);
@@ -425,9 +714,13 @@ class Engine {
425
714
  }),
426
715
  this.storage.insertAppliedTransaction({
427
716
  txid,
428
- topic
717
+ topic,
718
+ ...appliedRecord
429
719
  })
430
720
  ]);
721
+ if (appliedRecord.blockHeight !== undefined && appliedRecord.blockHash !== undefined) {
722
+ await this.recomputeTopicBlockAnchor(topic, appliedRecord.blockHeight, appliedRecord.blockHash);
723
+ }
431
724
  this.endTime(`outputConsumed_${txid.substring(0, 10)}`);
432
725
  }
433
726
  catch (error) {
@@ -723,7 +1016,7 @@ class Engine {
723
1016
  // Parse out the advertisements using the provided parser
724
1017
  const tx = sdk_1.Transaction.fromBEEF(output.beef);
725
1018
  const advertisement = (_a = this.advertiser) === null || _a === void 0 ? void 0 : _a.parseAdvertisement(tx.outputs[output.outputIndex].lockingScript);
726
- if (advertisement !== undefined && advertisement !== null && advertisement.protocol === 'SHIP') {
1019
+ if ((advertisement === null || advertisement === void 0 ? void 0 : advertisement.protocol) === 'SHIP') {
727
1020
  endpointSet.add(advertisement.domain);
728
1021
  }
729
1022
  }
@@ -761,6 +1054,287 @@ class Engine {
761
1054
  }
762
1055
  }
763
1056
  }
1057
+ async resolveSyncEndpointsForTopic(topic) {
1058
+ if (this.syncConfiguration === undefined) {
1059
+ return [];
1060
+ }
1061
+ let syncEndpoints = this.syncConfiguration[topic];
1062
+ if (syncEndpoints === false || syncEndpoints === undefined) {
1063
+ return [];
1064
+ }
1065
+ if (syncEndpoints === 'SHIP') {
1066
+ const resolverConfig = this.slapTrackers
1067
+ ? { slapTrackers: this.slapTrackers }
1068
+ : {};
1069
+ const resolver = new sdk_1.LookupResolver(resolverConfig);
1070
+ const lookupAnswer = await resolver.query({
1071
+ service: 'ls_ship',
1072
+ query: {
1073
+ topics: [topic]
1074
+ }
1075
+ });
1076
+ const endpointSet = new Set();
1077
+ if (lookupAnswer.type === 'output-list') {
1078
+ lookupAnswer.outputs.forEach(output => {
1079
+ var _a;
1080
+ try {
1081
+ const tx = sdk_1.Transaction.fromBEEF(output.beef);
1082
+ const advertisement = (_a = this.advertiser) === null || _a === void 0 ? void 0 : _a.parseAdvertisement(tx.outputs[output.outputIndex].lockingScript);
1083
+ if ((advertisement === null || advertisement === void 0 ? void 0 : advertisement.protocol) === 'SHIP') {
1084
+ endpointSet.add(advertisement.domain);
1085
+ }
1086
+ }
1087
+ catch (error) {
1088
+ this.logger.error('Failed to parse BASM advertisement output:', error);
1089
+ }
1090
+ });
1091
+ }
1092
+ syncEndpoints = Array.from(endpointSet);
1093
+ }
1094
+ if (!Array.isArray(syncEndpoints)) {
1095
+ return [];
1096
+ }
1097
+ return syncEndpoints.filter(endpoint => endpoint !== this.hostingURL);
1098
+ }
1099
+ async provideTopicAnchorTip(topic) {
1100
+ var _a, _b;
1101
+ const tip = await ((_b = (_a = this.storage).findTopicAnchorTip) === null || _b === void 0 ? void 0 : _b.call(_a, topic));
1102
+ return tip !== null && tip !== void 0 ? tip : {
1103
+ topic,
1104
+ blockHeight: -1,
1105
+ tac: BASM_js_1.BASM_ZERO_HASH
1106
+ };
1107
+ }
1108
+ async provideTopicAnchorRange(topic, fromHeight, toHeight) {
1109
+ if (typeof this.storage.findTopicBlockAnchors !== 'function') {
1110
+ throw new TypeError('Storage does not support BASM topic anchor ranges');
1111
+ }
1112
+ if (!Number.isInteger(fromHeight) || !Number.isInteger(toHeight) || fromHeight < 0 || toHeight < fromHeight) {
1113
+ throw new Error('Invalid topic anchor range');
1114
+ }
1115
+ if (toHeight - fromHeight + 1 > DEFAULT_BASM_RANGE_LIMIT) {
1116
+ throw new Error(`Topic anchor range is capped at ${DEFAULT_BASM_RANGE_LIMIT} heights`);
1117
+ }
1118
+ return {
1119
+ topic,
1120
+ anchors: await this.storage.findTopicBlockAnchors(topic, fromHeight, toHeight)
1121
+ };
1122
+ }
1123
+ async provideAdmittedList(topic, blockHeight, blockHash) {
1124
+ if (typeof this.storage.findAdmittedTransactionsForBlock !== 'function') {
1125
+ throw new TypeError('Storage does not support BASM admitted lists');
1126
+ }
1127
+ return {
1128
+ topic,
1129
+ blockHeight,
1130
+ blockHash,
1131
+ admitted: await this.storage.findAdmittedTransactionsForBlock(topic, blockHeight, blockHash)
1132
+ };
1133
+ }
1134
+ async provideCompoundMerklePath(topic, blockHeight, txids) {
1135
+ var _a, _b;
1136
+ if (typeof this.storage.findTransactionMerklePaths !== 'function') {
1137
+ throw new TypeError('Storage does not support direct Merkle path lookup');
1138
+ }
1139
+ if (txids.length === 0) {
1140
+ throw new Error('At least one txid is required');
1141
+ }
1142
+ const admitted = await ((_b = (_a = this.storage).findAdmittedTransactionsForBlock) === null || _b === void 0 ? void 0 : _b.call(_a, topic, blockHeight));
1143
+ if (admitted !== undefined) {
1144
+ const admittedSet = new Set(admitted.map(item => item.txid));
1145
+ const missingAdmissions = txids.filter(txid => !admittedSet.has(txid));
1146
+ if (missingAdmissions.length > 0) {
1147
+ throw new Error(`Requested txids are not admitted to topic ${topic} at height ${blockHeight}: ${missingAdmissions.join(',')}`);
1148
+ }
1149
+ }
1150
+ const proofs = await this.storage.findTransactionMerklePaths(txids);
1151
+ const proofByTxid = new Map(proofs.map(proof => [proof.txid, proof]));
1152
+ const missing = txids.filter(txid => !proofByTxid.has(txid));
1153
+ if (missing.length > 0) {
1154
+ throw new Error(`No direct Merkle path found for txids: ${missing.join(',')}`);
1155
+ }
1156
+ let compound;
1157
+ for (const txid of txids) {
1158
+ const proof = proofByTxid.get(txid);
1159
+ if (proof === undefined)
1160
+ continue;
1161
+ const path = sdk_1.MerklePath.fromHex(proof.merklePath);
1162
+ if (path.blockHeight !== blockHeight) {
1163
+ throw new Error(`Merkle path for ${txid} is at height ${path.blockHeight}, expected ${blockHeight}`);
1164
+ }
1165
+ if (compound === undefined) {
1166
+ compound = path;
1167
+ }
1168
+ else {
1169
+ compound.combine(path);
1170
+ }
1171
+ }
1172
+ if (compound === undefined) {
1173
+ throw new Error('Unable to build compound Merkle path');
1174
+ }
1175
+ return {
1176
+ topic,
1177
+ blockHeight,
1178
+ txids,
1179
+ merklePath: compound.toHex()
1180
+ };
1181
+ }
1182
+ async provideRawTransactions(txids) {
1183
+ if (typeof this.storage.findRawTransactions !== 'function') {
1184
+ throw new TypeError('Storage does not support raw transaction lookup');
1185
+ }
1186
+ const transactions = await this.storage.findRawTransactions(txids);
1187
+ const found = new Set(transactions.map(tx => tx.txid));
1188
+ return {
1189
+ transactions,
1190
+ missing: txids.filter(txid => !found.has(txid))
1191
+ };
1192
+ }
1193
+ async startBASMSync() {
1194
+ if (this.syncConfiguration === undefined) {
1195
+ throw new Error('Overlay Service Engine not configured for topical synchronization!');
1196
+ }
1197
+ const reports = [];
1198
+ for (const topic of Object.keys(this.syncConfiguration)) {
1199
+ const endpoints = await this.resolveSyncEndpointsForTopic(topic);
1200
+ for (const endpoint of endpoints) {
1201
+ reports.push(await this.reconcileBASMWithPeer(topic, endpoint));
1202
+ }
1203
+ }
1204
+ return reports;
1205
+ }
1206
+ async reconcileBASMWithPeer(topic, endpoint) {
1207
+ const report = {
1208
+ topic,
1209
+ endpoint,
1210
+ status: 'skipped',
1211
+ checkedHeights: [],
1212
+ missingTxids: [],
1213
+ fetchedTxCount: 0
1214
+ };
1215
+ try {
1216
+ const remote = new BASMRemote_js_1.BASMRemote(endpoint, topic);
1217
+ const [localTip, remoteTip] = await Promise.all([
1218
+ this.provideTopicAnchorTip(topic),
1219
+ remote.requestTopicAnchorTip()
1220
+ ]);
1221
+ report.localTip = localTip;
1222
+ report.remoteTip = remoteTip;
1223
+ if (localTip.blockHeight >= remoteTip.blockHeight) {
1224
+ report.status = localTip.tac === remoteTip.tac && localTip.blockHeight === remoteTip.blockHeight ? 'matched' : 'diverged';
1225
+ report.message = report.status === 'matched'
1226
+ ? 'Topic anchor tips match'
1227
+ : 'Remote tip is not ahead; historical divergence needs manual or binary-search reconciliation';
1228
+ return report;
1229
+ }
1230
+ const fromHeight = Math.max(localTip.blockHeight + 1, remoteTip.blockHeight - DEFAULT_BASM_RANGE_LIMIT + 1, 0);
1231
+ const range = await remote.requestTopicAnchorRange(fromHeight, remoteTip.blockHeight);
1232
+ for (const remoteAnchor of range.anchors) {
1233
+ await this.reconcileRemoteAnchor(topic, remote, remoteAnchor, report);
1234
+ }
1235
+ const refreshedTip = await this.provideTopicAnchorTip(topic);
1236
+ report.localTip = refreshedTip;
1237
+ report.status = refreshedTip.blockHeight >= remoteTip.blockHeight && refreshedTip.tac === remoteTip.tac ? 'matched' : 'advanced';
1238
+ return report;
1239
+ }
1240
+ catch (error) {
1241
+ report.status = 'error';
1242
+ report.message = error instanceof Error ? error.message : String(error);
1243
+ this.logger.error(`[BASM SYNC] Sync failed for topic "${topic}" with peer "${endpoint}"`, error);
1244
+ return report;
1245
+ }
1246
+ }
1247
+ async reconcileRemoteAnchor(topic, remote, remoteAnchor, report) {
1248
+ var _a, _b, _c, _d, _f;
1249
+ report.checkedHeights.push(remoteAnchor.blockHeight);
1250
+ const localAnchor = await ((_b = (_a = this.storage).findTopicBlockAnchor) === null || _b === void 0 ? void 0 : _b.call(_a, topic, remoteAnchor.blockHeight, remoteAnchor.blockHash));
1251
+ if ((localAnchor === null || localAnchor === void 0 ? void 0 : localAnchor.tac) === remoteAnchor.tac) {
1252
+ return;
1253
+ }
1254
+ const admittedResponse = await remote.requestAdmittedList(remoteAnchor.blockHeight, remoteAnchor.blockHash);
1255
+ const remoteBasmRoot = (0, BASM_js_1.computeBasmRoot)(admittedResponse.admitted);
1256
+ if (remoteBasmRoot !== remoteAnchor.basmRoot ||
1257
+ admittedResponse.admitted.length !== remoteAnchor.admittedCount) {
1258
+ throw new Error(`Peer ${report.endpoint} supplied an admitted list inconsistent with its anchor at height ${remoteAnchor.blockHeight}`);
1259
+ }
1260
+ const localAdmitted = (_f = await ((_d = (_c = this.storage).findAdmittedTransactionsForBlock) === null || _d === void 0 ? void 0 : _d.call(_c, topic, remoteAnchor.blockHeight, remoteAnchor.blockHash))) !== null && _f !== void 0 ? _f : [];
1261
+ const localTxids = new Set(localAdmitted.map(item => item.txid));
1262
+ const missingTxids = admittedResponse.admitted
1263
+ .map(item => item.txid)
1264
+ .filter(txid => !localTxids.has(txid));
1265
+ report.missingTxids.push(...missingTxids);
1266
+ if (missingTxids.length === 0) {
1267
+ report.status = 'diverged';
1268
+ return;
1269
+ }
1270
+ await this.fetchBASMMissingTransactions(remote, topic, remoteAnchor, missingTxids);
1271
+ report.fetchedTxCount += missingTxids.length;
1272
+ }
1273
+ async fetchBASMMissingTransactions(remote, topic, anchor, txids) {
1274
+ if (this.chainTracker === 'scripts only') {
1275
+ throw new Error('BASM reconciliation requires a ChainTracker capable of validating BUMP proofs');
1276
+ }
1277
+ const proofResponse = await remote.requestCompoundMerklePath(anchor.blockHeight, txids);
1278
+ const compoundPath = sdk_1.MerklePath.fromHex(proofResponse.merklePath);
1279
+ for (const txid of txids) {
1280
+ const valid = await compoundPath.verify(txid, this.chainTracker);
1281
+ if (!valid) {
1282
+ throw new Error(`Peer supplied invalid compound Merkle path for ${txid} at height ${anchor.blockHeight}`);
1283
+ }
1284
+ }
1285
+ const rawResponse = await remote.requestRawTransactions(txids);
1286
+ if (rawResponse.missing.length > 0) {
1287
+ throw new Error(`Peer did not return raw transactions for txids: ${rawResponse.missing.join(',')}`);
1288
+ }
1289
+ for (const record of rawResponse.transactions) {
1290
+ const tx = sdk_1.Transaction.fromHex(record.rawTx);
1291
+ if (tx.id('hex') !== record.txid) {
1292
+ throw new Error(`Raw transaction txid mismatch: expected ${record.txid}, got ${tx.id('hex')}`);
1293
+ }
1294
+ try {
1295
+ tx.merklePath = compoundPath.extract([record.txid]);
1296
+ }
1297
+ catch {
1298
+ tx.merklePath = compoundPath;
1299
+ }
1300
+ await this.submit({ beef: tx.toBEEF(), topics: [topic] }, undefined, 'historical-tx');
1301
+ }
1302
+ }
1303
+ async evictUnprovenTransactions(options = {}) {
1304
+ var _a, _b, _c;
1305
+ if (typeof this.storage.findUnprovenAppliedTransactions !== 'function') {
1306
+ throw new TypeError('Storage does not support unproven transaction eviction');
1307
+ }
1308
+ if (this.chainTracker === 'scripts only') {
1309
+ throw new Error('Unproven eviction requires a ChainTracker to determine block age');
1310
+ }
1311
+ const thresholdBlocks = (_a = options.thresholdBlocks) !== null && _a !== void 0 ? _a : this.unprovenEvictionBlocks;
1312
+ const currentHeight = await this.chainTracker.currentHeight();
1313
+ const cutoffHeight = currentHeight - thresholdBlocks;
1314
+ const candidates = await this.storage.findUnprovenAppliedTransactions(cutoffHeight, options.topic);
1315
+ let evictedOutputs = 0;
1316
+ for (const candidate of candidates) {
1317
+ for (const output of candidate.outputs) {
1318
+ for (const service of Object.values(this.lookupServices)) {
1319
+ try {
1320
+ await service.outputEvicted(output.txid, output.outputIndex);
1321
+ }
1322
+ catch (error) {
1323
+ this.logger.debug(`outputEvicted notification failed for ${output.txid}.${output.outputIndex}: ${error}`);
1324
+ }
1325
+ }
1326
+ await this.storage.deleteOutput(output.txid, output.outputIndex, candidate.topic);
1327
+ evictedOutputs++;
1328
+ }
1329
+ await ((_c = (_b = this.storage).deleteAppliedTransaction) === null || _c === void 0 ? void 0 : _c.call(_b, candidate.txid, candidate.topic));
1330
+ }
1331
+ return {
1332
+ cutoffHeight,
1333
+ candidates: candidates.length,
1334
+ evictedTransactions: candidates.length,
1335
+ evictedOutputs
1336
+ };
1337
+ }
764
1338
  /**
765
1339
  * Given a GASP request, create an initial response.
766
1340
  *
@@ -810,29 +1384,17 @@ class Engine {
810
1384
  // For each input, look it up and recurse.
811
1385
  for (const input of tx.inputs) {
812
1386
  // We should always have a source transaction
813
- if (input.sourceTransaction !== undefined) {
814
- searchInput(input.sourceTransaction);
1387
+ if (input.sourceTransaction === undefined) {
1388
+ throw new Error('Incomplete SPV data!');
815
1389
  }
816
1390
  else {
817
- throw new Error('Incomplete SPV data!');
1391
+ searchInput(input.sourceTransaction);
818
1392
  }
819
1393
  }
820
1394
  }
821
1395
  };
822
1396
  searchInput(rootTx);
823
- if (correctTx !== undefined) {
824
- const rawTx = correctTx.toHex();
825
- const node = {
826
- rawTx,
827
- graphID,
828
- outputIndex
829
- };
830
- if (correctTx.merklePath !== undefined) {
831
- node.proof = correctTx.merklePath.toHex();
832
- }
833
- return node;
834
- }
835
- else {
1397
+ if (correctTx === undefined) {
836
1398
  // Recursively try to find a matching output
837
1399
  let foundNode;
838
1400
  for (const currentOutput of output.outputsConsumed) {
@@ -842,6 +1404,8 @@ class Engine {
842
1404
  break;
843
1405
  }
844
1406
  catch (error) {
1407
+ // Best-effort: output may not be found or hydration fails for this candidate; try the next one
1408
+ this.logger.debug(`Unable to hydrate output ${currentOutput.txid}.${currentOutput.outputIndex}: ${error}`);
845
1409
  continue;
846
1410
  }
847
1411
  }
@@ -849,6 +1413,18 @@ class Engine {
849
1413
  return foundNode;
850
1414
  }
851
1415
  }
1416
+ else {
1417
+ const rawTx = correctTx.toHex();
1418
+ const node = {
1419
+ rawTx,
1420
+ graphID,
1421
+ outputIndex
1422
+ };
1423
+ if (correctTx.merklePath !== undefined) {
1424
+ node.proof = correctTx.merklePath.toHex();
1425
+ }
1426
+ return node;
1427
+ }
852
1428
  throw new Error('Unable to find output associated with your request!');
853
1429
  };
854
1430
  const [rootTxid, rootOutputIndex] = graphID.split('.');
@@ -872,7 +1448,7 @@ class Engine {
872
1448
  */
873
1449
  async getUTXOHistory(output, historySelector, currentDepth = 0, context = this.createUTXOHistoryHydrationContext()) {
874
1450
  // If we have an output but no history selector, just return the output.
875
- if (typeof historySelector === 'undefined') {
1451
+ if (historySelector === undefined) {
876
1452
  return output;
877
1453
  }
878
1454
  try {
@@ -912,7 +1488,10 @@ class Engine {
912
1488
  try {
913
1489
  await ((_a = l.outputNoLongerRetainedInHistory) === null || _a === void 0 ? void 0 : _a.call(l, output.txid, output.outputIndex, output.topic));
914
1490
  }
915
- catch (_) { }
1491
+ catch (e) {
1492
+ // Best-effort notification; lookup service failure must not abort UTXO deletion
1493
+ this.logger.debug(`outputNoLongerRetainedInHistory notification failed for ${output.txid}.${output.outputIndex}: ${e}`);
1494
+ }
916
1495
  }
917
1496
  }
918
1497
  // If there are no more consumed utxos, return
@@ -920,7 +1499,7 @@ class Engine {
920
1499
  return;
921
1500
  }
922
1501
  // Delete any stale outputs that were consumed as inputs
923
- output.outputsConsumed.map(async (outputIdentifier) => {
1502
+ await Promise.all(output.outputsConsumed.map(async (outputIdentifier) => {
924
1503
  const staleOutput = await this.storage.findOutput(outputIdentifier.txid, outputIdentifier.outputIndex, output.topic);
925
1504
  // Make sure an output was found
926
1505
  if (staleOutput === null || staleOutput === undefined) {
@@ -934,7 +1513,7 @@ class Engine {
934
1513
  }
935
1514
  // Find previousUTXO history
936
1515
  return await this.deleteUTXODeep(staleOutput);
937
- });
1516
+ }));
938
1517
  }
939
1518
  catch (error) {
940
1519
  throw new Error(`Failed to delete all stale outputs: ${error} `);
@@ -1005,17 +1584,40 @@ class Engine {
1005
1584
  * @param blockHeight - The block height associated with the incoming merkle proof.
1006
1585
  */
1007
1586
  async handleNewMerkleProof(txid, proof, blockHeight) {
1008
- var _a, _b;
1587
+ var _a, _b, _c, _d;
1009
1588
  const outputs = await this.storage.findOutputsForTransaction(txid, true);
1010
1589
  if (outputs === undefined || outputs.length === 0) {
1011
1590
  throw new Error('Could not find matching transaction outputs for proof ingest!');
1012
1591
  }
1592
+ const proofMetadata = (0, BASM_js_1.extractMerkleProofMetadata)(txid, proof);
1593
+ const resolvedBlockHeight = blockHeight !== null && blockHeight !== void 0 ? blockHeight : proofMetadata === null || proofMetadata === void 0 ? void 0 : proofMetadata.blockHeight;
1594
+ const resolvedBlockHash = resolvedBlockHeight === undefined
1595
+ ? undefined
1596
+ : await this.resolveBlockHash(resolvedBlockHeight, proofMetadata === null || proofMetadata === void 0 ? void 0 : proofMetadata.merkleRoot);
1013
1597
  for (const output of outputs) {
1014
1598
  await this.updateMerkleProof(output, txid, proof);
1015
1599
  // Add the associated blockHeight
1016
- if (blockHeight !== undefined) {
1017
- output.blockHeight = blockHeight;
1018
- await ((_b = (_a = this.storage).updateOutputBlockHeight) === null || _b === void 0 ? void 0 : _b.call(_a, output.txid, output.outputIndex, output.topic, blockHeight));
1600
+ if (resolvedBlockHeight !== undefined) {
1601
+ output.blockHeight = resolvedBlockHeight;
1602
+ await ((_b = (_a = this.storage).updateOutputBlockHeight) === null || _b === void 0 ? void 0 : _b.call(_a, output.txid, output.outputIndex, output.topic, resolvedBlockHeight));
1603
+ }
1604
+ if (output.beef !== undefined) {
1605
+ const tx = sdk_1.Transaction.fromBEEF(output.beef);
1606
+ this.updateInputProofs(tx, txid, proof);
1607
+ await this.recordTransactionData(tx, tx.toBEEF(), resolvedBlockHash);
1608
+ }
1609
+ if (resolvedBlockHeight !== undefined) {
1610
+ await ((_d = (_c = this.storage).updateAppliedTransactionProof) === null || _d === void 0 ? void 0 : _d.call(_c, {
1611
+ txid,
1612
+ topic: output.topic,
1613
+ blockHeight: resolvedBlockHeight,
1614
+ blockHash: resolvedBlockHash,
1615
+ blockIndex: proofMetadata === null || proofMetadata === void 0 ? void 0 : proofMetadata.blockIndex,
1616
+ merkleRoot: proofMetadata === null || proofMetadata === void 0 ? void 0 : proofMetadata.merkleRoot
1617
+ }));
1618
+ }
1619
+ if (resolvedBlockHeight !== undefined && resolvedBlockHash !== undefined) {
1620
+ await this.recomputeTopicBlockAnchor(output.topic, resolvedBlockHeight, resolvedBlockHash);
1019
1621
  }
1020
1622
  }
1021
1623
  }
@@ -1031,7 +1633,7 @@ class Engine {
1031
1633
  result[t] = await this.managers[t].getMetaData();
1032
1634
  }
1033
1635
  catch (e) {
1034
- this.logger.warn(`Unable to get metadata for topic manager: ${t}`);
1636
+ this.logger.warn(`Unable to get metadata for topic manager: ${t}: ${e}`);
1035
1637
  result[t] = {
1036
1638
  name: t,
1037
1639
  shortDescription: 'No topical tagline.'
@@ -1052,7 +1654,7 @@ class Engine {
1052
1654
  result[ls] = await this.lookupServices[ls].getMetaData();
1053
1655
  }
1054
1656
  catch (e) {
1055
- this.logger.warn(`Unable to get metadata for lookup service: ${ls}`);
1657
+ this.logger.warn(`Unable to get metadata for lookup service: ${ls}: ${e}`);
1056
1658
  result[ls] = {
1057
1659
  name: ls,
1058
1660
  shortDescription: 'No lookup service tagline.'
@@ -1069,7 +1671,7 @@ class Engine {
1069
1671
  async getDocumentationForTopicManager(manager) {
1070
1672
  var _a, _b;
1071
1673
  const documentation = await ((_b = (_a = this.managers[manager]) === null || _a === void 0 ? void 0 : _a.getDocumentation) === null || _b === void 0 ? void 0 : _b.call(_a));
1072
- return documentation !== undefined ? documentation : 'No documentation found!';
1674
+ return documentation !== null && documentation !== void 0 ? documentation : 'No documentation found!';
1073
1675
  }
1074
1676
  /**
1075
1677
  * Run a query to get the documentation for a particular lookup service
@@ -1079,7 +1681,7 @@ class Engine {
1079
1681
  async getDocumentationForLookupServiceProvider(provider) {
1080
1682
  var _a, _b;
1081
1683
  const documentation = await ((_b = (_a = this.lookupServices[provider]) === null || _a === void 0 ? void 0 : _a.getDocumentation) === null || _b === void 0 ? void 0 : _b.call(_a));
1082
- return documentation !== undefined ? documentation : 'No documentation found!';
1684
+ return documentation !== null && documentation !== void 0 ? documentation : 'No documentation found!';
1083
1685
  }
1084
1686
  /**
1085
1687
  * Validates a URL to ensure it does not match disallowed patterns:
@@ -1109,7 +1711,7 @@ class Engine {
1109
1711
  /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, // Loopback IPs
1110
1712
  /^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, // 10.x.x.x private IPs
1111
1713
  /^192\.168\.\d{1,3}\.\d{1,3}$/, // 192.168.x.x private IPs
1112
- /^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/, // 172.16.x.x to 172.31.x.x private IPs
1714
+ /^172\.(1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3}$/, // 172.16.x.x to 172.31.x.x private IPs
1113
1715
  /^0\.0\.0\.0$/ // Non-routable address
1114
1716
  ];
1115
1717
  // Check for IPv4 matches
@@ -1123,8 +1725,8 @@ class Engine {
1123
1725
  // If none of the disallowed conditions matched, the URL is valid
1124
1726
  return true;
1125
1727
  }
1126
- catch (error) {
1127
- // If the input is not a valid URL, return false
1728
+ catch (_e) {
1729
+ // URL constructor throws on malformed input not a valid URL, return false
1128
1730
  return false;
1129
1731
  }
1130
1732
  }