@bsv/overlay 0.1.0-alpha.9 → 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 +1 -0
  2. package/dist/cjs/src/Engine.js +560 -41
  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 +20 -0
  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 +562 -41
  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 +20 -0
  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 +161 -9
  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 +636 -46
  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 +26 -0
@@ -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
  */
@@ -16,9 +17,10 @@ class Engine {
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,17 +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
37
  * @param {TaggedBEEF} taggedBEEF - The transaction to process
35
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.
36
40
  *
37
41
  * The optional callback function should be used to get STEAK when ready, and avoid waiting for broadcast and transaction propagation to complete.
38
42
  *
39
43
  * @returns {Promise<STEAK>} The submitted transaction execution acknowledgement
40
44
  */
41
- async submit(taggedBEEF, onSteakReady) {
45
+ async submit(taggedBEEF, onSteakReady, mode = 'current-tx') {
42
46
  var _a, _b;
43
47
  for (const t of taggedBEEF.topics) {
44
48
  if (this.managers[t] === undefined || this.managers[t] === null) {
@@ -48,9 +52,16 @@ class Engine {
48
52
  // Validate the transaction SPV information
49
53
  const tx = sdk_1.Transaction.fromBEEF(taggedBEEF.beef);
50
54
  const txid = tx.id('hex');
51
- const txValid = await tx.verify(this.chainTracker);
55
+ const txValid = await tx.verify(this.chainTracker); // Note: also verifying historical-tx with SPV. Needed?
52
56
  if (!txValid)
53
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
+ }
54
65
  // Find UTXOs belonging to a particular topic
55
66
  const steak = {};
56
67
  for (const topic of taggedBEEF.topics) {
@@ -181,15 +192,12 @@ class Engine {
181
192
  steak[topic] = admissableOutputs;
182
193
  }
183
194
  // Call the callback function if it is provided
184
- if (onSteakReady) {
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) {
185
197
  onSteakReady(steak);
186
198
  }
187
- // Broadcast the transaction
188
- if (Object.keys(steak).length > 0 && this.broadcaster !== undefined) {
189
- await this.broadcaster.broadcast(tx);
190
- }
191
- // If we don't have an advertiser, just return the steak
192
- 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') {
193
201
  return steak;
194
202
  }
195
203
  // Propagate transaction to other nodes according to synchronization agreements
@@ -403,6 +411,108 @@ class Engine {
403
411
  }
404
412
  }
405
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
+ }
406
516
  /**
407
517
  * Traverse and return the history of a UTXO.
408
518
  *
@@ -523,47 +633,52 @@ class Engine {
523
633
  }
524
634
  }
525
635
  /**
526
- * Recursively updates the Merkle proof for the given output and its consumedBy outputs.
527
- * If the output matches the source transaction ID, its Merkle proof is updated directly.
528
- * Otherwise, the Merkle proof is updated for the corresponding input in each transaction.
636
+ * Given a new transaction proof (txid, proof),
637
+ *
638
+ * update tx.merklePath if appropriate,
529
639
  *
530
- * @param output - The output to update with the new Merkle proof.
531
- * @param proof - The Merkle proof to be applied to the output or its inputs.
532
- * @param sourceTxid - The transaction ID of the source output whose Merkle proof is being updated.
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
533
645
  */
534
- async updateMerkleProof(output, proof, recursionPath) {
535
- // Add current output to recursionPath
536
- recursionPath.push({ txid: output.txid, outputIndex: output.outputIndex });
537
- const tx = sdk_1.Transaction.fromBEEF(output.beef);
538
- // Handle the base case
539
- 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) {
540
651
  tx.merklePath = proof;
541
652
  }
542
653
  else {
543
- // Traverse inputs to update the Merkle proof according to the recursionPath
544
- let currentInputs = tx.inputs;
545
- for (let i = recursionPath.length - 1; i >= 0; i--) {
546
- const crumb = recursionPath[i];
547
- for (const input of currentInputs) {
548
- if (input.sourceTXID === crumb.txid && input.sourceOutputIndex === crumb.outputIndex) {
549
- if (i === 0 && input.sourceTransaction !== undefined) {
550
- input.sourceTransaction.merklePath = proof;
551
- }
552
- else if (input.sourceTransaction !== undefined) {
553
- currentInputs = input.sourceTransaction.inputs;
554
- break;
555
- }
556
- }
557
- }
654
+ for (const input of tx.inputs) {
655
+ // All inputs must have sourceTransactions
656
+ const stx = input.sourceTransaction;
657
+ this.updateInputProofs(stx, txid, proof);
558
658
  }
559
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);
560
675
  // Update the output's BEEF in the storage DB
561
676
  await this.storage.updateOutputBeef(output.txid, output.outputIndex, output.topic, tx.toBEEF());
562
677
  // Recursively update the consumedBy outputs
563
678
  for (const consumingOutput of output.consumedBy) {
564
679
  const consumedOutputs = await this.storage.findOutputsForTransaction(consumingOutput.txid);
565
680
  for (const consumedOutput of consumedOutputs) {
566
- await this.updateMerkleProof(consumedOutput, proof, []);
681
+ await this.updateMerkleProof(consumedOutput, txid, proof);
567
682
  }
568
683
  }
569
684
  }
@@ -579,7 +694,7 @@ class Engine {
579
694
  throw new Error('Could not find matching transaction outputs for proof ingest!');
580
695
  }
581
696
  for (const output of outputs) {
582
- await this.updateMerkleProof(output, proof, []);
697
+ await this.updateMerkleProof(output, txid, proof);
583
698
  }
584
699
  }
585
700
  /**
@@ -620,4 +735,408 @@ class Engine {
620
735
  }
621
736
  }
622
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;
623
1142
  //# sourceMappingURL=Engine.js.map