@bsv/overlay 0.1.12 → 0.1.14

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 (37) hide show
  1. package/dist/cjs/package.json +2 -2
  2. package/dist/cjs/src/Engine.js +82 -411
  3. package/dist/cjs/src/Engine.js.map +1 -1
  4. package/dist/cjs/src/GASP/OverlayGASPRemote.js +6 -4
  5. package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
  6. package/dist/cjs/src/GASP/OverlayGASPStorage.js +152 -77
  7. package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
  8. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  9. package/dist/esm/src/Engine.js +82 -411
  10. package/dist/esm/src/Engine.js.map +1 -1
  11. package/dist/esm/src/GASP/OverlayGASPRemote.js +7 -4
  12. package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
  13. package/dist/esm/src/GASP/OverlayGASPStorage.js +151 -77
  14. package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
  15. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  16. package/dist/types/src/Engine.d.ts +14 -115
  17. package/dist/types/src/Engine.d.ts.map +1 -1
  18. package/dist/types/src/GASP/OverlayGASPRemote.d.ts +3 -2
  19. package/dist/types/src/GASP/OverlayGASPRemote.d.ts.map +1 -1
  20. package/dist/types/src/GASP/OverlayGASPStorage.d.ts +29 -3
  21. package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
  22. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  23. package/package.json +1 -1
  24. package/src/Engine.ts +68 -9
  25. package/src/__tests/Engine.test.ts +95 -1
  26. package/dist/cjs/src/SHIPAdvertisement.js +0 -3
  27. package/dist/cjs/src/SHIPAdvertisement.js.map +0 -1
  28. package/dist/cjs/src/SLAPAdvertisement.js +0 -3
  29. package/dist/cjs/src/SLAPAdvertisement.js.map +0 -1
  30. package/dist/esm/src/SHIPAdvertisement.js +0 -2
  31. package/dist/esm/src/SHIPAdvertisement.js.map +0 -1
  32. package/dist/esm/src/SLAPAdvertisement.js +0 -2
  33. package/dist/esm/src/SLAPAdvertisement.js.map +0 -1
  34. package/dist/types/src/SHIPAdvertisement.d.ts +0 -9
  35. package/dist/types/src/SHIPAdvertisement.d.ts.map +0 -1
  36. package/dist/types/src/SLAPAdvertisement.d.ts +0 -9
  37. package/dist/types/src/SLAPAdvertisement.d.ts.map +0 -1
@@ -1,5 +1,8 @@
1
- import { Transaction, MerklePath, isBroadcastFailure } from '@bsv/sdk';
1
+ import { Transaction, isBroadcastFailure } from '@bsv/sdk';
2
2
  import { GASP } from '@bsv/gasp';
3
+ import { OverlayGASPRemote } from './GASP/OverlayGASPRemote.js';
4
+ import { OverlayGASPStorage } from './GASP/OverlayGASPStorage.js';
5
+ import { URL } from "url";
3
6
  /**
4
7
  * Am engine for running BSV Overlay Services (topic managers and lookup services).
5
8
  */
