GASP enables two overlay nodes to efficiently synchronize transaction state by walking the transaction graph together. Instead of broadcasting individual transactions, nodes exchange UTXO lists, then request only missing transaction ancestry and descendancy. Merkle proof validation ensures legitimacy without trusting peers.
| Field | Value |
|---|---|
| Format | AsyncAPI 3.0 |
| Version | 1.0.0 |
| Status | stable |
| Implementations | @bsv/gasp |
Efficient state sync between overlay nodes. When two overlay nodes meet, they need to exchange their UTXO sets so they have consistent state. Broadcasting every transaction is slow. GASP exchanges UTXO lists first, then each node requests only the inputs it's missing, walking the transaction graph backward until all dependencies are satisfied.
Completeness verification. Nodes can prove they have a complete, unbroken transaction history from some anchor point (e.g., a confirmed block) back to inputs. If a peer claims to have a UTXO but can't provide its full ancestry, the node rejects it.
Bidirectional and unidirectional modes. Nodes can sync in both directions (both exchange missing UTXOs) or one-way (only one side sends). This supports various topologies: hub-and-spoke (spoke only receives), peer-to-peer (both ways), etc.
Four-phase synchronization (if bidirectional):
Phase 1 — Initial Request/Response
Initiator → Responder GASPInitialRequest
since) — initiator wants UTXOs from responder created after this timeResponder → Initiator GASPInitialResponse
since timestamp (what it wants from initiator)Phase 2 — Local UTXO push (bidirectional only)
submitNode(GASPNode)
since timestamp, excluding UTXOs already shared in the initial responsegetInitialReply, but GASP.sync() performs the bidirectional push by submitting hydrated graph nodesPhase 3 & 4 — Graph Walking
For each UTXO the peer is missing, request the full transaction and ancestry:
Either party → Peer GASPNodeRequest
graphID — unique identifier for this sync sessiontxid, outputIndex — UTXO to requestmetadata — optional custom dataPeer → Requester GASPNode
If the transaction has inputs the requester doesn't have, it requests those too (recursive graph walk). Continue until all dependencies are satisfied.
GASPNodeResponse
| Channel | Direction | Message Type | Purpose |
|---|---|---|---|
gasp/initialRequest | Send | GASPInitialRequest | Initiator sends UTXO list and since timestamp |
gasp/initialResponse | Receive | GASPInitialResponse | Responder sends UTXO list and its since |
gasp/initialReply | Send | GASPInitialReply | Initiator sends missing UTXOs (bidirectional mode) |
gasp/requestNode | Bidirectional | GASPNodeRequest | Request a transaction and its inputs |
gasp/node | Bidirectional | GASPNode | Respond with transaction data |
gasp/nodeResponse | Bidirectional | GASPNodeResponse | Confirm receipt; request more inputs if needed |
import {
GASP,
type GASPInitialRequest,
type GASPInitialResponse,
type GASPNode,
type GASPNodeResponse,
type GASPRemote,
type GASPStorage
} from '@bsv/gasp'
// 1. Implement storage interface
class MyStorage implements GASPStorage {
async findKnownUTXOs(since: number, limit?: number) {
// Return all UTXOs created after `since`
return [
{ txid: 'abc...', outputIndex: 0, score: Date.now() },
{ txid: 'def...', outputIndex: 1, score: Date.now() }
]
}
async hydrateGASPNode(graphID, txid, outputIndex, metadata) {
// Return the transaction and proof
return {
graphID,
rawTx: await getTransactionHex(txid),
outputIndex,
proof: await getMerkleProof(txid),
txMetadata: metadata ? 'transaction metadata' : undefined
}
}
async findNeededInputs(tx: GASPNode): Promise<GASPNodeResponse | void> {
return
}
async appendToGraph(tx: GASPNode, spentBy?: string) {
await appendTemporaryGraphNode(tx, spentBy)
}
async validateGraphAnchor(graphID: string) {
await assertGraphIsAnchored(graphID)
}
async discardGraph(graphID: string) {
await removeTemporaryGraph(graphID)
}
async finalizeGraph(graphID: string) {
await promoteTemporaryGraph(graphID)
}
}
// 2. Implement remote peer interface
class MyRemote implements GASPRemote {
async getInitialResponse(request: GASPInitialRequest) {
// Send request to peer, receive response
const response = await fetch(`https://peer.example.com/gasp/initial`, {
method: 'POST',
body: JSON.stringify(request)
})
return response.json()
}
async getInitialReply(response: GASPInitialResponse) {
const reply = await fetch('https://peer.example.com/gasp/reply', {
method: 'POST',
body: JSON.stringify(response)
})
return reply.json()
}
async requestNode(
graphID: string,
txid: string,
outputIndex: number,
metadata: boolean
): Promise<GASPNode> {
const response = await fetch('https://peer.example.com/gasp/node', {
method: 'POST',
body: JSON.stringify({ graphID, txid, outputIndex, metadata })
})
return response.json()
}
async submitNode(node: GASPNode): Promise<GASPNodeResponse | void> {
const response = await fetch('https://peer.example.com/gasp/submit', {
method: 'POST',
body: JSON.stringify(node)
})
return response.json()
}
}
// 3. Run sync
const storage = new MyStorage()
const remote = new MyRemote()
const gasp = new GASP(storage, remote)
await gasp.sync('https://peer.example.com')
console.log('Sync complete; state is now consistent with peer')GASP conformance is tested in conformance/vectors/sync/gasp/:
| Package | Notes |
|---|---|
| @bsv/gasp | Core GASP protocol implementation; orchestrates graph walking, validates proofs, manages sync state |
| @bsv/overlay | Integrates GASP for syncing state between overlay nodes |