GASP — Graph Aware Sync Protocol

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.

Interactive spec

At a glance

FieldValue
FormatAsyncAPI 3.0
Version1.0.0
Statusstable
Implementations@bsv/gasp

What problem this solves

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.

Protocol overview

Four-phase synchronization (if bidirectional):

Phase 1 — Initial Request/Response

  1. Initiator → Responder GASPInitialRequest

    • Protocol version
    • Timestamp (since) — initiator wants UTXOs from responder created after this time
    • Limit (max UTXOs to return)
  2. Responder → Initiator GASPInitialResponse

    • Responder's UTXO list (what initiator is missing)
    • Responder's since timestamp (what it wants from initiator)

Phase 2 — Local UTXO push (bidirectional only)

  1. Initiator → Responder submitNode(GASPNode)
    • Local UTXOs newer than the responder's since timestamp, excluding UTXOs already shared in the initial response
    • The core implementation exposes getInitialReply, but GASP.sync() performs the bidirectional push by submitting hydrated graph nodes

Phase 3 & 4 — Graph Walking

For each UTXO the peer is missing, request the full transaction and ancestry:

  1. Either party → Peer GASPNodeRequest

    • graphID — unique identifier for this sync session
    • txid, outputIndex — UTXO to request
    • metadata — optional custom data
  2. Peer → Requester GASPNode

    • Raw transaction hex
    • Output index
    • Merkle proof (if available)
    • Transaction metadata
    • Input requirements (what inputs does this transaction need?)

If the transaction has inputs the requester doesn't have, it requests those too (recursive graph walk). Continue until all dependencies are satisfied.

  1. Requester → Peer GASPNodeResponse
    • If more inputs are needed: list of input txids to request
    • Or completion signal

Key types / channels

ChannelDirectionMessage TypePurpose
gasp/initialRequestSendGASPInitialRequestInitiator sends UTXO list and since timestamp
gasp/initialResponseReceiveGASPInitialResponseResponder sends UTXO list and its since
gasp/initialReplySendGASPInitialReplyInitiator sends missing UTXOs (bidirectional mode)
gasp/requestNodeBidirectionalGASPNodeRequestRequest a transaction and its inputs
gasp/nodeBidirectionalGASPNodeRespond with transaction data
gasp/nodeResponseBidirectionalGASPNodeResponseConfirm receipt; request more inputs if needed

Example: Sync two overlay nodes

typescript
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')

Conformance vectors

GASP conformance is tested in conformance/vectors/sync/gasp/:

  • UTXO list exchange and deduplication
  • Graph walking (recursive input resolution)
  • Merkle proof validation
  • Phase ordering (initial before graph walk)
  • Bidirectional vs. unidirectional modes
  • Anchor validation (transactions must connect to confirmed blocks)

Implementations in ts-stack

PackageNotes
@bsv/gaspCore GASP protocol implementation; orchestrates graph walking, validates proofs, manages sync state
@bsv/overlayIntegrates GASP for syncing state between overlay nodes

Spec artifact

gasp-asyncapi.yaml