@@ -16,12 +19,13 @@ export class Engine {
16
19
  syncConfiguration;
17
20
  logTime;
18
21
  logPrefix;
22
+ throwOnBroadcastFailure;
19
23
  /**
20
24
  * Creates a new Overlay Services Engine
21
25
  * @param {[key: string]: TopicManager} managers - manages topic admittance
22
26
  * @param {[key: string]: LookupService} lookupServices - manages UTXO lookups
23
27
  * @param {Storage} storage - for interacting with internally-managed persistent data
24
- * @param {ChainTracker} chainTracker - Verifies SPV data associated with transactions
28
+ * @param {ChainTracker | 'scripts only'} chainTracker - Verifies SPV data associated with transactions
25
29
  * @param {string} [hostingURL] - The URL this engine is hosted at. Required if going to support peer-discovery with an advertiser.
26
30
  * @param {Broadcaster} [Broadcaster] - broadcaster used for broadcasting the incoming transaction
27
31
  * @param {Advertiser} [Advertiser] - handles SHIP and SLAP advertisements for peer-discovery
@@ -30,8 +34,9 @@ export class Engine {
30
34
  * @param {SyncConfiguration} syncConfiguration — Configuration object describing historical synchronization of topics.
31
35
  * @param {boolean} logTime - Enables / disables the timing logs for various operations in the Overlay submit route.
32
36
  * @param {string} logPrefix - Supports overriding the log prefix with a custom string.
37
+ * @param {boolean} throwOnBroadcastFailure - Enables / disables throwing an error when a transaction broadcast failure is detected.
33
38
  */
34
- constructor(managers, lookupServices, storage, chainTracker, hostingURL, shipTrackers, slapTrackers, broadcaster, advertiser, syncConfiguration, logTime = false, logPrefix = '[OVERLAY_ENGINE] ') {
39
+ constructor(managers, lookupServices, storage, chainTracker, hostingURL, shipTrackers, slapTrackers, broadcaster, advertiser, syncConfiguration, logTime = false, logPrefix = '[OVERLAY_ENGINE] ', throwOnBroadcastFailure = false) {
35
40
  this.managers = managers;
36
41
  this.lookupServices = lookupServices;
37
42
  this.storage = storage;
@@ -44,6 +49,7 @@ export class Engine {
44
49
  this.syncConfiguration = syncConfiguration;
45
50
  this.logTime = logTime;
46
51
  this.logPrefix = logPrefix;
52
+ this.throwOnBroadcastFailure = throwOnBroadcastFailure;
47
53
  // To encourage synchronization of overlay services, the SHIP sync strategy is used by default for all overlay topics, except for 'tm_ship' and 'tm_slap'.
48
54
  // For these two topics, any existing trackers are combined with the provided shipTrackers and slapTrackers omitting any duplicates.
49
55
  if (syncConfiguration === undefined) {
@@ -93,7 +99,6 @@ export class Engine {
93
99
  * @param {TaggedBEEF} taggedBEEF - The transaction to process
94
100
  * @param {function(STEAK): void} [onSTEAKReady] - Optional callback function invoked when the STEAK is ready.
95
101
  * @param {string} mode — Indicates the submission behavior, whether historical or current. Historical transactions are not broadcast or propagated.
96
- * @param {boolean} log
97
102
  *
98
103
  * The optional callback function should be used to get STEAK when ready, and avoid waiting for broadcast and transaction propagation to complete.
99
104
  *
@@ -111,11 +116,11 @@ export class Engine {
111
116
  this.startTime(`submit_${txid}`);
112
117
  this.startTime(`chainTracker_${txid.substring(0, 10)}`);
113
118
  const txValid = await tx.verify(this.chainTracker);
114
- this.endTime(`chainTracker_${txid.substring(0, 10)}`);
115
119
  if (!txValid)
116
120
  throw new Error('Unable to verify SPV information.');
121
+ this.endTime(`chainTracker_${txid.substring(0, 10)}`);
117
122
  const steak = {};
118
- let admissableOutputs = { outputsToAdmit: [], coinsToRetain: [] };
123
+ let admissibleOutputs = { outputsToAdmit: [], coinsToRetain: [] };
119
124
  const previousCoins = [];
120
125
  // Parallelize the topic processing
121
126
  const topicPromises = taggedBEEF.topics.map(async (topic) => {
@@ -131,19 +136,21 @@ export class Engine {
131
136
  return;
132
137
  }
133
138
  // Check if any input of this transaction is a previous UTXO
134
- const outputPromises = tx.inputs.map(input => {
135
- const previousTXID = input.sourceTXID || input.sourceTransaction?.id('hex');
136
- return this.storage.findOutput(previousTXID, input.sourceOutputIndex, topic);
139
+ const outputPromises = tx.inputs.map(async (input, i) => {
140
+ const previousTXID = input.sourceTXID !== undefined ? input.sourceTXID : input.sourceTransaction?.id('hex');
141
+ if (previousTXID !== undefined) {
142
+ const output = this.storage.findOutput(previousTXID, input.sourceOutputIndex, topic);
143
+ if (output !== undefined && output !== null) {
144
+ previousCoins.push(i);
145
+ return await Promise.resolve(output);
146
+ }
147
+ }
148
+ return await Promise.resolve(null);
137
149
  });
138
150
  this.startTime(`previousOutputQuery_${txid.substring(0, 10)}`);
139
151
  const outputs = await Promise.all(outputPromises);
140
152
  this.endTime(`previousOutputQuery_${txid.substring(0, 10)}`);
141
- outputs.forEach((output, i) => {
142
- if (output !== undefined && output !== null) {
143
- previousCoins.push(i);
144
- }
145
- });
146
- const markSpentPromises = outputs.map(async (output, i) => {
153
+ const markSpentPromises = outputs.map(async (output) => {
147
154
  if (output !== undefined && output !== null) {
148
155
  try {
149
156
  await this.storage.markUTXOAsSpent(output.txid, output.outputIndex, topic);
@@ -165,7 +172,7 @@ export class Engine {
165
172
  const admissibleOutputsPromise = (async () => {
166
173
  try {
167
174
  this.startTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`);
168
- admissableOutputs = await this.managers[topic].identifyAdmissibleOutputs(taggedBEEF.beef, previousCoins);
175
+ admissibleOutputs = await this.managers[topic].identifyAdmissibleOutputs(taggedBEEF.beef, previousCoins);
169
176
  this.endTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`);
170
177
  }
171
178
  catch (_) {
@@ -175,15 +182,17 @@ export class Engine {
175
182
  // Wait for both tasks to complete
176
183
  await Promise.all([markSpentPromises, admissibleOutputsPromise]);
177
184
  // Keep track of what outputs were admitted for what topic
178
- steak[topic] = admissableOutputs;
185
+ steak[topic] = admissibleOutputs;
179
186
  });
180
187
  await Promise.all(topicPromises);
181
188
  // Broadcast the transaction if not historical and broadcaster is configured
182
189
  this.startTime(`broadcast_${txid.substring(0, 10)}`);
183
190
  if (mode !== 'historical-tx' && this.broadcaster !== undefined) {
184
191
  const response = await this.broadcaster.broadcast(tx);
185
- if (isBroadcastFailure(response)) {
186
- throw new Error(`Failed to broadcast transaction! Error: ${response.description}`);
192
+ if (isBroadcastFailure(response) && this.throwOnBroadcastFailure) {
193
+ const e = new Error(`Failed to broadcast transaction! Error: ${response.description}`);
194
+ e.more = response.more;
195
+ throw e;
187
196
  }
188
197
  }
189
198
  this.endTime(`broadcast_${txid.substring(0, 10)}`);
@@ -194,7 +203,7 @@ export class Engine {
194
203
  }
195
204
  for (const topic of taggedBEEF.topics) {
196
205
  // Keep track of which outputs to admit, mark as stale, or retain
197
- const outputsToAdmit = admissableOutputs.outputsToAdmit;
206
+ const outputsToAdmit = admissibleOutputs.outputsToAdmit;
198
207
  const staleCoins = [];
199
208
  const outputsConsumed = [];
200
209
  // Find which outputs should not be retained and mark them as stale
@@ -202,7 +211,7 @@ export class Engine {
202
211
  for (const inputIndex of previousCoins) {
203
212
  const previousTXID = tx.inputs[inputIndex].sourceTXID || tx.inputs[inputIndex].sourceTransaction?.id('hex');
204
213
  const previousOutputIndex = tx.inputs[inputIndex].sourceOutputIndex;
205
- if (!admissableOutputs.coinsToRetain.includes(inputIndex)) {
214
+ if (!admissibleOutputs.coinsToRetain.includes(inputIndex)) {
206
215
  staleCoins.push({
207
216
  txid: previousTXID,
208
217
  outputIndex: previousOutputIndex
@@ -242,7 +251,7 @@ export class Engine {
242
251
  this.endTime(`insertNewOutput_${txid.substring(0, 10)}`);
243
252
  newUTXOs.push({ txid, outputIndex });
244
253
  this.startTime(`notifyLookupService${txid.substring(0, 10)}`);
245
- await Promise.all(Object.values(this.lookupServices).map(l => l.outputAdded?.(txid, outputIndex, tx.outputs[outputIndex].lockingScript, topic)));
254
+ await Promise.all(Object.values(this.lookupServices).map(async (l) => await l.outputAdded?.(txid, outputIndex, tx.outputs[outputIndex].lockingScript, topic)));
246
255
  this.endTime(`notifyLookupService${txid.substring(0, 10)}`);
247
256
  }));
248
257
  this.startTime(`outputConsumed_${txid.substring(0, 10)}`);
@@ -266,10 +275,10 @@ export class Engine {
266
275
  if (this.advertiser === undefined || mode === 'historical-tx') {
267
276
  return steak;
268
277
  }
269
- this.startTime(`transactionPropgation_${txid.substring(0, 10)}`);
278
+ this.startTime(`transactionPropagation_${txid.substring(0, 10)}`);
270
279
  // Propagate transaction to other nodes according to synchronization agreements
271
280
  // 1. Find nodes that host the topics associated with admissable outputs
272
- // We want to figure out which topics we actually care about(because their associated outputs were admitted)
281
+ // We want to figure out which topics we actually care about (because their associated outputs were admitted)
273
282
  // AND if the topic was not admitted we want to remove it from the list of topics we care about.
274
283
  const relevantTopics = taggedBEEF.topics.filter(topic => steak[topic] !== undefined && steak[topic].outputsToAdmit.length !== 0);
275
284
  // TODO: Cache ship/slap lookup with expiry (every 5min)
@@ -419,7 +428,7 @@ export class Engine {
419
428
  * @returns {Promise<void>} A promise that resolves when the synchronization process is complete.
420
429
  */
421
430
  async syncAdvertisements() {
422
- if (this.advertiser === undefined) {
431
+ if (this.advertiser === undefined || !this.hostingURL || !this.isValidUrl(this.hostingURL)) {
423
432
  return;
424
433
  }
425
434
  const advertiser = this.advertiser;
@@ -850,6 +859,53 @@ export class Engine {
850
859
  const documentation = await this.lookupServices[provider]?.getDocumentation?.();
851
860
  return documentation !== undefined ? documentation : 'No documentation found!';
852
861
  }
862
+ /**
863
+ * Validates a URL to ensure it does not match disallowed patterns:
864
+ * - Contains "http:" protocol
865
+ * - Contains "localhost" (with or without a port)
866
+ * - Internal or non-routable IP addresses (e.g., 192.168.x.x, 10.x.x.x, 172.16.x.x to 172.31.x.x)
867
+ * - Non-routable IPs like 127.x.x.x, 0.0.0.0, or IPv6 loopback (::1)
868
+ *
869
+ * @param url - The URL string to validate
870
+ * @returns {boolean} - Returns `false` if the URL violates any of the conditions `true` otherwise
871
+ */
872
+ isValidUrl(url) {
873
+ try {
874
+ const parsedUrl = new URL(url);
875
+ // Disallow http:
876
+ if (parsedUrl.protocol === "http:") {
877
+ return false;
878
+ }
879
+ // Disallow localhost with or without a port
880
+ if (/^localhost(:\d+)?$/i.test(parsedUrl.hostname)) {
881
+ return false;
882
+ }
883
+ // Disallow internal and non-routable IP addresses
884
+ const ipAddress = parsedUrl.hostname;
885
+ // Regex for non-routable IPv4 IPs
886
+ const nonRoutableIpv4Patterns = [
887
+ /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, // Loopback IPs
888
+ /^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, // 10.x.x.x private IPs
889
+ /^192\.168\.\d{1,3}\.\d{1,3}$/, // 192.168.x.x private IPs
890
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/, // 172.16.x.x to 172.31.x.x private IPs
891
+ /^0\.0\.0\.0$/ // Non-routable address
892
+ ];
893
+ // Check for IPv4 matches
894
+ if (nonRoutableIpv4Patterns.some((pattern) => pattern.test(ipAddress))) {
895
+ return false;
896
+ }
897
+ // Check for non-routable IPv6 addresses explicitly
898
+ if (ipAddress === "[::1]") {
899
+ return false;
900
+ }
901
+ // If none of the disallowed conditions matched, the URL is valid
902
+ return true;
903
+ }
904
+ catch (error) {
905
+ // If the input is not a valid URL, return false
906
+ return false;
907
+ }
908
+ }
853
909
  }
854
910
  //////////
855
911
  // OTHER FILES
@@ -859,390 +915,5 @@ There is currently a bug with the test runner that prevents importing and using
859
915
  Thus, all non-type exports have been moved to Engine.
860
916
  */
861
917
  // TODO: fix bug with imports that break tests. -----[GASP/OverlayGASPRemote.ts]-----
862
- export class OverlayGASPRemote {
863
- endpointURL;
864
- topic;
865
- constructor(endpointURL, topic) {
866
- this.endpointURL = endpointURL;
867
- this.topic = topic;
868
- }
869
- /**
870
- * Given an outgoing initial request, sends the request to the foreign instance and obtains their initial response.
871
- * @param request
872
- * @returns
873
- */
874
- async getInitialResponse(request) {
875
- // Send out an HTTP request to the URL (current host for topic)
876
- // Include the topic in the request
877
- // Parse out response and return correct format
878
- const url = `${this.endpointURL}/requestSyncResponse`;
879
- const response = await fetch(url, {
880
- method: 'POST',
881
- headers: {
882
- 'Content-Type': 'application/json',
883
- 'X-BSV-Topic': this.topic
884
- },
885
- body: JSON.stringify(request)
886
- });
887
- if (!response.ok) {
888
- throw new Error(`HTTP error! Status: ${response.status}`);
889
- }
890
- const result = await response.json();
891
- // Validate and return the response in the correct format
892
- if (!Array.isArray(result.UTXOList) || typeof result.since !== 'number') {
893
- throw new Error('Invalid response format');
894
- }
895
- return {
896
- UTXOList: result.UTXOList.map((utxo) => ({
897
- txid: utxo.txid,
898
- outputIndex: utxo.outputIndex
899
- })),
900
- since: result.since
901
- };
902
- }
903
- /**
904
- * Given an outgoing txid, outputIndex and optional metadata, request the associated GASP node from the foreign instance.
905
- * @param graphID
906
- * @param txid
907
- * @param outputIndex
908
- * @param metadata
909
- * @returns
910
- */
911
- async requestNode(graphID, txid, outputIndex, metadata) {
912
- // Send an HTTP request with the provided info and get back a gaspNode
913
- const url = `${this.endpointURL}/requestForeignGASPNode`;
914
- const body = {
915
- graphID,
916
- txid,
917
- outputIndex,
918
- metadata
919
- };
920
- const response = await fetch(url, {
921
- method: 'POST',
922
- headers: {
923
- 'Content-Type': 'application/json'
924
- },
925
- body: JSON.stringify(body)
926
- });
927
- if (!response.ok) {
928
- throw new Error(`HTTP error! Status: ${response.status}`);
929
- }
930
- const result = await response.json();
931
- // Validate and return the response in the correct format
932
- if (typeof result.graphID !== 'string' || typeof result.rawTx !== 'string' || typeof result.outputIndex !== 'number') {
933
- throw new Error('Invalid response format');
934
- }
935
- const gaspNode = {
936
- graphID: result.graphID,
937
- rawTx: result.rawTx,
938
- outputIndex: result.outputIndex,
939
- proof: result.proof,
940
- txMetadata: result.txMetadata,
941
- outputMetadata: result.outputMetadata,
942
- inputs: result.inputs
943
- };
944
- return gaspNode;
945
- }
946
- // ---- Now optional methods ----
947
- // When are only syncing to them
948
- async getInitialReply(response) {
949
- throw new Error('Function not supported!');
950
- }
951
- // Only used when supporting bidirectional sync.
952
- // Overlay services does not support this.
953
- async submitNode(node) {
954
- throw new Error('Node submission not supported!');
955
- }
956
- }
957
- export class OverlayGASPStorage {
958
- topic;
959
- engine;
960
- maxNodesInGraph;
961
- temporaryGraphNodeRefs = {};
962
- constructor(topic, engine, maxNodesInGraph) {
963
- this.topic = topic;
964
- this.engine = engine;
965
- this.maxNodesInGraph = maxNodesInGraph;
966
- }
967
- /**
968
- *
969
- * @param since
970
- * @returns
971
- */
972
- async findKnownUTXOs(since) {
973
- const UTXOs = await this.engine.storage.findUTXOsForTopic(this.topic, since);
974
- return UTXOs.map(output => ({
975
- txid: output.txid,
976
- outputIndex: output.outputIndex
977
- }));
978
- }
979
- /**
980
- * For a given txid and output index, returns the associated transaction, a merkle proof if the transaction is in a block, and metadata if if requested. If no metadata is requested, metadata hashes on inputs are not returned.
981
- * @param graphID
982
- * @param txid
983
- * @param outputIndex
984
- * @param metadata
985
- * @returns
986
- */
987
- async hydrateGASPNode(graphID, txid, outputIndex, metadata) {
988
- const output = await this.engine.storage.findOutput(txid, outputIndex, undefined, undefined, true);
989
- if (output?.beef === undefined) {
990
- throw new Error('No matching output found!');
991
- }
992
- const tx = Transaction.fromBEEF(output.beef);
993
- const rawTx = tx.toHex();
994
- const node = {
995
- rawTx,
996
- graphID,
997
- outputIndex
998
- };
999
- if (tx.merklePath !== undefined) {
1000
- node.proof = tx.merklePath.toHex();
1001
- }
1002
- return node;
1003
- }
1004
- /**
1005
- * For a given node, returns the inputs needed to complete the graph, including whether updated metadata is requested for those inputs.
1006
- * @param tx The node for which needed inputs should be found.
1007
- * @returns A promise for a mapping of requested input transactions and whether metadata should be provided for each.
1008
- */
1009
- async findNeededInputs(tx) {
1010
- // If there is no Merkle proof, we always need the inputs
1011
- const response = {
1012
- requestedInputs: {}
1013
- };
1014
- const parsedTx = Transaction.fromHex(tx.rawTx);
1015
- if (tx.proof === undefined) {
1016
- for (const input of parsedTx.inputs) {
1017
- response.requestedInputs[`${input.sourceTXID}.${input.sourceOutputIndex}`] = {
1018
- metadata: false
1019
- };
1020
- }
1021
- return await this.stripAlreadyKnownInputs(response);
1022
- }
1023
- // Attempt to check if the current transaction is admissible
1024
- parsedTx.merklePath = MerklePath.fromHex(tx.proof);
1025
- const admittanceResult = await this.engine.managers[this.topic].identifyAdmissibleOutputs(parsedTx.toBEEF(), []);
1026
- if (admittanceResult.outputsToAdmit.includes(tx.outputIndex)) {
1027
- // The transaction is admissible, no further inputs are needed
1028
- }
1029
- else {
1030
- // The transaction is not admissible, get inputs needed for further verification
1031
- // TopicManagers should implement a function to identify which inputs are needed.
1032
- if (this.engine.managers[this.topic] !== undefined && typeof this.engine.managers[this.topic].identifyNeededInputs === 'function') {
1033
- try {
1034
- const neededInputs = await this.engine.managers[this.topic].identifyNeededInputs?.(parsedTx.toBEEF()) ?? [];
1035
- for (const input of neededInputs) {
1036
- response.requestedInputs[`${input.txid}.${input.outputIndex}`] = {
1037
- metadata: false
1038
- };
1039
- }
1040
- return await this.stripAlreadyKnownInputs(response);
1041
- }
1042
- catch (e) {
1043
- console.error(`An error occurred when identifying needed inputs for transaction: ${parsedTx.id('hex')}.${tx.outputIndex}!`);
1044
- // Cut off the graph in case of an error here.
1045
- }
1046
- }
1047
- // By default, if the topic manager isn't able to stipulate needed inputs, only the inputs necessary for SPV are requested.
1048
- }
1049
- // Everything else falls through to returning undefined/void, which will terminate the synchronization at this point.
1050
- }
1051
- /**
1052
- * Ensures that no inputs are requested from foreign nodes before sending any GASP response
1053
- * Also terminates graphs if the response would be empty.
1054
- */
1055
- async stripAlreadyKnownInputs(response) {
1056
- if (typeof response === 'undefined') {
1057
- return response;
1058
- }
1059
- for (const inputNodeId of Object.keys(response.requestedInputs)) {
1060
- const [txid, outputIndex] = inputNodeId.split('.');
1061
- const found = await this.engine.storage.findOutput(txid, Number(outputIndex), this.topic);
1062
- if (found !== null && found !== undefined) {
1063
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1064
- delete response.requestedInputs[inputNodeId];
1065
- }
1066
- }
1067
- if (Object.keys(response.requestedInputs).length === 0) {
1068
- return undefined;
1069
- }
1070
- return response;
1071
- }
1072
- /**
1073
- * Appends a new node to a temporary graph.
1074
- * @param tx The node to append to this graph.
1075
- * @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.
1076
- * @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.
1077
- */
1078
- async appendToGraph(tx, spentBy) {
1079
- if (this.maxNodesInGraph !== undefined && Object.keys(this.temporaryGraphNodeRefs).length >= this.maxNodesInGraph) {
1080
- throw new Error('The max number of nodes in transaction graph has been reached!');
1081
- }
1082
- const parsedTx = Transaction.fromHex(tx.rawTx);
1083
- const txid = parsedTx.id('hex');
1084
- if (tx.proof !== undefined) {
1085
- parsedTx.merklePath = MerklePath.fromHex(tx.proof);
1086
- }
1087
- // Given the passed in node, append to the temp graph
1088
- // Use the spentBy param which should be a txid.inputIndex for the node which spent this one in 36-byte format
1089
- const newGraphNode = {
1090
- txid,
1091
- graphID: tx.graphID,
1092
- rawTx: tx.rawTx,
1093
- outputIndex: tx.outputIndex,
1094
- proof: tx.proof,
1095
- txMetadata: tx.txMetadata,
1096
- outputMetadata: tx.outputMetadata,
1097
- inputs: tx.inputs,
1098
- children: []
1099
- };
1100
- // If spentBy is undefined, then we know it's the root node.
1101
- if (spentBy === undefined) {
1102
- this.temporaryGraphNodeRefs[tx.graphID] = newGraphNode;
1103
- }
1104
- else {
1105
- // Find the parent node based on spentBy
1106
- const parentNode = this.temporaryGraphNodeRefs[spentBy];
1107
- if (parentNode !== undefined) {
1108
- // Set parent-child relationship
1109
- parentNode.children.push(newGraphNode);
1110
- newGraphNode.parent = parentNode;
1111
- this.temporaryGraphNodeRefs[`${newGraphNode.txid}.${newGraphNode.outputIndex}`] = newGraphNode;
1112
- }
1113
- else {
1114
- throw new Error(`Parent node with GraphID ${spentBy} not found`);
1115
- }
1116
- }
1117
- }
1118
- /**
1119
- * 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.
1120
- * 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,
1121
- * while considering any coins which the Manager had previously indicated were either valid or invalid.
1122
- * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph.
1123
- * @throws If the graph is not well-anchored, according to the rules of Bitcoin or the rules of the Overlay Topic Manager.
1124
- */
1125
- async validateGraphAnchor(graphID) {
1126
- const rootNode = this.temporaryGraphNodeRefs[graphID];
1127
- if (rootNode === undefined) {
1128
- throw new Error(`Graph node with ID ${graphID} not found`);
1129
- }
1130
- // Check that the root node is Bitcoin-valid.
1131
- const beef = this.getBEEFForNode(rootNode);
1132
- const spvTx = Transaction.fromBEEF(beef);
1133
- const isBitcoinValid = await spvTx.verify(this.engine.chainTracker);
1134
- if (!isBitcoinValid) {
1135
- throw new Error('The graph is not well-anchored according to the rules of Bitcoin.');
1136
- }
1137
- // Then, ensure the node is Overlay-valid.
1138
- const beefs = this.computeOrderedBEEFsForGraph(graphID);
1139
- // coins: a Set of all historical coins to retain (no need to remove them), used to emulate topical admittance of previous inputs over time.
1140
- const coins = new Set();
1141
- // Submit all historical BEEFs in order through the topic manager, tracking what would be retained until we submit the root node last.
1142
- // If, at the end, the root node is admitted, we have a valid overlay-specific graph.
1143
- for (const beef of beefs) {
1144
- // For any input to this transaction, see if it's a valid coin that's admitted. If so, it's a previous coin.
1145
- const previousCoins = [];
1146
- const tx = Transaction.fromBEEF(beef);
1147
- for (const inputIndex in tx.inputs) {
1148
- const input = tx.inputs[inputIndex];
1149
- const sourceTXID = input.sourceTXID || input.sourceTransaction?.id('hex');
1150
- const coin = `${sourceTXID}.${input.sourceOutputIndex}`;
1151
- if (coins.has(coin)) {
1152
- previousCoins.push(Number(inputIndex));
1153
- }
1154
- }
1155
- const admittanceInstructions = await this.engine.managers[this.topic].identifyAdmissibleOutputs(beef, previousCoins);
1156
- // Every admitted output is now a coin.
1157
- for (const outputIndex of admittanceInstructions.outputsToAdmit) {
1158
- coins.add(`${tx.id('hex')}.${outputIndex}`);
1159
- }
1160
- }
1161
- // After sending through all the graph's BEEFs...
1162
- // If the root node is now a coin, we have acceptance by the overlay.
1163
- // Otherwise, throw.
1164
- if (!coins.has(graphID)) {
1165
- throw new Error('This graph did not result in topical admittance of the root node. Rejecting.');
1166
- }
1167
- }
1168
- /**
1169
- * Deletes all data associated with a temporary graph that has failed to sync, if the graph exists.
1170
- * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph.
1171
- */
1172
- async discardGraph(graphID) {
1173
- for (const [nodeId, graphRef] of Object.entries(this.temporaryGraphNodeRefs)) {
1174
- if (graphRef.graphID === graphID) {
1175
- // Delete child node
1176
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1177
- delete this.temporaryGraphNodeRefs[nodeId];
1178
- }
1179
- }
1180
- }
1181
- /**
1182
- * Finalizes a graph, solidifying the new UTXO and its ancestors so that it will appear in the list of known UTXOs.
1183
- * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the root of this graph.
1184
- */
1185
- async finalizeGraph(graphID) {
1186
- const beefs = this.computeOrderedBEEFsForGraph(graphID);
1187
- // Submit all historical BEEFs in order, finalizing the graph for the current UTXO
1188
- for (const beef of beefs) {
1189
- await this.engine.submit({
1190
- beef,
1191
- topics: [this.topic]
1192
- }, () => { }, 'historical-tx');
1193
- }
1194
- }
1195
- /**
1196
- * Computes an ordered set of BEEFs for the graph with the given graph IDs
1197
- * @param {string} graphID — The ID of the graph for which BEEFs are required
1198
- * @returns Ordered BEEFs for the graph
1199
- */
1200
- computeOrderedBEEFsForGraph(graphID) {
1201
- const beefs = [];
1202
- const hydrator = (node) => {
1203
- const currentBEEF = this.getBEEFForNode(node);
1204
- if (beefs.indexOf(currentBEEF) === -1) {
1205
- beefs.unshift(currentBEEF);
1206
- }
1207
- for (const child of node.children) {
1208
- // Continue backwards to the earliest nodes, adding them onto the beginning
1209
- hydrator(child);
1210
- }
1211
- };
1212
- // Start the hydrator with the root node
1213
- const foundRoot = this.temporaryGraphNodeRefs[graphID];
1214
- if (!foundRoot) {
1215
- throw new Error('Unable to find root node in graph for finalization!');
1216
- }
1217
- hydrator(foundRoot);
1218
- return beefs;
1219
- }
1220
- /**
1221
- * Computes a full BEEF for a given graph node, based on the temporary graph store.
1222
- * @param node Graph node for which BEEF is needed.
1223
- * @returns BEEF array, including all proofs on inputs.
1224
- */
1225
- getBEEFForNode(node) {
1226
- // Given a node, hydrate its merkle proof or all inputs, returning a reference to the hydrated node's Transaction object
1227
- const hydrator = (node) => {
1228
- const tx = Transaction.fromHex(node.rawTx);
1229
- if (node.proof) {
1230
- tx.merklePath = MerklePath.fromHex(node.proof);
1231
- return tx; // Transaction with proof, end of the line.
1232
- }
1233
- // For each input, look it up and recurse.
1234
- for (const inputIndex in tx.inputs) {
1235
- const input = tx.inputs[inputIndex];
1236
- const foundNode = this.temporaryGraphNodeRefs[`${input.sourceTXID}.${input.sourceOutputIndex}`];
1237
- if (!foundNode) {
1238
- 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.');
1239
- }
1240
- tx.inputs[inputIndex].sourceTransaction = hydrator(foundNode);
1241
- }
1242
- return tx;
1243
- };
1244
- const finalTX = hydrator(node);
1245
- return finalTX.toBEEF();
1246
- }
1247
- }
918
+ // TODO: fix bug with imports that break tests. -----[GASP/OverlayGASPStorage.ts]-----
1248
919
  //# sourceMappingURL=Engine.js.map