@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.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/Engine.js +9 -7
- package/dist/cjs/src/Engine.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPStorage.js +89 -44
- package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/cjs/src/storage/knex/KnexStorage.js +23 -18
- package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/Engine.js +9 -7
- package/dist/esm/src/Engine.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPStorage.js +89 -44
- package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/esm/src/storage/knex/KnexStorage.js +23 -18
- package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/Engine.d.ts +1 -1
- package/dist/types/src/Engine.d.ts.map +1 -1
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts +10 -0
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
- package/dist/types/src/TopicManager.d.ts +1 -1
- package/dist/types/src/TopicManager.d.ts.map +1 -1
- package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/docs/API.md +4 -4
- package/package.json +1 -1
- package/src/Engine.ts +10 -7
- package/src/GASP/OverlayGASPStorage.ts +105 -49
- package/src/TopicManager.ts +6 -1
- package/src/__tests/Engine.test.ts +6 -2
- package/src/storage/knex/KnexStorage.ts +25 -20
|
@@ -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(
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
//
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
package/src/TopicManager.ts
CHANGED
|
@@ -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: (
|
|
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(
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
|