@bsv/overlay 0.1.0-alpha.8 → 0.1.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.
Files changed (50) hide show
  1. package/dist/cjs/package.json +2 -1
  2. package/dist/cjs/src/Engine.js +573 -42
  3. package/dist/cjs/src/Engine.js.map +1 -1
  4. package/dist/cjs/src/GASP/OverlayGASPRemote.js +96 -0
  5. package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -0
  6. package/dist/cjs/src/GASP/OverlayGASPStorage.js +221 -0
  7. package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -0
  8. package/dist/cjs/src/SyncConfiguration.js +3 -0
  9. package/dist/cjs/src/SyncConfiguration.js.map +1 -0
  10. package/dist/cjs/src/storage/knex/KnexStorage.js +26 -6
  11. package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
  12. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  13. package/dist/esm/src/Engine.js +575 -42
  14. package/dist/esm/src/Engine.js.map +1 -1
  15. package/dist/esm/src/GASP/OverlayGASPRemote.js +93 -0
  16. package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -0
  17. package/dist/esm/src/GASP/OverlayGASPStorage.js +219 -0
  18. package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -0
  19. package/dist/esm/src/SyncConfiguration.js +2 -0
  20. package/dist/esm/src/SyncConfiguration.js.map +1 -0
  21. package/dist/esm/src/storage/knex/KnexStorage.js +26 -6
  22. package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
  23. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  24. package/dist/types/mod.d.ts +1 -1
  25. package/dist/types/mod.d.ts.map +1 -1
  26. package/dist/types/src/Engine.d.ts +169 -13
  27. package/dist/types/src/Engine.d.ts.map +1 -1
  28. package/dist/types/src/GASP/OverlayGASPRemote.d.ts +23 -0
  29. package/dist/types/src/GASP/OverlayGASPRemote.d.ts.map +1 -0
  30. package/dist/types/src/GASP/OverlayGASPStorage.d.ts +68 -0
  31. package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -0
  32. package/dist/types/src/SyncConfiguration.d.ts +20 -0
  33. package/dist/types/src/SyncConfiguration.d.ts.map +1 -0
  34. package/dist/types/src/TopicManager.d.ts +8 -0
  35. package/dist/types/src/TopicManager.d.ts.map +1 -1
  36. package/dist/types/src/storage/Storage.d.ts +7 -0
  37. package/dist/types/src/storage/Storage.d.ts.map +1 -1
  38. package/dist/types/src/storage/knex/KnexStorage.d.ts +1 -0
  39. package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
  40. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  41. package/mod.ts +2 -2
  42. package/package.json +2 -1
  43. package/src/Engine.ts +652 -47
  44. package/src/SyncConfiguration.ts +19 -0
  45. package/src/TopicManager.ts +6 -0
  46. package/src/__tests/Engine.test.ts +151 -25
  47. package/src/__tests/OverlayGASPRemote.test.ts +132 -0
  48. package/src/__tests/OverlayGASPStorage.test.ts +166 -0
  49. package/src/storage/Storage.ts +8 -0
  50. package/src/storage/knex/KnexStorage.ts +34 -5
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Engine = void 0;
3
+ exports.OverlayGASPStorage = exports.OverlayGASPRemote = exports.Engine = void 0;
4
4
  const sdk_1 = require("@bsv/sdk");
5
+ const gasp_1 = require("@bsv/gasp");
5
6
  /**
6
7
  * Am engine for running BSV Overlay Services (topic managers and lookup services).
7
8
  */
