@bsv/overlay 0.1.7 → 0.1.9

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 (83) hide show
  1. package/dist/cjs/package.json +2 -2
  2. package/dist/cjs/src/Engine.js +185 -496
  3. package/dist/cjs/src/Engine.js.map +1 -1
  4. package/dist/cjs/src/GASP/OverlayGASPRemote.js +6 -4
  5. package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
  6. package/dist/cjs/src/GASP/OverlayGASPStorage.js +152 -77
  7. package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
  8. package/dist/cjs/src/storage/knex/KnexStorage.js +106 -38
  9. package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
  10. package/dist/cjs/src/storage/knex/all-migrations.js +5 -1
  11. package/dist/cjs/src/storage/knex/all-migrations.js.map +1 -1
  12. package/dist/cjs/src/storage/knex/migrations/2024-07-17-001-transactions.js +39 -0
  13. package/dist/cjs/src/storage/knex/migrations/2024-07-17-001-transactions.js.map +1 -0
  14. package/dist/cjs/src/storage/knex/migrations/2024-07-18-001-indexes.js +34 -0
  15. package/dist/cjs/src/storage/knex/migrations/2024-07-18-001-indexes.js.map +1 -0
  16. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  17. package/dist/esm/src/Engine.js +183 -495
  18. package/dist/esm/src/Engine.js.map +1 -1
  19. package/dist/esm/src/GASP/OverlayGASPRemote.js +7 -4
  20. package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
  21. package/dist/esm/src/GASP/OverlayGASPStorage.js +151 -77
  22. package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
  23. package/dist/esm/src/storage/knex/KnexStorage.js +106 -38
  24. package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
  25. package/dist/esm/src/storage/knex/all-migrations.js +5 -1
  26. package/dist/esm/src/storage/knex/all-migrations.js.map +1 -1
  27. package/dist/esm/src/storage/knex/migrations/2024-07-17-001-transactions.js +34 -0
  28. package/dist/esm/src/storage/knex/migrations/2024-07-17-001-transactions.js.map +1 -0
  29. package/dist/esm/src/storage/knex/migrations/2024-07-18-001-indexes.js +29 -0
  30. package/dist/esm/src/storage/knex/migrations/2024-07-18-001-indexes.js.map +1 -0
  31. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  32. package/dist/types/src/Engine.d.ts +9 -116
  33. package/dist/types/src/Engine.d.ts.map +1 -1
  34. package/dist/types/src/GASP/OverlayGASPRemote.d.ts +3 -2
  35. package/dist/types/src/GASP/OverlayGASPRemote.d.ts.map +1 -1
  36. package/dist/types/src/GASP/OverlayGASPStorage.d.ts +29 -3
  37. package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
  38. package/dist/types/src/Output.d.ts +2 -2
  39. package/dist/types/src/Output.d.ts.map +1 -1
  40. package/dist/types/src/SyncConfiguration.d.ts +1 -1
  41. package/dist/types/src/SyncConfiguration.d.ts.map +1 -1
  42. package/dist/types/src/storage/Storage.d.ts +6 -8
  43. package/dist/types/src/storage/Storage.d.ts.map +1 -1
  44. package/dist/types/src/storage/knex/KnexStorage.d.ts +4 -4
  45. package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
  46. package/dist/types/src/storage/knex/all-migrations.d.ts.map +1 -1
  47. package/dist/types/src/storage/knex/migrations/2024-07-17-001-transactions.d.ts +4 -0
  48. package/dist/types/src/storage/knex/migrations/2024-07-17-001-transactions.d.ts.map +1 -0
  49. package/dist/types/src/storage/knex/migrations/2024-07-18-001-indexes.d.ts +4 -0
  50. package/dist/types/src/storage/knex/migrations/2024-07-18-001-indexes.d.ts.map +1 -0
  51. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  52. package/package.json +2 -2
  53. package/src/Engine.ts +191 -555
  54. package/src/GASP/OverlayGASPRemote.ts +107 -0
  55. package/src/GASP/OverlayGASPStorage.ts +331 -0
  56. package/src/Output.ts +2 -2
  57. package/src/SyncConfiguration.ts +1 -1
  58. package/src/__tests/Engine.test.ts +105 -26
  59. package/src/__tests/OverlayGASPRemote.test.ts +1 -1
  60. package/src/__tests/OverlayGASPStorage.test.ts +4 -3
  61. package/src/storage/Storage.ts +6 -8
  62. package/src/storage/knex/KnexStorage.ts +119 -41
  63. package/src/storage/knex/all-migrations.ts +10 -6
  64. package/src/storage/knex/migrations/2024-07-17-001-transactions.ts +40 -0
  65. package/src/storage/knex/migrations/2024-07-18-001-indexes.ts +30 -0
  66. package/dist/cjs/src/GASP.js +0 -275
  67. package/dist/cjs/src/GASP.js.map +0 -1
  68. package/dist/cjs/src/SHIPAdvertisement.js +0 -3
  69. package/dist/cjs/src/SHIPAdvertisement.js.map +0 -1
  70. package/dist/cjs/src/SLAPAdvertisement.js +0 -3
  71. package/dist/cjs/src/SLAPAdvertisement.js.map +0 -1
  72. package/dist/esm/src/GASP.js +0 -279
  73. package/dist/esm/src/GASP.js.map +0 -1
  74. package/dist/esm/src/SHIPAdvertisement.js +0 -2
  75. package/dist/esm/src/SHIPAdvertisement.js.map +0 -1
  76. package/dist/esm/src/SLAPAdvertisement.js +0 -2
  77. package/dist/esm/src/SLAPAdvertisement.js.map +0 -1
  78. package/dist/types/src/GASP.d.ts +0 -215
  79. package/dist/types/src/GASP.d.ts.map +0 -1
  80. package/dist/types/src/SHIPAdvertisement.d.ts +0 -9
  81. package/dist/types/src/SHIPAdvertisement.d.ts.map +0 -1
  82. package/dist/types/src/SLAPAdvertisement.d.ts +0 -9
  83. package/dist/types/src/SLAPAdvertisement.d.ts.map +0 -1
@@ -1,5 +1,7 @@
1
- import { Transaction, MerklePath, isBroadcastFailure } from '@bsv/sdk';
1
+ import { Transaction, isBroadcastFailure } from '@bsv/sdk';
2
2
  import { GASP } from '@bsv/gasp';
