@bsv/overlay 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/package.json +2 -2
- package/dist/cjs/src/Engine.js +153 -493
- package/dist/cjs/src/Engine.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPRemote.js +6 -4
- package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPStorage.js +152 -77
- package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/cjs/src/storage/knex/KnexStorage.js +106 -38
- package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/cjs/src/storage/knex/all-migrations.js +5 -1
- package/dist/cjs/src/storage/knex/all-migrations.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2024-07-17-001-transactions.js +39 -0
- package/dist/cjs/src/storage/knex/migrations/2024-07-17-001-transactions.js.map +1 -0
- package/dist/cjs/src/storage/knex/migrations/2024-07-18-001-indexes.js +34 -0
- package/dist/cjs/src/storage/knex/migrations/2024-07-18-001-indexes.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/Engine.js +151 -492
- package/dist/esm/src/Engine.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPRemote.js +7 -4
- package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPStorage.js +151 -77
- package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/esm/src/storage/knex/KnexStorage.js +106 -38
- package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/esm/src/storage/knex/all-migrations.js +5 -1
- package/dist/esm/src/storage/knex/all-migrations.js.map +1 -1
- package/dist/esm/src/storage/knex/migrations/2024-07-17-001-transactions.js +34 -0
- package/dist/esm/src/storage/knex/migrations/2024-07-17-001-transactions.js.map +1 -0
- package/dist/esm/src/storage/knex/migrations/2024-07-18-001-indexes.js +29 -0
- package/dist/esm/src/storage/knex/migrations/2024-07-18-001-indexes.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/Engine.d.ts +10 -117
- package/dist/types/src/Engine.d.ts.map +1 -1
- package/dist/types/src/GASP/OverlayGASPRemote.d.ts +3 -2
- package/dist/types/src/GASP/OverlayGASPRemote.d.ts.map +1 -1
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts +29 -3
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
- package/dist/types/src/Output.d.ts +2 -2
- package/dist/types/src/Output.d.ts.map +1 -1
- package/dist/types/src/storage/Storage.d.ts +6 -8
- package/dist/types/src/storage/Storage.d.ts.map +1 -1
- package/dist/types/src/storage/knex/KnexStorage.d.ts +4 -4
- package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
- package/dist/types/src/storage/knex/all-migrations.d.ts.map +1 -1
- package/dist/types/src/storage/knex/migrations/2024-07-17-001-transactions.d.ts +4 -0
- package/dist/types/src/storage/knex/migrations/2024-07-17-001-transactions.d.ts.map +1 -0
- package/dist/types/src/storage/knex/migrations/2024-07-18-001-indexes.d.ts +4 -0
- package/dist/types/src/storage/knex/migrations/2024-07-18-001-indexes.d.ts.map +1 -0
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/Engine.ts +160 -552
- package/src/GASP/OverlayGASPRemote.ts +107 -0
- package/src/GASP/OverlayGASPStorage.ts +331 -0
- package/src/Output.ts +2 -2
- package/src/__tests/Engine.test.ts +27 -26
- package/src/__tests/OverlayGASPRemote.test.ts +1 -1
- package/src/__tests/OverlayGASPStorage.test.ts +4 -3
- package/src/storage/Storage.ts +6 -8
- package/src/storage/knex/KnexStorage.ts +119 -41
- package/src/storage/knex/all-migrations.ts +10 -6
- package/src/storage/knex/migrations/2024-07-17-001-transactions.ts +40 -0
- package/src/storage/knex/migrations/2024-07-18-001-indexes.ts +30 -0
- package/dist/cjs/src/GASP.js +0 -275
- package/dist/cjs/src/GASP.js.map +0 -1
- package/dist/cjs/src/SHIPAdvertisement.js +0 -3
- package/dist/cjs/src/SHIPAdvertisement.js.map +0 -1
- package/dist/cjs/src/SLAPAdvertisement.js +0 -3
- package/dist/cjs/src/SLAPAdvertisement.js.map +0 -1
- package/dist/esm/src/GASP.js +0 -279
- package/dist/esm/src/GASP.js.map +0 -1
- package/dist/esm/src/SHIPAdvertisement.js +0 -2
- package/dist/esm/src/SHIPAdvertisement.js.map +0 -1
- package/dist/esm/src/SLAPAdvertisement.js +0 -2
- package/dist/esm/src/SLAPAdvertisement.js.map +0 -1
- package/dist/types/src/GASP.d.ts +0 -215
- package/dist/types/src/GASP.d.ts.map +0 -1
- package/dist/types/src/SHIPAdvertisement.d.ts +0 -9
- package/dist/types/src/SHIPAdvertisement.d.ts.map +0 -1
- package/dist/types/src/SLAPAdvertisement.d.ts +0 -9
- 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,
|
|
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).
|
|
@@ -23,25 +25,29 @@ export class Engine {
|
|
|
23
25
|
* @param {[key: string]: TopicManager} managers - manages topic admittance
|
|
24
26
|
* @param {[key: string]: LookupService} lookupServices - manages UTXO lookups
|
|
25
27
|
* @param {Storage} storage - for interacting with internally-managed persistent data
|
|
26
|
-
* @param {ChainTracker} chainTracker - Verifies SPV data associated with transactions
|
|
28
|
+
* @param {ChainTracker | 'scripts only'} chainTracker - Verifies SPV data associated with transactions
|
|
27
29
|
* @param {string} [hostingURL] - The URL this engine is hosted at. Required if going to support peer-discovery with an advertiser.
|
|
28
30
|
* @param {Broadcaster} [Broadcaster] - broadcaster used for broadcasting the incoming transaction
|
|
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
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 },
|
|
36
40
|
public lookupServices: { [key: string]: LookupService },
|
|
37
41
|
public storage: Storage,
|
|
38
|
-
public chainTracker: ChainTracker,
|
|
42
|
+
public chainTracker: ChainTracker | 'scripts only',
|
|
39
43
|
public hostingURL?: string,
|
|
40
44
|
public shipTrackers?: string[],
|
|
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
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'.
|
|
47
53
|
// For these two topics, any existing trackers are combined with the provided shipTrackers and slapTrackers omitting any duplicates.
|
|
@@ -75,6 +81,19 @@ export class Engine {
|
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
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
|
+
|
|
78
97
|
/**
|
|
79
98
|
* Submits a transaction for processing by Overlay Services.
|
|
80
99
|
* @param {TaggedBEEF} taggedBEEF - The transaction to process
|
|
@@ -91,89 +110,107 @@ export class Engine {
|
|
|
91
110
|
throw new Error(`This server does not support this topic: ${t}`)
|
|
92
111
|
}
|
|
93
112
|
}
|
|
113
|
+
|
|
94
114
|
// Validate the transaction SPV information
|
|
95
115
|
const tx = Transaction.fromBEEF(taggedBEEF.beef)
|
|
96
116
|
const txid = tx.id('hex')
|
|
97
|
-
|
|
117
|
+
|
|
118
|
+
this.startTime(`submit_${txid}`)
|
|
119
|
+
this.startTime(`chainTracker_${txid.substring(0, 10)}`)
|
|
120
|
+
const txValid = await tx.verify(this.chainTracker)
|
|
98
121
|
if (!txValid) throw new Error('Unable to verify SPV information.')
|
|
122
|
+
this.endTime(`chainTracker_${txid.substring(0, 10)}`)
|
|
99
123
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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}`)
|
|
105
132
|
}
|
|
106
|
-
}
|
|
107
133
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
txid,
|
|
114
|
-
topic
|
|
115
|
-
})
|
|
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
|
+
|
|
116
139
|
if (dupeCheck) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// 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.
|
|
120
|
-
// This was decided against, because we don't want to encourage unnecessary flooding of duplicative transactions to overlay services.
|
|
121
|
-
steak[topic] = {
|
|
122
|
-
outputsToAdmit: [],
|
|
123
|
-
coinsToRetain: []
|
|
124
|
-
}
|
|
125
|
-
continue
|
|
140
|
+
steak[topic] = { outputsToAdmit: [], coinsToRetain: [] }
|
|
141
|
+
return
|
|
126
142
|
}
|
|
127
143
|
|
|
128
|
-
// Check if any input of this transaction is a previous UTXO
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
})
|
|
140
156
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
output.outputIndex,
|
|
145
|
-
topic
|
|
146
|
-
)
|
|
157
|
+
this.startTime(`previousOutputQuery_${txid.substring(0, 10)}`)
|
|
158
|
+
const outputs = await Promise.all(outputPromises)
|
|
159
|
+
this.endTime(`previousOutputQuery_${txid.substring(0, 10)}`)
|
|
147
160
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
)
|
|
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)
|
|
157
170
|
}
|
|
158
|
-
}
|
|
171
|
+
}))
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('Error marking UTXO as spent:', error)
|
|
159
174
|
}
|
|
160
175
|
}
|
|
161
|
-
}
|
|
176
|
+
})
|
|
162
177
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
outputsToAdmit: [],
|
|
172
|
-
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: [] }
|
|
173
186
|
}
|
|
174
|
-
|
|
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}`)
|
|
175
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
|
+
}
|
|
176
212
|
|
|
213
|
+
for (const topic of taggedBEEF.topics) {
|
|
177
214
|
// Keep track of which outputs to admit, mark as stale, or retain
|
|
178
215
|
const outputsToAdmit: number[] = admissableOutputs.outputsToAdmit
|
|
179
216
|
const staleCoins: Array<{
|
|
@@ -204,17 +241,19 @@ export class Engine {
|
|
|
204
241
|
}
|
|
205
242
|
|
|
206
243
|
// Remove stale outputs recursively
|
|
207
|
-
|
|
244
|
+
this.startTime(`lookForStaleOutputs_${txid.substring(0, 10)}`)
|
|
245
|
+
await Promise.all(staleCoins.map(async coin => {
|
|
208
246
|
const output = await this.storage.findOutput(coin.txid, coin.outputIndex, topic)
|
|
209
247
|
if (output !== undefined && output !== null) {
|
|
210
248
|
await this.deleteUTXODeep(output)
|
|
211
249
|
}
|
|
212
|
-
}
|
|
250
|
+
}))
|
|
251
|
+
this.endTime(`lookForStaleOutputs_${txid.substring(0, 10)}`)
|
|
213
252
|
|
|
214
253
|
// Handle admittance and notification of incoming UTXOs
|
|
215
254
|
const newUTXOs: Array<{ txid: string, outputIndex: number }> = []
|
|
216
|
-
|
|
217
|
-
|
|
255
|
+
await Promise.all(outputsToAdmit.map(async outputIndex => {
|
|
256
|
+
this.startTime(`insertNewOutput_${txid.substring(0, 10)}`)
|
|
218
257
|
await this.storage.insertOutput({
|
|
219
258
|
txid,
|
|
220
259
|
outputIndex,
|
|
@@ -226,45 +265,30 @@ export class Engine {
|
|
|
226
265
|
consumedBy: [],
|
|
227
266
|
outputsConsumed
|
|
228
267
|
})
|
|
229
|
-
|
|
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({
|
|
230
287
|
txid,
|
|
231
|
-
|
|
288
|
+
topic
|
|
232
289
|
})
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
for (const l of Object.values(this.lookupServices)) {
|
|
236
|
-
try {
|
|
237
|
-
if (l.outputAdded !== undefined && l.outputAdded !== null) {
|
|
238
|
-
await l.outputAdded(txid, outputIndex, tx.outputs[outputIndex].lockingScript, topic)
|
|
239
|
-
}
|
|
240
|
-
} catch (_) { }
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Update each output consumed to know who consumed it
|
|
245
|
-
for (const output of outputsConsumed) {
|
|
246
|
-
const outputToUpdate = await this.storage.findOutput(output.txid, output.outputIndex, topic)
|
|
247
|
-
if (outputToUpdate !== undefined && outputToUpdate !== null) {
|
|
248
|
-
const newConsumedBy = [...new Set([...newUTXOs, ...outputToUpdate.consumedBy])]
|
|
249
|
-
// Note: only update if newConsumedBy !== new Set(JSON.parse(outputToUpdate.consumedBy)) ?
|
|
250
|
-
await this.storage.updateConsumedBy(output.txid, output.outputIndex, topic, newConsumedBy)
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Insert the applied transaction to prevent duplicate processing
|
|
255
|
-
await this.storage.insertAppliedTransaction({
|
|
256
|
-
txid,
|
|
257
|
-
topic
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
// Keep track of what outputs were admitted for what topic
|
|
261
|
-
steak[topic] = admissableOutputs
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Call the callback function if it is provided
|
|
265
|
-
// 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.
|
|
266
|
-
if (onSteakReady !== undefined) {
|
|
267
|
-
onSteakReady(steak)
|
|
290
|
+
])
|
|
291
|
+
this.endTime(`outputConsumed_${txid.substring(0, 10)}`)
|
|
268
292
|
}
|
|
269
293
|
|
|
270
294
|
// If we don't have an advertiser or we are dealing with historical transactions, just return the steak
|
|
@@ -272,6 +296,7 @@ export class Engine {
|
|
|
272
296
|
return steak
|
|
273
297
|
}
|
|
274
298
|
|
|
299
|
+
this.startTime(`transactionPropagation_${txid.substring(0, 10)}`)
|
|
275
300
|
// Propagate transaction to other nodes according to synchronization agreements
|
|
276
301
|
// 1. Find nodes that host the topics associated with admissable outputs
|
|
277
302
|
// We want to figure out which topics we actually care about (because their associated outputs were admitted)
|
|
@@ -280,6 +305,7 @@ export class Engine {
|
|
|
280
305
|
steak[topic] !== undefined && steak[topic].outputsToAdmit.length !== 0
|
|
281
306
|
)
|
|
282
307
|
|
|
308
|
+
// TODO: Cache ship/slap lookup with expiry (every 5min)
|
|
283
309
|
if (relevantTopics.length > 0) {
|
|
284
310
|
// Find all SHIP advertisements for the topics we care about
|
|
285
311
|
const domainToTopicsMap = new Map<string, Set<string>>()
|
|
@@ -367,6 +393,7 @@ export class Engine {
|
|
|
367
393
|
console.error('Error during broadcasting:', error)
|
|
368
394
|
}
|
|
369
395
|
}
|
|
396
|
+
this.endTime(`transactionPropgation_${txid.substring(0, 10)}`)
|
|
370
397
|
|
|
371
398
|
// Immediately return from the function without waiting for the promises to resolve.
|
|
372
399
|
return steak
|
|
@@ -380,7 +407,7 @@ export class Engine {
|
|
|
380
407
|
async lookup(lookupQuestion: LookupQuestion): Promise<LookupAnswer> {
|
|
381
408
|
// Validate a lookup service for the provider is found
|
|
382
409
|
const lookupService = this.lookupServices[lookupQuestion.service]
|
|
383
|
-
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} `)
|
|
384
411
|
|
|
385
412
|
let lookupResult = await lookupService.lookup(lookupQuestion)
|
|
386
413
|
// Handle custom lookup service answers
|
|
@@ -397,13 +424,14 @@ export class Engine {
|
|
|
397
424
|
txid,
|
|
398
425
|
outputIndex,
|
|
399
426
|
undefined,
|
|
400
|
-
|
|
427
|
+
undefined,
|
|
428
|
+
true
|
|
401
429
|
)
|
|
402
430
|
if (UTXO === undefined || UTXO === null) continue
|
|
403
431
|
|
|
404
432
|
// Get the history for this utxo and construct a BRC-8 Envelope
|
|
405
433
|
const output = await this.getUTXOHistory(UTXO, history, 0)
|
|
406
|
-
if (output !== undefined
|
|
434
|
+
if (output?.beef !== undefined) {
|
|
407
435
|
hydratedOutputs.push({
|
|
408
436
|
beef: output.beef,
|
|
409
437
|
outputIndex: output.outputIndex
|
|
@@ -548,7 +576,7 @@ export class Engine {
|
|
|
548
576
|
if (Array.isArray(syncEndpoints)) {
|
|
549
577
|
await Promise.all(syncEndpoints.map(async endpoint => {
|
|
550
578
|
// Sync to each host that is associated with this topic
|
|
551
|
-
const gasp = new GASP(new OverlayGASPStorage(topic, this), new OverlayGASPRemote(endpoint, topic), 0, `[GASP Sync of ${topic} with ${endpoint}]
|
|
579
|
+
const gasp = new GASP(new OverlayGASPStorage(topic, this), new OverlayGASPRemote(endpoint, topic), 0, `[GASP Sync of ${topic} with ${endpoint}]`, true)
|
|
552
580
|
await gasp.sync()
|
|
553
581
|
}))
|
|
554
582
|
}
|
|
@@ -591,7 +619,7 @@ export class Engine {
|
|
|
591
619
|
*/
|
|
592
620
|
async provideForeignGASPNode(graphID: string, txid: string, outputIndex: number): Promise<GASPNode> {
|
|
593
621
|
const hydrator = async (output: Output | null): Promise<GASPNode> => {
|
|
594
|
-
if (output === undefined
|
|
622
|
+
if (output?.beef === undefined) {
|
|
595
623
|
throw new Error('No matching output found!')
|
|
596
624
|
}
|
|
597
625
|
|
|
@@ -677,6 +705,10 @@ export class Engine {
|
|
|
677
705
|
return output
|
|
678
706
|
}
|
|
679
707
|
|
|
708
|
+
if (output.beef === undefined) {
|
|
709
|
+
throw new Error('Output must have associated transaction BEEF!')
|
|
710
|
+
}
|
|
711
|
+
|
|
680
712
|
// Determine if history traversal should continue for the current node
|
|
681
713
|
let shouldTraverseHistory
|
|
682
714
|
if (typeof historySelector !== 'number') {
|
|
@@ -699,7 +731,7 @@ export class Engine {
|
|
|
699
731
|
// Find the child outputs for each utxo consumed by the current output
|
|
700
732
|
const childHistories = (await Promise.all(
|
|
701
733
|
outputsConsumed.map(async (outputIdentifier) => {
|
|
702
|
-
const output = await this.storage.findOutput(outputIdentifier.txid, outputIdentifier.outputIndex)
|
|
734
|
+
const output = await this.storage.findOutput(outputIdentifier.txid, outputIdentifier.outputIndex, undefined, undefined, true)
|
|
703
735
|
|
|
704
736
|
// Make sure an output was found
|
|
705
737
|
if (output === undefined || output === null) {
|
|
@@ -720,6 +752,9 @@ export class Engine {
|
|
|
720
752
|
: input.sourceTransaction?.id('hex')
|
|
721
753
|
return sourceTXID === output.txid && input.sourceOutputIndex === output.outputIndex
|
|
722
754
|
})
|
|
755
|
+
if (input.beef === undefined) {
|
|
756
|
+
throw new Error('Input must have associated transaction BEEF!')
|
|
757
|
+
}
|
|
723
758
|
tx.inputs[inputIndex].sourceTransaction = Transaction.fromBEEF(input.beef)
|
|
724
759
|
}
|
|
725
760
|
const beef = tx.toBEEF()
|
|
@@ -730,9 +765,9 @@ export class Engine {
|
|
|
730
765
|
} catch (e) {
|
|
731
766
|
// Handle any errors that occurred
|
|
732
767
|
// Note: Test this!
|
|
733
|
-
console.error(`Error retrieving UTXO history: ${e}`)
|
|
768
|
+
console.error(`Error retrieving UTXO history: ${e} `)
|
|
734
769
|
// return []
|
|
735
|
-
throw new Error(`Error retrieving UTXO history: ${e}`)
|
|
770
|
+
throw new Error(`Error retrieving UTXO history: ${e} `)
|
|
736
771
|
}
|
|
737
772
|
}
|
|
738
773
|
|
|
@@ -784,7 +819,7 @@ export class Engine {
|
|
|
784
819
|
return await this.deleteUTXODeep(staleOutput)
|
|
785
820
|
})
|
|
786
821
|
} catch (error) {
|
|
787
|
-
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} `)
|
|
788
823
|
}
|
|
789
824
|
}
|
|
790
825
|
|
|
@@ -822,6 +857,10 @@ export class Engine {
|
|
|
822
857
|
* @param proof - The merklePath proving txid is a mined transaction hash
|
|
823
858
|
*/
|
|
824
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
|
+
|
|
825
864
|
const tx = Transaction.fromBEEF(output.beef)
|
|
826
865
|
if (tx.merklePath !== undefined) {
|
|
827
866
|
// Update the merkle path to handle potential reorgs
|
|
@@ -833,11 +872,11 @@ export class Engine {
|
|
|
833
872
|
this.updateInputProofs(tx, txid, proof)
|
|
834
873
|
|
|
835
874
|
// Update the output's BEEF in the storage DB
|
|
836
|
-
await this.storage.
|
|
875
|
+
await this.storage.updateTransactionBEEF(output.txid, tx.toBEEF())
|
|
837
876
|
|
|
838
877
|
// Recursively update the consumedBy outputs
|
|
839
878
|
for (const consumingOutput of output.consumedBy) {
|
|
840
|
-
const consumedOutputs = await this.storage.findOutputsForTransaction(consumingOutput.txid)
|
|
879
|
+
const consumedOutputs = await this.storage.findOutputsForTransaction(consumingOutput.txid, true)
|
|
841
880
|
for (const consumedOutput of consumedOutputs) {
|
|
842
881
|
await this.updateMerkleProof(consumedOutput, txid, proof)
|
|
843
882
|
}
|
|
@@ -852,7 +891,7 @@ export class Engine {
|
|
|
852
891
|
* @param blockHeight - The block height associated with the incoming merkle proof.
|
|
853
892
|
*/
|
|
854
893
|
async handleNewMerkleProof(txid: string, proof: MerklePath, blockHeight?: number): Promise<void> {
|
|
855
|
-
const outputs = await this.storage.findOutputsForTransaction(txid)
|
|
894
|
+
const outputs = await this.storage.findOutputsForTransaction(txid, true)
|
|
856
895
|
|
|
857
896
|
if (outputs === undefined || outputs.length === 0) {
|
|
858
897
|
throw new Error('Could not find matching transaction outputs for proof ingest!')
|
|
@@ -919,437 +958,6 @@ Thus, all non-type exports have been moved to Engine.
|
|
|
919
958
|
|
|
920
959
|
// TODO: fix bug with imports that break tests. -----[GASP/OverlayGASPRemote.ts]-----
|
|
921
960
|
|
|
922
|
-
export class OverlayGASPRemote implements GASPRemote {
|
|
923
|
-
constructor(public endpointURL: string, public topic: string) { }
|
|
924
|
-
|
|
925
|
-
/**
|
|
926
|
-
* Given an outgoing initial request, sends the request to the foreign instance and obtains their initial response.
|
|
927
|
-
* @param request
|
|
928
|
-
* @returns
|
|
929
|
-
*/
|
|
930
|
-
async getInitialResponse(request: GASPInitialRequest): Promise<GASPInitialResponse> {
|
|
931
|
-
// Send out an HTTP request to the URL (current host for topic)
|
|
932
|
-
// Include the topic in the request
|
|
933
|
-
// Parse out response and return correct format
|
|
934
|
-
const url = `${this.endpointURL}/requestSyncResponse`
|
|
935
|
-
const response = await fetch(url, {
|
|
936
|
-
method: 'POST',
|
|
937
|
-
headers: {
|
|
938
|
-
'Content-Type': 'application/json',
|
|
939
|
-
'X-BSV-Topic': this.topic
|
|
940
|
-
},
|
|
941
|
-
body: JSON.stringify(request)
|
|
942
|
-
})
|
|
943
|
-
|
|
944
|
-
if (!response.ok) {
|
|
945
|
-
throw new Error(`HTTP error! Status: ${response.status}`)
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
const result: GASPInitialResponse = await response.json()
|
|
949
|
-
|
|
950
|
-
// Validate and return the response in the correct format
|
|
951
|
-
if (!Array.isArray(result.UTXOList) || typeof result.since !== 'number') {
|
|
952
|
-
throw new Error('Invalid response format')
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
return {
|
|
956
|
-
UTXOList: result.UTXOList.map((utxo: any) => ({
|
|
957
|
-
txid: utxo.txid,
|
|
958
|
-
outputIndex: utxo.outputIndex
|
|
959
|
-
})),
|
|
960
|
-
since: result.since
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
/**
|
|
965
|
-
* Given an outgoing txid, outputIndex and optional metadata, request the associated GASP node from the foreign instance.
|
|
966
|
-
* @param graphID
|
|
967
|
-
* @param txid
|
|
968
|
-
* @param outputIndex
|
|
969
|
-
* @param metadata
|
|
970
|
-
* @returns
|
|
971
|
-
*/
|
|
972
|
-
async requestNode(graphID: string, txid: string, outputIndex: number, metadata: boolean): Promise<GASPNode> {
|
|
973
|
-
// Send an HTTP request with the provided info and get back a gaspNode
|
|
974
|
-
const url = `${this.endpointURL}/requestForeignGASPNode`
|
|
975
|
-
const body = {
|
|
976
|
-
graphID,
|
|
977
|
-
txid,
|
|
978
|
-
outputIndex,
|
|
979
|
-
metadata
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
const response = await fetch(url, {
|
|
983
|
-
method: 'POST',
|
|
984
|
-
headers: {
|
|
985
|
-
'Content-Type': 'application/json'
|
|
986
|
-
},
|
|
987
|
-
body: JSON.stringify(body)
|
|
988
|
-
})
|
|
989
|
-
|
|
990
|
-
if (!response.ok) {
|
|
991
|
-
throw new Error(`HTTP error! Status: ${response.status}`)
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
const result = await response.json()
|
|
995
961
|
|
|
996
|
-
// Validate and return the response in the correct format
|
|
997
|
-
if (typeof result.graphID !== 'string' || typeof result.rawTx !== 'string' || typeof result.outputIndex !== 'number') {
|
|
998
|
-
throw new Error('Invalid response format')
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
const gaspNode: GASPNode = {
|
|
1002
|
-
graphID: result.graphID,
|
|
1003
|
-
rawTx: result.rawTx,
|
|
1004
|
-
outputIndex: result.outputIndex,
|
|
1005
|
-
proof: result.proof,
|
|
1006
|
-
txMetadata: result.txMetadata,
|
|
1007
|
-
outputMetadata: result.outputMetadata,
|
|
1008
|
-
inputs: result.inputs
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
return gaspNode
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// ---- Now optional methods ----
|
|
1015
|
-
|
|
1016
|
-
// When are only syncing to them
|
|
1017
|
-
async getInitialReply(response: GASPInitialResponse): Promise<GASPInitialReply> {
|
|
1018
|
-
throw new Error('Function not supported!')
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
// Only used when supporting bidirectional sync.
|
|
1022
|
-
// Overlay services does not support this.
|
|
1023
|
-
async submitNode(node: GASPNode): Promise<void | GASPNodeResponse> {
|
|
1024
|
-
throw new Error('Node submission not supported!')
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
962
|
|
|
1028
963
|
// TODO: fix bug with imports that break tests. -----[GASP/OverlayGASPStorage.ts]-----
|
|
1029
|
-
|
|
1030
|
-
/**
|
|
1031
|
-
* Represents a node in the temporary graph.
|
|
1032
|
-
*/
|
|
1033
|
-
export interface GraphNode {
|
|
1034
|
-
txid: string
|
|
1035
|
-
graphID: string
|
|
1036
|
-
rawTx: string
|
|
1037
|
-
outputIndex: number
|
|
1038
|
-
spentBy?: string
|
|
1039
|
-
proof?: string
|
|
1040
|
-
txMetadata?: string
|
|
1041
|
-
outputMetadata?: string
|
|
1042
|
-
inputs?: Record<string, { hash: string }> | undefined
|
|
1043
|
-
children: GraphNode[]
|
|
1044
|
-
parent?: GraphNode
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
export class OverlayGASPStorage implements GASPStorage {
|
|
1048
|
-
readonly temporaryGraphNodeRefs: Record<string, GraphNode> = {}
|
|
1049
|
-
|
|
1050
|
-
constructor(public topic: string, public engine: Engine, public maxNodesInGraph?: number) { }
|
|
1051
|
-
|
|
1052
|
-
/**
|
|
1053
|
-
*
|
|
1054
|
-
* @param since
|
|
1055
|
-
* @returns
|
|
1056
|
-
*/
|
|
1057
|
-
async findKnownUTXOs(since: number): Promise<Array<{ txid: string, outputIndex: number }>> {
|
|
1058
|
-
const UTXOs = await this.engine.storage.findUTXOsForTopic(this.topic, since)
|
|
1059
|
-
return UTXOs.map(output => ({
|
|
1060
|
-
txid: output.txid,
|
|
1061
|
-
outputIndex: output.outputIndex
|
|
1062
|
-
}))
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
/**
|
|
1066
|
-
* 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.
|
|
1067
|
-
* @param graphID
|
|
1068
|
-
* @param txid
|
|
1069
|
-
* @param outputIndex
|
|
1070
|
-
* @param metadata
|
|
1071
|
-
* @returns
|
|
1072
|
-
*/
|
|
1073
|
-
async hydrateGASPNode(graphID: string, txid: string, outputIndex: number, metadata: boolean): Promise<GASPNode> {
|
|
1074
|
-
const output = await this.engine.storage.findOutput(txid, outputIndex)
|
|
1075
|
-
|
|
1076
|
-
if (output === undefined || output === null) {
|
|
1077
|
-
throw new Error('No matching output found!')
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
const tx = Transaction.fromBEEF(output.beef)
|
|
1081
|
-
const rawTx = tx.toHex()
|
|
1082
|
-
|
|
1083
|
-
const node: GASPNode = {
|
|
1084
|
-
rawTx,
|
|
1085
|
-
graphID,
|
|
1086
|
-
outputIndex
|
|
1087
|
-
}
|
|
1088
|
-
if (tx.merklePath !== undefined) {
|
|
1089
|
-
node.proof = tx.merklePath.toHex()
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
return node
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
/**
|
|
1096
|
-
* For a given node, returns the inputs needed to complete the graph, including whether updated metadata is requested for those inputs.
|
|
1097
|
-
* @param tx The node for which needed inputs should be found.
|
|
1098
|
-
* @returns A promise for a mapping of requested input transactions and whether metadata should be provided for each.
|
|
1099
|
-
*/
|
|
1100
|
-
async findNeededInputs(tx: GASPNode): Promise<GASPNodeResponse | undefined> {
|
|
1101
|
-
// If there is no Merkle proof, we always need the inputs
|
|
1102
|
-
const response: GASPNodeResponse = {
|
|
1103
|
-
requestedInputs: {}
|
|
1104
|
-
}
|
|
1105
|
-
const parsedTx = Transaction.fromHex(tx.rawTx)
|
|
1106
|
-
if (tx.proof === undefined) {
|
|
1107
|
-
for (const input of parsedTx.inputs) {
|
|
1108
|
-
response.requestedInputs[`${input.sourceTXID}.${input.sourceOutputIndex}`] = {
|
|
1109
|
-
metadata: false
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
return await this.stripAlreadyKnownInputs(response)
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
// Attempt to check if the current transaction is admissible
|
|
1117
|
-
parsedTx.merklePath = MerklePath.fromHex(tx.proof)
|
|
1118
|
-
const admittanceResult = await this.engine.managers[this.topic].identifyAdmissibleOutputs(parsedTx.toBEEF(), [])
|
|
1119
|
-
|
|
1120
|
-
if (admittanceResult.outputsToAdmit.includes(tx.outputIndex)) {
|
|
1121
|
-
// The transaction is admissible, no further inputs are needed
|
|
1122
|
-
} else {
|
|
1123
|
-
// The transaction is not admissible, get inputs needed for further verification
|
|
1124
|
-
// TopicManagers should implement a function to identify which inputs are needed.
|
|
1125
|
-
if (this.engine.managers[this.topic] !== undefined && typeof this.engine.managers[this.topic].identifyNeededInputs === 'function') {
|
|
1126
|
-
try {
|
|
1127
|
-
const neededInputs = await this.engine.managers[this.topic].identifyNeededInputs?.(parsedTx.toBEEF()) ?? []
|
|
1128
|
-
for (const input of neededInputs) {
|
|
1129
|
-
response.requestedInputs[`${input.txid}.${input.outputIndex}`] = {
|
|
1130
|
-
metadata: false
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
return await this.stripAlreadyKnownInputs(response)
|
|
1134
|
-
} catch (e) {
|
|
1135
|
-
console.error(`An error occurred when identifying needed inputs for transaction: ${parsedTx.id('hex')}.${tx.outputIndex}!`)
|
|
1136
|
-
// Cut off the graph in case of an error here.
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
// By default, if the topic manager isn't able to stipulate needed inputs, only the inputs necessary for SPV are requested.
|
|
1140
|
-
}
|
|
1141
|
-
// Everything else falls through to returning undefined/void, which will terminate the synchronization at this point.
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
/**
|
|
1145
|
-
* Ensures that no inputs are requested from foreign nodes before sending any GASP response
|
|
1146
|
-
* Also terminates graphs if the response would be empty.
|
|
1147
|
-
*/
|
|
1148
|
-
private async stripAlreadyKnownInputs(response: GASPNodeResponse | undefined): Promise<GASPNodeResponse | undefined> {
|
|
1149
|
-
if (typeof response === 'undefined') {
|
|
1150
|
-
return response
|
|
1151
|
-
}
|
|
1152
|
-
for (const inputNodeId of Object.keys(response.requestedInputs)) {
|
|
1153
|
-
const [txid, outputIndex] = inputNodeId.split('.')
|
|
1154
|
-
const found = await this.engine.storage.findOutput(txid, Number(outputIndex), this.topic)
|
|
1155
|
-
if (found) {
|
|
1156
|
-
delete response.requestedInputs[inputNodeId]
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
if (Object.keys(response.requestedInputs).length === 0) {
|
|
1160
|
-
return undefined
|
|
1161
|
-
}
|
|
1162
|
-
return response
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
/**
|
|
1166
|
-
* Appends a new node to a temporary graph.
|
|
1167
|
-
* @param tx The node to append to this graph.
|
|
1168
|
-
* @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.
|
|
1169
|
-
* @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.
|
|
1170
|
-
*/
|
|
1171
|
-
async appendToGraph(tx: GASPNode, spentBy?: string | undefined): Promise<void> {
|
|
1172
|
-
if (this.maxNodesInGraph !== undefined && Object.keys(this.temporaryGraphNodeRefs).length >= this.maxNodesInGraph) {
|
|
1173
|
-
throw new Error('The max number of nodes in transaction graph has been reached!')
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
const parsedTx = Transaction.fromHex(tx.rawTx)
|
|
1177
|
-
const txid = parsedTx.id('hex')
|
|
1178
|
-
if (tx.proof !== undefined) {
|
|
1179
|
-
parsedTx.merklePath = MerklePath.fromHex(tx.proof)
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
// Given the passed in node, append to the temp graph
|
|
1183
|
-
// Use the spentBy param which should be a txid.inputIndex for the node which spent this one in 36-byte format
|
|
1184
|
-
const newGraphNode: GraphNode = {
|
|
1185
|
-
txid,
|
|
1186
|
-
graphID: tx.graphID,
|
|
1187
|
-
rawTx: tx.rawTx,
|
|
1188
|
-
outputIndex: tx.outputIndex,
|
|
1189
|
-
proof: tx.proof,
|
|
1190
|
-
txMetadata: tx.txMetadata,
|
|
1191
|
-
outputMetadata: tx.outputMetadata,
|
|
1192
|
-
inputs: tx.inputs,
|
|
1193
|
-
children: []
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
// If spentBy is undefined, then we know it's the root node.
|
|
1197
|
-
if (spentBy === undefined) {
|
|
1198
|
-
this.temporaryGraphNodeRefs[tx.graphID] = newGraphNode
|
|
1199
|
-
} else {
|
|
1200
|
-
// Find the parent node based on spentBy
|
|
1201
|
-
const parentNode = this.temporaryGraphNodeRefs[spentBy]
|
|
1202
|
-
|
|
1203
|
-
if (parentNode !== undefined) {
|
|
1204
|
-
// Set parent-child relationship
|
|
1205
|
-
parentNode.children.push(newGraphNode)
|
|
1206
|
-
newGraphNode.parent = parentNode
|
|
1207
|
-
this.temporaryGraphNodeRefs[`${newGraphNode.txid}.${newGraphNode.outputIndex}`] = newGraphNode
|
|
1208
|
-
} else {
|
|
1209
|
-
throw new Error(`Parent node with GraphID ${spentBy} not found`)
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
/**
|
|
1215
|
-
* 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.
|
|
1216
|
-
* 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,
|
|
1217
|
-
* while considering any coins which the Manager had previously indicated were either valid or invalid.
|
|
1218
|
-
* @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph.
|
|
1219
|
-
* @throws If the graph is not well-anchored, according to the rules of Bitcoin or the rules of the Overlay Topic Manager.
|
|
1220
|
-
*/
|
|
1221
|
-
async validateGraphAnchor(graphID: string): Promise<void> {
|
|
1222
|
-
const rootNode = this.temporaryGraphNodeRefs[graphID]
|
|
1223
|
-
if (rootNode === undefined) {
|
|
1224
|
-
throw new Error(`Graph node with ID ${graphID} not found`)
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
// Check that the root node is Bitcoin-valid.
|
|
1228
|
-
const beef = this.getBEEFForNode(rootNode)
|
|
1229
|
-
const spvTx = Transaction.fromBEEF(beef)
|
|
1230
|
-
const isBitcoinValid = await spvTx.verify(this.engine.chainTracker)
|
|
1231
|
-
if (!isBitcoinValid) {
|
|
1232
|
-
throw new Error('The graph is not well-anchored according to the rules of Bitcoin.')
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
// Then, ensure the node is Overlay-valid.
|
|
1236
|
-
const beefs = this.computeOrderedBEEFsForGraph(graphID)
|
|
1237
|
-
|
|
1238
|
-
// coins: a Set of all historical coins to retain (no need to remove them), used to emulate topical admittance of previous inputs over time.
|
|
1239
|
-
const coins = new Set<string>()
|
|
1240
|
-
|
|
1241
|
-
// Submit all historical BEEFs in order through the topic manager, tracking what would be retained until we submit the root node last.
|
|
1242
|
-
// If, at the end, the root node is admitted, we have a valid overlay-specific graph.
|
|
1243
|
-
for (const beef of beefs) {
|
|
1244
|
-
// For any input to this transaction, see if it's a valid coin that's admitted. If so, it's a previous coin.
|
|
1245
|
-
const previousCoins: number[] = []
|
|
1246
|
-
const tx = Transaction.fromBEEF(beef)
|
|
1247
|
-
for (const inputIndex in tx.inputs) {
|
|
1248
|
-
const input = tx.inputs[inputIndex]
|
|
1249
|
-
const sourceTXID = input.sourceTXID || input.sourceTransaction?.id('hex')
|
|
1250
|
-
const coin = `${sourceTXID}.${input.sourceOutputIndex}`
|
|
1251
|
-
if (coins.has(coin)) {
|
|
1252
|
-
previousCoins.push(Number(inputIndex))
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
const admittanceInstructions = await this.engine.managers[this.topic].identifyAdmissibleOutputs(beef, previousCoins)
|
|
1256
|
-
// Every admitted output is now a coin.
|
|
1257
|
-
for (const outputIndex of admittanceInstructions.outputsToAdmit) {
|
|
1258
|
-
coins.add(`${tx.id('hex')}.${outputIndex}`)
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
// After sending through all the graph's BEEFs...
|
|
1262
|
-
// If the root node is now a coin, we have acceptance by the overlay.
|
|
1263
|
-
// Otherwise, throw.
|
|
1264
|
-
if (!coins.has(graphID)) {
|
|
1265
|
-
throw new Error('This graph did not result in topical admittance of the root node. Rejecting.')
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
/**
|
|
1270
|
-
* Deletes all data associated with a temporary graph that has failed to sync, if the graph exists.
|
|
1271
|
-
* @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph.
|
|
1272
|
-
*/
|
|
1273
|
-
async discardGraph(graphID: string): Promise<void> {
|
|
1274
|
-
for (const [nodeId, graphRef] of Object.entries(this.temporaryGraphNodeRefs)) {
|
|
1275
|
-
if (graphRef.graphID === graphID) {
|
|
1276
|
-
// Delete child node
|
|
1277
|
-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
1278
|
-
delete this.temporaryGraphNodeRefs[nodeId]
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
/**
|
|
1284
|
-
* Finalizes a graph, solidifying the new UTXO and its ancestors so that it will appear in the list of known UTXOs.
|
|
1285
|
-
* @param graphID The TXID and output index (in 36-byte format) for the UTXO at the root of this graph.
|
|
1286
|
-
*/
|
|
1287
|
-
async finalizeGraph(graphID: string): Promise<void> {
|
|
1288
|
-
const beefs = this.computeOrderedBEEFsForGraph(graphID)
|
|
1289
|
-
|
|
1290
|
-
// Submit all historical BEEFs in order, finalizing the graph for the current UTXO
|
|
1291
|
-
for (const beef of beefs) {
|
|
1292
|
-
await this.engine.submit({
|
|
1293
|
-
beef,
|
|
1294
|
-
topics: [this.topic]
|
|
1295
|
-
}, () => { }, 'historical-tx')
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
/**
|
|
1300
|
-
* Computes an ordered set of BEEFs for the graph with the given graph IDs
|
|
1301
|
-
* @param {string} graphID — The ID of the graph for which BEEFs are required
|
|
1302
|
-
* @returns Ordered BEEFs for the graph
|
|
1303
|
-
*/
|
|
1304
|
-
private computeOrderedBEEFsForGraph(graphID: string): number[][] {
|
|
1305
|
-
const beefs: number[][] = []
|
|
1306
|
-
const hydrator = (node: GraphNode): void => {
|
|
1307
|
-
const currentBEEF = this.getBEEFForNode(node)
|
|
1308
|
-
if (beefs.indexOf(currentBEEF) === -1) {
|
|
1309
|
-
beefs.unshift(currentBEEF)
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
for (const child of node.children) {
|
|
1313
|
-
// Continue backwards to the earliest nodes, adding them onto the beginning
|
|
1314
|
-
hydrator(child)
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
// Start the hydrator with the root node
|
|
1319
|
-
const foundRoot = this.temporaryGraphNodeRefs[graphID]
|
|
1320
|
-
if (!foundRoot) {
|
|
1321
|
-
throw new Error('Unable to find root node in graph for finalization!')
|
|
1322
|
-
}
|
|
1323
|
-
hydrator(foundRoot)
|
|
1324
|
-
return beefs
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
/**
|
|
1328
|
-
* Computes a full BEEF for a given graph node, based on the temporary graph store.
|
|
1329
|
-
* @param node Graph node for which BEEF is needed.
|
|
1330
|
-
* @returns BEEF array, including all proofs on inputs.
|
|
1331
|
-
*/
|
|
1332
|
-
private getBEEFForNode(node: GraphNode): number[] {
|
|
1333
|
-
// Given a node, hydrate its merkle proof or all inputs, returning a reference to the hydrated node's Transaction object
|
|
1334
|
-
const hydrator = (node: GraphNode): Transaction => {
|
|
1335
|
-
const tx = Transaction.fromHex(node.rawTx)
|
|
1336
|
-
if (node.proof) {
|
|
1337
|
-
tx.merklePath = MerklePath.fromHex(node.proof)
|
|
1338
|
-
return tx // Transaction with proof, end of the line.
|
|
1339
|
-
}
|
|
1340
|
-
// For each input, look it up and recurse.
|
|
1341
|
-
for (const inputIndex in tx.inputs) {
|
|
1342
|
-
const input = tx.inputs[inputIndex]
|
|
1343
|
-
const foundNode = this.temporaryGraphNodeRefs[`${input.sourceTXID}.${input.sourceOutputIndex}`]
|
|
1344
|
-
if (!foundNode) {
|
|
1345
|
-
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.')
|
|
1346
|
-
}
|
|
1347
|
-
tx.inputs[inputIndex].sourceTransaction = hydrator(foundNode)
|
|
1348
|
-
}
|
|
1349
|
-
return tx
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
const finalTX = hydrator(node)
|
|
1353
|
-
return finalTX.toBEEF()
|
|
1354
|
-
}
|
|
1355
|
-
}
|