@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
package/src/Engine.ts CHANGED
@@ -10,9 +10,11 @@ import { LookupAnswer } from './LookupAnswer.js'
10
10
  import { LookupFormula } from './LookupFormula.js'
11
11
  import { Transaction, ChainTracker, MerklePath, Broadcaster, isBroadcastFailure } from '@bsv/sdk'
12
12
  import { AdvertisementData, Advertiser } from './Advertiser.js'
13
- import { GASP, GASPInitialReply, GASPInitialRequest, GASPInitialResponse, GASPNode, GASPNodeResponse, GASPRemote, GASPStorage } from '@bsv/gasp'
13
+ import { GASP, GASPInitialRequest, GASPInitialResponse, GASPNode } from '@bsv/gasp'
14
14
  import { SyncConfiguration } from './SyncConfiguration.js'
15
15
  import { Advertisement } from './Advertisement.js'
16
+ import { OverlayGASPRemote } from './GASP/OverlayGASPRemote.js'
17
+ import { OverlayGASPStorage } from './GASP/OverlayGASPStorage.js'
16
18
 
17
19
  /**
18
20
  * Am engine for running BSV Overlay Services (topic managers and lookup services).
@@ -29,7 +31,9 @@ export class Engine {
29
31
  * @param {Advertiser} [Advertiser] - handles SHIP and SLAP advertisements for peer-discovery
30
32
  * @param {string} shipTrackers - SHIP domains we know to bootstrap the system
31
33
  * @param {string} slapTrackers - SLAP domains we know to bootstrap the system
32
- * @param {Record<string, string[] | 'SHIP'>} syncConfiguration — Configuration object describing historical synchronization of topics.
34
+ * @param {SyncConfiguration} syncConfiguration — Configuration object describing historical synchronization of topics.
35
+ * @param {boolean} logTime - Enables / disables the timing logs for various operations in the Overlay submit route.
36
+ * @param {string} logPrefix - Supports overriding the log prefix with a custom string.
33
37
  */
