@bsv/overlay 0.5.4 → 0.6.0

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.
@@ -21,9 +21,45 @@ export interface GraphNode {
21
21
 
22
22
  export class OverlayGASPStorage implements GASPStorage {
23
23
  readonly temporaryGraphNodeRefs: Record<string, GraphNode> = {}
24
+ private static activeAnchorValidations = 0
25
+ private static readonly anchorValidationQueue: Array<() => void> = []
26
+ private static activeFinalizations = 0
27
+ private static readonly finalizationQueue: Array<() => void> = []
28
+ private static readonly MAX_CONCURRENT_ANCHOR_VALIDATIONS = 4
29
+ private static readonly MAX_CONCURRENT_FINALIZATIONS = 2
24
30
 
25
31
  constructor (public topic: string, public engine: Engine, public maxNodesInGraph?: number) { }
26
32
 
33
+ private static async acquireAnchorValidationSlot (): Promise<void> {
34
+ if (OverlayGASPStorage.activeAnchorValidations >= OverlayGASPStorage.MAX_CONCURRENT_ANCHOR_VALIDATIONS) {
35
+ await new Promise<void>(resolve => { OverlayGASPStorage.anchorValidationQueue.push(resolve) })
36
+ }
37
+ OverlayGASPStorage.activeAnchorValidations++
38
+ }
39
+
40
+ private static releaseAnchorValidationSlot (): void {
41
+ OverlayGASPStorage.activeAnchorValidations--
42
+ const next = OverlayGASPStorage.anchorValidationQueue.shift()
43
+ if (next !== undefined) {
44
+ next()
45
+ }
46
+ }
47
+
48
+ private static async acquireFinalizationSlot (): Promise<void> {
49
+ if (OverlayGASPStorage.activeFinalizations >= OverlayGASPStorage.MAX_CONCURRENT_FINALIZATIONS) {
50
+ await new Promise<void>(resolve => { OverlayGASPStorage.finalizationQueue.push(resolve) })
51
+ }
52
+ OverlayGASPStorage.activeFinalizations++
53
+ }
54
+
55
+ private static releaseFinalizationSlot (): void {
56
+ OverlayGASPStorage.activeFinalizations--
57
+ const next = OverlayGASPStorage.finalizationQueue.shift()
58
+ if (next !== undefined) {
59
+ next()
60
+ }
61
+ }
62
+
27
63
  /**
28
64
  *
29
65
  * @param since
@@ -91,8 +127,12 @@ export class OverlayGASPStorage implements GASPStorage {
91
127
 
92
128
  // Attempt to check if the current transaction is admissible
93
129
  parsedTx.merklePath = MerklePath.fromHex(tx.proof)
94
- const admittanceResult = await this.engine.managers[this.topic].identifyAdmissibleOutputs(parsedTx.toBEEF(), [], typeof tx.txMetadata === 'string' ? Utils.toArray(tx.txMetadata) : undefined)
95
-
130
+ const admittanceResult = await this.engine.managers[this.topic].identifyAdmissibleOutputs(
131
+ parsedTx.toBEEF(),
132
+ [],
133
+ typeof tx.txMetadata === 'string' ? Utils.toArray(tx.txMetadata) : undefined,
134
+ 'historical-tx'
135
+ )
96
136
  if (admittanceResult.outputsToAdmit.includes(tx.outputIndex)) {
97
137
  // The transaction is admissible, no further inputs are needed
98
138
  } else {
@@ -196,51 +236,61 @@ export class OverlayGASPStorage implements GASPStorage {
196
236
  * @throws If the graph is not well-anchored, according to the rules of Bitcoin or the rules of the Overlay Topic Manager.
197
237
  */
198
238
  async validateGraphAnchor (graphID: string): Promise<void> {
199
- const rootNode = this.temporaryGraphNodeRefs[graphID]
200
- if (rootNode === undefined) {
201
- throw new Error(`Graph node with ID ${graphID} not found`)
202
- }
203
-
204
- // Check that the root node is Bitcoin-valid.
205
- const beef = this.getBEEFForNode(rootNode)
206
- const spvTx = Transaction.fromBEEF(beef)
207
- const isBitcoinValid = await spvTx.verify(this.engine.chainTracker)
208
- if (!isBitcoinValid) {
209
- throw new Error('The graph is not well-anchored according to the rules of Bitcoin.')
210
- }
211
-
212
- // Then, ensure the node is Overlay-valid.
213
- const beefs = this.computeOrderedBEEFsForGraph(graphID)
239
+ await OverlayGASPStorage.acquireAnchorValidationSlot()
240
+ try {
241
+ const rootNode = this.temporaryGraphNodeRefs[graphID]
242
+ if (rootNode === undefined) {
243
+ throw new Error(`Graph node with ID ${graphID} not found`)
244
+ }
214
245
 
215
- // coins: a Set of all historical coins to retain (no need to remove them), used to emulate topical admittance of previous inputs over time.
216
- const coins = new Set<string>()
246
+ // Check that the root node is Bitcoin-valid.
247
+ const beef = this.getBEEFForNode(rootNode)
248
+ const spvTx = Transaction.fromBEEF(beef)
249
+ const isBitcoinValid = await spvTx.verify(this.engine.chainTracker)
250
+ if (!isBitcoinValid) {
251
+ throw new Error('The graph is not well-anchored according to the rules of Bitcoin.')
252
+ }
217
253
 
218
- // Submit all historical BEEFs in order through the topic manager, tracking what would be retained until we submit the root node last.
219
- // If, at the end, the root node is admitted, we have a valid overlay-specific graph.
220
- for (const beef of beefs) {
221
- // For any input to this transaction, see if it's a valid coin that's admitted. If so, it's a previous coin.
222
- const previousCoins: number[] = []
223
- const tx = Transaction.fromBEEF(beef)
224
- for (const [inputIndex, input] of tx.inputs.entries()) {
225
- const sourceTXID = input.sourceTXID ?? input.sourceTransaction?.id('hex')
226
- if (sourceTXID != null && sourceTXID !== '') {
227
- const coin = `${sourceTXID}.${input.sourceOutputIndex}`
228
- if (coins.has(coin)) {
229
- previousCoins.push(Number(inputIndex))
254
+ // Then, ensure the node is Overlay-valid.
255
+ const beefs = this.computeOrderedBEEFsForGraph(graphID)
256
+
257
+ // coins: a Set of all historical coins to retain (no need to remove them), used to emulate topical admittance of previous inputs over time.
258
+ const coins = new Set<string>()
259
+
260
+ // Submit all historical BEEFs in order through the topic manager, tracking what would be retained until we submit the root node last.
261
+ // If, at the end, the root node is admitted, we have a valid overlay-specific graph.
262
+ for (const beef of beefs) {
263
+ // For any input to this transaction, see if it's a valid coin that's admitted. If so, it's a previous coin.
264
+ const previousCoins: number[] = []
265
+ const tx = Transaction.fromBEEF(beef)
266
+ for (const [inputIndex, input] of tx.inputs.entries()) {
267
+ const sourceTXID = input.sourceTXID ?? input.sourceTransaction?.id('hex')
268
+ if (sourceTXID != null && sourceTXID !== '') {
269
+ const coin = `${sourceTXID}.${input.sourceOutputIndex}`
270
+ if (coins.has(coin)) {
271
+ previousCoins.push(Number(inputIndex))
272
+ }
230
273
  }
231
274
  }
275
+ const admittanceInstructions = await this.engine.managers[this.topic].identifyAdmissibleOutputs(
276
+ beef,
277
+ previousCoins,
278
+ undefined,
279
+ 'historical-tx'
280
+ )
281
+ // Every admitted output is now a coin.
282
+ for (const outputIndex of admittanceInstructions.outputsToAdmit) {
283
+ coins.add(`${tx.id('hex')}.${outputIndex}`)
284
+ }
232
285
  }
233
- const admittanceInstructions = await this.engine.managers[this.topic].identifyAdmissibleOutputs(beef, previousCoins)
234
- // Every admitted output is now a coin.
235
- for (const outputIndex of admittanceInstructions.outputsToAdmit) {
236
- coins.add(`${tx.id('hex')}.${outputIndex}`)
286
+ // After sending through all the graph's BEEFs...
287
+ // If the root node is now a coin, we have acceptance by the overlay.
288
+ // Otherwise, throw.
289
+ if (!coins.has(graphID)) {
290
+ throw new Error('This graph did not result in topical admittance of the root node. Rejecting.')
237
291
  }
238
- }
239
- // After sending through all the graph's BEEFs...
240
- // If the root node is now a coin, we have acceptance by the overlay.
241
- // Otherwise, throw.
242
- if (!coins.has(graphID)) {
243
- throw new Error('This graph did not result in topical admittance of the root node. Rejecting.')
292
+ } finally {
293
+ OverlayGASPStorage.releaseAnchorValidationSlot()
244
294
  }
245
295
  }
246
296
 
@@ -263,14 +313,20 @@ export class OverlayGASPStorage implements GASPStorage {
263
313
  * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the root of this graph.
264
314
  */
265
315
  async finalizeGraph (graphID: string): Promise<void> {
266
- const beefs = this.computeOrderedBEEFsForGraph(graphID)
267
-
268
- // Submit all historical BEEFs in order, finalizing the graph for the current UTXO
269
- for (const beef of beefs) {
270
- await this.engine.submit({
271
- beef,
272
- topics: [this.topic]
273
- }, () => { }, 'historical-tx')
316
+ await OverlayGASPStorage.acquireFinalizationSlot()
317
+ try {
318
+ const beefs = this.computeOrderedBEEFsForGraph(graphID)
319
+
320
+ // Submit all historical BEEFs in order, finalizing the graph for the current UTXO.
321
+ // We skip SPV verification here because validateGraphAnchor has already done it.
322
+ for (const beef of beefs) {
323
+ await this.engine.submit({
324
+ beef,
325
+ topics: [this.topic]
326
+ }, () => { }, 'historical-tx-no-spv')
327
+ }
328
+ } finally {
329
+ OverlayGASPStorage.releaseFinalizationSlot()
274
330
  }
275
331
  }
276
332
 
@@ -9,7 +9,12 @@ export interface TopicManager {
9
9
  * Accepts the transaction in BEEF format and an array of those input indices which spend previously-admitted outputs from the same topic.
10
10
  * The transaction's BEEF structure will always contain the transactions associated with previous coins for reference (if any), regardless of whether the current transaction was directly proven.
11
11
  */
12
- identifyAdmissibleOutputs: (beef: number[], previousCoins: number[], offChainValues?: number[]) => Promise<AdmittanceInstructions>
12
+ identifyAdmissibleOutputs: (
13
+ beef: number[],
14
+ previousCoins: number[],
15
+ offChainValues?: number[],
16
+ mode?: 'historical-tx' | 'current-tx' | 'historical-tx-no-spv'
17
+ ) => Promise<AdmittanceInstructions>
13
18
 
14
19
  /**
15
20
  * Identifies and returns the inputs needed to anchor any topical outputs from this transaction to their associated previous history.
@@ -528,12 +528,16 @@ describe('BSV Overlay Services Engine', () => {
528
528
  mockChainTracker
529
529
  )
530
530
 
531
- // Submit the utxo
532
531
  await engine.submit({
533
532
  beef: exampleBeef,
534
533
  topics: ['Hello']
535
534
  })
536
- expect(engine.managers.Hello.identifyAdmissibleOutputs).toHaveBeenCalledWith(exampleBeef, [0], undefined)
535
+ expect(engine.managers.Hello.identifyAdmissibleOutputs).toHaveBeenCalledWith(
536
+ exampleBeef,
537
+ [0],
538
+ undefined,
539
+ 'current-tx'
540
+ )
537
541
  })
538
542
  describe('When previous UTXOs were retained by the topic manager', () => {
539
543
  it('Notifies all lookup services about the output being spent (not deleted, see the comment about this in deleteUTXODeep)', async () => {
@@ -170,28 +170,33 @@ export class KnexStorage implements Storage {
170
170
  }
171
171
 
172
172
  async insertOutput (output: Output): Promise<void> {
173
- const insertPromises = [this.knex('outputs').insert({
174
- txid: output.txid,
175
- outputIndex: Number(output.outputIndex),
176
- outputScript: Buffer.from(output.outputScript),
177
- topic: output.topic,
178
- satoshis: Number(output.satoshis),
179
- outputsConsumed: JSON.stringify(output.outputsConsumed),
180
- consumedBy: JSON.stringify(output.consumedBy),
181
- spent: output.spent,
182
- score: output.score
183
- })]
184
-
185
- if (output.beef !== undefined) {
186
- const insertTransactionPromise = this.knex('transactions').insert({
173
+ await this.knex.transaction(async trx => {
174
+ const existing = await trx('outputs').where({
187
175
  txid: output.txid,
188
- beef: Buffer.from(output.beef)
189
- }).onConflict('txid').ignore()
190
- insertPromises.push(insertTransactionPromise)
191
- }
176
+ outputIndex: Number(output.outputIndex),
177
+ topic: output.topic
178
+ }).first()
179
+
180
+ if (existing === undefined || existing === null) {
181
+ await trx('outputs').insert({
182
+ txid: output.txid,
183
+ outputIndex: Number(output.outputIndex),
184
+ outputScript: Buffer.from(output.outputScript),
185
+ topic: output.topic,
186
+ satoshis: Number(output.satoshis),
187
+ outputsConsumed: JSON.stringify(output.outputsConsumed),
188
+ consumedBy: JSON.stringify(output.consumedBy),
189
+ spent: output.spent,
190
+ score: output.score
191
+ })
192
+ }
192
193
 
193
- await this.knex.transaction(async trx => {
194
- await Promise.all(insertPromises.map(promise => promise.transacting(trx)))
194
+ if (output.beef !== undefined) {
195
+ await trx('transactions').insert({
196
+ txid: output.txid,
197
+ beef: Buffer.from(output.beef)
198
+ }).onConflict('txid').ignore()
199
+ }
195
200
  })
196
201
  }
197
202