@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.
- package/README.md +226 -106
- package/dist/cjs/mod.js +6 -1
- package/dist/cjs/mod.js.map +1 -1
- package/dist/cjs/package.json +13 -11
- package/dist/cjs/src/BASM.js +92 -0
- package/dist/cjs/src/BASM.js.map +1 -0
- package/dist/cjs/src/BASMRemote.js +43 -0
- package/dist/cjs/src/BASMRemote.js.map +1 -0
- package/dist/cjs/src/Engine.js +649 -47
- package/dist/cjs/src/Engine.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPRemote.js +2 -2
- package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPStorage.js +18 -25
- package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/cjs/src/storage/knex/KnexStorage.js +329 -12
- package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/cjs/src/storage/knex/all-migrations.js +3 -1
- package/dist/cjs/src/storage/knex/all-migrations.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.js +2 -24
- package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js +114 -0
- package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/mod.js +1 -0
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/src/BASM.js +85 -0
- package/dist/esm/src/BASM.js.map +1 -0
- package/dist/esm/src/BASMRemote.js +42 -0
- package/dist/esm/src/BASMRemote.js.map +1 -0
- package/dist/esm/src/Engine.js +637 -46
- package/dist/esm/src/Engine.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPRemote.js +2 -2
- package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPStorage.js +18 -25
- package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/esm/src/storage/knex/KnexStorage.js +323 -12
- package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/esm/src/storage/knex/all-migrations.js +3 -1
- package/dist/esm/src/storage/knex/all-migrations.js.map +1 -1
- package/dist/esm/src/storage/knex/migrations/2025-05-28-001-enlarge.js +2 -24
- package/dist/esm/src/storage/knex/migrations/2025-05-28-001-enlarge.js.map +1 -1
- package/dist/esm/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js +109 -0
- package/dist/esm/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/mod.d.ts +2 -0
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/BASM.d.ts +110 -0
- package/dist/types/src/BASM.d.ts.map +1 -0
- package/dist/types/src/BASMRemote.d.ts +14 -0
- package/dist/types/src/BASMRemote.d.ts.map +1 -0
- package/dist/types/src/Engine.d.ts +78 -1
- package/dist/types/src/Engine.d.ts.map +1 -1
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts +1 -1
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
- package/dist/types/src/LookupService.d.ts.map +1 -1
- package/dist/types/src/storage/Storage.d.ts +118 -0
- package/dist/types/src/storage/Storage.d.ts.map +1 -1
- package/dist/types/src/storage/knex/KnexStorage.d.ts +38 -6
- package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
- package/dist/types/src/storage/knex/all-migrations.d.ts.map +1 -1
- package/dist/types/src/storage/knex/migrations/2025-05-28-001-enlarge.d.ts.map +1 -1
- package/dist/types/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts +4 -0
- package/dist/types/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts.map +1 -0
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/docs/API.md +72 -580
- package/docs/BRC-136-BASM.md +285 -0
- package/docs/README.md +2 -0
- package/docs/Synchronization.md +10 -1
- package/docs/examples/README.md +2 -2
- package/docs/examples/gs-wip.md +68 -76
- package/mod.ts +25 -1
- package/package.json +25 -23
- package/src/BASM.ts +208 -0
- package/src/BASMRemote.ts +54 -0
- package/src/Engine.ts +762 -47
- package/src/GASP/OverlayGASPRemote.ts +2 -2
- package/src/GASP/OverlayGASPStorage.ts +17 -22
- package/src/LookupService.ts +2 -1
- package/src/__tests/BASM.test.ts +44 -0
- package/src/__tests/BASMChain.test.ts +290 -0
- package/src/__tests/Engine.test.ts +3 -4
- package/src/storage/Storage.ts +127 -0
- package/src/storage/knex/KnexStorage.ts +376 -14
- package/src/storage/knex/all-migrations.ts +3 -1
- package/src/storage/knex/migrations/2025-05-28-001-enlarge.ts +2 -22
- package/src/storage/knex/migrations/2026-05-29-001-brc136-basm.ts +123 -0
package/dist/cjs/src/Engine.js
CHANGED
|
@@ -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
|
-
|
|
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 !==
|
|
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 (
|
|
338
|
-
|
|
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
|
-
|
|
629
|
+
outputsToMarkStale.push({
|
|
346
630
|
txid: previousTXID,
|
|
347
|
-
|
|
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
|
|
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
|
|
814
|
-
|
|
1387
|
+
if (input.sourceTransaction === undefined) {
|
|
1388
|
+
throw new Error('Incomplete SPV data!');
|
|
815
1389
|
}
|
|
816
1390
|
else {
|
|
817
|
-
|
|
1391
|
+
searchInput(input.sourceTransaction);
|
|
818
1392
|
}
|
|
819
1393
|
}
|
|
820
1394
|
}
|
|
821
1395
|
};
|
|
822
1396
|
searchInput(rootTx);
|
|
823
|
-
if (correctTx
|
|
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 (
|
|
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 (
|
|
1017
|
-
output.blockHeight =
|
|
1018
|
-
await ((_b = (_a = this.storage).updateOutputBlockHeight) === null || _b === void 0 ? void 0 : _b.call(_a, output.txid, output.outputIndex, output.topic,
|
|
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 !==
|
|
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 !==
|
|
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
|
|
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 (
|
|
1127
|
-
//
|
|
1728
|
+
catch (_e) {
|
|
1729
|
+
// URL constructor throws on malformed input — not a valid URL, return false
|
|
1128
1730
|
return false;
|
|
1129
1731
|
}
|
|
1130
1732
|
}
|