Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@bsv/gasp

Package Overview
Dependencies
Maintainers
3
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@bsv/gasp - npm Package Compare versions

Comparing version
0.1.3
to
1.0.0
+18
dist/cjs/mod.js
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./src/GASP.js"), exports);
//# sourceMappingURL=mod.js.map
{"version":3,"file":"mod.js","sourceRoot":"","sources":["../../mod.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,gDAA8B"}
{
"name": "@bsv/gasp",
"version": "1.0.0",
"type": "commonjs",
"description": "Graph Aware Sync Protocol",
"files": [
"dist",
"src",
"mod.ts",
"LICENSE.txt"
],
"scripts": {
"test": "npm run build && jest",
"test:watch": "npm run build && jest --watch",
"test:coverage": "npm run build && jest --coverage",
"lint": "ts-standard --fix src/**/*.ts",
"build": "tsc -b && tsconfig-to-dual-package tsconfig.cjs.json",
"dev": "tsc -b -w",
"prepublish": "npm run build",
"doc": "ts2md --inputFilename=mod.ts --outputFilename=API.md --filenameSubstring=API --firstHeadingLevel=1"
},
"keywords": [
"blockchain",
"protocol",
"sync",
"transaction",
"GASP"
],
"author": "BSV Blockchain Association",
"license": "SEE LICENSE IN LICENSE.txt",
"devDependencies": {
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"ts-standard": "^12.0.2",
"ts2md": "^0.2.0",
"tsconfig-to-dual-package": "^1.2.0",
"typescript": "^5.2.2"
},
"dependencies": {
"@bsv/sdk": "1.1.6"
}
}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GASP = exports.LogLevel = exports.GASPVersionMismatchError = void 0;
const sdk_1 = require("@bsv/sdk");
class GASPVersionMismatchError extends Error {
constructor(message, currentVersion, foreignVersion) {
super(message);
this.code = 'ERR_GASP_VERSION_MISMATCH';
this.currentVersion = currentVersion;
this.foreignVersion = foreignVersion;
}
}
exports.GASPVersionMismatchError = GASPVersionMismatchError;
/**
* Log levels for controlling output verbosity.
*/
var LogLevel;
(function (LogLevel) {
LogLevel[LogLevel["NONE"] = 0] = "NONE";
LogLevel[LogLevel["ERROR"] = 1] = "ERROR";
LogLevel[LogLevel["WARN"] = 2] = "WARN";
LogLevel[LogLevel["INFO"] = 3] = "INFO";
LogLevel[LogLevel["DEBUG"] = 4] = "DEBUG";
})(LogLevel || (exports.LogLevel = LogLevel = {}));
/**
* Main class implementing the Graph Aware Sync Protocol.
*/
class GASP {
/**
*
* @param storage The GASP Storage interface to use
* @param remote The GASP Remote interface to use
* @param lastInteraction The timestamp when we last interacted with this remote party
* @param logPrefix Optional prefix for log messages
* @param log Whether to log messages (backwards-compatibility only)
* @param unidirectional Whether to disable the "reply" side and do pull-only
* @param logLevel The log level for the instance
* @param sequential Whether to run tasks sequentially (avoid Promise.all) or in parallel
*/
constructor(storage, remote, lastInteraction = 0, logPrefix = '[GASP] ', log = false, unidirectional = false, logLevel = LogLevel.INFO, sequential = false) {
this.storage = storage;
this.remote = remote;
this.lastInteraction = lastInteraction;
this.version = 1;
this.logPrefix = logPrefix;
this.log = log;
this.unidirectional = unidirectional;
this.logLevel = logLevel;
this.sequential = sequential;
this.validateTimestamp(this.lastInteraction);
this.logData(`GASP initialized with version: ${this.version}, lastInteraction: ${this.lastInteraction}, unidirectional: ${this.unidirectional}, logLevel: ${LogLevel[this.logLevel]}, sequential: ${this.sequential}`);
}
/**
* Helper method to execute callbacks either in parallel or sequentially,
* depending on the `sequential` flag.
*/
async runConcurrently(items, callback) {
if (this.sequential) {
// Run sequentially
for (const item of items) {
await callback(item);
}
}
else {
// Run in parallel
await Promise.all(items.map(callback));
}
}
/**
* Legacy log method for backwards compatibility only.
* Internally, logs at INFO level if `log === true`.
*/
logData(...data) {
if (this.log) {
this.infoLog(...data);
}
}
/**
* New recommended methods for logging, respecting the logLevel.
*/
errorLog(...data) {
if (this.logLevel >= LogLevel.ERROR) {
console.error(this.logPrefix, '[ERROR]', ...data);
}
}
warnLog(...data) {
if (this.logLevel >= LogLevel.WARN) {
console.warn(this.logPrefix, '[WARN]', ...data);
}
}
infoLog(...data) {
if (this.logLevel >= LogLevel.INFO) {
console.info(this.logPrefix, '[INFO]', ...data);
}
}
debugLog(...data) {
if (this.logLevel >= LogLevel.DEBUG) {
console.debug(this.logPrefix, '[DEBUG]', ...data);
}
}
validateTimestamp(timestamp) {
if (typeof timestamp !== 'number' || isNaN(timestamp) || timestamp < 0 || !Number.isInteger(timestamp)) {
throw new Error('Invalid timestamp format');
}
}
/**
* Computes a 36-byte structure from a transaction ID and output index.
* @param txid The transaction ID.
* @param index The output index.
* @returns A string representing the 36-byte structure.
*/
compute36ByteStructure(txid, index) {
const result = `${txid}.${index.toString()}`;
this.debugLog(`Computed 36-byte structure: ${result} from txid: ${txid}, index: ${index}`);
return result;
}
/**
* Deconstructs a 36-byte structure into a transaction ID and output index.
* @param outpoint The 36-byte structure.
* @returns An object containing the transaction ID and output index.
*/
deconstruct36ByteStructure(outpoint) {
const [txid, index] = outpoint.split('.');
const result = {
txid,
outputIndex: parseInt(index, 10)
};
this.debugLog(`Deconstructed 36-byte structure: ${outpoint} into txid: ${txid}, outputIndex: ${result.outputIndex}`);
return result;
}
/**
* Computes the transaction ID for a given transaction.
* @param tx The transaction string.
* @returns The computed transaction ID.
*/
computeTXID(tx) {
const txid = sdk_1.Transaction.fromHex(tx).id('hex');
this.debugLog(`Computed TXID: ${txid} from transaction: ${tx}`);
return txid;
}
/**
* Synchronizes the transaction data between the local and remote participants.
*/
async sync() {
this.infoLog(`Starting sync process. Last interaction timestamp: ${this.lastInteraction}`);
const initialRequest = await this.buildInitialRequest(this.lastInteraction);
const initialResponse = await this.remote.getInitialResponse(initialRequest);
// 1. Pull the remote UTXOs that we don't already have
if (initialResponse.UTXOList.length > 0) {
const foreignUTXOs = await this.storage.findKnownUTXOs(0);
await this.runConcurrently(initialResponse.UTXOList.filter(x => !foreignUTXOs.some(y => x.txid === y.txid && x.outputIndex === y.outputIndex)), async (UTXO) => {
try {
this.infoLog(`Requesting node for UTXO: ${JSON.stringify(UTXO)}`);
const resolvedNode = await this.remote.requestNode(this.compute36ByteStructure(UTXO.txid, UTXO.outputIndex), UTXO.txid, UTXO.outputIndex, true);
this.debugLog(`Received unspent graph node from remote: ${JSON.stringify(resolvedNode)}`);
await this.processIncomingNode(resolvedNode);
await this.completeGraph(resolvedNode.graphID);
}
catch (e) {
this.warnLog(`Error with incoming UTXO ${UTXO.txid}.${UTXO.outputIndex}: ${e.message}`);
}
});
}
// 2. Only do the “reply” half if unidirectional is disabled
if (!this.unidirectional) {
const initialReply = await this.getInitialReply(initialResponse);
this.infoLog(`Received initial reply: ${JSON.stringify(initialReply)}`);
if (initialReply.UTXOList.length > 0) {
await this.runConcurrently(initialReply.UTXOList, async (UTXO) => {
try {
this.infoLog(`Hydrating GASP node for UTXO: ${JSON.stringify(UTXO)}`);
const outgoingNode = await this.storage.hydrateGASPNode(this.compute36ByteStructure(UTXO.txid, UTXO.outputIndex), UTXO.txid, UTXO.outputIndex, true);
this.debugLog(`Sending unspent graph node for remote: ${JSON.stringify(outgoingNode)}`);
await this.processOutgoingNode(outgoingNode);
}
catch (e) {
this.warnLog(`Error with outgoing UTXO ${UTXO.txid}.${UTXO.outputIndex}: ${e.message}`);
}
});
}
}
this.infoLog('Sync completed!');
}
/**
* Builds the initial request for the sync process.
* @returns A promise for the initial request object.
*/
async buildInitialRequest(since) {
const request = {
version: this.version,
since
};
this.debugLog(`Built initial request: ${JSON.stringify(request)}`);
return request;
}
/**
* Builds the initial response based on the received request.
* @param request The initial request object.
* @returns A promise for an initial response
*/
async getInitialResponse(request) {
this.infoLog(`Received initial request: ${JSON.stringify(request)}`);
if (request.version !== this.version) {
const error = new GASPVersionMismatchError(`GASP version mismatch. Current version: ${this.version}, foreign version: ${request.version}`, this.version, request.version);
this.errorLog(`GASP version mismatch error: ${error.message}`);
throw error;
}
this.validateTimestamp(request.since);
const response = {
since: this.lastInteraction,
UTXOList: await this.storage.findKnownUTXOs(request.since)
};
this.debugLog(`Built initial response: ${JSON.stringify(response)}`);
return response;
}
/**
* Builds the initial reply based on the received response.
* @param response The initial response object.
* @returns A promise for an initial reply
*/
async getInitialReply(response) {
this.infoLog(`Received initial response: ${JSON.stringify(response)}`);
const knownUTXOs = await this.storage.findKnownUTXOs(response.since);
const filteredUTXOs = knownUTXOs.filter(x => !response.UTXOList.some(y => y.txid === x.txid && y.outputIndex === x.outputIndex));
const reply = {
UTXOList: filteredUTXOs
};
this.debugLog(`Built initial reply: ${JSON.stringify(reply)}`);
return reply;
}
/**
* Provides a requested node to a foreign instance who requested it.
*/
async requestNode(graphID, txid, outputIndex, metadata) {
this.infoLog(`Remote is requesting node with graphID: ${graphID}, txid: ${txid}, outputIndex: ${outputIndex}, metadata: ${metadata}`);
const node = await this.storage.hydrateGASPNode(graphID, txid, outputIndex, metadata);
this.debugLog(`Returning node: ${JSON.stringify(node)}`);
return node;
}
/**
* Provides a set of inputs we care about after processing a new incoming node.
* Also finalizes or discards a graph if no additional data is requested from the foreign instance.
*/
async submitNode(node) {
this.infoLog(`Remote party is submitting node: ${JSON.stringify(node)}`);
await this.storage.appendToGraph(node);
const requestedInputs = await this.storage.findNeededInputs(node);
this.debugLog(`Requested inputs: ${JSON.stringify(requestedInputs)}`);
if (!requestedInputs) {
await this.completeGraph(node.graphID);
}
return requestedInputs;
}
/**
* Handles the completion of a newly-synced graph
* @param {string} graphID The ID of the newly-synced graph
*/
async completeGraph(graphID) {
this.infoLog(`Completing newly-synced graph: ${graphID}`);
try {
await this.storage.validateGraphAnchor(graphID);
this.debugLog(`Graph validated for node: ${graphID}`);
await this.storage.finalizeGraph(graphID);
this.infoLog(`Graph finalized for node: ${graphID}`);
}
catch (e) {
this.warnLog(`Error validating graph: ${e.message}. Discarding graph for node: ${graphID}`);
await this.storage.discardGraph(graphID);
}
}
/**
* Processes an incoming node from the remote participant.
* @param node The incoming GASP node.
* @param spentBy The 36-byte structure of the node that spent this one, if applicable.
*/
async processIncomingNode(node, spentBy, seenNodes = new Set()) {
const nodeId = `${this.computeTXID(node.rawTx)}.${node.outputIndex}`;
this.debugLog(`Processing incoming node: ${JSON.stringify(node)}, spentBy: ${spentBy}`);
if (seenNodes.has(nodeId)) {
this.debugLog(`Node ${nodeId} already processed, skipping.`);
return; // Prevent infinite recursion
}
seenNodes.add(nodeId);
await this.storage.appendToGraph(node, spentBy);
const neededInputs = await this.storage.findNeededInputs(node);
this.debugLog(`Needed inputs for node ${nodeId}: ${JSON.stringify(neededInputs)}`);
if (neededInputs) {
await this.runConcurrently(Object.entries(neededInputs.requestedInputs), async ([outpoint, { metadata }]) => {
const { txid, outputIndex } = this.deconstruct36ByteStructure(outpoint);
this.infoLog(`Requesting new node for txid: ${txid}, outputIndex: ${outputIndex}, metadata: ${metadata}`);
const newNode = await this.remote.requestNode(node.graphID, txid, outputIndex, metadata);
this.debugLog(`Received new node: ${JSON.stringify(newNode)}`);
await this.processIncomingNode(newNode, this.compute36ByteStructure(this.computeTXID(node.rawTx), node.outputIndex), seenNodes);
});
}
}
/**
* Processes an outgoing node to the remote participant.
* @param node The outgoing GASP node.
*/
async processOutgoingNode(node, seenNodes = new Set()) {
if (this.unidirectional) {
this.debugLog(`Skipping outgoing node processing in unidirectional mode.`);
return;
}
const nodeId = `${this.computeTXID(node.rawTx)}.${node.outputIndex}`;
this.debugLog(`Processing outgoing node: ${JSON.stringify(node)}`);
if (seenNodes.has(nodeId)) {
this.debugLog(`Node ${nodeId} already processed, skipping.`);
return; // Prevent infinite recursion
}
seenNodes.add(nodeId);
// Attempt to submit the node to the remote
const response = await this.remote.submitNode(node);
this.debugLog(`Received response for submitted node: ${JSON.stringify(response)}`);
if (response) {
await this.runConcurrently(Object.entries(response.requestedInputs), async ([outpoint, { metadata }]) => {
const { txid, outputIndex } = this.deconstruct36ByteStructure(outpoint);
try {
this.infoLog(`Hydrating node for txid: ${txid}, outputIndex: ${outputIndex}, metadata: ${metadata}`);
const hydratedNode = await this.storage.hydrateGASPNode(node.graphID, txid, outputIndex, metadata);
this.debugLog(`Hydrated node: ${JSON.stringify(hydratedNode)}`);
await this.processOutgoingNode(hydratedNode, seenNodes);
}
catch (e) {
this.errorLog(`Error hydrating node: ${e.message}`);
// If we can't send the outgoing node, we just stop. The remote won't validate the anchor, and their temporary graph will be discarded.
return;
}
});
}
}
}
exports.GASP = GASP;
//# sourceMappingURL=GASP.js.map
{"version":3,"file":"GASP.js","sourceRoot":"","sources":["../../../src/GASP.ts"],"names":[],"mappings":"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"}

Sorry, the diff of this file is not supported yet

export * from './src/GASP.js';
//# sourceMappingURL=mod.js.map
{"version":3,"file":"mod.js","sourceRoot":"","sources":["../../mod.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC"}
import { Transaction } from '@bsv/sdk';
export class GASPVersionMismatchError extends Error {
code;
currentVersion;
foreignVersion;
constructor(message, currentVersion, foreignVersion) {
super(message);
this.code = 'ERR_GASP_VERSION_MISMATCH';
this.currentVersion = currentVersion;
this.foreignVersion = foreignVersion;
}
}
/**
* Log levels for controlling output verbosity.
*/
export var LogLevel;
(function (LogLevel) {
LogLevel[LogLevel["NONE"] = 0] = "NONE";
LogLevel[LogLevel["ERROR"] = 1] = "ERROR";
LogLevel[LogLevel["WARN"] = 2] = "WARN";
LogLevel[LogLevel["INFO"] = 3] = "INFO";
LogLevel[LogLevel["DEBUG"] = 4] = "DEBUG";
})(LogLevel || (LogLevel = {}));
/**
* Main class implementing the Graph Aware Sync Protocol.
*/
export class GASP {
version;
storage;
remote;
lastInteraction;
/**
* @deprecated Retained for backwards compatibility. Use `logLevel` and the new logging methods instead.
*/
log;
/**
* The log level: NONE, ERROR, WARN, INFO, DEBUG.
*/
logLevel;
logPrefix;
unidirectional;
/**
* When true, run tasks sequentially rather than using Promise.all (parallel).
*/
sequential;
/**
*
* @param storage The GASP Storage interface to use
* @param remote The GASP Remote interface to use
* @param lastInteraction The timestamp when we last interacted with this remote party
* @param logPrefix Optional prefix for log messages
* @param log Whether to log messages (backwards-compatibility only)
* @param unidirectional Whether to disable the "reply" side and do pull-only
* @param logLevel The log level for the instance
* @param sequential Whether to run tasks sequentially (avoid Promise.all) or in parallel
*/
constructor(storage, remote, lastInteraction = 0, logPrefix = '[GASP] ', log = false, unidirectional = false, logLevel = LogLevel.INFO, sequential = false) {
this.storage = storage;
this.remote = remote;
this.lastInteraction = lastInteraction;
this.version = 1;
this.logPrefix = logPrefix;
this.log = log;
this.unidirectional = unidirectional;
this.logLevel = logLevel;
this.sequential = sequential;
this.validateTimestamp(this.lastInteraction);
this.logData(`GASP initialized with version: ${this.version}, lastInteraction: ${this.lastInteraction}, unidirectional: ${this.unidirectional}, logLevel: ${LogLevel[this.logLevel]}, sequential: ${this.sequential}`);
}
/**
* Helper method to execute callbacks either in parallel or sequentially,
* depending on the `sequential` flag.
*/
async runConcurrently(items, callback) {
if (this.sequential) {
// Run sequentially
for (const item of items) {
await callback(item);
}
}
else {
// Run in parallel
await Promise.all(items.map(callback));
}
}
/**
* Legacy log method for backwards compatibility only.
* Internally, logs at INFO level if `log === true`.
*/
logData(...data) {
if (this.log) {
this.infoLog(...data);
}
}
/**
* New recommended methods for logging, respecting the logLevel.
*/
errorLog(...data) {
if (this.logLevel >= LogLevel.ERROR) {
console.error(this.logPrefix, '[ERROR]', ...data);
}
}
warnLog(...data) {
if (this.logLevel >= LogLevel.WARN) {
console.warn(this.logPrefix, '[WARN]', ...data);
}
}
infoLog(...data) {
if (this.logLevel >= LogLevel.INFO) {
console.info(this.logPrefix, '[INFO]', ...data);
}
}
debugLog(...data) {
if (this.logLevel >= LogLevel.DEBUG) {
console.debug(this.logPrefix, '[DEBUG]', ...data);
}
}
validateTimestamp(timestamp) {
if (typeof timestamp !== 'number' || isNaN(timestamp) || timestamp < 0 || !Number.isInteger(timestamp)) {
throw new Error('Invalid timestamp format');
}
}
/**
* Computes a 36-byte structure from a transaction ID and output index.
* @param txid The transaction ID.
* @param index The output index.
* @returns A string representing the 36-byte structure.
*/
compute36ByteStructure(txid, index) {
const result = `${txid}.${index.toString()}`;
this.debugLog(`Computed 36-byte structure: ${result} from txid: ${txid}, index: ${index}`);
return result;
}
/**
* Deconstructs a 36-byte structure into a transaction ID and output index.
* @param outpoint The 36-byte structure.
* @returns An object containing the transaction ID and output index.
*/
deconstruct36ByteStructure(outpoint) {
const [txid, index] = outpoint.split('.');
const result = {
txid,
outputIndex: parseInt(index, 10)
};
this.debugLog(`Deconstructed 36-byte structure: ${outpoint} into txid: ${txid}, outputIndex: ${result.outputIndex}`);
return result;
}
/**
* Computes the transaction ID for a given transaction.
* @param tx The transaction string.
* @returns The computed transaction ID.
*/
computeTXID(tx) {
const txid = Transaction.fromHex(tx).id('hex');
this.debugLog(`Computed TXID: ${txid} from transaction: ${tx}`);
return txid;
}
/**
* Synchronizes the transaction data between the local and remote participants.
*/
async sync() {
this.infoLog(`Starting sync process. Last interaction timestamp: ${this.lastInteraction}`);
const initialRequest = await this.buildInitialRequest(this.lastInteraction);
const initialResponse = await this.remote.getInitialResponse(initialRequest);
// 1. Pull the remote UTXOs that we don't already have
if (initialResponse.UTXOList.length > 0) {
const foreignUTXOs = await this.storage.findKnownUTXOs(0);
await this.runConcurrently(initialResponse.UTXOList.filter(x => !foreignUTXOs.some(y => x.txid === y.txid && x.outputIndex === y.outputIndex)), async (UTXO) => {
try {
this.infoLog(`Requesting node for UTXO: ${JSON.stringify(UTXO)}`);
const resolvedNode = await this.remote.requestNode(this.compute36ByteStructure(UTXO.txid, UTXO.outputIndex), UTXO.txid, UTXO.outputIndex, true);
this.debugLog(`Received unspent graph node from remote: ${JSON.stringify(resolvedNode)}`);
await this.processIncomingNode(resolvedNode);
await this.completeGraph(resolvedNode.graphID);
}
catch (e) {
this.warnLog(`Error with incoming UTXO ${UTXO.txid}.${UTXO.outputIndex}: ${e.message}`);
}
});
}
// 2. Only do the “reply” half if unidirectional is disabled
if (!this.unidirectional) {
const initialReply = await this.getInitialReply(initialResponse);
this.infoLog(`Received initial reply: ${JSON.stringify(initialReply)}`);
if (initialReply.UTXOList.length > 0) {
await this.runConcurrently(initialReply.UTXOList, async (UTXO) => {
try {
this.infoLog(`Hydrating GASP node for UTXO: ${JSON.stringify(UTXO)}`);
const outgoingNode = await this.storage.hydrateGASPNode(this.compute36ByteStructure(UTXO.txid, UTXO.outputIndex), UTXO.txid, UTXO.outputIndex, true);
this.debugLog(`Sending unspent graph node for remote: ${JSON.stringify(outgoingNode)}`);
await this.processOutgoingNode(outgoingNode);
}
catch (e) {
this.warnLog(`Error with outgoing UTXO ${UTXO.txid}.${UTXO.outputIndex}: ${e.message}`);
}
});
}
}
this.infoLog('Sync completed!');
}
/**
* Builds the initial request for the sync process.
* @returns A promise for the initial request object.
*/
async buildInitialRequest(since) {
const request = {
version: this.version,
since
};
this.debugLog(`Built initial request: ${JSON.stringify(request)}`);
return request;
}
/**
* Builds the initial response based on the received request.
* @param request The initial request object.
* @returns A promise for an initial response
*/
async getInitialResponse(request) {
this.infoLog(`Received initial request: ${JSON.stringify(request)}`);
if (request.version !== this.version) {
const error = new GASPVersionMismatchError(`GASP version mismatch. Current version: ${this.version}, foreign version: ${request.version}`, this.version, request.version);
this.errorLog(`GASP version mismatch error: ${error.message}`);
throw error;
}
this.validateTimestamp(request.since);
const response = {
since: this.lastInteraction,
UTXOList: await this.storage.findKnownUTXOs(request.since)
};
this.debugLog(`Built initial response: ${JSON.stringify(response)}`);
return response;
}
/**
* Builds the initial reply based on the received response.
* @param response The initial response object.
* @returns A promise for an initial reply
*/
async getInitialReply(response) {
this.infoLog(`Received initial response: ${JSON.stringify(response)}`);
const knownUTXOs = await this.storage.findKnownUTXOs(response.since);
const filteredUTXOs = knownUTXOs.filter(x => !response.UTXOList.some(y => y.txid === x.txid && y.outputIndex === x.outputIndex));
const reply = {
UTXOList: filteredUTXOs
};
this.debugLog(`Built initial reply: ${JSON.stringify(reply)}`);
return reply;
}
/**
* Provides a requested node to a foreign instance who requested it.
*/
async requestNode(graphID, txid, outputIndex, metadata) {
this.infoLog(`Remote is requesting node with graphID: ${graphID}, txid: ${txid}, outputIndex: ${outputIndex}, metadata: ${metadata}`);
const node = await this.storage.hydrateGASPNode(graphID, txid, outputIndex, metadata);
this.debugLog(`Returning node: ${JSON.stringify(node)}`);
return node;
}
/**
* Provides a set of inputs we care about after processing a new incoming node.
* Also finalizes or discards a graph if no additional data is requested from the foreign instance.
*/
async submitNode(node) {
this.infoLog(`Remote party is submitting node: ${JSON.stringify(node)}`);
await this.storage.appendToGraph(node);
const requestedInputs = await this.storage.findNeededInputs(node);
this.debugLog(`Requested inputs: ${JSON.stringify(requestedInputs)}`);
if (!requestedInputs) {
await this.completeGraph(node.graphID);
}
return requestedInputs;
}
/**
* Handles the completion of a newly-synced graph
* @param {string} graphID The ID of the newly-synced graph
*/
async completeGraph(graphID) {
this.infoLog(`Completing newly-synced graph: ${graphID}`);
try {
await this.storage.validateGraphAnchor(graphID);
this.debugLog(`Graph validated for node: ${graphID}`);
await this.storage.finalizeGraph(graphID);
this.infoLog(`Graph finalized for node: ${graphID}`);
}
catch (e) {
this.warnLog(`Error validating graph: ${e.message}. Discarding graph for node: ${graphID}`);
await this.storage.discardGraph(graphID);
}
}
/**
* Processes an incoming node from the remote participant.
* @param node The incoming GASP node.
* @param spentBy The 36-byte structure of the node that spent this one, if applicable.
*/
async processIncomingNode(node, spentBy, seenNodes = new Set()) {
const nodeId = `${this.computeTXID(node.rawTx)}.${node.outputIndex}`;
this.debugLog(`Processing incoming node: ${JSON.stringify(node)}, spentBy: ${spentBy}`);
if (seenNodes.has(nodeId)) {
this.debugLog(`Node ${nodeId} already processed, skipping.`);
return; // Prevent infinite recursion
}
seenNodes.add(nodeId);
await this.storage.appendToGraph(node, spentBy);
const neededInputs = await this.storage.findNeededInputs(node);
this.debugLog(`Needed inputs for node ${nodeId}: ${JSON.stringify(neededInputs)}`);
if (neededInputs) {
await this.runConcurrently(Object.entries(neededInputs.requestedInputs), async ([outpoint, { metadata }]) => {
const { txid, outputIndex } = this.deconstruct36ByteStructure(outpoint);
this.infoLog(`Requesting new node for txid: ${txid}, outputIndex: ${outputIndex}, metadata: ${metadata}`);
const newNode = await this.remote.requestNode(node.graphID, txid, outputIndex, metadata);
this.debugLog(`Received new node: ${JSON.stringify(newNode)}`);
await this.processIncomingNode(newNode, this.compute36ByteStructure(this.computeTXID(node.rawTx), node.outputIndex), seenNodes);
});
}
}
/**
* Processes an outgoing node to the remote participant.
* @param node The outgoing GASP node.
*/
async processOutgoingNode(node, seenNodes = new Set()) {
if (this.unidirectional) {
this.debugLog(`Skipping outgoing node processing in unidirectional mode.`);
return;
}
const nodeId = `${this.computeTXID(node.rawTx)}.${node.outputIndex}`;
this.debugLog(`Processing outgoing node: ${JSON.stringify(node)}`);
if (seenNodes.has(nodeId)) {
this.debugLog(`Node ${nodeId} already processed, skipping.`);
return; // Prevent infinite recursion
}
seenNodes.add(nodeId);
// Attempt to submit the node to the remote
const response = await this.remote.submitNode(node);
this.debugLog(`Received response for submitted node: ${JSON.stringify(response)}`);
if (response) {
await this.runConcurrently(Object.entries(response.requestedInputs), async ([outpoint, { metadata }]) => {
const { txid, outputIndex } = this.deconstruct36ByteStructure(outpoint);
try {
this.infoLog(`Hydrating node for txid: ${txid}, outputIndex: ${outputIndex}, metadata: ${metadata}`);
const hydratedNode = await this.storage.hydrateGASPNode(node.graphID, txid, outputIndex, metadata);
this.debugLog(`Hydrated node: ${JSON.stringify(hydratedNode)}`);
await this.processOutgoingNode(hydratedNode, seenNodes);
}
catch (e) {
this.errorLog(`Error hydrating node: ${e.message}`);
// If we can't send the outgoing node, we just stop. The remote won't validate the anchor, and their temporary graph will be discarded.
return;
}
});
}
}
}
//# sourceMappingURL=GASP.js.map
{"version":3,"file":"GASP.js","sourceRoot":"","sources":["../../../src/GASP.ts"],"names":[],"mappings":"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"}

Sorry, the diff of this file is not supported yet

export * from './src/GASP.js';
//# sourceMappingURL=mod.d.ts.map
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../mod.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC"}
/**
* Represents the initial request made under the Graph Aware Sync Protocol.
*/
export type GASPInitialRequest = {
/** GASP version. Currently 1. */
version: number;
/** An optional timestamp (UNIX-1970-seconds) of the last time these two parties synced */
since: number;
};
/**
* Represents the initial response made under the Graph Aware Sync Protocol.
*/
export type GASPInitialResponse = {
/** A list of outputs witnessed by the recipient since the initial request's timestamp. If not provided, a complete list of outputs since the beginning of time is returned. Unconfirmed (non-timestamped) UTXOs are always returned. */
UTXOList: Array<{
txid: string;
outputIndex: number;
}>;
/** A timestamp from when the responder wants to receive UTXOs in the other direction, back from the requester. */
since: number;
};
/** Represents the subsequent message sent in reply to the initial response. */
export type GASPInitialReply = {
/** A list of outputs (excluding outputs received from the Initial Response), and ONLY after the timestamp from the initial response. We don't need to send back things from the initial response, since those were already seen by the counterparty. */
UTXOList: Array<{
txid: string;
outputIndex: number;
}>;
};
/**
* Represents an output, its encompassing transaction, and the associated metadata, together with references to inputs and their metadata.
*/
export type GASPNode = {
/** The graph ID to which this node belongs. */
graphID: string;
/** The Bitcoin transaction in rawTX format. */
rawTx: string;
/** The index of the output in the transaction. */
outputIndex: number;
/** A BUMP proof for the transaction, if it is in a block. */
proof?: string;
/** Metadata associated with the transaction, if it was requested. */
txMetadata?: string;
/** Metadata associated with the output, if it was requested. */
outputMetadata?: string;
/** A mapping of transaction inputs to metadata hashes, if metadata was requested. */
inputs?: Record<string, {
hash: string;
}>;
};
/**
* Denotes which input transactions are requested, and whether metadata needs to be sent.
*/
export type GASPNodeResponse = {
requestedInputs: Record<string, {
metadata: boolean;
}>;
};
/**
* Facilitates the finding of UTXOs, determination of needed inputs, temporary graph management, and eventual graph finalization.
*/
export interface GASPStorage {
/**
* Returns an array of transaction outpoints that are currently known to be unspent (given an optional timestamp).
* Non-confirmed (non-timestamped) outputs should always be returned, regardless of the timestamp.
* @returns A promise for an array of objects, each containing txid and outputIndex properties.
*/
findKnownUTXOs: (since: number) => Promise<Array<{
txid: string;
outputIndex: number;
}>>;
/**
* 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 txid The transaction ID for the node to hydrate.
* @param outputIndex The output index for the node to hydrate.
* @param metadata Whether transaction and output metadata should be returned.
* @returns The hydrated GASP node, with or without metadata.
*/
hydrateGASPNode: (graphID: string, txid: string, outputIndex: number, metadata: boolean) => Promise<GASPNode>;
/**
* 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.
*/
findNeededInputs: (tx: GASPNode) => Promise<GASPNodeResponse | void>;
/**
* 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.
*/
appendToGraph: (tx: GASPNode, spentBy?: string) => Promise<void>;
/**
* 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.
* @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.
*/
validateGraphAnchor: (graphID: string) => Promise<void>;
/**
* 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.
*/
discardGraph: (graphID: string) => Promise<void>;
/**
* 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 tip of this graph.
*/
finalizeGraph: (graphID: string) => Promise<void>;
}
/**
* The communications mechanism between a local GASP instance and a foreign GASP instance.
*/
export interface GASPRemote {
/** Given an outgoing initial request, send the request to the foreign instance and obtain their initial response. */
getInitialResponse: (request: GASPInitialRequest) => Promise<GASPInitialResponse>;
/** Given an outgoing initial response, obtain the reply from the foreign instance. */
getInitialReply: (response: GASPInitialResponse) => Promise<GASPInitialReply>;
/** Given an outgoing txid, outputIndex and optional metadata, request the associated GASP node from the foreign instane. */
requestNode: (graphID: string, txid: string, outputIndex: number, metadata: boolean) => Promise<GASPNode>;
/** Given an outgoing node, send the node to the foreign instance and determine which additional inputs (if any) they request in response. */
submitNode: (node: GASPNode) => Promise<GASPNodeResponse | void>;
}
export declare class GASPVersionMismatchError extends Error {
code: 'ERR_GASP_VERSION_MISMATCH';
currentVersion: number;
foreignVersion: number;
constructor(message: string, currentVersion: number, foreignVersion: number);
}
/**
* Log levels for controlling output verbosity.
*/
export declare enum LogLevel {
NONE = 0,
ERROR = 1,
WARN = 2,
INFO = 3,
DEBUG = 4
}
/**
* Main class implementing the Graph Aware Sync Protocol.
*/
export declare class GASP implements GASPRemote {
version: number;
storage: GASPStorage;
remote: GASPRemote;
lastInteraction: number;
/**
* @deprecated Retained for backwards compatibility. Use `logLevel` and the new logging methods instead.
*/
log: boolean;
/**
* The log level: NONE, ERROR, WARN, INFO, DEBUG.
*/
logLevel: LogLevel;
logPrefix: string;
unidirectional: boolean;
/**
* When true, run tasks sequentially rather than using Promise.all (parallel).
*/
sequential: boolean;
/**
*
* @param storage The GASP Storage interface to use
* @param remote The GASP Remote interface to use
* @param lastInteraction The timestamp when we last interacted with this remote party
* @param logPrefix Optional prefix for log messages
* @param log Whether to log messages (backwards-compatibility only)
* @param unidirectional Whether to disable the "reply" side and do pull-only
* @param logLevel The log level for the instance
* @param sequential Whether to run tasks sequentially (avoid Promise.all) or in parallel
*/
constructor(storage: GASPStorage, remote: GASPRemote, lastInteraction?: number, logPrefix?: string, log?: boolean, unidirectional?: boolean, logLevel?: LogLevel, sequential?: boolean);
/**
* Helper method to execute callbacks either in parallel or sequentially,
* depending on the `sequential` flag.
*/
private runConcurrently;
/**
* Legacy log method for backwards compatibility only.
* Internally, logs at INFO level if `log === true`.
*/
private logData;
/**
* New recommended methods for logging, respecting the logLevel.
*/
private errorLog;
private warnLog;
private infoLog;
private debugLog;
private validateTimestamp;
/**
* Computes a 36-byte structure from a transaction ID and output index.
* @param txid The transaction ID.
* @param index The output index.
* @returns A string representing the 36-byte structure.
*/
private compute36ByteStructure;
/**
* Deconstructs a 36-byte structure into a transaction ID and output index.
* @param outpoint The 36-byte structure.
* @returns An object containing the transaction ID and output index.
*/
private deconstruct36ByteStructure;
/**
* Computes the transaction ID for a given transaction.
* @param tx The transaction string.
* @returns The computed transaction ID.
*/
private computeTXID;
/**
* Synchronizes the transaction data between the local and remote participants.
*/
sync(): Promise<void>;
/**
* Builds the initial request for the sync process.
* @returns A promise for the initial request object.
*/
buildInitialRequest(since: number): Promise<GASPInitialRequest>;
/**
* Builds the initial response based on the received request.
* @param request The initial request object.
* @returns A promise for an initial response
*/
getInitialResponse(request: GASPInitialRequest): Promise<GASPInitialResponse>;
/**
* Builds the initial reply based on the received response.
* @param response The initial response object.
* @returns A promise for an initial reply
*/
getInitialReply(response: GASPInitialResponse): Promise<GASPInitialReply>;
/**
* Provides a requested node to a foreign instance who requested it.
*/
requestNode(graphID: string, txid: string, outputIndex: number, metadata: boolean): Promise<GASPNode>;
/**
* Provides a set of inputs we care about after processing a new incoming node.
* Also finalizes or discards a graph if no additional data is requested from the foreign instance.
*/
submitNode(node: GASPNode): Promise<GASPNodeResponse | void>;
/**
* Handles the completion of a newly-synced graph
* @param {string} graphID The ID of the newly-synced graph
*/
completeGraph(graphID: string): Promise<void>;
/**
* Processes an incoming node from the remote participant.
* @param node The incoming GASP node.
* @param spentBy The 36-byte structure of the node that spent this one, if applicable.
*/
private processIncomingNode;
/**
* Processes an outgoing node to the remote participant.
* @param node The outgoing GASP node.
*/
private processOutgoingNode;
}
//# sourceMappingURL=GASP.d.ts.map
{"version":3,"file":"GASP.d.ts","sourceRoot":"","sources":["../../../src/GASP.ts"],"names":[],"mappings":"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"}

Sorry, the diff of this file is not supported yet

Open BSV License version 4
Copyright (c) 2023 BSV Blockchain Association ("Bitcoin Association")
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
1 - The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2 - The Software, and any software that is derived from the Software or parts thereof,
can only be used on the Bitcoin SV blockchains. The Bitcoin SV blockchains are defined,
for purposes of this license, as the Bitcoin blockchain containing block height #556767
with the hash "000000000000000001d956714215d96ffc00e0afda4cd0a96c96f8d802b1662b" and
that contains the longest persistent chain of blocks accepted by this Software and which are valid under the rules set forth in the Bitcoin white paper (S. Nakamoto, Bitcoin: A Peer-to-Peer Electronic Cash System, posted online October 2008) and the latest version of this Software available in this repository or another repository designated by Bitcoin Association,
as well as the test blockchains that contain the longest persistent chains of blocks accepted by this Software and which are valid under the rules set forth in the Bitcoin whitepaper (S. Nakamoto, Bitcoin: A Peer-to-Peer Electronic Cash System, posted online October 2008) and the latest version of this Software available in this repository, or another repository designated by Bitcoin Association
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
export * from './src/GASP.js';
/* eslint-env jest */
import { GASP, GASPInitialRequest, GASPNode, GASPNodeResponse, GASPStorage, GASPRemote, GASPInitialReply, GASPInitialResponse } from '../GASP'
type Graph = {
graphID: string,
time: number,
txid: string,
outputIndex: number,
rawTx: string,
inputs: Record<string, Graph>
}
// Used to construct a non-functional remote that will be replaced after being constructed.
// Useful when directly using another GASP instance as a remote.
const throwawayRemote: GASPRemote = {
getInitialResponse: function (request: GASPInitialRequest): Promise<GASPInitialResponse> {
throw new Error('Function not implemented.')
},
getInitialReply: function (response: GASPInitialResponse): Promise<GASPInitialReply> {
throw new Error('Function not implemented.')
},
requestNode: function (graphID: string, txid: string, outputIndex: number, metadata: boolean): Promise<GASPNode> {
throw new Error('Function not implemented.')
},
submitNode: function (node: GASPNode): Promise<void | GASPNodeResponse> {
throw new Error('Function not implemented.')
}
}
class MockStorage implements GASPStorage {
knownStore: Array<Graph>
tempGraphStore: Record<string, Graph>
updateCallback: Function
logPrefix: string
log: boolean
constructor(knownStore: Array<Graph> = [], tempGraphStore: Record<string, Graph> = {}, updateCallback: Function = () => { }, logPrefix = '[Storage] ', log = false) {
this.knownStore = knownStore
this.tempGraphStore = tempGraphStore
this.updateCallback = updateCallback
this.logPrefix = logPrefix
this.log = log
// Initialize methods with default implementations
this.findKnownUTXOs = jest.fn(this.findKnownUTXOs.bind(this))
this.hydrateGASPNode = jest.fn(this.hydrateGASPNode.bind(this))
this.findNeededInputs = jest.fn(this.findNeededInputs.bind(this))
this.appendToGraph = jest.fn(this.appendToGraph.bind(this))
this.validateGraphAnchor = jest.fn(this.validateGraphAnchor.bind(this))
this.discardGraph = jest.fn(this.discardGraph.bind(this))
this.finalizeGraph = jest.fn(this.finalizeGraph.bind(this))
}
private logData(...data: any): void {
if (this.log) {
console.log(this.logPrefix, ...data)
}
}
async findKnownUTXOs(since: number): Promise<{ txid: string; outputIndex: number }[]> {
const utxos = this.knownStore
.filter(x => !x.time || x.time > since) // Include UTXOs with no timestamp or timestamps greater than 'since'
.map(x => ({ txid: x.txid, outputIndex: x.outputIndex }))
this.logData('findKnownUTXOs', since, utxos)
return utxos
}
async hydrateGASPNode(graphID: string, txid: string, outputIndex: number, metadata: boolean): Promise<GASPNode> {
const found = this.knownStore.find(x => x.txid === txid && x.outputIndex === outputIndex)
if (!found) {
throw new Error('Not found')
}
this.logData('hydrateGASPNode', graphID, txid, outputIndex, metadata, found)
return {
graphID,
rawTx: found.rawTx,
outputIndex: found.outputIndex,
proof: 'mock_proof', // Mock proof
txMetadata: metadata ? 'mock_tx_metadata' : undefined,
outputMetadata: metadata ? 'mock_output_metadata' : undefined,
inputs: metadata ? { 'mock_input': { hash: 'mock_hash' } } : undefined
}
}
async findNeededInputs(tx: GASPNode): Promise<void | GASPNodeResponse> {
this.logData('findNeededInputs', tx)
// For testing, assume no additional inputs are needed, unless specified
if (tx.graphID.includes('recursive')) {
return {
requestedInputs: {
'recursive_txid.1': { metadata: true }
}
}
}
return
}
async appendToGraph(tx: GASPNode, spentBy?: string | undefined): Promise<void> {
this.logData('appendToGraph', tx, spentBy)
this.tempGraphStore[tx.graphID] = {
...tx,
time: Date.now(),
txid: tx.graphID.split('.')[0],
inputs: {}
}
}
async validateGraphAnchor(graphID: string): Promise<void> {
this.logData('validateGraphAnchor', graphID)
// Allow validation to pass
}
async discardGraph(graphID: string): Promise<void> {
this.logData('discardGraph', graphID)
delete this.tempGraphStore[graphID]
}
async finalizeGraph(graphID: string): Promise<void> {
const tempGraph = this.tempGraphStore[graphID]
if (tempGraph) {
this.logData('finalizeGraph', graphID, tempGraph)
this.knownStore.push(tempGraph)
this.updateCallback()
delete this.tempGraphStore[graphID]
} else {
this.logData('no graph to finalize', graphID, tempGraph)
}
}
}
const mockUTXO = {
graphID: 'mock_sender1_txid1.0',
rawTx: 'mock_sender1_rawtx1',
outputIndex: 0,
time: 111,
txid: 'mock_sender1_txid1',
inputs: {}
}
const mockInputNode = {
graphID: 'mock_sender1_txid1.0',
rawTx: 'deadbeef01010101',
outputIndex: 0,
time: 222,
txid: 'mock_sender1_txid2',
inputs: {}
}
const mockUTXOWithInput = {
...mockUTXO,
inputs: {
'mock_sender1_txid2.0': mockInputNode
}
}
describe('GASP', () => {
afterEach(() => {
jest.resetAllMocks()
})
it('Fails to sync if versions are wrong', async () => {
const originalError = console.error
console.error = jest.fn()
const storage1 = new MockStorage()
const storage2 = new MockStorage()
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 0, '[GASP #2] ')
gasp1.remote = gasp2
gasp1.version = 2
await expect(gasp1.sync()).rejects.toThrow(new Error('GASP version mismatch. Current version: 1, foreign version: 2'))
expect(console.error).toHaveBeenCalledWith('[GASP #2] ', '[ERROR]', 'GASP version mismatch error: GASP version mismatch. Current version: 1, foreign version: 2')
console.error = originalError
})
it('Synchronizes a single UTXO from Alice to Bob', async () => {
const storage1 = new MockStorage([mockUTXO])
const storage2 = new MockStorage()
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 0, '[GASP #2] ')
gasp1.remote = gasp2
await gasp1.sync()
expect((await storage2.findKnownUTXOs(0)).length).toBe(1)
expect(await storage2.findKnownUTXOs(0)).toEqual(await storage1.findKnownUTXOs(0))
})
it('Synchronizes a single UTXO from Bob to Alice', async () => {
const storage1 = new MockStorage()
const storage2 = new MockStorage([mockUTXO])
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 0, '[GASP #2] ')
gasp1.remote = gasp2
await gasp1.sync()
expect((await storage1.findKnownUTXOs(0)).length).toBe(1)
expect(await storage1.findKnownUTXOs(0)).toEqual(await storage2.findKnownUTXOs(0))
})
it('Discards graphs that do not validate from Alice to Bob', async () => {
const storage1 = new MockStorage([mockUTXO])
const storage2 = new MockStorage()
storage2.validateGraphAnchor = jest.fn().mockImplementation((graphID: string) => {
throw new Error('Invalid graph anchor.')
})
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 0, '[GASP #2] ')
gasp1.remote = gasp2
await gasp1.sync()
expect((await storage2.findKnownUTXOs(0)).length).toBe(0)
expect(storage2.discardGraph).toHaveBeenCalledWith('mock_sender1_txid1.0')
})
it('Discards graphs that do not validate from Bob to Alice', async () => {
const storage1 = new MockStorage()
const storage2 = new MockStorage([mockUTXO])
storage1.validateGraphAnchor = jest.fn().mockImplementation((graphID: string) => {
throw new Error('Invalid graph anchor.')
})
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 0, '[GASP #2] ')
gasp1.remote = gasp2
await gasp1.sync()
expect((await storage1.findKnownUTXOs(0)).length).toBe(0)
expect(storage1.discardGraph).toHaveBeenCalledWith('mock_sender1_txid1.0')
})
it('Synchronizes a deep UTXO from Bob to Alice', async () => {
const storage1 = new MockStorage()
storage1.findNeededInputs = jest.fn().mockImplementationOnce(async (n: GASPNode): Promise<GASPNodeResponse> => {
return {
requestedInputs: {
'mock_sender1_txid2.0': {
metadata: true
}
}
}
})
const storage2 = new MockStorage([mockUTXOWithInput])
storage2.hydrateGASPNode = jest.fn().mockReturnValueOnce(mockUTXO).mockReturnValueOnce(mockInputNode)
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 0, '[GASP #2] ')
gasp1.remote = gasp2
await gasp1.sync()
expect((await storage1.findKnownUTXOs(0)).length).toBe(1)
expect(await storage1.findKnownUTXOs(0)).toEqual(await storage2.findKnownUTXOs(0))
})
it('Synchronizes a deep UTXO from Alice to Bob', async () => {
const storage2 = new MockStorage()
storage2.findNeededInputs = jest.fn().mockImplementationOnce(async (n: GASPNode): Promise<GASPNodeResponse> => {
return {
requestedInputs: {
'mock_sender1_txid2.0': {
metadata: true
}
}
}
})
const storage1 = new MockStorage([mockUTXOWithInput])
storage1.hydrateGASPNode = jest.fn().mockReturnValueOnce(mockUTXO).mockReturnValueOnce(mockInputNode)
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 0, '[GASP #2] ')
gasp1.remote = gasp2
await gasp1.sync()
expect((await storage2.findKnownUTXOs(0)).length).toBe(1)
expect(await storage2.findKnownUTXOs(0)).toEqual(await storage1.findKnownUTXOs(0))
})
it('Synchronizes multiple graphs from Alice to Bob', async () => {
const mockUTXO2 = {
graphID: 'mock_sender2_txid1.0',
rawTx: 'mock_sender2_rawtx1',
outputIndex: 0,
time: 222,
txid: 'mock_sender2_txid1',
inputs: {}
}
const storage1 = new MockStorage([mockUTXO, mockUTXO2])
const storage2 = new MockStorage()
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 0, '[GASP #2] ')
gasp1.remote = gasp2
await gasp1.sync()
expect((await storage2.findKnownUTXOs(0)).length).toBe(2)
expect(await storage2.findKnownUTXOs(0)).toEqual(await storage1.findKnownUTXOs(0))
})
it('Synchronizes a graph with recursive inputs from Bob to Alice', async () => {
const recursiveInputNode = {
graphID: 'recursive_txid.1',
rawTx: 'recursive_rawtx',
outputIndex: 1,
time: 333,
txid: 'recursive_txid',
inputs: {}
}
const complexUTXOWithInput = {
...mockUTXOWithInput,
inputs: {
...mockUTXOWithInput.inputs,
'recursive_txid.1': recursiveInputNode
}
}
const storage1 = new MockStorage()
storage1.findNeededInputs = jest.fn().mockImplementationOnce(async (n: GASPNode): Promise<GASPNodeResponse> => {
return {
requestedInputs: {
'mock_sender1_txid2.0': {
metadata: true
}
}
}
}).mockImplementationOnce(async (n: GASPNode): Promise<GASPNodeResponse> => {
return {
requestedInputs: {
'recursive_txid.1': {
metadata: true
}
}
}
})
const storage2 = new MockStorage([complexUTXOWithInput])
storage2.hydrateGASPNode = jest.fn()
.mockReturnValueOnce(mockUTXO)
.mockReturnValueOnce(mockInputNode)
.mockReturnValueOnce(recursiveInputNode)
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 0, '[GASP #2] ')
gasp1.remote = gasp2
await gasp1.sync()
expect((await storage1.findKnownUTXOs(0)).length).toBe(1)
expect(await storage1.findKnownUTXOs(0)).toEqual(await storage2.findKnownUTXOs(0))
})
it('Synchronizes only UTXOs created after the specified since timestamp', async () => {
const oldUTXO = {
graphID: 'old_txid.0',
rawTx: 'old_rawtx',
outputIndex: 0,
time: 100,
txid: 'old_txid',
inputs: {}
}
const newUTXO = {
graphID: 'new_txid.1',
rawTx: 'new_rawtx',
outputIndex: 1,
time: 200,
txid: 'new_txid',
inputs: {}
}
const storage1 = new MockStorage([oldUTXO, newUTXO])
const storage2 = new MockStorage()
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 150, '[GASP #2] ') // Setting the `since` timestamp to 150
gasp1.remote = gasp2
await gasp1.sync()
// Ensure only the new UTXO is synchronized
const syncedUTXOs = await storage2.findKnownUTXOs(0)
expect(syncedUTXOs.length).toBe(1)
expect(syncedUTXOs).toEqual([{ txid: 'new_txid', outputIndex: 1 }])
})
it('Will not sync unnecessary graphs', async () => {
const storage1 = new MockStorage([mockUTXO])
const storage2 = new MockStorage([mockUTXO])
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 0, '[GASP #2] ')
gasp1.remote = gasp2
await gasp1.sync()
expect((await storage1.findKnownUTXOs(0)).length).toBe(1)
expect((await storage2.findKnownUTXOs(0)).length).toBe(1)
expect(storage1.finalizeGraph).not.toHaveBeenCalled()
expect(storage2.finalizeGraph).not.toHaveBeenCalled()
expect(await storage2.findKnownUTXOs(0)).toEqual(await storage1.findKnownUTXOs(0))
})
it('Handles invalid timestamp format gracefully', async () => {
const storage1 = new MockStorage()
expect(() => new GASP(storage1, throwawayRemote, -1)).toThrow('Invalid timestamp format')
})
it('Handles missing UTXO during node hydration', async () => {
const storage1 = new MockStorage()
const storage2 = new MockStorage([mockUTXO])
storage2.hydrateGASPNode = jest.fn().mockRejectedValueOnce(new Error('Not found'))
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 0, '[GASP #2] ')
gasp1.remote = gasp2
await gasp1.sync()
expect(await storage2.findKnownUTXOs(0)).not.toEqual(await storage1.findKnownUTXOs(0))
})
it('Handles multiple UTXOs with mixed success and failure', async () => {
const invalidUTXO = {
graphID: 'invalid_txid.0',
rawTx: 'invalid_rawtx',
outputIndex: 0,
time: 150,
txid: 'invalid_txid',
inputs: {}
}
const storage1 = new MockStorage([mockUTXO, invalidUTXO])
const storage2 = new MockStorage()
storage1.hydrateGASPNode = jest.fn().mockImplementation(async (graphID: string, txid: string, outputIndex: number, metadata: boolean) => {
if (txid === 'invalid_txid') {
throw new Error('Invalid transaction')
}
return {
graphID,
rawTx: mockUTXO.rawTx,
outputIndex,
proof: 'mock_proof',
txMetadata: metadata ? 'mock_tx_metadata' : undefined,
outputMetadata: metadata ? 'mock_output_metadata' : undefined,
inputs: metadata ? { 'mock_input': { hash: 'mock_hash' } } : undefined
}
})
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 0, '[GASP #2] ')
gasp1.remote = gasp2
await gasp1.sync()
const syncedUTXOs = await storage2.findKnownUTXOs(0)
expect(syncedUTXOs.length).toBe(1)
expect(syncedUTXOs).toEqual([{ txid: 'mock_sender1_txid1', outputIndex: 0 }])
})
describe('Not that this should ever happen in Bitcoin, but...', () => {
it('Prevents infinite recursion with cyclically referencing nodes', async () => {
const cyclicNode1 = {
graphID: 'cyclic_txid1.0',
rawTx: 'cyclic_rawtx1',
outputIndex: 0,
time: 300,
txid: 'cyclic_txid1',
inputs: {
'cyclic_txid2.0': {
graphID: 'cyclic_txid2.0',
rawTx: 'deadbeef2024',
outputIndex: 0,
time: 300,
txid: 'cyclic_txid2',
inputs: {
'cyclic_txid1.0': {
graphID: 'cyclic_txid1.0',
rawTx: 'cyclic_rawtx1',
outputIndex: 0,
time: 300,
txid: 'cyclic_txid1',
inputs: {}
}
}
}
}
}
const storage1 = new MockStorage([cyclicNode1])
const storage2 = new MockStorage()
storage2.findNeededInputs = jest.fn().mockImplementationOnce(async (n: GASPNode): Promise<GASPNodeResponse> => {
return {
requestedInputs: {
'cyclic_txid2.0': {
metadata: true
}
}
}
}).mockImplementationOnce(async (n: GASPNode): Promise<GASPNodeResponse> => {
return {
requestedInputs: {
'cyclic_txid1.0': {
metadata: true
}
}
}
}).mockImplementationOnce(async (n: GASPNode): Promise<GASPNodeResponse> => {
return {
requestedInputs: {
'cyclic_txid2.0': {
metadata: true
}
}
}
})
storage1.hydrateGASPNode = jest.fn()
.mockReturnValueOnce(cyclicNode1)
.mockReturnValueOnce(cyclicNode1.inputs['cyclic_txid2.0'])
.mockReturnValueOnce(cyclicNode1)
.mockReturnValueOnce(cyclicNode1.inputs['cyclic_txid2.0'])
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 0, '[GASP #2] ')
gasp1.remote = gasp2
await gasp1.sync()
// No UTXOs were synced between the parties
expect((await storage2.findKnownUTXOs(0)).length).toBe(0)
// The sync process did not complete
expect(await storage2.findKnownUTXOs(0)).not.toEqual(await storage1.findKnownUTXOs(0))
// Two nodes were appended to the temporary graph
expect(storage2.appendToGraph).toHaveBeenCalledTimes(2)
// Two nodes are in temporary storage, the ones that were sent
expect(Object.keys(storage2.tempGraphStore).length).toEqual(2)
})
it('Prevents infinite recursion with cyclically referencing nodes the other direction', async () => {
const cyclicNode1 = {
graphID: 'cyclic_txid1.0',
rawTx: 'cyclic_rawtx1',
outputIndex: 0,
time: 300,
txid: 'cyclic_txid1',
inputs: {
'cyclic_txid2.0': {
graphID: 'cyclic_txid2.0',
rawTx: 'deadbeef2024',
outputIndex: 0,
time: 300,
txid: 'cyclic_txid2',
inputs: {
'cyclic_txid1.0': {
graphID: 'cyclic_txid1.0',
rawTx: 'cyclic_rawtx1',
outputIndex: 0,
time: 300,
txid: 'cyclic_txid1',
inputs: {}
}
}
}
}
}
const storage1 = new MockStorage()
const storage2 = new MockStorage([cyclicNode1])
storage1.findNeededInputs = jest.fn().mockImplementationOnce(async (n: GASPNode): Promise<GASPNodeResponse> => {
return {
requestedInputs: {
'cyclic_txid2.0': {
metadata: true
}
}
}
}).mockImplementationOnce(async (n: GASPNode): Promise<GASPNodeResponse> => {
return {
requestedInputs: {
'cyclic_txid1.0': {
metadata: true
}
}
}
}).mockImplementationOnce(async (n: GASPNode): Promise<GASPNodeResponse> => {
return {
requestedInputs: {
'cyclic_txid2.0': {
metadata: true
}
}
}
})
storage2.hydrateGASPNode = jest.fn()
.mockReturnValueOnce(cyclicNode1)
.mockReturnValueOnce(cyclicNode1.inputs['cyclic_txid2.0'])
.mockReturnValueOnce(cyclicNode1)
.mockReturnValueOnce(cyclicNode1.inputs['cyclic_txid2.0'])
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 0, '[GASP #2] ')
gasp1.remote = gasp2
await gasp1.sync()
// This direction, the UTXO does sync because the recipient is able to proceed to graph finalization after refusing to process duplicative data.
expect((await storage1.findKnownUTXOs(0)).length).toBe(1)
expect(await storage1.findKnownUTXOs(0)).toEqual(await storage1.findKnownUTXOs(0))
expect(storage1.appendToGraph).toHaveBeenCalledTimes(2)
})
it('Prevents infinite recursion with complex cyclic dependencies', async () => {
const cyclicNodeA = {
graphID: 'cyclicA_txid.0',
rawTx: 'cyclicA_rawtx',
outputIndex: 0,
time: 300,
txid: 'cyclicA_txid',
inputs: {
'cyclicB_txid.0': {
graphID: 'cyclicB_txid.0',
rawTx: 'cyclicB_rawtx',
outputIndex: 0,
time: 300,
txid: 'cyclicB_txid',
inputs: {
'cyclicA_txid.0': {
graphID: 'cyclicA_txid.0',
rawTx: 'cyclicA_rawtx',
outputIndex: 0,
time: 300,
txid: 'cyclicA_txid',
inputs: {}
}
}
}
}
}
const cyclicNodeB = {
graphID: 'cyclicB_txid.0',
rawTx: 'cyclicB_rawtx',
outputIndex: 0,
time: 300,
txid: 'cyclicB_txid',
inputs: {
'cyclicC_txid.0': {
graphID: 'cyclicC_txid.0',
rawTx: 'cyclicC_rawtx',
outputIndex: 0,
time: 300,
txid: 'cyclicC_txid',
inputs: {
'cyclicA_txid.0': {
graphID: 'cyclicA_txid.0',
rawTx: 'cyclicA_rawtx',
outputIndex: 0,
time: 300,
txid: 'cyclicA_txid',
inputs: {}
}
}
}
}
};
const cyclicNodeC = {
graphID: 'cyclicC_txid.0',
rawTx: 'cyclicC_rawtx',
outputIndex: 0,
time: 300,
txid: 'cyclicC_txid',
inputs: {
'cyclicA_txid.0': {
graphID: 'cyclicA_txid.0',
rawTx: 'cyclicA_rawtx',
outputIndex: 0,
time: 300,
txid: 'cyclicA_txid',
inputs: {}
}
}
};
const storage1 = new MockStorage([cyclicNodeA]);
const storage2 = new MockStorage();
storage2.findNeededInputs = jest.fn()
.mockImplementationOnce(async (n: GASPNode): Promise<GASPNodeResponse> => {
return {
requestedInputs: {
'cyclicB_txid.0': {
metadata: true
}
}
};
})
.mockImplementationOnce(async (n: GASPNode): Promise<GASPNodeResponse> => {
return {
requestedInputs: {
'cyclicC_txid.0': {
metadata: true
}
}
};
})
.mockImplementationOnce(async (n: GASPNode): Promise<GASPNodeResponse> => {
return {
requestedInputs: {
'cyclicA_txid.0': {
metadata: true
}
}
};
});
storage1.hydrateGASPNode = jest.fn()
.mockReturnValueOnce(cyclicNodeA)
.mockReturnValueOnce(cyclicNodeB)
.mockReturnValueOnce(cyclicNodeC)
.mockReturnValueOnce(cyclicNodeA);
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 0, '[GASP #2] ')
gasp1.remote = gasp2;
await gasp1.sync();
expect((await storage2.findKnownUTXOs(0)).length).toBe(0);
expect(await storage2.findKnownUTXOs(0)).not.toEqual(await storage1.findKnownUTXOs(0));
expect(storage2.appendToGraph).toHaveBeenCalledTimes(3);
expect(Object.keys(storage2.tempGraphStore).length).toEqual(3);
})
it('Prevents infinite recursion with complex cyclic dependencies in the other direction', async () => {
const cyclicNodeA = {
graphID: 'cyclicA_txid.0',
rawTx: 'cyclicA_rawtx',
outputIndex: 0,
time: 300,
txid: 'cyclicA_txid',
inputs: {
'cyclicB_txid.0': {
graphID: 'cyclicB_txid.0',
rawTx: 'cyclicB_rawtx',
outputIndex: 0,
time: 300,
txid: 'cyclicB_txid',
inputs: {
'cyclicA_txid.0': {
graphID: 'cyclicA_txid.0',
rawTx: 'cyclicA_rawtx',
outputIndex: 0,
time: 300,
txid: 'cyclicA_txid',
inputs: {}
}
}
}
}
};
const cyclicNodeB = {
graphID: 'cyclicB_txid.0',
rawTx: 'cyclicB_rawtx',
outputIndex: 0,
time: 300,
txid: 'cyclicB_txid',
inputs: {
'cyclicC_txid.0': {
graphID: 'cyclicC_txid.0',
rawTx: 'cyclicC_rawtx',
outputIndex: 0,
time: 300,
txid: 'cyclicC_txid',
inputs: {
'cyclicA_txid.0': {
graphID: 'cyclicA_txid.0',
rawTx: 'cyclicA_rawtx',
outputIndex: 0,
time: 300,
txid: 'cyclicA_txid',
inputs: {}
}
}
}
}
};
const cyclicNodeC = {
graphID: 'cyclicC_txid.0',
rawTx: 'cyclicC_rawtx',
outputIndex: 0,
time: 300,
txid: 'cyclicC_txid',
inputs: {
'cyclicA_txid.0': {
graphID: 'cyclicA_txid.0',
rawTx: 'cyclicA_rawtx',
outputIndex: 0,
time: 300,
txid: 'cyclicA_txid',
inputs: {}
}
}
};
const storage1 = new MockStorage();
const storage2 = new MockStorage([cyclicNodeA]);
storage1.findNeededInputs = jest.fn()
.mockImplementationOnce(async (n: GASPNode): Promise<GASPNodeResponse> => {
return {
requestedInputs: {
'cyclicB_txid.0': {
metadata: true
}
}
};
})
.mockImplementationOnce(async (n: GASPNode): Promise<GASPNodeResponse> => {
return {
requestedInputs: {
'cyclicC_txid.0': {
metadata: true
}
}
};
})
.mockImplementationOnce(async (n: GASPNode): Promise<GASPNodeResponse> => {
return {
requestedInputs: {
'cyclicA_txid.0': {
metadata: true
}
}
};
});
storage2.hydrateGASPNode = jest.fn()
.mockReturnValueOnce(cyclicNodeA)
.mockReturnValueOnce(cyclicNodeB)
.mockReturnValueOnce(cyclicNodeC)
.mockReturnValueOnce(cyclicNodeA);
const gasp1 = new GASP(storage1, throwawayRemote, 0, '[GASP #1] ')
const gasp2 = new GASP(storage2, gasp1, 0, '[GASP #2] ')
gasp1.remote = gasp2;
await gasp1.sync();
expect((await storage1.findKnownUTXOs(0)).length).toBe(1);
expect(await storage1.findKnownUTXOs(0)).toEqual(await storage1.findKnownUTXOs(0));
expect(storage1.appendToGraph).toHaveBeenCalledTimes(3);
})
})
describe('Unidirectional Sync Tests', () => {
it('Pull-only from Bob to Alice (Alice is unidirectional client)', async () => {
// Alice has a UTXO that Bob does not have
const aliceUTXO = {
graphID: 'alice_txid.0',
rawTx: 'alice_rawtx',
outputIndex: 0,
time: 999,
txid: 'alice_txid',
inputs: {}
}
// Bob has a UTXO that Alice does not have
const bobUTXO = {
graphID: 'bob_txid.1',
rawTx: 'bob_rawtx',
outputIndex: 1,
time: 1000,
txid: 'bob_txid',
inputs: {}
}
// Alice's storage
const storageAlice = new MockStorage([aliceUTXO])
// Bob's storage
const storageBob = new MockStorage([bobUTXO])
// Alice is the one calling sync() with unidirectional = true,
// meaning "pull-only from Bob's perspective"
const gaspAlice = new GASP(storageAlice, throwawayRemote, 0, '[GASP-Alice] ', false, true)
// Bob is normal, but he doesn't call `sync`. He is the remote from Alice's perspective
const gaspBob = new GASP(storageBob, gaspAlice, 0, '[GASP-Bob] ')
// Alice uses Bob as the remote
gaspAlice.remote = gaspBob
// Let Alice do a unidirectional sync from Bob
await gaspAlice.sync()
// Expect that Bob's UTXO has arrived in Alice's store
expect((await storageAlice.findKnownUTXOs(0))).toEqual([
{ txid: 'alice_txid', outputIndex: 0 },
{ txid: 'bob_txid', outputIndex: 1 }
])
// But, Bob does NOT get Alice's UTXO, because unidirectional means no "reply" from Alice
expect((await storageBob.findKnownUTXOs(0))).toEqual([
{ txid: 'bob_txid', outputIndex: 1 }
])
})
it('Pull-only from Alice to Bob (Bob is unidirectional client)', async () => {
// Alice has a UTXO that Bob does not have
const aliceUTXO = {
graphID: 'alice_txid.0',
rawTx: 'alice_rawtx',
outputIndex: 0,
time: 999,
txid: 'alice_txid',
inputs: {}
}
// Bob has a UTXO that Alice does not have
const bobUTXO = {
graphID: 'bob_txid.1',
rawTx: 'bob_rawtx',
outputIndex: 1,
time: 1000,
txid: 'bob_txid',
inputs: {}
}
// Storage for each
const storageAlice = new MockStorage([aliceUTXO])
const storageBob = new MockStorage([bobUTXO])
// Bob is the one calling sync() with unidirectional = true
// Means Bob only pulls from Alice, but doesn't push his own data
const gaspBob = new GASP(storageBob, throwawayRemote, 0, '[GASP-Bob] ', false, true)
const gaspAlice = new GASP(storageAlice, gaspBob, 0, '[GASP-Alice] ')
// Bob uses Alice as his remote
gaspBob.remote = gaspAlice
// Bob does a unidirectional sync from Alice
await gaspBob.sync()
// Expect that Alice's UTXO has arrived in Bob's store
expect((await storageBob.findKnownUTXOs(0))).toEqual([
{ txid: 'bob_txid', outputIndex: 1 },
{ txid: 'alice_txid', outputIndex: 0 }
])
// But, Alice does NOT get Bob's UTXO, because Bob never pushes it in unidirectional mode
expect((await storageAlice.findKnownUTXOs(0))).toEqual([
{ txid: 'alice_txid', outputIndex: 0 }
])
})
})
})
import { Transaction } from '@bsv/sdk'
/**
* Represents the initial request made under the Graph Aware Sync Protocol.
*/
export type GASPInitialRequest = {
/** GASP version. Currently 1. */
version: number
/** An optional timestamp (UNIX-1970-seconds) of the last time these two parties synced */
since: number
}
/**
* Represents the initial response made under the Graph Aware Sync Protocol.
*/
export type GASPInitialResponse = {
/** A list of outputs witnessed by the recipient since the initial request's timestamp. If not provided, a complete list of outputs since the beginning of time is returned. Unconfirmed (non-timestamped) UTXOs are always returned. */
UTXOList: Array<{ txid: string, outputIndex: number }>,
/** A timestamp from when the responder wants to receive UTXOs in the other direction, back from the requester. */
since: number
}
/** Represents the subsequent message sent in reply to the initial response. */
export type GASPInitialReply = {
/** A list of outputs (excluding outputs received from the Initial Response), and ONLY after the timestamp from the initial response. We don't need to send back things from the initial response, since those were already seen by the counterparty. */
UTXOList: Array<{ txid: string, outputIndex: number }>,
}
/**
* Represents an output, its encompassing transaction, and the associated metadata, together with references to inputs and their metadata.
*/
export type GASPNode = {
/** The graph ID to which this node belongs. */
graphID: string
/** The Bitcoin transaction in rawTX format. */
rawTx: string
/** The index of the output in the transaction. */
outputIndex: number
/** A BUMP proof for the transaction, if it is in a block. */
proof?: string
/** Metadata associated with the transaction, if it was requested. */
txMetadata?: string
/** Metadata associated with the output, if it was requested. */
outputMetadata?: string
/** A mapping of transaction inputs to metadata hashes, if metadata was requested. */
inputs?: Record<string, { hash: string }>
}
/**
* Denotes which input transactions are requested, and whether metadata needs to be sent.
*/
export type GASPNodeResponse = {
requestedInputs: Record<string, { metadata: boolean }>
}
/**
* Facilitates the finding of UTXOs, determination of needed inputs, temporary graph management, and eventual graph finalization.
*/
export interface GASPStorage {
/**
* Returns an array of transaction outpoints that are currently known to be unspent (given an optional timestamp).
* Non-confirmed (non-timestamped) outputs should always be returned, regardless of the timestamp.
* @returns A promise for an array of objects, each containing txid and outputIndex properties.
*/
findKnownUTXOs: (since: number) => Promise<Array<{ txid: string, outputIndex: number }>>
/**
* 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 txid The transaction ID for the node to hydrate.
* @param outputIndex The output index for the node to hydrate.
* @param metadata Whether transaction and output metadata should be returned.
* @returns The hydrated GASP node, with or without metadata.
*/
hydrateGASPNode: (graphID: string, txid: string, outputIndex: number, metadata: boolean) => Promise<GASPNode>
/**
* 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.
*/
findNeededInputs: (tx: GASPNode) => Promise<GASPNodeResponse | void>
/**
* 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.
*/
appendToGraph: (tx: GASPNode, spentBy?: string) => Promise<void>
/**
* 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.
* @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.
*/
validateGraphAnchor: (graphID: string) => Promise<void>
/**
* 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.
*/
discardGraph: (graphID: string) => Promise<void>
/**
* 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 tip of this graph.
*/
finalizeGraph: (graphID: string) => Promise<void>
}
/**
* The communications mechanism between a local GASP instance and a foreign GASP instance.
*/
export interface GASPRemote {
/** Given an outgoing initial request, send the request to the foreign instance and obtain their initial response. */
getInitialResponse: (request: GASPInitialRequest) => Promise<GASPInitialResponse>
/** Given an outgoing initial response, obtain the reply from the foreign instance. */
getInitialReply: (response: GASPInitialResponse) => Promise<GASPInitialReply>
/** Given an outgoing txid, outputIndex and optional metadata, request the associated GASP node from the foreign instane. */
requestNode: (graphID: string, txid: string, outputIndex: number, metadata: boolean) => Promise<GASPNode>
/** Given an outgoing node, send the node to the foreign instance and determine which additional inputs (if any) they request in response. */
submitNode: (node: GASPNode) => Promise<GASPNodeResponse | void>
}
export class GASPVersionMismatchError extends Error {
code: 'ERR_GASP_VERSION_MISMATCH'
currentVersion: number
foreignVersion: number
constructor(message: string, currentVersion: number, foreignVersion: number) {
super(message)
this.code = 'ERR_GASP_VERSION_MISMATCH'
this.currentVersion = currentVersion
this.foreignVersion = foreignVersion
}
}
/**
* Log levels for controlling output verbosity.
*/
export enum LogLevel {
NONE = 0,
ERROR = 1,
WARN = 2,
INFO = 3,
DEBUG = 4
}
/**
* Main class implementing the Graph Aware Sync Protocol.
*/
export class GASP implements GASPRemote {
version: number
storage: GASPStorage
remote: GASPRemote
lastInteraction: number
/**
* @deprecated Retained for backwards compatibility. Use `logLevel` and the new logging methods instead.
*/
log: boolean
/**
* The log level: NONE, ERROR, WARN, INFO, DEBUG.
*/
logLevel: LogLevel
logPrefix: string
unidirectional: boolean
/**
* When true, run tasks sequentially rather than using Promise.all (parallel).
*/
sequential: boolean
/**
*
* @param storage The GASP Storage interface to use
* @param remote The GASP Remote interface to use
* @param lastInteraction The timestamp when we last interacted with this remote party
* @param logPrefix Optional prefix for log messages
* @param log Whether to log messages (backwards-compatibility only)
* @param unidirectional Whether to disable the "reply" side and do pull-only
* @param logLevel The log level for the instance
* @param sequential Whether to run tasks sequentially (avoid Promise.all) or in parallel
*/
constructor(
storage: GASPStorage,
remote: GASPRemote,
lastInteraction = 0,
logPrefix = '[GASP] ',
log = false,
unidirectional = false,
logLevel: LogLevel = LogLevel.INFO,
sequential = false
) {
this.storage = storage
this.remote = remote
this.lastInteraction = lastInteraction
this.version = 1
this.logPrefix = logPrefix
this.log = log
this.unidirectional = unidirectional
this.logLevel = logLevel
this.sequential = sequential
this.validateTimestamp(this.lastInteraction)
this.logData(`GASP initialized with version: ${this.version}, lastInteraction: ${this.lastInteraction}, unidirectional: ${this.unidirectional}, logLevel: ${LogLevel[this.logLevel]}, sequential: ${this.sequential}`)
}
/**
* Helper method to execute callbacks either in parallel or sequentially,
* depending on the `sequential` flag.
*/
private async runConcurrently<T>(
items: T[],
callback: (item: T) => Promise<void>
): Promise<void> {
if (this.sequential) {
// Run sequentially
for (const item of items) {
await callback(item)
}
} else {
// Run in parallel
await Promise.all(items.map(callback))
}
}
/**
* Legacy log method for backwards compatibility only.
* Internally, logs at INFO level if `log === true`.
*/
private logData(...data: any): void {
if (this.log) {
this.infoLog(...data)
}
}
/**
* New recommended methods for logging, respecting the logLevel.
*/
private errorLog(...data: any): void {
if (this.logLevel >= LogLevel.ERROR) {
console.error(this.logPrefix, '[ERROR]', ...data)
}
}
private warnLog(...data: any): void {
if (this.logLevel >= LogLevel.WARN) {
console.warn(this.logPrefix, '[WARN]', ...data)
}
}
private infoLog(...data: any): void {
if (this.logLevel >= LogLevel.INFO) {
console.info(this.logPrefix, '[INFO]', ...data)
}
}
private debugLog(...data: any): void {
if (this.logLevel >= LogLevel.DEBUG) {
console.debug(this.logPrefix, '[DEBUG]', ...data)
}
}
private validateTimestamp(timestamp: number): void {
if (typeof timestamp !== 'number' || isNaN(timestamp) || timestamp < 0 || !Number.isInteger(timestamp)) {
throw new Error('Invalid timestamp format')
}
}
/**
* Computes a 36-byte structure from a transaction ID and output index.
* @param txid The transaction ID.
* @param index The output index.
* @returns A string representing the 36-byte structure.
*/
private compute36ByteStructure(txid: string, index: number): string {
const result = `${txid}.${index.toString()}`
this.debugLog(`Computed 36-byte structure: ${result} from txid: ${txid}, index: ${index}`)
return result
}
/**
* Deconstructs a 36-byte structure into a transaction ID and output index.
* @param outpoint The 36-byte structure.
* @returns An object containing the transaction ID and output index.
*/
private deconstruct36ByteStructure(outpoint: string): { txid: string, outputIndex: number } {
const [txid, index] = outpoint.split('.')
const result = {
txid,
outputIndex: parseInt(index, 10)
}
this.debugLog(`Deconstructed 36-byte structure: ${outpoint} into txid: ${txid}, outputIndex: ${result.outputIndex}`)
return result
}
/**
* Computes the transaction ID for a given transaction.
* @param tx The transaction string.
* @returns The computed transaction ID.
*/
private computeTXID(tx: string): string {
const txid = Transaction.fromHex(tx).id('hex')
this.debugLog(`Computed TXID: ${txid} from transaction: ${tx}`)
return txid
}
/**
* Synchronizes the transaction data between the local and remote participants.
*/
async sync(): Promise<void> {
this.infoLog(`Starting sync process. Last interaction timestamp: ${this.lastInteraction}`)
const initialRequest = await this.buildInitialRequest(this.lastInteraction)
const initialResponse = await this.remote.getInitialResponse(initialRequest)
// 1. Pull the remote UTXOs that we don't already have
if (initialResponse.UTXOList.length > 0) {
const foreignUTXOs = await this.storage.findKnownUTXOs(0)
await this.runConcurrently(
initialResponse.UTXOList.filter(x =>
!foreignUTXOs.some(y => x.txid === y.txid && x.outputIndex === y.outputIndex)
),
async UTXO => {
try {
this.infoLog(`Requesting node for UTXO: ${JSON.stringify(UTXO)}`)
const resolvedNode = await this.remote.requestNode(
this.compute36ByteStructure(UTXO.txid, UTXO.outputIndex),
UTXO.txid,
UTXO.outputIndex,
true
)
this.debugLog(`Received unspent graph node from remote: ${JSON.stringify(resolvedNode)}`)
await this.processIncomingNode(resolvedNode)
await this.completeGraph(resolvedNode.graphID)
} catch (e) {
this.warnLog(`Error with incoming UTXO ${UTXO.txid}.${UTXO.outputIndex}: ${(e as Error).message}`)
}
}
)
}
// 2. Only do the “reply” half if unidirectional is disabled
if (!this.unidirectional) {
const initialReply = await this.getInitialReply(initialResponse)
this.infoLog(`Received initial reply: ${JSON.stringify(initialReply)}`)
if (initialReply.UTXOList.length > 0) {
await this.runConcurrently(initialReply.UTXOList, async UTXO => {
try {
this.infoLog(`Hydrating GASP node for UTXO: ${JSON.stringify(UTXO)}`)
const outgoingNode = await this.storage.hydrateGASPNode(
this.compute36ByteStructure(UTXO.txid, UTXO.outputIndex),
UTXO.txid,
UTXO.outputIndex,
true
)
this.debugLog(`Sending unspent graph node for remote: ${JSON.stringify(outgoingNode)}`)
await this.processOutgoingNode(outgoingNode)
} catch (e) {
this.warnLog(`Error with outgoing UTXO ${UTXO.txid}.${UTXO.outputIndex}: ${(e as Error).message}`)
}
})
}
}
this.infoLog('Sync completed!')
}
/**
* Builds the initial request for the sync process.
* @returns A promise for the initial request object.
*/
async buildInitialRequest(since: number): Promise<GASPInitialRequest> {
const request = {
version: this.version,
since
}
this.debugLog(`Built initial request: ${JSON.stringify(request)}`)
return request
}
/**
* Builds the initial response based on the received request.
* @param request The initial request object.
* @returns A promise for an initial response
*/
async getInitialResponse(request: GASPInitialRequest): Promise<GASPInitialResponse> {
this.infoLog(`Received initial request: ${JSON.stringify(request)}`)
if (request.version !== this.version) {
const error = new GASPVersionMismatchError(
`GASP version mismatch. Current version: ${this.version}, foreign version: ${request.version}`,
this.version,
request.version
)
this.errorLog(`GASP version mismatch error: ${error.message}`)
throw error
}
this.validateTimestamp(request.since)
const response = {
since: this.lastInteraction,
UTXOList: await this.storage.findKnownUTXOs(request.since)
}
this.debugLog(`Built initial response: ${JSON.stringify(response)}`)
return response
}
/**
* Builds the initial reply based on the received response.
* @param response The initial response object.
* @returns A promise for an initial reply
*/
async getInitialReply(response: GASPInitialResponse): Promise<GASPInitialReply> {
this.infoLog(`Received initial response: ${JSON.stringify(response)}`)
const knownUTXOs = await this.storage.findKnownUTXOs(response.since)
const filteredUTXOs = knownUTXOs.filter(
x => !response.UTXOList.some(y => y.txid === x.txid && y.outputIndex === x.outputIndex)
)
const reply = {
UTXOList: filteredUTXOs
}
this.debugLog(`Built initial reply: ${JSON.stringify(reply)}`)
return reply
}
/**
* Provides a requested node to a foreign instance who requested it.
*/
async requestNode(graphID: string, txid: string, outputIndex: number, metadata: boolean): Promise<GASPNode> {
this.infoLog(`Remote is requesting node with graphID: ${graphID}, txid: ${txid}, outputIndex: ${outputIndex}, metadata: ${metadata}`)
const node = await this.storage.hydrateGASPNode(graphID, txid, outputIndex, metadata)
this.debugLog(`Returning node: ${JSON.stringify(node)}`)
return node
}
/**
* Provides a set of inputs we care about after processing a new incoming node.
* Also finalizes or discards a graph if no additional data is requested from the foreign instance.
*/
async submitNode(node: GASPNode): Promise<GASPNodeResponse | void> {
this.infoLog(`Remote party is submitting node: ${JSON.stringify(node)}`)
await this.storage.appendToGraph(node)
const requestedInputs = await this.storage.findNeededInputs(node)
this.debugLog(`Requested inputs: ${JSON.stringify(requestedInputs)}`)
if (!requestedInputs) {
await this.completeGraph(node.graphID)
}
return requestedInputs
}
/**
* Handles the completion of a newly-synced graph
* @param {string} graphID The ID of the newly-synced graph
*/
async completeGraph(graphID: string): Promise<void> {
this.infoLog(`Completing newly-synced graph: ${graphID}`)
try {
await this.storage.validateGraphAnchor(graphID)
this.debugLog(`Graph validated for node: ${graphID}`)
await this.storage.finalizeGraph(graphID)
this.infoLog(`Graph finalized for node: ${graphID}`)
} catch (e) {
this.warnLog(`Error validating graph: ${(e as Error).message}. Discarding graph for node: ${graphID}`)
await this.storage.discardGraph(graphID)
}
}
/**
* Processes an incoming node from the remote participant.
* @param node The incoming GASP node.
* @param spentBy The 36-byte structure of the node that spent this one, if applicable.
*/
private async processIncomingNode(node: GASPNode, spentBy?: string, seenNodes = new Set()): Promise<void> {
const nodeId = `${this.computeTXID(node.rawTx)}.${node.outputIndex}`
this.debugLog(`Processing incoming node: ${JSON.stringify(node)}, spentBy: ${spentBy}`)
if (seenNodes.has(nodeId)) {
this.debugLog(`Node ${nodeId} already processed, skipping.`)
return // Prevent infinite recursion
}
seenNodes.add(nodeId)
await this.storage.appendToGraph(node, spentBy)
const neededInputs = await this.storage.findNeededInputs(node)
this.debugLog(`Needed inputs for node ${nodeId}: ${JSON.stringify(neededInputs)}`)
if (neededInputs) {
await this.runConcurrently(
Object.entries(neededInputs.requestedInputs),
async ([outpoint, { metadata }]) => {
const { txid, outputIndex } = this.deconstruct36ByteStructure(outpoint)
this.infoLog(`Requesting new node for txid: ${txid}, outputIndex: ${outputIndex}, metadata: ${metadata}`)
const newNode = await this.remote.requestNode(node.graphID, txid, outputIndex, metadata)
this.debugLog(`Received new node: ${JSON.stringify(newNode)}`)
await this.processIncomingNode(
newNode,
this.compute36ByteStructure(this.computeTXID(node.rawTx), node.outputIndex),
seenNodes
)
}
)
}
}
/**
* Processes an outgoing node to the remote participant.
* @param node The outgoing GASP node.
*/
private async processOutgoingNode(node: GASPNode, seenNodes = new Set()): Promise<void> {
if (this.unidirectional) {
this.debugLog(`Skipping outgoing node processing in unidirectional mode.`)
return
}
const nodeId = `${this.computeTXID(node.rawTx)}.${node.outputIndex}`
this.debugLog(`Processing outgoing node: ${JSON.stringify(node)}`)
if (seenNodes.has(nodeId)) {
this.debugLog(`Node ${nodeId} already processed, skipping.`)
return // Prevent infinite recursion
}
seenNodes.add(nodeId)
// Attempt to submit the node to the remote
const response = await this.remote.submitNode(node)
this.debugLog(`Received response for submitted node: ${JSON.stringify(response)}`)
if (response) {
await this.runConcurrently(
Object.entries(response.requestedInputs),
async ([outpoint, { metadata }]) => {
const { txid, outputIndex } = this.deconstruct36ByteStructure(outpoint)
try {
this.infoLog(`Hydrating node for txid: ${txid}, outputIndex: ${outputIndex}, metadata: ${metadata}`)
const hydratedNode = await this.storage.hydrateGASPNode(node.graphID, txid, outputIndex, metadata)
this.debugLog(`Hydrated node: ${JSON.stringify(hydratedNode)}`)
await this.processOutgoingNode(hydratedNode, seenNodes)
} catch (e) {
this.errorLog(`Error hydrating node: ${(e as Error).message}`)
// If we can't send the outgoing node, we just stop. The remote won't validate the anchor, and their temporary graph will be discarded.
return
}
}
)
}
}
}
+41
-25
{
"name": "@bsv/gasp",
"version": "0.1.3",
"version": "1.0.0",
"type": "module",
"description": "Graph Aware Sync Protocol",
"main": "dist/bundle.cjs.js",
"module": "dist/bundle.esm.js",
"types": "dist/index.d.ts",
"main": "dist/cjs/mod.js",
"module": "dist/esm/mod.js",
"types": "dist/types/mod.d.ts",
"files": [
"dist",
"src",
"mod.ts",
"LICENSE.txt"
],
"exports": {
".": {
"types": "./dist/types/mod.d.ts",
"import": "./dist/esm/mod.js",
"require": "./dist/cjs/mod.js"
},
"./*.ts": {
"types": "./dist/types/src/*.d.ts",
"import": "./dist/esm/src/*.js",
"require": "./dist/cjs/src/*.js"
}
},
"scripts": {
"build": "rollup -c",
"test": "jest",
"test:coverage": "jest --coverage",
"prepare": "npm run build"
"test": "npm run build && jest",
"test:watch": "npm run build && jest --watch",
"test:coverage": "npm run build && jest --coverage",
"lint": "ts-standard --fix src/**/*.ts",
"build": "tsc -b && tsconfig-to-dual-package tsconfig.cjs.json",
"dev": "tsc -b -w",
"prepublish": "npm run build",
"doc": "ts2md --inputFilename=mod.ts --outputFilename=API.md --filenameSubstring=API --firstHeadingLevel=1"
},
"files": [
"dist"
],
"keywords": [

@@ -24,20 +44,16 @@ "blockchain",

],
"author": "Your Name",
"license": "MIT",
"author": "BSV Blockchain Association",
"license": "SEE LICENSE IN LICENSE.txt",
"devDependencies": {
"@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-node-resolve": "^11.2.1",
"@rollup/plugin-typescript": "^8.2.0",
"@types/jest": "^26.0.24",
"jest": "^26.6.3",
"rollup": "^2.36.1",
"rollup-plugin-terser": "^7.0.2",
"ts-jest": "^26.5.4",
"ts-node": "^9.1.1",
"tslib": "^2.6.3",
"typescript": "^4.9.5"
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"ts-standard": "^12.0.2",
"ts2md": "^0.2.0",
"tsconfig-to-dual-package": "^1.2.0",
"typescript": "^5.2.2"
},
"dependencies": {
"@bsv/sdk": "^1.1.6"
"@bsv/sdk": "1.1.6"
}
}