GASP — Graph Aware Sync Protocol
The Graph Aware Sync Protocol (GASP) is a powerful protocol for synchronizing BSV transaction data between two or more parties. Unlike simplistic “UTXO list” or “transaction pushing” mechanisms, GASP allows each participant to incrementally build a graph of transaction ancestors and descendants. This ensures:
- Legitimacy: Parties only finalize data they can validate, using Merkle proofs, script evaluation, and the other rules of SPV.
- Completeness: Recursively, each party pulls in the inputs needed to prove correctness—avoiding partial or “broken” transaction data.
- Efficiency: Each participant only fetches and transmits data it doesn’t already have, minimizing bandwidth.
- Flexibility: Custom storage, custom remote mechanisms, unidirectional sync, concurrency options, and more.
Table of Contents
Key Features
- Recursive Sync: Fetches only the needed transaction outputs and recursively fetches input data on demand.
- Metadata Support: Optionally exchange metadata (e.g., invoice data, descriptions, basket or topical membership, etc.) for each transaction or output.
- Proof Anchoring: Merkle proofs can be attached to each transaction, ensuring on-chain verifiability.
- Unidirectional: If desired, you can configure “pull only” mode—where you fetch data from a remote but never push your own.
- Selective Concurrency: Use fully parallel fetches (
Promise.all
) or sequential fetches (one at a time) to avoid potential DB locking.
- Flexible Integration: The
GASPStorage
and GASPRemote
interfaces let you integrate with your own storage logic or remote transport.
How it Works
- Initial Request: One peer initiates a request, including a timestamp for when the two parties last synchronized.
- Initial Response: The other peer returns a set of UTXOs that it has observed since that timestamp, plus a “since” timestamp for a potential “reply.”
- Recursive Graph Building:
- Each side requests the transaction data (optionally including metadata) for each unknown UTXO.
- Each newly-received transaction might contain additional unknown inputs, which triggers further fetches.
- Graph Finalization: Once all required inputs are fetched, each peer finalizes the newly-validated transaction data into its own store.
- Optional “Reply”: In a bidirectional scenario, the second peer then does the same, ensuring both end up with a consistent set of data.
If you set GASP to unidirectional, step 5 is skipped: your local node simply pulls data from the remote, but never sends data back.
Installation
npm i @bsv/gasp
Or, if you use yarn
:
yarn add @bsv/gasp
Quick Start
Below is a bare-bones recipe to get GASP up and running.
1. Implement the GASPStorage
Interface
The GASPStorage interface is your local “database layer.” It controls how you store UTXOs, how you retrieve them, and how you handle partial transaction graphs.
import { GASPNode, GASPNodeResponse, GASPStorage } from '@bsv/gasp'
export class MyCustomStorage implements GASPStorage {
async findKnownUTXOs(since: number) { }
async hydrateGASPNode(graphID: string, txid: string, outputIndex: number, metadata: boolean) { }
async findNeededInputs(tx: GASPNode): Promise<GASPNodeResponse | void> { }
async appendToGraph(tx: GASPNode, spentBy?: string) { }
async validateGraphAnchor(graphID: string) { }
async discardGraph(graphID: string) { }
async finalizeGraph(graphID: string) { }
}
2. Implement (or Obtain) a GASPRemote
A GASPRemote is how you communicate with a remote GASP peer. You can implement your own HTTP fetch logic, use a WebSocket-based approach, or even run everything in the same process for testing.
import { GASPRemote, GASPNode, GASPInitialRequest, GASPInitialResponse } from '@bsv/gasp'
export class MyRemote implements GASPRemote {
async getInitialResponse(request: GASPInitialRequest): Promise<GASPInitialResponse> {
}
async getInitialReply(response: GASPInitialResponse) {
}
async requestNode(graphID: string, txid: string, outputIndex: number, metadata: boolean): Promise<GASPNode> {
}
async submitNode(node: GASPNode) {
}
}
3. Initialize and Sync
Finally, create the GASP instance, and call sync()
:
import { GASP, LogLevel } from '@bsv/gasp'
const myStorage = new MyCustomStorage()
const myRemote = new MyRemote()
const gasp = new GASP(
myStorage,
myRemote,
0,
'[GASP] ',
false,
false,
LogLevel.INFO,
false
)
gasp.sync()
.then(() => console.log('GASP sync complete!'))
.catch(err => console.error('GASP sync error:', err))
Examples
Minimal Example
If you just want a quick demonstration of pulling data from a remote, you can see a short example in our tests. This code snippet demonstrates a super-simplified approach:
import { GASP } from '@bsv/gasp'
const aliceStorage = new MyCustomStorage()
const bobRemote = new MyRemote()
const aliceGASP = new GASP(aliceStorage, bobRemote)
await aliceGASP.sync()
Advanced Example: sequential
and Log Levels
Sometimes, performing too many concurrent operations (e.g., writes to a database) can lead to locking issues. Also, you might want to control the verbosity of logs.
import { GASP, LogLevel } from '@bsv/gasp'
const gasp = new GASP(
myStorage,
myRemote,
0,
'[GASP Demo] ',
false,
false,
LogLevel.DEBUG,
true
)
await gasp.sync()
Unidirectional Pull-Only Sync
You may only want to “pull” data from a remote server without uploading your own. This is common in “SPV client” use-cases.
const gaspAlice = new GASP(
aliceStorage,
bobRemote,
0,
'[GASP-Alice] ',
false,
true
)
await gaspAlice.sync()
Dealing With “Deep” Transactions and Metadata
When a new transaction is received, GASP calls your findNeededInputs(...)
method. If you require further data (e.g., to verify a scriptSig or a custom metadata field), simply return a requestedInputs
object indicating which inputs you need. GASP will request them from the remote automatically.
async findNeededInputs(tx: GASPNode): Promise<GASPNodeResponse | void> {
if (tx.outputMetadata?.includes('magic')) {
return {
requestedInputs: {
'some_txid.0': { metadata: true },
'some_txid.1': { metadata: false }
}
}
}
}
Your remote peer’s requestNode(...)
method will deliver these missing pieces, if they exist, ensuring a “deep” transaction graph is built.
Useful Links
- Comprehensive Tests: The test suite covers everything from version mismatches to recursion edge cases.
- Real-World Integrations: See the “OverlayGASPStorage” and “OverlayGASPRemote” classes (in the Overlay Services repo) for a real-world application.
FAQ
-
Does GASP handle conflicting transactions?
GASP is agnostic about conflicts. It’s up to your GASPStorage
implementation to decide how to handle double spends or conflicting states.
-
How do I do only pure “SPV proof” validation?
GASP includes optional Merkle proofs via the proof
field. If your validateGraphAnchor(...)
checks them, you effectively get SPV-level validation.
-
What about specialized metadata or policies?
GASP was built for that. Use txMetadata
, outputMetadata
, or inputs
to store and propagate any custom data. Your code can then gather additional inputs if needed.
-
What if a remote fails to provide data?
GASP’s recursion stops. If you never receive inputs you request, you never finalize that transaction. This ensures consistent partial or full finalization.
License
The license for the code in this repository is the Open BSV License.