3
+ import { OverlayGASPRemote } from './GASP/OverlayGASPRemote.js';
4
+ import { OverlayGASPStorage } from './GASP/OverlayGASPStorage.js';
3
5
  /**
4
6
  * Am engine for running BSV Overlay Services (topic managers and lookup services).
5
7
  */
@@ -14,6 +16,8 @@ export class Engine {
14
16
  broadcaster;
15
17
  advertiser;
16
18
  syncConfiguration;
19
+ logTime;
20
+ logPrefix;
17
21
  /**
18
22
  * Creates a new Overlay Services Engine
19
23
  * @param {[key: string]: TopicManager} managers - manages topic admittance
@@ -25,9 +29,11 @@ export class Engine {
25
29
  * @param {Advertiser} [Advertiser] - handles SHIP and SLAP advertisements for peer-discovery
26
30
  * @param {string} shipTrackers - SHIP domains we know to bootstrap the system
27
31
  * @param {string} slapTrackers - SLAP domains we know to bootstrap the system
28
- * @param {Record<string, string[] | 'SHIP'>} syncConfiguration — Configuration object describing historical synchronization of topics.
32
+ * @param {SyncConfiguration} syncConfiguration — Configuration object describing historical synchronization of topics.
33
+ * @param {boolean} logTime - Enables / disables the timing logs for various operations in the Overlay submit route.
34
+ * @param {string} logPrefix - Supports overriding the log prefix with a custom string.
29
35
  */
30
- constructor(managers, lookupServices, storage, chainTracker, hostingURL, shipTrackers, slapTrackers, broadcaster, advertiser, syncConfiguration) {
36
+ constructor(managers, lookupServices, storage, chainTracker, hostingURL, shipTrackers, slapTrackers, broadcaster, advertiser, syncConfiguration, logTime = false, logPrefix = '[OVERLAY_ENGINE] ') {
31
37
  this.managers = managers;
32
38
  this.lookupServices = lookupServices;
33
39
  this.storage = storage;
@@ -38,14 +44,52 @@ export class Engine {
38
44
  this.broadcaster = broadcaster;
39
45
  this.advertiser = advertiser;
40
46
  this.syncConfiguration = syncConfiguration;
41
- // To encourage synchronization of overlay services, the SHIP sync strategy is used by default for all topics.
47
+ this.logTime = logTime;
48
+ this.logPrefix = logPrefix;
49
+ // 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
+ // For these two topics, any existing trackers are combined with the provided shipTrackers and slapTrackers omitting any duplicates.
42
51
  if (syncConfiguration === undefined) {
43
52
  this.syncConfiguration = {};
44
- for (const managerName of Object.keys(managers)) {
45
- this.syncConfiguration[managerName] = 'SHIP';
53
+ }
54
+ else {
55
+ this.syncConfiguration = syncConfiguration;
56
+ }
57
+ for (const managerName of Object.keys(managers)) {
58
+ if (managerName === 'tm_ship' && this.shipTrackers !== undefined && this.syncConfiguration[managerName] !== false) {
59
+ // Combine tm_ship trackers with preexisting entries if any
60
+ const combinedSet = new Set([
61
+ ...(Array.isArray(this.syncConfiguration[managerName]) ? this.syncConfiguration[managerName] : []),
62
+ ...this.shipTrackers
63
+ ]);
64
+ this.syncConfiguration[managerName] = Array.from(combinedSet);
65
+ }
66
+ else if (managerName === 'tm_slap' && this.slapTrackers !== undefined && this.syncConfiguration[managerName] !== false) {
67
+ // Combine tm_slap trackers with preexisting entries if any
68
+ const combinedSet = new Set([
69
+ ...(Array.isArray(this.syncConfiguration[managerName]) ? this.syncConfiguration[managerName] : []),
70
+ ...this.slapTrackers
71
+ ]);
72
+ this.syncConfiguration[managerName] = Array.from(combinedSet);
73
+ }
74
+ else {
75
+ // Set undefined managers to 'SHIP' by default
76
+ if (this.syncConfiguration[managerName] === undefined) {
77
+ this.syncConfiguration[managerName] = 'SHIP';
78
+ }
46
79
  }
47
80
  }
48
81
  }
82
+ // Helper functions for logging timings
83
+ startTime(label) {
84
+ if (this.logTime) {
85
+ console.time(`${this.logPrefix} ${label}`);
86
+ }
87
+ }
88
+ endTime(label) {
89
+ if (this.logTime) {
90
+ console.timeEnd(`${this.logPrefix} ${label}`);
91
+ }
92
+ }
49
93
  /**
50
94
  * Submits a transaction for processing by Overlay Services.
51
95
  * @param {TaggedBEEF} taggedBEEF - The transaction to process
@@ -65,70 +109,93 @@ export class Engine {
65
109
  // Validate the transaction SPV information
66
110
  const tx = Transaction.fromBEEF(taggedBEEF.beef);
67
111
  const txid = tx.id('hex');
68
- const txValid = await tx.verify(this.chainTracker); // Note: also verifying historical-tx with SPV. Needed?
112
+ this.startTime(`submit_${txid}`);
113
+ this.startTime(`chainTracker_${txid.substring(0, 10)}`);
114
+ const txValid = await tx.verify(this.chainTracker);
69
115
  if (!txValid)
70
116
  throw new Error('Unable to verify SPV information.');
71
- // Broadcast the transaction if not historical and broadcaster is configured
72
- if (mode !== 'historical-tx' && this.broadcaster !== undefined) {
73
- const response = await this.broadcaster.broadcast(tx);
74
- if (isBroadcastFailure(response)) {
75
- throw new Error(`Failed to broadcast transaction! Error: ${response.description}`);
76
- }
77
- }
78
- // Find UTXOs belonging to a particular topic
117
+ this.endTime(`chainTracker_${txid.substring(0, 10)}`);
79
118
  const steak = {};
80
- for (const topic of taggedBEEF.topics) {
81
- // Ensure transaction is not already applied to the topic
82
- const dupeCheck = await this.storage.doesAppliedTransactionExist({
83
- txid,
84
- topic
85
- });
119
+ let admissableOutputs = { outputsToAdmit: [], coinsToRetain: [] };
120
+ const previousCoins = [];
121
+ // Parallelize the topic processing
122
+ const topicPromises = taggedBEEF.topics.map(async (topic) => {
123
+ if (this.managers[topic] === undefined || this.managers[topic] === null) {
124
+ throw new Error(`This server does not support this topic: ${topic}`);
125
+ }
126
+ // Check for duplicate transactions
127
+ this.startTime(`dupCheck_${txid.substring(0, 10)}`);
128
+ const dupeCheck = await this.storage.doesAppliedTransactionExist({ txid, topic });
129
+ this.endTime(`dupCheck_${txid.substring(0, 10)}`);
86
130
  if (dupeCheck) {
87
- // The transaction was already processed.
88
- // Currently, NO OUTPUTS ARE ADMITTED FOR DUPLICATE TRANSACTIONS.
89
- // An alternative decision, one that was decided against, would be to act as if the operation was successful: looking up and returning the list of admitted outputs from when the transaction was originally processed.
90
- // This was decided against, because we don't want to encourage unnecessary flooding of duplicative transactions to overlay services.
91
- steak[topic] = {
92
- outputsToAdmit: [],
93
- coinsToRetain: []
94
- };
95
- continue;
131
+ steak[topic] = { outputsToAdmit: [], coinsToRetain: [] };
132
+ return;
96
133
  }
97
- // Check if any input of this transaction is a previous UTXO, adding previous UTXOs to the list
98
- const previousCoins = [];
99
- for (const [i, input] of tx.inputs.entries()) {
100
- const previousTXID = input.sourceTXID || input.sourceTransaction?.id('hex');
101
- // Check if a previous UTXO exists in the storage medium
102
- const output = await this.storage.findOutput(previousTXID, input.sourceOutputIndex, topic);
134
+ // Check if any input of this transaction is a previous UTXO
135
+ const outputPromises = tx.inputs.map(async (input, i) => {
136
+ const previousTXID = input.sourceTXID !== undefined ? input.sourceTXID : input.sourceTransaction?.id('hex');
137
+ if (previousTXID !== undefined) {
138
+ const output = this.storage.findOutput(previousTXID, input.sourceOutputIndex, topic);
139
+ if (output !== undefined && output !== null) {
140
+ previousCoins.push(i);
141
+ return await Promise.resolve(output);
142
+ }
143
+ }
144
+ return await Promise.resolve(null);
145
+ });
146
+ this.startTime(`previousOutputQuery_${txid.substring(0, 10)}`);
147
+ const outputs = await Promise.all(outputPromises);
148
+ this.endTime(`previousOutputQuery_${txid.substring(0, 10)}`);
149
+ const markSpentPromises = outputs.map(async (output) => {
103
150
  if (output !== undefined && output !== null) {
104
- previousCoins.push(i);
105
- // This output is now spent.
106
- await this.storage.markUTXOAsSpent(output.txid, output.outputIndex, topic);
107
- // Notify the lookup services about the spending of this output
108
- for (const l of Object.values(this.lookupServices)) {
109
- try {
110
- if (l.outputSpent !== undefined && l.outputSpent !== null) {
111
- await l.outputSpent(output.txid, output.outputIndex, topic);
151
+ try {
152
+ await this.storage.markUTXOAsSpent(output.txid, output.outputIndex, topic);
153
+ await Promise.all(Object.values(this.lookupServices).map(async (l) => {
154
+ try {
155
+ await l.outputSpent?.(output.txid, output.outputIndex, topic);
112
156
  }
113
- }
114
- catch (_) { }
157
+ catch (error) {
158
+ console.error('Error in lookup service for outputSpent:', error);
159
+ }
160
+ }));
161
+ }
162
+ catch (error) {
163
+ console.error('Error marking UTXO as spent:', error);
115
164
  }
116
165
  }
166
+ });
167
+ // Determine which outputs are admissible
168
+ const admissibleOutputsPromise = (async () => {
169
+ try {
170
+ this.startTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`);
171
+ admissableOutputs = await this.managers[topic].identifyAdmissibleOutputs(taggedBEEF.beef, previousCoins);
172
+ this.endTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`);
173
+ }
174
+ catch (_) {
175
+ steak[topic] = { outputsToAdmit: [], coinsToRetain: [] };
176
+ }
177
+ })();
178
+ // Wait for both tasks to complete
179
+ await Promise.all([markSpentPromises, admissibleOutputsPromise]);
180
+ // Keep track of what outputs were admitted for what topic
181
+ steak[topic] = admissableOutputs;
182
+ });
183
+ await Promise.all(topicPromises);
184
+ // Broadcast the transaction if not historical and broadcaster is configured
185
+ this.startTime(`broadcast_${txid.substring(0, 10)}`);
186
+ if (mode !== 'historical-tx' && this.broadcaster !== undefined) {
187
+ const response = await this.broadcaster.broadcast(tx);
188
+ if (isBroadcastFailure(response)) {
189
+ throw new Error(`Failed to broadcast transaction! Error: ${response.description}`);
117
190
  }
118
- // Use the manager to determine which outputs are admissable
119
- let admissableOutputs;
120
- try {
121
- admissableOutputs = await this.managers[topic].identifyAdmissibleOutputs(taggedBEEF.beef, previousCoins);
122
- }
123
- catch (_) {
124
- // If the topic manager throws an error, other topics may still succeed, so we continue to the next one.
125
- // No outputs were admitted to this topic in this case. Note, however, that the transaction is still valid according to Bitcoin, so it may have spent some previous overlay members. This is unavoidable and good.
126
- steak[topic] = {
127
- outputsToAdmit: [],
128
- coinsToRetain: []
129
- };
130
- continue;
131
- }
191
+ }
192
+ this.endTime(`broadcast_${txid.substring(0, 10)}`);
193
+ // Call the callback function if it is provided
194
+ if (onSteakReady !== undefined) {
195
+ this.endTime(`submit_${txid}`);
196
+ onSteakReady(steak);
197
+ }
198
+ for (const topic of taggedBEEF.topics) {
132
199
  // Keep track of which outputs to admit, mark as stale, or retain
133
200
  const outputsToAdmit = admissableOutputs.outputsToAdmit;
134
201
  const staleCoins = [];
@@ -152,16 +219,18 @@ export class Engine {
152
219
  }
153
220
  }
154
221
  // Remove stale outputs recursively
155
- for (const coin of staleCoins) {
222
+ this.startTime(`lookForStaleOutputs_${txid.substring(0, 10)}`);
223
+ await Promise.all(staleCoins.map(async (coin) => {
156
224
  const output = await this.storage.findOutput(coin.txid, coin.outputIndex, topic);
157
225
  if (output !== undefined && output !== null) {
158
226
  await this.deleteUTXODeep(output);
159
227
  }
160
- }
228
+ }));
229
+ this.endTime(`lookForStaleOutputs_${txid.substring(0, 10)}`);
161
230
  // Handle admittance and notification of incoming UTXOs
162
231
  const newUTXOs = [];
163
- for (const outputIndex of outputsToAdmit) {
164
- // Store the output
232
+ await Promise.all(outputsToAdmit.map(async (outputIndex) => {
233
+ this.startTime(`insertNewOutput_${txid.substring(0, 10)}`);
165
234
  await this.storage.insertOutput({
166
235
  txid,
167
236
  outputIndex,
@@ -173,51 +242,40 @@ export class Engine {
173
242
  consumedBy: [],
174
243
  outputsConsumed
175
244
  });
176
- newUTXOs.push({
177
- txid,
178
- outputIndex
179
- });
180
- // Notify all the lookup services about the new UTXO
181
- for (const l of Object.values(this.lookupServices)) {
182
- try {
183
- if (l.outputAdded !== undefined && l.outputAdded !== null) {
184
- await l.outputAdded(txid, outputIndex, tx.outputs[outputIndex].lockingScript, topic);
185
- }
245
+ this.endTime(`insertNewOutput_${txid.substring(0, 10)}`);
246
+ newUTXOs.push({ txid, outputIndex });
247
+ this.startTime(`notifyLookupService${txid.substring(0, 10)}`);
248
+ await Promise.all(Object.values(this.lookupServices).map(async (l) => await l.outputAdded?.(txid, outputIndex, tx.outputs[outputIndex].lockingScript, topic)));
249
+ this.endTime(`notifyLookupService${txid.substring(0, 10)}`);
250
+ }));
251
+ this.startTime(`outputConsumed_${txid.substring(0, 10)}`);
252
+ // Update each output consumed to know who consumed it and insert applied transaction in parallel
253
+ await Promise.all([
254
+ ...outputsConsumed.map(async (output) => {
255
+ const outputToUpdate = await this.storage.findOutput(output.txid, output.outputIndex, topic);
256
+ if (outputToUpdate !== undefined && outputToUpdate !== null) {
257
+ const newConsumedBy = [...new Set([...newUTXOs, ...outputToUpdate.consumedBy])];
258
+ await this.storage.updateConsumedBy(output.txid, output.outputIndex, topic, newConsumedBy);
186
259
  }
187
- catch (_) { }
188
- }
189
- }
190
- // Update each output consumed to know who consumed it
191
- for (const output of outputsConsumed) {
192
- const outputToUpdate = await this.storage.findOutput(output.txid, output.outputIndex, topic);
193
- if (outputToUpdate !== undefined && outputToUpdate !== null) {
194
- const newConsumedBy = [...new Set([...newUTXOs, ...outputToUpdate.consumedBy])];
195
- // Note: only update if newConsumedBy !== new Set(JSON.parse(outputToUpdate.consumedBy)) ?
196
- await this.storage.updateConsumedBy(output.txid, output.outputIndex, topic, newConsumedBy);
197
- }
198
- }
199
- // Insert the applied transaction to prevent duplicate processing
200
- await this.storage.insertAppliedTransaction({
201
- txid,
202
- topic
203
- });
204
- // Keep track of what outputs were admitted for what topic
205
- steak[topic] = admissableOutputs;
206
- }
207
- // Call the callback function if it is provided
208
- // TODO: To call `onSteakReady` sooner, we could have two loops. First we figure out topical admittance only, then we call `onSteakReeady` and do everything else after the first loop.
209
- if (onSteakReady !== undefined) {
210
- onSteakReady(steak);
260
+ }),
261
+ this.storage.insertAppliedTransaction({
262
+ txid,
263
+ topic
264
+ })
265
+ ]);
266
+ this.endTime(`outputConsumed_${txid.substring(0, 10)}`);
211
267
  }
212
268
  // If we don't have an advertiser or we are dealing with historical transactions, just return the steak
213
269
  if (this.advertiser === undefined || mode === 'historical-tx') {
214
270
  return steak;
215
271
  }
272
+ this.startTime(`transactionPropagation_${txid.substring(0, 10)}`);
216
273
  // Propagate transaction to other nodes according to synchronization agreements
217
274
  // 1. Find nodes that host the topics associated with admissable outputs
218
275
  // We want to figure out which topics we actually care about (because their associated outputs were admitted)
219
276
  // AND if the topic was not admitted we want to remove it from the list of topics we care about.
220
277
  const relevantTopics = taggedBEEF.topics.filter(topic => steak[topic] !== undefined && steak[topic].outputsToAdmit.length !== 0);
278
+ // TODO: Cache ship/slap lookup with expiry (every 5min)
221
279
  if (relevantTopics.length > 0) {
222
280
  // Find all SHIP advertisements for the topics we care about
223
281
  const domainToTopicsMap = new Map();
@@ -304,6 +362,7 @@ export class Engine {
304
362
  console.error('Error during broadcasting:', error);
305
363
  }
306
364
  }
365
+ this.endTime(`transactionPropgation_${txid.substring(0, 10)}`);
307
366
  // Immediately return from the function without waiting for the promises to resolve.
308
367
  return steak;
309
368
  }
@@ -316,7 +375,7 @@ export class Engine {
316
375
  // Validate a lookup service for the provider is found
317
376
  const lookupService = this.lookupServices[lookupQuestion.service];
318
377
  if (lookupService === undefined || lookupService === null)
319
- throw new Error(`Lookup service not found for provider: ${lookupQuestion.service}`);
378
+ throw new Error(`Lookup service not found for provider: ${lookupQuestion.service} `);
320
379
  let lookupResult = await lookupService.lookup(lookupQuestion);
321
380
  // Handle custom lookup service answers
322
381
  if (lookupResult.type === 'freeform' || lookupResult.type === 'output-list') {
@@ -326,12 +385,12 @@ export class Engine {
326
385
  const hydratedOutputs = [];
327
386
  for (const { txid, outputIndex, history } of lookupResult) {
328
387
  // Make sure this is an unspent output (UTXO)
329
- const UTXO = await this.storage.findOutput(txid, outputIndex, undefined, false);
388
+ const UTXO = await this.storage.findOutput(txid, outputIndex, undefined, undefined, true);
330
389
  if (UTXO === undefined || UTXO === null)
331
390
  continue;
332
391
  // Get the history for this utxo and construct a BRC-8 Envelope
333
392
  const output = await this.getUTXOHistory(UTXO, history, 0);
334
- if (output !== undefined && output !== null) {
393
+ if (output?.beef !== undefined) {
335
394
  hydratedOutputs.push({
336
395
  beef: output.beef,
337
396
  outputIndex: output.outputIndex
@@ -427,6 +486,10 @@ export class Engine {
427
486
  for (const topic of Object.keys(this.syncConfiguration)) {
428
487
  // Make sure syncEndpoints is an array or SHIP
429
488
  let syncEndpoints = this.syncConfiguration[topic];
489
+ // Check if this topic has been configured NOT to sync
490
+ if (syncEndpoints === false) {
491
+ continue;
492
+ }
430
493
  if (syncEndpoints === 'SHIP') {
431
494
  // Perform lookup and find ship advertisements to set syncEndpoints for topic
432
495
  const lookupAnswer = await this.lookup({
@@ -459,7 +522,7 @@ export class Engine {
459
522
  if (Array.isArray(syncEndpoints)) {
460
523
  await Promise.all(syncEndpoints.map(async (endpoint) => {
461
524
  // Sync to each host that is associated with this topic
462
- const gasp = new GASP(new OverlayGASPStorage(topic, this), new OverlayGASPRemote(endpoint, topic), 0, `[GASP Sync of ${topic} with ${endpoint}] `, true);
525
+ const gasp = new GASP(new OverlayGASPStorage(topic, this), new OverlayGASPRemote(endpoint, topic), 0, `[GASP Sync of ${topic} with ${endpoint}]`, true);
463
526
  await gasp.sync();
464
527
  }));
465
528
  }
@@ -499,7 +562,7 @@ export class Engine {
499
562
  */
500
563
  async provideForeignGASPNode(graphID, txid, outputIndex) {
501
564
  const hydrator = async (output) => {
502
- if (output === undefined || output === null) {
565
+ if (output?.beef === undefined) {
503
566
  throw new Error('No matching output found!');
504
567
  }
505
568
  const rootTx = Transaction.fromBEEF(output.beef);
@@ -577,6 +640,9 @@ export class Engine {
577
640
  if (typeof historySelector === 'undefined') {
578
641
  return output;
579
642
  }
643
+ if (output.beef === undefined) {
644
+ throw new Error('Output must have associated transaction BEEF!');
645
+ }
580
646
  // Determine if history traversal should continue for the current node
581
647
  let shouldTraverseHistory;
582
648
  if (typeof historySelector !== 'number') {
@@ -597,7 +663,7 @@ export class Engine {
597
663
  const outputsConsumed = output.outputsConsumed;
598
664
  // Find the child outputs for each utxo consumed by the current output
599
665
  const childHistories = (await Promise.all(outputsConsumed.map(async (outputIdentifier) => {
600
- const output = await this.storage.findOutput(outputIdentifier.txid, outputIdentifier.outputIndex);
666
+ const output = await this.storage.findOutput(outputIdentifier.txid, outputIdentifier.outputIndex, undefined, undefined, true);
601
667
  // Make sure an output was found
602
668
  if (output === undefined || output === null) {
603
669
  return undefined;
@@ -615,6 +681,9 @@ export class Engine {
615
681
  : input.sourceTransaction?.id('hex');
616
682
  return sourceTXID === output.txid && input.sourceOutputIndex === output.outputIndex;
617
683
  });
684
+ if (input.beef === undefined) {
685
+ throw new Error('Input must have associated transaction BEEF!');
686
+ }
618
687
  tx.inputs[inputIndex].sourceTransaction = Transaction.fromBEEF(input.beef);
619
688
  }
620
689
  const beef = tx.toBEEF();
@@ -626,9 +695,9 @@ export class Engine {
626
695
  catch (e) {
627
696
  // Handle any errors that occurred
628
697
  // Note: Test this!
629
- console.error(`Error retrieving UTXO history: ${e}`);
698
+ console.error(`Error retrieving UTXO history: ${e} `);
630
699
  // return []
631
- throw new Error(`Error retrieving UTXO history: ${e}`);
700
+ throw new Error(`Error retrieving UTXO history: ${e} `);
632
701
  }
633
702
  }
634
703
  /**
@@ -671,7 +740,7 @@ export class Engine {
671
740
  });
672
741
  }
673
742
  catch (error) {
674
- throw new Error(`Failed to delete all stale outputs: ${error}`);
743
+ throw new Error(`Failed to delete all stale outputs: ${error} `);
675
744
  }
676
745
  }
677
746
  /**
@@ -708,6 +777,9 @@ export class Engine {
708
777
  * @param proof - The merklePath proving txid is a mined transaction hash
709
778
  */
710
779
  async updateMerkleProof(output, txid, proof) {
780
+ if (output.beef === undefined) {
781
+ throw new Error('Output must have associated transaction BEEF!');
782
+ }
711
783
  const tx = Transaction.fromBEEF(output.beef);
712
784
  if (tx.merklePath !== undefined) {
713
785
  // Update the merkle path to handle potential reorgs
@@ -717,10 +789,10 @@ export class Engine {
717
789
  // recursively update all sourceTransactions proven by (txid,proof)
718
790
  this.updateInputProofs(tx, txid, proof);
719
791
  // Update the output's BEEF in the storage DB
720
- await this.storage.updateOutputBeef(output.txid, output.outputIndex, output.topic, tx.toBEEF());
792
+ await this.storage.updateTransactionBEEF(output.txid, tx.toBEEF());
721
793
  // Recursively update the consumedBy outputs
722
794
  for (const consumingOutput of output.consumedBy) {
723
- const consumedOutputs = await this.storage.findOutputsForTransaction(consumingOutput.txid);
795
+ const consumedOutputs = await this.storage.findOutputsForTransaction(consumingOutput.txid, true);
724
796
  for (const consumedOutput of consumedOutputs) {
725
797
  await this.updateMerkleProof(consumedOutput, txid, proof);
726
798
  }
@@ -734,7 +806,7 @@ export class Engine {
734
806
  * @param blockHeight - The block height associated with the incoming merkle proof.
735
807
  */
736
808
  async handleNewMerkleProof(txid, proof, blockHeight) {
737
- const outputs = await this.storage.findOutputsForTransaction(txid);
809
+ const outputs = await this.storage.findOutputsForTransaction(txid, true);
738
810
  if (outputs === undefined || outputs.length === 0) {
739
811
  throw new Error('Could not find matching transaction outputs for proof ingest!');
740
812
  }
@@ -790,389 +862,5 @@ There is currently a bug with the test runner that prevents importing and using
790
862
  Thus, all non-type exports have been moved to Engine.
791
863
  */
792
864
  // TODO: fix bug with imports that break tests. -----[GASP/OverlayGASPRemote.ts]-----
793
- export class OverlayGASPRemote {
794
- endpointURL;
795
- topic;
796
- constructor(endpointURL, topic) {
797
- this.endpointURL = endpointURL;
798
- this.topic = topic;
799
- }
800
- /**
801
- * Given an outgoing initial request, sends the request to the foreign instance and obtains their initial response.
802
- * @param request
803
- * @returns
804
- */
805
- async getInitialResponse(request) {
806
- // Send out an HTTP request to the URL (current host for topic)
807
- // Include the topic in the request
808
- // Parse out response and return correct format
809
- const url = `${this.endpointURL}/requestSyncResponse`;
810
- const response = await fetch(url, {
811
- method: 'POST',
812
- headers: {
813
- 'Content-Type': 'application/json',
814
- 'X-BSV-Topic': this.topic
815
- },
816
- body: JSON.stringify(request)
817
- });
818
- if (!response.ok) {
819
- throw new Error(`HTTP error! Status: ${response.status}`);
820
- }
821
- const result = await response.json();
822
- // Validate and return the response in the correct format
823
- if (!Array.isArray(result.UTXOList) || typeof result.since !== 'number') {
824
- throw new Error('Invalid response format');
825
- }
826
- return {
827
- UTXOList: result.UTXOList.map((utxo) => ({
828
- txid: utxo.txid,
829
- outputIndex: utxo.outputIndex
830
- })),
831
- since: result.since
832
- };
833
- }
834
- /**
835
- * Given an outgoing txid, outputIndex and optional metadata, request the associated GASP node from the foreign instance.
836
- * @param graphID
837
- * @param txid
838
- * @param outputIndex
839
- * @param metadata
840
- * @returns
841
- */
842
- async requestNode(graphID, txid, outputIndex, metadata) {
843
- // Send an HTTP request with the provided info and get back a gaspNode
844
- const url = `${this.endpointURL}/requestForeignGASPNode`;
845
- const body = {
846
- graphID,
847
- txid,
848
- outputIndex,
849
- metadata
850
- };
851
- const response = await fetch(url, {
852
- method: 'POST',
853
- headers: {
854
- 'Content-Type': 'application/json'
855
- },
856
- body: JSON.stringify(body)
857
- });
858
- if (!response.ok) {
859
- throw new Error(`HTTP error! Status: ${response.status}`);
860
- }
861
- const result = await response.json();
862
- // Validate and return the response in the correct format
863
- if (typeof result.graphID !== 'string' || typeof result.rawTx !== 'string' || typeof result.outputIndex !== 'number') {
864
- throw new Error('Invalid response format');
865
- }
866
- const gaspNode = {
867
- graphID: result.graphID,
868
- rawTx: result.rawTx,
869
- outputIndex: result.outputIndex,
870
- proof: result.proof,
871
- txMetadata: result.txMetadata,
872
- outputMetadata: result.outputMetadata,
873
- inputs: result.inputs
874
- };
875
- return gaspNode;
876
- }
877
- // ---- Now optional methods ----
878
- // When are only syncing to them
879
- async getInitialReply(response) {
880
- throw new Error('Function not supported!');
881
- }
882
- // Only used when supporting bidirectional sync.
883
- // Overlay services does not support this.
884
- async submitNode(node) {
885
- throw new Error('Node submission not supported!');
886
- }
887
- }
888
- export class OverlayGASPStorage {
889
- topic;
890
- engine;
891
- maxNodesInGraph;
892
- temporaryGraphNodeRefs = {};
893
- constructor(topic, engine, maxNodesInGraph) {
894
- this.topic = topic;
895
- this.engine = engine;
896
- this.maxNodesInGraph = maxNodesInGraph;
897
- }
898
- /**
899
- *
900
- * @param since
901
- * @returns
902
- */
903
- async findKnownUTXOs(since) {
904
- const UTXOs = await this.engine.storage.findUTXOsForTopic(this.topic, since);
905
- return UTXOs.map(output => ({
906
- txid: output.txid,
907
- outputIndex: output.outputIndex
908
- }));
909
- }
910
- /**
911
- * For a given txid and output index, returns the associated transaction, a merkle proof if the transaction is in a block, and metadata if if requested. If no metadata is requested, metadata hashes on inputs are not returned.
912
- * @param graphID
913
- * @param txid
914
- * @param outputIndex
915
- * @param metadata
916
- * @returns
917
- */
918
- async hydrateGASPNode(graphID, txid, outputIndex, metadata) {
919
- const output = await this.engine.storage.findOutput(txid, outputIndex);
920
- if (output === undefined || output === null) {
921
- throw new Error('No matching output found!');
922
- }
923
- const tx = Transaction.fromBEEF(output.beef);
924
- const rawTx = tx.toHex();
925
- const node = {
926
- rawTx,
927
- graphID,
928
- outputIndex
929
- };
930
- if (tx.merklePath !== undefined) {
931
- node.proof = tx.merklePath.toHex();
932
- }
933
- return node;
934
- }
935
- /**
936
- * For a given node, returns the inputs needed to complete the graph, including whether updated metadata is requested for those inputs.
937
- * @param tx The node for which needed inputs should be found.
938
- * @returns A promise for a mapping of requested input transactions and whether metadata should be provided for each.
939
- */
940
- async findNeededInputs(tx) {
941
- // If there is no Merkle proof, we always need the inputs
942
- const response = {
943
- requestedInputs: {}
944
- };
945
- const parsedTx = Transaction.fromHex(tx.rawTx);
946
- if (tx.proof === undefined) {
947
- for (const input of parsedTx.inputs) {
948
- response.requestedInputs[`${input.sourceTXID}.${input.sourceOutputIndex}`] = {
949
- metadata: false
950
- };
951
- }
952
- return await this.stripAlreadyKnownInputs(response);
953
- }
954
- // Attempt to check if the current transaction is admissible
955
- parsedTx.merklePath = MerklePath.fromHex(tx.proof);
956
- const admittanceResult = await this.engine.managers[this.topic].identifyAdmissibleOutputs(parsedTx.toBEEF(), []);
957
- if (admittanceResult.outputsToAdmit.includes(tx.outputIndex)) {
958
- // The transaction is admissible, no further inputs are needed
959
- }
960
- else {
961
- // The transaction is not admissible, get inputs needed for further verification
962
- // TopicManagers should implement a function to identify which inputs are needed.
963
- if (this.engine.managers[this.topic] !== undefined && typeof this.engine.managers[this.topic].identifyNeededInputs === 'function') {
964
- try {
965
- const neededInputs = await this.engine.managers[this.topic].identifyNeededInputs?.(parsedTx.toBEEF()) ?? [];
966
- for (const input of neededInputs) {
967
- response.requestedInputs[`${input.txid}.${input.outputIndex}`] = {
968
- metadata: false
969
- };
970
- }
971
- return await this.stripAlreadyKnownInputs(response);
972
- }
973
- catch (e) {
974
- console.error(`An error occurred when identifying needed inputs for transaction: ${parsedTx.id('hex')}.${tx.outputIndex}!`);
975
- // Cut off the graph in case of an error here.
976
- }
977
- }
978
- // By default, if the topic manager isn't able to stipulate needed inputs, only the inputs necessary for SPV are requested.
979
- }
980
- // Everything else falls through to returning undefined/void, which will terminate the synchronization at this point.
981
- }
982
- /**
983
- * Ensures that no inputs are requested from foreign nodes before sending any GASP response
984
- * Also terminates graphs if the response would be empty.
985
- */
986
- async stripAlreadyKnownInputs(response) {
987
- if (typeof response === 'undefined') {
988
- return response;
989
- }
990
- for (const inputNodeId of Object.keys(response.requestedInputs)) {
991
- const [txid, outputIndex] = inputNodeId.split('.');
992
- const found = await this.engine.storage.findOutput(txid, Number(outputIndex), this.topic);
993
- if (found) {
994
- delete response.requestedInputs[inputNodeId];
995
- }
996
- }
997
- if (Object.keys(response.requestedInputs).length === 0) {
998
- return undefined;
999
- }
1000
- return response;
1001
- }
1002
- /**
1003
- * Appends a new node to a temporary graph.
1004
- * @param tx The node to append to this graph.
1005
- * @param spentBy Unless this is the same node identified by the graph ID, denotes the TXID and input index for the node which spent this one, in 36-byte format.
1006
- * @throws If the node cannot be appended to the graph, either because the graph ID is for a graph the recipient does not want or because the graph has grown to be too large before being finalized.
1007
- */
1008
- async appendToGraph(tx, spentBy) {
1009
- if (this.maxNodesInGraph !== undefined && Object.keys(this.temporaryGraphNodeRefs).length >= this.maxNodesInGraph) {
1010
- throw new Error('The max number of nodes in transaction graph has been reached!');
1011
- }
1012
- const parsedTx = Transaction.fromHex(tx.rawTx);
1013
- const txid = parsedTx.id('hex');
1014
- if (tx.proof !== undefined) {
1015
- parsedTx.merklePath = MerklePath.fromHex(tx.proof);
1016
- }
1017
- // Given the passed in node, append to the temp graph
1018
- // Use the spentBy param which should be a txid.inputIndex for the node which spent this one in 36-byte format
1019
- const newGraphNode = {
1020
- txid,
1021
- graphID: tx.graphID,
1022
- rawTx: tx.rawTx,
1023
- outputIndex: tx.outputIndex,
1024
- proof: tx.proof,
1025
- txMetadata: tx.txMetadata,
1026
- outputMetadata: tx.outputMetadata,
1027
- inputs: tx.inputs,
1028
- children: []
1029
- };
1030
- // If spentBy is undefined, then we know it's the root node.
1031
- if (spentBy === undefined) {
1032
- this.temporaryGraphNodeRefs[tx.graphID] = newGraphNode;
1033
- }
1034
- else {
1035
- // Find the parent node based on spentBy
1036
- const parentNode = this.temporaryGraphNodeRefs[spentBy];
1037
- if (parentNode !== undefined) {
1038
- // Set parent-child relationship
1039
- parentNode.children.push(newGraphNode);
1040
- newGraphNode.parent = parentNode;
1041
- this.temporaryGraphNodeRefs[`${newGraphNode.txid}.${newGraphNode.outputIndex}`] = newGraphNode;
1042
- }
1043
- else {
1044
- throw new Error(`Parent node with GraphID ${spentBy} not found`);
1045
- }
1046
- }
1047
- }
1048
- /**
1049
- * Checks whether the given graph, in its current state, makes reference only to transactions that are proven in the blockchain, or already known by the recipient to be valid.
1050
- * Additionally, in a breadth-first manner (ensuring that all inputs for any given node are processed before nodes that spend them), it ensures that the root node remains valid according to the rules of the overlay's topic manager,
1051
- * while considering any coins which the Manager had previously indicated were either valid or invalid.
1052
- * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph.
1053
- * @throws If the graph is not well-anchored, according to the rules of Bitcoin or the rules of the Overlay Topic Manager.
1054
- */
1055
- async validateGraphAnchor(graphID) {
1056
- const rootNode = this.temporaryGraphNodeRefs[graphID];
1057
- if (rootNode === undefined) {
1058
- throw new Error(`Graph node with ID ${graphID} not found`);
1059
- }
1060
- // Check that the root node is Bitcoin-valid.
1061
- const beef = this.getBEEFForNode(rootNode);
1062
- const spvTx = Transaction.fromBEEF(beef);
1063
- const isBitcoinValid = await spvTx.verify(this.engine.chainTracker);
1064
- if (!isBitcoinValid) {
1065
- throw new Error('The graph is not well-anchored according to the rules of Bitcoin.');
1066
- }
1067
- // Then, ensure the node is Overlay-valid.
1068
- const beefs = this.computeOrderedBEEFsForGraph(graphID);
1069
- // coins: a Set of all historical coins to retain (no need to remove them), used to emulate topical admittance of previous inputs over time.
1070
- const coins = new Set();
1071
- // Submit all historical BEEFs in order through the topic manager, tracking what would be retained until we submit the root node last.
1072
- // If, at the end, the root node is admitted, we have a valid overlay-specific graph.
1073
- for (const beef of beefs) {
1074
- // For any input to this transaction, see if it's a valid coin that's admitted. If so, it's a previous coin.
1075
- const previousCoins = [];
1076
- const tx = Transaction.fromBEEF(beef);
1077
- for (const inputIndex in tx.inputs) {
1078
- const input = tx.inputs[inputIndex];
1079
- const sourceTXID = input.sourceTXID || input.sourceTransaction?.id('hex');
1080
- const coin = `${sourceTXID}.${input.sourceOutputIndex}`;
1081
- if (coins.has(coin)) {
1082
- previousCoins.push(Number(inputIndex));
1083
- }
1084
- }
1085
- const admittanceInstructions = await this.engine.managers[this.topic].identifyAdmissibleOutputs(beef, previousCoins);
1086
- // Every admitted output is now a coin.
1087
- for (const outputIndex of admittanceInstructions.outputsToAdmit) {
1088
- coins.add(`${tx.id('hex')}.${outputIndex}`);
1089
- }
1090
- }
1091
- // After sending through all the graph's BEEFs...
1092
- // If the root node is now a coin, we have acceptance by the overlay.
1093
- // Otherwise, throw.
1094
- if (!coins.has(graphID)) {
1095
- throw new Error('This graph did not result in topical admittance of the root node. Rejecting.');
1096
- }
1097
- }
1098
- /**
1099
- * Deletes all data associated with a temporary graph that has failed to sync, if the graph exists.
1100
- * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph.
1101
- */
1102
- async discardGraph(graphID) {
1103
- for (const [nodeId, graphRef] of Object.entries(this.temporaryGraphNodeRefs)) {
1104
- if (graphRef.graphID === graphID) {
1105
- // Delete child node
1106
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1107
- delete this.temporaryGraphNodeRefs[nodeId];
1108
- }
1109
- }
1110
- }
1111
- /**
1112
- * Finalizes a graph, solidifying the new UTXO and its ancestors so that it will appear in the list of known UTXOs.
1113
- * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the root of this graph.
1114
- */
1115
- async finalizeGraph(graphID) {
1116
- const beefs = this.computeOrderedBEEFsForGraph(graphID);
1117
- // Submit all historical BEEFs in order, finalizing the graph for the current UTXO
1118
- for (const beef of beefs) {
1119
- await this.engine.submit({
1120
- beef,
1121
- topics: [this.topic]
1122
- }, () => { }, 'historical-tx');
1123
- }
1124
- }
1125
- /**
1126
- * Computes an ordered set of BEEFs for the graph with the given graph IDs
1127
- * @param {string} graphID — The ID of the graph for which BEEFs are required
1128
- * @returns Ordered BEEFs for the graph
1129
- */
1130
- computeOrderedBEEFsForGraph(graphID) {
1131
- const beefs = [];
1132
- const hydrator = (node) => {
1133
- const currentBEEF = this.getBEEFForNode(node);
1134
- if (beefs.indexOf(currentBEEF) === -1) {
1135
- beefs.unshift(currentBEEF);
1136
- }
1137
- for (const child of node.children) {
1138
- // Continue backwards to the earliest nodes, adding them onto the beginning
1139
- hydrator(child);
1140
- }
1141
- };
1142
- // Start the hydrator with the root node
1143
- const foundRoot = this.temporaryGraphNodeRefs[graphID];
1144
- if (!foundRoot) {
1145
- throw new Error('Unable to find root node in graph for finalization!');
1146
- }
1147
- hydrator(foundRoot);
1148
- return beefs;
1149
- }
1150
- /**
1151
- * Computes a full BEEF for a given graph node, based on the temporary graph store.
1152
- * @param node Graph node for which BEEF is needed.
1153
- * @returns BEEF array, including all proofs on inputs.
1154
- */
1155
- getBEEFForNode(node) {
1156
- // Given a node, hydrate its merkle proof or all inputs, returning a reference to the hydrated node's Transaction object
1157
- const hydrator = (node) => {
1158
- const tx = Transaction.fromHex(node.rawTx);
1159
- if (node.proof) {
1160
- tx.merklePath = MerklePath.fromHex(node.proof);
1161
- return tx; // Transaction with proof, end of the line.
1162
- }
1163
- // For each input, look it up and recurse.
1164
- for (const inputIndex in tx.inputs) {
1165
- const input = tx.inputs[inputIndex];
1166
- const foundNode = this.temporaryGraphNodeRefs[`${input.sourceTXID}.${input.sourceOutputIndex}`];
1167
- if (!foundNode) {
1168
- throw new Error('Required input node for unproven parent not found in temporary graph store. Ensure, for every parent of any given already-proven node (kept for Overlay-specific historical reasons), that a proof is also provided on those inputs. While implicitly they are valid by virtue of their descendents being proven in the blockchain, BEEF serialization will still fail when winding forward the topical UTXO set histories during sync.');
1169
- }
1170
- tx.inputs[inputIndex].sourceTransaction = hydrator(foundNode);
1171
- }
1172
- return tx;
1173
- };
1174
- const finalTX = hydrator(node);
1175
- return finalTX.toBEEF();
1176
- }
1177
- }
865
+ // TODO: fix bug with imports that break tests. -----[GASP/OverlayGASPStorage.ts]-----
1178
866
  //# sourceMappingURL=Engine.js.map