34
38
  constructor(
35
39
  public managers: { [key: string]: TopicManager },
@@ -41,17 +45,55 @@ export class Engine {
41
45
  public slapTrackers?: string[],
42
46
  public broadcaster?: Broadcaster,
43
47
  public advertiser?: Advertiser,
44
- public syncConfiguration?: SyncConfiguration
48
+ public syncConfiguration?: SyncConfiguration,
49
+ public logTime = false,
50
+ public logPrefix = '[OVERLAY_ENGINE] '
45
51
  ) {
46
- // To encourage synchronization of overlay services, the SHIP sync strategy is used by default for all topics.
52
+ // 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'.
53
+ // For these two topics, any existing trackers are combined with the provided shipTrackers and slapTrackers omitting any duplicates.
47
54
  if (syncConfiguration === undefined) {
48
55
  this.syncConfiguration = {}
49
- for (const managerName of Object.keys(managers)) {
50
- this.syncConfiguration[managerName] = 'SHIP'
56
+ } else {
57
+ this.syncConfiguration = syncConfiguration
58
+ }
59
+
60
+ for (const managerName of Object.keys(managers)) {
61
+ if (managerName === 'tm_ship' && this.shipTrackers !== undefined && this.syncConfiguration[managerName] !== false) {
62
+ // Combine tm_ship trackers with preexisting entries if any
63
+ const combinedSet = new Set([
64
+ ...(Array.isArray(this.syncConfiguration[managerName]) ? this.syncConfiguration[managerName] as string[] : []),
65
+ ...this.shipTrackers
66
+ ])
67
+ this.syncConfiguration[managerName] = Array.from(combinedSet)
68
+ } else if (managerName === 'tm_slap' && this.slapTrackers !== undefined && this.syncConfiguration[managerName] !== false) {
69
+ // Combine tm_slap trackers with preexisting entries if any
70
+ const combinedSet = new Set([
71
+ ...(Array.isArray(this.syncConfiguration[managerName]) ? this.syncConfiguration[managerName] as string[] : []),
72
+ ...this.slapTrackers
73
+ ])
74
+ this.syncConfiguration[managerName] = Array.from(combinedSet)
75
+ } else {
76
+ // Set undefined managers to 'SHIP' by default
77
+ if (this.syncConfiguration[managerName] === undefined) {
78
+ this.syncConfiguration[managerName] = 'SHIP'
79
+ }
51
80
  }
52
81
  }
53
82
  }
54
83
 
84
+ // Helper functions for logging timings
85
+ private startTime(label: string): void {
86
+ if (this.logTime) {
87
+ console.time(`${this.logPrefix} ${label}`)
88
+ }
89
+ }
90
+
91
+ private endTime(label: string): void {
92
+ if (this.logTime) {
93
+ console.timeEnd(`${this.logPrefix} ${label}`)
94
+ }
95
+ }
96
+
55
97
  /**
56
98
  * Submits a transaction for processing by Overlay Services.
57
99
  * @param {TaggedBEEF} taggedBEEF - The transaction to process
@@ -68,89 +110,107 @@ export class Engine {
68
110
  throw new Error(`This server does not support this topic: ${t}`)
69
111
  }
70
112
  }
113
+
71
114
  // Validate the transaction SPV information
72
115
  const tx = Transaction.fromBEEF(taggedBEEF.beef)
73
116
  const txid = tx.id('hex')
74
- const txValid = await tx.verify(this.chainTracker) // Note: also verifying historical-tx with SPV. Needed?
117
+
118
+ this.startTime(`submit_${txid}`)
119
+ this.startTime(`chainTracker_${txid.substring(0, 10)}`)
120
+ const txValid = await tx.verify(this.chainTracker)
75
121
  if (!txValid) throw new Error('Unable to verify SPV information.')
122
+ this.endTime(`chainTracker_${txid.substring(0, 10)}`)
76
123
 
77
- // Broadcast the transaction if not historical and broadcaster is configured
78
- if (mode !== 'historical-tx' && this.broadcaster !== undefined) {
79
- const response = await this.broadcaster.broadcast(tx)
80
- if (isBroadcastFailure(response)) {
81
- throw new Error(`Failed to broadcast transaction! Error: ${response.description}`)
124
+ const steak: STEAK = {}
125
+ let admissableOutputs: AdmittanceInstructions = { outputsToAdmit: [], coinsToRetain: [] }
126
+ const previousCoins: number[] = []
127
+
128
+ // Parallelize the topic processing
129
+ const topicPromises = taggedBEEF.topics.map(async topic => {
130
+ if (this.managers[topic] === undefined || this.managers[topic] === null) {
131
+ throw new Error(`This server does not support this topic: ${topic}`)
82
132
  }
83
- }
84
133
 
85
- // Find UTXOs belonging to a particular topic
86
- const steak: STEAK = {}
87
- for (const topic of taggedBEEF.topics) {
88
- // Ensure transaction is not already applied to the topic
89
- const dupeCheck = await this.storage.doesAppliedTransactionExist({
90
- txid,
91
- topic
92
- })
134
+ // Check for duplicate transactions
135
+ this.startTime(`dupCheck_${txid.substring(0, 10)}`)
136
+ const dupeCheck = await this.storage.doesAppliedTransactionExist({ txid, topic })
137
+ this.endTime(`dupCheck_${txid.substring(0, 10)}`)
138
+
93
139
  if (dupeCheck) {
94
- // The transaction was already processed.
95
- // Currently, NO OUTPUTS ARE ADMITTED FOR DUPLICATE TRANSACTIONS.
96
- // 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.
97
- // This was decided against, because we don't want to encourage unnecessary flooding of duplicative transactions to overlay services.
98
- steak[topic] = {
99
- outputsToAdmit: [],
100
- coinsToRetain: []
101
- }
102
- continue
140
+ steak[topic] = { outputsToAdmit: [], coinsToRetain: [] }
141
+ return
103
142
  }
104
143
 
105
- // Check if any input of this transaction is a previous UTXO, adding previous UTXOs to the list
106
- const previousCoins: number[] = []
107
- for (const [i, input] of tx.inputs.entries()) {
108
- const previousTXID = input.sourceTXID || input.sourceTransaction?.id('hex') as string
109
- // Check if a previous UTXO exists in the storage medium
110
- const output = await this.storage.findOutput(
111
- previousTXID,
112
- input.sourceOutputIndex,
113
- topic
114
- )
115
- if (output !== undefined && output !== null) {
116
- previousCoins.push(i)
144
+ // Check if any input of this transaction is a previous UTXO
145
+ const outputPromises = tx.inputs.map(async (input, i) => {
146
+ const previousTXID = input.sourceTXID !== undefined ? input.sourceTXID : input.sourceTransaction?.id('hex')
147
+ if (previousTXID !== undefined) {
148
+ const output = this.storage.findOutput(previousTXID, input.sourceOutputIndex, topic)
149
+ if (output !== undefined && output !== null) {
150
+ previousCoins.push(i)
151
+ return await Promise.resolve(output)
152
+ }
153
+ }
154
+ return await Promise.resolve(null)
155
+ })
117
156
 
118
- // This output is now spent.
119
- await this.storage.markUTXOAsSpent(
120
- output.txid,
121
- output.outputIndex,
122
- topic
123
- )
157
+ this.startTime(`previousOutputQuery_${txid.substring(0, 10)}`)
158
+ const outputs = await Promise.all(outputPromises)
159
+ this.endTime(`previousOutputQuery_${txid.substring(0, 10)}`)
124
160
 
125
- // Notify the lookup services about the spending of this output
126
- for (const l of Object.values(this.lookupServices)) {
127
- try {
128
- if (l.outputSpent !== undefined && l.outputSpent !== null) {
129
- await l.outputSpent(
130
- output.txid,
131
- output.outputIndex,
132
- topic
133
- )
161
+ const markSpentPromises = outputs.map(async (output) => {
162
+ if (output !== undefined && output !== null) {
163
+ try {
164
+ await this.storage.markUTXOAsSpent(output.txid, output.outputIndex, topic)
165
+ await Promise.all(Object.values(this.lookupServices).map(async l => {
166
+ try {
167
+ await l.outputSpent?.(output.txid, output.outputIndex, topic)
168
+ } catch (error) {
169
+ console.error('Error in lookup service for outputSpent:', error)
134
170
  }
135
- } catch (_) { }
171
+ }))
172
+ } catch (error) {
173
+ console.error('Error marking UTXO as spent:', error)
136
174
  }
137
175
  }
138
- }
176
+ })
139
177
 
140
- // Use the manager to determine which outputs are admissable
141
- let admissableOutputs: AdmittanceInstructions
142
- try {
143
- admissableOutputs = await this.managers[topic].identifyAdmissibleOutputs(taggedBEEF.beef, previousCoins)
144
- } catch (_) {
145
- // If the topic manager throws an error, other topics may still succeed, so we continue to the next one.
146
- // 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.
147
- steak[topic] = {
148
- outputsToAdmit: [],
149
- coinsToRetain: []
178
+ // Determine which outputs are admissible
179
+ const admissibleOutputsPromise = (async () => {
180
+ try {
181
+ this.startTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`)
182
+ admissableOutputs = await this.managers[topic].identifyAdmissibleOutputs(taggedBEEF.beef, previousCoins)
183
+ this.endTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`)
184
+ } catch (_) {
185
+ steak[topic] = { outputsToAdmit: [], coinsToRetain: [] }
150
186
  }
151
- continue
187
+ })()
188
+
189
+ // Wait for both tasks to complete
190
+ await Promise.all([markSpentPromises, admissibleOutputsPromise])
191
+ // Keep track of what outputs were admitted for what topic
192
+ steak[topic] = admissableOutputs
193
+ })
194
+
195
+ await Promise.all(topicPromises)
196
+
197
+ // Broadcast the transaction if not historical and broadcaster is configured
198
+ this.startTime(`broadcast_${txid.substring(0, 10)}`)
199
+ if (mode !== 'historical-tx' && this.broadcaster !== undefined) {
200
+ const response = await this.broadcaster.broadcast(tx)
201
+ if (isBroadcastFailure(response)) {
202
+ throw new Error(`Failed to broadcast transaction! Error: ${response.description}`)
152
203
  }
204
+ }
205
+ this.endTime(`broadcast_${txid.substring(0, 10)}`)
206
+
207
+ // Call the callback function if it is provided
208
+ if (onSteakReady !== undefined) {
209
+ this.endTime(`submit_${txid}`)
210
+ onSteakReady(steak)
211
+ }
153
212
 
213
+ for (const topic of taggedBEEF.topics) {
154
214
  // Keep track of which outputs to admit, mark as stale, or retain
155
215
  const outputsToAdmit: number[] = admissableOutputs.outputsToAdmit
156
216
  const staleCoins: Array<{
@@ -181,17 +241,19 @@ export class Engine {
181
241
  }
182
242
 
183
243
  // Remove stale outputs recursively
184
- for (const coin of staleCoins) {
244
+ this.startTime(`lookForStaleOutputs_${txid.substring(0, 10)}`)
245
+ await Promise.all(staleCoins.map(async coin => {
185
246
  const output = await this.storage.findOutput(coin.txid, coin.outputIndex, topic)
186
247
  if (output !== undefined && output !== null) {
187
248
  await this.deleteUTXODeep(output)
188
249
  }
189
- }
250
+ }))
251
+ this.endTime(`lookForStaleOutputs_${txid.substring(0, 10)}`)
190
252
 
191
253
  // Handle admittance and notification of incoming UTXOs
192
254
  const newUTXOs: Array<{ txid: string, outputIndex: number }> = []
193
- for (const outputIndex of outputsToAdmit) {
194
- // Store the output
255
+ await Promise.all(outputsToAdmit.map(async outputIndex => {
256
+ this.startTime(`insertNewOutput_${txid.substring(0, 10)}`)
195
257
  await this.storage.insertOutput({
196
258
  txid,
197
259
  outputIndex,
@@ -203,45 +265,30 @@ export class Engine {
203
265
  consumedBy: [],
204
266
  outputsConsumed
205
267
  })
206
- newUTXOs.push({
268
+ this.endTime(`insertNewOutput_${txid.substring(0, 10)}`)
269
+ newUTXOs.push({ txid, outputIndex })
270
+
271
+ this.startTime(`notifyLookupService${txid.substring(0, 10)}`)
272
+ await Promise.all(Object.values(this.lookupServices).map(async l => await l.outputAdded?.(txid, outputIndex, tx.outputs[outputIndex].lockingScript, topic)))
273
+ this.endTime(`notifyLookupService${txid.substring(0, 10)}`)
274
+ }))
275
+
276
+ this.startTime(`outputConsumed_${txid.substring(0, 10)}`)
277
+ // Update each output consumed to know who consumed it and insert applied transaction in parallel
278
+ await Promise.all([
279
+ ...outputsConsumed.map(async output => {
280
+ const outputToUpdate = await this.storage.findOutput(output.txid, output.outputIndex, topic)
281
+ if (outputToUpdate !== undefined && outputToUpdate !== null) {
282
+ const newConsumedBy = [...new Set([...newUTXOs, ...outputToUpdate.consumedBy])]
283
+ await this.storage.updateConsumedBy(output.txid, output.outputIndex, topic, newConsumedBy)
284
+ }
285
+ }),
286
+ this.storage.insertAppliedTransaction({
207
287
  txid,
208
- outputIndex
288
+ topic
209
289
  })
210
-
211
- // Notify all the lookup services about the new UTXO
212
- for (const l of Object.values(this.lookupServices)) {
213
- try {
214
- if (l.outputAdded !== undefined && l.outputAdded !== null) {
215
- await l.outputAdded(txid, outputIndex, tx.outputs[outputIndex].lockingScript, topic)
216
- }
217
- } catch (_) { }
218
- }
219
- }
220
-
221
- // Update each output consumed to know who consumed it
222
- for (const output of outputsConsumed) {
223
- const outputToUpdate = await this.storage.findOutput(output.txid, output.outputIndex, topic)
224
- if (outputToUpdate !== undefined && outputToUpdate !== null) {
225
- const newConsumedBy = [...new Set([...newUTXOs, ...outputToUpdate.consumedBy])]
226
- // Note: only update if newConsumedBy !== new Set(JSON.parse(outputToUpdate.consumedBy)) ?
227
- await this.storage.updateConsumedBy(output.txid, output.outputIndex, topic, newConsumedBy)
228
- }
229
- }
230
-
231
- // Insert the applied transaction to prevent duplicate processing
232
- await this.storage.insertAppliedTransaction({
233
- txid,
234
- topic
235
- })
236
-
237
- // Keep track of what outputs were admitted for what topic
238
- steak[topic] = admissableOutputs
239
- }
240
-
241
- // Call the callback function if it is provided
242
- // 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.
243
- if (onSteakReady !== undefined) {
244
- onSteakReady(steak)
290
+ ])
291
+ this.endTime(`outputConsumed_${txid.substring(0, 10)}`)
245
292
  }
246
293
 
247
294
  // If we don't have an advertiser or we are dealing with historical transactions, just return the steak
@@ -249,6 +296,7 @@ export class Engine {
249
296
  return steak
250
297
  }
251
298
 
299
+ this.startTime(`transactionPropagation_${txid.substring(0, 10)}`)
252
300
  // Propagate transaction to other nodes according to synchronization agreements
253
301
  // 1. Find nodes that host the topics associated with admissable outputs
254
302
  // We want to figure out which topics we actually care about (because their associated outputs were admitted)
@@ -257,6 +305,7 @@ export class Engine {
257
305
  steak[topic] !== undefined && steak[topic].outputsToAdmit.length !== 0
258
306
  )
259
307
 
308
+ // TODO: Cache ship/slap lookup with expiry (every 5min)
260
309
  if (relevantTopics.length > 0) {
261
310
  // Find all SHIP advertisements for the topics we care about
262
311
  const domainToTopicsMap = new Map<string, Set<string>>()
@@ -344,6 +393,7 @@ export class Engine {
344
393
  console.error('Error during broadcasting:', error)
345
394
  }
346
395
  }
396
+ this.endTime(`transactionPropgation_${txid.substring(0, 10)}`)
347
397
 
348
398
  // Immediately return from the function without waiting for the promises to resolve.
349
399
  return steak
@@ -357,7 +407,7 @@ export class Engine {
357
407
  async lookup(lookupQuestion: LookupQuestion): Promise<LookupAnswer> {
358
408
  // Validate a lookup service for the provider is found
359
409
  const lookupService = this.lookupServices[lookupQuestion.service]
360
- if (lookupService === undefined || lookupService === null) throw new Error(`Lookup service not found for provider: ${lookupQuestion.service}`)
410
+ if (lookupService === undefined || lookupService === null) throw new Error(`Lookup service not found for provider: ${lookupQuestion.service} `)
361
411
 
362
412
  let lookupResult = await lookupService.lookup(lookupQuestion)
363
413
  // Handle custom lookup service answers
@@ -374,13 +424,14 @@ export class Engine {
374
424
  txid,
375
425
  outputIndex,
376
426
  undefined,
377
- false
427
+ undefined,
428
+ true
378
429
  )
379
430
  if (UTXO === undefined || UTXO === null) continue
380
431
 
381
432
  // Get the history for this utxo and construct a BRC-8 Envelope
382
433
  const output = await this.getUTXOHistory(UTXO, history, 0)
383
- if (output !== undefined && output !== null) {
434
+ if (output?.beef !== undefined) {
384
435
  hydratedOutputs.push({
385
436
  beef: output.beef,
386
437
  outputIndex: output.outputIndex
@@ -483,7 +534,12 @@ export class Engine {
483
534
 
484
535
  for (const topic of Object.keys(this.syncConfiguration)) {
485
536
  // Make sure syncEndpoints is an array or SHIP
486
- let syncEndpoints: string[] | string = this.syncConfiguration[topic]
537
+ let syncEndpoints: string[] | string | false = this.syncConfiguration[topic]
538
+
539
+ // Check if this topic has been configured NOT to sync
540
+ if (syncEndpoints === false) {
541
+ continue
542
+ }
487
543
 
488
544
  if (syncEndpoints === 'SHIP') {
489
545
  // Perform lookup and find ship advertisements to set syncEndpoints for topic
@@ -520,7 +576,7 @@ export class Engine {
520
576
  if (Array.isArray(syncEndpoints)) {
521
577
  await Promise.all(syncEndpoints.map(async endpoint => {
522
578
  // Sync to each host that is associated with this topic
523
- const gasp = new GASP(new OverlayGASPStorage(topic, this), new OverlayGASPRemote(endpoint, topic), 0, `[GASP Sync of ${topic} with ${endpoint}] `, true)
579
+ const gasp = new GASP(new OverlayGASPStorage(topic, this), new OverlayGASPRemote(endpoint, topic), 0, `[GASP Sync of ${topic} with ${endpoint}]`, true)
524
580
  await gasp.sync()
525
581
  }))
526
582
  }
@@ -563,7 +619,7 @@ export class Engine {
563
619
  */
564
620
  async provideForeignGASPNode(graphID: string, txid: string, outputIndex: number): Promise<GASPNode> {
565
621
  const hydrator = async (output: Output | null): Promise<GASPNode> => {
566
- if (output === undefined || output === null) {
622
+ if (output?.beef === undefined) {
567
623
  throw new Error('No matching output found!')
568
624
  }
569
625
 
@@ -649,6 +705,10 @@ export class Engine {
649
705
  return output
650
706
  }
651
707
 
708
+ if (output.beef === undefined) {
709
+ throw new Error('Output must have associated transaction BEEF!')
710
+ }
711
+
652
712
  // Determine if history traversal should continue for the current node
653
713
  let shouldTraverseHistory
654
714
  if (typeof historySelector !== 'number') {
@@ -671,7 +731,7 @@ export class Engine {
671
731
  // Find the child outputs for each utxo consumed by the current output
672
732
  const childHistories = (await Promise.all(
673
733
  outputsConsumed.map(async (outputIdentifier) => {
674
- const output = await this.storage.findOutput(outputIdentifier.txid, outputIdentifier.outputIndex)
734
+ const output = await this.storage.findOutput(outputIdentifier.txid, outputIdentifier.outputIndex, undefined, undefined, true)
675
735
 
676
736
  // Make sure an output was found
677
737
  if (output === undefined || output === null) {
@@ -692,6 +752,9 @@ export class Engine {
692
752
  : input.sourceTransaction?.id('hex')
693
753
  return sourceTXID === output.txid && input.sourceOutputIndex === output.outputIndex
694
754
  })
755
+ if (input.beef === undefined) {
756
+ throw new Error('Input must have associated transaction BEEF!')
757
+ }
695
758
  tx.inputs[inputIndex].sourceTransaction = Transaction.fromBEEF(input.beef)
696
759
  }
697
760
  const beef = tx.toBEEF()
@@ -702,9 +765,9 @@ export class Engine {
702
765
  } catch (e) {
703
766
  // Handle any errors that occurred
704
767
  // Note: Test this!
705
- console.error(`Error retrieving UTXO history: ${e}`)
768
+ console.error(`Error retrieving UTXO history: ${e} `)
706
769
  // return []
707
- throw new Error(`Error retrieving UTXO history: ${e}`)
770
+ throw new Error(`Error retrieving UTXO history: ${e} `)
708
771
  }
709
772
  }
710
773
 
@@ -756,7 +819,7 @@ export class Engine {
756
819
  return await this.deleteUTXODeep(staleOutput)
757
820
  })
758
821
  } catch (error) {
759
- throw new Error(`Failed to delete all stale outputs: ${error as string}`)
822
+ throw new Error(`Failed to delete all stale outputs: ${error as string} `)
760
823
  }
761
824
  }
762
825
 
@@ -794,6 +857,10 @@ export class Engine {
794
857
  * @param proof - The merklePath proving txid is a mined transaction hash
795
858
  */
796
859
  private async updateMerkleProof(output: Output, txid: string, proof: MerklePath): Promise<void> {
860
+ if (output.beef === undefined) {
861
+ throw new Error('Output must have associated transaction BEEF!')
862
+ }
863
+
797
864
  const tx = Transaction.fromBEEF(output.beef)
798
865
  if (tx.merklePath !== undefined) {
799
866
  // Update the merkle path to handle potential reorgs
@@ -805,11 +872,11 @@ export class Engine {
805
872
  this.updateInputProofs(tx, txid, proof)
806
873
 
807
874
  // Update the output's BEEF in the storage DB
808
- await this.storage.updateOutputBeef(output.txid, output.outputIndex, output.topic, tx.toBEEF())
875
+ await this.storage.updateTransactionBEEF(output.txid, tx.toBEEF())
809
876
 
810
877
  // Recursively update the consumedBy outputs
811
878
  for (const consumingOutput of output.consumedBy) {
812
- const consumedOutputs = await this.storage.findOutputsForTransaction(consumingOutput.txid)
879
+ const consumedOutputs = await this.storage.findOutputsForTransaction(consumingOutput.txid, true)
813
880
  for (const consumedOutput of consumedOutputs) {
814
881
  await this.updateMerkleProof(consumedOutput, txid, proof)
815
882
  }
@@ -824,7 +891,7 @@ export class Engine {
824
891
  * @param blockHeight - The block height associated with the incoming merkle proof.
825
892
  */
826
893
  async handleNewMerkleProof(txid: string, proof: MerklePath, blockHeight?: number): Promise<void> {
827
- const outputs = await this.storage.findOutputsForTransaction(txid)
894
+ const outputs = await this.storage.findOutputsForTransaction(txid, true)
828
895
 
829
896
  if (outputs === undefined || outputs.length === 0) {
830
897
  throw new Error('Could not find matching transaction outputs for proof ingest!')
@@ -891,437 +958,6 @@ Thus, all non-type exports have been moved to Engine.
891
958
 
892
959
  // TODO: fix bug with imports that break tests. -----[GASP/OverlayGASPRemote.ts]-----
893
960
 
894
- export class OverlayGASPRemote implements GASPRemote {
895
- constructor(public endpointURL: string, public topic: string) { }
896
961
 
897
- /**
898
- * Given an outgoing initial request, sends the request to the foreign instance and obtains their initial response.
899
- * @param request
900
- * @returns
901
- */
902
- async getInitialResponse(request: GASPInitialRequest): Promise<GASPInitialResponse> {
903
- // Send out an HTTP request to the URL (current host for topic)
904
- // Include the topic in the request
905
- // Parse out response and return correct format
906
- const url = `${this.endpointURL}/requestSyncResponse`
907
- const response = await fetch(url, {
908
- method: 'POST',
909
- headers: {
910
- 'Content-Type': 'application/json',
911
- 'X-BSV-Topic': this.topic
912
- },
913
- body: JSON.stringify(request)
914
- })
915
-
916
- if (!response.ok) {
917
- throw new Error(`HTTP error! Status: ${response.status}`)
918
- }
919
-
920
- const result: GASPInitialResponse = await response.json()
921
-
922
- // Validate and return the response in the correct format
923
- if (!Array.isArray(result.UTXOList) || typeof result.since !== 'number') {
924
- throw new Error('Invalid response format')
925
- }
926
-
927
- return {
928
- UTXOList: result.UTXOList.map((utxo: any) => ({
929
- txid: utxo.txid,
930
- outputIndex: utxo.outputIndex
931
- })),
932
- since: result.since
933
- }
934
- }
935
-
936
- /**
937
- * Given an outgoing txid, outputIndex and optional metadata, request the associated GASP node from the foreign instance.
938
- * @param graphID
939
- * @param txid
940
- * @param outputIndex
941
- * @param metadata
942
- * @returns
943
- */
944
- async requestNode(graphID: string, txid: string, outputIndex: number, metadata: boolean): Promise<GASPNode> {
945
- // Send an HTTP request with the provided info and get back a gaspNode
946
- const url = `${this.endpointURL}/requestForeignGASPNode`
947
- const body = {
948
- graphID,
949
- txid,
950
- outputIndex,
951
- metadata
952
- }
953
-
954
- const response = await fetch(url, {
955
- method: 'POST',
956
- headers: {
957
- 'Content-Type': 'application/json'
958
- },
959
- body: JSON.stringify(body)
960
- })
961
-
962
- if (!response.ok) {
963
- throw new Error(`HTTP error! Status: ${response.status}`)
964
- }
965
-
966
- const result = await response.json()
967
-
968
- // Validate and return the response in the correct format
969
- if (typeof result.graphID !== 'string' || typeof result.rawTx !== 'string' || typeof result.outputIndex !== 'number') {
970
- throw new Error('Invalid response format')
971
- }
972
-
973
- const gaspNode: GASPNode = {
974
- graphID: result.graphID,
975
- rawTx: result.rawTx,
976
- outputIndex: result.outputIndex,
977
- proof: result.proof,
978
- txMetadata: result.txMetadata,
979
- outputMetadata: result.outputMetadata,
980
- inputs: result.inputs
981
- }
982
-
983
- return gaspNode
984
- }
985
-
986
- // ---- Now optional methods ----
987
-
988
- // When are only syncing to them
989
- async getInitialReply(response: GASPInitialResponse): Promise<GASPInitialReply> {
990
- throw new Error('Function not supported!')
991
- }
992
-
993
- // Only used when supporting bidirectional sync.
994
- // Overlay services does not support this.
995
- async submitNode(node: GASPNode): Promise<void | GASPNodeResponse> {
996
- throw new Error('Node submission not supported!')
997
- }
998
- }
999
962
 
1000
963
  // TODO: fix bug with imports that break tests. -----[GASP/OverlayGASPStorage.ts]-----
1001
-
1002
- /**
1003
- * Represents a node in the temporary graph.
1004
- */
1005
- export interface GraphNode {
1006
- txid: string
1007
- graphID: string
1008
- rawTx: string
1009
- outputIndex: number
1010
- spentBy?: string
1011
- proof?: string
1012
- txMetadata?: string
1013
- outputMetadata?: string
1014
- inputs?: Record<string, { hash: string }> | undefined
1015
- children: GraphNode[]
1016
- parent?: GraphNode
1017
- }
1018
-
1019
- export class OverlayGASPStorage implements GASPStorage {
1020
- readonly temporaryGraphNodeRefs: Record<string, GraphNode> = {}
1021
-
1022
- constructor(public topic: string, public engine: Engine, public maxNodesInGraph?: number) { }
1023
-
1024
- /**
1025
- *
1026
- * @param since
1027
- * @returns
1028
- */
1029
- async findKnownUTXOs(since: number): Promise<Array<{ txid: string, outputIndex: number }>> {
1030
- const UTXOs = await this.engine.storage.findUTXOsForTopic(this.topic, since)
1031
- return UTXOs.map(output => ({
1032
- txid: output.txid,
1033
- outputIndex: output.outputIndex
1034
- }))
1035
- }
1036
-
1037
- /**
1038
- * 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.
1039
- * @param graphID
1040
- * @param txid
1041
- * @param outputIndex
1042
- * @param metadata
1043
- * @returns
1044
- */
1045
- async hydrateGASPNode(graphID: string, txid: string, outputIndex: number, metadata: boolean): Promise<GASPNode> {
1046
- const output = await this.engine.storage.findOutput(txid, outputIndex)
1047
-
1048
- if (output === undefined || output === null) {
1049
- throw new Error('No matching output found!')
1050
- }
1051
-
1052
- const tx = Transaction.fromBEEF(output.beef)
1053
- const rawTx = tx.toHex()
1054
-
1055
- const node: GASPNode = {
1056
- rawTx,
1057
- graphID,
1058
- outputIndex
1059
- }
1060
- if (tx.merklePath !== undefined) {
1061
- node.proof = tx.merklePath.toHex()
1062
- }
1063
-
1064
- return node
1065
- }
1066
-
1067
- /**
1068
- * For a given node, returns the inputs needed to complete the graph, including whether updated metadata is requested for those inputs.
1069
- * @param tx The node for which needed inputs should be found.
1070
- * @returns A promise for a mapping of requested input transactions and whether metadata should be provided for each.
1071
- */
1072
- async findNeededInputs(tx: GASPNode): Promise<GASPNodeResponse | undefined> {
1073
- // If there is no Merkle proof, we always need the inputs
1074
- const response: GASPNodeResponse = {
1075
- requestedInputs: {}
1076
- }
1077
- const parsedTx = Transaction.fromHex(tx.rawTx)
1078
- if (tx.proof === undefined) {
1079
- for (const input of parsedTx.inputs) {
1080
- response.requestedInputs[`${input.sourceTXID}.${input.sourceOutputIndex}`] = {
1081
- metadata: false
1082
- }
1083
- }
1084
-
1085
- return await this.stripAlreadyKnownInputs(response)
1086
- }
1087
-
1088
- // Attempt to check if the current transaction is admissible
1089
- parsedTx.merklePath = MerklePath.fromHex(tx.proof)
1090
- const admittanceResult = await this.engine.managers[this.topic].identifyAdmissibleOutputs(parsedTx.toBEEF(), [])
1091
-
1092
- if (admittanceResult.outputsToAdmit.includes(tx.outputIndex)) {
1093
- // The transaction is admissible, no further inputs are needed
1094
- } else {
1095
- // The transaction is not admissible, get inputs needed for further verification
1096
- // TopicManagers should implement a function to identify which inputs are needed.
1097
- if (this.engine.managers[this.topic] !== undefined && typeof this.engine.managers[this.topic].identifyNeededInputs === 'function') {
1098
- try {
1099
- const neededInputs = await this.engine.managers[this.topic].identifyNeededInputs?.(parsedTx.toBEEF()) ?? []
1100
- for (const input of neededInputs) {
1101
- response.requestedInputs[`${input.txid}.${input.outputIndex}`] = {
1102
- metadata: false
1103
- }
1104
- }
1105
- return await this.stripAlreadyKnownInputs(response)
1106
- } catch (e) {
1107
- console.error(`An error occurred when identifying needed inputs for transaction: ${parsedTx.id('hex')}.${tx.outputIndex}!`)
1108
- // Cut off the graph in case of an error here.
1109
- }
1110
- }
1111
- // By default, if the topic manager isn't able to stipulate needed inputs, only the inputs necessary for SPV are requested.
1112
- }
1113
- // Everything else falls through to returning undefined/void, which will terminate the synchronization at this point.
1114
- }
1115
-
1116
- /**
1117
- * Ensures that no inputs are requested from foreign nodes before sending any GASP response
1118
- * Also terminates graphs if the response would be empty.
1119
- */
1120
- private async stripAlreadyKnownInputs(response: GASPNodeResponse | undefined): Promise<GASPNodeResponse | undefined> {
1121
- if (typeof response === 'undefined') {
1122
- return response
1123
- }
1124
- for (const inputNodeId of Object.keys(response.requestedInputs)) {
1125
- const [txid, outputIndex] = inputNodeId.split('.')
1126
- const found = await this.engine.storage.findOutput(txid, Number(outputIndex), this.topic)
1127
- if (found) {
1128
- delete response.requestedInputs[inputNodeId]
1129
- }
1130
- }
1131
- if (Object.keys(response.requestedInputs).length === 0) {
1132
- return undefined
1133
- }
1134
- return response
1135
- }
1136
-
1137
- /**
1138
- * Appends a new node to a temporary graph.
1139
- * @param tx The node to append to this graph.
1140
- * @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.
1141
- * @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.
1142
- */
1143
- async appendToGraph(tx: GASPNode, spentBy?: string | undefined): Promise<void> {
1144
- if (this.maxNodesInGraph !== undefined && Object.keys(this.temporaryGraphNodeRefs).length >= this.maxNodesInGraph) {
1145
- throw new Error('The max number of nodes in transaction graph has been reached!')
1146
- }
1147
-
1148
- const parsedTx = Transaction.fromHex(tx.rawTx)
1149
- const txid = parsedTx.id('hex')
1150
- if (tx.proof !== undefined) {
1151
- parsedTx.merklePath = MerklePath.fromHex(tx.proof)
1152
- }
1153
-
1154
- // Given the passed in node, append to the temp graph
1155
- // Use the spentBy param which should be a txid.inputIndex for the node which spent this one in 36-byte format
1156
- const newGraphNode: GraphNode = {
1157
- txid,
1158
- graphID: tx.graphID,
1159
- rawTx: tx.rawTx,
1160
- outputIndex: tx.outputIndex,
1161
- proof: tx.proof,
1162
- txMetadata: tx.txMetadata,
1163
- outputMetadata: tx.outputMetadata,
1164
- inputs: tx.inputs,
1165
- children: []
1166
- }
1167
-
1168
- // If spentBy is undefined, then we know it's the root node.
1169
- if (spentBy === undefined) {
1170
- this.temporaryGraphNodeRefs[tx.graphID] = newGraphNode
1171
- } else {
1172
- // Find the parent node based on spentBy
1173
- const parentNode = this.temporaryGraphNodeRefs[spentBy]
1174
-
1175
- if (parentNode !== undefined) {
1176
- // Set parent-child relationship
1177
- parentNode.children.push(newGraphNode)
1178
- newGraphNode.parent = parentNode
1179
- this.temporaryGraphNodeRefs[`${newGraphNode.txid}.${newGraphNode.outputIndex}`] = newGraphNode
1180
- } else {
1181
- throw new Error(`Parent node with GraphID ${spentBy} not found`)
1182
- }
1183
- }
1184
- }
1185
-
1186
- /**
1187
- * 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.
1188
- * 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,
1189
- * while considering any coins which the Manager had previously indicated were either valid or invalid.
1190
- * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph.
1191
- * @throws If the graph is not well-anchored, according to the rules of Bitcoin or the rules of the Overlay Topic Manager.
1192
- */
1193
- async validateGraphAnchor(graphID: string): Promise<void> {
1194
- const rootNode = this.temporaryGraphNodeRefs[graphID]
1195
- if (rootNode === undefined) {
1196
- throw new Error(`Graph node with ID ${graphID} not found`)
1197
- }
1198
-
1199
- // Check that the root node is Bitcoin-valid.
1200
- const beef = this.getBEEFForNode(rootNode)
1201
- const spvTx = Transaction.fromBEEF(beef)
1202
- const isBitcoinValid = await spvTx.verify(this.engine.chainTracker)
1203
- if (!isBitcoinValid) {
1204
- throw new Error('The graph is not well-anchored according to the rules of Bitcoin.')
1205
- }
1206
-
1207
- // Then, ensure the node is Overlay-valid.
1208
- const beefs = this.computeOrderedBEEFsForGraph(graphID)
1209
-
1210
- // coins: a Set of all historical coins to retain (no need to remove them), used to emulate topical admittance of previous inputs over time.
1211
- const coins = new Set<string>()
1212
-
1213
- // Submit all historical BEEFs in order through the topic manager, tracking what would be retained until we submit the root node last.
1214
- // If, at the end, the root node is admitted, we have a valid overlay-specific graph.
1215
- for (const beef of beefs) {
1216
- // For any input to this transaction, see if it's a valid coin that's admitted. If so, it's a previous coin.
1217
- const previousCoins: number[] = []
1218
- const tx = Transaction.fromBEEF(beef)
1219
- for (const inputIndex in tx.inputs) {
1220
- const input = tx.inputs[inputIndex]
1221
- const sourceTXID = input.sourceTXID || input.sourceTransaction?.id('hex')
1222
- const coin = `${sourceTXID}.${input.sourceOutputIndex}`
1223
- if (coins.has(coin)) {
1224
- previousCoins.push(Number(inputIndex))
1225
- }
1226
- }
1227
- const admittanceInstructions = await this.engine.managers[this.topic].identifyAdmissibleOutputs(beef, previousCoins)
1228
- // Every admitted output is now a coin.
1229
- for (const outputIndex of admittanceInstructions.outputsToAdmit) {
1230
- coins.add(`${tx.id('hex')}.${outputIndex}`)
1231
- }
1232
- }
1233
- // After sending through all the graph's BEEFs...
1234
- // If the root node is now a coin, we have acceptance by the overlay.
1235
- // Otherwise, throw.
1236
- if (!coins.has(graphID)) {
1237
- throw new Error('This graph did not result in topical admittance of the root node. Rejecting.')
1238
- }
1239
- }
1240
-
1241
- /**
1242
- * Deletes all data associated with a temporary graph that has failed to sync, if the graph exists.
1243
- * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph.
1244
- */
1245
- async discardGraph(graphID: string): Promise<void> {
1246
- for (const [nodeId, graphRef] of Object.entries(this.temporaryGraphNodeRefs)) {
1247
- if (graphRef.graphID === graphID) {
1248
- // Delete child node
1249
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1250
- delete this.temporaryGraphNodeRefs[nodeId]
1251
- }
1252
- }
1253
- }
1254
-
1255
- /**
1256
- * Finalizes a graph, solidifying the new UTXO and its ancestors so that it will appear in the list of known UTXOs.
1257
- * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the root of this graph.
1258
- */
1259
- async finalizeGraph(graphID: string): Promise<void> {
1260
- const beefs = this.computeOrderedBEEFsForGraph(graphID)
1261
-
1262
- // Submit all historical BEEFs in order, finalizing the graph for the current UTXO
1263
- for (const beef of beefs) {
1264
- await this.engine.submit({
1265
- beef,
1266
- topics: [this.topic]
1267
- }, () => { }, 'historical-tx')
1268
- }
1269
- }
1270
-
1271
- /**
1272
- * Computes an ordered set of BEEFs for the graph with the given graph IDs
1273
- * @param {string} graphID — The ID of the graph for which BEEFs are required
1274
- * @returns Ordered BEEFs for the graph
1275
- */
1276
- private computeOrderedBEEFsForGraph(graphID: string): number[][] {
1277
- const beefs: number[][] = []
1278
- const hydrator = (node: GraphNode): void => {
1279
- const currentBEEF = this.getBEEFForNode(node)
1280
- if (beefs.indexOf(currentBEEF) === -1) {
1281
- beefs.unshift(currentBEEF)
1282
- }
1283
-
1284
- for (const child of node.children) {
1285
- // Continue backwards to the earliest nodes, adding them onto the beginning
1286
- hydrator(child)
1287
- }
1288
- }
1289
-
1290
- // Start the hydrator with the root node
1291
- const foundRoot = this.temporaryGraphNodeRefs[graphID]
1292
- if (!foundRoot) {
1293
- throw new Error('Unable to find root node in graph for finalization!')
1294
- }
1295
- hydrator(foundRoot)
1296
- return beefs
1297
- }
1298
-
1299
- /**
1300
- * Computes a full BEEF for a given graph node, based on the temporary graph store.
1301
- * @param node Graph node for which BEEF is needed.
1302
- * @returns BEEF array, including all proofs on inputs.
1303
- */
1304
- private getBEEFForNode(node: GraphNode): number[] {
1305
- // Given a node, hydrate its merkle proof or all inputs, returning a reference to the hydrated node's Transaction object
1306
- const hydrator = (node: GraphNode): Transaction => {
1307
- const tx = Transaction.fromHex(node.rawTx)
1308
- if (node.proof) {
1309
- tx.merklePath = MerklePath.fromHex(node.proof)
1310
- return tx // Transaction with proof, end of the line.
1311
- }
1312
- // For each input, look it up and recurse.
1313
- for (const inputIndex in tx.inputs) {
1314
- const input = tx.inputs[inputIndex]
1315
- const foundNode = this.temporaryGraphNodeRefs[`${input.sourceTXID}.${input.sourceOutputIndex}`]
1316
- if (!foundNode) {
1317
- 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.')
1318
- }
1319
- tx.inputs[inputIndex].sourceTransaction = hydrator(foundNode)
1320
- }
1321
- return tx
1322
- }
1323
-
1324
- const finalTX = hydrator(node)
1325
- return finalTX.toBEEF()
1326
- }
1327
- }