@@ -12,13 +13,14 @@ class Engine {
12
13
  * @param {[key: string]: LookupService} lookupServices - manages UTXO lookups
13
14
  * @param {Storage} storage - for interacting with internally-managed persistent data
14
15
  * @param {ChainTracker} chainTracker - Verifies SPV data associated with transactions
15
- * @param {string} hostingURL
16
+ * @param {string} [hostingURL] - The URL this engine is hosted at. Required if going to support peer-discovery with an advertiser.
16
17
  * @param {Broadcaster} [Broadcaster] - broadcaster used for broadcasting the incoming transaction
17
18
  * @param {Advertiser} [Advertiser] - handles SHIP and SLAP advertisements for peer-discovery
18
19
  * @param {string} shipTrackers - SHIP domains we know to bootstrap the system
19
- * @param {string} slapTrackers - SAP domains we know to bootstrap the system
20
+ * @param {string} slapTrackers - SLAP domains we know to bootstrap the system
21
+ * @param {Record<string, string[] | 'SHIP'>} syncConfiguration — Configuration object describing historical synchronization of topics.
20
22
  */
21
- constructor(managers, lookupServices, storage, chainTracker, hostingURL, shipTrackers, slapTrackers, broadcaster, advertiser) {
23
+ constructor(managers, lookupServices, storage, chainTracker, hostingURL, shipTrackers, slapTrackers, broadcaster, advertiser, syncConfiguration) {
22
24
  this.managers = managers;
23
25
  this.lookupServices = lookupServices;
24
26
  this.storage = storage;
@@ -28,13 +30,19 @@ class Engine {
28
30
  this.slapTrackers = slapTrackers;
29
31
  this.broadcaster = broadcaster;
30
32
  this.advertiser = advertiser;
33
+ this.syncConfiguration = syncConfiguration;
31
34
  }
32
35
  /**
33
36
  * Submits a transaction for processing by Overlay Services.
34
- * @param taggedBEEF — The transaction to process
35
- * @returns The submitted transaction execution acknowledgement
37
+ * @param {TaggedBEEF} taggedBEEF - The transaction to process
38
+ * @param {function(STEAK): void} [onSTEAKReady] - Optional callback function invoked when the STEAK is ready.
39
+ * @param {string} mode — Indicates the submission behavior, whether historical or current. Historical transactions are not broadcast or propagated.
40
+ *
41
+ * The optional callback function should be used to get STEAK when ready, and avoid waiting for broadcast and transaction propagation to complete.
42
+ *
43
+ * @returns {Promise<STEAK>} The submitted transaction execution acknowledgement
36
44
  */
37
- async submit(taggedBEEF) {
45
+ async submit(taggedBEEF, onSteakReady, mode = 'current-tx') {
38
46
  var _a, _b;
39
47
  for (const t of taggedBEEF.topics) {
40
48
  if (this.managers[t] === undefined || this.managers[t] === null) {
@@ -44,9 +52,16 @@ class Engine {
44
52
  // Validate the transaction SPV information
45
53
  const tx = sdk_1.Transaction.fromBEEF(taggedBEEF.beef);
46
54
  const txid = tx.id('hex');
47
- const txValid = await tx.verify(this.chainTracker);
55
+ const txValid = await tx.verify(this.chainTracker); // Note: also verifying historical-tx with SPV. Needed?
48
56
  if (!txValid)
49
57
  throw new Error('Unable to verify SPV information.');
58
+ // Broadcast the transaction if not historical and broadcaster is configured
59
+ if (mode !== 'historical-tx' && this.broadcaster !== undefined) {
60
+ const response = await this.broadcaster.broadcast(tx);
61
+ if ((0, sdk_1.isBroadcastFailure)(response)) {
62
+ throw new Error(`Failed to broadcast transaction! Error: ${response.description}`);
63
+ }
64
+ }
50
65
  // Find UTXOs belonging to a particular topic
51
66
  const steak = {};
52
67
  for (const topic of taggedBEEF.topics) {
@@ -176,12 +191,13 @@ class Engine {
176
191
  // Keep track of what outputs were admitted for what topic
177
192
  steak[topic] = admissableOutputs;
178
193
  }
179
- // Broadcast the transaction
180
- if (Object.keys(steak).length > 0 && this.broadcaster !== undefined) {
181
- await this.broadcaster.broadcast(tx);
194
+ // Call the callback function if it is provided
195
+ // 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.
196
+ if (onSteakReady !== undefined) {
197
+ onSteakReady(steak);
182
198
  }
183
- // If we don't have an advertiser, just return the steak
184
- if (this.advertiser === undefined) {
199
+ // If we don't have an advertiser or we are dealing with historical transactions, just return the steak
200
+ if (this.advertiser === undefined || mode === 'historical-tx') {
185
201
  return steak;
186
202
  }
187
203
  // Propagate transaction to other nodes according to synchronization agreements
@@ -279,6 +295,7 @@ class Engine {
279
295
  console.error('Error during broadcasting:', error);
280
296
  }
281
297
  }
298
+ // Immediately return from the function without waiting for the promises to resolve.
282
299
  return steak;
283
300
  }
284
301
  /**
@@ -394,6 +411,108 @@ class Engine {
394
411
  }
395
412
  }
396
413
  }
414
+ /**
415
+ * This method goes through each topic that we support syncing and attempts to sync with each endpoint
416
+ * associated with that topic. If the sync configuration is 'SHIP', it will sync to all peers that support
417
+ * the topic.
418
+ *
419
+ * @throws Error if the overlay service engine is not configured for topical synchronization.
420
+ */
421
+ async startGASPSync() {
422
+ if (this.syncConfiguration === undefined) {
423
+ throw new Error('Overlay Service Engine not configured for topical synchronization!');
424
+ }
425
+ for (const topic of Object.keys(this.syncConfiguration)) {
426
+ // Make sure syncEndpoints is an array or SHIP
427
+ let syncEndpoints = this.syncConfiguration[topic];
428
+ if (syncEndpoints === 'SHIP') {
429
+ // Perform lookup and find ship advertisements to set syncEndpoints for topic
430
+ const lookupAnswer = await this.lookup({
431
+ service: 'ls_ship',
432
+ query: {
433
+ topic
434
+ }
435
+ });
436
+ // Lookup will currently always return type output-list
437
+ if (lookupAnswer.type === 'output-list') {
438
+ const endpointSet = new Set();
439
+ lookupAnswer.outputs.forEach(output => {
440
+ var _a;
441
+ try {
442
+ // Parse out the advertisements using the provided parser
443
+ const tx = sdk_1.Transaction.fromBEEF(output.beef);
444
+ const advertisement = (_a = this.advertiser) === null || _a === void 0 ? void 0 : _a.parseAdvertisement(tx.outputs[output.outputIndex].lockingScript);
445
+ if (advertisement !== undefined && advertisement !== null && advertisement.protocol === 'SHIP') {
446
+ endpointSet.add(advertisement.domain);
447
+ }
448
+ }
449
+ catch (error) {
450
+ console.error('Failed to parse advertisement output:', error);
451
+ }
452
+ });
453
+ syncEndpoints = Array.from(endpointSet);
454
+ }
455
+ }
456
+ // Now syncEndpoints is guaranteed to be an array of strings without duplicates
457
+ // Note: Consider MySQL DB locking implications when running synchronization in parallel
458
+ if (Array.isArray(syncEndpoints)) {
459
+ await Promise.all(syncEndpoints.map(async (endpoint) => {
460
+ // Sync to each host that is associated with this topic
461
+ const gasp = new gasp_1.GASP(new OverlayGASPStorage(topic, this), new OverlayGASPRemote(endpoint, topic), 0, `[GASP Sync of ${topic} with ${endpoint}] `, true);
462
+ await gasp.sync();
463
+ }));
464
+ }
465
+ }
466
+ }
467
+ /**
468
+ * Given a GASP request, create an initial response.
469
+ *
470
+ * This method processes an initial synchronization request by finding the relevant UTXOs for the given topic
471
+ * since the provided (TODO: timestamp or block height, we need to decide on sync timing semantics) in the request. It constructs a response that includes a list of these UTXOs
472
+ * and the (timestamp or block height, TODO...) from the initial request.
473
+ *
474
+ * @param initialRequest - The GASP initial request containing the version and the (timestamp or block height, TODO...) since the last sync.
475
+ * @param topic - The topic for which UTXOs are being requested.
476
+ * @returns A promise that resolves to a GASPInitialResponse containing the list of UTXOs and the provided timestamp.
477
+ */
478
+ async provideForeignSyncResponse(initialRequest, topic) {
479
+ const UTXOs = await this.storage.findUTXOsForTopic(topic, initialRequest.since);
480
+ return {
481
+ UTXOList: UTXOs.map(output => {
482
+ return {
483
+ txid: output.txid,
484
+ outputIndex: output.outputIndex
485
+ };
486
+ }),
487
+ since: initialRequest.since
488
+ };
489
+ }
490
+ /**
491
+ * Provides a GASPNode for the given graphID, transaction ID, and output index.
492
+ *
493
+ * @param graphID - The identifier for the graph to which this node belongs (in the format txid.outputIndex).
494
+ * @param txid - The transaction ID for the requested output from somewhere within the graph's history.
495
+ * @param outputIndex - The index of the output in the transaction.
496
+ * @returns A promise that resolves to a GASPNode containing the raw transaction and other optional data.
497
+ * @throws An error if no output is found for the given transaction ID and output index.
498
+ */
499
+ async provideForeignGASPNode(graphID, txid, outputIndex) {
500
+ const output = await this.storage.findOutput(txid, outputIndex);
501
+ if (output === undefined || output === null) {
502
+ throw new Error('No matching output found!');
503
+ }
504
+ const tx = sdk_1.Transaction.fromBEEF(output.beef);
505
+ const rawTx = tx.toHex();
506
+ const node = {
507
+ rawTx,
508
+ graphID,
509
+ outputIndex
510
+ };
511
+ if (tx.merklePath !== undefined) {
512
+ node.proof = tx.merklePath.toHex();
513
+ }
514
+ return node;
515
+ }
397
516
  /**
398
517
  * Traverse and return the history of a UTXO.
399
518
  *
@@ -514,47 +633,52 @@ class Engine {
514
633
  }
515
634
  }
516
635
  /**
517
- * Recursively updates the Merkle proof for the given output and its consumedBy outputs.
518
- * If the output matches the source transaction ID, its Merkle proof is updated directly.
519
- * Otherwise, the Merkle proof is updated for the corresponding input in each transaction.
636
+ * Given a new transaction proof (txid, proof),
520
637
  *
521
- * @param output - The output to update with the new Merkle proof.
522
- * @param proof - The Merkle proof to be applied to the output or its inputs.
523
- * @param sourceTxid - The transaction ID of the source output whose Merkle proof is being updated.
638
+ * update tx.merklePath if appropriate,
639
+ *
640
+ * and if not, recurse through all input sourceTransactions.
641
+ *
642
+ * @param tx transaction which may benefit from new proof.
643
+ * @param txid BE hex string double hash of transaction proven by proof.
644
+ * @param proof for txid
524
645
  */
525
- async updateMerkleProof(output, proof, recursionPath) {
526
- // Add current output to recursionPath
527
- recursionPath.push({ txid: output.txid, outputIndex: output.outputIndex });
528
- const tx = sdk_1.Transaction.fromBEEF(output.beef);
529
- // Handle the base case
530
- if (output.txid === recursionPath[0].txid) {
646
+ updateInputProofs(tx, txid, proof) {
647
+ if (tx.merklePath)
648
+ // transaction already has a proof
649
+ return;
650
+ if (tx.id('hex') === txid) {
531
651
  tx.merklePath = proof;
532
652
  }
533
653
  else {
534
- // Traverse inputs to update the Merkle proof according to the recursionPath
535
- let currentInputs = tx.inputs;
536
- for (let i = recursionPath.length - 1; i >= 0; i--) {
537
- const crumb = recursionPath[i];
538
- for (const input of currentInputs) {
539
- if (input.sourceTXID === crumb.txid && input.sourceOutputIndex === crumb.outputIndex) {
540
- if (i === 0 && input.sourceTransaction !== undefined) {
541
- input.sourceTransaction.merklePath = proof;
542
- }
543
- else if (input.sourceTransaction !== undefined) {
544
- currentInputs = input.sourceTransaction.inputs;
545
- break;
546
- }
547
- }
548
- }
654
+ for (const input of tx.inputs) {
655
+ // All inputs must have sourceTransactions
656
+ const stx = input.sourceTransaction;
657
+ this.updateInputProofs(stx, txid, proof);
549
658
  }
550
659
  }
660
+ }
661
+ /**
662
+ * Recursively updates beefs (merkle proofs) of this output and its consumedBy lineage.
663
+ *
664
+ * @param output - An output derived from txid which may benefit from new proof.
665
+ * @param txid - The txid for which proof is a valid merkle path.
666
+ * @param proof - The merklePath proving txid is a mined transaction hash
667
+ */
668
+ async updateMerkleProof(output, txid, proof) {
669
+ const tx = sdk_1.Transaction.fromBEEF(output.beef);
670
+ if (tx.merklePath)
671
+ // Already have a proof for this output's transaction.
672
+ return;
673
+ // recursively update all sourceTransactions proven by (txid,proof)
674
+ this.updateInputProofs(tx, txid, proof);
551
675
  // Update the output's BEEF in the storage DB
552
676
  await this.storage.updateOutputBeef(output.txid, output.outputIndex, output.topic, tx.toBEEF());
553
677
  // Recursively update the consumedBy outputs
554
678
  for (const consumingOutput of output.consumedBy) {
555
679
  const consumedOutputs = await this.storage.findOutputsForTransaction(consumingOutput.txid);
556
680
  for (const consumedOutput of consumedOutputs) {
557
- await this.updateMerkleProof(consumedOutput, proof, []);
681
+ await this.updateMerkleProof(consumedOutput, txid, proof);
558
682
  }
559
683
  }
560
684
  }
@@ -566,8 +690,11 @@ class Engine {
566
690
  */
567
691
  async handleNewMerkleProof(txid, proof) {
568
692
  const outputs = await this.storage.findOutputsForTransaction(txid);
693
+ if (outputs == undefined || outputs.length === 0) {
694
+ throw new Error('Could not find matching transaction outputs for proof ingest!');
695
+ }
569
696
  for (const output of outputs) {
570
- await this.updateMerkleProof(output, proof, []);
697
+ await this.updateMerkleProof(output, txid, proof);
571
698
  }
572
699
  }
573
700
  /**
@@ -608,4 +735,408 @@ class Engine {
608
735
  }
609
736
  }
610
737
  exports.Engine = Engine;
738
+ //////////
739
+ // OTHER FILES
740
+ //////////
741
+ /*
742
+ There is currently a bug with the test runner that prevents importing and using files that export variables other than type definitions within implementation files that are not directly imported themselves.
743
+ Thus, all non-type exports have been moved to Engine.
744
+ */
745
+ // TODO: fix bug with imports that break tests. -----[GASP/OverlayGASPRemote.ts]-----
746
+ class OverlayGASPRemote {
747
+ constructor(endpointURL, topic) {
748
+ this.endpointURL = endpointURL;
749
+ this.topic = topic;
750
+ }
751
+ /**
752
+ * Given an outgoing initial request, sends the request to the foreign instance and obtains their initial response.
753
+ * @param request
754
+ * @returns
755
+ */
756
+ async getInitialResponse(request) {
757
+ // Send out an HTTP request to the URL (current host for topic)
758
+ // Include the topic in the request
759
+ // Parse out response and return correct format
760
+ const url = `${this.endpointURL}/requestSyncResponse`;
761
+ const response = await fetch(url, {
762
+ method: 'POST',
763
+ headers: {
764
+ 'Content-Type': 'application/json',
765
+ 'X-BSV-Topic': this.topic
766
+ },
767
+ body: JSON.stringify(request)
768
+ });
769
+ if (!response.ok) {
770
+ throw new Error(`HTTP error! Status: ${response.status}`);
771
+ }
772
+ const result = await response.json();
773
+ // Validate and return the response in the correct format
774
+ if (!Array.isArray(result.UTXOList) || typeof result.since !== 'number') {
775
+ throw new Error('Invalid response format');
776
+ }
777
+ return {
778
+ UTXOList: result.UTXOList.map((utxo) => ({
779
+ txid: utxo.txid,
780
+ outputIndex: utxo.outputIndex
781
+ })),
782
+ since: result.since
783
+ };
784
+ }
785
+ /**
786
+ * Given an outgoing txid, outputIndex and optional metadata, request the associated GASP node from the foreign instance.
787
+ * @param graphID
788
+ * @param txid
789
+ * @param outputIndex
790
+ * @param metadata
791
+ * @returns
792
+ */
793
+ async requestNode(graphID, txid, outputIndex, metadata) {
794
+ // Send an HTTP request with the provided info and get back a gaspNode
795
+ const url = `${this.endpointURL}/requestForeignGASPNode`;
796
+ const body = {
797
+ graphID,
798
+ txid,
799
+ outputIndex,
800
+ metadata
801
+ };
802
+ const response = await fetch(url, {
803
+ method: 'POST',
804
+ headers: {
805
+ 'Content-Type': 'application/json'
806
+ },
807
+ body: JSON.stringify(body)
808
+ });
809
+ if (!response.ok) {
810
+ throw new Error(`HTTP error! Status: ${response.status}`);
811
+ }
812
+ const result = await response.json();
813
+ // Validate and return the response in the correct format
814
+ if (typeof result.graphID !== 'string' || typeof result.rawTx !== 'string' || typeof result.outputIndex !== 'number') {
815
+ throw new Error('Invalid response format');
816
+ }
817
+ const gaspNode = {
818
+ graphID: result.graphID,
819
+ rawTx: result.rawTx,
820
+ outputIndex: result.outputIndex,
821
+ proof: result.proof,
822
+ txMetadata: result.txMetadata,
823
+ outputMetadata: result.outputMetadata,
824
+ inputs: result.inputs
825
+ };
826
+ return gaspNode;
827
+ }
828
+ // ---- Now optional methods ----
829
+ // When are only syncing to them
830
+ async getInitialReply(response) {
831
+ throw new Error('Function not supported!');
832
+ }
833
+ // Only used when supporting bidirectional sync.
834
+ // Overlay services does not support this.
835
+ async submitNode(node) {
836
+ throw new Error('Node submission not supported!');
837
+ }
838
+ }
839
+ exports.OverlayGASPRemote = OverlayGASPRemote;
840
+ class OverlayGASPStorage {
841
+ constructor(topic, engine, maxNodesInGraph) {
842
+ this.topic = topic;
843
+ this.engine = engine;
844
+ this.maxNodesInGraph = maxNodesInGraph;
845
+ this.temporaryGraphNodeRefs = {};
846
+ }
847
+ /**
848
+ *
849
+ * @param since
850
+ * @returns
851
+ */
852
+ async findKnownUTXOs(since) {
853
+ const UTXOs = await this.engine.storage.findUTXOsForTopic(this.topic, since);
854
+ return UTXOs.map(output => ({
855
+ txid: output.txid,
856
+ outputIndex: output.outputIndex
857
+ }));
858
+ }
859
+ // TODO: Consider optionality on interface
860
+ async hydrateGASPNode(graphID, txid, outputIndex, metadata) {
861
+ throw new Error('GASP node hydration Not supported!');
862
+ }
863
+ /**
864
+ * For a given node, returns the inputs needed to complete the graph, including whether updated metadata is requested for those inputs.
865
+ * @param tx The node for which needed inputs should be found.
866
+ * @returns A promise for a mapping of requested input transactions and whether metadata should be provided for each.
867
+ */
868
+ async findNeededInputs(tx) {
869
+ var _a, _b, _c;
870
+ // If there is no Merkle proof, we always need the inputs
871
+ const response = {
872
+ requestedInputs: {}
873
+ };
874
+ const parsedTx = sdk_1.Transaction.fromHex(tx.rawTx);
875
+ if (tx.proof === undefined) {
876
+ for (const input of parsedTx.inputs) {
877
+ response.requestedInputs[`${input.sourceTXID}.${input.sourceOutputIndex}`] = {
878
+ metadata: false
879
+ };
880
+ }
881
+ return await this.stripAlreadyKnownInputs(response);
882
+ }
883
+ // Attempt to check if the current transaction is admissible
884
+ parsedTx.merklePath = sdk_1.MerklePath.fromHex(tx.proof);
885
+ const admittanceResult = await this.engine.managers[this.topic].identifyAdmissibleOutputs(parsedTx.toBEEF(), []);
886
+ if (admittanceResult.outputsToAdmit.includes(tx.outputIndex)) {
887
+ // The transaction is admissible, no further inputs are needed
888
+ return;
889
+ }
890
+ else {
891
+ // The transaction is not admissible, get inputs needed for further verification
892
+ // TopicManagers should implement a function to identify which inputs are needed.
893
+ if (this.engine.managers[this.topic] !== undefined && typeof this.engine.managers[this.topic].identifyNeededInputs === 'function') {
894
+ try {
895
+ const neededInputs = (_c = await ((_b = (_a = this.engine.managers[this.topic]).identifyNeededInputs) === null || _b === void 0 ? void 0 : _b.call(_a, parsedTx.toBEEF()))) !== null && _c !== void 0 ? _c : [];
896
+ for (const input of neededInputs) {
897
+ response.requestedInputs[`${input.txid}.${input.outputIndex}`] = {
898
+ metadata: false
899
+ };
900
+ }
901
+ return await this.stripAlreadyKnownInputs(response);
902
+ }
903
+ catch (e) {
904
+ console.error(`An error occurred when identifying needed inputs for transaction: ${parsedTx.id('hex')}.${tx.outputIndex}!`);
905
+ // Cut off the graph in case of an error here.
906
+ return;
907
+ }
908
+ }
909
+ else {
910
+ // In case the topic manager isn't able to stipulate needed inputs, we need to request all inputs as if we had no merkle proof.
911
+ // However, it's dubious that we sometimes don't know — QUESTION: Should we require all topic managers to support this functionality?
912
+ // The alternative to requiring ALL inputs by default is to require NO inputs by default, cutting off the historical graph at this point
913
+ // (e.g. `return undefined`).
914
+ for (const input of parsedTx.inputs) {
915
+ response.requestedInputs[`${input.sourceTXID}.${input.sourceOutputIndex}`] = {
916
+ metadata: false
917
+ };
918
+ }
919
+ return await this.stripAlreadyKnownInputs(response);
920
+ }
921
+ }
922
+ // Everything else falls through to returning undefined/void, which will terminate the synchronization at this point.
923
+ }
924
+ /**
925
+ * Ensures that no inputs are requested from foreign nodes before sending any GASP response
926
+ * Also terminates graphs if the response would be empty.
927
+ */
928
+ async stripAlreadyKnownInputs(response) {
929
+ if (typeof response === 'undefined') {
930
+ return response;
931
+ }
932
+ for (const inputNodeId of Object.keys(response.requestedInputs)) {
933
+ const [txid, outputIndex] = inputNodeId.split('.');
934
+ const found = await this.engine.storage.findOutput(txid, Number(outputIndex), this.topic);
935
+ if (found) {
936
+ delete response.requestedInputs[inputNodeId];
937
+ }
938
+ }
939
+ if (Object.keys(response.requestedInputs).length === 0) {
940
+ return undefined;
941
+ }
942
+ }
943
+ /**
944
+ * Appends a new node to a temporary graph.
945
+ * @param tx The node to append to this graph.
946
+ * @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.
947
+ * @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.
948
+ */
949
+ async appendToGraph(tx, spentBy) {
950
+ if (this.maxNodesInGraph !== undefined && Object.keys(this.temporaryGraphNodeRefs).length >= this.maxNodesInGraph) {
951
+ throw new Error('The max number of nodes in transaction graph has been reached!');
952
+ }
953
+ const parsedTx = sdk_1.Transaction.fromHex(tx.rawTx);
954
+ const txid = parsedTx.id('hex');
955
+ if (tx.proof !== undefined) {
956
+ parsedTx.merklePath = sdk_1.MerklePath.fromHex(tx.proof);
957
+ }
958
+ // Given the passed in node, append to the temp graph
959
+ // Use the spentBy param which should be a txid.inputIndex for the node which spent this one in 36-byte format
960
+ const newGraphNode = {
961
+ txid,
962
+ time: 0, // TODO: Determine required format for Time (either block height or timestamp, undefined / Infinity for unconfirmed transactions
963
+ graphID: tx.graphID,
964
+ rawTx: tx.rawTx,
965
+ outputIndex: tx.outputIndex,
966
+ proof: tx.proof,
967
+ txMetadata: tx.txMetadata,
968
+ outputMetadata: tx.outputMetadata,
969
+ inputs: tx.inputs,
970
+ children: []
971
+ };
972
+ // If spentBy is undefined, then we know it's the root node.
973
+ if (spentBy === undefined) {
974
+ this.temporaryGraphNodeRefs[tx.graphID] = newGraphNode;
975
+ }
976
+ else {
977
+ // Find the parent node based on spentBy
978
+ const parentNode = this.temporaryGraphNodeRefs[spentBy];
979
+ if (parentNode !== undefined) {
980
+ // Set parent-child relationship
981
+ parentNode.children.push(newGraphNode);
982
+ newGraphNode.parent = parentNode;
983
+ this.temporaryGraphNodeRefs[`${newGraphNode.txid}.${newGraphNode.outputIndex}`] = newGraphNode;
984
+ }
985
+ else {
986
+ throw new Error(`Parent node with GraphID ${spentBy} not found`);
987
+ }
988
+ }
989
+ }
990
+ /**
991
+ * 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.
992
+ * 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,
993
+ * while considering any coins which the Manager had previously indicated were either valid or invalid.
994
+ *
995
+ * 1. Confirm that we are well anchored (according to the rules of SPV)
996
+ * 2. Is there some sequence of nodes that will result in the admittance of the root node into the topic
997
+ * 3. If there is some spend chain that, when executed in order to the root node, leads to a valid admittance, we are good to go.
998
+ * a) For each node in the chain we need to check topical admittance (such that all relevant inputs to any given node are executed before the node in question).
999
+ * b) Once previous nodes are validated, they are taken into account as previous coins to their direct spenders.
1000
+ *
1001
+ * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph.
1002
+ * @throws If the graph is not well-anchored, according to the rules of Bitcoin or the rules of the Overlay Topic Manager.
1003
+ */
1004
+ async validateGraphAnchor(graphID) {
1005
+ const rootNode = this.temporaryGraphNodeRefs[graphID];
1006
+ if (rootNode === undefined) {
1007
+ throw new Error(`Graph node with ID ${graphID} not found`);
1008
+ }
1009
+ // Check that the root node is Bitcoin-valid.
1010
+ const beef = this.getBEEFForNode(rootNode);
1011
+ const spvTx = sdk_1.Transaction.fromBEEF(beef);
1012
+ const isBitcoinValid = await spvTx.verify(this.engine.chainTracker);
1013
+ if (!isBitcoinValid) {
1014
+ throw new Error('The graph is not well-anchored according to the rules of Bitcoin.');
1015
+ }
1016
+ // Then, ensure the node is Overlay-valid.
1017
+ const validationMap = new Map(); // Tracks historical node validity, avoiding duplication of work
1018
+ const ensureTopicalAnchor = async (graphNode) => {
1019
+ const dupeCheckNode = validationMap.get(`${graphNode.txid}.${graphNode.outputIndex}`);
1020
+ if (typeof dupeCheckNode !== 'undefined') {
1021
+ return dupeCheckNode;
1022
+ }
1023
+ // Parse the transaction, adding a proof if we have one
1024
+ const parsedTX = sdk_1.Transaction.fromHex(graphNode.rawTx);
1025
+ if (graphNode.proof !== undefined) {
1026
+ parsedTX.merklePath = sdk_1.MerklePath.fromHex(graphNode.proof);
1027
+ }
1028
+ const validatedChildren = [];
1029
+ for (const child of graphNode.children) {
1030
+ // If the child has not been validated, check it.
1031
+ if (!validationMap.has(`${child.txid}.${child.outputIndex}`)) {
1032
+ const isValidChild = await ensureTopicalAnchor(child);
1033
+ if (isValidChild) {
1034
+ validatedChildren.push(child);
1035
+ }
1036
+ }
1037
+ else {
1038
+ // If the child has been validated already, just add it to the list.
1039
+ validatedChildren.push(child);
1040
+ }
1041
+ }
1042
+ let admittanceResult;
1043
+ try {
1044
+ // Previous coins are input indices corresponding to outputs redeemed.
1045
+ const previousCoins = validatedChildren.map(child => parsedTX.inputs.findIndex(i => i.sourceOutputIndex === child.outputIndex && i.sourceTXID === child.txid));
1046
+ admittanceResult = await this.engine.managers[this.topic].identifyAdmissibleOutputs(parsedTX.toBEEF(), previousCoins);
1047
+ }
1048
+ catch (error) {
1049
+ console.error('Error in admittance check:', error);
1050
+ validationMap.set(`${graphNode.txid}.${graphNode.outputIndex}`, false);
1051
+ return false;
1052
+ }
1053
+ const isValid = admittanceResult.outputsToAdmit.includes(graphNode.outputIndex);
1054
+ validationMap.set(`${graphNode.txid}.${graphNode.outputIndex}`, isValid);
1055
+ if (isValid === false) {
1056
+ return false;
1057
+ }
1058
+ // Reached the root successfully
1059
+ if (graphNode.parent === undefined) {
1060
+ return true;
1061
+ }
1062
+ return await ensureTopicalAnchor(graphNode.parent);
1063
+ };
1064
+ const isOverlayValid = await ensureTopicalAnchor(rootNode);
1065
+ if (!isOverlayValid) {
1066
+ throw new Error('The graph is not well-anchored according to the rules of this overlay topic.');
1067
+ }
1068
+ }
1069
+ /**
1070
+ * Deletes all data associated with a temporary graph that has failed to sync, if the graph exists.
1071
+ * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph.
1072
+ */
1073
+ async discardGraph(graphID) {
1074
+ for (const [nodeId, graphRef] of Object.entries(this.temporaryGraphNodeRefs)) {
1075
+ if (graphRef.graphID === graphID) {
1076
+ // Delete child node
1077
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1078
+ delete this.temporaryGraphNodeRefs[nodeId];
1079
+ }
1080
+ }
1081
+ }
1082
+ /**
1083
+ * Finalizes a graph, solidifying the new UTXO and its ancestors so that it will appear in the list of known UTXOs.
1084
+ * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the root of this graph.
1085
+ */
1086
+ async finalizeGraph(graphID) {
1087
+ // Construct an ordered set of BEEFs for the graph
1088
+ const beefs = [];
1089
+ const hydrator = (node) => {
1090
+ const currentBEEF = this.getBEEFForNode(node);
1091
+ if (beefs.indexOf(currentBEEF) === -1) {
1092
+ beefs.unshift(currentBEEF);
1093
+ }
1094
+ for (const child of node.children) {
1095
+ // Continue backwards to the earliest nodes, adding them onto the beginning
1096
+ hydrator(child);
1097
+ }
1098
+ };
1099
+ // Start the hydrator with the root node
1100
+ const foundRoot = this.temporaryGraphNodeRefs[graphID];
1101
+ if (!foundRoot) {
1102
+ throw new Error('Unable to find root node in graph for finalization!');
1103
+ }
1104
+ hydrator(foundRoot);
1105
+ // Submit all historical BEEFs in order, finalizing the graph for the current UTXO
1106
+ for (const beef of beefs) {
1107
+ await this.engine.submit({
1108
+ beef,
1109
+ topics: [this.topic]
1110
+ }, () => { }, 'historical-tx');
1111
+ }
1112
+ }
1113
+ /**
1114
+ * Computes a full BEEF for a given graph node, based on the temporary graph store.
1115
+ * @param node Graph node for which BEEF is needed.
1116
+ * @returns BEEF array, including all proofs on inputs.
1117
+ */
1118
+ getBEEFForNode(node) {
1119
+ // Given a node, hydrate its merkle proof or all inputs, returning a reference to the hydrated node's Transaction object
1120
+ const hydrator = (node) => {
1121
+ const tx = sdk_1.Transaction.fromHex(node.rawTx);
1122
+ if (node.proof) {
1123
+ tx.merklePath = sdk_1.MerklePath.fromHex(node.proof);
1124
+ return tx; // Transaction with proof, end of the line.
1125
+ }
1126
+ // For each input, look it up and recurse.
1127
+ for (const inputIndex in tx.inputs) {
1128
+ const input = tx.inputs[inputIndex];
1129
+ const foundNode = this.temporaryGraphNodeRefs[`${input.sourceTXID}.${input.sourceOutputIndex}`];
1130
+ if (!foundNode) {
1131
+ 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.');
1132
+ }
1133
+ tx.inputs[inputIndex].sourceTransaction = hydrator(foundNode);
1134
+ }
1135
+ return tx;
1136
+ };
1137
+ const finalTX = hydrator(node);
1138
+ return finalTX.toBEEF();
1139
+ }
1140
+ }
1141
+ exports.OverlayGASPStorage = OverlayGASPStorage;
611
1142
  //# sourceMappingURL=Engine.js.map