UNPKG

@bsv/overlay

Version:
301 lines 15.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OverlayGASPStorage = void 0; const sdk_1 = require("@bsv/sdk"); class OverlayGASPStorage { constructor(topic, engine, maxNodesInGraph) { this.topic = topic; this.engine = engine; this.maxNodesInGraph = maxNodesInGraph; this.temporaryGraphNodeRefs = {}; } /** * * @param since * @returns */ async findKnownUTXOs(since) { const UTXOs = await this.engine.storage.findUTXOsForTopic(this.topic, since); return UTXOs.map(output => { var _a; return ({ txid: output.txid, outputIndex: output.outputIndex, score: (_a = output.score) !== null && _a !== void 0 ? _a : 0 }); }); } /** * 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. * @param graphID * @param txid * @param outputIndex * @param metadata * @returns */ async hydrateGASPNode(graphID, txid, outputIndex, metadata) { const output = await this.engine.storage.findOutput(txid, outputIndex, undefined, undefined, true); if ((output === null || output === void 0 ? void 0 : output.beef) === undefined) { throw new Error('No matching output found!'); } const tx = sdk_1.Transaction.fromBEEF(output.beef); const rawTx = tx.toHex(); const node = { rawTx, graphID, outputIndex }; if (tx.merklePath !== undefined) { node.proof = tx.merklePath.toHex(); } return node; } /** * For a given node, returns the inputs needed to complete the graph, including whether updated metadata is requested for those inputs. * @param tx The node for which needed inputs should be found. * @returns A promise for a mapping of requested input transactions and whether metadata should be provided for each. */ async findNeededInputs(tx) { var _a, _b, _c, _d; // If there is no Merkle proof, we always need the inputs const response = { requestedInputs: {} }; const parsedTx = sdk_1.Transaction.fromHex(tx.rawTx); if (tx.proof === undefined) { for (const input of parsedTx.inputs) { response.requestedInputs[`${(_a = input.sourceTXID) !== null && _a !== void 0 ? _a : ''}.${input.sourceOutputIndex}`] = { metadata: false }; } return await this.stripAlreadyKnownInputs(response); } // Attempt to check if the current transaction is admissible parsedTx.merklePath = sdk_1.MerklePath.fromHex(tx.proof); const admittanceResult = await this.engine.managers[this.topic].identifyAdmissibleOutputs(parsedTx.toBEEF(), [], typeof tx.txMetadata === 'string' ? sdk_1.Utils.toArray(tx.txMetadata) : undefined); if (admittanceResult.outputsToAdmit.includes(tx.outputIndex)) { // The transaction is admissible, no further inputs are needed } else { // The transaction is not admissible, get inputs needed for further verification // TopicManagers should implement a function to identify which inputs are needed. if (this.engine.managers[this.topic] !== undefined && typeof this.engine.managers[this.topic].identifyNeededInputs === 'function') { try { const neededInputs = (_d = await ((_c = (_b = this.engine.managers[this.topic]).identifyNeededInputs) === null || _c === void 0 ? void 0 : _c.call(_b, parsedTx.toBEEF()))) !== null && _d !== void 0 ? _d : []; for (const input of neededInputs) { response.requestedInputs[`${input.txid}.${input.outputIndex}`] = { metadata: false }; } return await this.stripAlreadyKnownInputs(response); } catch (e) { console.error(`An error occurred when identifying needed inputs for transaction: ${parsedTx.id('hex')}.${tx.outputIndex}!`); // Cut off the graph in case of an error here. } } // By default, if the topic manager isn't able to stipulate needed inputs, only the inputs necessary for SPV are requested. } // Everything else falls through to returning undefined/void, which will terminate the synchronization at this point. } /** * Ensures that no inputs are requested from foreign nodes before sending any GASP response * Also terminates graphs if the response would be empty. */ async stripAlreadyKnownInputs(response) { if (typeof response === 'undefined') { return response; } for (const inputNodeId of Object.keys(response.requestedInputs)) { const [txid, outputIndex] = inputNodeId.split('.'); const found = await this.engine.storage.findOutput(txid, Number(outputIndex), this.topic); if (found !== null && found !== undefined) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete response.requestedInputs[inputNodeId]; } } if (Object.keys(response.requestedInputs).length === 0) { return undefined; } return response; } /** * Appends a new node to a temporary graph. * @param tx The node to append to this graph. * @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. * @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. */ async appendToGraph(tx, spentBy) { if (this.maxNodesInGraph !== undefined && Object.keys(this.temporaryGraphNodeRefs).length >= this.maxNodesInGraph) { throw new Error('The max number of nodes in transaction graph has been reached!'); } const parsedTx = sdk_1.Transaction.fromHex(tx.rawTx); const txid = parsedTx.id('hex'); if (tx.proof !== undefined) { parsedTx.merklePath = sdk_1.MerklePath.fromHex(tx.proof); } // Given the passed in node, append to the temp graph // Use the spentBy param which should be a txid.inputIndex for the node which spent this one in 36-byte format const newGraphNode = { txid, graphID: tx.graphID, rawTx: tx.rawTx, outputIndex: tx.outputIndex, proof: tx.proof, txMetadata: tx.txMetadata, outputMetadata: tx.outputMetadata, inputs: tx.inputs, children: [] }; // If spentBy is undefined, then we know it's the root node. if (spentBy === undefined) { this.temporaryGraphNodeRefs[tx.graphID] = newGraphNode; } else { // Find the parent node based on spentBy const parentNode = this.temporaryGraphNodeRefs[spentBy]; if (parentNode !== undefined) { // Set parent-child relationship parentNode.children.push(newGraphNode); newGraphNode.parent = parentNode; this.temporaryGraphNodeRefs[`${newGraphNode.txid}.${newGraphNode.outputIndex}`] = newGraphNode; } else { throw new Error(`Parent node with GraphID ${spentBy} not found`); } } } /** * 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. * 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, * while considering any coins which the Manager had previously indicated were either valid or invalid. * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph. * @throws If the graph is not well-anchored, according to the rules of Bitcoin or the rules of the Overlay Topic Manager. */ async validateGraphAnchor(graphID) { var _a, _b; const rootNode = this.temporaryGraphNodeRefs[graphID]; if (rootNode === undefined) { throw new Error(`Graph node with ID ${graphID} not found`); } // Check that the root node is Bitcoin-valid. const beef = this.getBEEFForNode(rootNode); const spvTx = sdk_1.Transaction.fromBEEF(beef); const isBitcoinValid = await spvTx.verify(this.engine.chainTracker); if (!isBitcoinValid) { throw new Error('The graph is not well-anchored according to the rules of Bitcoin.'); } // Then, ensure the node is Overlay-valid. const beefs = this.computeOrderedBEEFsForGraph(graphID); // coins: a Set of all historical coins to retain (no need to remove them), used to emulate topical admittance of previous inputs over time. const coins = new Set(); // Submit all historical BEEFs in order through the topic manager, tracking what would be retained until we submit the root node last. // If, at the end, the root node is admitted, we have a valid overlay-specific graph. for (const beef of beefs) { // For any input to this transaction, see if it's a valid coin that's admitted. If so, it's a previous coin. const previousCoins = []; const tx = sdk_1.Transaction.fromBEEF(beef); for (const [inputIndex, input] of tx.inputs.entries()) { const sourceTXID = (_a = input.sourceTXID) !== null && _a !== void 0 ? _a : (_b = input.sourceTransaction) === null || _b === void 0 ? void 0 : _b.id('hex'); if (sourceTXID != null && sourceTXID !== '') { const coin = `${sourceTXID}.${input.sourceOutputIndex}`; if (coins.has(coin)) { previousCoins.push(Number(inputIndex)); } } } const admittanceInstructions = await this.engine.managers[this.topic].identifyAdmissibleOutputs(beef, previousCoins); // Every admitted output is now a coin. for (const outputIndex of admittanceInstructions.outputsToAdmit) { coins.add(`${tx.id('hex')}.${outputIndex}`); } } // After sending through all the graph's BEEFs... // If the root node is now a coin, we have acceptance by the overlay. // Otherwise, throw. if (!coins.has(graphID)) { throw new Error('This graph did not result in topical admittance of the root node. Rejecting.'); } } /** * Deletes all data associated with a temporary graph that has failed to sync, if the graph exists. * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph. */ async discardGraph(graphID) { for (const [nodeId, graphRef] of Object.entries(this.temporaryGraphNodeRefs)) { if (graphRef.graphID === graphID) { // Delete child node // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.temporaryGraphNodeRefs[nodeId]; } } } /** * Finalizes a graph, solidifying the new UTXO and its ancestors so that it will appear in the list of known UTXOs. * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the root of this graph. */ async finalizeGraph(graphID) { const beefs = this.computeOrderedBEEFsForGraph(graphID); // Submit all historical BEEFs in order, finalizing the graph for the current UTXO for (const beef of beefs) { await this.engine.submit({ beef, topics: [this.topic] }, () => { }, 'historical-tx'); } } /** * Computes an ordered set of BEEFs for the graph with the given graph IDs * @param {string} graphID — The ID of the graph for which BEEFs are required * @returns Ordered BEEFs for the graph */ computeOrderedBEEFsForGraph(graphID) { const beefs = []; const hydrator = (node) => { const currentBEEF = this.getBEEFForNode(node); if (!beefs.includes(currentBEEF)) { beefs.unshift(currentBEEF); } for (const child of node.children) { // Continue backwards to the earliest nodes, adding them onto the beginning hydrator(child); } }; // Start the hydrator with the root node const foundRoot = this.temporaryGraphNodeRefs[graphID]; if (foundRoot == null) { throw new Error('Unable to find root node in graph for finalization!'); } hydrator(foundRoot); return beefs; } /** * Computes a full BEEF for a given graph node, based on the temporary graph store. * @param node Graph node for which BEEF is needed. * @returns BEEF array, including all proofs on inputs. */ getBEEFForNode(node) { // Given a node, hydrate its merkle proof or all inputs, returning a reference to the hydrated node's Transaction object const hydrator = (node) => { var _a; const tx = sdk_1.Transaction.fromHex(node.rawTx); if (node.proof != null && node.proof !== '') { tx.merklePath = sdk_1.MerklePath.fromHex(node.proof); return tx; // Transaction with proof, end of the line. } // For each input, look it up and recurse. for (const [inputIndex, input] of tx.inputs.entries()) { const foundNode = this.temporaryGraphNodeRefs[`${(_a = input.sourceTXID) !== null && _a !== void 0 ? _a : ''}.${input.sourceOutputIndex}`]; if (foundNode == null) { 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.'); } tx.inputs[inputIndex].sourceTransaction = hydrator(foundNode); } return tx; }; const finalTX = hydrator(node); return finalTX.toBEEF(); } } exports.OverlayGASPStorage = OverlayGASPStorage; //# sourceMappingURL=OverlayGASPStorage.js.map